Files
initiative/packages/domain/src/set-temp-hp.ts
Lukas 8bf69fd47d Add temporary hit points as a separate damage buffer
Temp HP absorbs damage before current HP, cannot be healed, and
does not stack (higher value wins). Displayed as cyan +N after
current HP with a Shield button in the HP adjustment popover.
Column space is reserved across all rows only when any combatant
has temp HP. Concentration pulse fires on any damage, including
damage fully absorbed by temp HP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 11:39:47 +01:00

79 lines
2.0 KiB
TypeScript

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,
},
],
};
}