fix(content): guard optional durationMs and cover loop validation

This commit is contained in:
ginnoir
2026-06-11 20:17:50 -05:00
parent c48e53cc7b
commit c1b38e36aa
4 changed files with 20 additions and 2 deletions
+17
View File
@@ -208,4 +208,21 @@ describe('action kind schema', () => {
}); });
expect(content.actionsById.rest.kind).toBe('loop'); expect(content.actionsById.rest.kind).toBe('loop');
}); });
it('rejects loop action without durationMs', () => {
expect(() =>
buildContent({
resources: [{ id: 'supplies', name: 'Supplies' }],
actions: [
{
id: 'broken_loop',
name: 'Broken loop',
kind: 'loop',
group: { id: 'camp', label: 'Camp' },
yields: [],
},
],
}),
).toThrow(/durationMs/);
});
}); });
+1 -1
View File
@@ -61,7 +61,7 @@ export const actionDefSchema = z
path: ['durationMs'], path: ['durationMs'],
}); });
} }
if (action.kind === 'story' && !action.storyChoiceId) { if (action.kind === 'story' && action.storyChoiceId === undefined) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'story actions require storyChoiceId', message: 'story actions require storyChoiceId',
+1
View File
@@ -169,6 +169,7 @@ export function tickGame(state: GameState, content: Content, tickMs: number): Ti
const actionId = state.activeActionId; const actionId = state.activeActionId;
const action = content.actionsById[actionId]; const action = content.actionsById[actionId];
if (!action) return { completedActionIds }; if (!action) return { completedActionIds };
if (action.durationMs === undefined) return { completedActionIds };
if (state.actionElapsedMs < action.durationMs) return { completedActionIds }; if (state.actionElapsedMs < action.durationMs) return { completedActionIds };
state.actionElapsedMs -= action.durationMs; state.actionElapsedMs -= action.durationMs;
+1 -1
View File
@@ -80,7 +80,7 @@ export function toView(state: GameState, content: GameContent): GameView {
})); }));
const action = state.activeActionId ? content.actionsById[state.activeActionId] : undefined; const action = state.activeActionId ? content.actionsById[state.activeActionId] : undefined;
const actionProgress = action ? Math.min(1, state.actionElapsedMs / action.durationMs) : 0; const actionProgress = action && action.durationMs ? Math.min(1, state.actionElapsedMs / action.durationMs) : 0;
const queuedActionIds = [...state.actionQueue]; const queuedActionIds = [...state.actionQueue];
const queuedActionNames = queuedActionIds.map((id) => content.actionsById[id]?.name ?? id); const queuedActionNames = queuedActionIds.map((id) => content.actionsById[id]?.name ?? id);