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>
This commit is contained in:
@@ -6,12 +6,18 @@ import { expectDomainError } from "./test-helpers.js";
|
||||
|
||||
function makeCombatant(
|
||||
name: string,
|
||||
opts?: { maxHp: number; currentHp: number },
|
||||
opts?: { maxHp: number; currentHp: number; tempHp?: number },
|
||||
): Combatant {
|
||||
return {
|
||||
id: combatantId(name),
|
||||
name,
|
||||
...(opts ? { maxHp: opts.maxHp, currentHp: opts.currentHp } : {}),
|
||||
...(opts
|
||||
? {
|
||||
maxHp: opts.maxHp,
|
||||
currentHp: opts.currentHp,
|
||||
tempHp: opts.tempHp,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,4 +158,96 @@ describe("adjustHp", () => {
|
||||
expect(encounter.combatants[0].currentHp).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("temporary HP absorption", () => {
|
||||
it("damage fully absorbed by temp HP — currentHp unchanged", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 15, tempHp: 8 }),
|
||||
]);
|
||||
const { encounter } = successResult(e, "A", -5);
|
||||
expect(encounter.combatants[0].currentHp).toBe(15);
|
||||
expect(encounter.combatants[0].tempHp).toBe(3);
|
||||
});
|
||||
|
||||
it("damage partially absorbed by temp HP — overflow reduces currentHp", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 15, tempHp: 3 }),
|
||||
]);
|
||||
const { encounter } = successResult(e, "A", -10);
|
||||
expect(encounter.combatants[0].tempHp).toBeUndefined();
|
||||
expect(encounter.combatants[0].currentHp).toBe(8);
|
||||
});
|
||||
|
||||
it("damage exceeding both temp HP and currentHp — both reach minimum", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 5, tempHp: 3 }),
|
||||
]);
|
||||
const { encounter } = successResult(e, "A", -50);
|
||||
expect(encounter.combatants[0].tempHp).toBeUndefined();
|
||||
expect(encounter.combatants[0].currentHp).toBe(0);
|
||||
});
|
||||
|
||||
it("healing does not restore temp HP", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 10, tempHp: 3 }),
|
||||
]);
|
||||
const { encounter } = successResult(e, "A", 5);
|
||||
expect(encounter.combatants[0].currentHp).toBe(15);
|
||||
expect(encounter.combatants[0].tempHp).toBe(3);
|
||||
});
|
||||
|
||||
it("temp HP cleared to undefined when fully depleted", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 15, tempHp: 5 }),
|
||||
]);
|
||||
const { encounter } = successResult(e, "A", -5);
|
||||
expect(encounter.combatants[0].tempHp).toBeUndefined();
|
||||
expect(encounter.combatants[0].currentHp).toBe(15);
|
||||
});
|
||||
|
||||
it("emits only TempHpSet when damage fully absorbed", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 15, tempHp: 8 }),
|
||||
]);
|
||||
const { events } = successResult(e, "A", -3);
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "TempHpSet",
|
||||
combatantId: combatantId("A"),
|
||||
previousTempHp: 8,
|
||||
newTempHp: 5,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits both TempHpSet and CurrentHpAdjusted when damage overflows", () => {
|
||||
const e = enc([
|
||||
makeCombatant("A", { maxHp: 20, currentHp: 15, tempHp: 3 }),
|
||||
]);
|
||||
const { events } = successResult(e, "A", -10);
|
||||
expect(events).toHaveLength(2);
|
||||
expect(events[0]).toEqual({
|
||||
type: "TempHpSet",
|
||||
combatantId: combatantId("A"),
|
||||
previousTempHp: 3,
|
||||
newTempHp: undefined,
|
||||
});
|
||||
expect(events[1]).toEqual({
|
||||
type: "CurrentHpAdjusted",
|
||||
combatantId: combatantId("A"),
|
||||
previousHp: 15,
|
||||
newHp: 8,
|
||||
delta: -10,
|
||||
});
|
||||
});
|
||||
|
||||
it("damage with no temp HP works as before", () => {
|
||||
const e = enc([makeCombatant("A", { maxHp: 20, currentHp: 15 })]);
|
||||
const { encounter, events } = successResult(e, "A", -5);
|
||||
expect(encounter.combatants[0].currentHp).toBe(10);
|
||||
expect(encounter.combatants[0].tempHp).toBeUndefined();
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].type).toBe("CurrentHpAdjusted");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user