229 lines
6.9 KiB
TypeScript
229 lines
6.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { buildContent } from '../schema';
|
|
|
|
const validResources = [{ id: 'gold', name: 'Gold' }];
|
|
const validActions = [
|
|
{
|
|
id: 'forage',
|
|
name: 'Forage',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: 3000,
|
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
|
},
|
|
];
|
|
|
|
describe('buildContent()', () => {
|
|
it('validates definitions and indexes them by id', () => {
|
|
const content = buildContent({ resources: validResources, actions: validActions });
|
|
expect(content.resourcesById.gold.name).toBe('Gold');
|
|
expect(content.actionsById.forage.durationMs).toBe(3000);
|
|
});
|
|
|
|
it('applies the startAmount default of 0', () => {
|
|
const content = buildContent({ resources: validResources, actions: validActions });
|
|
expect(content.resourcesById.gold.startAmount).toBe(0);
|
|
});
|
|
|
|
it('rejects an action that yields an unknown resource', () => {
|
|
const actions = [
|
|
{
|
|
id: 'forage',
|
|
name: 'Forage',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: 3000,
|
|
yields: [{ resourceId: 'ghost', amount: 1 }],
|
|
},
|
|
];
|
|
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
|
|
});
|
|
|
|
it('rejects duplicate resource ids', () => {
|
|
const resources = [
|
|
{ id: 'gold', name: 'Gold' },
|
|
{ id: 'gold', name: 'Gold Again' },
|
|
];
|
|
expect(() => buildContent({ resources, actions: validActions })).toThrow(/duplicate/i);
|
|
});
|
|
|
|
it('rejects a structurally invalid definition', () => {
|
|
const actions = [
|
|
{
|
|
id: 'forage',
|
|
name: 'Forage',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: -1,
|
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
|
},
|
|
];
|
|
expect(() => buildContent({ resources: validResources, actions })).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('costs and multi-yield', () => {
|
|
it('accepts optional costs and multiple yields', () => {
|
|
const resources = [
|
|
{ id: 'gold', name: 'Gold' },
|
|
{ id: 'wood', name: 'Wood' },
|
|
];
|
|
const actions = [
|
|
{
|
|
id: 'craft',
|
|
name: 'Craft',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: 1000,
|
|
costs: [{ resourceId: 'wood', amount: 2 }],
|
|
yields: [
|
|
{ resourceId: 'gold', amount: 1 },
|
|
{ resourceId: 'wood', amount: 1 },
|
|
],
|
|
},
|
|
];
|
|
const content = buildContent({ resources, actions });
|
|
expect(content.actionsById.craft.costs).toEqual([{ resourceId: 'wood', amount: 2 }]);
|
|
expect(content.actionsById.craft.yields).toHaveLength(2);
|
|
});
|
|
|
|
it('rejects cost referencing unknown resource', () => {
|
|
const actions = [
|
|
{
|
|
id: 'craft',
|
|
name: 'Craft',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: 1000,
|
|
costs: [{ resourceId: 'ghost', amount: 1 }],
|
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
|
},
|
|
];
|
|
expect(() => buildContent({ resources: validResources, actions })).toThrow(/unknown resource/i);
|
|
});
|
|
|
|
it('defaults costs to empty array', () => {
|
|
const content = buildContent({ resources: validResources, actions: validActions });
|
|
expect(content.actionsById.forage.costs).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('unlock conditions', () => {
|
|
it('accepts optional minResources and requireStoryFlags', () => {
|
|
const actions = [
|
|
{
|
|
id: 'forage',
|
|
name: 'Forage',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: 3000,
|
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
|
unlock: {
|
|
minResources: { gold: 10 },
|
|
requireStoryFlags: ['intro_complete'],
|
|
},
|
|
},
|
|
];
|
|
const content = buildContent({ resources: validResources, actions });
|
|
expect(content.actionsById.forage.unlock).toEqual({
|
|
minResources: { gold: 10 },
|
|
requireStoryFlags: ['intro_complete'],
|
|
});
|
|
});
|
|
|
|
it('defaults unlock to undefined when omitted', () => {
|
|
const content = buildContent({ resources: validResources, actions: validActions });
|
|
expect(content.actionsById.forage.unlock).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('action narrative fields', () => {
|
|
it('accepts optional storyHint and storyTooltip', () => {
|
|
const actions = [
|
|
{
|
|
id: 'forage',
|
|
name: 'Forage',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
durationMs: 3000,
|
|
yields: [{ resourceId: 'gold', amount: 1 }],
|
|
storyHint: 'Gather what the forest offers.',
|
|
storyTooltip: 'Your first step into the wild.',
|
|
},
|
|
];
|
|
const content = buildContent({ resources: validResources, actions });
|
|
expect(content.actionsById.forage.storyHint).toBe('Gather what the forest offers.');
|
|
expect(content.actionsById.forage.storyTooltip).toBe('Your first step into the wild.');
|
|
});
|
|
|
|
it('defaults storyHint and storyTooltip to undefined when omitted', () => {
|
|
const content = buildContent({ resources: validResources, actions: validActions });
|
|
expect(content.actionsById.forage.storyHint).toBeUndefined();
|
|
expect(content.actionsById.forage.storyTooltip).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('action kind schema', () => {
|
|
it('accepts kind, group, and storyChoiceId', () => {
|
|
const content = buildContent({
|
|
resources: [{ id: 'supplies', name: 'Supplies' }],
|
|
actions: [
|
|
{
|
|
id: 'pick_high_road',
|
|
name: 'Take the high road',
|
|
kind: 'story',
|
|
group: { id: 'fork', label: 'Crossroads' },
|
|
storyChoiceId: 'pick_a',
|
|
yields: [],
|
|
},
|
|
],
|
|
});
|
|
expect(content.actionsById.pick_high_road.kind).toBe('story');
|
|
expect(content.actionsById.pick_high_road.group.label).toBe('Crossroads');
|
|
});
|
|
|
|
it('defaults kind to timed and requires durationMs for timed actions', () => {
|
|
expect(() =>
|
|
buildContent({
|
|
resources: [{ id: 'supplies', name: 'Supplies' }],
|
|
actions: [
|
|
{
|
|
id: 'broken',
|
|
name: 'Broken',
|
|
kind: 'timed',
|
|
group: { id: 'camp', label: 'Camp' },
|
|
yields: [{ resourceId: 'supplies', amount: 1 }],
|
|
},
|
|
],
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it('requires durationMs for loop actions', () => {
|
|
const content = buildContent({
|
|
resources: [{ id: 'supplies', name: 'Supplies' }],
|
|
actions: [
|
|
{
|
|
id: 'rest',
|
|
name: 'Rest',
|
|
kind: 'loop',
|
|
group: { id: 'camp_loop', label: 'Camp activities' },
|
|
durationMs: 2000,
|
|
yields: [{ resourceId: 'supplies', amount: 1 }],
|
|
},
|
|
],
|
|
});
|
|
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/);
|
|
});
|
|
});
|