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>
79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
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);
|
||
});
|
||
});
|