621 lines
24 KiB
JavaScript
621 lines
24 KiB
JavaScript
/**
|
||
* Mining flow (headless browser — NOT a Node test).
|
||
*
|
||
* Drives the REAL game's input pipeline (mousedown/keydown on the canvas)
|
||
* and walks the whole arm sequence against a seeded galaxy — including the
|
||
* SHIP'S STATE (js/entities/Ship.js): 'normal' by default, 'mining' while
|
||
* the arm's sequence runs, and ANY movement (a world click or the compass
|
||
* autopilot) drops it back to 'normal' and ends the sequence:
|
||
*
|
||
* click a rock → the menu opens at the click (MINE ASTEROIDS)
|
||
* a click OUTSIDE it → closes, and does not fly the ship
|
||
* click MINE ASTEROIDS → "extending": the ship is in 'mining' state,
|
||
* holds station, console says "Extending..."
|
||
* a world click meanwhile → MOVES the ship: the extension ABORTS, the
|
||
* ship goes back to 'normal' and flies
|
||
* mine again, the ~1.5 s → "mining": beam live, ore motes ride it,
|
||
* reach the ship still 'mining'
|
||
* the COMPASS AUTOPILOT → MOVES the ship: 'normal' again, the beam
|
||
* retracts as the ship goes
|
||
* mine again; ESC → breaks the beam, the ship is 'normal'
|
||
* ESC with the menu open → closes it, no mining
|
||
* click CANCEL → closes, no mining, ship 'normal'
|
||
*
|
||
* HEADLESS PUMP: this box's headless Firefox freezes rAF and timers once
|
||
* the first paint is done, so the page can't run itself. The runner
|
||
* (dev/cdp-firefox.mjs) keeps waking the JS thread with execute/sync —
|
||
* so the whole flow is a synchronous stage machine, and each wake calls
|
||
* `window.__MINING_PUMP__()`, which (a) steps the Phaser loop manually
|
||
* (`game.loop.step(performance.now())` — one frame per call, which also
|
||
* processes the queued pointer/keyboard events) and (b) advances the
|
||
* next stage. Long game-time waits (the 1.5 s arm extension) may span
|
||
* many runner polls — the stage machine RESUMES where it left off.
|
||
* Results land in `window.__MINING__`:
|
||
*
|
||
* python3 -m http.server 8080
|
||
* node dev/cdp-firefox.mjs http://localhost:8080/dev/mining-test.html \
|
||
* 'window.__MINING_PUMP__(); return window.__MINING__;'
|
||
*/
|
||
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);
|
||
|
||
// A deterministic galaxy (same clusters every run) + quiet audio in headless.
|
||
globalThis.__ORBIT_DEV_SEED = 'MINING';
|
||
const gameConfig = createGameConfig();
|
||
gameConfig.scene = [GameScene]; // GameScene boots first
|
||
if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true;
|
||
|
||
const game = new Phaser.Game(gameConfig);
|
||
window.game = game;
|
||
|
||
// ----------------------------------------------------------------------
|
||
// Results + checks
|
||
// ----------------------------------------------------------------------
|
||
const results = [];
|
||
const check = (label, cond) => {
|
||
const pass = !!cond;
|
||
results.push({ label, pass });
|
||
console.log(`${pass ? '✔' : '✘ FAIL'} ${label}`);
|
||
};
|
||
|
||
// ----------------------------------------------------------------------
|
||
// Manual frame pump (the only clock this box has)
|
||
// ----------------------------------------------------------------------
|
||
const pumpErrors = [];
|
||
const pump = (maxSteps = 20, cpuBudgetMs = 700) => {
|
||
const g = window.game;
|
||
if (!g || !g.loop) return 0;
|
||
const t0 = performance.now();
|
||
let n = 0;
|
||
while (n < maxSteps) {
|
||
if (performance.now() - t0 > cpuBudgetMs) break;
|
||
try {
|
||
if (g.input && typeof g.input.update === 'function') g.input.update();
|
||
g.loop.step(performance.now());
|
||
} catch (e) {
|
||
pumpErrors.push(`pump: ${String((e && e.message) || e)}`);
|
||
break;
|
||
}
|
||
n++;
|
||
}
|
||
return n;
|
||
};
|
||
|
||
/**
|
||
* Resumable wait. Returns:
|
||
* true — `pred()` is met
|
||
* false — `inProgress()` stopped holding, or the wait outlived
|
||
* `maxTotalMs` (wall clock) → the wait FAILED
|
||
* null — the CPU budget for THIS wake is spent but `inProgress()`
|
||
* still holds → the stage must `return`; the runner's next
|
||
* wake re-enters the same stage and keeps waiting.
|
||
*/
|
||
let stageSince = -1;
|
||
let stageSinceMs = 0;
|
||
const wait = (pred, inProgress, note, cpuBudgetMs = 1200, maxTotalMs = 60000) => {
|
||
if (stageSince !== stage) { stageSince = stage; stageSinceMs = performance.now(); }
|
||
const t0 = performance.now();
|
||
for (;;) {
|
||
if (pred()) {
|
||
if (note) note();
|
||
return true;
|
||
}
|
||
if (!inProgress()) return false;
|
||
if (performance.now() - stageSinceMs > maxTotalMs) return false;
|
||
if (performance.now() - t0 > cpuBudgetMs) return null;
|
||
pump(8, 120);
|
||
}
|
||
};
|
||
|
||
// ----------------------------------------------------------------------
|
||
// Input (real events on the canvas / window)
|
||
// ----------------------------------------------------------------------
|
||
const press = (x, y) => {
|
||
const canvas = game.canvas;
|
||
const r = canvas.getBoundingClientRect();
|
||
const zoom = r.width / canvas.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,
|
||
});
|
||
canvas.dispatchEvent(mk('mouseover', 0));
|
||
canvas.dispatchEvent(mk('mousedown', 1));
|
||
canvas.dispatchEvent(mk('mouseup', 0));
|
||
};
|
||
|
||
const esc = () =>
|
||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||
key: 'Escape',
|
||
keyCode: 27,
|
||
which: 27,
|
||
bubbles: true,
|
||
cancelable: true,
|
||
}));
|
||
|
||
/** World → canvas screen coords (zoom 1, no rotation: screen = world − scroll). */
|
||
const w2s = (wx, wy) => {
|
||
const c = game.scene.getScene('GameScene').cameras.main;
|
||
return { x: wx - c.scrollX, y: wy - c.scrollY };
|
||
};
|
||
|
||
const btnScreen = (btn) => {
|
||
const s = game.scene.getScene('GameScene');
|
||
return w2s(s.miningPopup.x + btn.x, s.miningPopup.y + btn.y);
|
||
};
|
||
const rockScreen = () => {
|
||
const s = game.scene.getScene('GameScene');
|
||
return w2s(rock.wx, rock.wy);
|
||
};
|
||
// A screen point guaranteed to be OFF every rock of the cluster (the
|
||
// members are scattered — a fixed point can land on one) AND outside the
|
||
// mining panel's footprint (a click right after a button press that
|
||
// lands on the panel area is consumed as the panel's own click).
|
||
const openSpaceScreen = () => {
|
||
const s = game.scene.getScene('GameScene');
|
||
const cam = s.cameras.main;
|
||
const W = s.scale.width, H = s.scale.height;
|
||
const p = s.miningPopup;
|
||
const inPanel = (wx, wy) => {
|
||
const r = p && p.rect;
|
||
if (!r) return false;
|
||
return wx >= r.x - 20 && wx <= r.x + r.w + 20 && wy >= r.y - 20 && wy <= r.y + r.h + 20;
|
||
};
|
||
const cands = [
|
||
[W / 2, H / 2], [120, H / 2], [W - 120, H / 2],
|
||
[W / 2, 120], [W / 2, H - 120], [120, 120], [W - 120, H - 120],
|
||
];
|
||
for (const [cx, cy] of cands) {
|
||
const wx = cam.scrollX + cx, wy = cam.scrollY + cy;
|
||
let clear = true;
|
||
for (const m of (cluster ? cluster.members : [])) {
|
||
if (Math.hypot(m.wx - wx, m.wy - wy) < m.radius + 40) { clear = false; break; }
|
||
}
|
||
// Far enough from the ship that the flight stays observable (a click
|
||
// next to the ship arrives instantly and clears the target).
|
||
const fromShip = Math.hypot(s.ship.x - wx, s.ship.y - wy) > 400;
|
||
if (clear && fromShip && !inPanel(wx, wy)) return { x: cx, y: cy };
|
||
}
|
||
return { x: 120, y: 120 }; // last resort (practically unreachable)
|
||
};
|
||
|
||
// ----------------------------------------------------------------------
|
||
// The stage machine (resumable across runner polls)
|
||
// ----------------------------------------------------------------------
|
||
let stage = 0;
|
||
let cluster = null;
|
||
let rock = null;
|
||
let finished = false;
|
||
let sawExtendingToast = false;
|
||
|
||
const finish = (pass) => {
|
||
if (finished) return;
|
||
finished = true;
|
||
const all = [...results];
|
||
if (pumpErrors.length) {
|
||
for (const e of pumpErrors) all.push({ label: e, pass: false });
|
||
}
|
||
const errors = (window.__CAPTURED_ERRORS__ || []).concat(pumpErrors);
|
||
if (errors.length === 0) all.push({ label: 'no console errors were captured', pass: true });
|
||
const ok = all.every((r) => r.pass);
|
||
window.__MINING__ = { pass: ok, results: all, errors, dbg: window.__DBG__STATE__ || null };
|
||
console.log(ok ? 'MINING PASS' : 'MINING FAIL');
|
||
};
|
||
|
||
// ----------------------------------------------------------------------
|
||
// Diagnostics: the state machine can stash snapshots here; finish()
|
||
// ships them back in window.__MINING__.dbg.
|
||
// ----------------------------------------------------------------------
|
||
const dbg = (window.__DBG__STATE__ = {});
|
||
const snapshot = (tag) => {
|
||
try {
|
||
const s = scene();
|
||
const arr = (dbg[tag] = dbg[tag] || []);
|
||
arr.push({
|
||
t: Math.round(s.time.now),
|
||
mining: s.mining.state,
|
||
ship: s.ship.state,
|
||
target: s.ship.target ? [Math.round(s.ship.target.x), Math.round(s.ship.target.y)] : null,
|
||
xy: [Math.round(s.ship.x), Math.round(s.ship.y)],
|
||
scroll: [Math.round(s.cameras.main.scrollX), Math.round(s.cameras.main.scrollY)],
|
||
popup: s.miningPopup ? s.miningPopup.state : null,
|
||
stamp: s.miningPopup ? s.miningPopup.closedByButtonAt : null,
|
||
});
|
||
if (arr.length > 30) arr.shift();
|
||
} catch { /* booting */ }
|
||
};
|
||
|
||
const scene = () => game.scene.getScene('GameScene');
|
||
|
||
const fail = (label) => {
|
||
check(label, false);
|
||
finish(false);
|
||
};
|
||
|
||
/** Park the ship by the rock and frame it (the test's teleport). */
|
||
const parkNearRock = () => {
|
||
const s = scene();
|
||
s.ship.stop();
|
||
const sx = rock.wx - 190;
|
||
s.ship.setPosition(sx, rock.wy);
|
||
s.cameras.main.setScroll(
|
||
(sx + rock.wx) / 2 - s.scale.width / 2,
|
||
rock.wy - s.scale.height / 2,
|
||
);
|
||
};
|
||
|
||
// The camera eases onto the ship (GameScene.updateCamera) — after a park
|
||
// or a flight it keeps drifting for a while. Pump until it settles so a
|
||
// rock's screen position is stable when we compute the next click.
|
||
const settleCamera = (maxBatches = 60) => {
|
||
let last = null;
|
||
let still = 0;
|
||
for (let i = 0; i < maxBatches; i++) {
|
||
pump(6, 80);
|
||
try {
|
||
const cam = scene().cameras.main;
|
||
const cur = [cam.scrollX, cam.scrollY];
|
||
if (last) {
|
||
if (Math.abs(cur[0] - last[0]) < 0.5 && Math.abs(cur[1] - last[1]) < 0.5) {
|
||
still++;
|
||
if (still >= 3) return true;
|
||
} else still = 0;
|
||
}
|
||
last = cur;
|
||
} catch { /* booting */ }
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const stepMachine = () => {
|
||
let s = null;
|
||
try { s = scene(); } catch { /* not booted yet */ }
|
||
if (!s || !s.ship || !s.mining || !s.miningPopup) {
|
||
pump(10, 400); // let the scene manager finish booting
|
||
return;
|
||
}
|
||
|
||
// Small helper for this scene: pump a little so a dispatched input
|
||
// event is processed + a couple of frames of reaction settle.
|
||
const settle = (ms = 250) => pump(10, ms);
|
||
|
||
switch (stage) {
|
||
// ---- 0) park the ship by a rock, click the rock ---------------------
|
||
case 0: {
|
||
if (!s.asteroidClusters || s.asteroidClusters.length === 0) {
|
||
fail('the seeded system has asteroid clusters');
|
||
return;
|
||
}
|
||
cluster = s.asteroidClusters[0];
|
||
rock = cluster.members[0];
|
||
parkNearRock();
|
||
settleCamera();
|
||
settle(200);
|
||
press(rockScreen().x, rockScreen().y);
|
||
stage = 1;
|
||
return;
|
||
}
|
||
|
||
// ---- 1) the menu opened at the click ---------------------------------
|
||
case 1: {
|
||
const r = wait(() => s.miningPopup.isOpen === true,
|
||
() => true, // a click either opens the menu or it never will
|
||
null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('a click on a rock opens the mining menu');
|
||
check('a click on a rock opens the mining menu', true);
|
||
check('the primary button says MINE ASTEROIDS',
|
||
s.miningPopup.primaryBtn.labelText.text === 'MINE ASTEROIDS');
|
||
check('the header names the cluster',
|
||
s.miningPopup.headerText.text.toUpperCase().includes(String(cluster.discoveryName).toUpperCase()));
|
||
check('the rock click did NOT fly the ship', s.ship.target === null);
|
||
check('the rock click did NOT start mining', s.mining.state === 'idle');
|
||
check('the ship is in its NORMAL state', s.ship.state === 'normal');
|
||
press(openSpaceScreen().x, openSpaceScreen().y); // open space — outside the panel
|
||
stage = 2;
|
||
return;
|
||
}
|
||
|
||
// ---- 2) the outside click closed it, without flying ------------------
|
||
case 2: {
|
||
const r = wait(() => s.miningPopup.isOpen === false, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('a click outside the menu closes it');
|
||
check('a click outside the menu closes it', true);
|
||
check('the dismissing click does not fly the ship', s.ship.target === null);
|
||
check('the dismissing click does not start mining', s.mining.state === 'idle');
|
||
press(rockScreen().x, rockScreen().y); // re-open
|
||
stage = 3;
|
||
return;
|
||
}
|
||
|
||
// ---- 3) press MINE ASTEROIDS -----------------------------------------
|
||
case 3: {
|
||
const r = wait(() => s.miningPopup.isOpen === true, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('the menu re-opens on the rock');
|
||
check('the menu re-opens on the rock', true);
|
||
const b = btnScreen(s.miningPopup.primaryBtn);
|
||
press(b.x, b.y);
|
||
stage = 4;
|
||
return;
|
||
}
|
||
|
||
// ---- 4) extending: locked + the console call --------------------------
|
||
case 4: {
|
||
const r = wait(
|
||
() => s.mining.state === 'extending' || s.mining.state === 'mining',
|
||
() => ['extending', 'mining'].includes(s.mining.state) || s.mining.state === 'idle',
|
||
() => {
|
||
const t = (s.consoleToastG || []).map((x) => x.text).join(' ');
|
||
if (/extending mining arm/i.test(t)) sawExtendingToast = true;
|
||
},
|
||
1200, 60000);
|
||
if (r === null) return;
|
||
if (!r) fail('MINE ASTEROIDS starts the extension');
|
||
check('MINE ASTEROIDS starts the extension', true);
|
||
check('the menu closed on the button press', s.miningPopup.isOpen === false);
|
||
check('the ship is in the MINING state while the arm extends', s.ship.state === 'mining');
|
||
check('the ship holds station while the arm extends', s.ship.target === null);
|
||
if (!sawExtendingToast) {
|
||
// One more chance while the arm is still out (the toast holds the
|
||
// whole extension window).
|
||
const r2 = wait(
|
||
() => s.mining.state === 'mining' || /extending mining arm/i.test((s.consoleToastG || []).map((x) => x.text).join(' ')),
|
||
() => s.mining.state !== 'idle',
|
||
null, 1200, 60000);
|
||
if (r2 === null) { stage = 4; return; }
|
||
sawExtendingToast = /extending mining arm/i.test((s.consoleToastG || []).map((x) => x.text).join(' ')) || sawExtendingToast;
|
||
}
|
||
check('the console says Extending Mining Arm...', sawExtendingToast);
|
||
|
||
// A world click mid-extension MOVES the ship — which ends the
|
||
// mining state (the new contract: movement beats the arm's reach).
|
||
if (s.mining.state === 'extending') {
|
||
const p5 = openSpaceScreen();
|
||
dbg.stage4press = p5;
|
||
press(p5.x, p5.y);
|
||
}
|
||
stage = 5;
|
||
return;
|
||
}
|
||
|
||
// ---- 5) the movement broke the arm; set up the real attempt ----------
|
||
case 5: {
|
||
// If the "open space" click landed on another member of the cluster
|
||
// (the members are scattered), it opened the menu instead of flying
|
||
// — close it and click true open space.
|
||
if (s.miningPopup.isOpen) {
|
||
esc();
|
||
settle(150);
|
||
const p = openSpaceScreen();
|
||
dbg.stage5repress = p;
|
||
press(p.x, p.y);
|
||
}
|
||
const r = wait(() => s.mining.state === 'idle' && s.mining.beam === null,
|
||
() => ['extending', 'mining', 'retracting', 'idle'].includes(s.mining.state),
|
||
() => snapshot('stage5'), 1200, 90000);
|
||
if (r === null) return;
|
||
if (!r) fail('a world click breaks the arm (abort or retract)');
|
||
check('a world click breaks the arm (abort or retract)', true);
|
||
check('the beam is gone (aborted, or retracted and reaped)', s.mining.beam === null);
|
||
check('the ship is back in the NORMAL state', s.ship.state === 'normal');
|
||
check('the breaking click flew the ship', s.ship.target !== null);
|
||
|
||
// Set up the next attempt: park by the rock, let the camera settle,
|
||
// menu, MINE.
|
||
parkNearRock();
|
||
settleCamera();
|
||
dbg.stage6press = rockScreen();
|
||
press(rockScreen().x, rockScreen().y);
|
||
stage = 6;
|
||
return;
|
||
}
|
||
|
||
// ---- 6) menu again; start mining for real ------------------------------
|
||
case 6: {
|
||
const r = wait(() => s.miningPopup.isOpen === true, () => true,
|
||
() => snapshot('stage6'), 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) {
|
||
const pp = dbg.stage6press || rockScreen();
|
||
dbg.stage6 = {
|
||
press: pp,
|
||
rockHit: !!s.rockAt(rock.wx, rock.wy),
|
||
rockScreenNow: rockScreen(),
|
||
savePanelOpen: !!(s.savePanel && s.savePanel.isOpen),
|
||
subbarOpen: !!(s.menuSubBar && s.menuSubBar.isOpen),
|
||
actionbarHit: !!(s.actionBar && s.actionBar.contains(pp.x, pp.y)),
|
||
compassHit: !!s.compass.contains(pp.x, pp.y),
|
||
titleHit: !!s.hudTitleContains(pp.x, pp.y),
|
||
popupState: s.miningPopup ? s.miningPopup.state : null,
|
||
miningState: s.mining.state,
|
||
shipTarget: s.ship.target,
|
||
};
|
||
fail('the menu re-opens (mine-for-real test)');
|
||
}
|
||
const b = btnScreen(s.miningPopup.primaryBtn);
|
||
press(b.x, b.y);
|
||
stage = 7;
|
||
return;
|
||
}
|
||
|
||
// ---- 7) the reach completes into mining --------------------------------
|
||
case 7: {
|
||
const r = wait(() => s.mining.state === 'mining',
|
||
() => ['extending', 'mining'].includes(s.mining.state),
|
||
null, 1200, 90000);
|
||
if (r === null) return;
|
||
if (!r) fail('the extension completes into mining');
|
||
check('the extension completes into mining', true);
|
||
check('the ship is still in the MINING state', s.ship.state === 'mining');
|
||
check('the beam is live', !!s.mining.beam && (s.mining.beam.state === 'in' || s.mining.beam.state === 'steady'));
|
||
check('the beam is locked to the clicked rock',
|
||
!!s.mining.beam && s.mining.beam.member === rock && s.mining.member === rock);
|
||
stage = 8;
|
||
return;
|
||
}
|
||
|
||
// ---- 8) beam steady + ore; then the AUTOPILOT moves the ship -----------
|
||
case 8: {
|
||
const r2 = wait(
|
||
() => !!s.mining.beam && s.mining.beam.state === 'steady' && s.mining.beam.particles.some((p) => p.active),
|
||
() => !!s.mining.beam && s.mining.state === 'mining',
|
||
null, 1200, 90000);
|
||
if (r2 === null) return;
|
||
if (!r2) fail('the beam settles and ore motes ride it');
|
||
check('the beam is live and steady', true);
|
||
check('ore motes ride the beam toward the ship', true);
|
||
check('the ship is in the MINING state while the beam is live', s.ship.state === 'mining');
|
||
|
||
// The compass autopilot seam — the player sends the ship elsewhere
|
||
// (a different discovered object — the mining cluster's rim is right
|
||
// next to the ship and the flight would "arrive" before it's
|
||
// observable): movement must drop the ship back to 'normal' and end
|
||
// the mining.
|
||
const objs = s.discoverableObjects();
|
||
const other = objs.find((o) => o.id !== cluster.discoveryId) || objs[0];
|
||
dbg.autopilotTarget = {
|
||
id: other.id,
|
||
distFromShip: Math.round(Math.hypot(other.x - s.ship.x, other.y - s.ship.y)),
|
||
};
|
||
s.autopilotTo(other.id);
|
||
stage = 9;
|
||
return;
|
||
}
|
||
|
||
// ---- 9) the autopilot break: normal state, beam reaped ------------------
|
||
case 9: {
|
||
const r = wait(() => s.mining.state === 'idle' && s.mining.beam === null,
|
||
() => ['mining', 'retracting', 'idle'].includes(s.mining.state), null, 1200, 90000);
|
||
if (r === null) return;
|
||
if (!r) fail('the autopilot ends the mining and reaps the beam');
|
||
check('the autopilot ends the mining and reaps the beam', true);
|
||
check('the ship is back in the NORMAL state', s.ship.state === 'normal');
|
||
check('the autopilot flew the ship', s.ship.target !== null);
|
||
|
||
parkNearRock();
|
||
settleCamera();
|
||
press(rockScreen().x, rockScreen().y); // mine again (ESC test)
|
||
stage = 10;
|
||
return;
|
||
}
|
||
|
||
// ---- 10) menu again; start a third mining ---------------------------------
|
||
case 10: {
|
||
const r = wait(() => s.miningPopup.isOpen === true, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('the menu opens again (ESC test)');
|
||
const b = btnScreen(s.miningPopup.primaryBtn);
|
||
press(b.x, b.y);
|
||
stage = 11;
|
||
return;
|
||
}
|
||
|
||
// ---- 11) mining is live again; ESC breaks it --------------------------------
|
||
case 11: {
|
||
const r = wait(() => s.mining.state === 'mining',
|
||
() => ['extending', 'mining'].includes(s.mining.state), null, 1200, 90000);
|
||
if (r === null) return;
|
||
if (!r) fail('mining is live again (ESC test)');
|
||
check('mining is live again (ESC test)', true);
|
||
check('the ship is in the MINING state', s.ship.state === 'mining');
|
||
esc();
|
||
stage = 12;
|
||
return;
|
||
}
|
||
|
||
// ---- 12) the ESC stop landed ------------------------------------------------
|
||
case 12: {
|
||
const r = wait(() => s.mining.state === 'retracting' || s.mining.state === 'idle',
|
||
() => ['retracting', 'mining', 'idle'].includes(s.mining.state), null, 1200, 60000);
|
||
if (r === null) return;
|
||
if (!r) fail('ESC breaks the beam');
|
||
check('ESC breaks the beam', true);
|
||
stage = 13;
|
||
return;
|
||
}
|
||
|
||
// ---- 13) arm idle after the ESC stop; the ship is NORMAL again ------------
|
||
case 13: {
|
||
const r = wait(() => s.mining.state === 'idle' && s.mining.beam === null,
|
||
() => ['retracting', 'idle'].includes(s.mining.state), null, 1200, 60000);
|
||
if (r === null) return;
|
||
if (!r) fail('the arm is idle after the ESC stop');
|
||
check('the arm is idle after the ESC stop', true);
|
||
check('the ship is back in the NORMAL state', s.ship.state === 'normal');
|
||
|
||
press(rockScreen().x, rockScreen().y); // menu again (ESC-closes test)
|
||
stage = 14;
|
||
return;
|
||
}
|
||
|
||
// ---- 14) ESC closes an open menu --------------------------------------------
|
||
case 14: {
|
||
const r = wait(() => s.miningPopup.isOpen === true, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('the menu opens again (ESC-closes test)');
|
||
esc();
|
||
stage = 15;
|
||
return;
|
||
}
|
||
|
||
// ---- 15) the menu closed; now CANCEL ----------------------------------------
|
||
case 15: {
|
||
const r = wait(() => s.miningPopup.isOpen === false, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('ESC closes the open menu');
|
||
check('ESC closes the open menu', true);
|
||
check('...and did not start mining', s.mining.state === 'idle');
|
||
check('the ship is in the NORMAL state', s.ship.state === 'normal');
|
||
press(rockScreen().x, rockScreen().y);
|
||
stage = 16;
|
||
return;
|
||
}
|
||
|
||
// ---- 16) CANCEL closes without mining ---------------------------------------
|
||
case 16: {
|
||
const r = wait(() => s.miningPopup.isOpen === true, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('the menu opens again (CANCEL test)');
|
||
const c = btnScreen(s.miningPopup.cancelBtn);
|
||
press(c.x, c.y);
|
||
stage = 17;
|
||
return;
|
||
}
|
||
|
||
// ---- 17) done -----------------------------------------------------------------
|
||
case 17: {
|
||
const r = wait(() => s.miningPopup.isOpen === false, () => true, null, 1500, 20000);
|
||
if (r === null) return;
|
||
if (!r) fail('CANCEL closes the menu');
|
||
check('CANCEL closes the menu', true);
|
||
check('CANCEL does not start mining', s.mining.state === 'idle');
|
||
check('CANCEL does not fly the ship', s.ship.target === null);
|
||
check('the ship is in the NORMAL state', s.ship.state === 'normal');
|
||
finish(results.every((x) => x.pass));
|
||
return;
|
||
}
|
||
}
|
||
};
|
||
|
||
window.__MINING_PUMP__ = () => {
|
||
try {
|
||
stepMachine();
|
||
} catch (e) {
|
||
(window.__CAPTURED_ERRORS__ || (window.__CAPTURED_ERRORS__ = [])).push(`stage: ${String((e && e.stack) || e)}`);
|
||
check(`THREW: ${String((e && e.message) || e)}`, false);
|
||
finish(false);
|
||
}
|
||
};
|