Implement the 017-combat-conditions feature that adds D&D 5e status conditions to combatants with icon tags, color coding, and a compact toggle picker in the encounter tracker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-06 11:29:39 +01:00
parent 78c6591973
commit febe892e15
22 changed files with 1301 additions and 62 deletions

View File

@@ -0,0 +1,35 @@
# Specification Quality Checklist: Combat Conditions
**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.plan`.
- **Clarify pass (2026-03-06)**: FR-001 reworded to explicitly scope the 15 conditions to MVP (aligning with FR-011 extensibility). FR-002 expanded from examples to the full canonical icon/color mapping table for all 15 conditions.

View File

@@ -0,0 +1,88 @@
# Data Model: Combat Conditions
## Entities
### ConditionId (new type)
String literal union representing the 15 valid condition identifiers.
```
"blinded" | "charmed" | "deafened" | "exhaustion" | "frightened" |
"grappled" | "incapacitated" | "invisible" | "paralyzed" | "petrified" |
"poisoned" | "prone" | "restrained" | "stunned" | "unconscious"
```
- Order of literals in the union defines the canonical display order (FR-003).
- Extensible: future conditions can be added to the union.
### ConditionDefinition (new type)
Static metadata for each condition.
| Field | Type | Description |
| --------- | ----------- | ------------------------------------------------ |
| id | ConditionId | Unique identifier |
| label | string | Human-readable display name (e.g., "Blinded") |
| iconName | string | Lucide icon component name (e.g., "EyeOff") |
| color | string | Color category (e.g., "neutral", "pink", "amber") |
- Registry: `CONDITION_DEFINITIONS` — a static readonly array of all 15 definitions, ordered by display priority.
- No lifecycle/state transitions — definitions are immutable reference data.
### Combatant (modified)
| Field | Type | Change |
| ---------- | ----------------------------- | -------- |
| id | CombatantId | existing |
| name | string | existing |
| initiative | number \| undefined | existing |
| maxHp | number \| undefined | existing |
| currentHp | number \| undefined | existing |
| ac | number \| undefined | existing |
| conditions | readonly ConditionId[] \| undefined | **new** |
- `conditions` is optional; `undefined` means no conditions active.
- When present, the array contains unique `ConditionId` values sorted in definition order.
- Maximum cardinality: 15 (one of each condition).
## Relationships
```
Encounter 1──* Combatant
Combatant 0──* ConditionId (from CONDITION_DEFINITIONS registry)
```
## Validation Rules
- A condition can appear at most once per combatant.
- Only valid `ConditionId` values are accepted (unknown strings rejected).
- The conditions array is always sorted in definition order after any mutation.
- An empty conditions array is normalized to `undefined`.
## Domain Events
### ConditionAdded
| Field | Type |
| ----------- | ----------- |
| type | "ConditionAdded" |
| combatantId | CombatantId |
| condition | ConditionId |
Emitted when a condition is toggled on or explicitly added.
### ConditionRemoved
| Field | Type |
| ----------- | ----------- |
| type | "ConditionRemoved" |
| combatantId | CombatantId |
| condition | ConditionId |
Emitted when a condition is toggled off (via picker or tag click).
## Persistence
- Serialization: `conditions` array serializes to JSON as `["blinded", "poisoned"]`.
- Deserialization: Filter stored values through the `ConditionId` set; discard unknown strings. If resulting array is empty, set to `undefined`.
- No schema migration required (field is optional with `undefined` default).

View File

@@ -0,0 +1,80 @@
# Implementation Plan: Combat Conditions
**Branch**: `017-combat-conditions` | **Date**: 2026-03-06 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/017-combat-conditions/spec.md`
## Summary
Add a `conditions` field to the domain `Combatant` type representing a set of active D&D 5e status conditions (blinded, charmed, etc.). A single `toggleCondition` domain operation manages the set with immutable state transitions (adds if absent, removes if present). The web layer renders conditions as compact Lucide icon tags with color coding below the combatant name, with a "+" button opening a popover picker for toggling conditions on/off.
## 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**: Browser (single-user, local-first)
**Project Type**: Web application (monorepo with domain/application/web layers)
**Performance Goals**: N/A (single-user local app, no performance-critical paths)
**Constraints**: Conditions are visual tracking only; no mechanical effects in MVP
**Scale/Scope**: 15 fixed conditions per the D&D 5e SRD
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
| --------- | ------ | ----- |
| I. Deterministic Domain Core | PASS | `toggleCondition` is a pure function; condition registry is static data |
| II. Layered Architecture | PASS | Domain defines condition types and operations; Application orchestrates via use cases; Web renders UI |
| III. Agent Boundary | N/A | No agent layer involvement |
| IV. Clarification-First | PASS | Two clarifications resolved (display order, "+" button visibility) |
| V. Escalation Gates | PASS | Spec complete and clarified before planning |
| VI. MVP Baseline Language | PASS | FR-001 uses "MVP MUST support" language; homebrew conditions explicitly deferred |
| VII. No Gameplay Rules | PASS | Constitution contains no condition mechanics; spec handles that |
## Project Structure
### Documentation (this feature)
```text
specs/017-combat-conditions/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
└── tasks.md # Phase 2 output (created by /speckit.tasks)
```
### Source Code (repository root)
```text
packages/domain/src/
├── types.ts # Add conditions field to Combatant
├── conditions.ts # NEW: ConditionId type, CONDITION_DEFINITIONS registry, ordering
├── toggle-condition.ts # NEW: toggleCondition domain operation (adds if absent, removes if present)
├── events.ts # Add ConditionAdded, ConditionRemoved events
├── index.ts # Re-export new symbols
└── __tests__/
└── toggle-condition.test.ts # NEW
packages/application/src/
├── toggle-condition-use-case.ts # NEW
└── index.ts # Re-export new use case
apps/web/src/
├── components/
│ ├── combatant-row.tsx # Add conditions area below name
│ ├── condition-tags.tsx # NEW: renders active condition icon tags + "+" button
│ └── condition-picker.tsx # NEW: popover picker for toggling conditions
├── hooks/
│ └── use-encounter.ts # Add toggleCondition callback
└── persistence/
└── encounter-storage.ts # Add conditions rehydration validation
```
**Structure Decision**: Follows existing monorepo structure with domain/application/web layers. New domain files mirror the `set-ac.ts` pattern. UI components split into condition-tags (display) and condition-picker (interaction) for clarity.
## Complexity Tracking
> No violations. No entries needed.

