feat(content): stub story graph with A/B branch and route-gated actions
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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.',
|
||||
},
|
||||
];
|
||||
|
||||
+40
-1
@@ -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.' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user