Files
fvtt-oath-hammer/module/applications/spell-dialog.mjs
LeRatierBretonnier b3fd7e1aa1 feat: add Settlement actor type with Overview/Buildings/Inventory tabs
- New TypeDataModel: archetype, territory, renown, currency (gp/sp/cp),
  garrison, underSiege, isCapital, founded, taxNotes, description, notes
- 3-tab ApplicationV2 sheet with drag & drop for building/weapon/armor/equipment
- Currency steppers (+/−), building constructed toggle, qty controls
- LESS-based CSS (settlement-sheet.less) + base.less updated for shared styles
- Full i18n keys in lang/en.json (8 settlement archetypes)
- system.json: registered settlement actor type

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 17:01:38 +01:00

148 lines
5.6 KiB
JavaScript

import { SYSTEM } from "../config/system.mjs"
/**
* Spell enhancements — applied before casting (p.96-97).
* stress: Arcane Stress cost regardless of roll result
* penalty: dice pool penalty
* redDice: true = roll red dice (3+ threshold) on the check
* noStress: true = 1s rolled do NOT add Arcane Stress (Safe Spell)
*/
export const SPELL_ENHANCEMENTS = {
none: { label: "OATHHAMMER.Enhancement.None", stress: 0, penalty: 0, redDice: false, noStress: false },
focused: { label: "OATHHAMMER.Enhancement.Focused", stress: 1, penalty: 0, redDice: true, noStress: false },
controlled: { label: "OATHHAMMER.Enhancement.Controlled", stress: 1, penalty: 0, redDice: false, noStress: false },
empowered: { label: "OATHHAMMER.Enhancement.Empowered", stress: 2, penalty: 0, redDice: false, noStress: false },
extended: { label: "OATHHAMMER.Enhancement.Extended", stress: 1, penalty: -1, redDice: false, noStress: false },
penetrating: { label: "OATHHAMMER.Enhancement.Penetrating", stress: 1, penalty: -1, redDice: false, noStress: false },
lethal: { label: "OATHHAMMER.Enhancement.Lethal", stress: 2, penalty: -2, redDice: false, noStress: false },
hastened: { label: "OATHHAMMER.Enhancement.Hastened", stress: 3, penalty: -3, redDice: false, noStress: false },
safe: { label: "OATHHAMMER.Enhancement.Safe", stress: 0, penalty: -3, redDice: false, noStress: true },
}
export default class OathHammerSpellDialog {
static async prompt(actor, spell) {
const sys = spell.system
const actorSys = actor.system
const intRank = actorSys.attributes.intelligence.rank
const magicRank = actorSys.skills.magic.rank
const basePool = intRank + magicRank
const currentStress = actorSys.arcaneStress.value
const stressThreshold = actorSys.arcaneStress.threshold
const isOverThreshold = currentStress >= stressThreshold
const isElemental = sys.tradition === "elemental"
const dv = sys.difficultyValue
const traditionLabel = (() => {
const entry = SYSTEM.SORCEROUS_TRADITIONS?.[sys.tradition]
return entry ? game.i18n.localize(entry.label) : (sys.tradition ?? "")
})()
const enhancementOptions = Object.entries(SPELL_ENHANCEMENTS).map(([key, def]) => ({
value: key,
label: game.i18n.localize(def.label),
selected: key === "none",
}))
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,
}))
// Pool size selector: casters may roll fewer dice to reduce Arcane Stress (p.101)
const poolSizeOptions = Array.from({ length: basePool }, (_, i) => ({
value: i + 1,
label: `${i + 1}d`,
selected: i + 1 === basePool,
}))
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
const context = {
actorName: actor.name,
spellName: spell.name,
spellImg: spell.img,
dv,
traditionLabel,
isRitual: sys.isRitual,
isMagicMissile: sys.isMagicMissile,
range: sys.range,
duration: sys.duration,
spellSave: sys.spellSave,
isElemental,
element: sys.element,
intRank,
magicRank,
basePool,
poolSizeOptions,
currentStress,
stressThreshold,
isOverThreshold,
enhancementOptions,
bonusOptions,
availableLuck,
isHuman,
luckOptions,
rollModes,
visibility: game.settings.get("core", "rollMode"),
}
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/spell-cast-dialog.hbs",
context
)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.SpellCastTitle", { spell: spell.name }) },
classes: ["fvtt-oath-hammer"],
content,
rejectClose: false,
buttons: [{
label: game.i18n.localize("OATHHAMMER.Dialog.CastSpell"),
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 enhKey = result.enhancement ?? "none"
const enh = SPELL_ENHANCEMENTS[enhKey] ?? SPELL_ENHANCEMENTS.none
return {
dv,
enhancement: enhKey,
stressCost: enh.stress,
poolPenalty: enh.penalty,
redDice: enh.redDice,
noStress: enh.noStress,
elementalBonus: parseInt(result.elementalBonus) || 0,
bonus: parseInt(result.bonus) || 0,
poolSize: Math.min(Math.max(1, parseInt(result.poolSize) || basePool), basePool),
grimPenalty: parseInt(result.noGrimoire) || 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",
}
}
}