diff --git a/src/engine/__tests__/save.test.ts b/src/engine/__tests__/save.test.ts index e65f78b..b0eaf37 100644 --- a/src/engine/__tests__/save.test.ts +++ b/src/engine/__tests__/save.test.ts @@ -58,6 +58,17 @@ describe('createSave()', () => { state.actionQueue.push('forage'); expect(save.state.actionQueue).toEqual(['forage', 'forage']); }); + + it('snapshots story fields in the save payload', () => { + const state = sampleState(); + state.storyFlags = { route_a: true }; + state.currentStoryNodeId = 'route_a_beat'; + state.seenStoryNodeIds = ['boot_intro', 'route_a_beat']; + const save = createSave(state, 1700); + expect(save.state.storyFlags).toEqual({ route_a: true }); + expect(save.state.currentStoryNodeId).toBe('route_a_beat'); + expect(save.state.seenStoryNodeIds).toHaveLength(2); + }); }); describe('serialize / deserialize round-trip', () => { diff --git a/src/engine/save.ts b/src/engine/save.ts index 7aa6059..f4c4cf5 100644 --- a/src/engine/save.ts +++ b/src/engine/save.ts @@ -29,6 +29,9 @@ export const gameStateSchema = z.object({ activeActionId: z.string().nullable(), actionElapsedMs: z.number().nonnegative(), actionQueue: z.array(z.string()).default([]), + storyFlags: z.record(z.string(), z.boolean()).default({}), + currentStoryNodeId: z.string().default(''), + seenStoryNodeIds: z.array(z.string()).default([]), }); export const saveSchema = z.object({ @@ -49,6 +52,9 @@ export function createSave(state: GameState, now: number): SaveData { activeActionId: state.activeActionId, actionElapsedMs: state.actionElapsedMs, actionQueue: [...state.actionQueue], + storyFlags: { ...state.storyFlags }, + currentStoryNodeId: state.currentStoryNodeId, + seenStoryNodeIds: [...state.seenStoryNodeIds], }, }; } diff --git a/src/state/__tests__/persistence.test.ts b/src/state/__tests__/persistence.test.ts index c105d89..22f4629 100644 --- a/src/state/__tests__/persistence.test.ts +++ b/src/state/__tests__/persistence.test.ts @@ -70,6 +70,19 @@ describe('loadGame()', () => { expect(result.state.actionQueue).toEqual(['b']); }); + it('restores story fields on load', async () => { + const content = testContent(); + const backend = createMemoryBackend(); + const state = createGameState(content); + state.storyFlags = { route_b: true }; + state.currentStoryNodeId = 'fork_choice'; + state.seenStoryNodeIds = ['boot_intro']; + await saveGame(state, backend, 1000); + const loaded = await loadGame(content, backend, 1000); + expect(loaded.state.storyFlags.route_b).toBe(true); + expect(loaded.state.currentStoryNodeId).toBe('fork_choice'); + }); + it('drops an active action that no longer exists in content', async () => { const content = testContent(); const stale = serializeSave( diff --git a/src/state/persistence.ts b/src/state/persistence.ts index e0843fe..8455258 100644 --- a/src/state/persistence.ts +++ b/src/state/persistence.ts @@ -103,9 +103,9 @@ export async function loadGame( activeActionId, actionElapsedMs: save.state.actionElapsedMs, actionQueue: [...(save.state.actionQueue ?? [])], - storyFlags: { ...base.storyFlags }, - currentStoryNodeId: base.currentStoryNodeId, - seenStoryNodeIds: [...base.seenStoryNodeIds], + storyFlags: { ...save.state.storyFlags }, + currentStoryNodeId: save.state.currentStoryNodeId ?? '', + seenStoryNodeIds: [...(save.state.seenStoryNodeIds ?? [])], }; savedAt = save.savedAt; } catch {