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>
74 lines
1.7 KiB
TypeScript
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,
|
|
},
|
|
],
|
|
};
|
|
}
|