/** * Dev-only SURFACE HUD test: boots into the GameScene, lands on the home * world, and checks the new upper-left HUD (SurfaceScene.buildHud): * * - the launch payload carries the world's type + tether level * - the name text: Ethnocentric (header face), the menu title's shade * (data/menu.json → colors.title), a black stroke behind it * - the line below: "TYPE, TETHER LEVEL N" in the body face (Centauri) * - both lines decode in (the shared scramble) and settle on the * final strings * - destroyHud() tears it down (take-off/shutdown path) * * python3 -m http.server 8124 * node dev/cdp-firefox.mjs http://127.0.0.1:8124/dev/surface-hud.html \ * 'return window.__SURFACE_HUD_TEST__;' */ 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 { SurfaceScene } from '../js/scenes/SurfaceScene.js'; const fail = (msg, extra = {}) => { window.__SURFACE_HUD_TEST__ = { pass: false, error: msg, diag: extra }; console.error('[surface-hud] FAIL:', msg, extra); }; window.__FLOW_STARTED__ = true; const step = (name) => { window.__STEPS__ = (window.__STEPS__ ?? []).concat(name); }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const data = await ConfigLoader.load(); config.init(data); const gameConfig = createGameConfig(); gameConfig.scene = [GameScene, SurfaceScene]; const game = new Phaser.Game(gameConfig); window.game = game; (async () => { // 1 — the flight scene is live (v4 boots asynchronously). let gs = null; for (let i = 0; i < 300; i++) { gs = game.scene?.getScene('GameScene') ?? null; if (gs && gs.ship) break; await sleep(100); } if (!gs || !gs.ship) throw new Error('GameScene never came up'); step('boot'); // 2 — the tether lookup the launch payload relies on. const target = gs.commsTargetFor(gs.planet); const level = gs.tetherLevelFor(target.name); if (!(level === gs.tetherField.tethers.find((t) => t.label === target.name)?.level)) throw new Error('tetherLevelFor mismatch', { level, tethers: gs.tetherField.tethers }); if (gs.tetherLevelFor('NOT A PLANET') !== 0) throw new Error('unknown world must be 0'); step('tether-level-lookup'); // 3 — the real entry point: the launch payload carries type + level. gs.startLanding(target); await sleep(250); const ss = game.scene.getScene('SurfaceScene'); if (!ss || !ss.sys.isActive()) throw new Error('SurfaceScene did not launch'); if (ss.planetType !== target.kindLabel) throw new Error('planetType', { got: ss.planetType, want: target.kindLabel }); if (ss.tetherLevel !== level) throw new Error('tetherLevel', { got: ss.tetherLevel, want: level }); step('launch-payload', { name: ss.planetName, type: ss.planetType, level: ss.tetherLevel }); // 4 — the HUD exists: auto-built with the loop clip (startSurface), or // built directly when headless never cached the video. ss.startSurface(); await sleep(50); if (!ss.hudName) ss.buildHud(); if (!ss.hudName || !ss.hudLine || !ss.hudLines) throw new Error('HUD missing after buildHud'); step('hud-built'); // 5 — the decode settles on the final strings (poll a few seconds). const nameFinal = String(ss.planetName).toUpperCase(); const lineFinal = `${ss.planetType}, Tether Level ${ss.tetherLevel}`; let settled = false; for (let i = 0; i < 200 && !settled; i++) { await sleep(100); if (ss.hudLines === null && ss.hudName.text === nameFinal && ss.hudLine.text === lineFinal) settled = true; } if (!settled) throw new Error('decode did not settle', { name: ss.hudName?.text, line: ss.hudLine?.text, hudLines: !!ss.hudLines, }); step('decode-settled', { name: ss.hudName.text, line: ss.hudLine.text }); // 6 — the styling contract. const nStyle = ss.hudName.style; const lStyle = ss.hudLine.style; if (!/Ethnocentric/.test(nStyle.fontFamily)) throw new Error('name font', { got: nStyle.fontFamily }); if (!/Centauri/.test(lStyle.fontFamily)) throw new Error('line font', { got: lStyle.fontFamily }); const titleCss = '#' + (config.get('menu.colors.title', '#eaf6ff').slice(1) || 'eaf6ff'); if (nStyle.color.toLowerCase() !== titleCss) throw new Error('name shade', { got: nStyle.color, want: titleCss }); if (nStyle.stroke.toLowerCase() !== '#000000' || nStyle.strokeThickness !== 4) throw new Error('name stroke', { got: [nStyle.stroke, nStyle.strokeThickness] }); if (lStyle.stroke.toLowerCase() !== '#000000') throw new Error('line stroke', { got: lStyle.stroke }); if (!(ss.hudName.x >= 16 && ss.hudName.y >= 12)) throw new Error('corner placement', { x: ss.hudName.x, y: ss.hudName.y }); if (!(ss.hudLine.y > ss.hudName.y)) throw new Error('line sits below the name'); step('styling'); // 7 — teardown (the take-off/shutdown path). ss.destroyHud(); if (ss.hudName !== null || ss.hudLine !== null || ss.hudLines !== null) throw new Error('destroyHud left state'); step('teardown'); window.__SURFACE_HUD_TEST__ = { pass: true, steps: window.__STEPS__ }; console.info('[surface-hud] PASS:', window.__STEPS__.join(' → ')); })().catch((err) => fail(String(err && err.message || err), { gs: game.scene?.getScene('GameScene')?.sys.getStatus?.(), ss: game.scene?.getScene('SurfaceScene') ? { phase: game.scene.getScene('SurfaceScene').phase } : null, }));