View File

@@ -0,0 +1,57 @@
# Quickstart: Combat Conditions
## Prerequisites
```bash
pnpm install # install dependencies (no new packages needed)
pnpm --filter web dev # start dev server at localhost:5173
```
## Implementation Order
1. **Domain: Condition types & registry** (`packages/domain/src/conditions.ts`)
- Define `ConditionId` union type and `ConditionDefinition` interface
- Create `CONDITION_DEFINITIONS` static array with all 15 entries
- Export `VALID_CONDITION_IDS` set for validation
2. **Domain: Extend Combatant** (`packages/domain/src/types.ts`)
- Add `readonly conditions?: readonly ConditionId[]` to `Combatant`
3. **Domain: Events** (`packages/domain/src/events.ts`)
- Add `ConditionAdded` and `ConditionRemoved` event types
- Add to `DomainEvent` union
4. **Domain: Operations** (`packages/domain/src/toggle-condition.ts`)
- `toggleCondition(encounter, combatantId, conditionId)` — add if absent, remove if present
- Maintains sorted order and emits `ConditionAdded` or `ConditionRemoved` event
5. **Domain: Tests** (`packages/domain/src/__tests__/`)
- Test toggle on/off, ordering, duplicate prevention, unknown condition rejection, immutability
6. **Application: Use case** (`packages/application/src/`)
- `toggleConditionUseCase` following `setAcUseCase` pattern
7. **Web: Persistence** (`apps/web/src/persistence/encounter-storage.ts`)
- Add conditions rehydration with validation against `VALID_CONDITION_IDS`
8. **Web: Components** (`apps/web/src/components/`)
- `condition-tags.tsx` — renders icon tags + "+" button
- `condition-picker.tsx` — popover with all 15 conditions for toggling
- Update `combatant-row.tsx` to include condition area below name
9. **Web: Hook** (`apps/web/src/hooks/use-encounter.ts`)
- Add `toggleCondition` callback
## Verification
```bash
pnpm check # must pass: knip + format + lint + typecheck + test
```
## Key Patterns to Follow
- **Domain operations**: See `packages/domain/src/set-ac.ts` for the exact pattern
- **Use cases**: See `packages/application/src/set-ac-use-case.ts`
- **UI components**: See `combatant-row.tsx` AcInput for inline editing pattern
- **Persistence**: See `encounter-storage.ts` AC validation for rehydration pattern
- **Hook integration**: See `use-encounter.ts` setAc callback for wiring pattern

