feat(engine): add instant action execution

Implements executeInstant, which applies an instant action's costs and
yields immediately with no queue slot and no duration requirement.
This commit is contained in:
ginnoir
2026-06-11 20:29:28 -05:00
parent b54805637c
commit b3940774c4
2 changed files with 52 additions and 0 deletions
+35
View File
@@ -6,6 +6,7 @@ import {
clearQueue, clearQueue,
createGameState, createGameState,
enqueueAction, enqueueAction,
executeInstant,
startAction, startAction,
tickGame, tickGame,
} from '../game'; } from '../game';
@@ -367,3 +368,37 @@ describe('tickGame()', () => {
expect(state.activeActionId).toBeNull(); expect(state.activeActionId).toBeNull();
}); });
}); });
describe('executeInstant()', () => {
const instantContent = buildContent({
resources: [
{ id: 'supplies', name: 'Supplies', startAmount: 0 },
{ id: 'coin', name: 'Coin', startAmount: 5 },
],
actions: [
{
id: 'buy_supply',
name: 'Buy supply',
kind: 'instant',
group: { id: 'buy', label: 'Buy' },
costs: [{ resourceId: 'coin', amount: 2 }],
yields: [{ resourceId: 'supplies', amount: 1 }],
},
],
});
it('applies costs and yields immediately without queueing', () => {
const state = createGameState(instantContent);
executeInstant(state, instantContent, 'buy_supply');
expect(state.resources.coin).toBe(3);
expect(state.resources.supplies).toBe(1);
expect(state.activeActionId).toBeNull();
expect(state.actionQueue).toEqual([]);
});
it('throws when unaffordable', () => {
const state = createGameState(instantContent);
state.resources.coin = 0;
expect(() => executeInstant(state, instantContent, 'buy_supply')).toThrow(/Cannot/);
});
});
+17
View File
@@ -156,6 +156,23 @@ export function clearQueue(state: GameState): void {
state.actionQueue.length = 0; state.actionQueue.length = 0;
} }
/**
* Execute an instant action immediately, deducting costs and granting yields
* without occupying a queue slot or requiring a duration.
* Throws if the action is not of kind 'instant' or is unavailable.
*/
export function executeInstant(state: GameState, content: Content, actionId: string): void {
const action = content.actionsById[actionId];
if (!action || action.kind !== 'instant') {
throw new Error(`Action "${actionId}" is not instant`);
}
if (!isActionAvailable(state, content, actionId)) {
throw new Error(`Cannot perform instant action "${actionId}"`);
}
deductCosts(state, content, actionId);
grantYields(state, content, actionId);
}
/** /**
* Advance the active action by `tickMs`. On completion, grants yields and * Advance the active action by `tickMs`. On completion, grants yields and
* advances the queue — actions do not auto-repeat when the queue is empty. * advances the queue — actions do not auto-repeat when the queue is empty.