Files
initiative/packages/domain/src/__tests__/hp-range.test.ts
T
LukasandClaude Opus 5 be3778a0c9 Add min/max hit point variants for D&D creatures
A D&D stat block opened for a combatant now offers a Min/Avg/Max toggle
below the Hit Points line, deriving both ends from the Hit Dice formula
(3d6 + 6 -> 9 / 16 / 24). The switch is applied as a delta, so manual HP
edits and damage already taken survive it. Unlike the PF2e weak/elite
adjustment this is a convenience tool, not a rules mechanic: nothing but
HP changes and the combatant is not renamed. The toggle is hidden when
the HP field carries prose instead of a dice pool.

Also extracts a SegmentedControl primitive. Game system, theme and the
PF2e weak/elite toggle were three hand-rolled copies of the same markup,
which is why the D&D toggle first came out chunkier and in the wrong
accent color. All four now share one definition, and segments expose
aria-pressed instead of signalling the active state by color alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 17:06:01 +02:00

79 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { hpForVariant, hpRange } from "../hp-range.js";
describe("hpRange", () => {
it("derives min and max from a formula with a positive modifier", () => {
const range = hpRange({ average: 16, formula: "3d6 + 6" });
expect(range).toEqual({ min: 9, average: 16, max: 24 });
});
it("derives min and max from a formula without a modifier", () => {
const range = hpRange({ average: 40, formula: "9d8" });
expect(range).toEqual({ min: 9, average: 40, max: 72 });
});
it("subtracts a negative modifier", () => {
const range = hpRange({ average: 45, formula: "10d8 - 5" });
expect(range).toEqual({ min: 5, average: 45, max: 75 });
});
it("treats an en dash as a minus sign", () => {
// Bundled bestiary data contains e.g. "2d6 2" (U+2013).
const range = hpRange({ average: 5, formula: "2d6 2" });
expect(range).toEqual({ min: 1, average: 5, max: 10 });
});
it("never drops below 1 HP when the modifier exceeds the dice", () => {
const range = hpRange({ average: 1, formula: "1d4 - 10" });
expect(range?.min).toBe(1);
expect(range?.max).toBe(1);
});
it("handles large pools without whitespace", () => {
expect(hpRange({ average: 256, formula: "19d12+133" })).toEqual({
min: 152,
average: 256,
max: 361,
});
});
it("returns null for a missing formula", () => {
expect(hpRange({ average: 7, formula: "" })).toBeNull();
});
it("returns null for prose instead of a dice pool", () => {
expect(
hpRange({ average: 0, formula: "equal to the summoner's" }),
).toBeNull();
});
it("returns null for a flat number", () => {
expect(hpRange({ average: 50, formula: "50" })).toBeNull();
});
it("returns null for a pool with zero dice or zero sides", () => {
expect(hpRange({ average: 0, formula: "0d6" })).toBeNull();
expect(hpRange({ average: 0, formula: "2d0" })).toBeNull();
});
it("returns null for compound formulas it cannot reason about", () => {
expect(hpRange({ average: 20, formula: "2d6 + 2d8" })).toBeNull();
});
});
describe("hpForVariant", () => {
const range = { min: 9, average: 16, max: 24 };
it("returns the average when no variant is set", () => {
expect(hpForVariant(range, undefined)).toBe(16);
});
it("returns the minimum for the min variant", () => {
expect(hpForVariant(range, "min")).toBe(9);
});
it("returns the maximum for the max variant", () => {
expect(hpForVariant(range, "max")).toBe(24);
});
});