# 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`.