217 lines
7.9 KiB
JavaScript
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?.();
|