Files
initiative/specs/009-combatant-hp/data-model.md

3.4 KiB

Data Model: Combatant HP Tracking

Feature: 009-combatant-hp | Date: 2026-03-05

Entities

Combatant (extended)

Field Type Required Constraints
id CombatantId (branded string) Yes Unique within encounter
name string Yes Non-empty
initiative number or undefined No Integer when set
maxHp number or undefined No Positive integer (>= 1) when set
currentHp number or undefined No Integer in [0, maxHp] when set; requires maxHp

Invariants:

  • If maxHp is undefined, currentHp must also be undefined (no current HP without a max).
  • If maxHp is defined, currentHp must be defined and satisfy 0 <= currentHp <= maxHp.
  • When maxHp is first set, currentHp defaults to maxHp (full health).
  • When maxHp changes and currentHp === maxHp (full health), currentHp stays synced to the new maxHp.
  • When maxHp changes and currentHp < maxHp (not full health), currentHp is unchanged unless it exceeds the new maxHp, in which case it is clamped.
  • When maxHp is cleared (set to undefined), currentHp is also cleared.

Encounter (unchanged structure)

No structural changes. The encounter continues to hold readonly combatants: readonly Combatant[], activeIndex, and roundNumber. HP state lives on individual combatants.

Domain Events (new)

MaxHpSet

Emitted when a combatant's max HP is set, changed, or cleared.

Field Type Description
type "MaxHpSet" Event discriminant
combatantId CombatantId Target combatant
previousMaxHp number or undefined Max HP before change
newMaxHp number or undefined Max HP after change
previousCurrentHp number or undefined Current HP before change
newCurrentHp number or undefined Current HP after change (may differ due to clamping)

CurrentHpAdjusted

Emitted when a combatant's current HP is adjusted via +/- or direct entry.

Field Type Description
type "CurrentHpAdjusted" Event discriminant
combatantId CombatantId Target combatant
previousHp number Current HP before adjustment
newHp number Current HP after adjustment (clamped)
delta number Requested change amount (positive = heal, negative = damage)

State Transitions

setHp(encounter, combatantId, maxHp)

Input:  Encounter, CombatantId, number | undefined
Output: { encounter: Encounter, events: [MaxHpSet] } | DomainError

- combatant not found       → DomainError("combatant-not-found")
- maxHp <= 0 or non-integer → DomainError("invalid-max-hp")
- maxHp = undefined         → clear both maxHp and currentHp
- maxHp = N (new)           → set maxHp=N, currentHp=N
- maxHp = N (changed, at full health: currentHp=prevMaxHp) → set maxHp=N, currentHp=N
- maxHp = N (changed, not full health) → set maxHp=N, currentHp=min(currentHp, N)

adjustHp(encounter, combatantId, delta)

Input:  Encounter, CombatantId, number
Output: { encounter: Encounter, events: [CurrentHpAdjusted] } | DomainError

- combatant not found        → DomainError("combatant-not-found")
- combatant has no maxHp set → DomainError("no-hp-tracking")
- delta = 0                  → DomainError("zero-delta")
- delta non-integer          → DomainError("invalid-delta")
- result                     → currentHp = clamp(currentHp + delta, 0, maxHp)