Implement the 009-combatant-hp feature that adds optional max HP and current HP tracking per combatant with +/- controls, direct entry, and persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-05 17:18:03 +01:00
parent a9c280a6d6
commit 8185fde0e8
21 changed files with 1367 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
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 newHp = Math.max(0, Math.min(target.maxHp, previousHp + delta));
return {
encounter: {
combatants: encounter.combatants.map((c) =>
c.id === combatantId ? { ...c, currentHp: newHp } : c,
),
activeIndex: encounter.activeIndex,
roundNumber: encounter.roundNumber,
},
events: [
{
type: "CurrentHpAdjusted",
combatantId,
previousHp,
newHp,
delta,
},
],
};
}