/** * THE USER'S EXACT PATH — click the home planet, then click the * REQUEST LANDING button in the comms panel (real pointer events, no * direct scene calls). This is the path the headless flow test skipped * (it called startLanding() directly) — and it caught the lastTarget * pruning bug where the panel's button payload lost `isPlanet`/`frame`. * * PUMP-DRIVEN (like dev/mining-test.mjs): this box's headless Firefox * freezes rAF/timers after first paint, so the page is advanced by the * runner's wake — each poll calls __LC_TICK__(), which steps the Phaser * loop once and advances the stage machine one step. * * python3 -m http.server 8123 * node dev/cdp-firefox.mjs http://127.0.0.1:8123/dev/landing-click.html \ * 'window.__LC_TICK__(); return window.__LANDED__;' 60000 30 */ 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 data = await ConfigLoader.load(); config.init(data); const gameConfig = createGameConfig(); gameConfig.scene = [GameScene, SurfaceScene]; const game = new Phaser.Game(gameConfig); window.game = game; const out = { pass: false, steps: [], error: null }; const step = (label, cond, diag) => { out.steps.push(label); if (!cond) { out.error = `${label}${diag ? ': ' + JSON.stringify(diag).slice(0, 300) : ''}`; window.__LANDED__ = out; throw new Error(out.error); } }; let stage = 0; let pendingPoke = null; // { x, y } — a canvas pointer click to dispatch this tick const diag = () => { let gs = null, cp = null; try { gs = game.scene?.getScene('GameScene') ?? null; } catch { /* not up yet */ } if (gs?.commsPanel) cp = { open: gs.commsPanel.isOpen, state: gs.commsPanel.state }; window.__LC_STATE__ = { stage, steps: out.steps.slice(), error: out.error, gs: gs ? { status: gs.sys.getStatus(), ship: !!gs.ship, cp } : 'none', ss: (() => { try { const s = game.scene.getScene('SurfaceScene'); return s ? { status: s.sys.getStatus(), phase: s.phase } : null; } catch { return null; } })(), }; }; const canvas = () => game.canvas; const poke = (sx, sy) => { const opts = { bubbles: true, cancelable: true, pointerId: 1, pointerType: 'mouse', isPrimary: true, clientX: sx, clientY: sy, button: 0, buttons: 1, }; canvas().dispatchEvent(new PointerEvent('pointerover', opts)); canvas().dispatchEvent(new PointerEvent('pointerdown', opts)); canvas().dispatchEvent(new PointerEvent('pointerup', { ...opts, buttons: 0 })); }; const scene = (key) => { try { return game.scene?.getScene(key) ?? null; } catch { return null; } }; function tick() { diag(); // v4: do NOT step the loop before boot completes — `game.scene` (the // scene manager) is created during boot, and pumping loop.step() before // that wedges the boot (the scene never comes up). The mining test's // pump guards the same way (it only steps once g.loop is live). if (!game.loop || !game.scene) return; try { // (a) dispatch this tick's click, (b) one engine frame, (c) advance. if (pendingPoke) { poke(pendingPoke.x, pendingPoke.y); pendingPoke = null; } game.loop.step(performance.now()); const gs = scene('GameScene'); const ss = scene('SurfaceScene'); switch (stage) { case 0: // boot: the flight scene is live if (!(gs && gs.ship)) return; step('boot', true, { frame: gs.planet.sheetFrame }); { // Click a VISIBLE point of the planet disc: the ship spawns on // the rim (camera centered on the ship), so the disc rim facing // the ship is the on-screen part — click just inside that rim. const cam = gs.cameras.main; const dx = gs.ship.x - gs.planet.x, dy = gs.ship.y - gs.planet.y; const d = Math.hypot(dx, dy) || 1; const wx = gs.planet.x + (dx / d) * gs.planet.radius * 0.98; const wy = gs.planet.y + (dy / d) * gs.planet.radius * 0.98; const px = wx - cam.scrollX, py = wy - cam.scrollY; step('planet-on-screen', px > 8 && px < 1272 && py > 8 && py < 712, { px, py, planetRadius: gs.planet.radius }); pendingPoke = { x: px, y: py }; } stage = 1; return; case 1: // the planet click opened the comms panel if (!(gs.commsPanel && gs.commsPanel.isOpen)) return; step('comms-opened', true); step('button-enabled', gs.commsPanel.btn1.disabled === false, { label: gs.commsPanel.btn1.text?.text, disabled: gs.commsPanel.btn1.disabled }); { const t = gs.commsPanel.lastTarget; step('lastTarget-isPlanet', t?.isPlanet === true, { lastTarget: t }); step('lastTarget-frame', t?.frame === gs.planet.sheetFrame, { frame: t?.frame, sheetFrame: gs.planet.sheetFrame }); // Panel is origin (0, 0.5): local (0, H/2) sits at (cp.x, cp.y). const cam = gs.cameras.main; const cp = gs.commsPanel; const bx = cp.x + cp.btn1.x - cam.scrollX; const by = cp.y + cp.btn1.y - cp.H / 2 - cam.scrollY; step('button-on-screen', bx > 8 && bx < 1272 && by > 8 && by < 712, { bx, by }); pendingPoke = { x: bx, y: by }; } stage = 2; return; case 2: // the button press landed the ship if (!(ss && ss.sys.isActive())) return; step('panel-closed', gs.commsPanel.isOpen === false, { state: gs.commsPanel.state }); step('surface-running', true, { keys: game.scene.getScenes().map((s) => `${s.sys.id}:${s.sys.getStatus()}`) }); step('game-sleeping', gs.sys.isSleeping() === true, { gsStatus: gs.sys.getStatus() }); step('phase-landing', ss.phase === 'landing', { phase: ss.phase, landVideo: !!ss.landVideo }); step('handoff-frame', ss.planetFrame === gs.planet.sheetFrame, { ss: ss.planetFrame, planet: gs.planet.sheetFrame }); out.pass = true; window.__LANDED__ = out; stage = 3; return; } } catch (err) { out.error = String(err?.message ?? err); window.__LANDED__ = out; stage = 99; // stop } } window.__LC_TICK__ = tick;