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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `Story.on('storyinit', callback)` event that fires after `StoryInit` completes — on initial boot and after every `restart()` call (including `Story.storage.clearGameData()` and `Story.storage.clearAllData()`). Allows external state engines to reliably re-sync after a restart. ([#115](https://github.com/rohal12/spindle/issues/115))
- Tooling API: `Story.getMacroRegistry()` returns metadata for all registered macros (built-in and user-defined) — name, block status, sub-macros, feature flags, source origin, and optional description/parameters
- `@rohal12/spindle/tooling` entry point for Node.js/LSP use — lightweight `defineMacro()` shim that captures metadata without Preact, pre-loaded with builtin metadata from build-time JSON
- Optional `description` and `parameters` fields on `defineMacro()` config for tooling hints (LSP hover docs, completions, parameter info)
Expand Down
5 changes: 5 additions & 0 deletions docs/story-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ Story.on('actionsChanged', function () {
console.log('Actions:', Story.getActions().length);
});

// Story initialization (fires on boot and after every restart)
Story.on('storyinit', function () {
console.log('Story initialized — re-sync external state here');
});

// Variable changes
Story.on('variableChanged', function (changed) {
// changed = { health: { from: 100, to: 90 }, ... }
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function boot() {

// Execute StoryInit passage if it exists
executeStoryInit();
useStoryStore.getState().bumpInitCount();

// Restore session state if the page was refreshed
const sessionPayload = loadSession(storyData.ifid);
Expand Down
10 changes: 10 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,15 @@ export interface StoryState {
renderCounts: Record<string, number>;
knownSaves: Record<string, true>;
playthroughId: string;
initCount: number;
maxHistory: number;
saveError: string | null;
loadError: string | null;
transitionConfig: TransitionConfig | null;
nextTransition: TransitionConfig | null;
nobr: boolean;

bumpInitCount: () => void;
setMaxHistory: (limit: number) => void;
init: (
storyData: StoryData,
Expand Down Expand Up @@ -258,13 +260,20 @@ export const useStoryStore = create<StoryState>()(
renderCounts: {},
knownSaves: {},
playthroughId: '',
initCount: 0,
maxHistory: 40,
saveError: null,
loadError: null,
transitionConfig: null,
nextTransition: null,
nobr: false,

bumpInitCount: () => {
set((state) => {
state.initCount++;
});
},

setMaxHistory: (limit: number) => {
set((state) => {
state.maxHistory = Math.max(1, Math.round(limit));
Expand Down Expand Up @@ -494,6 +503,7 @@ export const useStoryStore = create<StoryState>()(

lastNavigationVars = get().variables;
executeStoryInit();
get().bumpInitCount();
clearSession(storyData.ifid);

// Start a new playthrough on restart
Expand Down
12 changes: 12 additions & 0 deletions src/story-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type { StoryAction };
export type { MacroMetadata };

type NavigateCallback = (to: string, from: string) => void;
type StoryInitCallback = () => void;
type ActionsChangedCallback = () => void;
type VariableChangedCallback = (
changed: Record<string, { from: unknown; to: unknown }>,
Expand Down Expand Up @@ -100,6 +101,7 @@ export interface StoryAPI {
getActions(): StoryAction[];
performAction(id: string, value?: unknown): void;
on(event: 'navigate', callback: NavigateCallback): () => void;
on(event: 'storyinit', callback: StoryInitCallback): () => void;
on(event: 'actionsChanged', callback: ActionsChangedCallback): () => void;
on(event: 'variableChanged', callback: VariableChangedCallback): () => void;
waitForActions(): Promise<StoryAction[]>;
Expand Down Expand Up @@ -356,6 +358,16 @@ function createStoryAPI(): StoryAPI {
});
}

if (event === 'storyinit') {
let prevCount = useStoryStore.getState().initCount;
return useStoryStore.subscribe((state) => {
if (state.initCount !== prevCount) {
prevCount = state.initCount;
(callback as StoryInitCallback)();
}
});
}

if (event === 'actionsChanged') {
return onActionsChanged(callback as ActionsChangedCallback);
}
Expand Down
44 changes: 44 additions & 0 deletions test/unit/story-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,50 @@ describe('StoryAPI', () => {
});
});

describe('on(storyinit)', () => {
it('fires callback on restart via initCount', () => {
const cb = vi.fn();
let prevCount = useStoryStore.getState().initCount;
const unsub = useStoryStore.subscribe((state) => {
if (state.initCount !== prevCount) {
prevCount = state.initCount;
cb();
}
});

useStoryStore.getState().navigate('Room');
expect(cb).not.toHaveBeenCalled();

useStoryStore.getState().restart();
// restart() calls bumpInitCount internally
expect(cb).toHaveBeenCalledTimes(1);
unsub();
});

it('fires callback via Story.on API', () => {
const cb = vi.fn();
const unsub = Story.on('storyinit', cb);

useStoryStore.getState().navigate('Room');
expect(cb).not.toHaveBeenCalled();

useStoryStore.getState().restart();
expect(cb).toHaveBeenCalledTimes(1);
unsub();
});

it('fires on every restart', () => {
const cb = vi.fn();
const unsub = Story.on('storyinit', cb);

useStoryStore.getState().restart();
useStoryStore.getState().restart();
useStoryStore.getState().restart();
expect(cb).toHaveBeenCalledTimes(3);
unsub();
});
});

describe('on(unknown event)', () => {
it('throws for unknown event', () => {
expect(() => {
Expand Down
Loading