Implement the 009-combatant-hp feature that adds optional max HP and current HP tracking per combatant with +/- controls, direct entry, and persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-05 17:18:03 +01:00
parent a9c280a6d6
commit 8185fde0e8
21 changed files with 1367 additions and 2 deletions

View File

@@ -0,0 +1,156 @@
# Tasks: Combatant HP Tracking
**Input**: Design documents from `/specs/009-combatant-hp/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md
**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 (Shared Infrastructure)
**Purpose**: Extend domain types and events shared by all user stories
- [x] T001 Extend `Combatant` interface with optional `maxHp` and `currentHp` fields in `packages/domain/src/types.ts`
- [x] T002 Add `MaxHpSet` and `CurrentHpAdjusted` event types to `DomainEvent` union in `packages/domain/src/events.ts`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Domain pure functions that all user stories depend on
**CRITICAL**: No user story work can begin until this phase is complete
- [x] T003 Implement `setHp` pure function in `packages/domain/src/set-hp.ts` — accepts (Encounter, CombatantId, maxHp: number | undefined), returns {encounter, events} | DomainError. Handles: set new maxHp (currentHp defaults to maxHp), update maxHp (full-health sync: if currentHp === previousMaxHp then currentHp = newMaxHp; otherwise clamp currentHp), clear maxHp (clear both), validate positive integer, combatant-not-found error
- [x] T004 Write tests for `setHp` in `packages/domain/src/__tests__/set-hp.test.ts` — cover acceptance scenarios (set, update, full-health sync on increase, clamp on reduce, clear), invariants (pure, immutable, event shape), error cases (not found, invalid values), edge cases (maxHp=1, reduce below currentHp)
- [x] T005 Implement `adjustHp` pure function in `packages/domain/src/adjust-hp.ts` — accepts (Encounter, CombatantId, delta: number), returns {encounter, events} | DomainError. Clamps result to [0, maxHp]. Errors: combatant-not-found, no-hp-tracking, zero-delta, invalid-delta
- [x] T006 Write tests for `adjustHp` in `packages/domain/src/__tests__/adjust-hp.test.ts` — cover acceptance scenarios (+1, -1, clamp at 0, clamp at max), invariants (pure, immutable, event shape with delta), error cases, edge cases (large delta beyond bounds)
- [x] T007 Re-export `setHp` and `adjustHp` from `packages/domain/src/index.ts`
- [x] T008 [P] Create `setHpUseCase` in `packages/application/src/set-hp-use-case.ts` following get-call-save pattern via `EncounterStore`
- [x] T009 [P] Create `adjustHpUseCase` in `packages/application/src/adjust-hp-use-case.ts` following get-call-save pattern via `EncounterStore`
- [x] T010 Re-export new use cases from `packages/application/src/index.ts`
**Checkpoint**: Domain and application layers complete. `pnpm test` and `pnpm typecheck` pass.
---
## Phase 3: User Story 1 — Set Max HP for a Combatant (Priority: P1) + User Story 2 — Quick Adjust Current HP (Priority: P1) MVP
**Goal**: A game master can set max HP on a combatant and use +/- controls to adjust current HP during combat. These two P1 stories are combined because the UI naturally presents them together (max HP input + current HP with +/- buttons in one combatant row).
**Independent Test**: Add a combatant, set max HP, verify it displays. Press -/+ buttons, verify current HP changes within bounds. Reduce max HP below current HP, verify clamping.
### Implementation
- [x] T011 [US1] [US2] Add `setHp` and `adjustHp` callbacks to `useEncounter` hook in `apps/web/src/hooks/use-encounter.ts` — follow existing pattern (call use case, check error, update events)
- [x] T012 [US1] [US2] Add HP controls to combatant rows in `apps/web/src/App.tsx` — both Current HP and Max HP inputs always visible. Current HP input disabled when maxHp is undefined. +/- buttons only shown when HP tracking is active. Max HP input uses local draft state and commits on blur/Enter only (not per-keystroke) to prevent premature clearing of currentHp. Max HP input allows clearing (returning combatant to no-HP state). Clamp visual state matches domain invariants.
**Checkpoint**: US1 + US2 fully functional. User can set max HP, see current HP, and use +/- buttons. `pnpm check` passes.
---
## Phase 4: User Story 3 — Direct HP Entry (Priority: P2)
**Goal**: A game master can type a specific current HP value directly instead of using +/- buttons.
**Independent Test**: Set max HP to 50, type 35 in current HP field, verify it updates. Type 60, verify clamped to 50. Type -5, verify clamped to 0.
### Implementation
- [x] T013 [US3] Make current HP display editable (click-to-edit or inline input) in `apps/web/src/App.tsx` — on confirm, compute delta from current value and call `adjustHp`. Apply clamping via domain function.
**Checkpoint**: US3 functional. Direct numeric entry works alongside +/- controls. `pnpm check` passes.
---
## Phase 5: User Story 4 — HP Persists Across Reloads (Priority: P2)
**Goal**: HP values survive page reloads via existing localStorage persistence.
**Independent Test**: Set max HP and adjust current HP, reload the page, verify both values are restored.
### Implementation
- [x] T014 [US4] Extend `loadEncounter()` validation in `apps/web/src/persistence/encounter-storage.ts` — validate optional `maxHp` (positive integer) and `currentHp` (integer in [0, maxHp]) on each combatant during deserialization. Strip invalid HP fields per-combatant rather than failing the entire encounter.
**Checkpoint**: US4 functional. HP values persist across reloads. `pnpm check` passes.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Final validation and cleanup
- [x] T015 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues
- [x] T016 Verify layer boundary test still passes in `packages/domain/src/__tests__/layer-boundaries.test.ts`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — start immediately
- **Foundational (Phase 2)**: Depends on Phase 1 completion — BLOCKS all user stories
- **US1+US2 (Phase 3)**: Depends on Phase 2 completion
- **US3 (Phase 4)**: Depends on Phase 3 (extends the HP UI from US1+US2)
- **US4 (Phase 5)**: Can start after Phase 2 (independent of UI work), but naturally follows Phase 3
- **Polish (Phase 6)**: Depends on all previous phases
### Within Each Phase
- T001 and T002 are parallel (different files)
- T003 → T004 (implement then test setHp)
- T005 → T006 (implement then test adjustHp)
- T003 and T005 are parallel (different files, no dependency)
- T008 and T009 are parallel (different files)
- T008/T009 depend on T007 (need exports)
- T011 → T012 (hook before UI)
- T013 depends on T012 (extends existing HP UI)
### Parallel Opportunities
```text
Parallel group A (Phase 1): T001 || T002
Parallel group B (Phase 2): T003+T004 || T005+T006 (then T007, then T008 || T009, then T010)
Sequential (Phase 3): T011 → T012
Sequential (Phase 4): T013
Independent (Phase 5): T014
```
---
## Implementation Strategy
### MVP First (US1 + US2)
1. Complete Phase 1: Setup (types + events)
2. Complete Phase 2: Foundational (domain functions + use cases)
3. Complete Phase 3: US1 + US2 (set max HP + quick adjust)
4. **STOP and VALIDATE**: Can set HP and use +/- controls
5. Continue with US3 (direct entry) and US4 (persistence)
### Incremental Delivery
1. Phase 1 + 2 → Domain + application ready
2. Phase 3 → MVP: max HP + quick adjust functional
3. Phase 4 → Direct HP entry added
4. Phase 5 → Persistence extended
5. Phase 6 → Quality gate passes, ready to merge
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Commit after each phase checkpoint
- US1 and US2 are combined in Phase 3 because they share UI surface (HP controls on combatant row)
- Domain functions are designed for extensibility: `adjustHp` accepts any integer delta, so a future damage/heal dialog can call it directly