Files
initiative/specs/018-combatant-concentration/tasks.md

9.8 KiB
Raw Blame History

Tasks: Combatant Concentration

Input: Design documents from /specs/018-combatant-concentration/ Prerequisites: plan.md (required), spec.md (required), research.md, data-model.md, quickstart.md

Tests: Domain tests included (testing strategy from quickstart.md). UI behavior verified manually.

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: Foundational (Domain + Application Layer)

Purpose: Core domain type change, domain function, events, use case, persistence, and re-exports that ALL user stories depend on.

Note: No setup phase needed — existing project with all dependencies in place.

  • T001 [P] Add readonly isConcentrating?: boolean field to the Combatant interface in packages/domain/src/types.ts
  • T002 [P] Add ConcentrationStarted and ConcentrationEnded event interfaces to packages/domain/src/events.ts and include them in the DomainEvent union type. Each event has type and combatantId fields only.
  • T003 Create packages/domain/src/toggle-concentration.ts — pure function toggleConcentration(encounter, combatantId) returning ToggleConcentrationSuccess | DomainError. Mirror the pattern in toggle-condition.ts: validate combatant exists, flip isConcentrating (falsy→true emits ConcentrationStarted, true→falsy emits ConcentrationEnded), return new encounter + events array.
  • T004 Create domain tests in packages/domain/src/__tests__/toggle-concentration.test.ts — test cases: toggle on (falsy→true, emits ConcentrationStarted), toggle off (true→falsy, emits ConcentrationEnded), combatant not found returns DomainError, original encounter is not mutated (immutability), other combatants are unaffected.
  • T005 [P] Re-export toggleConcentration function and ConcentrationStarted/ConcentrationEnded event types from packages/domain/src/index.ts
  • T006 Create packages/application/src/toggle-concentration-use-case.ts — thin orchestration: toggleConcentrationUseCase(store: EncounterStore, combatantId: CombatantId) following the pattern in toggle-condition-use-case.ts (get→call domain→check error→save→return events).
  • T007 [P] Re-export toggleConcentrationUseCase from packages/application/src/index.ts
  • T008 Add isConcentrating boolean to combatant rehydration in apps/web/src/persistence/encounter-storage.ts — extract isConcentrating from stored entry, validate it is true (else undefined), include in reconstructed combatant object. Follow the AC field pattern.

Checkpoint: Domain function tested, use case ready, persistence handles the new field. All user story implementation can now begin.


Phase 2: User Story 1 - Toggle Concentration (Priority: P1)

Goal: Brain icon button on combatant rows that toggles concentration on/off with hover-to-reveal behavior and tooltip.

Independent Test: Hover a combatant row → Brain icon appears → click toggles concentration on → icon stays visible when unhovered → click again toggles off → icon hides.

Implementation for User Story 1

  • T009 [US1] Add toggleConcentration callback to apps/web/src/hooks/use-encounter.ts — follow the toggleCondition pattern: useCallback((id: CombatantId) => { const result = toggleConcentrationUseCase(makeStore(), id); ... }), add to returned object.
  • T010 [US1] Add onToggleConcentration prop to CombatantRowProps in apps/web/src/components/combatant-row.tsx and add the Brain icon button (from lucide-react) on the left side of each combatant row. Implement hover-to-reveal: icon hidden by default (CSS opacity/visibility), visible on row hover (use existing group-hover or add group class), always visible when combatant.isConcentrating is truthy. Active state: distinct icon style (e.g., filled/colored). Muted state on hover when inactive.
  • T011 [US1] Add tooltip "Concentrating" on the Brain icon hover in apps/web/src/components/combatant-row.tsx — use a title attribute or existing tooltip pattern.
  • T012 [US1] Wire onToggleConcentration prop through from apps/web/src/App.tsx to CombatantRow, passing toggleConcentration from the useEncounter hook.

Checkpoint: User Story 1 fully functional — concentration toggles on/off via Brain icon with hover behavior and tooltip.


Phase 3: User Story 2 - Visual Feedback for Concentration (Priority: P2)

Goal: Concentrating combatant rows display a subtle colored left border accent, visually distinct from the active-turn indicator.

Independent Test: Activate concentration on a combatant → row shows colored left border → deactivate → border returns to normal.

Implementation for User Story 2

  • T013 [US2] Add concentration-specific left border accent to the combatant row wrapper in apps/web/src/components/combatant-row.tsx — when combatant.isConcentrating is truthy, apply a colored left border class (e.g., border-l-purple-400 or similar) that is visually distinct from the existing active-turn border-l-accent. Ensure the concentration border coexists with or takes precedence alongside the active-turn border when both apply.

