3.4 KiB
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
maxHpis undefined,currentHpmust also be undefined (no current HP without a max). - If
maxHpis defined,currentHpmust be defined and satisfy0 <= currentHp <= maxHp. - When
maxHpis first set,currentHpdefaults tomaxHp(full health). - When
maxHpchanges andcurrentHp === maxHp(full health),currentHpstays synced to the newmaxHp. - When
maxHpchanges andcurrentHp < maxHp(not full health),currentHpis unchanged unless it exceeds the newmaxHp, in which case it is clamped. - When
maxHpis cleared (set to undefined),currentHpis 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)