Files
initiative/specs/016-combatant-ac/data-model.md

59 lines
2.3 KiB
Markdown

# Data Model: Combatant Armor Class Display
**Feature**: 016-combatant-ac | **Date**: 2026-03-06
## Entity Changes
### Combatant (modified)
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| id | CombatantId (branded string) | Yes | Non-empty | Existing — unchanged |
| name | string | Yes | Non-empty after trim | Existing — unchanged |
| initiative | number \| undefined | No | Integer | Existing — unchanged |
| maxHp | number \| undefined | No | Integer >= 1 | Existing — unchanged |
| currentHp | number \| undefined | No | Integer >= 0, <= maxHp | Existing — unchanged |
| **ac** | **number \| undefined** | **No** | **Integer >= 0** | **NEW — Armor Class** |
### Key Differences from Other Optional Fields
- Unlike `maxHp`/`currentHp`, AC has no paired or derived field — it is a single standalone value.
- Unlike `initiative`, AC does not affect combatant sort order.
- AC validation is `>= 0` (not `>= 1` like `maxHp`), because AC 0 is valid in tabletop RPGs.
## Domain Events
### AcSet (new)
| Field | Type | Description |
|-------|------|-------------|
| type | `"AcSet"` | Event discriminant |
| combatantId | CombatantId | Target combatant |
| previousAc | number \| undefined | AC before the change |
| newAc | number \| undefined | AC after the change |
## State Transitions
### setAc(encounter, combatantId, ac)
**Input**: Encounter, CombatantId, ac: number | undefined
**Validation**:
- Combatant must exist in encounter (error: `"combatant-not-found"`)
- If `ac` is defined: must be a non-negative integer (error: `"invalid-ac"`)
**Behavior**:
- Replaces the combatant's `ac` field with the new value (or `undefined` to clear)
- No side effects on other fields (unlike `setHp` which initializes `currentHp`)
- No reordering (unlike `setInitiative` which re-sorts)
**Output**: `{ encounter: Encounter, events: [AcSet] }` or `DomainError`
## Persistence Format
### localStorage JSON (unchanged key: `"initiative:encounter"`)
Combatant objects are serialized as plain JSON. The `ac` field is included when defined, omitted when `undefined` (standard JSON behavior).
**Rehydration validation**: `typeof entry.ac === "number" && Number.isInteger(entry.ac) && entry.ac >= 0` — invalid values are silently discarded as `undefined`.