Implement the 018-combatant-concentration feature that adds a per-combatant concentration toggle with Brain icon, purple border accent, and damage pulse animation in the encounter tracker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
154
specs/018-combatant-concentration/tasks.md
Normal file
154
specs/018-combatant-concentration/tasks.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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 (T001–T008)
|
||||
2. Complete Phase 2: User Story 1 (T009–T012)
|
||||
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
|
||||
Reference in New Issue
Block a user