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>
This commit is contained in:
Lukas
2026-03-26 23:30:33 +01:00
parent 9d81c8ad27
commit 17cc6ed72c
22 changed files with 1127 additions and 61 deletions

View File

@@ -10,7 +10,9 @@ export type {
BestiarySourceCache,
EncounterStore,
PlayerCharacterStore,
UndoRedoStore,
} from "./ports.js";
export { redoUseCase } from "./redo-use-case.js";
export { removeCombatantUseCase } from "./remove-combatant-use-case.js";
export { retreatTurnUseCase } from "./retreat-turn-use-case.js";
export {
@@ -24,3 +26,4 @@ export { setInitiativeUseCase } from "./set-initiative-use-case.js";
export { setTempHpUseCase } from "./set-temp-hp-use-case.js";
export { toggleConcentrationUseCase } from "./toggle-concentration-use-case.js";
export { toggleConditionUseCase } from "./toggle-condition-use-case.js";
export { undoUseCase } from "./undo-use-case.js";

View File

@@ -3,6 +3,7 @@ import type {
CreatureId,
Encounter,
PlayerCharacter,
UndoRedoState,
} from "@initiative/domain";
export interface EncounterStore {
@@ -19,3 +20,8 @@ export interface PlayerCharacterStore {
getAll(): PlayerCharacter[];
save(characters: PlayerCharacter[]): void;
}
export interface UndoRedoStore {
get(): UndoRedoState;
save(state: UndoRedoState): void;
}

View File

@@ -0,0 +1,24 @@
import {
type DomainError,
type Encounter,
isDomainError,
redo,
} from "@initiative/domain";
import type { EncounterStore, UndoRedoStore } from "./ports.js";
export function redoUseCase(
encounterStore: EncounterStore,
undoRedoStore: UndoRedoStore,
): Encounter | DomainError {
const current = encounterStore.get();
const state = undoRedoStore.get();
const result = redo(state, current);
if (isDomainError(result)) {
return result;
}
encounterStore.save(result.encounter);
undoRedoStore.save(result.state);
return result.encounter;
}

View File

@@ -0,0 +1,24 @@
import {
type DomainError,
type Encounter,
isDomainError,
undo,
} from "@initiative/domain";
import type { EncounterStore, UndoRedoStore } from "./ports.js";
export function undoUseCase(
encounterStore: EncounterStore,
undoRedoStore: UndoRedoStore,
): Encounter | DomainError {
const current = encounterStore.get();
const state = undoRedoStore.get();
const result = undo(state, current);
if (isDomainError(result)) {
return result;
}
encounterStore.save(result.encounter);
undoRedoStore.save(result.state);
return result.encounter;
}