View File

@@ -0,0 +1,51 @@
# Research: Combat Conditions
## Decision 1: Condition Data Representation
**Decision**: Store conditions on `Combatant` as `readonly conditions?: readonly ConditionId[]` where `ConditionId` is a string literal union type of the 15 condition identifiers.
**Rationale**: A typed union ensures compile-time safety — invalid condition strings are rejected by the type checker. Using an array (not a Set) preserves JSON serialization compatibility with existing localStorage persistence. The array is kept sorted in definition order by domain operations, so display order is deterministic.
**Alternatives considered**:
- `Set<string>`: Not JSON-serializable without custom logic; breaks existing transparent persistence pattern.
- `Record<string, boolean>`: More verbose, harder to iterate for display, no inherent ordering.
- Branded type `ConditionId`: Possible but overkill — a string literal union provides equivalent safety with less ceremony.
## Decision 2: Domain Operation Design
**Decision**: Implement `toggleCondition(encounter, combatantId, conditionId)` as the sole operation. It adds the condition if absent, removes if present, and always returns the array in fixed definition order. Both the picker and the click-to-remove tag flow use toggle.
**Rationale**: The picker UI toggles conditions on/off, making a single toggle operation the natural fit. The combat row click-to-remove also uses toggle — since the condition is guaranteed to be active when its tag is visible, toggle will always remove it. A separate `removeCondition` operation was considered but deemed unnecessary for a single-user local app. The toggle emits distinct events (`ConditionAdded` / `ConditionRemoved`) depending on direction.
**Alternatives considered**:
- Separate `addCondition` + `removeCondition` only: Would require the picker UI to check current state before deciding which to call — pushes domain logic into the adapter layer.
- `toggleCondition` + separate `removeCondition`: The dedicated remove would error if condition not active, preventing accidental re-add from stale state. Rejected as overengineering for a single-user local app.
## Decision 3: Condition Registry Location
**Decision**: Define the condition registry (id, display name, icon component reference, color class) in a new domain file `conditions.ts`. The registry is a static readonly array, not a lookup map.
**Rationale**: The registry is pure data with no I/O — it belongs in the domain layer. An ordered array naturally encodes the fixed display order (FR-003). The web layer imports the registry to map condition IDs to icon components and colors.
**Alternatives considered**:
- Registry in web layer only: Would violate the principle that domain defines the canonical condition set. Ordering logic would leak into the adapter.
- Registry in application layer: No benefit over domain; application should not own entity definitions.
## Decision 4: Condition Icon/Color Mapping Architecture
**Decision**: The domain `conditions.ts` defines condition metadata (id, label, iconName as string, colorClass as string). The web layer maps `iconName` strings to actual Lucide React components at render time.
**Rationale**: Domain must not import React components (layer boundary). Storing icon names as strings keeps domain pure. The web layer maintains a simple `Record<string, LucideIcon>` lookup from icon name to component.
**Alternatives considered**:
- Domain exports icon components directly: Violates layered architecture — domain cannot import from `lucide-react`.
- Separate mapping file in web layer only: Would duplicate the condition list, creating sync risk.
## Decision 5: Persistence Compatibility
**Decision**: No migration needed. The `conditions` field is optional (`conditions?: readonly ConditionId[]`). Existing persisted encounters without conditions will rehydrate with `conditions: undefined`, which the UI treats as "no conditions." Rehydration validates that stored condition values are valid `ConditionId` strings, filtering out any unknown values.
**Rationale**: Follows the same pattern as `ac?` field added in feature 016. Optional fields with undefined defaults require zero migration.
**Alternatives considered**:
- Default to empty array `[]`: Would work but deviates from the established optional-field pattern (ac, maxHp, currentHp all use `undefined`).

