diff --git a/src/engine/__tests__/game.test.ts b/src/engine/__tests__/game.test.ts index aee2c49..0a58bfe 100644 --- a/src/engine/__tests__/game.test.ts +++ b/src/engine/__tests__/game.test.ts @@ -6,6 +6,7 @@ import { clearQueue, createGameState, enqueueAction, + executeInstant, startAction, tickGame, } from '../game'; @@ -367,3 +368,37 @@ describe('tickGame()', () => { 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/); + }); +}); diff --git a/src/engine/game.ts b/src/engine/game.ts index 4b7a040..c7110a8 100644 --- a/src/engine/game.ts +++ b/src/engine/game.ts @@ -156,6 +156,23 @@ export function clearQueue(state: GameState): void { 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 * advances the queue — actions do not auto-repeat when the queue is empty.