59 lines
2.3 KiB
Markdown
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`.
|