diff --git a/src/store.ts b/src/store.ts index 2a929ab..7288c8f 100644 --- a/src/store.ts +++ b/src/store.ts @@ -197,6 +197,25 @@ export function fireStoryInit(): void { for (const cb of storyInitListeners) cb(); } +// --------------------------------------------------------------------------- +// beforerestart callbacks +// --------------------------------------------------------------------------- + +type BeforeRestartListener = () => void; +let beforeRestartListeners: BeforeRestartListener[] = []; + +/** Register a callback to run before restart resets any state. */ +export function onBeforeRestart(cb: BeforeRestartListener): () => void { + beforeRestartListeners.push(cb); + return () => { + beforeRestartListeners = beforeRestartListeners.filter((l) => l !== cb); + }; +} + +function fireBeforeRestart(): void { + for (const cb of beforeRestartListeners) cb(); +} + // --------------------------------------------------------------------------- // Store // --------------------------------------------------------------------------- @@ -497,6 +516,7 @@ export const useStoryStore = create()( const startPassage = storyData.passagesById.get(storyData.startNode); if (!startPassage) return; + fireBeforeRestart(); resetPRNG(); resetTriggers(); const initialVars = deepClone(variableDefaults); diff --git a/src/story-api.ts b/src/story-api.ts index d79bb84..c31126d 100644 --- a/src/story-api.ts +++ b/src/story-api.ts @@ -1,4 +1,4 @@ -import { useStoryStore, onStoryInit } from './store'; +import { useStoryStore, onStoryInit, onBeforeRestart } from './store'; import type { Passage } from './parser'; import { settings } from './settings'; import type { @@ -70,6 +70,7 @@ export function _resetReadyState(): void { type NavigateCallback = (to: string, from: string) => void; type StoryInitCallback = () => void; +type BeforeRestartCallback = () => void; type ActionsChangedCallback = () => void; type VariableChangedCallback = ( changed: Record, @@ -119,6 +120,7 @@ export interface StoryAPI { getActions(): StoryAction[]; performAction(id: string, value?: unknown): void; on(event: 'navigate', callback: NavigateCallback): () => void; + on(event: 'beforerestart', callback: BeforeRestartCallback): () => void; on(event: 'storyinit', callback: StoryInitCallback): () => void; on(event: 'actionsChanged', callback: ActionsChangedCallback): () => void; on(event: 'variableChanged', callback: VariableChangedCallback): () => void; @@ -378,6 +380,10 @@ function createStoryAPI(): StoryAPI { }); } + if (event === 'beforerestart') { + return onBeforeRestart(callback as BeforeRestartCallback); + } + if (event === 'storyinit') { return onStoryInit(callback as StoryInitCallback); } diff --git a/test/unit/story-api.test.ts b/test/unit/story-api.test.ts index 36a062d..6c2eecb 100644 --- a/test/unit/story-api.test.ts +++ b/test/unit/story-api.test.ts @@ -286,6 +286,72 @@ describe('StoryAPI', () => { }); }); + describe('on(beforerestart)', () => { + it('fires callback before variables are reset', () => { + const cb = vi.fn(); + const unsub = Story.on('beforerestart', cb); + + useStoryStore.getState().setVariable('health', 30); + useStoryStore.getState().navigate('Room'); + + useStoryStore.getState().restart(); + + expect(cb).toHaveBeenCalledTimes(1); + unsub(); + }); + + it('fires before storyinit', () => { + const order: string[] = []; + const unsubBefore = Story.on('beforerestart', () => { + order.push('beforerestart'); + }); + const unsubInit = Story.on('storyinit', () => { + order.push('storyinit'); + }); + + useStoryStore.getState().restart(); + + expect(order).toEqual(['beforerestart', 'storyinit']); + unsubBefore(); + unsubInit(); + }); + + it('can read pre-restart variables inside callback', () => { + useStoryStore.getState().setVariable('health', 42); + useStoryStore.getState().navigate('Room'); + + let capturedHealth: unknown; + const unsub = Story.on('beforerestart', () => { + capturedHealth = useStoryStore.getState().variables.health; + }); + + useStoryStore.getState().restart(); + + expect(capturedHealth).toBe(42); + unsub(); + }); + + it('fires on every restart', () => { + const cb = vi.fn(); + const unsub = Story.on('beforerestart', cb); + + useStoryStore.getState().restart(); + useStoryStore.getState().restart(); + useStoryStore.getState().restart(); + expect(cb).toHaveBeenCalledTimes(3); + unsub(); + }); + + it('unsubscribe stops future callbacks', () => { + const cb = vi.fn(); + const unsub = Story.on('beforerestart', cb); + unsub(); + + useStoryStore.getState().restart(); + expect(cb).not.toHaveBeenCalled(); + }); + }); + describe('on(unknown event)', () => { it('throws for unknown event', () => { expect(() => {