feat(engine): execute story actions via storyChoiceId
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { content } from '../../content/index';
|
||||||
|
import { createGameState, executeStoryAction } from '../game';
|
||||||
|
import { enterStoryNode } from '../story';
|
||||||
|
|
||||||
|
describe('executeStoryAction()', () => {
|
||||||
|
it('applies the linked story choice', () => {
|
||||||
|
const state = createGameState(content);
|
||||||
|
enterStoryNode(state, content, 'fork_choice');
|
||||||
|
executeStoryAction(state, content, 'pick_high_road');
|
||||||
|
expect(state.storyFlags.route_a).toBe(true);
|
||||||
|
expect(state.currentStoryNodeId).toBe('route_a_beat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when the choice is not available', () => {
|
||||||
|
const state = createGameState(content);
|
||||||
|
enterStoryNode(state, content, 'fork_choice');
|
||||||
|
executeStoryAction(state, content, 'pick_high_road');
|
||||||
|
// after taking route A, the current node has no choices, so follow_river (pick_b) is unavailable
|
||||||
|
expect(() => executeStoryAction(state, content, 'follow_river')).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { Content } from '../content/schema';
|
import type { Content } from '../content/schema';
|
||||||
|
import type { StoryContent } from '../content/storySchema';
|
||||||
|
import { applyChoice, isStoryChoiceAvailable } from './story';
|
||||||
|
|
||||||
|
type GameContent = Content & StoryContent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core game state and per-tick simulation.
|
* Core game state and per-tick simulation.
|
||||||
@@ -173,6 +177,26 @@ export function executeInstant(state: GameState, content: Content, actionId: str
|
|||||||
grantYields(state, content, actionId);
|
grantYields(state, content, actionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a story action by resolving its storyChoiceId and applying it.
|
||||||
|
* Throws if the action is not of kind 'story', has no storyChoiceId, or the
|
||||||
|
* choice is not currently available.
|
||||||
|
*/
|
||||||
|
export function executeStoryAction(
|
||||||
|
state: GameState,
|
||||||
|
content: GameContent,
|
||||||
|
actionId: string,
|
||||||
|
): void {
|
||||||
|
const action = content.actionsById[actionId];
|
||||||
|
if (!action || action.kind !== 'story' || !action.storyChoiceId) {
|
||||||
|
throw new Error(`Action "${actionId}" is not a story action`);
|
||||||
|
}
|
||||||
|
if (!isStoryChoiceAvailable(state, content, action.storyChoiceId)) {
|
||||||
|
throw new Error(`Story choice "${action.storyChoiceId}" is not available`);
|
||||||
|
}
|
||||||
|
applyChoice(state, content, action.storyChoiceId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
|||||||
@@ -165,6 +165,18 @@ export function getAvailableChoices(state: GameState, content: GameContent): Sto
|
|||||||
return node.choices.filter((c) => meetsChoiceRequirements(state, c.requirements));
|
return node.choices.filter((c) => meetsChoiceRequirements(state, c.requirements));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isStoryChoiceAvailable(
|
||||||
|
state: GameState,
|
||||||
|
content: GameContent,
|
||||||
|
storyChoiceId: string,
|
||||||
|
): boolean {
|
||||||
|
const node = getCurrentNode(state, content);
|
||||||
|
if (!node?.choices) return false;
|
||||||
|
const choice = node.choices.find((c) => c.id === storyChoiceId);
|
||||||
|
if (!choice) return false;
|
||||||
|
return meetsChoiceRequirements(state, choice.requirements);
|
||||||
|
}
|
||||||
|
|
||||||
export function applyChoice(
|
export function applyChoice(
|
||||||
state: GameState,
|
state: GameState,
|
||||||
content: GameContent,
|
content: GameContent,
|
||||||
|
|||||||
Reference in New Issue
Block a user