Add roll windows from actor sheet
This commit is contained in:
215
module/applications/weapon-dialog.mjs
Normal file
215
module/applications/weapon-dialog.mjs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user