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>
142 lines
4.4 KiB
Markdown
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.
|