From fce561b8e5bc7ba90d7a93c00402c851d995c72d Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 11 Jun 2026 18:21:12 -0500 Subject: [PATCH] feat(state): wire enqueueAction through runtime and view model --- src/state/__tests__/viewModel.test.ts | 18 +++++++++++++++++- src/state/runtime.ts | 18 ++++++++++++------ src/state/store.ts | 2 ++ src/state/viewModel.ts | 6 ++++++ src/ui/ActionPanel.tsx | 2 +- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/state/__tests__/viewModel.test.ts b/src/state/__tests__/viewModel.test.ts index 48ab5b6..c3e4b65 100644 --- a/src/state/__tests__/viewModel.test.ts +++ b/src/state/__tests__/viewModel.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 { formatOfflineDuration, toView } from '../viewModel'; function testContent() { @@ -44,6 +44,22 @@ describe('toView()', () => { state.actionElapsedMs = 999; expect(toView(state, content).actionProgress).toBe(1); }); + + it('includes queued action ids in order', () => { + const content = buildContent({ + resources: [{ id: 'gold', name: 'Gold' }], + actions: [ + { id: 'a', name: 'Alpha', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] }, + { id: 'b', name: 'Bravo', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] }, + ], + }); + const state = createGameState(content); + enqueueAction(state, content, 'a'); + enqueueAction(state, content, 'b'); + const view = toView(state, content); + expect(view.queuedActionIds).toEqual(['b']); + expect(view.queuedActionNames).toEqual(['Bravo']); + }); }); describe('formatOfflineDuration()', () => { diff --git a/src/state/runtime.ts b/src/state/runtime.ts index 63b3ea8..be5d570 100644 --- a/src/state/runtime.ts +++ b/src/state/runtime.ts @@ -1,5 +1,5 @@ import { content } from '../content'; -import { startAction as engineStartAction, type GameState, tickGame } from '../engine/game'; +import { enqueueAction as engineEnqueueAction, type GameState, tickGame } from '../engine/game'; import { advance, createTickLoop, TICK_MS, type TickLoop } from '../engine/tickLoop'; import { createDefaultBackend, loadGame, type SaveBackend, saveGame } from './persistence'; import { useGameStore } from './store'; @@ -58,15 +58,21 @@ class GameRuntime { } } - startAction(actionId: string): void { + enqueueAction(actionId: string): void { const state = this.state; if (!state) { return; } - engineStartAction(state, content, actionId); - const action = content.actionsById[actionId]; - if (action) { - useGameStore.getState().appendLog(`Started: ${action.name}.`); + try { + engineEnqueueAction(state, content, actionId); + const action = content.actionsById[actionId]; + if (action) { + const verb = state.actionQueue.includes(actionId) ? 'Queued' : 'Started'; + useGameStore.getState().appendLog(`${verb}: ${action.name}.`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : 'Cannot start action'; + useGameStore.getState().appendLog(msg); } this.publish(); } diff --git a/src/state/store.ts b/src/state/store.ts index 910e3fc..cf55e94 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -20,6 +20,8 @@ export const useGameStore = create((set) => ({ activeActionId: null, actionName: null, actionProgress: 0, + queuedActionIds: [], + queuedActionNames: [], log: [], setView: (view) => set(view), appendLog: (line) => set((state) => ({ log: [...state.log, line].slice(-MAX_LOG_LINES) })), diff --git a/src/state/viewModel.ts b/src/state/viewModel.ts index 77b1875..8e40b00 100644 --- a/src/state/viewModel.ts +++ b/src/state/viewModel.ts @@ -18,6 +18,8 @@ export interface GameView { actionName: string | null; /** Progress of the active action, clamped to 0..1. */ actionProgress: number; + queuedActionIds: string[]; + queuedActionNames: string[]; } export function toView(state: GameState, content: Content): GameView { @@ -29,12 +31,16 @@ export function toView(state: GameState, content: Content): GameView { const action = state.activeActionId ? content.actionsById[state.activeActionId] : undefined; const actionProgress = action ? Math.min(1, state.actionElapsedMs / action.durationMs) : 0; + const queuedActionIds = [...state.actionQueue]; + const queuedActionNames = queuedActionIds.map((id) => content.actionsById[id]?.name ?? id); return { resources, activeActionId: state.activeActionId, actionName: action ? action.name : null, actionProgress, + queuedActionIds, + queuedActionNames, }; } diff --git a/src/ui/ActionPanel.tsx b/src/ui/ActionPanel.tsx index 027f5be..e306d7f 100644 --- a/src/ui/ActionPanel.tsx +++ b/src/ui/ActionPanel.tsx @@ -16,7 +16,7 @@ export function ActionPanel() {