# 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.