orbit/dev/surface-skip.mjs

217 lines
7.9 KiB
JavaScript

/**
* 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?.();