221 lines
10 KiB
JavaScript
221 lines
10 KiB
JavaScript
/**
|
|
* Dev-only smoke test: boots the game directly into the GameScene
|
|
* (skipping the menu), so the flight scene can be screenshotted/checked
|
|
* without clicking "New Game".
|
|
*
|
|
* python3 -m http.server 8080
|
|
* firefox --headless --screenshot shot.png \
|
|
* --window-size=1280,720 http://localhost:8080/dev/test-game.html
|
|
*
|
|
* Dev hooks (URL params, dev only — ignored by the game itself):
|
|
* ?ship=<x>,<y> teleport the ship to a world position after boot
|
|
* (screenshots of far-off states — e.g. the off-screen
|
|
* compass arrows pointing back at the home world).
|
|
* ?near=<px> fly the ship to <px> (edge-to-edge) from the first
|
|
* system planet — screenshots the discovery moment
|
|
* (rim ping + toast) with the world in view.
|
|
* ?thrusttest=1 run the Shift+click THROTTLE LOCK flow end-to-end in
|
|
* the live scene (synthetic pointerdowns with
|
|
* pointer.event.shiftKey set) and paint the verdict
|
|
* on the canvas (window.__thrustResult for CDP).
|
|
* ?tethers=x,y,level;x,y,level add dev tethers (union-boundary tests)
|
|
* ?homeLevel=<n> set the home tether's level (bigger/smaller range)
|
|
* ?seed=<seed> use a DETERMINISTIC dev galaxy (same seed ⇒ same
|
|
* system, planets, asteroid clusters) — reproducible shots
|
|
* ?report=1 paint console errors (or "SMOKE OK") on the canvas
|
|
*/
|
|
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';
|
|
|
|
const data = await ConfigLoader.load();
|
|
config.init(data);
|
|
const gameConfig = createGameConfig();
|
|
const params = typeof location !== 'undefined' ? new URLSearchParams(location.search) : null;
|
|
const shipParam = params ? params.get('ship') : null;
|
|
const nearParam = params ? params.get('near') : null;
|
|
const tethersParam = params ? params.get('tethers') : null;
|
|
const homeLevelParam = params ? params.get('homeLevel') : null;
|
|
const seedParam = params ? params.get('seed') : null;
|
|
|
|
// Deterministic dev galaxy for reproducible screenshots (GameScene.
|
|
// ensureGalaxy reads this when the registry is empty).
|
|
if (seedParam && typeof globalThis !== 'undefined') globalThis.__ORBIT_DEV_SEED = seedParam;
|
|
|
|
// Dev: capture console errors + uncaught exceptions for ?report=1.
|
|
const __errors = [];
|
|
if (typeof window !== 'undefined') {
|
|
const origErr = console.error.bind(console);
|
|
console.error = (...a) => { __errors.push(a.map(String).join(' ')); origErr(...a); };
|
|
window.addEventListener('error', (e) => __errors.push(String(e.message)));
|
|
}
|
|
gameConfig.scene = [GameScene];
|
|
const game = new Phaser.Game(gameConfig);
|
|
window.game = game;
|
|
console.info('smoke: game booted into GameScene');
|
|
|
|
if (shipParam) {
|
|
const [sx, sy] = shipParam.split(',').map(Number);
|
|
if (Number.isFinite(sx) && Number.isFinite(sy)) {
|
|
setTimeout(() => {
|
|
const s = game.scene.getScene('GameScene');
|
|
s.ship.setPosition(sx, sy);
|
|
s.cameras.main.setScroll(sx - s.scale.width / 2, sy - s.scale.height / 2);
|
|
console.info(`smoke: ship teleported to (${sx}, ${sy})`);
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
if (tethersParam || homeLevelParam) {
|
|
setTimeout(() => {
|
|
const s = game.scene.getScene('GameScene');
|
|
if (homeLevelParam) {
|
|
const home = s.tetherField.tethers[0];
|
|
if (home) s.tetherField.setLevel(home.id, Number(homeLevelParam) || 1);
|
|
}
|
|
if (tethersParam) {
|
|
tethersParam.split(';').forEach((triple, i) => {
|
|
const [tx, ty, tl] = triple.split(',').map(Number);
|
|
if (Number.isFinite(tx) && Number.isFinite(ty)) {
|
|
s.tetherField.add('dev' + i, tx, ty, Number.isFinite(tl) ? tl : 1, 'dev');
|
|
}
|
|
});
|
|
}
|
|
console.info(`smoke: dev tethers ready (homeLevel=${homeLevelParam ?? '—'}, extra=${tethersParam ?? '—'})`);
|
|
}, 600);
|
|
}
|
|
|
|
if (params && params.get('report') === '1') {
|
|
// After boot, paint any captured console errors onto the canvas so a
|
|
// headless screenshot shows whether the scene is healthy.
|
|
setTimeout(() => {
|
|
const s = game.scene.getScene('GameScene');
|
|
if (!s) return;
|
|
const msg = __errors.length === 0
|
|
? 'SMOKE OK — no console errors'
|
|
: __errors.slice(0, 3).join('\n');
|
|
s.add.text(16, 690, msg, {
|
|
fontFamily: 'monospace',
|
|
fontSize: '12px',
|
|
color: __errors.length === 0 ? '#7ce8a4' : '#ff2d6f',
|
|
align: 'left',
|
|
}).setOrigin(0, 1).setScrollFactor(0).setDepth(9000);
|
|
}, 3000);
|
|
}
|
|
|
|
if (params && params.get('thrusttest') === '1') {
|
|
// The Shift+click THROTTLE LOCK, end-to-end in the LIVE scene: fire
|
|
// synthetic pointerdown events through the scene's own input handler
|
|
// (pointer.event carries the shiftKey, as a real press would), then
|
|
// check the ship (a) keeps flying the committed heading PAST the
|
|
// click point, (b) a plain click steers it to a stop there, and
|
|
// (c) thrusting INTO an obstacle stops it dead on the rim.
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const until = async (fn, ms = 25000) => {
|
|
const t0 = Date.now();
|
|
for (;;) {
|
|
const v = fn();
|
|
if (v) return v;
|
|
if (Date.now() - t0 > ms) return null;
|
|
await sleep(200);
|
|
}
|
|
};
|
|
const fire = (s, wx, wy, shift) => {
|
|
const pointer = s.input.activePointer;
|
|
pointer.event = { shiftKey: !!shift }; // the DOM press (with modifiers)
|
|
pointer.x = 100; pointer.y = 100; // screen space (clear of deck/panels)
|
|
pointer.worldX = wx; pointer.worldY = wy;
|
|
pointer.downElement = s.game.canvas;
|
|
s.input.emit('pointerdown', pointer, []);
|
|
};
|
|
const stopAim = (s, wx, wy) => {
|
|
let aim = { x: wx, y: wy };
|
|
for (const sol of s.solids) aim = sol.aimPoint(aim.x, aim.y, s.ship.radius);
|
|
return s.tetherField.clampPoint(aim.x, aim.y);
|
|
};
|
|
setTimeout(async () => {
|
|
const s = game.scene.getScene('GameScene');
|
|
const report = (ok, details) => {
|
|
window.__thrustResult = { ok, details };
|
|
if (!s) return;
|
|
const text = `THRUST TEST ${ok ? 'PASS \u2714' : 'FAIL \u2718'}\n${details.join('\n')}`;
|
|
s.add.text(16, 690, text, {
|
|
fontFamily: 'monospace',
|
|
fontSize: '14px',
|
|
color: ok ? '#7ce8a4' : '#ff2d6f',
|
|
align: 'left',
|
|
}).setOrigin(0, 1).setScrollFactor(0).setDepth(9000);
|
|
};
|
|
try {
|
|
const d = [];
|
|
// Wait for the scene to finish create() (asset preload first —
|
|
// the videos in particular can be slow in headless).
|
|
const ready = await until(() => (s && s.ship && s.input && s.input.activePointer && s.tetherField ? s : null));
|
|
if (!ready) { report(false, ['scene never became ready (ship/input/tether missing)']); return; }
|
|
// Park the ship at a known spot, clear of the central body.
|
|
const px = s.planet.radius + 320;
|
|
s.ship.setPosition(px, 0);
|
|
s.ship.stop();
|
|
s.cameras.main.setScroll(px - s.scale.width / 2, -s.scale.height / 2);
|
|
await sleep(400);
|
|
const sx = s.ship.x;
|
|
// 1) Shift+click 500 px ahead — the ship must KEEP FLYING that
|
|
// way, past the click point, at full throttle.
|
|
const thrustPoint = { x: sx + 500, y: 0 };
|
|
if (s.rockAt(thrustPoint.x, thrustPoint.y)) { report(false, ['SKIP: thrust click point is on a rock']); return; }
|
|
if (s.worldObjectAt(thrustPoint.x, thrustPoint.y)) { report(false, ['SKIP: thrust click point is on a world']); return; }
|
|
fire(s, thrustPoint.x, thrustPoint.y, true);
|
|
await sleep(2500);
|
|
const past = s.ship.x - thrustPoint.x;
|
|
d.push(`thrust: state=${s.ship.state} x=${s.ship.x.toFixed(1)} (past click by ${past.toFixed(1)} px, speed=${s.ship.body.velocity.length().toFixed(0)})`);
|
|
const thrustOk = s.ship.state === 'thrust' && past > 80 && s.ship.y < 2 && s.ship.y > -2;
|
|
// 2) Plain click somewhere else — the ship must fly THERE and
|
|
// stop (the hold ends exactly like a normal re-target).
|
|
const stopPoint = { x: s.ship.x - 300, y: s.ship.y + 380 };
|
|
const aim = stopAim(s, stopPoint.x, stopPoint.y);
|
|
fire(s, stopPoint.x, stopPoint.y, false);
|
|
await sleep(5000);
|
|
const dist = Math.hypot(s.ship.x - aim.x, s.ship.y - aim.y);
|
|
d.push(`steer-back: state=${s.ship.state} dist=${dist.toFixed(1)} px speed=${s.ship.body.velocity.length().toFixed(1)}`);
|
|
const steerOk = s.ship.state === 'normal' && dist < 20 && s.ship.body.velocity.length() < 5;
|
|
// 3) Shift+click INTO the planet — the ship must run in and STOP
|
|
// dead on its rim (contact ends the hold; the world wins).
|
|
const rim = s.planet.minCenterDistance(s.ship.radius);
|
|
s.ship.setPosition(rim + 400, 0);
|
|
s.ship.stop();
|
|
await sleep(300);
|
|
fire(s, s.ship.x - 200, 0, true); // straight at the planet
|
|
await sleep(3000);
|
|
const rimGap = Math.hypot(s.ship.x - s.planet.x, s.ship.y - s.planet.y) - rim;
|
|
d.push(`hit-stop: state=${s.ship.state} rim-gap=${rimGap.toFixed(1)} px speed=${s.ship.body.velocity.length().toFixed(1)}`);
|
|
const hitOk = s.ship.state === 'normal' && Math.abs(rimGap) < 4 && s.ship.body.velocity.length() < 1;
|
|
report(thrustOk && steerOk && hitOk, d);
|
|
} catch (err) {
|
|
report(false, ['EXCEPTION: ' + String(err?.stack ?? err)]);
|
|
}
|
|
}, 300);
|
|
// Watchdog: a verdict MUST land — never let the poller time out silent.
|
|
setTimeout(() => {
|
|
if (!window.__thrustResult) window.__thrustResult = { ok: false, details: ['watchdog: no verdict within 40 s'] };
|
|
}, 40000);
|
|
}
|
|
|
|
if (nearParam) {
|
|
const px = Number(nearParam) || 400;
|
|
setTimeout(() => {
|
|
const s = game.scene.getScene('GameScene');
|
|
const p = s.systemPlanets[0];
|
|
if (!p) return;
|
|
// Place the ship on the planet→origin axis, `px` (edge-to-edge) off
|
|
// the rim — inside the discovery distance, with the world in view.
|
|
const ang = Math.atan2(-p.y, -p.x);
|
|
const sx = p.x + Math.cos(ang) * (p.radius + px);
|
|
const sy = p.y + Math.sin(ang) * (p.radius + px);
|
|
s.ship.setPosition(sx, sy);
|
|
s.cameras.main.setScroll(sx - s.scale.width / 2, sy - s.scale.height / 2);
|
|
console.info(`smoke: ship parked ${px}px off ${p.discoveryName}'s rim`);
|
|
}, 500);
|
|
}
|