91 lines
5.6 KiB
JavaScript
91 lines
5.6 KiB
JavaScript
/**
|
||
* Research & build data-layer test (dev tool, run with Node — no browser):
|
||
*
|
||
* node dev/research-builds.test.mjs
|
||
*
|
||
* The player's progression loop has two halves — research (time-based,
|
||
* one project at a time) and building (credits + minerals) — and the
|
||
* command deck that will host them. The RULES are not implemented yet;
|
||
* this test pins the data contract the future code will lean on:
|
||
* - the three config files are registered in data/manifest.json;
|
||
* - the rule knobs exist (timeUnit, maxConcurrent = 1, resources);
|
||
* - projects/builds are typed, empty maps (no content yet);
|
||
* - the `_template` entries document every required field;
|
||
* - the deck has exactly six slots in the right order.
|
||
*/
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
import fs from 'node:fs';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const dataDir = join(__dirname, '../data');
|
||
const read = (name) => JSON.parse(fs.readFileSync(join(dataDir, name), 'utf8'));
|
||
|
||
const manifest = read('manifest.json');
|
||
const research = read('research.json');
|
||
const builds = read('builds.json');
|
||
const actionbar = read('actionbar.json');
|
||
|
||
let failures = 0;
|
||
const check = (label, cond) => {
|
||
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
|
||
if (!cond) failures++;
|
||
};
|
||
|
||
// ----------------------------------------------------------------------
|
||
// 1. Manifest registration
|
||
// ----------------------------------------------------------------------
|
||
for (const f of ['research.json', 'builds.json', 'actionbar.json']) {
|
||
check(`manifest registers ${f}`, manifest.files.includes(f));
|
||
}
|
||
|
||
// ----------------------------------------------------------------------
|
||
// 2. Research — time-based, one at a time
|
||
// ----------------------------------------------------------------------
|
||
check('research: player researches one thing at a time (maxConcurrent === 1)', research.maxConcurrent === 1);
|
||
check('research: timeUnit is a named unit', typeof research.timeUnit === 'string' && research.timeUnit.length > 0);
|
||
check('research: projects is a map', !!research.projects && typeof research.projects === 'object' && !Array.isArray(research.projects));
|
||
check('research: no projects yet (empty map)', Object.keys(research.projects ?? {}).filter((k) => !k.startsWith('_')).length === 0);
|
||
|
||
const rt = research._template ?? {};
|
||
for (const k of ['label', 'description', 'duration', 'requires', 'unlocks', 'effects', 'theme']) {
|
||
check(`research._template documents "${k}"`, k in rt);
|
||
}
|
||
check('research._template.duration is a positive number', typeof rt.duration === 'number' && rt.duration > 0);
|
||
check('research._template.requires is an array', Array.isArray(rt.requires));
|
||
check('research._template.unlocks names builds[] + research[]', Array.isArray(rt.unlocks?.builds) && Array.isArray(rt.unlocks?.research));
|
||
|
||
// ----------------------------------------------------------------------
|
||
// 3. Builds — credits + minerals
|
||
// ----------------------------------------------------------------------
|
||
check('builds: credits resource defined', typeof builds.resources?.credits?.label === 'string');
|
||
check('builds: minerals resource defined', typeof builds.resources?.minerals?.label === 'string');
|
||
check('builds: builds is a map', !!builds.builds && typeof builds.builds === 'object' && !Array.isArray(builds.builds));
|
||
check('builds: no builds yet (empty map)', Object.keys(builds.builds ?? {}).filter((k) => !k.startsWith('_')).length === 0);
|
||
|
||
const bt = builds._template ?? {};
|
||
for (const k of ['label', 'description', 'category', 'cost', 'requires', 'repeatable', 'effects', 'theme']) {
|
||
check(`builds._template documents "${k}"`, k in bt);
|
||
}
|
||
check('builds._template.category is a known kind', ['ship', 'planet', 'station', 'general'].includes(bt.category));
|
||
check('builds._template.cost pays in credits + minerals', typeof bt.cost?.credits === 'number' && typeof bt.cost?.minerals === 'number');
|
||
check('builds._template.repeatable is a boolean', typeof bt.repeatable === 'boolean');
|
||
check('builds._template.requires is an array', Array.isArray(bt.requires));
|
||
|
||
// ----------------------------------------------------------------------
|
||
// 4. Command deck — six evenly spaced slots, right order
|
||
// ----------------------------------------------------------------------
|
||
const slots = actionbar.buttons ?? [];
|
||
check('actionbar: exactly six slots', slots.length === 6);
|
||
check('actionbar: slot ids in order (Research, Build, Ship, ·, ·, Menu)', JSON.stringify(slots.map((s) => s.id)) === JSON.stringify(['research', 'build', 'ship', null, null, 'menu']));
|
||
check('actionbar: labels (Research / Build / Ship / · / · / Menu)', JSON.stringify(slots.map((s) => s.label)) === JSON.stringify(['Research', 'Build', 'Ship', null, null, 'Menu']));
|
||
const hex = /^#[0-9a-fA-F]{6}$/;
|
||
check('actionbar: live slots carry hex accents', slots.filter((s) => s.id).every((s) => hex.test(s.accent ?? '')));
|
||
check('actionbar: reserved slots stay null', slots.filter((s) => s.id === null).every((s) => s.label === null));
|
||
check('actionbar: CRT scanlines configured (pitch + alpha)', typeof actionbar.scanline?.pitch === 'number' && typeof actionbar.scanline?.alpha === 'number');
|
||
check('actionbar: RGB pull-apart configured (offsets + alphas)', typeof actionbar.animation?.rgb?.idleOffset === 'number' && typeof actionbar.animation?.rgb?.burstOffset === 'number' && typeof actionbar.animation?.rgb?.idleAlpha === 'number' && typeof actionbar.animation?.rgb?.burstAlpha === 'number');
|
||
check('actionbar: sane geometry (height 40–200 px)', typeof actionbar.height === 'number' && actionbar.height > 40 && actionbar.height < 200);
|
||
|
||
console.log(failures === 0 ? '\nall checks passed ✔' : `\n${failures} check(s) FAILED ✘`);
|
||
process.exit(failures === 0 ? 0 : 1);
|