From 964e9260faa52a60d20274bee6a0ce673089d5c9 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 11 Jun 2026 18:19:49 -0500 Subject: [PATCH] feat(save): persist action queue in v1 save payload --- src/engine/__tests__/save.test.ts | 9 +++++++++ src/engine/save.ts | 2 ++ src/state/__tests__/persistence.test.ts | 25 ++++++++++++++++++++++++- src/state/persistence.ts | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/engine/__tests__/save.test.ts b/src/engine/__tests__/save.test.ts index e806006..2ae64bd 100644 --- a/src/engine/__tests__/save.test.ts +++ b/src/engine/__tests__/save.test.ts @@ -44,6 +44,15 @@ describe('createSave()', () => { state.resources.gold = 999; expect(save.state.resources.gold).toBe(12); }); + + it('snapshots actionQueue in save payload', () => { + const state = sampleState(); + state.actionQueue = ['forage', 'forage']; + const save = createSave(state, 1700); + expect(save.state.actionQueue).toEqual(['forage', 'forage']); + state.actionQueue.push('forage'); + expect(save.state.actionQueue).toEqual(['forage', 'forage']); + }); }); describe('serialize / deserialize round-trip', () => { diff --git a/src/engine/save.ts b/src/engine/save.ts index 5636131..7aa6059 100644 --- a/src/engine/save.ts +++ b/src/engine/save.ts @@ -28,6 +28,7 @@ export const gameStateSchema = z.object({ resources: z.record(z.string(), z.number()), activeActionId: z.string().nullable(), actionElapsedMs: z.number().nonnegative(), + actionQueue: z.array(z.string()).default([]), }); export const saveSchema = z.object({ @@ -47,6 +48,7 @@ export function createSave(state: GameState, now: number): SaveData { resources: { ...state.resources }, activeActionId: state.activeActionId, actionElapsedMs: state.actionElapsedMs, + actionQueue: [...state.actionQueue], }, }; } diff --git a/src/state/__tests__/persistence.test.ts b/src/state/__tests__/persistence.test.ts index dc8a39d..7e6f696 100644 --- a/src/state/__tests__/persistence.test.ts +++ b/src/state/__tests__/persistence.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { buildContent } from '../../content/schema'; -import { createGameState, startAction } from '../../engine/game'; +import { createGameState, enqueueAction, startAction } from '../../engine/game'; import { createSave, serializeSave } from '../../engine/save'; import { createMemoryBackend, loadGame, saveGame } from '../persistence'; @@ -13,6 +13,16 @@ function testContent() { }); } +function queueTestContent() { + return buildContent({ + resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }], + actions: [ + { id: 'a', name: 'A', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] }, + { id: 'b', name: 'B', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] }, + ], + }); +} + describe('loadGame()', () => { it('returns a fresh game when no save exists', async () => { const content = testContent(); @@ -42,6 +52,19 @@ describe('loadGame()', () => { expect(result.offlineMs).toBe(0); }); + it('restores actionQueue on load', async () => { + const content = queueTestContent(); + const state = createGameState(content); + enqueueAction(state, content, 'a'); + enqueueAction(state, content, 'b'); + const backend = createMemoryBackend(); + await saveGame(state, backend, 1000); + + const result = await loadGame(content, backend, 1000); + expect(result.state.activeActionId).toBe('a'); + expect(result.state.actionQueue).toEqual(['b']); + }); + 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 7fca78c..e4284d6 100644 --- a/src/state/persistence.ts +++ b/src/state/persistence.ts @@ -102,7 +102,7 @@ export async function loadGame( resources: { ...base.resources, ...save.state.resources }, activeActionId, actionElapsedMs: save.state.actionElapsedMs, - actionQueue: [...base.actionQueue], + actionQueue: [...(save.state.actionQueue ?? [])], }; savedAt = save.savedAt; } catch {