Replace 250ms click timer and double-click detection with immediate single-click rename for all combatant types. Add a BookOpen icon before the name on bestiary rows as the dedicated stat block trigger. Remove auto-show stat block on turn advance. Update specs to match: consistent collapse/expand terminology, book icon requirements, no row-click stat block behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
401 lines
27 KiB
Markdown
401 lines
27 KiB
Markdown
# Feature Specification: Combatant Management
|
||
|
||
**Feature Branch**: `001-combatant-management`
|
||
**Created**: 2026-03-03
|
||
**Status**: Implemented
|
||
|
||
## Overview
|
||
|
||
Combatant Management covers the complete lifecycle of combatants within an encounter: adding (individually or in batch), editing, removing, clearing the entire encounter, persisting encounter state across page reloads, and the confirmation UX applied to all destructive actions.
|
||
|
||
---
|
||
|
||
## User Scenarios & Testing *(mandatory)*
|
||
|
||
### Adding Combatants
|
||
|
||
**Story A1 — Add a single combatant (Priority: P1)**
|
||
|
||
A game master adds a new combatant to an existing encounter. The new combatant is appended to the end of the initiative order, allowing late-joining participants or newly discovered enemies to enter combat.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an empty encounter (no combatants, activeIndex 0, roundNumber 1), **When** AddCombatant with name "Gandalf", **Then** combatants is [Gandalf], activeIndex is 0, roundNumber is 1, and a CombatantAdded event is emitted with the new combatant's id, name "Gandalf", and position 0.
|
||
|
||
2. **Given** an encounter with combatants [A, B], activeIndex 0, roundNumber 1, **When** AddCombatant with name "C", **Then** combatants is [A, B, C], activeIndex is 0, roundNumber is 1, and a CombatantAdded event is emitted with position 2.
|
||
|
||
3. **Given** an encounter with combatants [A, B, C], activeIndex 2, roundNumber 3, **When** AddCombatant with name "D", **Then** combatants is [A, B, C, D], activeIndex is 2, roundNumber is 3, and a CombatantAdded event is emitted with position 3. The active combatant does not change.
|
||
|
||
4. **Given** an encounter with combatants [A], **When** AddCombatant is applied twice with names "B" then "C", **Then** combatants is [A, B, C] in that order. Each operation emits its own CombatantAdded event.
|
||
|
||
5. **Given** an encounter with combatants [A, B], **When** AddCombatant with an empty name "", **Then** the operation MUST fail with a validation error. No events are emitted. State is unchanged.
|
||
|
||
6. **Given** an encounter with combatants [A, B], **When** AddCombatant with a whitespace-only name " ", **Then** the operation MUST fail with a validation error. No events are emitted. State is unchanged.
|
||
|
||
---
|
||
|
||
> **Batch add and custom creature workflows** are defined in `specs/004-bestiary/spec.md` (Stories US-S2, US-S3). Those stories cover the bestiary search dropdown, count badge, batch confirm, and custom creature stat fields. This spec covers only the domain-level AddCombatant operation that those workflows invoke.
|
||
|
||
---
|
||
|
||
### Removing Combatants
|
||
|
||
**Story B1 — Remove a combatant from an active encounter (Priority: P1)**
|
||
|
||
A game master is running a combat encounter and a combatant is defeated or leaves. The GM removes that combatant. The combatant disappears from the initiative order and the turn continues correctly without disruption.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an encounter with combatants [A, B, C] and activeIndex 1 (B's turn), **When** the GM removes combatant C (index 2, after active), **Then** the encounter has [A, B], activeIndex remains 1, roundNumber unchanged, and a CombatantRemoved event is emitted.
|
||
|
||
2. **Given** an encounter with combatants [A, B, C] and activeIndex 2 (C's turn), **When** the GM removes combatant A (index 0, before active), **Then** the encounter has [B, C], activeIndex becomes 1 (still C's turn), roundNumber unchanged.
|
||
|
||
3. **Given** an encounter with combatants [A, B, C] and activeIndex 1 (B's turn), **When** the GM removes combatant B (the active combatant), **Then** the encounter has [A, C], activeIndex becomes 1 (C is now active), roundNumber unchanged.
|
||
|
||
4. **Given** an encounter with combatants [A, B, C] and activeIndex 2 (C's turn, last position), **When** the GM removes combatant C (active and last), **Then** the encounter has [A, B], activeIndex wraps to 0 (A is now active), roundNumber unchanged.
|
||
|
||
5. **Given** an encounter with combatants [A] and activeIndex 0, **When** the GM removes combatant A, **Then** the encounter has [], activeIndex is 0, roundNumber unchanged.
|
||
|
||
6. **Given** an encounter with combatants [A, B, C], **When** the GM attempts to remove a combatant with an ID that does not exist, **Then** a domain error is returned with code `"combatant-not-found"`, and the encounter is unchanged.
|
||
|
||
---
|
||
|
||
**Story B2 — Inline confirmation before removing (Priority: P1)**
|
||
|
||
A user clicking the remove (X) button on a combatant row is protected from accidental deletion by a two-step inline confirmation flow.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** a combatant row is visible, **When** the user clicks the remove (X) button once, **Then** the button transitions to a confirm state showing a checkmark icon on a red/danger background with a scale pulse animation.
|
||
|
||
2. **Given** the remove button is in confirm state, **When** the user clicks it again, **Then** the combatant is removed from the encounter.
|
||
|
||
3. **Given** the remove button is in confirm state, **When** 5 seconds elapse without a second click, **Then** the button reverts to its original X icon and default styling.
|
||
|
||
4. **Given** the remove button is in confirm state, **When** the user clicks outside the button, **Then** the button reverts to its original state without removing the combatant.
|
||
|
||
5. **Given** the remove button is in confirm state, **When** the user presses Escape, **Then** the button reverts to its original state without removing the combatant.
|
||
|
||
6. **Given** a destructive button has keyboard focus, **When** the user presses Enter or Space, **Then** the button enters confirm state.
|
||
|
||
7. **Given** a destructive button is in confirm state with focus, **When** the user presses Enter or Space, **Then** the destructive action executes.
|
||
|
||
8. **Given** a destructive button is in confirm state with focus, **When** the user presses Escape, **Then** the button reverts to its original state.
|
||
|
||
9. **Given** a destructive button is in confirm state, **When** the button loses focus (e.g., Tab away), **Then** the button reverts to its original state.
|
||
|
||
---
|
||
|
||
### Editing Combatants
|
||
|
||
**Story C1 — Rename a combatant (Priority: P1)**
|
||
|
||
A user running an encounter realizes a combatant's name is misspelled or wants to change it. They select the combatant by its identity, provide a new name, and the system updates the combatant in-place while preserving turn order and round state.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an encounter with combatants [Alice, Bob], **When** the user updates Bob's name to "Robert", **Then** the encounter contains [Alice, Robert] and a `CombatantUpdated` event is emitted with the combatant's id, old name, and new name.
|
||
|
||
2. **Given** an encounter with combatants [Alice, Bob] where Bob is the active combatant, **When** the user updates Bob's name to "Robert", **Then** Bob remains the active combatant (active index unchanged) and the round number is preserved.
|
||
|
||
---
|
||
|
||
**Story C2 — Error feedback on invalid edit (Priority: P2)**
|
||
|
||
A user attempts to edit a combatant that no longer exists or provides an invalid name. The system returns a clear error without modifying the encounter.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an encounter with combatants [Alice, Bob], **When** the user attempts to update a combatant with a non-existent id, **Then** the system returns a "combatant not found" error and the encounter is unchanged.
|
||
|
||
2. **Given** an encounter with combatants [Alice, Bob], **When** the user attempts to update Alice's name to an empty string, **Then** the system returns an "invalid name" error and the encounter is unchanged.
|
||
|
||
3. **Given** an encounter with combatants [Alice, Bob], **When** the user attempts to update Alice's name to a whitespace-only string, **Then** the system returns an "invalid name" error and the encounter is unchanged.
|
||
|
||
---
|
||
|
||
**Story C3 — Rename trigger UX (Priority: P1)**
|
||
|
||
A user wants to rename a combatant. Clicking the combatant's name immediately enters inline edit mode — no delay, no timer, consistent for all combatant types. A `cursor-text` cursor on hover signals that the name is editable. Stat block access is handled separately via a dedicated book icon (see `specs/004-bestiary/spec.md`, FR-062).
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** a combatant row is visible, **When** the user clicks the combatant name, **Then** inline edit mode is entered immediately for that combatant's name — no delay or timer.
|
||
|
||
2. **Given** a combatant row is visible on a pointer device, **When** the user hovers over the combatant name, **Then** the cursor changes to a text cursor (`cursor-text`) to signal editability.
|
||
|
||
3. **Given** inline edit mode has been entered, **When** the user types a new name and presses Enter or blurs the field, **Then** the name is committed. **When** the user presses Escape, **Then** the edit is cancelled and the original name is restored.
|
||
|
||
4. **Given** a bestiary combatant row and a custom combatant row, **When** the user clicks either combatant's name, **Then** the behavior is identical — inline edit mode is entered immediately in both cases.
|
||
|
||
---
|
||
|
||
### Clearing the Encounter
|
||
|
||
**Story D1 — Clear encounter to start fresh (Priority: P1)**
|
||
|
||
As a DM who has just finished a combat encounter, I want to clear the entire encounter with a single confirmed action so that I can quickly set up a new combat without manually removing each combatant one by one.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an encounter with multiple combatants at round 3, **When** the user activates the clear encounter action and confirms, **Then** all combatants are removed, the round number resets to 1, and the active turn index resets to 0.
|
||
|
||
2. **Given** an encounter with a single combatant, **When** the user activates the clear encounter action and confirms, **Then** the encounter is fully cleared.
|
||
|
||
3. **Given** an encounter has no combatants, **When** the user views the clear button, **Then** it is disabled and cannot be activated.
|
||
|
||
---
|
||
|
||
**Story D2 — Inline confirmation before clearing (Priority: P1)**
|
||
|
||
A user clicks the trash button to clear the entire encounter. Instead of a browser confirm dialog, the trash button itself transitions into a red confirm state with a checkmark icon and a scale pulse. A second click clears the encounter; otherwise the button reverts after 5 seconds or on dismiss.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an encounter has combatants, **When** the user clicks the clear encounter (trash) button once, **Then** the button transitions to a confirm state with a checkmark icon on a red/danger background with a scale pulse animation.
|
||
|
||
2. **Given** the trash button is in confirm state, **When** the user clicks it again, **Then** the entire encounter is cleared.
|
||
|
||
3. **Given** the trash button is in confirm state, **When** 5 seconds pass, the user clicks outside, or the user presses Escape, **Then** the button reverts to its original trash icon and default styling without clearing the encounter.
|
||
|
||
4. **Given** a confirmation prompt is displayed, **When** the user cancels, **Then** the encounter remains unchanged.
|
||
|
||
---
|
||
|
||
### Persistence
|
||
|
||
**Story E1 — Encounter survives page reload (Priority: P1)**
|
||
|
||
A user is managing a combat encounter. They accidentally refresh the page or their browser restarts. When the page reloads, the encounter is restored exactly as it was — same combatants, same active turn, same round number.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** an encounter with combatants, active turn, and round number, **When** the user reloads the page, **Then** the encounter is restored with all state intact.
|
||
|
||
2. **Given** an encounter that has been modified (combatant added, removed, or renamed), **When** the user reloads the page, **Then** the latest state is reflected.
|
||
|
||
3. **Given** the user advances the turn multiple times, **When** the user reloads the page, **Then** the active turn and round number are preserved.
|
||
|
||
---
|
||
|
||
**Story E2 — Fresh start with no saved data (Priority: P2)**
|
||
|
||
A first-time user opens the application with no previously saved encounter. The application shows the default demo encounter so the user can immediately start exploring.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** no saved encounter exists in the browser, **When** the user opens the application, **Then** the default demo encounter is displayed.
|
||
|
||
2. **Given** saved encounter data has been manually cleared from the browser, **When** the user opens the application, **Then** the default demo encounter is displayed.
|
||
|
||
---
|
||
|
||
**Story E3 — Graceful handling of corrupt data (Priority: P3)**
|
||
|
||
Saved data may become invalid (e.g., manually edited in dev tools, schema changes between versions). The application handles this gracefully rather than crashing.
|
||
|
||
**Acceptance Scenarios**:
|
||
|
||
1. **Given** the saved encounter data is malformed or unparseable, **When** the user opens the application, **Then** the default demo encounter is displayed and the corrupt data is discarded.
|
||
|
||
2. **Given** the saved data is missing required fields, **When** the user opens the application, **Then** the default demo encounter is displayed.
|
||
|
||
---
|
||
|
||
## Domain Model
|
||
|
||
### Key Entities
|
||
|
||
- **Combatant**: An identified participant with a unique `CombatantId` (branded string), a required non-empty `name`, and optional `initiative`, `maxHp`, `currentHp`, `ac`, `conditions`, `isConcentrating`, and `creatureId` fields.
|
||
- **Encounter**: The aggregate root. Contains an ordered `readonly` list of combatants, an `activeIndex` (zero-based integer), and a `roundNumber` (positive integer, starting at 1).
|
||
> Queued Creature and Custom Creature Input entities are defined in `specs/004-bestiary/spec.md`.
|
||
|
||
### Domain Events
|
||
|
||
- **CombatantAdded**: Emitted on every successful AddCombatant. Carries: `combatantId`, `name`, `position` (zero-based index).
|
||
- **CombatantRemoved**: Emitted on every successful RemoveCombatant. Carries: `combatantId`, `name`.
|
||
- **CombatantUpdated**: Emitted on every successful EditCombatant. Carries: `combatantId`, `oldName`, `newName`.
|
||
|
||
### Invariants
|
||
|
||
- **INV-1**: An encounter MAY have zero combatants (after clearing or removing the last combatant).
|
||
- **INV-2**: If `combatants.length > 0`, `activeIndex` MUST satisfy `0 <= activeIndex < combatants.length`. If `combatants.length == 0`, `activeIndex` MUST be 0.
|
||
- **INV-3**: `roundNumber` MUST be a positive integer (>= 1) and MUST only increase during normal turn advancement. Clearing resets it to 1.
|
||
- **INV-4**: `CombatantId` values MUST be unique within an encounter.
|
||
- **INV-5**: All domain state transitions (add, remove, edit, clear) are pure functions; no I/O, randomness, or clocks.
|
||
- **INV-6**: Every successful state transition emits exactly one corresponding domain event. No silent state changes.
|
||
- **INV-7**: AddCombatant and RemoveCombatant MUST NOT change the `roundNumber`.
|
||
- **INV-8**: EditCombatant MUST NOT change `activeIndex`, `roundNumber`, or the combatant's position in the list.
|
||
|
||
---
|
||
|
||
## Requirements *(mandatory)*
|
||
|
||
### Functional Requirements
|
||
|
||
#### FR-001 — Add: Append combatant
|
||
The domain MUST expose an AddCombatant operation that accepts an Encounter and a combatant name (plus a pre-generated `CombatantId`), and returns the updated Encounter plus emitted domain events. The new combatant MUST be appended to the end of the combatants list.
|
||
|
||
#### FR-002 — Add: Reject invalid names
|
||
AddCombatant MUST reject empty or whitespace-only names by returning a `DomainError` without modifying state or emitting events. Name validation trims whitespace; a name that is empty after trimming is invalid.
|
||
|
||
#### FR-003 — Add: Preserve activeIndex and roundNumber
|
||
AddCombatant MUST NOT alter the `activeIndex` or `roundNumber` of the encounter.
|
||
|
||
#### FR-004 — Add: Unique CombatantId
|
||
AddCombatant MUST assign a unique `CombatantId` to the new combatant. Id generation is the caller's responsibility (application layer), keeping the domain function pure.
|
||
|
||
#### FR-005 — Add: Duplicate names allowed
|
||
Duplicate combatant names are permitted. Combatants are distinguished solely by their unique `CombatantId`.
|
||
|
||
#### FR-006 — Add: UI form
|
||
The UI MUST provide an add-combatant form accessible from the bottom bar. The search field MUST display action-oriented placeholder text (e.g., "Search creatures to add...").
|
||
|
||
> FR-007 through FR-013 (batch add and custom creature) are defined in `specs/004-bestiary/spec.md` (FR-007–FR-015).
|
||
|
||
#### FR-014 — Remove: Domain operation
|
||
The domain MUST expose a RemoveCombatant operation that accepts an Encounter and a `CombatantId`, and returns the updated Encounter plus emitted domain events.
|
||
|
||
#### FR-015 — Remove: Error on unknown ID
|
||
RemoveCombatant MUST return a domain error with code `"combatant-not-found"` when the given `CombatantId` does not match any combatant in the encounter.
|
||
|
||
#### FR-016 — Remove: activeIndex adjustment
|
||
RemoveCombatant MUST adjust `activeIndex` according to these rules:
|
||
- Removed combatant is **after** the active one: `activeIndex` unchanged.
|
||
- Removed combatant is **before** the active one: `activeIndex` decrements by 1.
|
||
- Removed combatant **is** the active one and is not last: `activeIndex` stays at the same integer value (the next combatant in line becomes active).
|
||
- Removed combatant **is** the active one and **is last**: `activeIndex` wraps to 0.
|
||
- Last remaining combatant is removed (encounter becomes empty): `activeIndex` is set to 0.
|
||
|
||
#### FR-017 — Remove: roundNumber preserved
|
||
RemoveCombatant MUST preserve `roundNumber` unchanged.
|
||
|
||
#### FR-018 — Remove: UI control
|
||
The UI MUST provide a remove control for each combatant row.
|
||
|
||
#### FR-019 — Remove: ConfirmButton
|
||
The remove control MUST use the `ConfirmButton` two-step confirmation pattern (see FR-025 through FR-030). Silent no-op on domain error (combatant already gone).
|
||
|
||
#### FR-020 — Edit: Domain operation
|
||
The domain MUST expose an EditCombatant operation that accepts an Encounter, a `CombatantId`, and a new name, and returns the updated Encounter plus emitted domain events.
|
||
|
||
#### FR-021 — Edit: Error on unknown ID
|
||
EditCombatant MUST return a `"combatant-not-found"` error when the provided id does not match any combatant.
|
||
|
||
#### FR-022 — Edit: Reject invalid names
|
||
EditCombatant MUST return an `"invalid-name"` error when the new name is empty or whitespace-only. The same trimming rules as AddCombatant apply.
|
||
|
||
#### FR-023 — Edit: Preserve position and counters
|
||
EditCombatant MUST preserve the combatant's position in the list, `activeIndex`, and `roundNumber`. Setting a name to the same value it already has is treated as a valid update; a `CombatantUpdated` event is still emitted.
|
||
|
||
#### FR-024 — Edit: UI
|
||
The UI MUST provide an inline name-edit mechanism for each combatant, activated by a single click on the name. Clicking the name MUST enter inline edit mode immediately — no delay, no timer, consistent for all combatant types. The name MUST display a `cursor-text` cursor on hover to signal editability. The updated name MUST be immediately visible after submission. The 250ms click timer and double-click detection logic MUST be removed entirely.
|
||
|
||
#### FR-025 — ConfirmButton: Reusable component
|
||
The system MUST provide a reusable `ConfirmButton` component that wraps any icon button to add a two-step confirmation flow.
|
||
|
||
#### FR-026 — ConfirmButton: Confirm state on first activation
|
||
On first activation (click, Enter, or Space), the button MUST transition to a confirm state displaying a checkmark icon on a red/danger background with a scale pulse animation.
|
||
|
||
#### FR-027 — ConfirmButton: Auto-revert after 5 seconds
|
||
The button MUST automatically revert to its original state after 5 seconds if not confirmed.
|
||
|
||
#### FR-028 — ConfirmButton: Cancel on outside click, Escape, or focus loss
|
||
Clicking outside the button, pressing Escape, or moving focus away MUST cancel the confirm state and revert the button.
|
||
|
||
#### FR-029 — ConfirmButton: Execute on second activation
|
||
A second activation (click, Enter, or Space) while in confirm state MUST execute the destructive action.
|
||
|
||
#### FR-030 — ConfirmButton: Independent state per instance
|
||
Each `ConfirmButton` instance MUST manage its confirm state independently of other instances.
|
||
|
||
#### FR-031 — Clear: Domain operation
|
||
The domain MUST expose a ClearEncounter operation that removes all combatants, resets `roundNumber` to 1, and resets `activeIndex` to 0.
|
||
|
||
#### FR-032 — Clear: UI button with ConfirmButton
|
||
The UI MUST provide a clear encounter button that uses the `ConfirmButton` pattern. The button MUST be disabled when the encounter has no combatants.
|
||
|
||
#### FR-033 — Clear: Cancellation leaves state unchanged
|
||
Cancelling the confirmation (via timeout, outside click, Escape, or focus loss) MUST leave the encounter completely unchanged.
|
||
|
||
#### FR-034 — Clear: Cleared state persisted
|
||
After clearing, the empty encounter state MUST be persisted so that a page refresh does not restore the previous encounter.
|
||
|
||
#### FR-035 — Persistence: Save on every change
|
||
The system MUST save the full encounter state (combatants, `activeIndex`, `roundNumber`) to browser `localStorage` after every state change.
|
||
|
||
#### FR-036 — Persistence: Restore on load
|
||
The system MUST restore the saved encounter state when the application loads, if valid saved data exists.
|
||
|
||
#### FR-037 — Persistence: Fallback to demo encounter
|
||
The system MUST fall back to the default demo encounter when no saved data exists or saved data is invalid/corrupt.
|
||
|
||
#### FR-038 — Persistence: No crash on storage failure
|
||
The system MUST NOT crash or show an error to the user when storage is unavailable or data is corrupt. When storage is unavailable, the application falls back to in-memory-only behavior.
|
||
|
||
#### FR-039 — Persistence: Preserve combatant identity across reloads
|
||
The system MUST preserve combatant `CombatantId` values, names, and any other persisted fields across reloads, so that new combatants added after a reload do not collide with existing IDs.
|
||
|
||
#### FR-040 — Domain events as values
|
||
All domain events MUST be returned as plain data values from operations, not dispatched via side effects.
|
||
|
||
---
|
||
|
||
## Edge Cases
|
||
|
||
- **Empty name**: AddCombatant and EditCombatant return a `DomainError`; state and events are unchanged.
|
||
- **Whitespace-only name**: Treated identically to empty name after trimming.
|
||
- **Adding to an empty encounter**: The new combatant becomes the first and only participant; `activeIndex` remains 0.
|
||
- **Adding during mid-round**: `activeIndex` is never shifted by an add operation.
|
||
- **Duplicate combatant names**: Permitted. Combatants are distinguished by `CombatantId`.
|
||
- **Removing the last combatant**: Encounter becomes empty; `activeIndex` is set to 0.
|
||
- **Removing with unknown ID**: Returns `"combatant-not-found"` error; state unchanged. Removing the same ID twice: second call returns an error.
|
||
- **Removing from empty encounter**: Covered by the unknown-ID error (no IDs exist).
|
||
- **Editing a combatant to the same name**: Valid; `CombatantUpdated` event is still emitted.
|
||
- **Editing a combatant in an empty encounter**: Returns `"combatant-not-found"` error.
|
||
- **Clearing an already empty encounter**: The clear button is disabled; no operation is executed.
|
||
- **Clearing and reloading**: The empty (cleared) state is persisted; the previous encounter is not restored.
|
||
- **Storage quota exceeded**: Persistence silently fails; current in-memory session continues normally.
|
||
- **Multiple browser tabs**: MVP baseline does not include cross-tab synchronization. Each tab operates independently; the last tab to save wins.
|
||
> Batch add and custom creature edge cases are defined in `specs/004-bestiary/spec.md`.
|
||
- **ConfirmButton: rapid triple-click**: First click enters confirm state; second executes the action; subsequent clicks are no-ops.
|
||
- **ConfirmButton: component unmounts in confirm state**: The auto-revert timer MUST be cleaned up to prevent memory leaks or stale state updates.
|
||
- **ConfirmButton: two instances in confirm state simultaneously**: Each manages its own state independently.
|
||
- **ConfirmButton: combatant row re-renders while in confirm state**: Confirm state persists through re-renders as long as combatant identity is stable.
|
||
- **Name click behavior is uniform**: A single click on any combatant's name enters inline edit mode immediately. There is no gesture disambiguation (no timer, no double-click detection). Stat block access is handled via the dedicated book icon on bestiary rows (see `specs/004-bestiary/spec.md`, FR-062).
|
||
|
||
---
|
||
|
||
## Success Criteria *(mandatory)*
|
||
|
||
- **SC-001**: All add-combatant acceptance scenarios pass as deterministic, pure-function tests with no I/O dependencies.
|
||
- **SC-002**: Adding a combatant to an encounter preserves all existing combatants, their order, `activeIndex`, and `roundNumber` unchanged.
|
||
- **SC-003**: All six remove-combatant acceptance scenarios pass as automated tests covering every `activeIndex` adjustment rule.
|
||
- **SC-004**: The round number never changes as a result of a remove operation.
|
||
- **SC-005**: Users can rename any combatant in the encounter in a single action without disrupting turn order, active combatant, or round number.
|
||
- **SC-006**: Invalid edit attempts (missing combatant, empty or whitespace-only name) produce a domain error with no state change and no emitted events.
|
||
- **SC-007**: All destructive actions (remove combatant, clear encounter) require exactly two deliberate user interactions to execute, eliminating single-click accidental mutations.
|
||
- **SC-008**: The `ConfirmButton` confirm state auto-reverts reliably after 5 seconds. All confirmation flows are fully operable via keyboard alone.
|
||
> SC-009 and SC-010 (batch add and custom creature success criteria) are defined in `specs/004-bestiary/spec.md`.
|
||
- **SC-011**: Users can reload the page and see their encounter fully restored, with zero data loss.
|
||
- **SC-012**: First-time users see the demo encounter immediately on first visit with no extra steps.
|
||
- **SC-013**: 100% of corrupt or missing data scenarios result in a usable application (demo encounter displayed), never a crash or blank screen.
|
||
- **SC-014**: After clearing, the encounter tracker displays an empty state with round and turn counters at their initial values, and this state persists across page refreshes.
|
||
- **SC-015**: The domain module has zero imports from the application, adapter, or UI layers (layer boundary compliance verified by automated check).
|
||
|
||
---
|
||
|
||
## Assumptions
|
||
|
||
- `CombatantId` generation is the caller's responsibility (application layer), keeping domain functions pure and deterministic.
|
||
- Name validation trims whitespace; a name that is empty after trimming is invalid.
|
||
- No uniqueness constraint on combatant names — multiple combatants may share the same name.
|
||
- Clearing results in an empty encounter state (no combatants, `roundNumber` 1, `activeIndex` 0). The user will then add new combatants using the existing add-combatant flow.
|
||
- MVP baseline does not include undo/restore functionality after clearing or removing. Once confirmed, the action is final.
|
||
- MVP baseline does not include encounter history or the ability to save/archive encounters before clearing.
|
||
- A single `localStorage` key is sufficient for the MVP (one encounter at a time).
|
||
- Cross-tab synchronization is not required for the MVP baseline.
|
||
- The `ConfirmButton` 5-second timeout is a fixed value and is not configurable in the MVP baseline.
|
||
- The `Check` icon from the Lucide icon library is used for the `ConfirmButton` confirm state.
|
||
- The inline name-edit mechanism is activated by a single click on the name. A `cursor-text` cursor on hover signals editability. There is no double-click or long-press gesture; stat block access uses a dedicated book icon on bestiary rows.
|