Checkpoint: User Stories 1 AND 2 work independently — toggle + visual accent.


Phase 4: User Story 3 - Damage Pulse Alert (Priority: P3)

Goal: When a concentrating combatant takes damage, the Brain icon and row accent briefly pulse/flash to draw attention.

Independent Test: Activate concentration → apply damage via HP input → Brain icon and border pulse briefly → animation ends, returns to normal concentration appearance.

Implementation for User Story 3

  • T014 [US3] Add CSS @keyframes pulse animation to the app's stylesheet or as a Tailwind utility in apps/web/src/index.css (or inline via Tailwind arbitrary values). The animation should briefly intensify the left border color and Brain icon, lasting ~700ms.
  • T015 [US3] Add damage detection logic in apps/web/src/components/combatant-row.tsx — use a useRef to track previous currentHp value. In a useEffect, compare previous HP to current HP: if HP decreased AND combatant.isConcentrating is truthy, set a transient isPulsing state to true. Auto-clear isPulsing after animation duration (~700ms) via setTimeout.
  • T016 [US3] Apply the pulse animation class conditionally in apps/web/src/components/combatant-row.tsx — when isPulsing is true, add the pulse animation class to both the row wrapper (left border) and the Brain icon. When pulse ends (isPulsing resets to false), classes are removed and row returns to normal concentration appearance. Handle edge case: if concentration is toggled off during pulse, cancel the timeout and remove pulse immediately.

Checkpoint: All user stories functional — toggle + visual accent + damage pulse.


Phase 5: Polish & Cross-Cutting Concerns

Purpose: Validation and cleanup across all stories.

  • T017 Run pnpm check (knip + format + lint + typecheck + test) and fix any issues
  • T018 Verify concentration does NOT appear in condition tags or condition picker in apps/web/src/components/condition-tags.tsx and apps/web/src/components/condition-picker.tsx (should require no changes — just verify FR-011)

Dependencies & Execution Order

Phase Dependencies

  • Foundational (Phase 1): No dependencies — can start immediately. BLOCKS all user stories.
  • User Story 1 (Phase 2): Depends on Phase 1 completion.
  • User Story 2 (Phase 3): Depends on Phase 2 (US1) — builds on the same component with concentration state already wired.
  • User Story 3 (Phase 4): Depends on Phase 3 (US2) — pulse animates the border accent from US2.
  • Polish (Phase 5): Depends on all user stories being complete.

User Story Dependencies

  • User Story 1 (P1): Can start after Foundational — no other story dependencies.
  • User Story 2 (P2): Depends on US1 (concentration state must be toggleable to show the accent).
  • User Story 3 (P3): Depends on US2 (pulse animates the accent border from US2) and US1 (needs concentration state + icon).

Within Each User Story

  • T009 (hook) before T010 (component) — component needs the callback
  • T010 (icon) before T011 (tooltip) — tooltip is on the icon
  • T010, T011 before T012 (wiring) — App needs component props to exist

Parallel Opportunities

Within Phase 1 (Foundational):

Parallel: T001 (types.ts) + T002 (events.ts)
Then:     T003 (toggle-concentration.ts) — depends on T001, T002
Then:     T004 (tests) — depends on T003
Parallel: T005 (domain index.ts) + T006 (use case) + T008 (persistence)
Then:     T007 (app index.ts) — depends on T006

Implementation Strategy

MVP First (User Story 1 Only)

  1. Complete Phase 1: Foundational (T001T008)
  2. Complete Phase 2: User Story 1 (T009T012)
  3. STOP and VALIDATE: Toggle concentration via Brain icon works end-to-end
  4. Run pnpm check to verify no regressions

Incremental Delivery

  1. Foundational → Domain + app layer ready
  2. Add User Story 1 → Toggle works → Validate
  3. Add User Story 2 → Visual accent → Validate
  4. Add User Story 3 → Damage pulse → Validate
  5. Polish → Full quality gate pass

Notes

  • [P] tasks = different files, no dependencies
  • [Story] label maps task to specific user story for traceability
  • Concentration is NOT a condition — no changes to condition-related files
  • No localStorage migration needed — optional boolean field is backward-compatible
  • Pulse animation is purely UI-layer (CSS + React state), no domain logic
  • Commit after each phase or logical group