diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css index e48de6a..4a95fc4 100644 --- a/css/fvtt-oath-hammer.css +++ b/css/fvtt-oath-hammer.css @@ -550,6 +550,26 @@ opacity: 0.7; font-size: calc(0.86rem * 0.9); } +.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-stepper { + display: flex; + align-items: center; + gap: 1px; +} +.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.2rem; + height: 1.4rem; + font-size: calc(0.86rem * 0.85); + font-weight: bold; + color: #535128; + cursor: pointer; + line-height: 1; +} +.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-btn:hover { + color: #2a1a0a; +} .oathhammer .character-main .character-stats-band .character-resources .resource-label { min-width: 4.2rem; font-family: "BlueDragon", "Palatino Linotype", serif; @@ -919,7 +939,7 @@ grid-template-columns: 1fr 1fr 1fr; } .oathhammer .npc-main .regiment-vitals-grid.regiment-row2 { - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; border-top: none; margin-top: 4px; padding-top: 4px; @@ -1160,7 +1180,7 @@ } .oathhammer .item-list--oath .item-list-header, .oathhammer .item-list--oath .item-entry { - grid-template-columns: 24px 1fr 7rem 3.5rem 3.5rem; + grid-template-columns: 24px 12rem 1fr 3.5rem 3.5rem; } .oathhammer .item-list--npc-skill .item-list-header, .oathhammer .item-list--npc-skill .item-entry { @@ -2574,9 +2594,39 @@ .oathhammer .party-main .party-treasury .party-currency-cp .currency-label { color: #aa6633; } +.oathhammer .party-main .party-slots { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.3rem; + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.85); +} +.oathhammer .party-main .party-slots .party-slots-label { + font-weight: bold; + color: #535128; + margin-right: 0.3rem; + text-transform: uppercase; + font-size: calc(0.86rem * 0.9); + letter-spacing: 0.04em; +} +.oathhammer .party-main .party-slots .party-slots-current { + font-weight: bold; + min-width: 1.8rem; + text-align: right; +} +.oathhammer .party-main .party-slots .party-slots-sep { + color: #535128; +} +.oathhammer .party-main .party-slots .party-slots-max { + width: 3.5rem; + text-align: center; + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.85); +} .oathhammer .item-list--party-member .item-list-header, .oathhammer .item-list--party-member .item-entry { - grid-template-columns: 1.8rem 24px 1fr 7rem 3rem 5rem 5.5rem; + grid-template-columns: 1.8rem 24px 1fr 7rem 5rem 3rem 5.5rem; } .oathhammer .item-list--party-member .party-member-order { font-family: "BlueDragon", "Palatino Linotype", serif; @@ -2586,6 +2636,15 @@ text-align: center; align-self: center; } +.oathhammer .item-list--party-member .item-detail--center { + text-align: center; +} +.oathhammer .item-list--party-member .item-detail--center a { + color: #2a1a0a; +} +.oathhammer .item-list--party-member .item-detail--center .fa-faded { + opacity: 0.2; +} .oathhammer .item-list--party-loot .item-list-header, .oathhammer .item-list--party-loot .item-entry { grid-template-columns: 24px 1fr 6rem 5.5rem 5rem; @@ -2752,3 +2811,79 @@ color: #987d2e; font-family: "BlueDragon", "Palatino Linotype", serif; } +.oh-free-roll-bar { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + padding: 4px 6px; + border-top: 1px solid rgba(83, 81, 40, 0.4); + background: rgba(83, 81, 40, 0.08); + flex-shrink: 0; + flex-wrap: wrap; + z-index: 1; + position: relative; +} +.oh-free-roll-bar .oh-frb-label { + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.9); + font-weight: bold; + color: #2a1a0a; + white-space: nowrap; + display: flex; + align-items: center; + gap: 4px; +} +.oh-free-roll-bar .oh-frb-controls { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + flex-wrap: wrap; + flex: 1; +} +.oh-free-roll-bar .oh-frb-controls select { + font-size: calc(0.86rem * 0.9); + padding: 1px 2px; + height: 1.6rem; + border: 1px solid #535128; + border-radius: 3px; + background: #fff; + cursor: pointer; +} +.oh-free-roll-bar .oh-frb-controls .oh-frb-pool { + width: 3.8rem; +} +.oh-free-roll-bar .oh-frb-controls .oh-frb-color { + flex: 1; + min-width: 6rem; +} +.oh-free-roll-bar .oh-frb-controls .oh-frb-explode-label { + display: flex; + align-items: center; + gap: 3px; + font-size: calc(0.86rem * 0.9); + cursor: pointer; + white-space: nowrap; + color: #2a1a0a; +} +.oh-free-roll-bar .oh-frb-controls .oh-frb-explode-label input[type="checkbox"] { + cursor: pointer; +} +.oh-free-roll-bar .oh-frb-controls .oh-frb-roll-btn { + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.9); + font-weight: bold; + padding: 2px 8px; + height: 1.6rem; + border: 1px solid #535128; + border-radius: 3px; + background: rgba(83, 81, 40, 0.2); + color: #2a1a0a; + cursor: pointer; + white-space: nowrap; +} +.oh-free-roll-bar .oh-frb-controls .oh-frb-roll-btn:hover { + background: #c8a84b; + border-color: #ac8d34; +} diff --git a/lang/en.json b/lang/en.json index 2966300..49af425 100644 --- a/lang/en.json +++ b/lang/en.json @@ -190,7 +190,8 @@ "ClassTrait": "Class Trait", "LineageTrait": "Lineage Trait", "NpcTrait": "NPC Trait", - "CreatureTrait": "Creature Trait" + "CreatureTrait": "Creature Trait", + "RegimentTrait": "Regiment Trait" }, "Condition": { "Blinded": "Blinded", @@ -355,7 +356,7 @@ "DropLeaderHint": "Drop a linked actor here", "MarchingOrder": "Marching Order", "NoMembers": "No members yet — drag characters here.", - "DropMemberHint": "Drag a character actor here to add them to the party.", + "DropMemberHint": "Drag a character or NPC actor here to add them to the party.", "Loot": "Loot", "NoLoot": "No loot yet — drag items here.", "DropLootHint": "Drag weapons, armor or equipment here to add party loot.", @@ -368,7 +369,11 @@ "Location": "Location", "Regiments": "Regiments", "DropRegimentHint": "Drag a regiment actor (must be token-linked) to add it to this army.", - "TotalSupply": "Total Supply" + "TotalSupply": "Total Supply", + "Mercenary": "Mercenary", + "CurrentXP": "XP", + "CarriesLight": "Carries Light", + "Slots": "Slots" }, "ColorDice": { "White": "White (4+)", diff --git a/less/actor-sheet.less b/less/actor-sheet.less index 5b19130..5765a0b 100644 --- a/less/actor-sheet.less +++ b/less/actor-sheet.less @@ -168,6 +168,27 @@ } .res-sep { opacity: 0.7; font-size: @font-size-xs; } + + &.character-resource--luck { + .luck-stepper { + display: flex; + align-items: center; + gap: 1px; + } + .luck-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.2rem; + height: 1.4rem; + font-size: @font-size-sm; + font-weight: bold; + color: @color-olive; + cursor: pointer; + line-height: 1; + &:hover { color: @color-dark; } + } + } } .resource-label { diff --git a/less/free-roll.less b/less/free-roll.less new file mode 100644 index 0000000..389eb6b --- /dev/null +++ b/less/free-roll.less @@ -0,0 +1,77 @@ +// ============================================================ +// FREE ROLL BAR — injected below the chat log +// ============================================================ + +.oh-free-roll-bar { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + padding: 4px 6px; + border-top: 1px solid fade(@color-olive, 40%); + background: fade(@color-olive, 8%); + flex-shrink: 0; + flex-wrap: wrap; + z-index: 1; + position: relative; + + .oh-frb-label { + font-family: @font-secondary; + font-size: @font-size-xs; + font-weight: bold; + color: @color-dark; + white-space: nowrap; + display: flex; + align-items: center; + gap: 4px; + } + + .oh-frb-controls { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + flex-wrap: wrap; + flex: 1; + + select { + font-size: @font-size-xs; + padding: 1px 2px; + height: 1.6rem; + border: 1px solid @color-olive; + border-radius: 3px; + background: #fff; + cursor: pointer; + } + + .oh-frb-pool { width: 3.8rem; } + .oh-frb-color { flex: 1; min-width: 6rem; } + + .oh-frb-explode-label { + display: flex; + align-items: center; + gap: 3px; + font-size: @font-size-xs; + cursor: pointer; + white-space: nowrap; + color: @color-dark; + + input[type="checkbox"] { cursor: pointer; } + } + + .oh-frb-roll-btn { + font-family: @font-secondary; + font-size: @font-size-xs; + font-weight: bold; + padding: 2px 8px; + height: 1.6rem; + border: 1px solid @color-olive; + border-radius: 3px; + background: @color-olive-faint; + color: @color-dark; + cursor: pointer; + white-space: nowrap; + &:hover { background: @color-gold; border-color: darken(@color-gold, 10%); } + } + } +} diff --git a/less/fvtt-oath-hammer.less b/less/fvtt-oath-hammer.less index 8aa4ffd..38e483c 100644 --- a/less/fvtt-oath-hammer.less +++ b/less/fvtt-oath-hammer.less @@ -14,3 +14,4 @@ @import "settlement-sheet"; @import "party-sheet"; @import "army-sheet"; +@import "free-roll"; diff --git a/less/item-list.less b/less/item-list.less index 74ea3fe..d71a634 100644 --- a/less/item-list.less +++ b/less/item-list.less @@ -191,7 +191,7 @@ .item-list--oath { .item-list-header, .item-entry { - grid-template-columns: @item-img-size 1fr 7rem 3.5rem 3.5rem; + grid-template-columns: @item-img-size 12rem 1fr 3.5rem 3.5rem; } } diff --git a/less/npc-sheet.less b/less/npc-sheet.less index c14618a..33b59c5 100644 --- a/less/npc-sheet.less +++ b/less/npc-sheet.less @@ -216,7 +216,7 @@ .oathhammer .npc-main .regiment-vitals-grid { &.regiment-row1 { grid-template-columns: 1fr 1fr 1fr; } &.regiment-row2 { - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; border-top: none; margin-top: 4px; padding-top: 4px; diff --git a/less/party-sheet.less b/less/party-sheet.less index 87599bd..e6b98f2 100644 --- a/less/party-sheet.less +++ b/less/party-sheet.less @@ -132,13 +132,43 @@ .party-currency-sp .currency-label { color: #888; } .party-currency-cp .currency-label { color: #aa6633; } } + + .party-slots { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.3rem; + font-family: @font-secondary; + font-size: @font-size-sm; + + .party-slots-label { + font-weight: bold; + color: @color-olive; + margin-right: 0.3rem; + text-transform: uppercase; + font-size: @font-size-xs; + letter-spacing: 0.04em; + } + .party-slots-current { + font-weight: bold; + min-width: 1.8rem; + text-align: right; + } + .party-slots-sep { color: @color-olive; } + .party-slots-max { + width: 3.5rem; + text-align: center; + font-family: @font-secondary; + font-size: @font-size-sm; + } + } } // ── Member list ──────────────────────────────────────────────── .oathhammer .item-list--party-member { .item-list-header, .item-entry { - // order# | img | name | class | level | grit | actions - grid-template-columns: 1.8rem @item-img-size 1fr 7rem 3rem 5rem 5.5rem; + // order# | img | name | lineage | grit | light | actions + grid-template-columns: 1.8rem @item-img-size 1fr 7rem 5rem 3rem 5.5rem; } .party-member-order { @@ -149,6 +179,12 @@ text-align: center; align-self: center; } + + .item-detail--center { + text-align: center; + a { color: @color-dark; } + .fa-faded { opacity: 0.2; } + } } // ── Loot list ────────────────────────────────────────────────── diff --git a/module/applications/free-roll.mjs b/module/applications/free-roll.mjs index cd55794..b18913e 100644 --- a/module/applications/free-roll.mjs +++ b/module/applications/free-roll.mjs @@ -56,10 +56,11 @@ export function injectFreeRollBar(_chatLog, html) { rollFree(pool, color, explode5) }) - // Insert between .chat-scroll and .chat-form + // Insert before the chat form — use chatForm.parentElement for AppV2 compatibility + // (in v13 parts are nested inside the app element, not direct children) const chatForm = html.querySelector(".chat-form") if (chatForm) { - html.insertBefore(bar, chatForm) + chatForm.parentElement.insertBefore(bar, chatForm) } else { html.appendChild(bar) } diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index c80f7af..1ad6327 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -37,7 +37,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { rollInitiative: OathHammerCharacterSheet.#onRollInitiative, adjustQty: OathHammerCharacterSheet.#onAdjustQty, adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency, - adjustStress: OathHammerCharacterSheet.#onAdjustStress, + adjustLuck: OathHammerCharacterSheet.#onAdjustLuck, clearStress: OathHammerCharacterSheet.#onClearStress, }, } @@ -410,6 +410,13 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { await this.document.update({ [field]: Math.max(0, current + delta) }) } + static async #onAdjustLuck(event, target) { + const delta = parseInt(target.dataset.delta, 10) + const current = this.document.system.luck.value ?? 0 + // No upper cap — luck can exceed max (e.g. from blessings/bonuses) + await this.document.update({ "system.luck.value": Math.max(0, current + delta) }) + } + static async #onAdjustStress(event, target) { const delta = parseInt(target.dataset.delta, 10) const current = this.document.system.arcaneStress.value ?? 0 diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs index 42113b7..a991b2e 100644 --- a/module/applications/sheets/npc-sheet.mjs +++ b/module/applications/sheets/npc-sheet.mjs @@ -172,6 +172,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { colorType: attack.system.colorDiceType, threshold: attack.system.threshold, bonusOptions, + showExplodeOn5: true, colorChoices: Object.fromEntries( Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) ), @@ -194,6 +195,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { await rollNPCAttackDamage(this.document, attack, { bonus: parseInt(getValue("bonus")) || 0, + explodeOn5: getValue("explodeOn5") === "true", visibility: getValue("visibility"), }) } @@ -228,6 +230,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { itemName: spell.name, itemImg: spell.img, dv: spell.system.difficultyValue, poolOptions, bonusOptions, colorChoices, showColor: true, + showExplodeOn5: true, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode"), } @@ -249,6 +252,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { dicePool: parseInt(getValue("dicePool")) || 3, bonus: parseInt(getValue("bonus")) || 0, colorOverride: getValue("colorOverride") || null, + explodeOn5: getValue("explodeOn5") === "true", visibility: getValue("visibility"), }) } @@ -272,6 +276,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { itemName: miracle.name, itemImg: miracle.img, dv: null, showColor: false, poolOptions, bonusOptions, + showExplodeOn5: true, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode"), } @@ -292,6 +297,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { await rollNPCMiracle(this.document, miracle, { dicePool: parseInt(getValue("dicePool")) || 3, bonus: parseInt(getValue("bonus")) || 0, + explodeOn5: getValue("explodeOn5") === "true", visibility: getValue("visibility"), }) } @@ -323,6 +329,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { threshold, bonusOptions, colorChoices, + showExplodeOn5: true, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode"), } @@ -343,6 +350,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { await rollNPCArmor(actor, { bonus: parseInt(getValue("bonus")) || 0, colorOverride: getValue("colorOverride") || null, + explodeOn5: getValue("explodeOn5") === "true", visibility: getValue("visibility"), }) } @@ -381,6 +389,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { threshold: item.system.threshold, bonusOptions, colorChoices, + showExplodeOn5: true, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode"), } @@ -401,6 +410,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { await rollNPCSkill(this.document, item, { bonus: parseInt(getValue("bonus")) || 0, colorOverride: getValue("colorOverride") || null, + explodeOn5: getValue("explodeOn5") === "true", visibility: getValue("visibility"), }) } diff --git a/module/applications/sheets/party-sheet.mjs b/module/applications/sheets/party-sheet.mjs index 32fea02..53fb6e9 100644 --- a/module/applications/sheets/party-sheet.mjs +++ b/module/applications/sheets/party-sheet.mjs @@ -10,9 +10,10 @@ export default class OathHammerPartySheet extends OathHammerActorSheet { window: { contentClasses: ["party-content"] }, actions: { openMember: OathHammerPartySheet.#onOpenMember, - removeMember: OathHammerPartySheet.#onRemoveMember, - moveMemberUp: OathHammerPartySheet.#onMoveMemberUp, - moveMemberDown: OathHammerPartySheet.#onMoveMemberDown, + removeMember: OathHammerPartySheet.#onRemoveMember, + moveMemberUp: OathHammerPartySheet.#onMoveMemberUp, + moveMemberDown: OathHammerPartySheet.#onMoveMemberDown, + toggleCarriesLight: OathHammerPartySheet.#onToggleCarriesLight, adjustCurrency: OathHammerPartySheet.#onAdjustCurrency, adjustQty: OathHammerPartySheet.#onAdjustQty, }, @@ -53,28 +54,41 @@ export default class OathHammerPartySheet extends OathHammerActorSheet { async _preparePartContext(partId, context) { const doc = this.document switch (partId) { - case "main": + case "main": { + const lootItems = doc.items.contents.filter(i => ALLOWED_LOOT_TYPES.has(i.type)) + context.currentSlots = lootItems.reduce((sum, i) => { + const slots = i.system.slots ?? 0 + const qty = i.system.quantity ?? 1 + return sum + slots * qty + }, 0) break + } case "members": { context.tab = context.tabs.members const refs = doc.system.memberRefs ?? [] - context.members = refs.map((id, idx) => { - const actor = game.actors?.get(id) + context.members = refs.map((ref, idx) => { + const actor = game.actors?.get(ref.id) if (!actor) return null const sys = actor.system - const classItem = actor.items?.find(i => i.type === "class") + const isNpc = actor.type === "npc" + const classItem = !isNpc ? actor.items?.find(i => i.type === "class") : null return { - id: actor.id, - name: actor.name, - img: actor.img, + id: actor.id, + name: actor.name, + img: actor.img, + type: actor.type, idx, - position: idx + 1, - isFirst: idx === 0, - isLast: idx === refs.length - 1, - classLabel: classItem?.name ?? "—", - level: sys.level ?? "—", - grit: sys.grit ? `${sys.grit.value}/${sys.grit.max}` : "—", + position: idx + 1, + isFirst: idx === 0, + isLast: idx === refs.length - 1, + carriesLight: ref.carriesLight ?? false, + classLabel: isNpc + ? game.i18n.localize(`OATHHAMMER.NpcSubtype.${sys.subtype === "creature" ? "Creature" : "Npc"}`) + : (classItem?.name ?? "—"), + lineage: !isNpc ? (sys.lineage?.name || "—") : "—", + current: !isNpc ? (sys.experience?.current ?? "—") : "—", + grit: sys.grit ? `${sys.grit.value}/${sys.grit.max}` : "—", } }).filter(Boolean) break @@ -109,10 +123,10 @@ export default class OathHammerPartySheet extends OathHammerActorSheet { if (data.type === "Actor") { const actor = await fromUuid(data.uuid) - if (!actor || actor.type !== "character") return + if (!actor || !["character", "npc"].includes(actor.type)) return const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? []) - if (refs.includes(actor.id)) return - refs.push(actor.id) + if (refs.some(r => r.id === actor.id)) return + refs.push({ id: actor.id, carriesLight: false }) return this.document.update({ "system.memberRefs": refs }) } @@ -132,7 +146,7 @@ export default class OathHammerPartySheet extends OathHammerActorSheet { static async #onRemoveMember(event, target) { const id = target.dataset.actorId - const refs = (this.document.system.memberRefs ?? []).filter(r => r !== id) + const refs = (this.document.system.memberRefs ?? []).filter(r => r.id !== id) await this.document.update({ "system.memberRefs": refs }) } @@ -152,6 +166,14 @@ export default class OathHammerPartySheet extends OathHammerActorSheet { await this.document.update({ "system.memberRefs": refs }) } + static async #onToggleCarriesLight(event, target) { + const idx = parseInt(target.dataset.idx, 10) + const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? []) + if (!refs[idx]) return + refs[idx].carriesLight = !refs[idx].carriesLight + await this.document.update({ "system.memberRefs": refs }) + } + static async #onAdjustCurrency(event, target) { const field = target.dataset.field const delta = parseInt(target.dataset.delta, 10) diff --git a/module/applications/sheets/regiment-sheet.mjs b/module/applications/sheets/regiment-sheet.mjs index d0a0d0b..b10a4fd 100644 --- a/module/applications/sheets/regiment-sheet.mjs +++ b/module/applications/sheets/regiment-sheet.mjs @@ -56,6 +56,9 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { context.colorChoices = Object.fromEntries( Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)]) ) + context.traitTypeLabels = Object.fromEntries( + Object.entries(SYSTEM.TRAIT_TYPE_CHOICES).map(([k, v]) => [k, v]) + ) // Resolve leader actor const leaderUuid = this.document.system.leaderUuid if (leaderUuid) { @@ -75,7 +78,7 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { break case "skills": context.tab = context.tabs.skills - context.skills = doc.itemTypes.skillnpc ?? [] + context.skills = (doc.itemTypes.skillnpc ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)) break case "combat": context.tab = context.tabs.combat @@ -141,6 +144,9 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { const doc = this.document const armorDice = doc.system.armorDice if (!armorDice?.value) return ui.notifications.info("No armor dice to roll.") + const colorType = armorDice.colorDiceType || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" const bonusOptions = Array.from({ length: 13 }, (_, i) => { const v = i - 6 return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } @@ -149,9 +155,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { "systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs", { skillName: game.i18n.localize("OATHHAMMER.Label.ArmorDice"), - skillImg: doc.img, basePool: armorDice.value, bonusOptions, + skillImg: doc.img, dicePool: armorDice.value, + colorType, colorEmoji, threshold, bonusOptions, colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" }, - selectedColor: armorDice.colorDiceType, + showExplodeOn5: true, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode") } @@ -165,15 +172,19 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { const form = new DOMParser().parseFromString(result, "text/html") const getValue = n => form.querySelector(`[name="${n}"]`)?.value await rollNPCArmor(doc, { - bonus: parseInt(getValue("bonus")) || 0, + bonus: parseInt(getValue("bonus")) || 0, colorOverride: getValue("colorOverride") || null, - visibility: getValue("visibility"), + explodeOn5: getValue("explodeOn5") === "true", + visibility: getValue("visibility"), }) } static async #onRollSkillNPC(event, target) { const skill = this.document.items.get(target.dataset.itemId) if (!skill) return + const colorType = skill.system.colorDiceType || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" const bonusOptions = Array.from({ length: 13 }, (_, i) => { const v = i - 6 return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } @@ -181,9 +192,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs", { - skillName: skill.name, skillImg: skill.img, basePool: skill.system.dicePool, bonusOptions, + skillName: skill.name, skillImg: skill.img, dicePool: skill.system.dicePool, + colorType, colorEmoji, threshold, bonusOptions, colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" }, - selectedColor: skill.system.colorDiceType, + showExplodeOn5: true, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode") } @@ -197,9 +209,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { const form = new DOMParser().parseFromString(result, "text/html") const getValue = n => form.querySelector(`[name="${n}"]`)?.value await rollNPCSkill(this.document, skill, { - bonus: parseInt(getValue("bonus")) || 0, + bonus: parseInt(getValue("bonus")) || 0, colorOverride: getValue("colorOverride") || null, - visibility: getValue("visibility"), + explodeOn5: getValue("explodeOn5") === "true", + visibility: getValue("visibility"), }) } @@ -212,6 +225,9 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { static async #onRollNpcAttack(event, target) { const attack = this.document.items.get(target.dataset.itemId) if (!attack) return + const colorType = attack.system.colorDiceType || "white" + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" const bonusOptions = Array.from({ length: 13 }, (_, i) => { const v = i - 6 return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } @@ -219,9 +235,11 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs", { - skillName: attack.name, skillImg: attack.img, basePool: attack.system.damageDice, bonusOptions, + skillName: attack.name, skillImg: attack.img, + dicePool: attack.system.damageDice, + colorType, colorEmoji, threshold, bonusOptions, + showExplodeOn5: true, colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" }, - selectedColor: attack.system.colorDiceType, rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes), visibility: game.settings.get("core", "rollMode") } @@ -235,9 +253,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { const form = new DOMParser().parseFromString(result, "text/html") const getValue = n => form.querySelector(`[name="${n}"]`)?.value await rollNPCAttackDamage(this.document, attack, { - bonus: parseInt(getValue("bonus")) || 0, + bonus: parseInt(getValue("bonus")) || 0, colorOverride: getValue("colorOverride") || null, - visibility: getValue("visibility"), + explodeOn5: getValue("explodeOn5") === "true", + visibility: getValue("visibility"), }) } diff --git a/module/config/system.mjs b/module/config/system.mjs index a59e404..be4c51c 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -215,7 +215,8 @@ export const TRAIT_TYPE_CHOICES = { "class-trait": "OATHHAMMER.TraitType.ClassTrait", "lineage-trait": "OATHHAMMER.TraitType.LineageTrait", "npc-trait": "OATHHAMMER.TraitType.NpcTrait", - "creature-trait": "OATHHAMMER.TraitType.CreatureTrait" + "creature-trait": "OATHHAMMER.TraitType.CreatureTrait", + "regiment-trait": "OATHHAMMER.TraitType.RegimentTrait" } export const NPC_SUBTYPES = { diff --git a/module/models/party.mjs b/module/models/party.mjs index 0a686fd..8a99759 100644 --- a/module/models/party.mjs +++ b/module/models/party.mjs @@ -6,9 +6,12 @@ export default class OathHammerParty extends foundry.abstract.TypeDataModel { schema.notes = new fields.HTMLField({ required: false, nullable: true, initial: "" }) - // Ordered list of character actor IDs — position = marching order + // Ordered list of member entries — position = marching order schema.memberRefs = new fields.ArrayField( - new fields.StringField({ required: true, nullable: false, blank: false }) + new fields.SchemaField({ + id: new fields.StringField({ required: true, nullable: false, blank: false }), + carriesLight: new fields.BooleanField({ initial: false }), + }) ) schema.treasury = new fields.SchemaField({ @@ -17,8 +20,19 @@ export default class OathHammerParty extends foundry.abstract.TypeDataModel { cp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), }) + schema.maxSlots = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) + return schema } + static migrateData(source) { + if (Array.isArray(source.memberRefs)) { + source.memberRefs = source.memberRefs.map(r => + typeof r === "string" ? { id: r, carriesLight: false } : r + ) + } + return super.migrateData(source) + } + static LOCALIZATION_PREFIXES = ["OATHHAMMER.Party"] } diff --git a/module/models/regiment.mjs b/module/models/regiment.mjs index af707f5..c93d11a 100644 --- a/module/models/regiment.mjs +++ b/module/models/regiment.mjs @@ -22,6 +22,7 @@ export default class OathHammerRegiment extends foundry.abstract.TypeDataModel { schema.movement = new fields.NumberField({ ...requiredInteger, initial: 60, min: 0, max: 500 }) schema.supplyCost = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) schema.recruitmentCost = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) + schema.mercenary = new fields.BooleanField({ required: true, initial: false }) schema.leaderUuid = new fields.StringField({ required: false, nullable: true, initial: null }) diff --git a/module/rolls.mjs b/module/rolls.mjs index dfe8ead..f6b2195 100644 --- a/module/rolls.mjs +++ b/module/rolls.mjs @@ -915,7 +915,15 @@ export async function rollInitiativeCheck(actor, options = {}) { }) } - // NPC: Fate rank + initiativeBonus + // NPC: find Leadership skillnpc item, fall back to Fate rank + initiativeBonus + const leadershipSkill = actor.items.find( + i => i.type === "skillnpc" && i.name.toLowerCase() === "leadership" + ) + if (leadershipSkill) { + return rollNPCSkill(actor, leadershipSkill, { bonus, visibility }) + } + + // Fallback: Fate rank + initiativeBonus const sys = actor.system const fateRank = sys.attributes?.fate?.rank ?? 1 const initBonus = sys.initiativeBonus ?? 0 @@ -967,7 +975,7 @@ export async function rollInitiativeCheck(actor, options = {}) { * @param {object} options */ export async function rollNPCSkill(actor, skillItem, options = {}) { - const { bonus = 0, colorOverride, visibility } = options + const { bonus = 0, colorOverride, visibility, explodeOn5 = false } = options const sys = skillItem.system const colorType = colorOverride || sys.colorDiceType @@ -976,12 +984,14 @@ export async function rollNPCSkill(actor, skillItem, options = {}) { const totalDice = Math.max(sys.dicePool + bonus, 1) - const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false) + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5) const diceHtml = _diceHtml(diceResults, threshold) - const modLine = bonus !== 0 - ? `