View File

@@ -0,0 +1,147 @@
# Feature Specification: Combat Conditions
**Feature Branch**: `017-combat-conditions`
**Created**: 2026-03-06
**Status**: Draft
**Input**: User description: "Add combat conditions to combatants with icon tags, compact picker, and Lucide icon mapping"
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Add a Condition to a Combatant (Priority: P1)
As a DM running an encounter, I want to quickly apply a condition to a combatant so I can track status effects during combat.
**Why this priority**: Core functionality -- without the ability to add conditions, the feature has no value.
**Independent Test**: Can be fully tested by clicking the "+" button on a combatant, selecting a condition, and verifying it appears as an icon tag under the combatant's name.
**Acceptance Scenarios**:
1. **Given** a combatant with no active conditions, **When** I look at the combatant row, **Then** I see a small "+" button in the condition area below the combatant name.
5. **Given** a combatant with one or more active conditions, **When** I look at the combatant row, **Then** I see the active condition icon tags followed by a "+" button.
2. **Given** a combatant row, **When** I click the "+" button, **Then** a compact picker opens showing all 15 available conditions, each displayed as an icon with a label.
3. **Given** the condition picker is open, **When** I click a condition entry, **Then** the condition is toggled on and appears as a colored icon tag under the combatant name in the combat row.
4. **Given** the condition picker is open with some conditions already active, **When** I view the picker, **Then** active conditions are visually distinguished from inactive ones.
---
### User Story 2 - Remove a Condition from a Combatant (Priority: P1)
As a DM, I want to quickly remove a condition from a combatant when the effect ends so the tracker stays accurate.
**Why this priority**: Equally critical as adding -- conditions must be removable to reflect combat state changes.
**Independent Test**: Can be tested by clicking an active condition icon tag and verifying it is removed.
**Acceptance Scenarios**:
1. **Given** a combatant with one or more active conditions, **When** I click an active condition icon tag in the combat row, **Then** the condition is removed and the icon tag disappears.
2. **Given** a combatant with one active condition, **When** I remove that condition, **Then** only the "+" button remains in the condition area.
3. **Given** the condition picker is open, **When** I click an active condition in the picker, **Then** the condition is toggled off and removed from the combatant row.
---
### User Story 3 - View Condition Details via Tooltip (Priority: P2)
As a DM, I want to hover over a condition icon to see its name so I can quickly identify conditions without memorizing icons.
**Why this priority**: Important for usability but the feature is functional without it.
**Independent Test**: Can be tested by hovering over an active condition icon tag and verifying the tooltip displays the condition name.
**Acceptance Scenarios**:
1. **Given** a combatant with an active condition, **When** I hover over the condition icon tag, **Then** a tooltip appears showing the condition's name (e.g., "Blinded", "Poisoned").
2. **Given** I am hovering over a condition icon, **When** I move the cursor away, **Then** the tooltip disappears.
---
### User Story 4 - Multiple Conditions on One Combatant (Priority: P2)
As a DM, I want to apply multiple conditions to a single combatant so I can track complex combat situations.
**Why this priority**: Common in D&D play but the feature works with single conditions first.
**Independent Test**: Can be tested by applying multiple conditions and verifying all appear as icon tags.
**Acceptance Scenarios**:
1. **Given** a combatant with one active condition, **When** I open the picker and add another condition, **Then** both conditions appear as icon tags under the combatant name.
2. **Given** a combatant with multiple active conditions, **When** I view the combat row, **Then** all condition icons are displayed compactly without increasing the row width.
3. **Given** a combatant with many conditions (e.g., 5+), **When** I view the combat row, **Then** the row may grow slightly taller to accommodate wrapping condition icons but does not grow wider.
4. **Given** a combatant has "poisoned" applied first and "blinded" applied second, **When** I view the combat row, **Then** "blinded" appears before "poisoned" (fixed definition order, not insertion order).
---
### Edge Cases
- What happens when all 15 conditions are applied to a single combatant? The icons wrap within the row, increasing row height but not width.
- What happens when a combatant is removed from the encounter? Its conditions are removed with it (no orphaned condition data).
- What happens when the picker is open and I click outside of it? The picker closes.
- What happens when a condition is toggled on then immediately toggled off in the picker? The condition does not appear in the combat row.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The MVP MUST support the following 15 standard D&D 5e conditions: blinded, charmed, deafened, exhaustion, frightened, grappled, incapacitated, invisible, paralyzed, petrified, poisoned, prone, restrained, stunned, unconscious. MVP baseline does not include additional or homebrew conditions.
- **FR-002**: Each condition MUST have a fixed icon and color mapping as follows:
| Condition | Icon | Color |
| -------------- | --------- | ------- |
| Blinded | EyeOff | neutral |
| Charmed | Heart | pink |
| Deafened | EarOff | neutral |
| Exhaustion | BatteryLow | amber |
| Frightened | Siren | orange |
| Grappled | Hand | neutral |
| Incapacitated | Ban | gray |
| Invisible | Ghost | violet |
| Paralyzed | ZapOff | yellow |
| Petrified | Gem | slate |
| Poisoned | Droplet | green |
| Prone | ArrowDown | neutral |
| Restrained | Link | neutral |
| Stunned | Sparkles | yellow |
| Unconscious | Moon | indigo |
- **FR-003**: Active conditions MUST appear as small icon tags below the combatant name in the combat row, displayed in the fixed definition order from FR-001 (blinded first, unconscious last), regardless of the order in which they were applied.
- **FR-004**: The condition area MUST always display a small "+" button, regardless of whether the combatant has active conditions. When conditions are active, the "+" button appears after the last condition icon tag.
- **FR-005**: Clicking the "+" button MUST open a compact condition picker showing all conditions as icon + label pairs.
- **FR-006**: Clicking a condition in the picker MUST toggle it on or off for that combatant.
- **FR-007**: Clicking an active condition icon tag in the combat row MUST remove that condition.
- **FR-008**: Hovering over an active condition icon tag MUST display a tooltip with the condition name.
- **FR-009**: Condition icons MUST NOT increase the width of the combat tracker; row height may increase slightly to accommodate wrapping.
- **FR-010**: Conditions MUST be persisted as part of the combatant's state (surviving page reload via existing persistence).
- **FR-011**: The condition data model MUST be extensible to support future additions (e.g., tooltips with descriptions, mechanical effects).
- **FR-012**: No emoji icons may be used; all icons MUST come from the Lucide icon library.
### Key Entities
- **Condition**: A status effect that can be applied to a combatant. Defined by a unique identifier (string literal), a display name, an associated icon, and a color category.
- **CombatantConditions**: The set of active conditions on a given combatant. Stored as part of the combatant's state. A combatant may have zero or more active conditions; each condition can appear at most once per combatant.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: A condition can be added to a combatant in 2 clicks or fewer (click "+", click condition).
- **SC-002**: A condition can be removed from a combatant in 1 click (click the active icon tag).
- **SC-003**: All 15 D&D 5e conditions are available and visually distinguishable by icon and color.
- **SC-004**: The combat tracker maintains its current width when conditions are displayed; only row height may increase.
- **SC-005**: Condition state survives a full page reload without data loss.
- **SC-006**: Users can identify any condition by hovering over its icon to see the name tooltip.
## Clarifications
### Session 2026-03-06
- Q: In which order should active conditions be displayed in the combat row? -> A: Fixed definition order (blinded, charmed, ..., unconscious) as listed in FR-001. Order does not depend on when a condition was applied.
- Q: Should the "+" button remain visible when conditions are already active? -> A: Yes, the "+" button is always visible alongside active condition tags, appearing after the last icon.
## Assumptions
- The 15 conditions listed are the standard D&D 5e conditions and represent the complete set for MVP. Additional conditions (e.g., homebrew) are not included but the data model should not prevent future extension.
- Color categories (neutral, pink, amber, orange, gray, violet, yellow, slate, green, indigo) map to subtle color classes already available in the project's design system.
- The existing persistence mechanism will transparently handle the new condition data as part of combatant serialization.
- The condition picker closes when a user clicks outside of it or when the user navigates away.
- Conditions have no mechanical effects in MVP (e.g., "blinded" does not auto-impose disadvantage). The spec is purely for visual tracking.

