Various fixes and changes based on tester feedback
This commit is contained in:
@@ -129,6 +129,143 @@ export default class OathHammerWeaponDialog {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// DEFENSE DIALOG
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
/**
|
||||
* Show the weapon defense dialog and return resolved options.
|
||||
*
|
||||
* Defense pool = Agility (or Might) + Defense skill + trait bonuses + armor penalty + diminish penalty + bonus
|
||||
*
|
||||
* Parry trait → red dice vs melee; +1 if two Parry weapons equipped
|
||||
* Block trait → red dice vs ranged; +1 bonus always
|
||||
* Diminishing defense: -2 per additional defense after the first in a turn
|
||||
*/
|
||||
static async promptDefense(actor, weapon) {
|
||||
const sys = weapon.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const agiRank = actorSys.attributes.agility.rank
|
||||
const mightRank = actorSys.attributes.might.rank
|
||||
const defRank = actorSys.skills.defense.rank
|
||||
|
||||
// Detect this weapon's defense-relevant traits
|
||||
const hasParry = sys.traits.has("parry")
|
||||
const hasBlock = sys.traits.has("block")
|
||||
|
||||
// Count all equipped parry/block weapons (for +1 with two Parry 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
|
||||
|
||||
// Armor penalty from all equipped armors
|
||||
const armorPenalty = actor.items
|
||||
.filter(i => i.type === "armor" && i.system.equipped)
|
||||
.reduce((sum, a) => sum + (a.system.penalty ?? 0), 0)
|
||||
|
||||
// Pre-select attack type: block weapons default to ranged, parry to melee
|
||||
const defaultAttackType = hasBlock && !hasParry ? "ranged" : "melee"
|
||||
|
||||
const traitLabels = [...sys.traits].map(t => {
|
||||
const key = SYSTEM.WEAPON_TRAITS[t]
|
||||
return key ? game.i18n.localize(key) : t
|
||||
})
|
||||
|
||||
const attackTypeOptions = [
|
||||
{ value: "melee", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseMelee"), selected: defaultAttackType === "melee" },
|
||||
{ value: "ranged", label: game.i18n.localize("OATHHAMMER.Dialog.DefenseRanged"), selected: defaultAttackType === "ranged" },
|
||||
]
|
||||
|
||||
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 diminishOptions = [
|
||||
{ value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishFirst"), selected: true },
|
||||
{ value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishSecond"), selected: false },
|
||||
{ value: -4, label: game.i18n.localize("OATHHAMMER.Dialog.DiminishThird"), 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,
|
||||
weaponName: weapon.name,
|
||||
weaponImg: weapon.img,
|
||||
agiRank,
|
||||
mightRank,
|
||||
defRank,
|
||||
hasParry,
|
||||
hasBlock,
|
||||
parryCount,
|
||||
blockCount,
|
||||
armorPenalty,
|
||||
traits: traitLabels,
|
||||
attackTypeOptions,
|
||||
attrOptions,
|
||||
diminishOptions,
|
||||
bonusOptions,
|
||||
rollModes,
|
||||
visibility: game.settings.get("core", "rollMode"),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-oath-hammer/templates/weapon-defense-dialog.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.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 ?? defaultAttackType
|
||||
const attrChoice = result.attribute ?? "agility"
|
||||
const attrRank = attrChoice === "might" ? mightRank : agiRank
|
||||
const diminishPenalty = parseInt(result.diminish) || 0
|
||||
const bonus = parseInt(result.bonus) || 0
|
||||
|
||||
// Resolve red dice and trait bonus based on selected attack type
|
||||
let redDice = false
|
||||
let traitBonus = 0
|
||||
if (attackType === "melee" && hasParry) {
|
||||
redDice = true
|
||||
traitBonus = parryCount >= 2 ? 1 : 0
|
||||
} else if (attackType === "ranged" && hasBlock) {
|
||||
redDice = true
|
||||
traitBonus = 1
|
||||
}
|
||||
|
||||
return {
|
||||
attackType,
|
||||
attrRank,
|
||||
attrChoice,
|
||||
redDice,
|
||||
traitBonus,
|
||||
armorPenalty,
|
||||
diminishPenalty,
|
||||
bonus,
|
||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// DAMAGE DIALOG
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
Reference in New Issue
Block a user