Files
initiative/specs/037-undo-redo/spec.md
Lukas 17cc6ed72c Add undo/redo for all encounter actions
Memento-based undo/redo with full encounter snapshots. Undo stack
capped at 50 entries, persisted to localStorage. Triggered via
buttons in the top bar (inboard of turn navigation) and keyboard
shortcuts (Ctrl+Z / Ctrl+Shift+Z, Cmd on Mac, case-insensitive key
matching). Clear encounter resets both stacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:30:33 +01:00

124 lines
8.1 KiB
Markdown

# Feature Specification: Undo/Redo
**Feature Branch**: `037-undo-redo`
**Created**: 2026-03-26
**Status**: Draft
**Input**: Gitea issue #16 — Undo/redo for encounter actions
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Undo a Mistake (Priority: P1)
A DM accidentally removes the wrong combatant, changes HP incorrectly, or advances the turn too early. They press an undo button (or keyboard shortcut) and the encounter returns to exactly the state it was in before that action.
**Why this priority**: Mistakes during live combat are stressful and time-sensitive. Undo is the core value proposition — without it, the DM must manually reconstruct state, which disrupts the game.
**Independent Test**: Can be fully tested by performing any encounter action, pressing undo, and verifying the encounter matches its pre-action state.
**Acceptance Scenarios**:
1. **Given** an encounter with 3 combatants, **When** the user removes a combatant and clicks Undo, **Then** the removed combatant reappears in the same position with all its stats intact.
2. **Given** a combatant with 30/45 HP, **When** the user adjusts HP to 20/45 and presses Undo, **Then** the combatant's HP returns to 30/45.
3. **Given** an encounter on round 3 turn 2, **When** the user advances the turn and presses Undo, **Then** the encounter returns to round 3 turn 2.
4. **Given** the undo stack is empty, **When** the user looks at the Undo button, **Then** it appears disabled and cannot be activated.
---
### User Story 2 - Redo an Undone Action (Priority: P2)
A DM presses undo but then realizes the original action was correct. They press redo to restore the undone change rather than re-entering it manually.
**Why this priority**: Redo complements undo — without it, undoing too far forces manual re-entry. Lower priority than undo because redo is used less frequently.
**Independent Test**: Can be tested by performing an action, undoing it, then redoing it, and verifying the state matches the post-action state.
**Acceptance Scenarios**:
1. **Given** the user has undone an HP adjustment, **When** they click Redo, **Then** the HP adjustment is reapplied exactly.
2. **Given** the user has undone two actions, **When** they click Redo twice, **Then** both actions are reapplied in order.
3. **Given** the user has undone an action and then performs a new action, **When** they look at the Redo button, **Then** it is disabled (the redo stack was cleared by the new action).
4. **Given** the redo stack is empty, **When** the user looks at the Redo button, **Then** it appears disabled and cannot be activated.
---
### User Story 3 - Keyboard Shortcuts (Priority: P3)
A DM who prefers keyboard interaction can undo with Ctrl+Z (Cmd+Z on Mac) and redo with Ctrl+Shift+Z (Cmd+Shift+Z on Mac) without reaching for buttons.
**Why this priority**: Keyboard shortcuts are a convenience layer. The feature is fully usable via buttons alone, so shortcuts are an enhancement.
**Independent Test**: Can be tested by pressing the keyboard shortcut and verifying the same behavior as the button.
**Acceptance Scenarios**:
1. **Given** the undo stack has entries, **When** the user presses Ctrl+Z (Cmd+Z on Mac), **Then** the most recent action is undone.
2. **Given** the redo stack has entries, **When** the user presses Ctrl+Shift+Z (Cmd+Shift+Z on Mac), **Then** the most recent undo is redone.
3. **Given** an input field or textarea has focus, **When** the user presses Ctrl+Z, **Then** the browser's native text undo fires instead of encounter undo.
4. **Given** no input has focus and the undo stack is empty, **When** the user presses Ctrl+Z, **Then** nothing happens.
---
### User Story 4 - Undo History Survives Refresh (Priority: P4)
A DM refreshes the page (or the browser restores the tab) and can still undo/redo recent actions, so history is not lost to accidental navigation.
**Why this priority**: Persistence is important for reliability but is secondary to the core undo/redo mechanics working correctly in-session.
**Independent Test**: Can be tested by performing actions, refreshing the page, and verifying the undo/redo buttons reflect the pre-refresh stack state.
**Acceptance Scenarios**:
1. **Given** the user has performed 5 actions, **When** they refresh the page, **Then** the undo stack contains 5 entries and undo works correctly.
2. **Given** the user has undone 2 of 5 actions, **When** they refresh the page, **Then** the undo stack has 3 entries and the redo stack has 2 entries.
---
### Edge Cases
- What happens when the undo stack reaches 50 entries? The oldest entry is dropped silently; the user can still undo the 50 most recent actions.
- What happens when the user clears the encounter? Both undo and redo stacks are reset to empty; there is no way to undo a clear.
- What happens when the user performs a new action after undoing? The redo stack is cleared entirely; the new action becomes the latest history entry.
- What happens if localStorage is full and the stacks cannot be persisted? The stacks continue to work in-memory for the current session; persistence is best-effort.
- What happens if persisted stack data is corrupt or invalid on load? The stacks start empty; the encounter itself loads normally from its own storage.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST capture a snapshot of the current encounter state before each state transition and push it onto the undo stack.
- **FR-002**: System MUST cap the undo stack at 50 entries, dropping the oldest entry when the cap is exceeded.
- **FR-003**: When the user triggers undo, system MUST restore the encounter to the most recent snapshot from the undo stack and push the current state onto the redo stack.
- **FR-004**: When the user triggers redo, system MUST restore the encounter to the most recent snapshot from the redo stack and push the current state onto the undo stack.
- **FR-005**: When the user performs any new encounter action (not undo/redo), system MUST clear the redo stack.
- **FR-006**: System MUST persist the undo and redo stacks to localStorage and restore them on page load.
- **FR-007**: System MUST display Undo and Redo buttons in the UI that are disabled when their respective stacks are empty.
- **FR-008**: System MUST support Ctrl+Z / Cmd+Z for undo and Ctrl+Shift+Z / Cmd+Shift+Z for redo.
- **FR-009**: System MUST suppress encounter undo/redo keyboard shortcuts when an input, textarea, or other text-editable element has focus, allowing native browser text editing behavior.
- **FR-010**: When the encounter is cleared, system MUST reset both undo and redo stacks to empty.
- **FR-011**: Undo/redo MUST operate on the full encounter snapshot (memento pattern), not on individual field changes.
### Key Entities
- **Undo Stack**: An ordered collection of encounter snapshots (most recent last), capped at 50 entries. Each entry is a complete encounter state captured before a state transition.
- **Redo Stack**: An ordered collection of encounter snapshots accumulated by undo operations. Cleared when any new (non-undo/redo) action occurs.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can reverse any single encounter action within 1 second via button or keyboard shortcut.
- **SC-002**: Users can undo and redo up to 50 sequential actions without data loss or state corruption.
- **SC-003**: Undo/redo history is preserved across page refresh with no user intervention.
- **SC-004**: Keyboard shortcuts do not interfere with native text editing in input fields.
## Assumptions
- The encounter data structure is small enough (tens of combatants) that storing 50 full snapshots in memory and localStorage is practical.
- The dependency on #15 (atomic addCombatant) is resolved, so each user action maps to exactly one snapshot.
- Player character template state is managed separately and is not part of the undo/redo scope — only encounter state is tracked.
- "Clear encounter" is an intentionally destructive action that should not be undoable, matching user expectations for a "reset" operation.
## Dependencies
- **#15 (Atomic addCombatant)**: Required so compound operations (add from bestiary, add from player character) produce a single state transition and thus a single undo entry.