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:
Lukas
2026-03-23 11:39:47 +01:00
parent 7b83e3c3ea
commit 8bf69fd47d
18 changed files with 731 additions and 29 deletions

View File

@@ -11,15 +11,21 @@ afterEach(cleanup);
function renderPopover(
overrides: Partial<{
onAdjust: (delta: number) => void;
onSetTempHp: (value: number) => void;
onClose: () => void;
}> = {},
) {
const onAdjust = overrides.onAdjust ?? vi.fn();
const onSetTempHp = overrides.onSetTempHp ?? vi.fn();
const onClose = overrides.onClose ?? vi.fn();
const result = render(
<HpAdjustPopover onAdjust={onAdjust} onClose={onClose} />,
<HpAdjustPopover
onAdjust={onAdjust}
onSetTempHp={onSetTempHp}
onClose={onClose}
/>,
);
return { ...result, onAdjust, onClose };
return { ...result, onAdjust, onSetTempHp, onClose };
}
describe("HpAdjustPopover", () => {
@@ -112,4 +118,31 @@ describe("HpAdjustPopover", () => {
await user.type(input, "12abc34");
expect(input).toHaveValue("1234");
});
describe("temp HP", () => {
it("shield button calls onSetTempHp with entered value and closes", async () => {
const user = userEvent.setup();
const { onSetTempHp, onClose } = renderPopover();
await user.type(screen.getByPlaceholderText("HP"), "8");
await user.click(screen.getByRole("button", { name: "Set temp HP" }));
expect(onSetTempHp).toHaveBeenCalledWith(8);
expect(onClose).toHaveBeenCalled();
});
it("shield button is disabled when input is empty", () => {
renderPopover();
expect(
screen.getByRole("button", { name: "Set temp HP" }),
).toBeDisabled();
});
it("shield button is disabled when input is '0'", async () => {
const user = userEvent.setup();
renderPopover();
await user.type(screen.getByPlaceholderText("HP"), "0");
expect(
screen.getByRole("button", { name: "Set temp HP" }),
).toBeDisabled();
});
});
});