51 lines
3.8 KiB
Markdown
51 lines
3.8 KiB
Markdown
# 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.
|