import type { DomainEvent } from "./events.js"; import type { CombatantId, DomainError, Encounter } from "./types.js"; export interface AdjustHpSuccess { readonly encounter: Encounter; readonly events: DomainEvent[]; } /** * Pure function that adjusts a combatant's current HP by a delta. * * The result is clamped to [0, maxHp]. Requires the combatant to have * HP tracking enabled (maxHp must be set). */ export function adjustHp( encounter: Encounter, combatantId: CombatantId, delta: number, ): AdjustHpSuccess | DomainError { const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId); if (targetIdx === -1) { return { kind: "domain-error", code: "combatant-not-found", message: `No combatant found with ID "${combatantId}"`, }; } const target = encounter.combatants[targetIdx]; if (target.maxHp === undefined || target.currentHp === undefined) { return { kind: "domain-error", code: "no-hp-tracking", message: `Combatant "${combatantId}" does not have HP tracking enabled`, }; } if (delta === 0) { return { kind: "domain-error", code: "zero-delta", message: "Delta must not be zero", }; } if (!Number.isInteger(delta)) { return { kind: "domain-error", code: "invalid-delta", message: `Delta must be an integer, got ${delta}`, }; } const previousHp = target.currentHp; const previousTempHp = target.tempHp ?? 0; let newTempHp = previousTempHp; let effectiveDelta = delta; if (delta < 0 && previousTempHp > 0) { const absorbed = Math.min(previousTempHp, Math.abs(delta)); newTempHp = previousTempHp - absorbed; effectiveDelta = delta + absorbed; } const newHp = Math.max( 0, Math.min(target.maxHp, previousHp + effectiveDelta), ); const events: DomainEvent[] = []; if (newTempHp !== previousTempHp) { events.push({ type: "TempHpSet", combatantId, previousTempHp: previousTempHp || undefined, newTempHp: newTempHp || undefined, }); } if (newHp !== previousHp) { events.push({ type: "CurrentHpAdjusted", combatantId, previousHp, newHp, delta, }); } return { encounter: { combatants: encounter.combatants.map((c) => c.id === combatantId ? { ...c, currentHp: newHp, tempHp: newTempHp || undefined } : c, ), activeIndex: encounter.activeIndex, roundNumber: encounter.roundNumber, }, events, }; }