/** * Defense roll dialog. * * Pool = governing attribute (Agility default; Might option for melee) + Defense skill * + armorPenalty (auto from equipped armor, always ≤ 0) * + parryBonus / blockBonus (from equipped weapon traits) * + manual bonus * * Parry trait on equipped weapon → red dice (3+) vs melee; +1 if two Parry weapons * Block trait on equipped weapon → red dice (3+) vs ranged; +1 to ranged defense */ export default class OathHammerDefenseDialog { static async prompt(actor) { const actorSys = actor.system // ── Attributes & skill ────────────────────────────────────────────── const agiRank = actorSys.attributes.agility.rank const mightRank = actorSys.attributes.might.rank const defRank = actorSys.skills.defense.rank // ── Equipped weapons ──────────────────────────────────────────────── const equipped = actor.items.filter(i => i.type === "weapon" && i.system.equipped) const parryCount = equipped.filter(w => w.system.traits?.has?.("parry") || [...(w.system.traits ?? [])].includes("parry")).length const blockCount = equipped.filter(w => w.system.traits?.has?.("block") || [...(w.system.traits ?? [])].includes("block")).length // ── Equipped armor penalty (sum) ──────────────────────────────────── const armorPenalty = actor.items .filter(i => i.type === "armor" && i.system.equipped) .reduce((sum, a) => sum + (a.system.penalty ?? 0), 0) // ── Build option lists ─────────────────────────────────────────────── const attackTypeOptions = [ { value: "melee", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseMelee"), selected: true }, { value: "ranged", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseRanged"), selected: false }, ] const attrOptions = [ { value: "agility", label: `${game.i18n.localize("OATHHAMMER.Attribute.Agility")} (${agiRank})`, selected: true }, { value: "might", label: `${game.i18n.localize("OATHHAMMER.Attribute.Might")} (${mightRank})`, selected: false }, ] const bonusOptions = Array.from({ length: 13 }, (_, i) => { const v = i - 6 return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } }) const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const context = { actorName: actor.name, agiRank, mightRank, defRank, parryCount, blockCount, armorPenalty, attackTypeOptions, attrOptions, bonusOptions, rollModes, visibility: game.settings.get("core", "rollMode"), } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-oath-hammer/templates/defense-roll-dialog.hbs", context ) const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }) }, classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, rejectClose: false, buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.RollDefense"), callback: (_ev, btn) => Object.fromEntries( [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value]) ), }], }) if (!result) return null const attackType = result.attackType ?? "melee" const attrChoice = result.attribute ?? "agility" const attrRank = attrChoice === "might" ? mightRank : agiRank const bonus = parseInt(result.bonus) || 0 // Determine red dice and trait bonus from equipped weapons let redDice = false let traitBonus = 0 if (attackType === "melee" && parryCount > 0) { redDice = true if (parryCount >= 2) traitBonus = 1 } else if (attackType === "ranged" && blockCount > 0) { redDice = true traitBonus = 1 } return { attackType, attrRank, attrChoice, redDice, traitBonus, armorPenalty, bonus, visibility: result.visibility ?? game.settings.get("core", "rollMode"), } } }