Files
initiative/specs/009-combatant-hp/tasks.md

7.9 KiB

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

  • T001 Extend Combatant interface with optional maxHp and currentHp fields in packages/domain/src/types.ts
  • 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

  • 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
  • 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)
  • 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
  • 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)
  • T007 Re-export setHp and adjustHp from packages/domain/src/index.ts
  • T008 [P] Create setHpUseCase in packages/application/src/set-hp-use-case.ts following get-call-save pattern via EncounterStore
  • T009 [P] Create adjustHpUseCase in packages/application/src/adjust-hp-use-case.ts following get-call-save pattern via EncounterStore
  • 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

  • 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)
  • 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

  • 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

  • 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

  • T015 Run pnpm check (knip + format + lint + typecheck + test) and fix any issues
  • 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

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