feat: scenario validator + AFTERIMAGE pilot data
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
export function validateScenario(s) {
|
||||
if (!s || typeof s !== 'object') return ['scenario must be an object'];
|
||||
const errors = [];
|
||||
if (!s.meta?.id) errors.push('meta.id required');
|
||||
if (!s.meta?.system) errors.push('meta.system required');
|
||||
if (!Array.isArray(s.timeline) || s.timeline.length === 0) {
|
||||
errors.push('timeline must be a non-empty array');
|
||||
} else {
|
||||
s.timeline.forEach((b, i) => {
|
||||
if (!b.id) errors.push(`timeline[${i}].id required`);
|
||||
if (typeof b.targetMin !== 'number') errors.push(`timeline[${i}].targetMin must be a number`);
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
meta: { id: 'afterimage', title: 'AFTERIMAGE', system: 'year-zero', players: 4, playMinutes: 210, slot: 'Fri · Slot 2' },
|
||||
timeline: [
|
||||
{ id: 'a1-open', act: 1, label: 'The pier — the case lands', targetMin: 0, hardTrigger: true },
|
||||
{ id: 'a1-parlor', act: 1, label: 'Parlor 88 interview', targetMin: 35, cutHint: 'Summarise the ledger; skip the tea ritual' },
|
||||
{ id: 'a2-market', act: 2, label: 'The Market', targetMin: 90 },
|
||||
{ id: 'a2-doorcam', act: 2, label: 'Door-cam reveal', targetMin: 120, hardTrigger: true, notes: 'The pivot — do not let this slip past 2:15' },
|
||||
{ id: 'a3-pier', act: 3, label: 'Pier confrontation', targetMin: 175, hardTrigger: true },
|
||||
{ id: 'epilogue', act: 4, label: 'Baseline / ambiguous end', targetMin: 200 },
|
||||
],
|
||||
clues: [],
|
||||
cast: [],
|
||||
props: [],
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateScenario } from '../../src/core/scenario.js';
|
||||
import afterimage from '../../src/scenarios/afterimage.js';
|
||||
import { getRulePack } from '../../src/dice/rulepacks/index.js';
|
||||
|
||||
describe('scenario validation', () => {
|
||||
it('flags missing required fields', () => {
|
||||
expect(validateScenario(null)).toContain('scenario must be an object');
|
||||
const errs = validateScenario({ meta: {}, timeline: [] });
|
||||
expect(errs).toContain('meta.id required');
|
||||
expect(errs).toContain('meta.system required');
|
||||
expect(errs).toContain('timeline must be a non-empty array');
|
||||
});
|
||||
|
||||
it('flags a beat missing targetMin', () => {
|
||||
const errs = validateScenario({ meta: { id: 'x', system: 'year-zero' }, timeline: [{ id: 'a' }] });
|
||||
expect(errs).toContain('timeline[0].targetMin must be a number');
|
||||
});
|
||||
|
||||
it('accepts the AFTERIMAGE pilot and its system resolves', () => {
|
||||
expect(validateScenario(afterimage)).toEqual([]);
|
||||
expect(afterimage.meta.system).toBe('year-zero');
|
||||
expect(getRulePack(afterimage.meta.system)).not.toBe(null);
|
||||
expect(afterimage.timeline.length).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user