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>
4.4 KiB
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 +
PlayerCharacterCreatedevent | 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 +
PlayerCharacterUpdatedevent | 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 +
PlayerCharacterDeletedevent | 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:
[
{
"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.