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

74 lines
1.7 KiB
TypeScript

import type { DomainEvent } from "./events.js";
import { type HpRange, type HpVariant, hpForVariant } from "./hp-range.js";
import {
type CombatantId,
type DomainError,
type Encounter,
findCombatant,
isDomainError,
} from "./types.js";
export interface SetHpVariantSuccess {
readonly encounter: Encounter;
readonly events: DomainEvent[];
}
/**
* Switches a combatant's max HP between the minimum, average and maximum roll
* of its Hit Dice.
*
* The shift is applied as a delta so manual HP edits and damage already taken
* survive the switch: a creature missing 5 HP still misses 5 HP afterwards.
*/
export function setHpVariant(
encounter: Encounter,
combatantId: CombatantId,
variant: HpVariant | undefined,
range: HpRange,
): SetHpVariantSuccess | DomainError {
const found = findCombatant(encounter, combatantId);
if (isDomainError(found)) return found;
const { combatant } = found;
if (combatant.hpVariant === variant) {
return { encounter, events: [] };
}
const delta =
hpForVariant(range, variant) - hpForVariant(range, combatant.hpVariant);
const newMaxHp =
combatant.maxHp === undefined
? undefined
: Math.max(1, combatant.maxHp + delta);
const newCurrentHp =
combatant.currentHp === undefined || newMaxHp === undefined
? combatant.currentHp
: Math.max(0, Math.min(combatant.currentHp + delta, newMaxHp));
return {
encounter: {
...encounter,
combatants: encounter.combatants.map((c) =>
c.id === combatantId
? {
...c,
maxHp: newMaxHp,
currentHp: newCurrentHp,
hpVariant: variant,
}
: c,
),
},
events: [
{
type: "HpVariantSet",
combatantId,
variant,
previousMaxHp: combatant.maxHp,
newMaxHp,
},
],
};
}