View File

@@ -0,0 +1,164 @@
# Tasks: Combat Conditions
**Input**: Design documents from `/specs/017-combat-conditions/`
**Prerequisites**: plan.md (required), spec.md (required), research.md, data-model.md, quickstart.md
**Tests**: Tests are included as this project follows a test-driven domain pattern (all domain operations have corresponding test files).
**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: Setup
**Purpose**: Define condition types, registry, and extend the domain model
- [x] T001 Define `ConditionId` union type, `ConditionDefinition` interface, and `CONDITION_DEFINITIONS` registry array with all 15 conditions (id, label, iconName, color) plus `VALID_CONDITION_IDS` set in `packages/domain/src/conditions.ts`
- [x] T002 Add `readonly conditions?: readonly ConditionId[]` field to `Combatant` interface in `packages/domain/src/types.ts`
- [x] T003 Add `ConditionAdded` and `ConditionRemoved` event interfaces and include them in the `DomainEvent` union in `packages/domain/src/events.ts`
- [x] T004 Re-export new symbols (`ConditionId`, `ConditionDefinition`, `CONDITION_DEFINITIONS`, `VALID_CONDITION_IDS`, `ConditionAdded`, `ConditionRemoved`) from `packages/domain/src/index.ts`
**Checkpoint**: Domain types compile; `pnpm typecheck` passes
---
## Phase 2: Foundational (Domain Operations)
**Purpose**: Implement core domain operations that all user stories depend on
- [x] T005 [P] Implement `toggleCondition(encounter, combatantId, conditionId)` pure function in `packages/domain/src/toggle-condition.ts` — adds condition if absent (emits `ConditionAdded`), removes if present (emits `ConditionRemoved`), maintains sorted definition order, normalizes empty array to `undefined`
- [x] ~~T006~~ Removed — `toggleCondition` handles both add and remove; a separate `removeCondition` was unnecessary
- [x] T007 [P] Write tests for `toggleCondition` in `packages/domain/src/__tests__/toggle-condition.test.ts` — toggle on, toggle off, ordering preserved, duplicate prevention, unknown condition rejected, combatant-not-found error, immutability, empty-to-undefined normalization
- [x] ~~T008~~ Removed — covered by toggle tests
- [x] T009 Re-export `toggleCondition` and its success type from `packages/domain/src/index.ts`
- [x] T010 [P] Implement `toggleConditionUseCase` in `packages/application/src/toggle-condition-use-case.ts` following `setAcUseCase` pattern
- [x] ~~T011~~ Removed — no separate `removeConditionUseCase` needed
- [x] T012 Re-export new use case from `packages/application/src/index.ts`
**Checkpoint**: `pnpm check` passes; domain operations fully tested
---
## Phase 3: User Story 1 — Add a Condition (Priority: P1) + User Story 2 — Remove a Condition (Priority: P1) 🎯 MVP
**Goal**: DM can add conditions via picker and remove them by clicking icon tags in the combat row
**Independent Test**: Click "+" on a combatant, select a condition in the picker, verify icon tag appears. Click the icon tag, verify it disappears. Reload page, verify conditions persist.
### Implementation
- [x] T013 Add conditions rehydration validation to `loadEncounter` in `apps/web/src/persistence/encounter-storage.ts` — filter stored condition values against `VALID_CONDITION_IDS`, normalize empty array to `undefined`
- [x] T014 Add `toggleCondition` callback to `useEncounter` hook in `apps/web/src/hooks/use-encounter.ts` following the `setAc` callback pattern
- [x] T015 Create `ConditionTags` component in `apps/web/src/components/condition-tags.tsx` — renders active condition icon tags (Lucide icons with color classes) in fixed definition order, plus always-visible "+" button; clicking a tag calls `onRemove(conditionId)`; clicking "+" calls `onOpenPicker()`
- [x] T016 Create `ConditionPicker` component in `apps/web/src/components/condition-picker.tsx` — popover/dropdown showing all 15 conditions as icon + label rows; active conditions visually distinguished; clicking toggles on/off via `onToggle(conditionId)`; closes on click-outside
- [x] T017 Integrate conditions into `CombatantRow` in `apps/web/src/components/combatant-row.tsx` — add `onToggleCondition` prop; render `ConditionTags` + `ConditionPicker` below the combatant name; wire picker open/close state; both tag clicks and picker use toggle
**Checkpoint**: Full add/remove flow works; conditions persist across reload; `pnpm check` passes
---
## Phase 4: User Story 3 — Tooltip on Hover (Priority: P2)
**Goal**: Hovering over a condition icon tag shows the condition name in a tooltip
**Independent Test**: Hover over any active condition icon tag, verify tooltip appears with condition name; move cursor away, verify tooltip disappears.
### Implementation
- [x] T018 [US3] Add `title` attribute or tooltip to condition icon buttons in `ConditionTags` component in `apps/web/src/components/condition-tags.tsx` — display `conditionDefinition.label` on hover
**Checkpoint**: Tooltips visible on hover for all condition icons
---
## Phase 5: User Story 4 — Multiple Conditions (Priority: P2)
**Goal**: Multiple conditions on one combatant display compactly with correct ordering
**Independent Test**: Apply 3+ conditions to a combatant in non-alphabetical order, verify they display in fixed definition order; verify row wraps without widening the tracker.
### Implementation
- [x] T019 [US4] Verify wrapping behavior in `ConditionTags` component in `apps/web/src/components/condition-tags.tsx` — ensure `flex-wrap` is applied so multiple conditions wrap within the row width; verify layout does not increase tracker width
**Checkpoint**: Multiple conditions wrap correctly; fixed order maintained regardless of application order
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Final validation and cleanup
- [x] T020 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues
- [x] T021 Verify layer boundary compliance — domain does not import React/Lucide components; icon name strings only in domain
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — T001 → T002 → T003 → T004 (sequential, same files)
- **Foundational (Phase 2)**: Depends on Phase 1 — T005/T007 parallel, then T009, then T010, then T012
- **US1+US2 (Phase 3)**: Depends on Phase 2 — T013 → T014 → T015/T016 parallel → T017
- **US3 (Phase 4)**: Depends on Phase 3 (T015 must exist) — T018 standalone
- **US4 (Phase 5)**: Depends on Phase 3 (T015 must exist) — T019 standalone
- **Polish (Phase 6)**: Depends on all previous phases — T020 → T021
### User Story Dependencies
- **US1+US2 (P1)**: Merged into one phase — add and remove are two sides of the same interaction; both require the picker and tags components
- **US3 (P2)**: Depends on US1+US2 (needs condition tags to exist); can run parallel with US4
- **US4 (P2)**: Depends on US1+US2 (needs condition tags to exist); can run parallel with US3
### Parallel Opportunities
- T005, T007 can run in parallel (different files)
- T015, T016 can run in parallel (different files)
- T018, T019 can run in parallel (different stories, different concerns)
---
## Parallel Example: Foundational Phase
```bash
# Launch domain operation and tests together:
Task: "Implement toggleCondition in packages/domain/src/toggle-condition.ts"
Task: "Write toggle-condition tests in packages/domain/src/__tests__/toggle-condition.test.ts"
```
---
## Implementation Strategy
### MVP First (US1 + US2)
1. Complete Phase 1: Setup (types and registry)
2. Complete Phase 2: Foundational (domain operations + use cases)
3. Complete Phase 3: US1+US2 (full add/remove UI flow + persistence)
4. **STOP and VALIDATE**: Add and remove conditions in the browser; reload page; verify persistence
5. Deploy/demo if ready
### Incremental Delivery
1. Setup + Foundational → Domain layer complete
2. US1+US2 → Full condition management flow (MVP!)
3. US3 → Tooltips for discoverability
4. US4 → Layout polish for heavy condition usage
5. Each increment adds value without breaking previous work
---
## Notes
- US1 and US2 are merged into Phase 3 because they share the same components (ConditionTags, ConditionPicker) and cannot be meaningfully separated
- US3 (tooltip) is a single-task phase — just adding `title` attributes to existing icon buttons
- US4 (wrapping) is a single-task phase — verifying and ensuring CSS flex-wrap behavior
- The Lucide icon → component mapping lives in the web layer only; domain uses string icon names
- Commit after each task or logical group