199 lines
8.5 KiB
JavaScript
199 lines
8.5 KiB
JavaScript
/**
|
|
* Compass FAR-display integration driver (headless browser — NOT a Node
|
|
* test). Boots the real GameScene and checks the near/far split of the
|
|
* off-screen compass (js/ui/DiscoveryCompass.js):
|
|
*
|
|
* - the ship is parked 3000 px from the home world (home is OFF-SCREEN
|
|
* but NEAR — full display), opposite a system planet (which is then
|
|
* > 5120 px away — FAR — compact display);
|
|
* - every discovered object gets a compass entry; FAR entries are the
|
|
* small text-less box + shrunken arrow, NEAR entries the full readout;
|
|
* - hovering a FAR chip expands it back to the full readout (type +
|
|
* name, arrow back to full) AFTER the pointer has rested
|
|
* (expandDelay — a flick over doesn't pop it), and hovering off
|
|
* folds it back in only after the grace (collapseDelay) runs out;
|
|
* - the scene's click guard (compass.contains) covers the small chip's
|
|
* generous hit slack but not a click well off it.
|
|
*
|
|
* Served by dev/compass-far.html; the results land in
|
|
* `window.__COMPASS_FAR__` for the CDP runner (dev/cdp-firefox.mjs):
|
|
*
|
|
* python3 -m http.server 8080
|
|
* node dev/cdp-firefox.mjs http://localhost:8080/dev/compass-far.html \
|
|
* 'return window.__COMPASS_FAR__;'
|
|
*/
|
|
import Phaser from '../js/vendor/phaser.js';
|
|
import { config } from '../js/config/Config.js';
|
|
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
|
import { createGameConfig } from '../js/config/GameConfig.js';
|
|
import { GameScene } from '../js/scenes/GameScene.js';
|
|
import { toColor } from '../js/utils/Color.js';
|
|
import { themeColor } from '../js/utils/Theme.js';
|
|
|
|
const data = await ConfigLoader.load();
|
|
config.init(data);
|
|
|
|
// A deterministic galaxy (same system every run) + a quiet run (no audio
|
|
// files to fetch in headless).
|
|
globalThis.__ORBIT_DEV_SEED = 'COMPASSFAR';
|
|
const gameConfig = createGameConfig();
|
|
gameConfig.scene = [GameScene]; // boots straight into the flight scene
|
|
if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true;
|
|
|
|
const game = new Phaser.Game(gameConfig);
|
|
window.game = game;
|
|
|
|
const results = [];
|
|
const check = (label, cond) => {
|
|
const pass = !!cond;
|
|
results.push({ label, pass });
|
|
console.log(`${pass ? '✔' : '✘ FAIL'} ${label}`);
|
|
};
|
|
|
|
// WAIT — throttling-proof (this headless box starves setTimeout). Poll the
|
|
// game's own clock (shared engine loop time) on rAF until it advances.
|
|
const gameClock = () => {
|
|
try {
|
|
const s = window.game.scene.getScenes(true)[0];
|
|
if (s && typeof s.time.now === 'number') return s.time.now;
|
|
} catch {}
|
|
return null;
|
|
};
|
|
const wait = (ms) => new Promise((resolve) => {
|
|
const base = gameClock();
|
|
if (base === null) {
|
|
const start = performance.now();
|
|
setTimeout(() => resolve(), ms);
|
|
return;
|
|
}
|
|
const poll = () => {
|
|
const now = gameClock();
|
|
if (now !== null && now - base >= ms) return resolve();
|
|
requestAnimationFrame(poll);
|
|
};
|
|
requestAnimationFrame(poll);
|
|
});
|
|
|
|
const run = async () => {
|
|
const scene = game.scene.getScene('GameScene');
|
|
const bootT0 = Date.now();
|
|
while (!game.scene.isActive('GameScene') || !scene.ship || !scene.compass) {
|
|
if (Date.now() - bootT0 > 90000) throw new Error('GameScene never came up');
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
}
|
|
await wait(300); // let the first frames settle
|
|
|
|
const cfg = config.get('game.discovery.compass', {});
|
|
check('far display configured (5120 / 0.6 / 26)',
|
|
cfg.farDistance === 5120 && cfg.farArrowScale === 0.6 && cfg.smallBox === 26);
|
|
|
|
// --- Park the ship: NEAR the home world, FAR from a system planet ----
|
|
// Home world at the origin: 3000 px away is off-screen but within
|
|
// farDistance (full display). The system planets sit ≥ 6400 px from the
|
|
// origin; the ship parks on the ray OPPOSITE one of them, so that
|
|
// planet is ≥ 6400 + 3000 px away (far beyond farDistance — compact).
|
|
const planet = scene.systemPlanets[0];
|
|
check('the test system has a planet to be far away', !!planet);
|
|
if (!planet) return finish();
|
|
const ang = Math.atan2(planet.y, planet.x);
|
|
const shipX = -Math.cos(ang) * 3000;
|
|
const shipY = -Math.sin(ang) * 3000;
|
|
scene.ship.setPosition(shipX, shipY);
|
|
scene.ship.setTarget(shipX, shipY); // no drift
|
|
const dHome = Math.hypot(shipX, shipY);
|
|
const dPlanet = Math.hypot(planet.x - shipX, planet.y - shipY);
|
|
check('ship split: home near (< 5120), planet far (> 5120)',
|
|
dHome < cfg.farDistance && dPlanet > cfg.farDistance);
|
|
|
|
// --- Discover everything (the compass shows DISCOVERED objects) ------
|
|
const sysId = scene.systemRecord.id;
|
|
const objs = scene.discoverableObjects();
|
|
const colorOf = {};
|
|
for (const o of objs) colorOf[o.id] = o.color;
|
|
const idOf = (e) => [...scene.compass.entries.entries()].find(([, en]) => en === e)?.[0];
|
|
let known = scene.discovery.bySystem.get(sysId) ?? new Set();
|
|
for (const o of objs) known.add(o.id);
|
|
scene.discovery.bySystem.set(sysId, known);
|
|
|
|
// Give the compass a couple of frames to reconcile its entries.
|
|
await wait(250);
|
|
check('compass built one entry per discovered object',
|
|
scene.compass.entries.size === objs.length);
|
|
|
|
const farE = [...scene.compass.entries.values()].filter((e) => e.far);
|
|
const nearE = [...scene.compass.entries.values()].filter((e) => !e.far);
|
|
check('at least one FAR entry and one NEAR entry', farE.length > 0 && nearE.length > 0);
|
|
check('FAR entries are the compact display (small box, no text, small arrow)',
|
|
farE.length > 0 && farE.every((e) => e.mode === 'small'
|
|
&& e.w === cfg.smallBox && e.h === cfg.smallBox
|
|
&& e.typeText.alpha < 0.1
|
|
&& (!e.nameText || e.nameText.alpha < 0.1)
|
|
&& e.arrow.scale < 0.9));
|
|
check('NEAR entries keep the full readout (text, full arrow)',
|
|
nearE.length > 0 && nearE.every((e) => e.mode === 'full'
|
|
&& e.typeText.alpha > 0.9
|
|
&& e.arrow.scale > 0.99));
|
|
|
|
// --- Hover a FAR chip: expand → read → (press) → fold back ----------
|
|
const e = farE[0];
|
|
scene.compass.onChipOver(e); // the chip's pointerover handler
|
|
check('hover IN: the small box brightens, but the expand WAITS (intent)',
|
|
e.mode === 'small' && e.expandAt != null);
|
|
// expandDelay + the 130 ms text fade + headroom, on the game clock.
|
|
await wait(900);
|
|
check('hover IN (after the rest): the far chip expands to the full readout',
|
|
e.mode === 'full' && e.w === e.baseW && e.h === e.baseH
|
|
&& e.typeText.alpha > 0.9
|
|
&& e.arrow.scale > 0.99);
|
|
check('hover IN: the chip keeps the target\u2019s accent color',
|
|
e.neon === (colorOf[idOf(e)] ? toColor(colorOf[idOf(e)]) : themeColor('neon', 0x00e5ff)),
|
|
);
|
|
// The press path still works from the expanded state (autopilot seam):
|
|
// onSelect is the scene's autopilotTo — the chip's own pointerdown fires
|
|
// pressChip, which calls onSelect; check the seam is wired, not the flight.
|
|
check('press seam intact (onSelect bound to the scene\u2019s autopilot)',
|
|
typeof scene.compass.onSelect === 'function');
|
|
scene.compass.onChipOut(e); // the chip's pointerout handler
|
|
check('hover OUT: a stray exit doesn\u2019t yank it down (grace armed)',
|
|
e.mode === 'full' && e.collapseAt != null);
|
|
// Grace (2000 ms) + the fold fade + headroom.
|
|
await wait(2600);
|
|
check('hover OUT (grace runs out): folds back to the compact display (still far)',
|
|
e.mode === 'small' && e.w === cfg.smallBox && e.typeText.alpha < 0.1);
|
|
|
|
// --- Hover back within the grace: the chip STAYS up ------------------
|
|
scene.compass.onChipOver(e);
|
|
await wait(900);
|
|
check('hover again: it expands once more (the seam is repeatable)',
|
|
e.mode === 'full' && e.typeText.alpha > 0.9);
|
|
scene.compass.onChipOut(e);
|
|
check('... and hovering back in during the grace cancels the fold',
|
|
(() => { scene.compass.onChipOver(e); return e.collapseAt === null && e.mode === 'full'; })());
|
|
// Leave it for good: folded back to the small box before the guard tests.
|
|
scene.compass.onChipOut(e);
|
|
await wait(2600);
|
|
check('final leave: back to the compact display', e.mode === 'small' && e.w === cfg.smallBox);
|
|
|
|
// --- The scene's click guard around the small box --------------------
|
|
const px = e.chipRoot.x, py = e.chipRoot.y;
|
|
check('contains: the small chip\u2019s generous slack is a chip click',
|
|
scene.compass.contains(px, py) === true);
|
|
check('contains: a click well off the small chip is a fly-here',
|
|
scene.compass.contains(px + 40, py) === false);
|
|
|
|
finish();
|
|
};
|
|
|
|
function finish() {
|
|
const pass = results.every((r) => r.pass) && results.length > 0;
|
|
window.__COMPASS_FAR__ = { pass, results };
|
|
}
|
|
|
|
game.events.once('ready', async () => {
|
|
try {
|
|
await run();
|
|
} catch (err) {
|
|
window.__COMPASS_FAR__ = { pass: false, error: String(err && err.stack || err) };
|
|
}
|
|
});
|