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