Add roll windows from actor sheet

This commit is contained in:
2026-03-15 23:20:32 +01:00
parent 82fddb0cb3
commit 49347370c7
57 changed files with 6372 additions and 184 deletions

View File

@@ -0,0 +1,215 @@
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)
}