orbit/dev/jumpgate.test.mjs

148 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* JumpGate entity test (dev tool, run with Node — no browser needed):
*
* node dev/jumpgate.test.mjs
*
* Stubs just enough of Phaser to construct the REAL JumpGate from
* js/entities/JumpGate.js (the system's exit — content.jumps, laid out
* by SystemGenerator.layoutGates), then asserts the contract the rest
* of the game relies on:
* - discovery identity: discoveryId/discoveryName from the record,
* size from data/gates.json, bound = size (compass/toast scale),
* clearance = gates.shipClearance, rotation = the record's bearing;
* - the SOLID contract (same as Planet/Station): minCenterDistance,
* edgePoint exactly `gap` past the surface, aimPoint clamping, and
* constrainShip pushing the ship out of the keepout circle
* (through the shared Planet.resolve);
* - update() breathes the field without throwing;
* - destroy() tears the children down.
*/
import { pathToFileURL, fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// --- Phaser stub: the Container base + a recording graphics --------------
class GameObject {
constructor(scene, x = 0, y = 0) {
this.scene = scene;
this.x = x;
this.y = y;
this.rotation = 0;
this.alpha = 1;
this.children = [];
}
add(o) { this.children.push(o); return this; }
remove(o) { this.children = this.children.filter((c) => c !== o); return this; }
removeChildren() { this.children.length = 0; return this; }
setOrigin() { return this; }
setDepth() { return this; }
setAlpha(a) { this.alpha = a; return this; }
destroy() { this.destroyed = true; return this; }
}
class Container extends GameObject {}
class Sprite extends GameObject {}
globalThis.window = {
Phaser: { GameObjects: { Container, Sprite } }, // js/vendor/phaser.js reads this
};
// --- Load the real config (data/*.json) into the config singleton --------
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
const fs = await import('node:fs');
const dataDir = join(__dirname, '../data');
const configData = {};
for (const f of fs.readdirSync(dataDir)) {
if (!f.endsWith('.json') || f === 'manifest.json') continue;
configData[f.replace(/.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8'));
}
config.init(configData);
let failures = 0;
const check = (label, cond) => {
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
if (!cond) failures++;
};
const { JumpGate } = await import(pathToFileURL(join(__dirname, '../js/entities/JumpGate.js')).href);
// A scene stub: add.existing (v4 quirk), add.graphics (recording no-op),
// add.circle (the field discs — setAlpha is driven by update()).
const alphas = [];
const scene = {
add: {
existing: (o) => o,
graphics: () => ({
lineStyle() { return this; },
strokeCircle() { return this; },
fillStyle() { return this; },
fillCircle() { return this; },
lineBetween() { return this; },
}),
circle: (x, y, r, fill, alpha) => {
const c = { x, y, r, fill, alpha, setAlpha(a) { this.alpha = a; return this; } };
alphas.push(c);
return c;
},
},
};
// A real gate record from the real generator (the home system's first).
const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href);
const g = Galaxy.create('jumpgate-entity-test');
const rec = g.currentSystem();
const content = g.ensureContent(rec.id);
const gateRec = content.jumps[0];
const gate = new JumpGate(scene, gateRec, { depth: 5 });
check('discoveryId / discoveryName come from the record', gate.discoveryId === gateRec.id && gate.discoveryName === gateRec.name);
check('position comes from the record', gate.x === gateRec.x && gate.y === gateRec.y);
check('size comes from data/gates.json', gate.size === config.get('gates.size', 96) && gate.bound === gate.size);
check('clearance comes from gates.shipClearance', gate.clearance === config.get('gates.shipClearance', 50));
check('rotation = the records bearing (facing the destination star)', gate.rotation === gateRec.rotation);
// --- The solid contract (same rules as Planet / Station) ------------------
const shipRadius = (config.get('ship.size', 46) * config.get('ship.scale', 1)) / 2;
const minDist = gate.minCenterDistance(shipRadius);
check('minCenterDistance = radius + clearance + shipRadius', Math.abs(minDist - (gate.radius + gate.clearance + shipRadius)) < 1e-9);
{
const p = gate.edgePoint(1.2, 25, shipRadius);
check('edgePoint sits exactly radius + gap + shipRadius out, on the bearing',
Math.abs(Math.hypot(p.x - gate.x, p.y - gate.y) - (gate.radius + 25 + shipRadius)) < 1e-6 &&
Math.abs(Math.atan2(p.y - gate.y, p.x - gate.x) - 1.2) < 1e-9);
}
{
const inside = { x: gate.x + 10, y: gate.y + 10 }; // inside the keepout
const clamped = gate.aimPoint(inside.x, inside.y, shipRadius);
check('aimPoint clamps an inside target out to the keepout',
Math.abs(Math.hypot(clamped.x - gate.x, clamped.y - gate.y) - minDist) < 1e-6);
const outside = { x: gate.x + minDist * 2, y: gate.y };
const asIs = gate.aimPoint(outside.x, outside.y, shipRadius);
check('aimPoint leaves an outside target alone', asIs.x === outside.x && asIs.y === outside.y);
}
{
const ship = (x, y, vx = 0, vy = 0) => ({ x, y, body: { velocity: { x: vx, y: vy }, acceleration: { x: 0, y: 0 } } });
const s = ship(gate.x + 1, gate.y + 1); // jammed inside the keepout
gate.constrainShip(s, shipRadius);
const d = Math.hypot(s.x - gate.x, s.y - gate.y);
check('constrainShip pushes a ship inside the keepout back out',
Math.abs(d - minDist) < 1e-6);
const s2 = ship(gate.x + minDist * 2, gate.y, 10, 0); // outside: untouched
gate.constrainShip(s2, shipRadius);
check('constrainShip leaves a ship outside the keepout untouched', s2.x === gate.x + minDist * 2 && s2.body.velocity.x === 10);
}
// --- Animation + teardown --------------------------------------------------
gate.update(1000); // must not throw; the field breathes
check('update() breathes the field discs (alpha set)', alphas.length === 2 && alphas.every((c) => typeof c.alpha === 'number' && c.alpha > 0 && c.alpha < 0.5));
const childCount = gate.children.length;
gate.destroy();
check('destroy() clears the children', gate.children.length === 0 && childCount > 0);
if (failures > 0) {
console.error(`\n${failures} jumpgate test(s) FAILED`);
process.exit(1);
}
console.log('\nAll JumpGate entity checks passed ✔');