Files
Lukas 91703ddebc
All checks were successful
CI / check (push) Successful in 45s
CI / build-image (push) Successful in 18s
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>
2026-03-12 18:11:08 +01:00

253 lines
16 KiB
Markdown

# Tasks: Player Character Management
**Input**: Design documents from `/specs/005-player-characters/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ui-contracts.md, quickstart.md
**Tests**: Included — `pnpm check` merge gate requires passing tests with coverage.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
---
## Phase 1: Setup
**Purpose**: No new project setup needed — existing monorepo structure is used. This phase handles shared type foundations.
_(No tasks — the project is already set up. Foundational tasks cover all shared work.)_
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Domain types, events, and port interface that ALL user stories depend on.
**⚠️ CRITICAL**: No user story work can begin until this phase is complete.
- [x] T001 [P] Define `PlayerCharacterId` branded type, `PlayerCharacter` interface, `PlayerColor` constrained type (10 colors), `PlayerIcon` constrained type (~15 Lucide icon identifiers), validation sets (`VALID_PLAYER_COLORS`, `VALID_PLAYER_ICONS`), and `PlayerCharacterList` aggregate type (`{ readonly characters: readonly PlayerCharacter[] }`) in `packages/domain/src/player-character-types.ts`
- [x] T002 [P] Add optional `color?: string`, `icon?: string`, and `playerCharacterId?: PlayerCharacterId` fields to the `Combatant` interface in `packages/domain/src/types.ts`. Import `PlayerCharacterId` from `player-character-types.js`.
- [x] T003 [P] Add `PlayerCharacterCreated`, `PlayerCharacterUpdated`, `PlayerCharacterDeleted` event types to the `DomainEvent` union in `packages/domain/src/events.ts` (see data-model.md for field definitions)
- [x] T004 [P] Add `PlayerCharacterStore` port interface (`getAll(): PlayerCharacter[]`, `save(characters: PlayerCharacter[]): void`) to `packages/application/src/ports.ts`
- [x] T005 Re-export all new types and functions from `packages/domain/src/index.ts` and `packages/application/src/index.ts`
- [x] T006 Update encounter storage rehydration in `apps/web/src/persistence/encounter-storage.ts` to handle the new optional `color`, `icon`, and `playerCharacterId` fields on `Combatant` (validate as optional strings, discard invalid values)
**Checkpoint**: Foundation ready — domain types defined, Combatant extended, events added, port declared.
---
## Phase 3: User Story 1 — Create & Persist Player Characters (Priority: P1) 🎯 MVP
**Goal**: Users can create player characters via a modal (name, AC, max HP, color, icon) and they persist across page reloads.
**Independent Test**: Create a player character, reload the page, verify it still exists with all attributes intact.
**Maps to**: Spec stories PC-1 (Create) and PC-2 (Persistence)
### Domain
- [x] T007 [P] [US1] Implement `createPlayerCharacter` pure function in `packages/domain/src/create-player-character.ts` — accepts `PlayerCharacter[]`, `PlayerCharacterId`, name, ac, maxHp, color, icon; returns updated list + `PlayerCharacterCreated` event or `DomainError`. Validate: name non-empty after trim, ac >= 0 integer, maxHp > 0 integer, color in `VALID_PLAYER_COLORS`, icon in `VALID_PLAYER_ICONS`.
- [x] T008 [P] [US1] Write unit tests for `createPlayerCharacter` in `packages/domain/src/__tests__/create-player-character.test.ts` — cover: valid creation, empty name, whitespace name, invalid ac, invalid maxHp, invalid color, invalid icon, event emission.
### Application
- [x] T009 [US1] Implement `createPlayerCharacterUseCase` in `packages/application/src/create-player-character-use-case.ts` — accepts `PlayerCharacterStore`, id, name, ac, maxHp, color, icon; calls domain function and `store.save()`.
### Persistence Adapter
- [x] T010 [P] [US1] Implement `savePlayerCharacters(characters: PlayerCharacter[]): void` and `loadPlayerCharacters(): PlayerCharacter[]` in `apps/web/src/persistence/player-character-storage.ts` — localStorage key `"initiative:player-characters"`, silent catch on save errors, return empty array on corrupt/missing data. Per-character rehydration with field validation (discard invalid characters).
- [x] T011 [P] [US1] Write tests for player character storage in `apps/web/src/persistence/__tests__/player-character-storage.test.ts` — cover: round-trip save/load, corrupt JSON, missing fields, invalid color/icon values, empty storage, storage errors.
### React Hook
- [x] T012 [US1] Implement `usePlayerCharacters` hook in `apps/web/src/hooks/use-player-characters.ts``useState` initialized from `loadPlayerCharacters()`, `useEffect` to persist on change, expose `characters`, `createCharacter(name, ac, maxHp, color, icon)`, and a `makeStore()` callback returning `PlayerCharacterStore`. Follow the `useEncounter` ref + effect pattern.
### UI Components
- [x] T013 [P] [US1] Create `ColorPalette` component in `apps/web/src/components/color-palette.tsx` — renders a grid of color swatches from `VALID_PLAYER_COLORS`, highlights selected color with a ring/border, accepts `value` and `onChange` props.
- [x] T014 [P] [US1] Create `IconGrid` component in `apps/web/src/components/icon-grid.tsx` — renders a grid of Lucide icons from `VALID_PLAYER_ICONS`, highlights selected icon, accepts `value` and `onChange` props. Map icon identifiers to Lucide components.
- [x] T015 [US1] Create `CreatePlayerModal` component in `apps/web/src/components/create-player-modal.tsx` — modal with Name (text), AC (number), Max HP (number) fields, `ColorPalette`, `IconGrid`, Save and Cancel buttons. Name validation error display. Truncate/ellipsize long names in preview. Props: `open`, `onClose`, `onSave(name, ac, maxHp, color, icon)`.
- [x] T016 [US1] Add "Create Player" icon button to the bottom bar in `apps/web/src/components/action-bar.tsx` (e.g., `Users` Lucide icon). Wire it to open the `CreatePlayerModal`.
- [x] T017 [US1] Wire `usePlayerCharacters` hook in `apps/web/src/App.tsx` — call hook at app level, pass `createCharacter` to the `CreatePlayerModal` via `ActionBar`.
**Checkpoint**: Users can create player characters with all attributes, and they persist across page reloads. This is the MVP.
---
## Phase 4: User Story 2 — Search & Add to Encounter (Priority: P1)
**Goal**: Player characters appear in the combatant search dropdown and can be added to an encounter with stats pre-filled.
**Independent Test**: Create a player character, type its name in the search field, select it, verify combatant is added with correct stats, color, and icon.
**Maps to**: Spec story PC-3
### Implementation
- [x] T018 [US2] Add player character search to `ActionBar` in `apps/web/src/components/action-bar.tsx` — accept a `playerCharacters` prop (or search function), filter by name substring, render a "Players" group above bestiary results in the dropdown. Hide section when no matches.
- [x] T019 [US2] Implement `addFromPlayerCharacter` callback in `apps/web/src/hooks/use-encounter.ts` — accepts a `PlayerCharacter`, creates a combatant with name, ac, maxHp, currentHp=maxHp, color, icon, and playerCharacterId. Use `resolveCreatureName` for name conflict resolution (same pattern as `addFromBestiary`).
- [x] T020 [US2] Wire search and add in `apps/web/src/App.tsx` — pass `playerCharacters` list and `addFromPlayerCharacter` handler to `ActionBar`. On player character selection, call `addFromPlayerCharacter`.
**Checkpoint**: Player characters are searchable and addable to encounters with pre-filled stats.
---
## Phase 5: User Story 3 — Visual Distinction in Combatant Row (Priority: P2)
**Goal**: Combatants from player characters display their color and icon in the initiative tracker.
**Independent Test**: Add a player character to an encounter, verify the combatant row shows the chosen icon and color accent.
**Maps to**: Spec story PC-4
### Implementation
- [x] T021 [US3] Update `CombatantRow` in `apps/web/src/components/combatant-row.tsx` — if combatant has `color` and `icon` fields, render the Lucide icon (small, tinted with color) to the left of the name. Apply a subtle color accent (e.g., left border or background tint). Ensure long names truncate with ellipsis. Map icon string identifiers to Lucide components (reuse mapping from `IconGrid`).
**Checkpoint**: Player character combatants are visually distinct from bestiary/custom combatants.
---
## Phase 6: User Story 4 — Management View (Priority: P2)
**Goal**: Users can view, edit, and delete saved player characters from a dedicated management panel.
**Independent Test**: Open management view, verify all characters listed, edit one's name and AC, delete another, verify changes persisted.
**Maps to**: Spec stories PC-5 (View), PC-6 (Edit), PC-7 (Delete)
### Domain
- [x] T022 [P] [US4] Implement `editPlayerCharacter` pure function in `packages/domain/src/edit-player-character.ts` — accepts `PlayerCharacter[]`, id, and partial update fields; returns updated list + `PlayerCharacterUpdated` event or `DomainError`. Validate changed fields same as create. Return `DomainError` if no fields actually change (no-op guard).
- [x] T023 [P] [US4] Implement `deletePlayerCharacter` pure function in `packages/domain/src/delete-player-character.ts` — accepts `PlayerCharacter[]` and id; returns updated list + `PlayerCharacterDeleted` event or `DomainError`. Error if id not found.
- [x] T024 [P] [US4] Write unit tests for `editPlayerCharacter` in `packages/domain/src/__tests__/edit-player-character.test.ts` — cover: valid edit, not-found, invalid fields, no-op edit (no fields changed), event emission.
- [x] T025 [P] [US4] Write unit tests for `deletePlayerCharacter` in `packages/domain/src/__tests__/delete-player-character.test.ts` — cover: valid delete, not-found, event emission.
### Application
- [x] T026 [P] [US4] Implement `editPlayerCharacterUseCase` in `packages/application/src/edit-player-character-use-case.ts`
- [x] T027 [P] [US4] Implement `deletePlayerCharacterUseCase` in `packages/application/src/delete-player-character-use-case.ts`
### React Hook
- [x] T028 [US4] Extend `usePlayerCharacters` hook in `apps/web/src/hooks/use-player-characters.ts` — add `editCharacter(id, updates)` and `deleteCharacter(id)` methods using the new use cases.
### UI Components
- [x] T029 [US4] Extend `CreatePlayerModal` in `apps/web/src/components/create-player-modal.tsx` to support edit mode — accept optional `playerCharacter` prop to pre-fill fields, change title to "Edit Player", save calls `editCharacter` instead of `createCharacter`.
- [x] T030 [US4] Create `PlayerManagement` component in `apps/web/src/components/player-management.tsx` — modal/panel listing all player characters (name, AC, max HP, color icon). Truncate/ellipsize long names. Each row has an edit button (opens modal in edit mode) and a delete button (`ConfirmButton` pattern from spec 001). Empty state with "Create your first player character" CTA.
- [x] T031 [US4] Add management view trigger to the UI — icon button in bottom bar or within `ActionBar` that opens `PlayerManagement`. Wire in `apps/web/src/App.tsx` with `editCharacter` and `deleteCharacter` handlers.
**Checkpoint**: Full CRUD for player characters — create, view, edit, delete all working and persisted.
---
## Phase 7: Polish & Cross-Cutting Concerns
**Purpose**: Final integration validation and cleanup.
- [x] T032 Extract shared icon identifier → Lucide component mapping into a utility (used by `IconGrid`, `CombatantRow`, and `PlayerManagement`) to avoid duplication, e.g., `apps/web/src/components/player-icon-map.ts`
- [x] T033 Run `pnpm check` (audit + knip + biome + typecheck + test/coverage + jscpd) and fix any issues
- [x] T034 Update `CLAUDE.md` to add `specs/005-player-characters/` to the current feature specs list and document the `PlayerCharacterStore` port
- [x] T035 Update `README.md` if it documents user-facing features (per constitution: features that alter what the product does must be reflected)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Foundational (Phase 2)**: No dependencies — can start immediately
- **US1 Create & Persist (Phase 3)**: Depends on Phase 2 completion
- **US2 Search & Add (Phase 4)**: Depends on Phase 3 (needs characters to exist and hook to be wired)
- **US3 Visual Distinction (Phase 5)**: Depends on Phase 4 (needs combatants with color/icon to exist)
- **US4 Management (Phase 6)**: Depends on Phase 3 (needs create flow and hook, but NOT on US2/US3)
- **Polish (Phase 7)**: Depends on all story phases complete
### User Story Dependencies
- **US1 (Create & Persist)**: Depends only on Foundational — can start immediately after Phase 2
- **US2 (Search & Add)**: Depends on US1 (needs `usePlayerCharacters` hook and characters to search)
- **US3 (Visual Distinction)**: Depends on US2 (needs combatants with color/icon fields populated)
- **US4 (Management)**: Depends on US1 only (needs hook and create flow). Can run in parallel with US2/US3.
### Within Each User Story
- Domain functions before application use cases
- Application use cases before React hooks
- React hooks before UI components
- Tests can run in parallel with their domain functions (written to same-phase files)
### Parallel Opportunities
Within Phase 2: T001, T002, T003, T004 can all run in parallel (different files)
Within US1: T007+T008 parallel with T010+T011, parallel with T013+T014
Within US4: T022-T027 can all run in parallel (different domain/application files)
---
## Parallel Example: Phase 2 (Foundational)
```
Parallel group 1:
T001: PlayerCharacter types in packages/domain/src/player-character-types.ts
T002: Combatant extension in packages/domain/src/types.ts
T003: Domain events in packages/domain/src/events.ts
T004: Port interface in packages/application/src/ports.ts
Sequential after group 1:
T005: Re-export from index files (depends on T001-T004)
T006: Encounter storage rehydration (depends on T002)
```
## Parallel Example: User Story 1
```
Parallel group 1 (after Phase 2):
T007+T008: Domain function + tests in packages/domain/src/
T010+T011: Storage adapter + tests in apps/web/src/persistence/
T013: ColorPalette component
T014: IconGrid component
Sequential after group 1:
T009: Application use case (depends on T007)
T012: React hook (depends on T009, T010)
T015: CreatePlayerModal (depends on T013, T014)
T016: ActionBar button (depends on T015)
T017: App.tsx wiring (depends on T012, T016)
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 2: Foundational types and ports
2. Complete Phase 3: US1 — Create + Persist
3. **STOP and VALIDATE**: Create a player character, reload page, verify persistence
4. This alone delivers value — users can save their party for reuse
### Incremental Delivery
1. Phase 2 → Foundation ready
2. US1 → Create & persist player characters (MVP!)
3. US2 → Search and add to encounters (core value unlocked)
4. US3 → Visual distinction in rows (UX polish)
5. US4 → Edit and delete (full CRUD) — can happen in parallel with US2/US3
6. Phase 7 → Polish and validation
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Commit after each phase or logical group of tasks
- Run `pnpm check` at each checkpoint to catch regressions early
- The icon identifier → Lucide component mapping will be needed in 3 places (T014, T021, T030) — T032 extracts it to avoid duplication, but initial implementations can inline it