Files
fvtt-oath-hammer/module/applications/miracle-dialog.mjs
T
uberwald 0381e8e024
Release Creation / build (release) Successful in 1m25s
feat: luck bonus, inventory slots bonus, multi-enhancements, magic skill modifier
- Add luck.bonus field to character DataModel; included in prepareDerivedData
   (luck.max = fate.rank + bonus); shown as editable +bonus field next to
   luck max in edit mode (same UX as grit bonus)

 - Add inventory.slotsBonus field to character DataModel; Equipment tab now
   shows an editable "Bonus Slots" input that adds to the calculated max slots
   (10 + Might×2 + bonus)

 - Replace single Enhancement <select> in spell cast dialog with a checkbox
   list; multiple enhancements can now be selected simultaneously — stress
   costs, pool penalties, and boolean flags (redDice, noStress) are aggregated
   across all active enhancements

 - Include skills.magic.modifier in basePool for both spell and miracle dialogs
   and in baseDice in rollSpellCast / rollMiracleCast; modifier is shown in
   the dialog pool-info line and in the chat card when non-zero

 - Fix: pool-reduction indicator in rollSpellCast now compares against
   intRank + magicRank + magicMod (was missing magicMod)
2026-05-12 08:16:57 +02:00

123 lines
4.1 KiB
JavaScript

import { SYSTEM } from "../config/system.mjs"
export default class OathHammerMiracleDialog {
static async prompt(actor, miracle) {
const sys = miracle.system
const actorSys = actor.system
const wpRank = actorSys.attributes.willpower.rank
const magicRank = actorSys.skills.magic.rank
const magicMod = actorSys.skills.magic.modifier ?? 0
const magicColor = actorSys.skills.magic.colorDiceType ?? "white"
const basePool = wpRank + magicRank + magicMod
const magicModDisplay = magicMod > 0 ? `+${magicMod}` : magicMod < 0 ? `${magicMod}` : ""
const isRitual = sys.isRitual
const dv = isRitual ? (sys.difficultyValue || 1) : null
const traditionLabel = (() => {
const key = SYSTEM.DIVINE_TRADITIONS?.[sys.divineTradition]
return key ? game.i18n.localize(key) : sys.divineTradition
})()
const colorOptions = [
{ value: "white", label: game.i18n.localize("OATHHAMMER.ColorDice.White"), selected: magicColor === "white" },
{ value: "red", label: game.i18n.localize("OATHHAMMER.ColorDice.Red"), selected: magicColor === "red" },
{ value: "black", label: game.i18n.localize("OATHHAMMER.ColorDice.Black"), selected: magicColor === "black" },
]
// Miracle count options — DV = miracle number today
const miracleCountOptions = Array.from({ length: 10 }, (_, i) => ({
value: i + 1,
label: `${i + 1}${_ordinal(i + 1)} — DV${i + 1}`,
selected: i === 0,
}))
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
})
const availableLuck = actorSys.luck?.value ?? 0
const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human"
const luckDicePerPoint = isHuman ? 3 : 2
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
value: i,
label: i === 0 ? "0" : `${i} (+${i * luckDicePerPoint}d)`,
selected: i === 0,
}))
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = {
actorName: actor.name,
miracleName: miracle.name,
miracleImg: miracle.img,
dv,
traditionLabel,
isRitual,
range: sys.range,
duration: sys.duration,
spellSave: sys.spellSave,
wpRank,
magicRank,
magicMod,
magicModDisplay,
basePool,
miracleCountOptions,
colorOptions,
bonusOptions,
availableLuck,
isHuman,
luckOptions,
rollModes,
visibility: game.settings.get("core", "rollMode"),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/miracle-cast-dialog.hbs",
context
)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.MiracleCastTitle", { miracle: miracle.name }) },
classes: ["fvtt-oath-hammer"],
content,
rejectClose: false,
buttons: [{
label: game.i18n.localize("OATHHAMMER.Dialog.InvokeMiracle"),
callback: (_ev, btn) => {
const out = {}
for (const el of btn.form.elements) {
if (!el.name) continue
out[el.name] = el.type === "checkbox" ? String(el.checked) : el.value
}
return out
},
}],
})
if (!result) return null
const computedDV = isRitual ? dv : (parseInt(result.miracleCount) || 1)
return {
dv: computedDV,
isRitual,
colorOverride: result.colorOverride ?? magicColor,
bonus: parseInt(result.bonus) || 0,
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
explodeOn5: result.explodeOn5 === "true",
luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
luckIsHuman: result.luckIsHuman === "true",
}
}
}
function _ordinal(n) {
const s = ["th", "st", "nd", "rd"]
const v = n % 100
return s[(v - 20) % 10] ?? s[v] ?? s[0]
}