Files
initiative/specs/005-player-characters/data-model.md
Lukas 91703ddebc
All checks were successful
CI / check (push) Successful in 45s
CI / build-image (push) Successful in 18s
Add player character management feature
Persistent player character templates (name, AC, HP, color, icon) with
full CRUD, bestiary-style search to add PCs to encounters with pre-filled
stats, and color/icon visual distinction in combatant rows. Also stops
the stat block panel from auto-opening when adding a creature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:11:08 +01:00

142 lines
4.4 KiB
Markdown

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