Files
hbm-foundry-system/src/logic/spell-damage.ts
T

74 lines
2.5 KiB
TypeScript

/**
* 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<Roll | null> {
const formula = buildDamageFormula(spellDamageBase, ctx);
if (!formula) return null;
const roll = new Roll(formula, {});
await roll.evaluate();
return roll;
}