feat(state): runtime automation and recipe commands
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { content } from '../../content';
|
||||
import type { GameState } from '../../engine/game';
|
||||
import { RECIPE_HEADER_V1 } from '../../engine/recipe';
|
||||
import { getPrefs } from '../prefs';
|
||||
import { GameRuntime } from '../runtime';
|
||||
import { useGameStore } from '../store';
|
||||
|
||||
function runtimeState(runtime: GameRuntime): GameState {
|
||||
return (runtime as unknown as { state: GameState }).state;
|
||||
}
|
||||
|
||||
describe('GameRuntime', () => {
|
||||
let runtime: GameRuntime;
|
||||
|
||||
@@ -78,6 +84,46 @@ describe('GameRuntime', () => {
|
||||
expect(view.actions.find((a) => a.id === 'rest')?.loopEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles an unlocked action in and out of the automation queue', () => {
|
||||
const state = runtimeState(runtime);
|
||||
state.manualCompletionCounts.gather_supplies = 1;
|
||||
|
||||
runtime.toggleAutomation('gather_supplies');
|
||||
|
||||
expect(useGameStore.getState().automationQueueIds).toEqual(['gather_supplies']);
|
||||
|
||||
runtime.toggleAutomation('gather_supplies');
|
||||
|
||||
expect(useGameStore.getState().automationQueueIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('logs automation toggle errors for locked actions', () => {
|
||||
runtime.toggleAutomation('gather_supplies');
|
||||
|
||||
expect(useGameStore.getState().log.at(-1)).toMatch(/automation.*locked/i);
|
||||
});
|
||||
|
||||
it('exports the automation recipe text', () => {
|
||||
const state = runtimeState(runtime);
|
||||
state.manualCompletionCounts.gather_supplies = 1;
|
||||
state.automationQueue = ['gather_supplies'];
|
||||
|
||||
const text = runtime.exportAutomationRecipe();
|
||||
|
||||
expect(text).toContain(RECIPE_HEADER_V1);
|
||||
expect(text).toContain('gather_supplies');
|
||||
});
|
||||
|
||||
it('imports an automation recipe and publishes the queue', () => {
|
||||
const state = runtimeState(runtime);
|
||||
state.manualCompletionCounts.gather_supplies = 1;
|
||||
|
||||
runtime.importAutomationRecipe(`${RECIPE_HEADER_V1}:gather_supplies`);
|
||||
|
||||
expect(useGameStore.getState().automationQueueIds).toEqual(['gather_supplies']);
|
||||
expect(useGameStore.getState().log.at(-1)).toMatch(/imported recipe/i);
|
||||
});
|
||||
|
||||
it('performs story action and appends story log', () => {
|
||||
// Let's first move state to fork_choice node where story choices are available
|
||||
runtime.continueStory();
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { content } from '../content';
|
||||
import {
|
||||
addToAutomationQueue,
|
||||
maybeRunAutomation,
|
||||
removeFromAutomationQueue,
|
||||
} from '../engine/automation';
|
||||
import {
|
||||
cancelQueuedAction as engineCancelQueuedAction,
|
||||
type GameState,
|
||||
@@ -6,6 +11,7 @@ import {
|
||||
performAction as performActionEngine,
|
||||
tickGame,
|
||||
} from '../engine/game';
|
||||
import { exportRecipe, importRecipe } from '../engine/recipe';
|
||||
import { applyChoice as engineApplyChoice, enterStoryNode, initStory } from '../engine/story';
|
||||
import { advance, createTickLoop, TICK_MS, type TickLoop } from '../engine/tickLoop';
|
||||
import { createDefaultBackend, loadGame, type SaveBackend, saveGame } from './persistence';
|
||||
@@ -147,6 +153,44 @@ export class GameRuntime {
|
||||
this.performAction(actionId);
|
||||
}
|
||||
|
||||
toggleAutomation(actionId: string): void {
|
||||
const state = this.state;
|
||||
if (!state) return;
|
||||
try {
|
||||
const existingIndex = state.automationQueue.indexOf(actionId);
|
||||
if (existingIndex >= 0) {
|
||||
removeFromAutomationQueue(state, existingIndex);
|
||||
} else {
|
||||
addToAutomationQueue(state, content, actionId);
|
||||
maybeRunAutomation(state, content);
|
||||
}
|
||||
this.publish();
|
||||
} catch (err) {
|
||||
useGameStore.getState().appendLog(err instanceof Error ? err.message : 'Automation failed');
|
||||
}
|
||||
}
|
||||
|
||||
exportAutomationRecipe(): string {
|
||||
const state = this.state;
|
||||
if (!state) return '';
|
||||
return exportRecipe(state, content);
|
||||
}
|
||||
|
||||
importAutomationRecipe(text: string): void {
|
||||
const state = this.state;
|
||||
if (!state) return;
|
||||
try {
|
||||
const meta = importRecipe(state, content, text);
|
||||
maybeRunAutomation(state, content);
|
||||
useGameStore
|
||||
.getState()
|
||||
.appendLog(meta.name ? `Imported recipe: ${meta.name}` : 'Imported recipe.');
|
||||
this.publish();
|
||||
} catch (err) {
|
||||
useGameStore.getState().appendLog(err instanceof Error ? err.message : 'Import failed');
|
||||
}
|
||||
}
|
||||
|
||||
applyStoryChoice(choiceId: string): void {
|
||||
const action = content.actions.find((a) => a.storyChoiceId === choiceId);
|
||||
if (action) {
|
||||
|
||||
Reference in New Issue
Block a user