/** 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 = 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; }