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