/** * DOUBLE-CLICK SKIP (headless browser — NOT a Node test). * * Drives the REAL input pipeline (dispatched mouse events on the canvas * → v4 pointer → the scene's pointerdown handler) and checks: * * 1. a SINGLE click during the landing clip does NOT skip it * (negative control — one press is not a double-click) * 2. a DOUBLE-click (two quick presses) during the landing clip skips * the rest of the clip — the deck comes up now, landing video gone * 3. a DOUBLE-click during the takeoff clip leaves immediately — * SurfaceScene stopped, GameScene woken, the ship where it was * * PUMP-DRIVEN (like dev/landing-click.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 __SS_TICK__(), which steps the engine * once and advances the stage machine one step. Real time between polls * (the poll period) is the clock the double-click window is measured * against, so the barrier stages gate on performance.now() (wall clock). * * python3 -m http.server 8123 * node dev/cdp-firefox.mjs http://127.0.0.1:8123/dev/surface-skip.html \ * 'window.__SS_TICK__(); return window.__SURFACE_SKIP__;' 240000 500 */ 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.__SURFACE_SKIP__ = out; throw new Error(out.error); } }; const diag = () => { let gs = null, ss = null; try { gs = game.scene?.getScene('GameScene') ?? null; } catch { /* not up yet */ } try { ss = game.scene?.getScene('SurfaceScene') ?? null; } catch { /* not up yet */ } window.__SS_STATE__ = { stage, steps: out.steps.slice(), error: out.error, gs: gs ? { status: gs.sys.getStatus(), ship: !!gs.ship } : 'none', ss: ss ? { status: ss.sys.getStatus(), phase: ss.phase, landVideo: !!ss.landVideo, surfaceVideo: !!ss.surfaceVideo, takeoffVideo: !!ss.takeoffVideo, deck: !!ss.actionBar, lastClickT: ss.lastClickT ?? null, } : 'none', }; }; let stage = 0; let negT = 0; // wall time of the negative-control click (barrier gate) let takeoffT = 0; // wall time the takeoff clip went up (barrier gate) const shipBefore = { x: null, y: null }; const canvas = () => game.canvas; // A real mouse press at CANVAS screen coords (clickout-test pattern: // v4 binds mousedown/mouseup on the canvas; FIT mode scales, so map // canvas → client space with the rendered rect). `times` = the presses. const press = (x, y, times = 1) => { const canvasEl = canvas(); const r = canvasEl.getBoundingClientRect(); const zoom = r.width / canvasEl.width; const mk = (type, buttons) => new MouseEvent(type, { bubbles: true, cancelable: true, view: window, button: 0, buttons, clientX: r.left + x * zoom, clientY: r.top + y * zoom, }); canvasEl.dispatchEvent(mk('mouseover', 0)); for (let i = 0; i < times; i++) { canvasEl.dispatchEvent(mk('mousedown', 1)); canvasEl.dispatchEvent(mk('mouseup', 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 (landing-click rule). if (!game.loop || !game.scene) return; try { 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 }); shipBefore.x = gs.ship.x; shipBefore.y = gs.ship.y; gs.startLanding(gs.commsTargetFor(gs.planet)); stage = 1; return; } case 1: { // the landing stage is up with its clip in flight if (!(ss && ss.sys.isActive() && ss.phase === 'landing')) return; step('landing-stage', true, { status: ss.sys.getStatus(), phase: ss.phase }); step('landing-clip', ss.landVideo !== null, { frame: ss.planetFrame, landKey: ss.landKey }); stage = 2; return; } case 2: { // NEGATIVE: one press is not a double-click — no skip press(640, 360, 1); game.loop.step(performance.now()); // flush the queued pointerdown step('single-click-no-skip', ss.phase === 'landing' && ss.landVideo !== null, { phase: ss.phase, landVideo: !!ss.landVideo }); negT = performance.now(); stage = 3; return; } case 3: { // barrier: out of the double-click window before the real one if (performance.now() - negT < 600) return; stage = 4; return; } case 4: { // DOUBLE-CLICK the landing clip — the rest of it is skipped press(640, 360, 2); game.loop.step(performance.now()); step('double-click-skips-landing', ss.phase === 'surface' && ss.landVideo === null, { phase: ss.phase, landVideo: !!ss.landVideo }); step('deck-came-up', ss.actionBar !== null && ss.sys.isActive() === true, { deck: !!ss.actionBar, status: ss.sys.getStatus() }); // the skipped landing must not have stranded the surface stage: const loopKey = `__surf_loop_${ss.planetFrame}`; step('surface-stage', ss.phase === 'surface' && (ss.cache.video.has(loopKey) ? ss.surfaceVideo !== null : true), { loopKey, surfaceVideo: !!ss.surfaceVideo }); stage = 5; return; } case 5: { // take off — the takeoff clip goes up (or leaves instantly) ss.takeOff(); game.loop.step(performance.now()); if (ss.sys.isActive()) { step('takeoff-clip', ss.takeoffVideo !== null, { frame: ss.planetFrame }); takeoffT = performance.now(); stage = 6; } else { step('no-takeoff-clip-left-instantly', gs.sys.isActive() === true, { gsStatus: gs.sys.getStatus() }); stage = 8; } return; } case 6: { // barrier: out of the double-click window before the real one if (performance.now() - takeoffT < 600) return; stage = 7; return; } case 7: { // DOUBLE-CLICK the takeoff clip — the flight world wakes now press(640, 360, 2); game.loop.step(performance.now()); step('double-click-skips-takeoff', ss.sys.isActive() === false && gs.sys.isActive() === true, { ssStatus: ss.sys.getStatus(), gsStatus: gs.sys.getStatus() }); // the run resumed exactly where the ship left it — shared check /* falls through to case 8 */ } case 8: { // the run resumed exactly where the ship left it step('ship-preserved', Math.abs(gs.ship.x - shipBefore.x) < 1e-6 && Math.abs(gs.ship.y - shipBefore.y) < 1e-6, { before: shipBefore, after: { x: gs.ship.x, y: gs.ship.y } }); out.pass = true; window.__SURFACE_SKIP__ = out; stage = 99; return; } } } catch (err) { out.error = String(err?.message ?? err); window.__SURFACE_SKIP__ = out; stage = 99; // stop } } window.__SS_TICK__ = tick; // Hard timeout: a hung flow must not hang the driver forever. const T0 = Date.now(); const arm = setTimeout(() => { if (!out.pass) { out.error = out.error ?? `TIMED OUT after ${Math.round((Date.now() - T0) / 1000)}s`; window.__SURFACE_SKIP__ = out; } }, 300000); arm.unref?.();