Files
initiative/specs/008-persist-encounter/data-model.md

51 lines
1.8 KiB
Markdown

# Data Model: Persist Encounter
## Existing Entities (unchanged)
### Combatant
| Field | Type | Notes |
|-------|------|-------|
| id | CombatantId (branded string) | Unique identifier |
| name | string | Display name |
| initiative | number or undefined | Initiative value, optional |
#### ID Format Convention (existing code)
- Demo combatants use plain numeric IDs: `"1"`, `"2"`, `"3"`
- User-added combatants use the pattern `c-{N}` (e.g., `"c-1"`, `"c-2"`)
- On reload, `nextId` counter is derived by scanning existing IDs matching `c-{N}` and starting from `max(N) + 1`
- IDs not matching `c-{N}` (e.g., demo IDs) are ignored during counter derivation
### Encounter
| Field | Type | Notes |
|-------|------|-------|
| combatants | readonly Combatant[] | Ordered list of combatants |
| activeIndex | number | Index of the combatant whose turn it is |
| roundNumber | number | Current round (positive integer) |
## Persisted State
### Storage Key
`"initiative:encounter"`
### Serialized Format
The `Encounter` object is serialized as-is via `JSON.stringify`. The branded `CombatantId` serializes as a plain string. On deserialization, IDs are rehydrated with `combatantId()`.
### Validation on Load
1. Parse JSON string into unknown value
2. Structural check: verify it is an object with `combatants` (array), `activeIndex` (number), `roundNumber` (number)
3. Verify each combatant has `id` (string) and `name` (string)
4. Pass through `createEncounter` to enforce domain invariants
5. On any failure: discard and return `null` (caller falls back to demo encounter)
### State Transitions
```
App Load
├─ localStorage has valid data → restore Encounter
└─ localStorage empty/invalid → create demo Encounter
State Change (any use case)
└─ new Encounter saved to React state
└─ useEffect triggers → serialize to localStorage
```