# Feature Specification: Combatant State **Feature Branch**: `003-combatant-state` **Created**: 2026-03-03 **Status**: Implemented --- ## Overview Combatant State covers all per-combatant data tracked during an encounter: hit points, armor class, conditions, concentration, and initiative. **Structure**: This spec is organized by topic area. Each topic section contains its own user scenarios, requirements, and edge cases. The Combatant Row Layout section covers cross-cutting UI concerns. ## Domain Model Reference ```ts interface Combatant { readonly id: CombatantId; // branded string readonly name: string; readonly initiative?: number; // integer, undefined = unset readonly maxHp?: number; // positive integer readonly currentHp?: number; // 0..maxHp readonly ac?: number; // non-negative integer readonly conditions?: readonly ConditionId[]; readonly isConcentrating?: boolean; readonly creatureId?: CreatureId; // link to bestiary entry } ``` --- ## Hit Points ### User Stories **Story HP-1 — Set Max HP (P1)** As a game master, I want to assign a maximum HP value to a combatant so that I can track their health during the encounter. Acceptance scenarios: 1. **Given** a combatant exists, **When** the user sets max HP to a positive integer, **Then** the combatant's max HP is stored and current HP defaults to that value. 2. **Given** a combatant has max HP 20 and current HP 20, **When** the user lowers max HP to 15, **Then** current HP is clamped to 15. 3. **Given** a combatant has max HP 20 and current HP 20 (full health), **When** the user increases max HP to 30, **Then** current HP increases to 30 (stays at full). 4. **Given** a combatant has max HP 20 and current HP 12 (not full), **When** the user increases max HP to 30, **Then** current HP remains at 12. 5. **Given** the max HP inline edit is active, **When** the user clears the field and confirms, **Then** max HP is unset and HP tracking is removed entirely. **Story HP-2 — Apply HP Delta (P1)** As a game master in the heat of combat, I want to type a damage or healing number and immediately apply it to a combatant's HP so that I can keep up with fast-paced encounters without mental arithmetic. Acceptance scenarios: 1. **Given** a combatant has 20/20 HP, **When** the user types 7 into the delta input and presses Enter, **Then** current HP decreases to 13. 2. **Given** a combatant has 10/20 HP, **When** the user types 15 and presses Enter, **Then** current HP is clamped to 0. 3. **Given** a combatant has 10/20 HP and types 5 then clicks the heal button, **Then** current HP increases to 15. 4. **Given** a combatant has 18/20 HP and types 10 then clicks the heal button, **Then** current HP is clamped to 20. 5. **Given** any confirmed delta, **Then** the input field clears automatically and is ready for the next entry. 6. **Given** the user types 0 and presses Enter, **Then** the input is rejected and HP remains unchanged. 7. **Given** the delta input is focused and the user presses Escape, **Then** the input clears without applying any change. **Story HP-3 — Click-to-Adjust Popover (P1)** As a DM running combat, I want to click on a combatant's current HP value to open a small adjustment popover so that the combatant row is visually clean and I can still quickly apply damage or healing when needed. Acceptance scenarios: 1. **Given** a combatant with max HP set, **When** viewing the row at rest, **Then** only the current HP number and max HP are visible — no delta input or action buttons. 2. **Given** a combatant with max HP set, **When** the user clicks the current HP number, **Then** a small popover opens with an auto-focused numeric input and Damage/Heal buttons. 3. **Given** the HP popover is open with a valid number, **When** the user presses Enter, **Then** damage is applied and the popover closes. 4. **Given** the HP popover is open with a valid number, **When** the user presses Shift+Enter, **Then** healing is applied and the popover closes. 5. **Given** the HP popover is open, **When** the user presses Escape, **Then** the popover closes without changes. 6. **Given** the HP popover is open, **When** the user clicks outside, **Then** the popover closes without changes. 7. **Given** a combatant with no max HP set, **When** viewing the row, **Then** the HP area shows only the max HP clickable placeholder — no current HP value. **Story HP-4 — Direct HP Entry (P2)** As a game master, I want to type a specific absolute current HP value directly so I can apply large corrections in one action. Acceptance scenarios: 1. **Given** a combatant has max HP 50, **When** the user types 35 into the current HP field, **Then** current HP is set to 35. 2. **Given** a combatant has max HP 50, **When** the user types 60, **Then** current HP is clamped to 50. 3. **Given** a combatant has max HP 50, **When** the user types -5, **Then** current HP is clamped to 0. **Story HP-5 — HP Status Indicators (P1)** As a game master, I want to see at a glance which combatants are bloodied or unconscious so I can narrate the battle and make tactical decisions without mental math. Acceptance scenarios: 1. **Given** a combatant has max HP 20 and current HP 10 (at half), **Then** no bloodied indicator is shown (10 is not less than 20/2 = 10). 2. **Given** a combatant has max HP 20 and current HP 9 (below half), **Then** the bloodied indicator is visible (amber color treatment on HP value). 3. **Given** a combatant has max HP 21 and current HP 10 (below 10.5), **Then** the bloodied indicator is shown. 4. **Given** a combatant has max HP 20 and current HP 0, **Then** the unconscious/dead indicator is shown (red color; row visually muted). 5. **Given** a combatant at 0 HP is healed above 0, **Then** the unconscious indicator is removed and the correct status is applied. 6. **Given** a combatant has no max HP set, **Then** no status indicator is shown. 7. **Given** a combatant at full HP 20/20, **When** 11 damage is dealt (-> 9/20), **Then** the indicator transitions to bloodied. 8. **Given** a bloodied combatant 5/20, **When** 5 damage is dealt (-> 0/20), **Then** the indicator transitions to unconscious/dead. 9. **Given** an unconscious combatant 0/20, **When** 15 HP is healed (-> 15/20), **Then** the indicator transitions directly to healthy (skips bloodied since 15 > 10). **Story HP-6 — HP Persists Across Reloads (P2)** As a game master, I want HP values to survive page reloads so that I do not lose health tracking mid-session. Acceptance scenarios: 1. **Given** a combatant has max HP 30 and current HP 18, **When** the page is reloaded, **Then** both values are restored exactly. ### Requirements - **FR-001**: Each combatant MAY have an optional `maxHp` value (positive integer >= 1). HP tracking is optional per combatant. - **FR-002**: When `maxHp` is first set, `currentHp` MUST default to `maxHp`. - **FR-003**: `currentHp` MUST be clamped to [0, `maxHp`] at all times. - **FR-004**: The system MUST provide an inline HP delta input per combatant (hidden behind a click-to-open popover on the current HP value). - **FR-005**: The HP popover MUST contain a single auto-focused numeric input and Damage and Heal action buttons. - **FR-006**: Pressing Enter in the popover MUST apply the entered value as damage; Shift+Enter MUST apply it as healing; Escape MUST dismiss without change; clicking outside MUST dismiss without change. - **FR-007**: When a damage value is confirmed, the system MUST subtract the entered amount from `currentHp`, clamping to 0. - **FR-008**: When a healing value is confirmed, the system MUST add the entered amount to `currentHp`, clamping to `maxHp`. - **FR-009**: After any delta is applied, the input MUST clear automatically. - **FR-010**: The delta input MUST only accept positive integers. Zero, negative, and non-numeric values MUST be rejected. - **FR-011**: Direct editing of the absolute `currentHp` value MUST remain available alongside the delta input. - **FR-012**: `maxHp` MUST display as compact static text with click-to-edit. The value is committed on Enter or blur; Escape cancels. Intermediate editing (clearing the field to retype) MUST NOT affect `currentHp` until committed. - **FR-013**: When `maxHp` is reduced below `currentHp`, `currentHp` MUST be clamped to the new `maxHp`. - **FR-014**: When `maxHp` increases and the combatant was at full health, `currentHp` MUST increase to match the new `maxHp`. - **FR-015**: When `maxHp` increases and the combatant was NOT at full health, `currentHp` MUST remain unchanged (unless clamped by FR-013). - **FR-016**: `maxHp` MUST reject zero, negative, and non-integer values. - **FR-017**: HP values MUST persist across page reloads via the existing persistence mechanism. - **FR-018**: The HP status MUST be derived as a pure domain computation: `healthy` (currentHp >= maxHp / 2), `bloodied` (0 < currentHp < maxHp / 2), `unconscious` (currentHp <= 0). The status is not stored — computed on demand. - **FR-019**: The HP area MUST display the bloodied color treatment (amber) on the current HP value when status is `bloodied`. - **FR-020**: The HP area MUST display the unconscious color treatment (red) and the combatant row MUST appear visually muted when status is `unconscious`. - **FR-021**: Status indicators MUST NOT be shown when `maxHp` is not set. - **FR-022**: Visual status indicators MUST update within the same interaction frame as the HP change — no perceptible delay. ### Edge Cases - `maxHp` of 1: at 1/1 the combatant is healthy; at 0/1 unconscious. No bloodied state is possible. - `maxHp` of 2: at 1/2 the combatant is healthy (1 is not strictly less than 1); at 0/2 unconscious. - When `maxHp` is cleared, `currentHp` is also cleared; the combatant returns to the no-HP state. - Entering a non-numeric value in any HP field is rejected; the previous value is preserved. - Entering a very large number (e.g., 99999) is applied normally; clamping prevents invalid state. - Submitting an empty delta input applies no change; the input remains ready. - When the user rapidly applies multiple deltas, each is applied sequentially; none are lost. - HP tracking is entirely absent for combatants with no `maxHp` set — no HP controls are shown. - There is no temporary HP in the MVP baseline. - There is no death/unconscious game mechanic triggered at 0 HP; the system displays the state only. - There is no damage type tracking, resistance/vulnerability calculation, or hit log in the MVP baseline. - There is no undo/redo for HP changes in the MVP baseline. --- ## Armor Class ### User Stories **Story AC-1 — Set and Display AC (P1)** As a game master, I want to assign an Armor Class value to a combatant and see it displayed as a shield shape so I can reference it at a glance during combat. Acceptance scenarios: 1. **Given** a combatant exists, **When** the user clicks the AC shield area and enters 17, **Then** the combatant's AC is stored and the shield shape displays "17". 2. **Given** a combatant with AC 15, **When** viewing the row, **Then** the AC number is displayed inside a shield-shaped outline (not a separate icon + number). 3. **Given** a combatant with no AC set, **When** viewing the row, **Then** the shield shape is shown in an empty/placeholder state. 4. **Given** multiple combatants with different AC values, **When** viewing the encounter list, **Then** each displays its own correct AC. **Story AC-2 — Edit AC (P2)** As a game master, I want to edit an existing combatant's AC inline so I can correct or update it without navigating away. Acceptance scenarios: 1. **Given** a combatant with AC 15, **When** the user clicks the shield, **Then** an inline input appears pre-filled with 15 and selected. 2. **Given** the inline AC edit is active, **When** the user types 18 and presses Enter, **Then** AC updates to 18 and the display returns to static mode. 3. **Given** the inline AC edit is active, **When** the user blurs, **Then** AC is committed and the display returns to static mode. 4. **Given** the inline AC edit is active, **When** the user presses Escape, **Then** the edit is cancelled and the original value is preserved. 5. **Given** the inline AC edit is active, **When** the user clears the field and presses Enter, **Then** AC is unset and the shield shows an empty state. ### Requirements - **FR-023**: Each combatant MAY have an optional `ac` value, a non-negative integer (>= 0). - **FR-024**: AC MUST be displayed inside a shield-shaped visual element. The separate shield icon is replaced by the shield shape itself. - **FR-025**: The shield shape MUST be shown in all cases (set, unset/empty state). No AC value means an empty-state shield, not hidden. - **FR-026**: Clicking the shield MUST open an inline edit input with the current value pre-filled and selected. - **FR-027**: The inline AC edit MUST commit on Enter or blur and cancel on Escape. - **FR-028**: Clearing the AC field and confirming MUST unset AC. - **FR-029**: AC MUST reject negative values. Zero is a valid AC. - **FR-030**: AC values MUST persist via the existing persistence mechanism. - **FR-031**: The AC shield MUST scale appropriately for single-digit, double-digit, and any valid AC values. ### Edge Cases - AC 0 is valid and MUST be displayed. - Negative AC is not accepted; the input is rejected. - MVP baseline does not include AC-based calculations (to-hit comparisons, conditional formatting based on AC thresholds). --- ## Conditions & Concentration ### User Stories **Story CC-1 — Add a Condition (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. Acceptance scenarios: 1. **Given** a combatant row is not hovered and has no conditions, **Then** no condition UI is visible. 2. **Given** a combatant row is hovered, **When** no conditions are active, **Then** a "+" button appears inline after the creature name. 3. **Given** the "+" button is visible, **When** the user clicks it, **Then** a compact condition picker opens showing all 15 conditions as icon + label pairs. 4. **Given** the picker is open, **When** the user clicks a condition, **Then** it is toggled on and its icon appears inline after the creature name. 5. **Given** the picker is open with active conditions already marked, **When** viewing the picker, **Then** active conditions are visually distinguished from inactive ones. **Story CC-2 — Remove a Condition (P1)** As a DM, I want to remove a condition from a combatant when the effect ends so the tracker stays accurate. Acceptance scenarios: 1. **Given** a combatant has active conditions, **When** the user clicks an active condition icon tag inline, **Then** the condition is removed and the icon disappears. 2. **Given** the condition picker is open, **When** the user clicks an active condition, **Then** it is toggled off and removed from the row. 3. **Given** a combatant with one condition and it is removed, **Then** only the hover-revealed "+" button remains. **Story CC-3 — View Condition Name via Tooltip (P2)** As a DM, I want to hover over a condition icon to see its name so I can identify conditions without memorizing icons. Acceptance scenarios: 1. **Given** a combatant has an active condition, **When** the user hovers over its icon, **Then** a tooltip shows the condition name (e.g., "Blinded"). 2. **Given** the user moves the cursor away from the icon, **Then** the tooltip disappears. **Story CC-4 — Multiple Conditions (P2)** As a DM, I want to apply multiple conditions to a single combatant so I can track complex combat situations. Acceptance scenarios: 1. **Given** a combatant with one condition, **When** another is added, **Then** both icons appear inline. 2. **Given** a combatant with many conditions, **When** viewing the row, **Then** icons wrap within the name column without increasing row width; row height may increase. 3. **Given** "poisoned" was applied first and "blinded" second, **When** viewing the row, **Then** "blinded" appears before "poisoned" (fixed definition order, not insertion order). **Story CC-5 — Toggle Concentration (P1)** As a DM, I want to mark a combatant as concentrating on a spell by clicking a Brain icon in the row gutter so I can track spells requiring concentration. Acceptance scenarios: 1. **Given** a combatant row is not hovered and concentration is inactive, **Then** the Brain icon is hidden. 2. **Given** a combatant row is hovered and concentration is inactive, **Then** the Brain icon appears in a muted/faded style. 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 and the row is not hovered, **Then** the Brain icon remains visible. 5. **Given** concentration is active, **When** the user clicks the Brain icon again, **Then** concentration deactivates and the icon hides (unless the row is still hovered). 6. **Given** the Brain icon is visible, **When** the user hovers over it, **Then** a tooltip reading "Concentrating" appears. **Story CC-6 — Visual Feedback for Concentration (P2)** As a DM, I want concentrating combatants to have a visible row accent so I can identify them at a glance without interacting. Acceptance scenarios: 1. **Given** concentration is active, **When** viewing the encounter tracker, **Then** the combatant row shows a colored left border accent (`border-l-purple-400`). 2. **Given** concentration is inactive, **Then** no concentration accent is shown. 3. **Given** concentration is toggled off, **Then** the left border accent disappears immediately. **Story CC-7 — Damage Pulse Alert (P3)** As a DM, I want a visual alert when a concentrating combatant takes damage so I remember to call for a concentration check. 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 for 700 ms. 2. **Given** a combatant is concentrating, **When** the combatant is healed, **Then** no pulse/flash occurs. 3. **Given** a combatant is NOT concentrating, **When** damage is taken, **Then** no pulse/flash occurs. 4. **Given** a concentrating combatant takes damage, **When** the animation completes, **Then** the row returns to its normal concentration-active appearance. ### Requirements - **FR-032**: 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. - **FR-033**: Each condition MUST have a fixed icon and color mapping (Lucide icons; no emoji): | 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-034**: Active condition icons MUST appear inline after the creature name within the same row, not on a separate line. - **FR-035**: Conditions MUST be displayed in the fixed definition order (blinded -> unconscious), regardless of application order. - **FR-036**: The "+" condition button MUST be hidden by default and appear only on row hover (or touch/focus on touch devices). - **FR-037**: Clicking "+" MUST open the compact condition picker showing all conditions as icon + label pairs. - **FR-038**: Clicking a condition in the picker MUST toggle it on or off. - **FR-039**: Clicking an active condition icon tag in the row MUST remove that condition. - **FR-040**: Hovering an active condition icon MUST show a tooltip with the condition name. - **FR-041**: Condition icons MUST NOT increase the row's width; row height MAY increase to accommodate wrapping. - **FR-042**: The condition picker MUST close when the user clicks outside of it. - **FR-043**: Conditions MUST persist as part of combatant state (surviving page reload). - **FR-044**: The condition data model MUST be extensible for future additions (e.g., mechanical effects, descriptions). - **FR-045**: `isConcentrating` MUST be stored as an optional boolean on the combatant, separate from the `conditions` array. - **FR-046**: Concentration MUST NOT appear in or interact with the condition tag system. - **FR-047**: The Brain icon toggle MUST be hidden at row rest and revealed on hover (same hover pattern as the "+" button). - **FR-048**: The Brain icon MUST remain visible whenever concentration is active, regardless of hover state. - **FR-049**: A tooltip reading "Concentrating" MUST appear when hovering the Brain icon. - **FR-050**: The active Brain icon MUST use `text-purple-400`; the inactive (hover-revealed) Brain icon MUST use a muted style (`text-muted-foreground opacity-50`). - **FR-051**: The concentration left border accent MUST use `border-l-purple-400`. - **FR-052**: The concentration toggle's clickable area MUST extend to fill the full gutter between the left border and the initiative column. - **FR-053**: When a concentrating combatant takes damage, the Brain icon and row accent MUST briefly pulse/flash for 700 ms. - **FR-054**: The pulse animation MUST NOT trigger on healing or when concentration is inactive. - **FR-055**: Concentration MUST persist across page reloads via existing storage. ### Edge Cases - When all 15 conditions are applied, icons wrap within the row; row height increases but width does not. - When a combatant is removed, all its conditions and concentration state are discarded. - When the picker is open and the user clicks outside, it closes. - When a condition is toggled on then immediately off in the picker, it does not appear in the row. - When concentration is toggled during an active pulse animation, the animation cancels and the new state applies immediately. - Multiple combatants may concentrate simultaneously; concentration is independent per combatant. - Conditions have no mechanical effects in the MVP baseline (no auto-disadvantage, no automation). --- ## Initiative ### User Stories **Story INI-1 — Set Initiative Value (P1)** As a game master running an encounter, I want to assign an initiative value to a combatant so that the encounter's turn order reflects each combatant's rolled initiative. Acceptance scenarios: 1. **Given** a combatant with no initiative set, **When** the user sets initiative to 15, **Then** the combatant has initiative value 15. 2. **Given** a combatant with initiative 15, **When** the user changes initiative to 8, **Then** the combatant has initiative value 8. 3. **Given** a combatant with no initiative set, **When** the user attempts to set a non-integer value, **Then** the system rejects the input and initiative remains unset. 4. **Given** a combatant with initiative 15, **When** the user clears initiative, **Then** the initiative is unset and the combatant moves to the end of the turn order. 5. **Given** a combatant has an initiative value displayed as plain text, **When** the user clicks it, **Then** an inline editor opens to change or clear it. **Story INI-2 — Roll Initiative for a Single Combatant (P1)** As a DM, I want to click a d20 icon next to a combatant's initiative slot to randomly roll initiative (1d20 + initiative modifier) so the result is immediately placed into the initiative field and the tracker re-sorts. Acceptance scenarios: 1. **Given** a combatant linked to a bestiary creature (e.g., Aboleth, initiative modifier +7) with no initiative, **When** the user clicks the d20 icon, **Then** a random value in the range [8, 27] is stored as initiative and the list re-sorts descending. 2. **Given** a combatant NOT linked to a bestiary creature, **When** viewing the row, **Then** the initiative slot shows "--" (clickable to type a value manually) — no d20 button. 3. **Given** a combatant whose initiative modifier is negative (e.g., -2), **When** the d20 button is clicked, **Then** the result ranges from -1 to 18. 4. **Given** a combatant already has an initiative value, **Then** the d20 button is replaced by the value as plain text; clicking it opens the inline editor. **Story INI-3 — Roll Initiative for All Eligible Combatants (P2)** As a DM, I want a "Roll All Initiative" button in the top bar to roll for all bestiary combatants at once so I can set up the initiative order quickly at the start of combat. Acceptance scenarios: 1. **Given** 3 bestiary combatants (no initiative) and 1 manual combatant, **When** the roll-all button is clicked, **Then** all 3 bestiary combatants receive rolled initiative values; the manual combatant is unchanged. 2. **Given** bestiary combatants that already have initiative values, **When** the roll-all button is clicked, **Then** those combatants are skipped; only bestiary combatants without initiative are rolled. 3. **Given** no bestiary combatants, **When** the roll-all button is clicked, **Then** no changes occur. **Story INI-4 — Display Initiative Modifier in Stat Block (P1)** As a DM viewing a creature's stat block, I want to see the creature's initiative modifier and passive initiative so I can reference it when rolling. Acceptance scenarios: 1. **Given** a creature with DEX 9, CR 10, initiative proficiency multiplier 2 (e.g., Aboleth), **When** the stat block is displayed, **Then** the initiative line shows "Initiative +7 (17)" (-1 + 2x4 = +7; passive = 17). 2. **Given** a creature with proficiency multiplier 1, **Then** the initiative modifier includes 1x the proficiency bonus. 3. **Given** a creature with no `initiative` field in bestiary data, **Then** only the DEX modifier is shown (e.g., DEX 14 -> "Initiative +2 (12)"). 4. **Given** a creature with a negative initiative modifier (e.g., DEX 8, no proficiency), **Then** the line uses a minus sign (e.g., "Initiative -1 (9)"). 5. **Given** a combatant without bestiary data, **Then** no initiative line is shown in the stat block. **Story INI-5 — Combatants Without Initiative (P2)** As a game master, I want combatants without initiative set to appear at the end of the turn order so the encounter remains usable while I am still entering values. Acceptance scenarios: 1. **Given** combatants A (initiative 15), B (no initiative), C (initiative 10), **Then** order is A, C, B. 2. **Given** A (no initiative) and B (no initiative), **Then** their relative order is preserved from when they were added. 3. **Given** combatant A (no initiative), **When** initiative is set to 12, **Then** A moves to its correct sorted position. ### Requirements - **FR-056**: System MUST allow setting an integer initiative value for any combatant. - **FR-057**: System MUST allow changing an existing initiative value. - **FR-058**: System MUST allow clearing (unsetting) a combatant's initiative value. - **FR-059**: System MUST reject non-integer initiative values and return a domain error. - **FR-060**: System MUST accept zero and negative integers as valid initiative values. - **FR-061**: System MUST automatically reorder combatants highest-to-lowest initiative whenever a value is set, changed, or cleared. - **FR-062**: Combatants without initiative MUST be placed after all combatants with initiative values. - **FR-063**: System MUST use a stable sort so combatants with equal initiative (or multiple without initiative) retain their relative order. - **FR-064**: System MUST preserve the active combatant's turn through reorders — the active turn tracks combatant identity, not list position. - **FR-065**: System MUST emit a domain event when a combatant's initiative is set or changed. - **FR-066**: System MUST display a d20 icon button in the initiative slot for every combatant that has a linked bestiary creature and does not yet have an initiative value. - **FR-067**: System MUST NOT display the d20 button for combatants without a linked bestiary creature; they see a "--" placeholder that is clickable to type a value. - **FR-068**: When the d20 button is clicked, the system MUST generate a uniform random integer in [1, 20], add the creature's initiative modifier, and set the result as the initiative value. - **FR-069**: The initiative modifier MUST be calculated as: DEX modifier + (proficiency multiplier x proficiency bonus), where DEX modifier = floor((DEX - 10) / 2) and proficiency bonus is derived from challenge rating. - **FR-070**: The initiative proficiency multiplier MUST be read from `initiative.proficiency` in bestiary data (0 if absent, 1 for proficiency, 2 for expertise). - **FR-071**: System MUST provide a "Roll All Initiative" button in the top bar. Clicking it MUST roll for every bestiary-linked combatant that does not already have an initiative value; all others MUST be left unchanged. - **FR-072**: After any initiative roll (single or batch), the encounter list MUST re-sort descending per existing behavior. - **FR-073**: Once a combatant has an initiative value, the d20 button is replaced by the value as plain text using a click-to-edit pattern (consistent with AC and name editing). - **FR-074**: The stat block header MUST display initiative in the format "Initiative +X (Y)" where X is the modifier (with + or - sign) and Y = 10 + X. - **FR-075**: The initiative display in the stat block MUST be positioned adjacent to the AC value, matching Monster Manual 2024 layout. - **FR-076**: No initiative line MUST be shown in the stat block for combatants without bestiary data. - **FR-077**: The initiative display in the stat block is display-only. It MUST NOT modify encounter turn order or trigger rolling automatically. - **FR-078**: The d20 roll-initiative icon MUST be displayed larger than 20x20 px while remaining contained within the initiative column. - **FR-079**: The "Roll All Initiative" d20 in the top bar MUST be sized at 24 px; the clear-encounter (trash) icon MUST be sized at 20 px. Both are visually grouped together, separated from turn navigation controls by spacing. ### Edge Cases - Setting initiative to zero is valid and treated normally in sorting. - Negative initiative values are valid (some game systems use them). - When a combatant is added without initiative during an ongoing encounter, it appears at the end of the order. - When all combatants have the same initiative value, their insertion order is preserved. - When the active combatant's own initiative changes causing a reorder, the active turn still points to that combatant. - When another combatant's initiative changes causing a reorder, the active turn still points to the current active combatant. - When a combatant's initiative modifier produces a roll result of 0 or negative, the value is stored as-is. - When multiple combatants roll the same initiative, ties are resolved by preserving relative insertion order. - A minus sign is used for negative modifiers in the stat block display, not a hyphen. - When a creature has DEX producing a modifier of exactly 0 and no initiative proficiency, display "Initiative +0 (10)". - For manually-added combatants: no initiative modifier is available, so no d20 button and no stat block initiative line. - The passive initiative value shown in the stat block is reference-only; only the active modifier (+X) is used for rolling. - Random number generation for dice rolls uses standard browser randomness; cryptographic randomness is not required. --- ## Combatant Row Layout ### User Stories **Story ROW-1 — Compact Resting State (P1)** As a DM, I want each combatant row to display a minimal, uncluttered view at rest so I can scan the encounter list quickly during play. Acceptance scenarios: 1. **Given** a combatant row is not hovered, **Then** no delta input, action buttons, "+" condition button, remove button, or Brain icon are visible. 2. **Given** a combatant has no conditions, **Then** the row occupies exactly one line height at rest. 3. **Given** a combatant has conditions applied, **Then** condition icons appear inline after the creature name on the same row (not a separate line). **Story ROW-2 — Hover Reveals Controls (P1)** As a DM, I want secondary controls to appear when I hover over a row so they are accessible without cluttering the resting view. Acceptance scenarios: 1. **Given** any combatant row, **When** hovered, **Then** the "+" condition button appears inline after the name/conditions. 2. **Given** any combatant row, **When** hovered, **Then** the remove (x) button becomes visible. 3. **Given** any combatant row, **When** hovered and concentration is inactive, **Then** the Brain icon becomes visible. 4. **Given** the remove button appears on hover, **Then** no layout shift occurs — space is reserved. **Story ROW-3 — Row Click Opens Stat Block (P1)** As a DM, I want to click anywhere on a bestiary combatant row to open its stat block so I have a large click target and a cleaner row without a dedicated book icon. Acceptance scenarios: 1. **Given** a combatant has a linked bestiary creature, **When** the user clicks the name text or empty row space, **Then** the stat block panel opens. 2. **Given** the user clicks an interactive element (initiative, HP, AC, condition icon, "+", "x", concentration), **Then** the stat block does NOT open — the element's own action fires. 3. **Given** a combatant does NOT have a linked creature, **When** the row is clicked, **Then** nothing happens. 4. **Given** viewing any bestiary combatant row, **Then** no BookOpen icon is visible. 5. **Given** a bestiary combatant row, **When** the user hovers over non-interactive areas, **Then** the cursor indicates clickability. 6. **Given** the stat block is already open for a creature, **When** the same row is clicked, **Then** the panel closes (toggle behavior). ### Requirements - **FR-080**: Condition icons MUST render inline after the creature name within the same row. - **FR-081**: The "+" condition button MUST be hidden at rest and appear on row hover (or touch/focus on touch devices). - **FR-082**: The remove (x) button MUST be hidden at rest and appear on row hover (or touch/focus on touch devices). - **FR-083**: Layout space for the remove button MUST be reserved so appearing/disappearing does not cause layout shifts. - **FR-084**: Clicking non-interactive areas of a bestiary combatant row MUST open the stat block panel. - **FR-085**: Clicking interactive elements (initiative, HP, AC, conditions, "+", "x", concentration) MUST NOT trigger the stat block — only the element's own action. - **FR-086**: The BookOpen icon MUST be removed from the combatant row. - **FR-087**: Bestiary combatant rows MUST show a pointer cursor on hover over non-interactive areas. - **FR-088**: All existing interactions (condition add/remove, HP adjustment, AC editing, initiative editing/rolling, concentration toggle, combatant removal) MUST continue to work. - **FR-089**: Browser scrollbars MUST be styled to match the dark UI theme (thin, dark-colored scrollbar thumbs). - **FR-090**: Turn navigation (Previous/Next) MUST use StepBack/StepForward icons in outline button style with foreground-colored borders. Utility actions (d20/trash) MUST use ghost button style to create clear visual hierarchy. - **FR-091**: Previous and Next turn buttons MUST be positioned at the far left and far right of the top bar respectively, with the round/combatant info centered between them. - **FR-092**: The "Initiative Tracker" heading MUST be removed to maximize vertical space for combatants. - **FR-093**: All interactive elements in the row MUST remain keyboard-accessible (focusable and operable via keyboard). - **FR-094**: On touch devices, hover-only controls ("+" button, "x" button) MUST remain accessible via tap or focus. ### Edge Cases - When a combatant has so many conditions that they exceed the available inline space, they wrap within the name column; row height increases but width does not. - The condition picker dropdown positions relative to the "+" button, flipping vertically if near the viewport edge. - When the stat block panel is already open and the user clicks the same row again, the panel closes. - Clicking the initiative area starts editing; it does not open the stat block. - Tablet-width screens (>= 768 px / Tailwind `md`): popovers and inline edits MUST remain accessible and not overflow or clip. --- ## Success Criteria *(mandatory)* - **SC-001**: Users can set max HP and adjust current HP for any combatant in under 5 seconds per action. - **SC-002**: `currentHp` never exceeds `maxHp` or drops below 0, regardless of input method. - **SC-003**: HP values survive a full page reload without data loss. - **SC-004**: A user can apply damage to a combatant in 2 interactions or fewer (click HP -> type number -> Enter). - **SC-005**: A user can apply healing in 3 interactions or fewer (click HP -> type number -> click Heal). - **SC-006**: Users can identify bloodied combatants at a glance without reading HP numbers — 100% of combatants below half HP display a distinct amber treatment. - **SC-007**: Users can identify unconscious/dead combatants at a glance — 100% of combatants at 0 HP or below display a distinct red treatment that differs from the bloodied indicator. - **SC-008**: Visual status indicators update within the same interaction frame as the HP change. - **SC-009**: Users can set an AC value for any combatant within the existing edit workflow with no additional steps. - **SC-010**: AC is visible at a glance in the encounter list without expanding or hovering. - **SC-011**: A condition can be added to a combatant in 2 clicks or fewer (click "+", click condition). - **SC-012**: A condition can be removed in 1 click (click the active icon tag). - **SC-013**: All 15 D&D 5e conditions are available and visually distinguishable by icon and color. - **SC-014**: Condition state survives a full page reload without data loss. - **SC-015**: Users can toggle concentration on/off for any combatant in a single click. - **SC-016**: Concentrating combatants are visually distinguishable from non-concentrating combatants at a glance. - **SC-017**: When a concentrating combatant takes damage, the visual pulse alert draws attention within the same interaction flow. - **SC-018**: Concentration state survives a full page reload. - **SC-019**: Users can set initiative for any combatant in a single action. - **SC-020**: After any initiative change, the encounter list immediately reflects the correct descending sort. - **SC-021**: A single combatant's initiative can be rolled with one click (the d20 button). - **SC-022**: All eligible combatants' initiative can be rolled with one click (roll-all button). - **SC-023**: Manual combatants (no stat block) are never affected by roll actions. - **SC-024**: Every bestiary creature displays an initiative value in its stat block matching D&D Beyond / Monster Manual 2024. - **SC-025**: The initiative line is visible without scrolling in the stat block header. - **SC-026**: Each combatant row without conditions takes up exactly one line height at rest. - **SC-027**: The DM can open a stat block by clicking anywhere on the combatant name area without needing a dedicated icon. - **SC-028**: The AC number is visually identifiable as armor class through the shield shape alone. - **SC-029**: No layout shift occurs when hovering/unhovering rows. - **SC-030**: All HP, AC, initiative, condition, and concentration interactions remain fully operable using only a keyboard.