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:
34
specs/018-combatant-concentration/checklists/requirements.md
Normal file
34
specs/018-combatant-concentration/checklists/requirements.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Specification Quality Checklist: Combatant Concentration
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-03-06
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
|
||||
45
specs/018-combatant-concentration/data-model.md
Normal file
45
specs/018-combatant-concentration/data-model.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Data Model: Combatant Concentration
|
||||
|
||||
**Feature**: 018-combatant-concentration | **Date**: 2026-03-06
|
||||
|
||||
## Entity Changes
|
||||
|
||||
### Combatant (modified)
|
||||
|
||||
| Field | Type | Required | Default | Notes |
|
||||
|-------|------|----------|---------|-------|
|
||||
| id | CombatantId | yes | - | Existing, unchanged |
|
||||
| name | string | yes | - | Existing, unchanged |
|
||||
| initiative | number | no | undefined | Existing, unchanged |
|
||||
| maxHp | number | no | undefined | Existing, unchanged |
|
||||
| currentHp | number | no | undefined | Existing, unchanged |
|
||||
| ac | number | no | undefined | Existing, unchanged |
|
||||
| conditions | ConditionId[] | no | undefined | Existing, unchanged |
|
||||
| **isConcentrating** | **boolean** | **no** | **undefined (falsy)** | **New field. Independent of conditions.** |
|
||||
|
||||
### Domain Events (new)
|
||||
|
||||
| Event | Fields | Emitted When |
|
||||
|-------|--------|-------------|
|
||||
| ConcentrationStarted | `type`, `combatantId` | Concentration toggled from off to on |
|
||||
| ConcentrationEnded | `type`, `combatantId` | Concentration toggled from on to off |
|
||||
|
||||
## State Transitions
|
||||
|
||||
```
|
||||
toggleConcentration(encounter, combatantId)
|
||||
├── combatant not found → DomainError("combatant-not-found")
|
||||
├── isConcentrating is falsy → set to true, emit ConcentrationStarted
|
||||
└── isConcentrating is true → set to undefined, emit ConcentrationEnded
|
||||
```
|
||||
|
||||
## Validation Rules
|
||||
|
||||
- `combatantId` must reference an existing combatant in the encounter.
|
||||
- No other validation needed (boolean toggle has no invalid input beyond missing combatant).
|
||||
|
||||
## Storage Impact
|
||||
|
||||
- **Format**: JSON via localStorage (existing adapter).
|
||||
- **Migration**: None. Field is optional; absent field is treated as `false`.
|
||||
- **Backward compatibility**: Old data loads without `isConcentrating`; new data with the field serializes/deserializes transparently.
|
||||
73
specs/018-combatant-concentration/plan.md
Normal file
73
specs/018-combatant-concentration/plan.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Implementation Plan: Combatant Concentration
|
||||
|
||||
**Branch**: `018-combatant-concentration` | **Date**: 2026-03-06 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `/specs/018-combatant-concentration/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Add concentration as a separate per-combatant boolean state (not a condition). A Brain icon toggle appears on hover (or stays visible when active), a colored left border accent marks concentrating combatants, and a pulse animation fires when a concentrating combatant takes damage. Implementation follows the existing toggle-condition pattern across all three layers (domain, application, web adapter).
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax)
|
||||
**Primary Dependencies**: React 19, Vite 6, Tailwind CSS v4, Lucide React (icons)
|
||||
**Storage**: Browser localStorage (existing adapter, transparent JSON serialization)
|
||||
**Testing**: Vitest
|
||||
**Target Platform**: Modern browsers (local-first, single-user)
|
||||
**Project Type**: Web application (monorepo: domain / application / web)
|
||||
**Performance Goals**: Instant UI response on toggle; pulse animation ~600-800ms
|
||||
**Constraints**: No migration needed for localStorage; new optional boolean field is backward-compatible
|
||||
**Scale/Scope**: Single-user encounter tracker; ~10-20 combatants per encounter
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Principle | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| I. Deterministic Domain Core | PASS | `toggleConcentration` is a pure function: same input encounter + combatant ID yields same output. No I/O, randomness, or clocks. |
|
||||
| II. Layered Architecture | PASS | Domain defines the toggle function; Application orchestrates via `EncounterStore` port; Web adapter implements UI + persistence. No layer violations. |
|
||||
| III. Agent Boundary | N/A | No agent/AI features in this feature. |
|
||||
| IV. Clarification-First | PASS | Spec is fully specified; no NEEDS CLARIFICATION markers. |
|
||||
| V. Escalation Gates | PASS | All work is within spec scope. |
|
||||
| VI. MVP Baseline Language | PASS | No permanent bans introduced. |
|
||||
| VII. No Gameplay Rules | PASS | Concentration is state tracking only; no automatic save mechanics or rule enforcement. |
|
||||
|
||||
All gates pass. No violations to track.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/018-combatant-concentration/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
packages/domain/src/
|
||||
├── types.ts # Add isConcentrating to Combatant
|
||||
├── events.ts # Add ConcentrationStarted/ConcentrationEnded
|
||||
├── toggle-concentration.ts # New pure domain function
|
||||
├── index.ts # Re-export new function + events
|
||||
└── __tests__/
|
||||
└── toggle-concentration.test.ts # New test file
|
||||
|
||||
packages/application/src/
|
||||
├── toggle-concentration-use-case.ts # New use case
|
||||
└── index.ts # Re-export
|
||||
|
||||
apps/web/src/
|
||||
├── hooks/use-encounter.ts # Add toggleConcentration callback
|
||||
├── components/combatant-row.tsx # Add Brain icon toggle + left border accent + pulse
|
||||
├── persistence/encounter-storage.ts # Add isConcentrating to rehydration
|
||||
└── App.tsx # Wire toggleConcentration prop
|
||||
```
|
||||
|
||||
**Structure Decision**: Follows existing monorepo layout with domain/application/web layers. Each new file mirrors the established pattern (e.g., `toggle-concentration.ts` mirrors `toggle-condition.ts`).
|
||||
48
specs/018-combatant-concentration/quickstart.md
Normal file
48
specs/018-combatant-concentration/quickstart.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Quickstart: Combatant Concentration
|
||||
|
||||
**Feature**: 018-combatant-concentration | **Date**: 2026-03-06
|
||||
|
||||
## Overview
|
||||
|
||||
This feature adds a per-combatant boolean `isConcentrating` state with a Brain icon toggle, a colored left border accent for visual identification, and a pulse animation when a concentrating combatant takes damage.
|
||||
|
||||
## Key Files to Modify
|
||||
|
||||
### Domain Layer
|
||||
1. **`packages/domain/src/types.ts`** — Add `isConcentrating?: boolean` to `Combatant` interface.
|
||||
2. **`packages/domain/src/events.ts`** — Add `ConcentrationStarted` and `ConcentrationEnded` event types to the `DomainEvent` union.
|
||||
3. **`packages/domain/src/toggle-concentration.ts`** (new) — Pure function mirroring `toggle-condition.ts` pattern.
|
||||
4. **`packages/domain/src/index.ts`** — Re-export new function and event types.
|
||||
|
||||
### Application Layer
|
||||
5. **`packages/application/src/toggle-concentration-use-case.ts`** (new) — Thin orchestration following `toggle-condition-use-case.ts` pattern.
|
||||
6. **`packages/application/src/index.ts`** — Re-export new use case.
|
||||
|
||||
### Web Adapter
|
||||
7. **`apps/web/src/persistence/encounter-storage.ts`** — Add `isConcentrating` to combatant rehydration.
|
||||
8. **`apps/web/src/hooks/use-encounter.ts`** — Add `toggleConcentration` callback.
|
||||
9. **`apps/web/src/components/combatant-row.tsx`** — Add Brain icon toggle, left border accent, and pulse animation.
|
||||
10. **`apps/web/src/App.tsx`** — Wire `onToggleConcentration` prop through to `CombatantRow`.
|
||||
|
||||
## Implementation Pattern
|
||||
|
||||
Follow the existing `toggleCondition` pattern end-to-end:
|
||||
|
||||
```
|
||||
Domain: toggleConcentration(encounter, combatantId) → { encounter, events } | DomainError
|
||||
App: toggleConcentrationUseCase(store, combatantId) → events | DomainError
|
||||
Hook: toggleConcentration = useCallback((id) => { ... toggleConcentrationUseCase(makeStore(), id) ... })
|
||||
Component: <Brain onClick={() => onToggleConcentration(id)} />
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Domain tests**: `toggle-concentration.test.ts` — toggle on/off, combatant not found, immutability, correct events emitted.
|
||||
- **UI behavior**: Manual verification of hover show/hide, tooltip, left border accent, pulse animation on damage.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- Concentration is **not** a condition — it has its own boolean field and separate UI treatment.
|
||||
- Pulse animation uses **CSS keyframes** triggered by transient React state, not a domain event.
|
||||
- Damage detection for pulse uses **HP comparison** in the component (prevHp vs currentHp), not domain events.
|
||||
- No localStorage migration needed — optional boolean field is backward-compatible.
|
||||
50
specs/018-combatant-concentration/research.md
Normal file
50
specs/018-combatant-concentration/research.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Research: Combatant Concentration
|
||||
|
||||
**Feature**: 018-combatant-concentration | **Date**: 2026-03-06
|
||||
|
||||
## R1: Domain Toggle Pattern
|
||||
|
||||
**Decision**: Mirror the `toggleCondition` pattern with a simpler `toggleConcentration` pure function.
|
||||
|
||||
**Rationale**: `toggleCondition` (in `packages/domain/src/toggle-condition.ts`) is the closest analogue. It takes an encounter + combatant ID, validates the combatant exists, toggles the state, and returns a new encounter + domain events. Concentration is simpler because it's a boolean (no condition ID validation needed).
|
||||
|
||||
**Alternatives considered**:
|
||||
- Reuse condition system with a special "concentration" condition ID: Rejected because the spec explicitly requires concentration to be separate from conditions and not appear in the condition tag UI.
|
||||
- Store concentration at the encounter level (map of combatant ID to boolean): Rejected because co-locating with the combatant is consistent with how all other per-combatant state (HP, AC, conditions) is stored.
|
||||
|
||||
## R2: Storage Backward Compatibility
|
||||
|
||||
**Decision**: Add `isConcentrating?: boolean` as an optional field on the `Combatant` type. No migration needed.
|
||||
|
||||
**Rationale**: The localStorage adapter (`apps/web/src/persistence/encounter-storage.ts`) rehydrates combatants field-by-field with lenient validation. The AC field was added the same way (optional, defaults to `undefined` if absent). Old saved data without `isConcentrating` will load with the field absent (treated as `false`).
|
||||
|
||||
**Alternatives considered**:
|
||||
- Versioned storage with explicit migration: Rejected because optional boolean fields don't require migration (same pattern used for AC).
|
||||
|
||||
## R3: Damage Pulse Detection
|
||||
|
||||
**Decision**: Detect damage in the UI layer by comparing previous and current `currentHp` values, triggering the pulse animation when HP decreases on a concentrating combatant.
|
||||
|
||||
**Rationale**: The domain emits `CurrentHpAdjusted` events with a `delta` field, but the UI layer already receives updated combatant props. Comparing `prevHp` vs `currentHp` via a React ref or `useEffect` is the simplest approach and avoids threading domain events through additional channels. This keeps the pulse animation purely in the adapter layer (no domain logic for "should pulse" needed).
|
||||
|
||||
**Alternatives considered**:
|
||||
- New domain event `ConcentrationCheckRequired`: Rejected because it would encode gameplay rules (concentration saves) in the domain, violating Constitution Principle VII (no gameplay rules in domain/constitution). The pulse is purely a UI hint.
|
||||
- Pass domain events to CombatantRow: Rejected because events are consumed at the hook level and not currently threaded to individual row components. Adding event-based props would increase coupling.
|
||||
|
||||
## R4: Pulse Animation Approach
|
||||
|
||||
**Decision**: Use CSS keyframe animation with a Tailwind utility class, triggered by a transient state flag.
|
||||
|
||||
**Rationale**: The project uses Tailwind CSS v4. A CSS `@keyframes` animation on the left border and icon can be triggered by adding/removing a CSS class. A short-lived React state flag (`isPulsing`) set on damage detection and auto-cleared after animation duration (~700ms) is the simplest approach.
|
||||
|
||||
**Alternatives considered**:
|
||||
- JavaScript-driven animation (requestAnimationFrame): Rejected as over-engineered for a simple pulse effect.
|
||||
- Framer Motion / React Spring: Rejected because neither is a project dependency, and a CSS keyframe animation is sufficient.
|
||||
|
||||
## R5: Brain Icon Availability
|
||||
|
||||
**Decision**: Use `Brain` from `lucide-react`, already a project dependency.
|
||||
|
||||
**Rationale**: Confirmed `lucide-react` is listed in `apps/web/package.json` dependencies. The `Brain` icon is part of the standard Lucide icon set. Other icons used in the project (`Swords`, `Heart`, `Shield`, `Plus`, `ChevronDown`, etc.) follow the same import pattern.
|
||||
|
||||
**Alternatives considered**: None needed; icon is available.
|
||||
106
specs/018-combatant-concentration/spec.md
Normal file
106
specs/018-combatant-concentration/spec.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Feature Specification: Combatant Concentration
|
||||
|
||||
**Feature Branch**: `018-combatant-concentration`
|
||||
**Created**: 2026-03-06
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Add concentration as a separate per-combatant state, not as a normal condition."
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Toggle Concentration (Priority: P1)
|
||||
|
||||
A DM wants to mark a combatant as concentrating on a spell. They hover over the combatant row to reveal a Brain icon button on the left side of the row, then click it to activate concentration. Clicking the icon again toggles concentration off.
|
||||
|
||||
**Why this priority**: Core interaction; without toggle, the feature has no function.
|
||||
|
||||
**Independent Test**: Can be fully tested by hovering a combatant row, clicking the Brain icon, and verifying concentration state toggles on/off.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a combatant row is not hovered, **When** the row is at rest, **Then** the Brain icon is hidden.
|
||||
2. **Given** a combatant row is hovered, **When** concentration is inactive, **Then** the Brain icon appears (muted/faded).
|
||||
3. **Given** the Brain icon is visible, **When** the user clicks it, **Then** concentration activates and the icon remains visible with an active style.
|
||||
4. **Given** concentration is active, **When** the user clicks the Brain icon again, **Then** concentration deactivates and the icon hides (unless row is still hovered).
|
||||
5. **Given** concentration is active, **When** the row is not hovered, **Then** the Brain icon remains visible (active state keeps it shown).
|
||||
6. **Given** the Brain icon is visible, **When** the user hovers over it, **Then** a tooltip reading "Concentrating" appears.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Visual Feedback for Concentration (Priority: P2)
|
||||
|
||||
A DM wants to see at a glance which combatants are concentrating. When concentration is active, the combatant row displays a subtle colored left border accent to visually distinguish it from normal conditions.
|
||||
|
||||
**Why this priority**: Provides passive visual awareness without requiring interaction; builds on toggle.
|
||||
|
||||
**Independent Test**: Can be tested by activating concentration on a combatant and verifying the row gains a colored left border accent.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a combatant has concentration active, **When** viewing the encounter tracker, **Then** the combatant row shows a colored left border accent.
|
||||
2. **Given** a combatant has concentration inactive, **When** viewing the encounter tracker, **Then** the combatant row has no concentration accent.
|
||||
3. **Given** concentration is active, **When** the user toggles concentration off, **Then** the left border accent disappears.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Damage Pulse Alert (Priority: P3)
|
||||
|
||||
A DM deals damage to a concentrating combatant. The concentration icon and the row accent briefly pulse/flash to draw attention, reminding the DM that a concentration check may be needed.
|
||||
|
||||
**Why this priority**: Enhances situational awareness but depends on both toggle (P1) and visual feedback (P2) being in place.
|
||||
|
||||
**Independent Test**: Can be tested by activating concentration on a combatant, applying damage, and verifying the pulse animation triggers.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a combatant is concentrating, **When** the combatant takes damage (HP reduced), **Then** the Brain icon and row accent briefly pulse/flash.
|
||||
2. **Given** a combatant is concentrating, **When** the combatant is healed (HP increased), **Then** no pulse/flash occurs.
|
||||
3. **Given** a combatant is not concentrating, **When** the combatant takes damage, **Then** no pulse/flash occurs.
|
||||
4. **Given** a concentrating combatant takes damage, **When** the pulse animation completes, **Then** the row returns to its normal concentration-active appearance.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- What happens when a combatant with concentration active is removed from the encounter? Concentration state is discarded with the combatant.
|
||||
- What happens when concentration is toggled during an active pulse animation? The animation cancels and the new state applies immediately.
|
||||
- Can multiple combatants concentrate simultaneously? Yes, concentration is independent per combatant.
|
||||
- Does concentration state persist across page reloads? Yes, it is part of the combatant state stored via existing persistence.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST store concentration as a boolean property on each combatant, separate from the conditions list.
|
||||
- **FR-002**: System MUST provide a toggle operation that flips concentration on/off for a given combatant.
|
||||
- **FR-003**: The Brain icon MUST be hidden by default and appear on combatant row hover.
|
||||
- **FR-004**: The Brain icon MUST remain visible whenever concentration is active, regardless of hover state.
|
||||
- **FR-005**: Clicking the Brain icon MUST toggle the combatant's concentration state.
|
||||
- **FR-006**: A tooltip reading "Concentrating" MUST appear when hovering the Brain icon.
|
||||
- **FR-007**: When concentration is active, the combatant row MUST display a subtle colored left border accent.
|
||||
- **FR-008**: When a concentrating combatant takes damage, the Brain icon and row accent MUST briefly pulse/flash.
|
||||
- **FR-009**: The pulse/flash MUST NOT trigger on healing or when concentration is inactive.
|
||||
- **FR-010**: Concentration MUST persist across page reloads via existing storage mechanisms.
|
||||
- **FR-011**: Concentration MUST NOT appear in or interact with the condition tag system.
|
||||
- **FR-012**: The concentration left border accent MUST use `border-l-purple-400`. The active Brain icon MUST use `text-purple-400` to visually associate icon and border.
|
||||
- **FR-013**: The inactive (hover-revealed) Brain icon MUST use a muted style (`text-muted-foreground opacity-50`).
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **Combatant**: Gains a new `isConcentrating` optional boolean property (default: `undefined`/falsy), independent of the existing `conditions` array.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Users can toggle concentration on/off for any combatant in a single click.
|
||||
- **SC-002**: Concentrating combatants are visually distinguishable from non-concentrating combatants at a glance without hovering or interacting.
|
||||
- **SC-003**: When a concentrating combatant takes damage, the visual alert draws attention within the same interaction flow (no separate notification needed).
|
||||
- **SC-004**: Concentration state survives a full page reload without data loss.
|
||||
- **SC-005**: The concentration UI does not increase the resting height of combatant rows (icon hidden by default keeps rows compact).
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The Lucide `Brain` icon is available in the project's existing Lucide React dependency.
|
||||
- The colored left border accent will use a distinct color that does not conflict with existing condition tag colors.
|
||||
- The pulse/flash animation duration MUST be 700ms. Both the CSS animation and the JS timeout MUST use this single value.
|
||||
- "Takes damage" means any HP reduction (negative delta applied to current HP).
|
||||
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