# Data Model: Player Character Management **Branch**: `005-player-characters` | **Date**: 2026-03-12 ## New Entities ### PlayerCharacterId (branded type) ``` PlayerCharacterId = string & { __brand: "PlayerCharacterId" } ``` Unique identifier for player characters. Generated by the application layer (same pattern as `CombatantId`). ### PlayerCharacter | Field | Type | Required | Constraints | |-------|------|----------|-------------| | id | PlayerCharacterId | yes | Unique, immutable after creation | | name | string | yes | Non-empty after trimming | | ac | number | yes | Non-negative integer | | maxHp | number | yes | Positive integer | | color | PlayerColor | yes | One of predefined color values | | icon | PlayerIcon | yes | One of predefined icon identifiers | ### PlayerColor (constrained string) Predefined set of ~10 distinguishable color identifiers: `"red" | "blue" | "green" | "purple" | "orange" | "pink" | "cyan" | "yellow" | "emerald" | "indigo"` Each maps to a specific hex/tailwind value at the adapter layer. ### PlayerIcon (constrained string) Predefined set of ~15 Lucide icon identifiers: `"sword" | "shield" | "skull" | "heart" | "wand" | "flame" | "crown" | "star" | "moon" | "sun" | "axe" | "crosshair" | "eye" | "feather" | "zap"` ### PlayerCharacterList (aggregate) | Field | Type | Constraints | |-------|------|-------------| | characters | readonly PlayerCharacter[] | May be empty. IDs unique within list. | ## Modified Entities ### Combatant (extended) Three new optional fields added: | Field | Type | Required | Constraints | |-------|------|----------|-------------| | color | string | no | Copied from PlayerCharacter at add-time | | icon | string | no | Copied from PlayerCharacter at add-time | | playerCharacterId | PlayerCharacterId | no | Reference to source player character (informational only) | These fields are set when a combatant is created from a player character. They are immutable snapshots — editing the source player character does not update existing combatants. ## State Transitions ### createPlayerCharacter - **Input**: PlayerCharacterList + name + ac + maxHp + color + icon + id - **Output**: Updated PlayerCharacterList + `PlayerCharacterCreated` event | DomainError - **Validation**: name non-empty after trim, ac >= 0 integer, maxHp > 0 integer, color in set, icon in set - **Errors**: `invalid-name`, `invalid-ac`, `invalid-max-hp`, `invalid-color`, `invalid-icon` ### editPlayerCharacter - **Input**: PlayerCharacterList + id + partial fields (name?, ac?, maxHp?, color?, icon?) - **Output**: Updated PlayerCharacterList + `PlayerCharacterUpdated` event | DomainError - **Validation**: Same as create for any provided field. At least one field must change. - **Errors**: `player-character-not-found`, `invalid-name`, `invalid-ac`, `invalid-max-hp`, `invalid-color`, `invalid-icon` ### deletePlayerCharacter - **Input**: PlayerCharacterList + id - **Output**: Updated PlayerCharacterList + `PlayerCharacterDeleted` event | DomainError - **Validation**: ID must exist in list - **Errors**: `player-character-not-found` ## Domain Events ### PlayerCharacterCreated | Field | Type | |-------|------| | type | `"PlayerCharacterCreated"` | | playerCharacterId | PlayerCharacterId | | name | string | ### PlayerCharacterUpdated | Field | Type | |-------|------| | type | `"PlayerCharacterUpdated"` | | playerCharacterId | PlayerCharacterId | | oldName | string | | newName | string | ### PlayerCharacterDeleted | Field | Type | |-------|------| | type | `"PlayerCharacterDeleted"` | | playerCharacterId | PlayerCharacterId | | name | string | ## Persistence Schema ### localStorage key: `"initiative:player-characters"` JSON array of serialized `PlayerCharacter` objects: ```json [ { "id": "pc_abc123", "name": "Aragorn", "ac": 16, "maxHp": 120, "color": "green", "icon": "sword" } ] ``` ### Rehydration rules - Parse JSON array; discard entire store on parse failure (return empty list) - Per-character validation: discard individual characters that fail validation - Required string fields: must be non-empty strings - Required number fields: must match domain constraints (ac >= 0, maxHp > 0) - Color/icon: must be members of the predefined sets; discard character if invalid ## Port Interface ### PlayerCharacterStore ``` getAll(): PlayerCharacter[] save(characters: PlayerCharacter[]): void ``` Synchronous, matching the `EncounterStore` pattern. Implementation: localStorage adapter.