/** * Spell damage formula evaluator. * * Tokens recognised in `spell.damageBase`: * magicalAbilities, devotion, soul, mind, body, magic * magicalAbilities/2 (and similar /N or *N suffixes) * 1d6, 2d6, 1d3 etc. (Foundry dice notation) * numeric literals * Operators: + - * / (integer division for /N tokens; standard for dice) * * Returns a Foundry Roll ready to evaluate, or null when the spell has * no damageBase formula. */ import type { CastableActor } from './spell-cast'; export interface DamageContext { actor: CastableActor; spellName?: string; } const ATTR_TOKENS = new Set(['body', 'mind', 'soul', 'magic', 'magicalAbilities', 'devotion']); function resolveToken(token: string, ctx: DamageContext): number { const a = (ctx.actor as any).system?.attributes ?? {}; const s = (ctx.actor as any).system?.skills ?? {}; switch (token) { case 'body': return Number(a.body?.value ?? 0); case 'mind': return Number(a.mind?.value ?? 0); case 'soul': return Number(a.soul?.value ?? 0); case 'magic': return Number(a.magic?.value ?? 0); case 'magicalAbilities': return Number(s.magicalAbilities?.value ?? 0); case 'devotion': return Number(s.devotion?.value ?? 0); default: return 0; } } /** * Substitutes attribute tokens with their numeric values, leaving dice * notation intact for Foundry to parse. */ export function buildDamageFormula(spellDamageBase: string, ctx: DamageContext): string | null { if (!spellDamageBase || !spellDamageBase.trim()) return null; let formula = spellDamageBase.trim(); // Replace attribute tokens (with optional /N or *N suffix). // Match e.g. magicalAbilities, magicalAbilities/2, soul*3 const tokenRe = /([a-zA-Z]+)(?:\s*([\/*])\s*(\d+))?/g; formula = formula.replace(tokenRe, (match, name: string, op: string | undefined, num: string | undefined) => { if (!ATTR_TOKENS.has(name)) { // Leave alone (likely dice notation like 1d6 - no, dice has digits before) return match; } let v = resolveToken(name, ctx); if (op && num) { const n = Number(num); if (op === '/') v = Math.ceil(v / n); // ceil per HbM rounding rules if (op === '*') v = v * n; } return String(v); }); return formula; } export async function rollSpellDamage(spellDamageBase: string, ctx: DamageContext): Promise { const formula = buildDamageFormula(spellDamageBase, ctx); if (!formula) return null; const roll = new Roll(formula, {}); await roll.evaluate(); return roll; }