import { SYSTEM } from "../config/system.mjs" /** * Roll dialogs for weapon attacks and damage. * * Attack flow: * 1. promptAttack(actor, weapon) → options * 2. rollWeaponAttack posts a chat card with a "Roll Damage" button * 3. Clicking the button calls promptDamage with attackSuccesses pre-filled * 4. rollWeaponDamage posts the damage chat card */ export default class OathHammerWeaponDialog { // ------------------------------------------------------------------ // // ATTACK DIALOG // ------------------------------------------------------------------ // static async promptAttack(actor, weapon) { const sys = weapon.system const actorSys = actor.system const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0) const skillKey = isRanged ? "shooting" : "fighting" const skillDef = SYSTEM.SKILLS[skillKey] const defaultAttr = skillDef.attribute const attrRank = actorSys.attributes[defaultAttr].rank const skillRank = actorSys.skills[skillKey].rank const skillColor = actorSys.skills[skillKey].colorDiceType ?? "white" const threshold = skillColor === "black" ? 2 : skillColor === "red" ? 3 : 4 const hasNimble = sys.traits.has("nimble") // Auto-bonuses from special properties let autoAttackBonus = 0 if (sys.specialProperties.has("master-crafted")) autoAttackBonus += 1 if (sys.specialProperties.has("accurate")) autoAttackBonus += 1 // bows if (sys.specialProperties.has("balanced")) autoAttackBonus += 1 // grants Fast // Damage info for reference const hasBrutal = sys.traits.has("brutal") const hasDeadly = sys.traits.has("deadly") const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white" const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4 const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜" const mightRank = actorSys.attributes.might.rank const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) const traitLabels = [...sys.traits].map(t => { const key = SYSTEM.WEAPON_TRAITS[t] return key ? game.i18n.localize(key) : t }) // Option arrays const attackBonusOptions = Array.from({ length: 13 }, (_, i) => { const v = i - 6 return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } }) const rangeOptions = [ { value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.RangeNormal") }, { value: -1, label: game.i18n.localize("OATHHAMMER.Dialog.RangeLong") + " (−1)" }, { value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeMoving") + " (−2)" }, { value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment") + " (−2)" }, { value: -3, label: game.i18n.localize("OATHHAMMER.Dialog.RangeCover") + " (−3)" }, ] const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const context = { actorName: actor.name, weaponName: weapon.name, weaponImg: weapon.img, skillKey, skillLabel: game.i18n.localize(skillDef.label), attrKey: defaultAttr, attrLabel: game.i18n.localize(`OATHHAMMER.Attribute.${_cap(defaultAttr)}`), attrRank, skillRank, colorType: skillColor, threshold, baseAttackPool: attrRank + skillRank, autoAttackBonus, hasNimble, mightLabel: game.i18n.localize("OATHHAMMER.Attribute.Might"), mightRank, agilityLabel: game.i18n.localize("OATHHAMMER.Attribute.Agility"), agilityRank: actorSys.attributes.agility.rank, isRanged, shortRange: sys.shortRange, longRange: sys.longRange, damageLabel: sys.damageLabel, damageColorType, damageThreshold, damageColorLabel, baseDamageDice, apValue: sys.ap, traits: traitLabels, attackBonusOptions, rangeOptions, rollModes, visibility: game.settings.get("core", "rollMode"), } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-oath-hammer/templates/weapon-attack-dialog.hbs", context ) const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) }, classes: ["fvtt-oath-hammer"], content, rejectClose: false, buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.RollAttack"), callback: (_ev, btn) => Object.fromEntries( [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value]) ), }], }) if (!result) return null return { attackBonus: parseInt(result.attackBonus) || 0, rangeCondition: parseInt(result.rangeCondition) || 0, attrOverride: result.attrOverride || defaultAttr, visibility: result.visibility ?? game.settings.get("core", "rollMode"), autoAttackBonus, } } // ------------------------------------------------------------------ // // DAMAGE DIALOG // ------------------------------------------------------------------ // static async promptDamage(actor, weapon, defaultSV = 0) { const sys = weapon.system const actorSys = actor.system const hasBrutal = sys.traits.has("brutal") const hasDeadly = sys.traits.has("deadly") const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white" const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4 const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜" const mightRank = actorSys.attributes.might.rank const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1) // Auto-bonuses from special properties let autoDamageBonus = 0 if (sys.specialProperties.has("master-crafted")) autoDamageBonus += 1 if (sys.specialProperties.has("tempered")) autoDamageBonus += 1 if (sys.specialProperties.has("heavy-draw")) autoDamageBonus += 1 const svOptions = Array.from({ length: 11 }, (_, i) => ({ value: i, label: i === 0 ? "0" : `+${i}d`, selected: i === defaultSV, })) const damageBonusOptions = Array.from({ length: 9 }, (_, i) => { const v = i - 4 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, damageLabel: sys.damageLabel, damageColorType, damageThreshold, damageColorLabel, baseDamageDice, autoDamageBonus, apValue: sys.ap, defaultSV, svOptions, damageBonusOptions, rollModes, visibility: game.settings.get("core", "rollMode"), } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-oath-hammer/templates/weapon-damage-dialog.hbs", context ) const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) }, classes: ["fvtt-oath-hammer"], content, rejectClose: false, buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.RollDamage"), callback: (_ev, btn) => Object.fromEntries( [...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value]) ), }], }) if (!result) return null return { sv: parseInt(result.sv) || 0, damageBonus: parseInt(result.damageBonus) || 0, visibility: result.visibility ?? game.settings.get("core", "rollMode"), autoDamageBonus, } } } function _cap(str) { return str.charAt(0).toUpperCase() + str.slice(1) }