Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -497,6 +516,7 @@ export const useStoryStore = create<StoryState>()(
const startPassage = storyData.passagesById.get(storyData.startNode);
if (!startPassage) return;

fireBeforeRestart();
resetPRNG();
resetTriggers();
const initialVars = deepClone(variableDefaults);
Expand Down
8 changes: 7 additions & 1 deletion src/story-api.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<string, { from: unknown; to: unknown }>,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -378,6 +380,10 @@ function createStoryAPI(): StoryAPI {
});
}

if (event === 'beforerestart') {
return onBeforeRestart(callback as BeforeRestartCallback);
}

if (event === 'storyinit') {
return onStoryInit(callback as StoryInitCallback);
}
Expand Down
66 changes: 66 additions & 0 deletions test/unit/story-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Loading