Compare commits

..

No commits in common. "7b48e030efabd612e6a26c1bcb02daa755adeb55" and "cff8fa335e53ce067634b1f3cba544ee9b332d63" have entirely different histories.

7 changed files with 114 additions and 350 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

View File

@ -2,24 +2,19 @@
* 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:
* and walks the whole arm sequence against a seeded galaxy:
*
* 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'
* click MINE ASTEROIDS "extending" (ship locked, console toast
* "Extending Mining Arm...")
* a world click meanwhile is held (no fly, no abort)
* the ~1.5 s reach "mining": the beam is live, ore motes ride it
* click the rock again the menu now says STOP MINING
* click STOP MINING the beam retracts, then the arm is idle
* ESC while mining breaks the beam
* ESC with the menu open closes it
* click CANCEL closes, no mining
*
* 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
@ -157,36 +152,9 @@ 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)
return { x: s.scale.width / 2, y: s.scale.height / 2 };
};
// ----------------------------------------------------------------------
@ -208,33 +176,10 @@ const finish = (pass) => {
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 };
window.__MINING__ = { pass: ok, results: all, errors };
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) => {
@ -242,41 +187,6 @@ const fail = (label) => {
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 */ }
@ -298,8 +208,13 @@ const stepMachine = () => {
}
cluster = s.asteroidClusters[0];
rock = cluster.members[0];
parkNearRock();
settleCamera();
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,
);
settle(200);
press(rockScreen().x, rockScreen().y);
stage = 1;
@ -320,7 +235,6 @@ const stepMachine = () => {
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;
@ -365,8 +279,8 @@ const stepMachine = () => {
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);
check('the ship is locked while the arm extends',
s.ship.target === null && s.mining.isLocked === true);
if (!sawExtendingToast) {
// One more chance while the arm is still out (the toast holds the
// whole extension window).
@ -379,95 +293,26 @@ const stepMachine = () => {
}
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);
}
// A world click mid-extension holds (no fly, no abort).
if (s.mining.state === 'extending') press(openSpaceScreen().x, openSpaceScreen().y);
stage = 5;
return;
}
// ---- 5) the movement broke the arm; set up the real attempt ----------
// ---- 5) the reach completes into mining --------------------------------
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),
() => s.mining.state === 'extending' || s.mining.state === 'mining',
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('clicks held while the arm extends (no fly)', s.ship.target === null);
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: {
// Let the beam settle to steady + the ore stream build.
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',
@ -476,133 +321,139 @@ const stepMachine = () => {
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);
press(rockScreen().x, rockScreen().y); // the beam target, again
stage = 6;
return;
}
// ---- 6) the target menu now offers STOP MINING ------------------------
case 6: {
const r = wait(() => s.miningPopup.isOpen === true, () => true, null, 1500, 20000);
if (r === null) return;
if (!r) fail('the menu opens on the mining target');
check('the menu opens on the mining target', true);
check('the primary button now says STOP MINING',
s.miningPopup.primaryBtn.labelText.text === 'STOP MINING');
const b = btnScreen(s.miningPopup.primaryBtn);
press(b.x, b.y);
stage = 7;
return;
}
// ---- 7) stop → retract --------------------------------------------------
case 7: {
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('STOP MINING retracts the beam');
check('STOP MINING retracts the beam', true);
check('the ship is free again while it retracts', s.mining.isLocked === false);
stage = 8;
return;
}
// ---- 8) the beam is reaped and the arm is idle ---------------------------
case 8: {
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 beam is reaped and the arm is idle');
check('the beam is reaped and the arm is idle', true);
press(rockScreen().x, rockScreen().y); // mine again (ESC test)
stage = 9;
return;
}
// ---- 9) the autopilot break: normal state, beam reaped ------------------
// ---- 9) menu again; start a second mining -------------------------------
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;
stage = 10;
return;
}
// ---- 11) mining is live again; ESC breaks it --------------------------------
case 11: {
// ---- 10) mining is live again; ESC breaks it ------------------------------
case 10: {
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;
stage = 11;
return;
}
// ---- 12) the ESC stop landed ------------------------------------------------
case 12: {
// ---- 11) the ESC stop landed ----------------------------------------------
case 11: {
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;
stage = 12;
return;
}
// ---- 13) arm idle after the ESC stop; the ship is NORMAL again ------------
case 13: {
// ---- 12) arm idle after the ESC stop ---------------------------------------
case 12: {
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;
stage = 13;
return;
}
// ---- 14) ESC closes an open menu --------------------------------------------
case 14: {
// ---- 13) ESC closes an open menu ---------------------------------------------
case 13: {
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;
stage = 14;
return;
}
// ---- 15) the menu closed; now CANCEL ----------------------------------------
case 15: {
// ---- 14) the menu closed; now CANCEL ------------------------------------------
case 14: {
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;
stage = 15;
return;
}
// ---- 16) CANCEL closes without mining ---------------------------------------
case 16: {
// ---- 15) CANCEL closes without mining --------------------------------------------
case 15: {
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;
stage = 16;
return;
}
// ---- 17) done -----------------------------------------------------------------
case 17: {
// ---- 16) done ----------------------------------------------------------------------
case 16: {
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;
}

View File

@ -99,49 +99,11 @@ export class Ship extends Phaser.Physics.Arcade.Sprite {
this.radius = worldSize / 2;
this.target = null;
// SHIP STATE — what the ship is doing right now. 'normal' is the
// default: free to fly wherever the player sends it. 'mining' = the
// arm's sequence owns the ship (js/mining/Mining.js drives it; the
// beam + ore stream only live while the state holds). More states
// will land here (docking, boarding, …). Any change OUT of a state —
// another state, or the player MOVING the ship (setTarget, autopilot)
// — is signalled via onStateChange, and the scene's handler tears
// down whatever that state was doing (ends the mining sequence).
this.state = 'normal'; // 'normal' | 'mining' (later: more)
this.onStateChange = null; // (next, prev, reason) => void — the scene installs
}
/**
* Change the ship's state ('normal' | 'mining' | ). A no-op when the
* state is unchanged. On a real change, onStateChange fires the scene
* ends the mining sequence when the ship leaves 'mining', whichever
* state (or movement) took it out.
*
* @returns {boolean} true when the state actually changed
*/
setState(next, reason) {
if (next === this.state) return false;
const prev = this.state;
this.state = next;
if (typeof this.onStateChange === 'function') {
try {
this.onStateChange(next, prev, reason);
} catch (err) {
console.error('[ship] onStateChange handler failed', err);
}
}
return true;
}
/** Set the destination to fly to (world coordinates). */
setTarget(x, y) {
this.target = { x, y };
// Moving the ship ends whatever non-normal state it was in (mining,
// and later states): the player's movement always wins, so any state
// hands the ship back to 'normal' — the scene's onStateChange handler
// finishes the teardown (retracts the beam, releases the arm).
this.setState('normal', 'move');
}
/** Stop steering and brake immediately. */

View File

@ -2,27 +2,20 @@ import { config } from '../config/Config.js';
import { MiningBeam } from './MiningBeam.js';
/**
* The ship's mining state — the machine behind the beam. The SHIP'S
* state gates it (js/entities/Ship.js): the sequence runs only while
* the ship is in its 'mining' state the scene puts the ship there
* when the arm engages and takes it back to 'normal' when the sequence
* ends. Moving the ship (a world click, the autopilot) or any other
* state change drops it out of 'mining', which ends the sequence:
* the scene's onStateChange handler does the teardown, and update()
* below enforces the gate.
* The ship's mining state the machine behind the beam:
*
* idle begin() extending (armExtendMs) mining stop() retracting
*
* (stop() cuts the extension) beam out
*
*
* extending the arm is reaching for the rock the ship holds
* station (GameScene stops it) and sits in 'mining';
* mining the beam is live (MiningBeam) the ship stays at its
* rim, still 'mining'; retargeting (begin on another rock)
* cuts the current beam and re-extends;
* extending the arm is reaching for the rock the ship is LOCKED
* (GameScene stops it) and the world holds;
* mining the beam is live (MiningBeam) the ship stays LOCKED at
* its rim; retargeting (begin on another rock) cuts the
* current beam and re-extends;
* retracting the beam is pulling back rock ship; the ship is already
* BACK TO 'normal' (FREE) the player can fly as it dies.
* FREE (isLocked is false) the player can fly as it dies.
*
* This class owns only the sequence + the beam's lifecycle. Scene
* concerns (ship stop, toasts, sfx) ride the onPhase seam the same
@ -45,8 +38,8 @@ export class Mining {
this.armExtendMs = config.get('asteroids.mining.armExtendMs', 1500);
}
/** The arm's sequence is live: mid-extension OR beam live. */
get isActive() {
/** The ship may not fly: mid-extension OR beam live. */
get isLocked() {
return this.state === 'extending' || this.state === 'mining';
}
@ -91,19 +84,6 @@ export class Mining {
* mining hand-off and reaps a finished retract.
*/
update(time, delta) {
// The ship's state is the gate: the sequence (and its visuals) run
// only while the ship is in its 'mining' state. It left that state —
// the player moved the ship, or a state change — so end the sequence
// right here (belt & braces over the scene's onStateChange handler).
const ship = this.scene && this.scene.ship;
if (
ship &&
ship.state !== 'mining' &&
(this.state === 'extending' || this.state === 'mining')
) {
this.stop();
return;
}
if (this.state === 'extending' && time >= this.stateT0 + this.armExtendMs) {
this.state = 'mining';
this.beam = new MiningBeam(this.scene, this);

View File

@ -318,11 +318,10 @@ export class GameScene extends Phaser.Scene {
// ---- MINING (the clusters are now verbs, not just rocks) -----------
// The state machine (js/mining/Mining.js): idle → extending (~1.5 s,
// the ship holds station, "Extending Mining Arm..." in the console
// slot) → mining (the beam is live — js/mining/MiningBeam.js)
// → retracting. The pop-up (js/ui/MiningPopup.js) is the context
// menu a rock click opens, world-anchored right where the player
// clicked.
// the ship is locked, "Extending Mining Arm..." in the console slot)
// → mining (the beam is live — js/mining/MiningBeam.js) → retracting.
// The pop-up (js/ui/MiningPopup.js) is the context menu a rock click
// opens, world-anchored right where the player clicked.
this.mining = new Mining(this, {
onPhase: (p) => this.onMiningPhase(p),
});
@ -330,24 +329,14 @@ export class GameScene extends Phaser.Scene {
onAction: (id, rock) => this.miningAction(id, rock),
});
// SHIP STATE (js/entities/Ship.js): 'normal' is the default — the
// ship is free; 'mining' — the arm's sequence owns the ship. The
// mining visuals live and die with that state: ANY change out of it
// (the player moving the ship — a world click or the compass
// autopilot — a cancel, or a future state) ends the mining sequence.
this.ship.onStateChange = (next, prev) => {
if (prev === 'mining' && next !== 'mining') this.mining.stop();
};
// Input: click = fly there. A click ON the command deck is deck
// business, a click on a compass name tag is an autopilot (it
// already retargeted the ship), and a click on the system NAME (or
// the caret beside it) toggles the dossier open/closed — none of
// those is a fly-here. A click on an ASTEROID opens the mining
// pop-up (a context menu — its buttons own their clicks; any click
// elsewhere closes it and is consumed). Any OTHER click moves the
// ship — which ends the mining state (the beam retracts as the
// ship goes; a mid-reach arm aborts).
// elsewhere closes it and is consumed). While the beam is live the
// ship is LOCKED: a world click breaks the beam first, then flies.
// A click inside a planet clamps to that planet's keep-out rim — the
// ship can stop at the clearance, never inside. (Worlds don't
// overlap, so sequential clamping is exact.) A click BEYOND the
@ -388,20 +377,19 @@ export class GameScene extends Phaser.Scene {
// A click the pop-up's OWN buttons just handled (they close it from
// their side first — the two handlers race on event order): it is
// the menu's click, never a fly-here. The button and this handler
// fire on the SAME input pass (same frame — this.time.now is
// identical), so "same frame AND inside the panel's footprint"
// isolates that exact click without swallowing the player's next
// deliberate one (a click on the rock, to re-open the menu).
// the menu's click, never a fly-here.
if (
this.miningPopup &&
this.miningPopup.closedByButtonAt !== null &&
this.time.now - this.miningPopup.closedByButtonAt < 20 &&
this.miningPopup.containsScreen(pointer.x, pointer.y)
this.time.now - this.miningPopup.closedByButtonAt < 300
) {
return;
}
// The arm is mid-extension: the ship is committed to the rock for
// the ~1.5 s reach — the world holds (the toast says why).
if (this.mining.state === 'extending') return;
// A rock under the cursor: clicking an asteroid opens the mining
// menu (or the stop menu, when this cluster is the beam's target)
// — this click does not fly the ship.
@ -411,10 +399,9 @@ export class GameScene extends Phaser.Scene {
return;
}
// Any other click MOVES the ship — which ends the mining state
// (the beam retracts as the ship goes; a mid-reach arm aborts).
// The state exit is signalled via ship.onStateChange above.
if (this.mining.isActive) this.mining.stop();
// Beam live and the click is OFF the rock: break the beam (it
// retracts while the ship goes) and fly to the click.
if (this.mining.state === 'mining') this.mining.stop();
let aim = { x: pointer.worldX, y: pointer.worldY };
for (const s of this.solids) {
@ -1063,12 +1050,11 @@ export class GameScene extends Phaser.Scene {
/**
* Mining phase changes (Mining onPhase): the scene's share of the
* sequence the ship's STATE, the ship lock, the console calls, the sfx.
* sequence the ship lock, the console calls, the sfx.
*/
onMiningPhase(phase) {
if (phase === 'extending') {
this.ship.stop(); // hold station — the arm needs a steady hull
this.ship.setState('mining', 'mining'); // the arm's sequence owns the ship
this.playSfx('mining'); // the arm powers up
this.hideHint();
// The console call, in the same slot as the discovery toasts —
@ -1080,9 +1066,6 @@ export class GameScene extends Phaser.Scene {
});
} else if (phase === 'stopped') {
this.playSfx('deconstruct'); // the arm pulls back
// Back to normal — a no-op when the ship already left 'mining'
// itself (the player moved it, which ended the sequence).
this.ship.setState('normal', 'mining-ended');
}
// 'mining' = the beam is live — the visual speaks for itself.
}

View File

@ -259,18 +259,6 @@ export class MiningPopup extends Phaser.GameObjects.Container {
}
}
/**
* Does a SCREEN point (canvas coords, top-left origin) sit inside the
* panel's current footprint buttons included? GameScene uses this to
* swallow the very click that just closed the panel through a button:
* that click landed ON the panel, so it was panel business, not a
* world click. Position-based, no timing.
*/
containsScreen(sx, sy) {
const cam = this.scene.cameras.main;
return this.contains(cam.scrollX + sx, cam.scrollY + sy);
}
destroy() {
this.panelG?.destroy();
this.headerText?.destroy();