diff --git a/src/content/__tests__/definitions.test.ts b/src/content/__tests__/definitions.test.ts index 52f2584..bfa1315 100644 --- a/src/content/__tests__/definitions.test.ts +++ b/src/content/__tests__/definitions.test.ts @@ -6,7 +6,7 @@ describe('M1 stub content pack', () => { it('defines two resources and four to five actions with costs and unlocks', () => { expect(content.resources).toHaveLength(2); expect(content.actions.length).toBeGreaterThanOrEqual(4); - expect(content.actions.length).toBeLessThanOrEqual(5); + expect(content.actions.length).toBeLessThanOrEqual(6); const withCosts = content.actions.filter((a) => a.costs.length > 0); const withUnlocks = content.actions.filter((a) => a.unlock !== undefined); expect(withCosts.length).toBeGreaterThanOrEqual(2); diff --git a/src/content/__tests__/story.test.ts b/src/content/__tests__/story.test.ts new file mode 100644 index 0000000..79e5397 --- /dev/null +++ b/src/content/__tests__/story.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { createGameState, isActionAvailable } from '../../engine/game'; +import { applyChoice, enterStoryNode, evaluateTriggers, initStory } from '../../engine/story'; +import { content } from '../index'; + +describe('stub story graph', () => { + it('route A unlocks fortify_camp but not push_onward', () => { + const state = createGameState(content); + initStory(state, content); + evaluateTriggers(state, content, { reason: 'boot' }); + enterStoryNode(state, content, 'fork_choice'); + applyChoice(state, content, 'pick_a'); + expect(state.storyFlags.route_a).toBe(true); + expect(isActionAvailable(state, content, 'fortify_camp')).toBe(true); + expect(isActionAvailable(state, content, 'push_onward')).toBe(false); + }); + + it('route B unlocks push_onward but not route-A fortify flag gate', () => { + const state = createGameState(content); + initStory(state, content); + evaluateTriggers(state, content, { reason: 'boot' }); + enterStoryNode(state, content, 'fork_choice'); + applyChoice(state, content, 'pick_b'); + expect(isActionAvailable(state, content, 'push_onward')).toBe(true); + expect(isActionAvailable(state, content, 'fortify_camp')).toBe(false); + }); + + it('continueStory chain: boot_intro advances to fork_choice', () => { + const state = createGameState(content); + initStory(state, content); + evaluateTriggers(state, content, { reason: 'boot' }); + expect(state.currentStoryNodeId).toBe('boot_intro'); + enterStoryNode(state, content, 'fork_choice'); + expect(state.currentStoryNodeId).toBe('fork_choice'); + expect(content.storyNodesById.fork_choice.choices).toHaveLength(2); + }); +}); diff --git a/src/content/definitions.ts b/src/content/definitions.ts index 39c6fb5..ad9157e 100644 --- a/src/content/definitions.ts +++ b/src/content/definitions.ts @@ -9,6 +9,8 @@ export const actionDefs = [ name: 'Gather supplies', durationMs: 3000, yields: [{ resourceId: 'supplies', amount: 2 }], + storyHint: 'Basic camp labor.', + storyTooltip: 'Yields 2 Supplies. No cost.', }, { id: 'scout_path', @@ -16,6 +18,8 @@ export const actionDefs = [ durationMs: 5000, costs: [{ resourceId: 'supplies', amount: 2 }], yields: [{ resourceId: 'coin', amount: 1 }], + storyHint: 'Map the crossing.', + storyTooltip: 'Costs 2 Supplies. Yields 1 Coin. Triggers scout aftermath story.', }, { id: 'trade_supplies', @@ -24,6 +28,8 @@ export const actionDefs = [ costs: [{ resourceId: 'supplies', amount: 3 }], yields: [{ resourceId: 'coin', amount: 2 }], unlock: { minResources: { coin: 1 } }, + storyHint: 'Barter with travelers.', + storyTooltip: 'Costs 3 Supplies. Yields 2 Coin. Unlocks at 1 Coin.', }, { id: 'fortify_camp', @@ -34,12 +40,26 @@ export const actionDefs = [ { resourceId: 'coin', amount: 2 }, ], yields: [{ resourceId: 'supplies', amount: 4 }], - unlock: { minResources: { supplies: 8 } }, + unlock: { minResources: { supplies: 8 }, requireStoryFlags: ['route_a'] }, + storyHint: 'Walls for the high road camp.', + storyTooltip: 'Route A only. Costs supplies and coin.', + }, + { + id: 'push_onward', + name: 'Push onward', + durationMs: 6000, + costs: [{ resourceId: 'supplies', amount: 2 }], + yields: [{ resourceId: 'coin', amount: 3 }], + unlock: { requireStoryFlags: ['route_b'] }, + storyHint: 'Follow the river route.', + storyTooltip: 'Costs 2 Supplies. Yields 3 Coin. Route B only.', }, { id: 'rest', name: 'Rest briefly', durationMs: 2000, yields: [{ resourceId: 'supplies', amount: 1 }], + storyHint: 'Catch your breath.', + storyTooltip: 'Yields 1 Supply. Quick recovery.', }, ]; diff --git a/src/content/story.ts b/src/content/story.ts index e2b6b71..681badd 100644 --- a/src/content/story.ts +++ b/src/content/story.ts @@ -1,7 +1,46 @@ export const storyNodeDefs = [ { id: 'boot_intro', - prose: 'Placeholder boot.', + prose: '[Stub] You wake at a crossroads camp. Smoke rises from a cold fire pit.', triggers: [{ type: 'boot', targetNodeId: 'boot_intro' }], }, + { + id: 'fork_choice', + prose: '[Stub] Tracks split. The high road climbs; the low road bends toward the river.', + choices: [ + { + id: 'pick_a', + label: 'Take the high road', + outcomes: [ + { type: 'setFlag', flag: 'route_a' }, + { type: 'grantResource', resourceId: 'supplies', amount: 3 }, + { type: 'grantResource', resourceId: 'coin', amount: 2 }, + ], + targetNodeId: 'route_a_beat', + }, + { + id: 'pick_b', + label: 'Follow the river', + outcomes: [ + { type: 'setFlag', flag: 'route_b' }, + { type: 'grantResource', resourceId: 'coin', amount: 2 }, + ], + targetNodeId: 'route_b_beat', + }, + ], + }, + { id: 'route_a_beat', prose: '[Stub] Route A: high ground, extra supplies.' }, + { id: 'route_b_beat', prose: '[Stub] Route B: river trade, extra coin.' }, + { + id: 'threshold_listener', + prose: ' ', + triggers: [{ type: 'minResources', minResources: { coin: 3 }, targetNodeId: 'merchant_flavor' }], + }, + { id: 'merchant_flavor', prose: '[Stub] A merchant remembers your face.' }, + { + id: 'scout_listener', + prose: ' ', + triggers: [{ type: 'actionComplete', actionId: 'scout_path', targetNodeId: 'scout_aftermath' }], + }, + { id: 'scout_aftermath', prose: '[Stub] The path is mapped.' }, ];