feat(content): migrate stub actions to kind/group model
This commit is contained in:
@@ -3,21 +3,28 @@ import { createGameState, enqueueAction, tickGame } from '../../engine/game';
|
||||
import { content } from '../index';
|
||||
|
||||
describe('M1 stub content pack', () => {
|
||||
it('defines two resources and four to five actions with costs and unlocks', () => {
|
||||
it('defines two resources and multiple actions with costs, unlocks, and varied kinds', () => {
|
||||
expect(content.resources).toHaveLength(2);
|
||||
expect(content.actions.length).toBeGreaterThanOrEqual(4);
|
||||
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);
|
||||
expect(withUnlocks.length).toBeGreaterThanOrEqual(1);
|
||||
// verify the new kinds are present
|
||||
const kinds = new Set(content.actions.map((a) => a.kind));
|
||||
expect(kinds.has('timed')).toBe(true);
|
||||
expect(kinds.has('loop')).toBe(true);
|
||||
expect(kinds.has('story')).toBe(true);
|
||||
});
|
||||
|
||||
it('can simulate a costed action without throwing', () => {
|
||||
it('can simulate a costed timed action without throwing', () => {
|
||||
const state = createGameState(content);
|
||||
const trade = content.actions.find((a) => a.costs.length > 0);
|
||||
const trade = content.actions.find((a) => a.costs.length > 0 && a.kind === 'timed');
|
||||
if (!trade) {
|
||||
throw new Error('expected at least one costed action');
|
||||
throw new Error('expected at least one costed timed action');
|
||||
}
|
||||
if (trade.durationMs === undefined) {
|
||||
throw new Error('expected costed timed action to have durationMs');
|
||||
}
|
||||
enqueueAction(state, content, trade.id);
|
||||
tickGame(state, content, trade.durationMs);
|
||||
|
||||
@@ -9,6 +9,8 @@ const actionsById = {
|
||||
scout_path: {
|
||||
id: 'scout_path',
|
||||
name: 'Scout',
|
||||
kind: 'timed' as const,
|
||||
group: { id: 'travel', label: 'Travel' },
|
||||
durationMs: 1000,
|
||||
costs: [],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
|
||||
@@ -7,6 +7,8 @@ export const actionDefs = [
|
||||
{
|
||||
id: 'gather_supplies',
|
||||
name: 'Gather supplies',
|
||||
kind: 'timed',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'supplies', amount: 2 }],
|
||||
storyHint: 'Basic camp labor.',
|
||||
@@ -15,6 +17,8 @@ export const actionDefs = [
|
||||
{
|
||||
id: 'scout_path',
|
||||
name: 'Scout the path',
|
||||
kind: 'timed',
|
||||
group: { id: 'travel', label: 'Travel' },
|
||||
durationMs: 5000,
|
||||
costs: [{ resourceId: 'supplies', amount: 2 }],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
@@ -24,6 +28,8 @@ export const actionDefs = [
|
||||
{
|
||||
id: 'trade_supplies',
|
||||
name: 'Trade at camp',
|
||||
kind: 'timed',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 4000,
|
||||
costs: [{ resourceId: 'supplies', amount: 3 }],
|
||||
yields: [{ resourceId: 'coin', amount: 2 }],
|
||||
@@ -34,6 +40,8 @@ export const actionDefs = [
|
||||
{
|
||||
id: 'fortify_camp',
|
||||
name: 'Fortify camp',
|
||||
kind: 'timed',
|
||||
group: { id: 'camp', label: 'Camp' },
|
||||
durationMs: 8000,
|
||||
costs: [
|
||||
{ resourceId: 'supplies', amount: 5 },
|
||||
@@ -47,6 +55,8 @@ export const actionDefs = [
|
||||
{
|
||||
id: 'push_onward',
|
||||
name: 'Push onward',
|
||||
kind: 'timed',
|
||||
group: { id: 'travel', label: 'Travel' },
|
||||
durationMs: 6000,
|
||||
costs: [{ resourceId: 'supplies', amount: 2 }],
|
||||
yields: [{ resourceId: 'coin', amount: 3 }],
|
||||
@@ -57,9 +67,32 @@ export const actionDefs = [
|
||||
{
|
||||
id: 'rest',
|
||||
name: 'Rest briefly',
|
||||
kind: 'loop',
|
||||
group: { id: 'camp_loop', label: 'Camp activities' },
|
||||
loopPriority: 0,
|
||||
durationMs: 2000,
|
||||
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||
storyHint: 'Catch your breath.',
|
||||
storyTooltip: 'Yields 1 Supply. Quick recovery.',
|
||||
storyTooltip: 'Idle upkeep — runs when nothing else is queued.',
|
||||
},
|
||||
{
|
||||
id: 'pick_high_road',
|
||||
name: 'Take the high road',
|
||||
kind: 'story',
|
||||
group: { id: 'fork', label: 'Crossroads' },
|
||||
storyChoiceId: 'pick_a',
|
||||
storyHint: 'Route A — high ground and supplies.',
|
||||
storyTooltip: 'Story fork: grants route A flag and resources. Hides river path.',
|
||||
yields: [],
|
||||
},
|
||||
{
|
||||
id: 'follow_river',
|
||||
name: 'Follow the river',
|
||||
kind: 'story',
|
||||
group: { id: 'fork', label: 'Crossroads' },
|
||||
storyChoiceId: 'pick_b',
|
||||
storyHint: 'Route B — river trade and coin.',
|
||||
storyTooltip: 'Story fork: grants route B flag. Hides high road path.',
|
||||
yields: [],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -10,6 +10,7 @@ function testContent() {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'test', label: 'Test' },
|
||||
durationMs: 300,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
tickGame,
|
||||
} from '../game';
|
||||
|
||||
const DEFAULT_GROUP = { id: 'test', label: 'Test' };
|
||||
|
||||
function testContent() {
|
||||
return buildContent({
|
||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 5 }],
|
||||
@@ -17,6 +19,7 @@ function testContent() {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 300,
|
||||
yields: [{ resourceId: 'gold', amount: 2 }],
|
||||
},
|
||||
@@ -28,9 +31,9 @@ function queueContent() {
|
||||
return buildContent({
|
||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
||||
actions: [
|
||||
{ id: 'a', name: 'A', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'b', name: 'B', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'c', name: 'C', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'a', name: 'A', group: DEFAULT_GROUP, durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'b', name: 'B', group: DEFAULT_GROUP, durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'c', name: 'C', group: DEFAULT_GROUP, durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -45,12 +48,14 @@ function costContent() {
|
||||
{
|
||||
id: 'gather',
|
||||
name: 'Gather',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 300,
|
||||
yields: [{ resourceId: 'supplies', amount: 2 }],
|
||||
},
|
||||
{
|
||||
id: 'trade',
|
||||
name: 'Trade',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 300,
|
||||
costs: [{ resourceId: 'supplies', amount: 5 }],
|
||||
yields: [{ resourceId: 'coin', amount: 3 }],
|
||||
@@ -58,6 +63,7 @@ function costContent() {
|
||||
{
|
||||
id: 'scout',
|
||||
name: 'Scout',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 300,
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
unlock: { minResources: { coin: 1 } },
|
||||
@@ -204,6 +210,7 @@ describe('unlock conditions', () => {
|
||||
{
|
||||
id: 'secret',
|
||||
name: 'Secret',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 100,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
unlock: { requireStoryFlags: ['path_scouted'] },
|
||||
@@ -245,12 +252,14 @@ describe('completion advances queue', () => {
|
||||
{
|
||||
id: 'cheap',
|
||||
name: 'Cheap',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 100,
|
||||
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||
},
|
||||
{
|
||||
id: 'dear',
|
||||
name: 'Dear',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 100,
|
||||
costs: [{ resourceId: 'supplies', amount: 6 }],
|
||||
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||
@@ -258,6 +267,7 @@ describe('completion advances queue', () => {
|
||||
{
|
||||
id: 'free',
|
||||
name: 'Free',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 100,
|
||||
yields: [{ resourceId: 'supplies', amount: 1 }],
|
||||
},
|
||||
@@ -283,6 +293,7 @@ describe('completion advances queue', () => {
|
||||
{
|
||||
id: 'combo',
|
||||
name: 'Combo',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 100,
|
||||
yields: [
|
||||
{ resourceId: 'a', amount: 2 },
|
||||
|
||||
@@ -18,6 +18,7 @@ function testContent() {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: { id: 'test', label: 'Test' },
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
},
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
type StoryEvent,
|
||||
} from '../story';
|
||||
|
||||
const DEFAULT_GROUP = { id: 'test', label: 'Test' };
|
||||
|
||||
function gameContent() {
|
||||
const base = buildContent({
|
||||
resources: [
|
||||
@@ -23,6 +25,7 @@ function gameContent() {
|
||||
{
|
||||
id: 'scout_path',
|
||||
name: 'Scout',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
costs: [],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
@@ -54,6 +57,7 @@ function gameContentWithActionTrigger() {
|
||||
{
|
||||
id: 'scout_path',
|
||||
name: 'Scout',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
costs: [],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
@@ -91,6 +95,7 @@ function gameContentWithThreshold() {
|
||||
{
|
||||
id: 'scout_path',
|
||||
name: 'Scout',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
costs: [],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
@@ -132,6 +137,7 @@ function gameContentWithFork() {
|
||||
{
|
||||
id: 'scout_path',
|
||||
name: 'Scout',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
costs: [],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
@@ -189,6 +195,7 @@ function gameContentWithGatedChoice() {
|
||||
{
|
||||
id: 'scout_path',
|
||||
name: 'Scout',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
costs: [],
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createGameState, enqueueAction, startAction } from '../../engine/game';
|
||||
import { createSave, serializeSave } from '../../engine/save';
|
||||
import { createMemoryBackend, loadGame, saveGame } from '../persistence';
|
||||
|
||||
const DEFAULT_GROUP = { id: 'test', label: 'Test' };
|
||||
|
||||
function testContent() {
|
||||
return buildContent({
|
||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
||||
@@ -11,6 +13,7 @@ function testContent() {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 3000,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
},
|
||||
@@ -22,8 +25,8 @@ function queueTestContent() {
|
||||
return buildContent({
|
||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }],
|
||||
actions: [
|
||||
{ id: 'a', name: 'A', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'b', name: 'B', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'a', name: 'A', group: DEFAULT_GROUP, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'b', name: 'B', group: DEFAULT_GROUP, durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createGameState, enqueueAction, startAction } from '../../engine/game';
|
||||
import { enterStoryNode } from '../../engine/story';
|
||||
import { formatOfflineDuration, toView } from '../viewModel';
|
||||
|
||||
const DEFAULT_GROUP = { id: 'test', label: 'Test' };
|
||||
|
||||
function testContent() {
|
||||
const base = buildContent({
|
||||
resources: [{ id: 'gold', name: 'Gold', startAmount: 4 }],
|
||||
@@ -12,6 +14,7 @@ function testContent() {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 200,
|
||||
yields: [{ resourceId: 'gold', amount: 1 }],
|
||||
},
|
||||
@@ -87,8 +90,8 @@ describe('toView()', () => {
|
||||
const content = contentWithActions(
|
||||
[{ id: 'gold', name: 'Gold' }],
|
||||
[
|
||||
{ id: 'a', name: 'Alpha', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'b', name: 'Bravo', durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'a', name: 'Alpha', group: DEFAULT_GROUP, durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
{ id: 'b', name: 'Bravo', group: DEFAULT_GROUP, durationMs: 1000, yields: [{ resourceId: 'gold', amount: 1 }] },
|
||||
],
|
||||
);
|
||||
const state = createGameState(content);
|
||||
@@ -108,6 +111,7 @@ describe('toView() action availability', () => {
|
||||
{
|
||||
id: 'locked',
|
||||
name: 'Locked',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
unlock: { requireStoryFlags: ['route_a'] },
|
||||
@@ -127,6 +131,7 @@ describe('toView() action availability', () => {
|
||||
{
|
||||
id: 'forage',
|
||||
name: 'Forage',
|
||||
group: DEFAULT_GROUP,
|
||||
durationMs: 1000,
|
||||
yields: [{ resourceId: 'coin', amount: 1 }],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user