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>
67 lines
4.7 KiB
Markdown
67 lines
4.7 KiB
Markdown
# Research: Player Character Management
|
|
|
|
**Branch**: `005-player-characters` | **Date**: 2026-03-12
|
|
|
|
## Key Decisions
|
|
|
|
### 1. PlayerCharacter as a separate domain entity
|
|
|
|
- **Decision**: `PlayerCharacter` is a new type in the domain layer, distinct from `Combatant`. When added to an encounter, a snapshot is copied into a `Combatant`.
|
|
- **Rationale**: Player characters are persistent templates reused across encounters; combatants are ephemeral per-encounter instances. Mixing them would violate the single-responsibility of the `Encounter` aggregate.
|
|
- **Alternatives considered**: Extending `Combatant` with a `isPlayerCharacter` flag — 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?: string` and `icon?: string` fields to the existing `Combatant` interface, plus `playerCharacterId?: PlayerCharacterId` to 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 `localStorage` with 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 `useBestiary` search hook or a new `usePlayerCharacters` hook provides player character search results. The `ActionBar` dropdown 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)
|
|
- `Combatant` type gains three optional fields: `color`, `icon`, `playerCharacterId`
|
|
- `encounter-storage.ts` rehydration 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
|
|
- `addFromBestiary` pattern informs the new `addFromPlayerCharacter` flow
|
|
- No changes to bestiary search itself
|
|
|
|
## Unresolved items
|
|
|
|
None. All technical decisions are resolved.
|