/** * 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) }; } });