import type { DomainEvent } from "./events.js"; import type { CombatantId, DomainError, Encounter } from "./types.js"; export interface SetTempHpSuccess { readonly encounter: Encounter; readonly events: DomainEvent[]; } /** * Pure function that sets or clears a combatant's temporary HP. * * - Setting tempHp when the combatant already has tempHp keeps the higher value. * - Clearing tempHp (undefined) removes temp HP entirely. * - Requires HP tracking to be enabled (maxHp must be set). */ export function setTempHp( encounter: Encounter, combatantId: CombatantId, tempHp: number | undefined, ): SetTempHpSuccess | 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 (tempHp !== undefined && (!Number.isInteger(tempHp) || tempHp < 1)) { return { kind: "domain-error", code: "invalid-temp-hp", message: `Temp HP must be a positive integer, got ${tempHp}`, }; } const previousTempHp = target.tempHp; // Higher value wins when both are defined let newTempHp: number | undefined; if (tempHp === undefined) { newTempHp = undefined; } else if (previousTempHp === undefined) { newTempHp = tempHp; } else { newTempHp = Math.max(previousTempHp, tempHp); } return { encounter: { combatants: encounter.combatants.map((c) => c.id === combatantId ? { ...c, tempHp: newTempHp } : c, ), activeIndex: encounter.activeIndex, roundNumber: encounter.roundNumber, }, events: [ { type: "TempHpSet", combatantId, previousTempHp, newTempHp, }, ], }; }