113 lines
4.4 KiB
JavaScript
113 lines
4.4 KiB
JavaScript
/**
|
|
* 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"],
|
|
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"),
|
|
}
|
|
}
|
|
}
|