feat(engine): add story fields to GameState
This commit is contained in:
@@ -76,6 +76,16 @@ describe('createGameState()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createGameState() story fields', () => {
|
||||||
|
it('initializes empty story state', () => {
|
||||||
|
const content = testContent();
|
||||||
|
const state = createGameState(content);
|
||||||
|
expect(state.storyFlags).toEqual({});
|
||||||
|
expect(state.currentStoryNodeId).toBe('');
|
||||||
|
expect(state.seenStoryNodeIds).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('startAction()', () => {
|
describe('startAction()', () => {
|
||||||
it('activates the action and resets its progress', () => {
|
it('activates the action and resets its progress', () => {
|
||||||
const content = testContent();
|
const content = testContent();
|
||||||
@@ -202,7 +212,8 @@ describe('unlock conditions', () => {
|
|||||||
});
|
});
|
||||||
const state = createGameState(content);
|
const state = createGameState(content);
|
||||||
expect(() => enqueueAction(state, content, 'secret')).toThrow(/cannot enqueue/i);
|
expect(() => enqueueAction(state, content, 'secret')).toThrow(/cannot enqueue/i);
|
||||||
expect(canUnlockAction(state, content, 'secret', { path_scouted: true })).toBe(true);
|
state.storyFlags.path_scouted = true;
|
||||||
|
expect(canUnlockAction(state, content, 'secret')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+19
-16
@@ -16,6 +16,12 @@ export interface GameState {
|
|||||||
actionElapsedMs: number;
|
actionElapsedMs: number;
|
||||||
/** Action ids waiting to run after the active action finishes. */
|
/** Action ids waiting to run after the active action finishes. */
|
||||||
actionQueue: string[];
|
actionQueue: string[];
|
||||||
|
/** Story progression flags set by the narrative graph. */
|
||||||
|
storyFlags: Record<string, boolean>;
|
||||||
|
/** Id of the story node currently displayed, or empty when none. */
|
||||||
|
currentStoryNodeId: string;
|
||||||
|
/** Story node ids the player has already seen. */
|
||||||
|
seenStoryNodeIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGameState(content: Content): GameState {
|
export function createGameState(content: Content): GameState {
|
||||||
@@ -23,7 +29,15 @@ export function createGameState(content: Content): GameState {
|
|||||||
for (const resource of content.resources) {
|
for (const resource of content.resources) {
|
||||||
resources[resource.id] = resource.startAmount;
|
resources[resource.id] = resource.startAmount;
|
||||||
}
|
}
|
||||||
return { resources, activeActionId: null, actionElapsedMs: 0, actionQueue: [] };
|
return {
|
||||||
|
resources,
|
||||||
|
activeActionId: null,
|
||||||
|
actionElapsedMs: 0,
|
||||||
|
actionQueue: [],
|
||||||
|
storyFlags: {},
|
||||||
|
currentStoryNodeId: '',
|
||||||
|
seenStoryNodeIds: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertKnownAction(content: Content, actionId: string): void {
|
function assertKnownAction(content: Content, actionId: string): void {
|
||||||
@@ -38,13 +52,7 @@ export function canAffordAction(state: GameState, content: Content, actionId: st
|
|||||||
return action.costs.every((cost) => (state.resources[cost.resourceId] ?? 0) >= cost.amount);
|
return action.costs.every((cost) => (state.resources[cost.resourceId] ?? 0) >= cost.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Story flags land in PR2; placeholder field keeps unlock schema honest. */
|
export function canUnlockAction(state: GameState, content: Content, actionId: string): boolean {
|
||||||
export function canUnlockAction(
|
|
||||||
state: GameState,
|
|
||||||
content: Content,
|
|
||||||
actionId: string,
|
|
||||||
storyFlags: Record<string, boolean> = {},
|
|
||||||
): boolean {
|
|
||||||
const action = content.actionsById[actionId];
|
const action = content.actionsById[actionId];
|
||||||
if (!action) return false;
|
if (!action) return false;
|
||||||
const unlock = action.unlock;
|
const unlock = action.unlock;
|
||||||
@@ -56,21 +64,16 @@ export function canUnlockAction(
|
|||||||
}
|
}
|
||||||
if (unlock.requireStoryFlags) {
|
if (unlock.requireStoryFlags) {
|
||||||
for (const flag of unlock.requireStoryFlags) {
|
for (const flag of unlock.requireStoryFlags) {
|
||||||
if (!storyFlags[flag]) return false;
|
if (!state.storyFlags[flag]) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isActionAvailable(
|
export function isActionAvailable(state: GameState, content: Content, actionId: string): boolean {
|
||||||
state: GameState,
|
|
||||||
content: Content,
|
|
||||||
actionId: string,
|
|
||||||
storyFlags: Record<string, boolean> = {},
|
|
||||||
): boolean {
|
|
||||||
return (
|
return (
|
||||||
canAffordAction(state, content, actionId) &&
|
canAffordAction(state, content, actionId) &&
|
||||||
canUnlockAction(state, content, actionId, storyFlags)
|
canUnlockAction(state, content, actionId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ describe('loadGame()', () => {
|
|||||||
activeActionId: 'ghost-action',
|
activeActionId: 'ghost-action',
|
||||||
actionElapsedMs: 0,
|
actionElapsedMs: 0,
|
||||||
actionQueue: [],
|
actionQueue: [],
|
||||||
|
storyFlags: {},
|
||||||
|
currentStoryNodeId: '',
|
||||||
|
seenStoryNodeIds: [],
|
||||||
},
|
},
|
||||||
1000,
|
1000,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ export async function loadGame(
|
|||||||
activeActionId,
|
activeActionId,
|
||||||
actionElapsedMs: save.state.actionElapsedMs,
|
actionElapsedMs: save.state.actionElapsedMs,
|
||||||
actionQueue: [...(save.state.actionQueue ?? [])],
|
actionQueue: [...(save.state.actionQueue ?? [])],
|
||||||
|
storyFlags: { ...base.storyFlags },
|
||||||
|
currentStoryNodeId: base.currentStoryNodeId,
|
||||||
|
seenStoryNodeIds: [...base.seenStoryNodeIds],
|
||||||
};
|
};
|
||||||
savedAt = save.savedAt;
|
savedAt = save.savedAt;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user