feat(m1): playable loop — story graph, story panel, queue UI #14

Merged
ginnoir merged 20 commits from feat/m1-playable-loop into main 2026-06-11 19:26:54 -05:00
2 changed files with 32 additions and 11 deletions
Showing only changes of commit dc7b87eab2 - Show all commits
+19
View File
@@ -299,6 +299,25 @@ describe('completion advances queue', () => {
});
});
describe('tickGame() completion result', () => {
it('returns the id of each action that completed this tick', () => {
const content = testContent();
const state = createGameState(content);
enqueueAction(state, content, 'forage');
const result = tickGame(state, content, 300);
expect(result.completedActionIds).toEqual(['forage']);
expect(state.activeActionId).toBeNull();
});
it('returns an empty array when nothing completes', () => {
const content = testContent();
const state = createGameState(content);
enqueueAction(state, content, 'forage');
const result = tickGame(state, content, 100);
expect(result.completedActionIds).toEqual([]);
});
});
describe('tickGame()', () => {
it('does nothing when no action is active', () => {
const content = testContent();
+13 -11
View File
@@ -115,11 +115,8 @@ function startNextFromQueue(state: GameState, content: Content): void {
state.actionElapsedMs = 0;
}
function completeActiveAction(state: GameState, content: Content): void {
const actionId = state.activeActionId;
if (!actionId) return;
grantYields(state, content, actionId);
startNextFromQueue(state, content);
export interface TickResult {
completedActionIds: string[];
}
/**
@@ -166,16 +163,21 @@ export function clearQueue(state: GameState): void {
* Advance the active action by `tickMs`. On completion, grants yields and
* advances the queue — actions do not auto-repeat when the queue is empty.
*/
export function tickGame(state: GameState, content: Content, tickMs: number): void {
if (!state.activeActionId) return;
export function tickGame(state: GameState, content: Content, tickMs: number): TickResult {
const completedActionIds: string[] = [];
if (!state.activeActionId) return { completedActionIds };
state.actionElapsedMs += tickMs;
while (state.activeActionId) {
const action = content.actionsById[state.activeActionId];
if (!action) return;
if (state.actionElapsedMs < action.durationMs) return;
const actionId = state.activeActionId;
const action = content.actionsById[actionId];
if (!action) return { completedActionIds };
if (state.actionElapsedMs < action.durationMs) return { completedActionIds };
state.actionElapsedMs -= action.durationMs;
completeActiveAction(state, content);
grantYields(state, content, actionId);
completedActionIds.push(actionId);
startNextFromQueue(state, content);
}
return { completedActionIds };
}