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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/** 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;
|
||||
}
|
||||
Reference in New Issue
Block a user