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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { content } from '../../content';
|
import { content } from '../../content';
|
||||||
|
import type { GameState } from '../../engine/game';
|
||||||
|
import { RECIPE_HEADER_V1 } from '../../engine/recipe';
|
||||||
import { getPrefs } from '../prefs';
|
import { getPrefs } from '../prefs';
|
||||||
import { GameRuntime } from '../runtime';
|
import { GameRuntime } from '../runtime';
|
||||||
import { useGameStore } from '../store';
|
import { useGameStore } from '../store';
|
||||||
|
|
||||||
|
function runtimeState(runtime: GameRuntime): GameState {
|
||||||
|
return (runtime as unknown as { state: GameState }).state;
|
||||||
|
}
|
||||||
|
|
||||||
describe('GameRuntime', () => {
|
describe('GameRuntime', () => {
|
||||||
let runtime: GameRuntime;
|
let runtime: GameRuntime;
|
||||||
|
|
||||||
@@ -78,6 +84,46 @@ describe('GameRuntime', () => {
|
|||||||
expect(view.actions.find((a) => a.id === 'rest')?.loopEnabled).toBe(true);
|
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', () => {
|
it('performs story action and appends story log', () => {
|
||||||
// Let's first move state to fork_choice node where story choices are available
|
// Let's first move state to fork_choice node where story choices are available
|
||||||
runtime.continueStory();
|
runtime.continueStory();
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { content } from '../content';
|
import { content } from '../content';
|
||||||
|
import {
|
||||||
|
addToAutomationQueue,
|
||||||
|
maybeRunAutomation,
|
||||||
|
removeFromAutomationQueue,
|
||||||
|
} from '../engine/automation';
|
||||||
import {
|
import {
|
||||||
cancelQueuedAction as engineCancelQueuedAction,
|
cancelQueuedAction as engineCancelQueuedAction,
|
||||||
type GameState,
|
type GameState,
|
||||||
@@ -6,6 +11,7 @@ import {
|
|||||||
performAction as performActionEngine,
|
performAction as performActionEngine,
|
||||||
tickGame,
|
tickGame,
|
||||||
} from '../engine/game';
|
} from '../engine/game';
|
||||||
|
import { exportRecipe, importRecipe } from '../engine/recipe';
|
||||||
import { applyChoice as engineApplyChoice, enterStoryNode, initStory } from '../engine/story';
|
import { applyChoice as engineApplyChoice, enterStoryNode, initStory } from '../engine/story';
|
||||||
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';
|
||||||
@@ -147,6 +153,44 @@ export class GameRuntime {
|
|||||||
this.performAction(actionId);
|
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 {
|
applyStoryChoice(choiceId: string): void {
|
||||||
const action = content.actions.find((a) => a.storyChoiceId === choiceId);
|
const action = content.actions.find((a) => a.storyChoiceId === choiceId);
|
||||||
if (action) {
|
if (action) {
|
||||||
|
|||||||
Reference in New Issue
Block a user