# 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