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>
59 lines
1.7 KiB
TypeScript
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;
|
|
}
|