feat(engine): automation unlock and queue CRUD
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { buildContent } from '../../content/schema';
|
||||||
|
import {
|
||||||
|
addToAutomationQueue,
|
||||||
|
clearAutomationQueue,
|
||||||
|
isAutomationUnlocked,
|
||||||
|
removeFromAutomationQueue,
|
||||||
|
} from '../automation';
|
||||||
|
import { createGameState } from '../game';
|
||||||
|
|
||||||
|
const DEFAULT_GROUP = { id: 'automation', label: 'Automation' };
|
||||||
|
|
||||||
|
function automationContent() {
|
||||||
|
return buildContent({
|
||||||
|
resources: [
|
||||||
|
{ id: 'supplies', name: 'Supplies', startAmount: 0 },
|
||||||
|
{ id: 'renown', name: 'Renown', startAmount: 0 },
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'forage',
|
||||||
|
name: 'Forage',
|
||||||
|
group: DEFAULT_GROUP,
|
||||||
|
durationMs: 100,
|
||||||
|
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||||
|
automation: { unlockAfterManualCompletions: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'survey',
|
||||||
|
name: 'Survey',
|
||||||
|
group: DEFAULT_GROUP,
|
||||||
|
durationMs: 100,
|
||||||
|
yields: [{ resourceId: 'renown', amount: 1 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('isAutomationUnlocked()', () => {
|
||||||
|
it('returns false before the first manual completion when an action unlocks after one', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
|
||||||
|
expect(isAutomationUnlocked(state, content, 'forage')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true once the manual completion threshold is met', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
state.manualCompletionCounts.forage = 1;
|
||||||
|
|
||||||
|
expect(isAutomationUnlocked(state, content, 'forage')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('automation queue CRUD', () => {
|
||||||
|
it('throws for unknown action ids', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
|
||||||
|
expect(() => addToAutomationQueue(state, content, 'missing')).toThrow(
|
||||||
|
'Unknown action "missing"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for locked action ids', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
|
||||||
|
expect(() => addToAutomationQueue(state, content, 'forage')).toThrow(/automation.*locked/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends an unlocked action id', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
state.manualCompletionCounts.forage = 1;
|
||||||
|
|
||||||
|
addToAutomationQueue(state, content, 'forage');
|
||||||
|
|
||||||
|
expect(state.automationQueue).toEqual(['forage']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate an action already in the queue', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
state.manualCompletionCounts.forage = 1;
|
||||||
|
|
||||||
|
addToAutomationQueue(state, content, 'forage');
|
||||||
|
addToAutomationQueue(state, content, 'forage');
|
||||||
|
|
||||||
|
expect(state.automationQueue).toEqual(['forage']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes by index', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
state.automationQueue = ['forage', 'survey'];
|
||||||
|
|
||||||
|
removeFromAutomationQueue(state, 0);
|
||||||
|
|
||||||
|
expect(state.automationQueue).toEqual(['survey']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws RangeError when removing an out-of-range index', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
state.automationQueue = ['forage'];
|
||||||
|
|
||||||
|
expect(() => removeFromAutomationQueue(state, 1)).toThrow(RangeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empties the queue', () => {
|
||||||
|
const content = automationContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
state.automationQueue = ['forage', 'survey'];
|
||||||
|
|
||||||
|
clearAutomationQueue(state);
|
||||||
|
|
||||||
|
expect(state.automationQueue).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Content } from '../content/schema';
|
||||||
|
import type { GameState } from './game';
|
||||||
|
|
||||||
|
function assertKnownAction(content: Content, actionId: string): void {
|
||||||
|
if (!content.actionsById[actionId]) {
|
||||||
|
throw new Error(`Unknown action "${actionId}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function automationUnlockThreshold(content: Content, actionId: string): number {
|
||||||
|
const action = content.actionsById[actionId];
|
||||||
|
if (!action) {
|
||||||
|
throw new Error(`Unknown action "${actionId}"`);
|
||||||
|
}
|
||||||
|
return action.automation?.unlockAfterManualCompletions ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAutomationUnlocked(
|
||||||
|
state: GameState,
|
||||||
|
content: Content,
|
||||||
|
actionId: string,
|
||||||
|
): boolean {
|
||||||
|
assertKnownAction(content, actionId);
|
||||||
|
return (
|
||||||
|
(state.manualCompletionCounts[actionId] ?? 0) >= automationUnlockThreshold(content, actionId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToAutomationQueue(state: GameState, content: Content, actionId: string): void {
|
||||||
|
assertKnownAction(content, actionId);
|
||||||
|
if (!isAutomationUnlocked(state, content, actionId)) {
|
||||||
|
throw new Error(`Automation for action "${actionId}" is locked`);
|
||||||
|
}
|
||||||
|
if (!state.automationQueue.includes(actionId)) {
|
||||||
|
state.automationQueue.push(actionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFromAutomationQueue(state: GameState, index: number): void {
|
||||||
|
if (index < 0 || index >= state.automationQueue.length) {
|
||||||
|
throw new RangeError(`Automation queue index ${index} is out of range`);
|
||||||
|
}
|
||||||
|
state.automationQueue.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAutomationQueue(state: GameState): void {
|
||||||
|
state.automationQueue.length = 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user