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.7 KiB
4.7 KiB
Research: Player Character Management
Branch: 005-player-characters | Date: 2026-03-12
Key Decisions
1. PlayerCharacter as a separate domain entity
- Decision:
PlayerCharacteris a new type in the domain layer, distinct fromCombatant. When added to an encounter, a snapshot is copied into aCombatant. - Rationale: Player characters are persistent templates reused across encounters; combatants are ephemeral per-encounter instances. Mixing them would violate the single-responsibility of the
Encounteraggregate. - Alternatives considered: Extending
Combatantwith aisPlayerCharacterflag — rejected because combatants belong to encounters and are cleared with them, while player characters must survive encounter clears.
2. Combatant type extension for color and icon
- Decision: Add optional
color?: stringandicon?: stringfields to the existingCombatantinterface, plusplayerCharacterId?: PlayerCharacterIdto track origin. - Rationale: The combatant row needs to render color/icon. Storing these on the combatant (copied from the player character at add-time) keeps the combatant self-contained and avoids runtime lookups against the player character store.
- Alternatives considered: Looking up color/icon from the player character store at render time — rejected because the player character might be deleted or edited after the combatant was added, and the spec says combatants are independent copies.
3. Storage: separate localStorage key
- Decision: Use
localStoragewith key"initiative:player-characters", separate from encounter storage ("initiative:encounter"). - Rationale: Follows existing pattern (encounter uses its own key). Player characters must survive encounter clears. IndexedDB is overkill for a small list of player characters.
- Alternatives considered: IndexedDB (like bestiary cache) — rejected as overly complex for simple JSON list. Shared key with encounter — rejected because clearing encounter would wipe player characters.
4. Search integration approach
- Decision: The
useBestiarysearch hook or a newusePlayerCharactershook provides player character search results. TheActionBardropdown renders a "Players" group above bestiary results. - Rationale: Player character search is a simple substring match on a small in-memory list — no index needed. Keeping it separate from bestiary search maintains separation of concerns.
- Alternatives considered: Merging into the bestiary index — rejected because player characters are user-created, not part of the pre-built index.
5. Color palette and icon set
- Decision: Use a fixed set of 10 distinguishable colors and ~15 Lucide icons already available in the project.
- Rationale: Lucide React is already a dependency. A fixed palette ensures visual consistency and simplifies the domain model (color is a string enum, not arbitrary hex).
- Alternatives considered: Arbitrary hex color picker — rejected for MVP as it complicates UX and validation.
6. Domain operations pattern
- Decision: Player character CRUD follows the same pattern as encounter operations: pure functions returning
{result, events} | DomainError. New domain events:PlayerCharacterCreated,PlayerCharacterUpdated,PlayerCharacterDeleted. - Rationale: Consistency with existing domain patterns. Events enable future features (undo, audit).
- Alternatives considered: Simpler CRUD without events — rejected for consistency with the project's event-driven domain.
7. Management view location
- Decision: A new icon button in the bottom bar (alongside the existing search) opens a player character management panel/modal.
- Rationale: The bottom bar already serves as the primary action area. A modal keeps the management view accessible without adding routing complexity.
- Alternatives considered: A separate route/page — rejected because the app is currently a single-page encounter tracker with no routing.
Cross-feature impacts
Spec 001 (Combatant Management)
Combatanttype gains three optional fields:color,icon,playerCharacterIdencounter-storage.tsrehydration needs to handle new optional fields- Combatant row component needs to render color/icon when present
Spec 003 (Combatant State)
- No changes needed. AC and HP management already works on optional fields that player characters pre-fill.
Spec 004 (Bestiary)
- ActionBar dropdown gains a "Players" section above bestiary results
addFromBestiarypattern informs the newaddFromPlayerCharacterflow- No changes to bestiary search itself
Unresolved items
None. All technical decisions are resolved.