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,73 @@
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user