import type { Pf2eCreature, TraitBlock, TraitSegment, } from "./creature-types.js"; export type CreatureAdjustment = "weak" | "elite"; /** HP bracket delta by creature level (standard PF2e table). */ function hpBracketDelta(level: number): number { if (level <= 1) return 10; if (level <= 4) return 15; if (level <= 19) return 20; return 30; } /** Level shift: elite +1 (or +2 if level ≤ 0), weak −1 (or −2 if level is 1). */ export function adjustedLevel( baseLevel: number, adjustment: CreatureAdjustment, ): number { if (adjustment === "elite") { return baseLevel <= 0 ? baseLevel + 2 : baseLevel + 1; } return baseLevel === 1 ? baseLevel - 2 : baseLevel - 1; } /** Signed HP delta for a given base level and adjustment. */ export function hpDelta( baseLevel: number, adjustment: CreatureAdjustment, ): number { const delta = hpBracketDelta(baseLevel); return adjustment === "elite" ? delta : -delta; } /** AC delta: +2 for elite, −2 for weak. */ export function acDelta(adjustment: CreatureAdjustment): number { return adjustment === "elite" ? 2 : -2; } /** Generic ±2 modifier delta. Used for saves, Perception, attacks, damage. */ export function modDelta(adjustment: CreatureAdjustment): number { return adjustment === "elite" ? 2 : -2; } const ATTACK_BONUS_RE = /^([+-])(\d+)/; const MAP_RE = /\[([+-]\d+)\/([+-]\d+)\]/g; const DAMAGE_BONUS_RE = /(\d+d\d+)([+-])(\d+)/g; /** * Adjust attack bonus in a formatted attack string. * "+15 (agile), 2d12+7 piercing plus Grab" → "+17 (agile), 2d12+9 piercing plus Grab" */ function adjustAttackText(text: string, delta: number): string { // Adjust leading attack bonus: "+15" → "+17" let result = text.replace(ATTACK_BONUS_RE, (_, sign, num) => { const adjusted = (sign === "+" ? 1 : -1) * Number(num) + delta; return adjusted >= 0 ? `+${adjusted}` : `${adjusted}`; }); // Adjust MAP values in brackets: "[+10/+5]" → "[+12/+7]" result = result.replace(MAP_RE, (_, m1, m2) => { const a1 = Number(m1) + delta; const a2 = Number(m2) + delta; const f = (n: number) => (n >= 0 ? `+${n}` : `${n}`); return `[${f(a1)}/${f(a2)}]`; }); // Adjust damage bonus in "NdN+N type" patterns result = result.replace(DAMAGE_BONUS_RE, (_, dice, sign, num) => { const current = (sign === "+" ? 1 : -1) * Number(num); const adjusted = current + delta; if (adjusted === 0) return dice as string; return adjusted > 0 ? `${dice}+${adjusted}` : `${dice}${adjusted}`; }); return result; } function adjustTraitBlock(block: TraitBlock, delta: number): TraitBlock { return { ...block, segments: block.segments.map( (seg): TraitSegment => seg.type === "text" ? { type: "text", value: adjustAttackText(seg.value, delta) } : seg, ), }; } /** * Apply a weak or elite adjustment to a full PF2e creature. * Returns a new Pf2eCreature with all numeric stats adjusted. */ export function applyPf2eAdjustment( creature: Pf2eCreature, adjustment: CreatureAdjustment, ): Pf2eCreature { const d = modDelta(adjustment); return { ...creature, level: adjustedLevel(creature.level, adjustment), ac: creature.ac + d, hp: creature.hp + hpDelta(creature.level, adjustment), perception: creature.perception + d, saveFort: creature.saveFort + d, saveRef: creature.saveRef + d, saveWill: creature.saveWill + d, attacks: creature.attacks?.map((a) => adjustTraitBlock(a, d)), }; }