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>
This commit is contained in:
141
specs/005-player-characters/data-model.md
Normal file
141
specs/005-player-characters/data-model.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user