Files
initiative/packages/domain/src/hp-range.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

59 lines
1.7 KiB
TypeScript

/** Which end of a creature's Hit Dice range its max HP is taken from. */
export type HpVariant = "min" | "max";
export const VALID_HP_VARIANTS: ReadonlySet<string> = new Set(["min", "max"]);
export interface HpRange {
/** Every Hit Die rolls a 1 (never below 1 HP). */
readonly min: number;
/** The statblock's printed average. */
readonly average: number;
/** Every Hit Die rolls its maximum. */
readonly max: number;
}
/**
* Matches a plain Hit Dice pool: "9d8", "3d6 + 6", "2d6 - 2".
* Bestiary data uses several dash characters, so hyphen, minus sign, en dash
* and em dash all count as a negative modifier.
*/
const DICE_FORMULA_REGEX =
/^\s*(\d+)\s*[dD]\s*(\d+)\s*(?:([-+−–—])\s*(\d+))?\s*$/;
/**
* Derives the min/average/max HP of a creature from its Hit Dice formula.
* Returns null when the formula is not a plain dice pool — some sources carry
* prose (`hp.special`) or an empty string instead.
*/
export function hpRange(
hp: Readonly<{ average: number; formula: string }>,
): HpRange | null {
const match = DICE_FORMULA_REGEX.exec(hp.formula);
if (!match) return null;
const count = Number(match[1]);
const sides = Number(match[2]);
if (count < 1 || sides < 1) return null;
const modifier =
match[4] === undefined
? 0
: (match[3] === "+" ? 1 : -1) * Number.parseInt(match[4], 10);
return {
min: Math.max(1, count + modifier),
average: hp.average,
max: Math.max(1, count * sides + modifier),
};
}
/** The HP value a variant selects; undefined means the printed average. */
export function hpForVariant(
range: HpRange,
variant: HpVariant | undefined,
): number {
if (variant === "min") return range.min;
if (variant === "max") return range.max;
return range.average;
}