feat(state): project actions into columns and story tree
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildContent } from '../../content/schema';
|
||||
import { buildStoryContent } from '../../content/storySchema';
|
||||
import { createGameState, enqueueAction, startAction } from '../../engine/game';
|
||||
import { content } from '../../content/index';
|
||||
import { createGameState, enqueueAction, performAction, startAction } from '../../engine/game';
|
||||
import { enterStoryNode } from '../../engine/story';
|
||||
import { formatOfflineDuration, toView } from '../viewModel';
|
||||
|
||||
@@ -193,3 +194,72 @@ describe('formatOfflineDuration()', () => {
|
||||
expect(formatOfflineDuration(7_200_000)).toBe('2h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('action columns projection', () => {
|
||||
it('produces all five kind columns in fixed order', () => {
|
||||
const state = createGameState(content);
|
||||
const view = toView(state, content);
|
||||
expect(view.actionColumns.map((c) => c.kind)).toEqual([
|
||||
'instant', 'loop', 'timed', 'story', 'context',
|
||||
]);
|
||||
});
|
||||
|
||||
it('groups timed actions by their content group', () => {
|
||||
const state = createGameState(content);
|
||||
const view = toView(state, content);
|
||||
const timed = view.actionColumns.find((c) => c.kind === 'timed');
|
||||
expect(timed?.groups.some((g) => g.id === 'camp')).toBe(true);
|
||||
expect(timed?.groups.some((g) => g.id === 'travel')).toBe(true);
|
||||
});
|
||||
|
||||
it('marks loopEnabled from enabledLoopActionIds', () => {
|
||||
const state = createGameState(content);
|
||||
state.enabledLoopActionIds = { rest: true };
|
||||
const view = toView(state, content);
|
||||
const rest = view.actionColumns
|
||||
.flatMap((c) => c.groups)
|
||||
.flatMap((g) => g.actions)
|
||||
.find((a) => a.id === 'rest');
|
||||
expect(rest?.loopEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('shows story actions only when their choice is available, hiding siblings after a fork is taken', () => {
|
||||
const state = createGameState(content);
|
||||
// before reaching the fork, story actions are hidden
|
||||
let storyCol = toView(state, content).actionColumns.find((c) => c.kind === 'story');
|
||||
expect(storyCol?.groups.flatMap((g) => g.actions)).toHaveLength(0);
|
||||
// at the fork, both story actions appear
|
||||
enterStoryNode(state, content, 'fork_choice');
|
||||
storyCol = toView(state, content).actionColumns.find((c) => c.kind === 'story');
|
||||
const idsAtFork = storyCol?.groups.flatMap((g) => g.actions).map((a) => a.id) ?? [];
|
||||
expect(idsAtFork).toEqual(expect.arrayContaining(['pick_high_road', 'follow_river']));
|
||||
// after taking route A, the sibling hides
|
||||
performAction(state, content, 'pick_high_road');
|
||||
storyCol = toView(state, content).actionColumns.find((c) => c.kind === 'story');
|
||||
expect(storyCol?.groups.flatMap((g) => g.actions)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('story tree projection', () => {
|
||||
it('builds a tree marking seen and active nodes', () => {
|
||||
const state = createGameState(content);
|
||||
enterStoryNode(state, content, 'fork_choice');
|
||||
const view = toView(state, content);
|
||||
// fork_choice should be a node in the tree, marked active+seen, with route children
|
||||
const findNode = (nodes: typeof view.story.tree, id: string): (typeof nodes)[number] | undefined => {
|
||||
for (const n of nodes) {
|
||||
if (n.id === id) return n;
|
||||
const deeper = findNode(n.children, id);
|
||||
if (deeper) return deeper;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const fork = findNode(view.story.tree, 'fork_choice');
|
||||
expect(fork).toBeDefined();
|
||||
expect(fork?.active).toBe(true);
|
||||
expect(fork?.seen).toBe(true);
|
||||
expect(fork?.children.map((c) => c.id)).toEqual(
|
||||
expect.arrayContaining(['route_a_beat', 'route_b_beat']),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user