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

155 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- [x] T001 [P] Add `readonly isConcentrating?: boolean` field to the `Combatant` interface in `packages/domain/src/types.ts`
- [x] 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.
- [x] 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.
- [x] 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.
- [x] T005 [P] Re-export `toggleConcentration` function and `ConcentrationStarted`/`ConcentrationEnded` event types from `packages/domain/src/index.ts`
- [x] 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).
- [x] T007 [P] Re-export `toggleConcentrationUseCase` from `packages/application/src/index.ts`
- [x] 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
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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
- [x] 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
- [x] 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.
- [x] 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`.
- [x] 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.
- [x] T017 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues
- [x] 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