9.8 KiB
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?: booleanfield to theCombatantinterface inpackages/domain/src/types.ts - T002 [P] Add
ConcentrationStartedandConcentrationEndedevent interfaces topackages/domain/src/events.tsand include them in theDomainEventunion type. Each event hastypeandcombatantIdfields only. - T003 Create
packages/domain/src/toggle-concentration.ts— pure functiontoggleConcentration(encounter, combatantId)returningToggleConcentrationSuccess | DomainError. Mirror the pattern intoggle-condition.ts: validate combatant exists, flipisConcentrating(falsy→true emitsConcentrationStarted, true→falsy emitsConcentrationEnded), 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
toggleConcentrationfunction andConcentrationStarted/ConcentrationEndedevent types frompackages/domain/src/index.ts - T006 Create
packages/application/src/toggle-concentration-use-case.ts— thin orchestration:toggleConcentrationUseCase(store: EncounterStore, combatantId: CombatantId)following the pattern intoggle-condition-use-case.ts(get→call domain→check error→save→return events). - T007 [P] Re-export
toggleConcentrationUseCasefrompackages/application/src/index.ts - T008 Add
isConcentratingboolean to combatant rehydration inapps/web/src/persistence/encounter-storage.ts— extractisConcentratingfrom stored entry, validate it istrue(elseundefined), 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
toggleConcentrationcallback toapps/web/src/hooks/use-encounter.ts— follow thetoggleConditionpattern:useCallback((id: CombatantId) => { const result = toggleConcentrationUseCase(makeStore(), id); ... }), add to returned object. - T010 [US1] Add
onToggleConcentrationprop toCombatantRowPropsinapps/web/src/components/combatant-row.tsxand add the Brain icon button (fromlucide-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 whencombatant.isConcentratingis 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 atitleattribute or existing tooltip pattern. - T012 [US1] Wire
onToggleConcentrationprop through fromapps/web/src/App.tsxtoCombatantRow, passingtoggleConcentrationfrom theuseEncounterhook.
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— whencombatant.isConcentratingis truthy, apply a colored left border class (e.g.,border-l-purple-400or similar) that is visually distinct from the existing active-turnborder-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
@keyframespulse animation to the app's stylesheet or as a Tailwind utility inapps/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 auseRefto track previouscurrentHpvalue. In auseEffect, compare previous HP to current HP: if HP decreased ANDcombatant.isConcentratingis truthy, set a transientisPulsingstate totrue. Auto-clearisPulsingafter animation duration (~700ms) viasetTimeout. - T016 [US3] Apply the pulse animation class conditionally in
apps/web/src/components/combatant-row.tsx— whenisPulsingis true, add the pulse animation class to both the row wrapper (left border) and the Brain icon. When pulse ends (isPulsingresets 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.tsxandapps/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)
- Complete Phase 1: Foundational (T001–T008)
- Complete Phase 2: User Story 1 (T009–T012)
- STOP and VALIDATE: Toggle concentration via Brain icon works end-to-end
- Run
pnpm checkto verify no regressions
Incremental Delivery
- Foundational → Domain + app layer ready
- Add User Story 1 → Toggle works → Validate
- Add User Story 2 → Visual accent → Validate
- Add User Story 3 → Damage pulse → Validate
- 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