feat(state): wire enqueueAction through runtime and view model
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { buildContent } from '../../content/schema';
|
import { buildContent } from '../../content/schema';
|
||||||
import { createGameState, startAction } from '../../engine/game';
|
import { createGameState, enqueueAction, startAction } from '../../engine/game';
|
||||||
import { formatOfflineDuration, toView } from '../viewModel';
|
import { formatOfflineDuration, toView } from '../viewModel';
|
||||||
|
|
||||||
function testContent() {
|
function testContent() {
|
||||||
@@ -44,6 +44,22 @@ describe('toView()', () => {
|
|||||||
state.actionElapsedMs = 999;
|
state.actionElapsedMs = 999;
|
||||||
expect(toView(state, content).actionProgress).toBe(1);
|
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()', () => {
|
describe('formatOfflineDuration()', () => {
|
||||||
|
|||||||
+12
-6
@@ -1,5 +1,5 @@
|
|||||||
import { content } from '../content';
|
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 { advance, createTickLoop, TICK_MS, type TickLoop } from '../engine/tickLoop';
|
||||||
import { createDefaultBackend, loadGame, type SaveBackend, saveGame } from './persistence';
|
import { createDefaultBackend, loadGame, type SaveBackend, saveGame } from './persistence';
|
||||||
import { useGameStore } from './store';
|
import { useGameStore } from './store';
|
||||||
@@ -58,15 +58,21 @@ class GameRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startAction(actionId: string): void {
|
enqueueAction(actionId: string): void {
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
engineStartAction(state, content, actionId);
|
try {
|
||||||
const action = content.actionsById[actionId];
|
engineEnqueueAction(state, content, actionId);
|
||||||
if (action) {
|
const action = content.actionsById[actionId];
|
||||||
useGameStore.getState().appendLog(`Started: ${action.name}.`);
|
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();
|
this.publish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const useGameStore = create<GameStoreState>((set) => ({
|
|||||||
activeActionId: null,
|
activeActionId: null,
|
||||||
actionName: null,
|
actionName: null,
|
||||||
actionProgress: 0,
|
actionProgress: 0,
|
||||||
|
queuedActionIds: [],
|
||||||
|
queuedActionNames: [],
|
||||||
log: [],
|
log: [],
|
||||||
setView: (view) => set(view),
|
setView: (view) => set(view),
|
||||||
appendLog: (line) => set((state) => ({ log: [...state.log, line].slice(-MAX_LOG_LINES) })),
|
appendLog: (line) => set((state) => ({ log: [...state.log, line].slice(-MAX_LOG_LINES) })),
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export interface GameView {
|
|||||||
actionName: string | null;
|
actionName: string | null;
|
||||||
/** Progress of the active action, clamped to 0..1. */
|
/** Progress of the active action, clamped to 0..1. */
|
||||||
actionProgress: number;
|
actionProgress: number;
|
||||||
|
queuedActionIds: string[];
|
||||||
|
queuedActionNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toView(state: GameState, content: Content): GameView {
|
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 action = state.activeActionId ? content.actionsById[state.activeActionId] : undefined;
|
||||||
const actionProgress = action ? Math.min(1, state.actionElapsedMs / action.durationMs) : 0;
|
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 {
|
return {
|
||||||
resources,
|
resources,
|
||||||
activeActionId: state.activeActionId,
|
activeActionId: state.activeActionId,
|
||||||
actionName: action ? action.name : null,
|
actionName: action ? action.name : null,
|
||||||
actionProgress,
|
actionProgress,
|
||||||
|
queuedActionIds,
|
||||||
|
queuedActionNames,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function ActionPanel() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={action.id}
|
key={action.id}
|
||||||
onClick={() => gameRuntime.startAction(action.id)}
|
onClick={() => gameRuntime.enqueueAction(action.id)}
|
||||||
className="relative overflow-hidden rounded-lg border border-slate-700 bg-slate-800/70 px-4 py-3 text-left transition-colors hover:border-amber-500/60 hover:bg-slate-800"
|
className="relative overflow-hidden rounded-lg border border-slate-700 bg-slate-800/70 px-4 py-3 text-left transition-colors hover:border-amber-500/60 hover:bg-slate-800"
|
||||||
>
|
>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user