2.5 KiB
2.5 KiB
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
packages/domain/src/types.ts— AddisConcentrating?: booleantoCombatantinterface.packages/domain/src/events.ts— AddConcentrationStartedandConcentrationEndedevent types to theDomainEventunion.packages/domain/src/toggle-concentration.ts(new) — Pure function mirroringtoggle-condition.tspattern.packages/domain/src/index.ts— Re-export new function and event types.
Application Layer
packages/application/src/toggle-concentration-use-case.ts(new) — Thin orchestration followingtoggle-condition-use-case.tspattern.packages/application/src/index.ts— Re-export new use case.
Web Adapter
apps/web/src/persistence/encounter-storage.ts— AddisConcentratingto combatant rehydration.apps/web/src/hooks/use-encounter.ts— AddtoggleConcentrationcallback.apps/web/src/components/combatant-row.tsx— Add Brain icon toggle, left border accent, and pulse animation.apps/web/src/App.tsx— WireonToggleConcentrationprop through toCombatantRow.
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.