Add Shift+click throttle lock for committed-heading flight
- Ship gains a `thrust` state and `thrustToward()` that commits to the heading toward the click point and holds full throttle past it, until a plain click or stop steers out. - GameScene pointer handler branches on `pointer.event.shiftKey`: Shift+click locks the heading (raw aim), plain click still flies to the clamped stop point. - Mining teardown no longer stomps the ship state when the player has already moved it into `thrust`/`normal`. - Update hint text and add unit tests (tests 7–10) plus a live-scene smoke harness (`?thrusttest=1`) verifying past-click flight, steer-back arrival, stop braking, and mining-exit signaling.
This commit is contained in:
parent
be6e9fc558
commit
75c9f99ecb
|
|
@ -4,7 +4,7 @@
|
|||
"width": 1280,
|
||||
"height": 720,
|
||||
"backgroundColor": "#04060d",
|
||||
"hintText": "click anywhere to fly · click a rock to mine · the tether holds your range",
|
||||
"hintText": "click anywhere to fly · shift+click to hold the heading · click a rock to mine · the tether holds your range",
|
||||
"markerColor": "#41c7ff",
|
||||
"physics": {
|
||||
"default": "arcade"
|
||||
|
|
|
|||
|
|
@ -7,9 +7,14 @@
|
|||
* loop from js/entities/Ship.js, then asserts the flight feel:
|
||||
* arrives and stops, respects maxSpeed, tracks its heading, stops facing
|
||||
* the direction it was traveling, can be re-targeted mid-flight, coasts
|
||||
* to rest, hard-brakes on stop(). Heading assertions account for the
|
||||
* ship's art render offset (ship.artOffset — data/ship.json → artFacing),
|
||||
* which is a presentation concern, not flight behavior.
|
||||
* to rest, hard-brakes on stop() — and the Shift+click THROTTLE LOCK:
|
||||
* thrustToward() commits to a heading and keeps flying it (past the
|
||||
* point, full throttle) until a plain setTarget/stop steers the ship
|
||||
* out of its 'thrust' state.
|
||||
*
|
||||
* Heading assertions account for the ship's art render offset
|
||||
* (ship.artOffset — data/ship.json → artFacing), which is a presentation
|
||||
* concern, not flight behavior.
|
||||
*
|
||||
* The harness integrates acceleration→velocity→position the way the
|
||||
* Arcade physics world does (after the scene's update).
|
||||
|
|
@ -249,5 +254,78 @@ const integrate = (ship) => {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Test 7: the Shift+click THROTTLE LOCK (thrustToward) --------------
|
||||
{
|
||||
// Commits to a heading and keeps flying it — PAST the click point,
|
||||
// full throttle — with no stop in sight.
|
||||
const ship = new Ship(scene, 100, 100);
|
||||
ship.thrustToward(400, 100); // straight right
|
||||
check('thrustToward enters its thrust state with a unit heading',
|
||||
ship.state === 'thrust' && ship.target === null &&
|
||||
Math.abs(Math.hypot(ship.thrustDir.x, ship.thrustDir.y) - 1) < 1e-9 &&
|
||||
ship.thrustDir.x > 0.999 && Math.abs(ship.thrustDir.y) < 1e-9);
|
||||
let maxSpeedSeen = 0;
|
||||
for (let t = 0; t < 120; t++) { // ~2 s
|
||||
ship.update(t * dt, dt);
|
||||
integrate(ship);
|
||||
maxSpeedSeen = Math.max(maxSpeedSeen, ship.body.velocity.length());
|
||||
}
|
||||
const passed = ship.x - 400;
|
||||
check(`keeps flying the heading PAST the click point (x=${ship.x.toFixed(1)}, passed by ${passed.toFixed(1)}px)`,
|
||||
passed > 100 && ship.y < 101 && ship.y > 99);
|
||||
check(`throttle-locked speed is up and capped (max=${maxSpeedSeen.toFixed(1)} / cap ${ship.maxSpeed})`,
|
||||
maxSpeedSeen > 200 && maxSpeedSeen <= ship.maxSpeed * 1.05 + 0.001);
|
||||
const hd = wrap(ship.rotation - (ship.artOffset || 0));
|
||||
check(`stays on the committed heading (heading=${hd.toFixed(3)} rad, want ~0)`, Math.abs(hd) < 0.1);
|
||||
check('still holding the throttle (state=thrust, dir kept)',
|
||||
ship.state === 'thrust' && ship.thrustDir !== null);
|
||||
}
|
||||
|
||||
// --- Test 8: a plain click (setTarget) steers out of the hold ------------
|
||||
{
|
||||
const ship = new Ship(scene, 100, 100);
|
||||
ship.thrustToward(2000, 100);
|
||||
for (let t = 0; t < 30; t++) { ship.update(t * dt, dt); integrate(ship); }
|
||||
const target = { x: 700, y: 500 };
|
||||
ship.setTarget(target.x, target.y);
|
||||
check('setTarget drops the thrust hold and takes the ship to the new point',
|
||||
ship.state === 'normal' && ship.thrustDir === null && ship.target !== null);
|
||||
let arrived = false;
|
||||
for (let t = 0; t < 60 * 30; t++) {
|
||||
ship.update(t * dt, dt);
|
||||
integrate(ship);
|
||||
if (ship.target === null && ship.body.velocity.length() === 0) { arrived = true; break; }
|
||||
}
|
||||
const d = Math.hypot(ship.x - target.x, ship.y - target.y);
|
||||
check(`arrives at the steered-to point and stops (dist=${d.toFixed(2)})`, arrived && d <= 8.5);
|
||||
}
|
||||
|
||||
// --- Test 9: stop() brakes the hold dead ---------------------------------
|
||||
{
|
||||
const ship = new Ship(scene, 300, 300);
|
||||
ship.thrustToward(900, 300);
|
||||
for (let t = 0; t < 30; t++) { ship.update(t * dt, dt); integrate(ship); }
|
||||
ship.stop();
|
||||
check('stop() clears the hold and kills the velocity',
|
||||
ship.thrustDir === null && ship.target === null && ship.body.velocity.length() === 0);
|
||||
for (let t = 0; t < 60; t++) { ship.update(t * dt, dt); integrate(ship); }
|
||||
check('after stop() the ship coasts to rest (no re-thrust)',
|
||||
ship.body.velocity.length() < 1);
|
||||
}
|
||||
|
||||
// --- Test 10: thrusting out of 'mining' signals the scene -----------------
|
||||
{
|
||||
const ship = new Ship(scene, 0, 0);
|
||||
const changes = [];
|
||||
ship.onStateChange = (next, prev) => changes.push([next, prev]);
|
||||
ship.setState('mining', 'mining');
|
||||
const baseline = changes.length; // the setup call above already fired once
|
||||
ship.thrustToward(500, 500);
|
||||
check('thrustToward from the mining state fires the scene teardown signal',
|
||||
ship.state === 'thrust' && changes.length === baseline + 1 &&
|
||||
changes[changes.length - 1][0] === 'thrust' &&
|
||||
changes[changes.length - 1][1] === 'mining');
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? '\nAll ship behavior tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
* ?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
|
||||
|
|
@ -101,6 +105,91 @@ if (params && params.get('report') === '1') {
|
|||
}, 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, and (b) a plain click steers it to a stop there.
|
||||
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;
|
||||
report(thrustOk && steerOk, 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(() => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { config } from '../config/Config.js';
|
|||
import { toColor } from '../utils/Color.js';
|
||||
|
||||
/**
|
||||
* The player's ship: an arcade-physics sprite that flies to wherever you click.
|
||||
* The player's ship: an arcade-physics sprite that flies to wherever you click
|
||||
* — and, with the Shift+click THROTTLE LOCK (thrustToward, the 'thrust'
|
||||
* state), keeps flying a committed heading until the player steers it
|
||||
* somewhere else.
|
||||
*
|
||||
* Art: the spritesheet in data/ship.json → `texture` (frameWidth×frameHeight
|
||||
* frames; `frame` picks which one — frame 0 is the starter ship). The art in
|
||||
|
|
@ -126,24 +129,31 @@ export class Ship extends Phaser.Physics.Arcade.Sprite {
|
|||
this.radius = worldSize / 2;
|
||||
|
||||
this.target = null;
|
||||
// THROTTLE LOCK (the Shift+click hold — js/scenes/GameScene.js): the
|
||||
// committed unit heading the ship keeps flying while it is in its
|
||||
// 'thrust' state — past the original click point and on. Cleared by
|
||||
// setTarget() / stop() the instant the player steers the ship back.
|
||||
this.thrustDir = 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)
|
||||
// default: free to fly wherever the player sends it. 'thrust' = the
|
||||
// Shift+click throttle lock (full-throttle along the committed
|
||||
// heading, see thrustToward + update below). '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.state = 'normal'; // 'normal' | 'thrust' | '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.
|
||||
* Change the ship's state ('normal' | 'thrust' | '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
|
||||
*/
|
||||
|
|
@ -189,6 +199,7 @@ export class Ship extends Phaser.Physics.Arcade.Sprite {
|
|||
/** Set the destination to fly to (world coordinates). */
|
||||
setTarget(x, y) {
|
||||
this.target = { x, y };
|
||||
this.thrustDir = null; // steering somewhere else ends the Shift+click hold
|
||||
// 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
|
||||
|
|
@ -196,9 +207,38 @@ export class Ship extends Phaser.Physics.Arcade.Sprite {
|
|||
this.setState('normal', 'move');
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit to the heading toward (x, y) and HOLD IT (the Shift+click
|
||||
* throttle lock): the ship flies that way at full throttle — PAST the
|
||||
* point and on — for as long as it stays in its 'thrust' state. A plain
|
||||
* setTarget() flies somewhere else and stops there (ending the hold);
|
||||
* stop() brakes it dead. The direction is ship → (x, y); a degenerate
|
||||
* point (right on the hull) keeps the heading it already has.
|
||||
*/
|
||||
thrustToward(x, y) {
|
||||
const dx = x - this.x;
|
||||
const dy = y - this.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
let nx;
|
||||
let ny;
|
||||
if (d > 0.5) {
|
||||
nx = dx / d;
|
||||
ny = dy / d;
|
||||
} else {
|
||||
// The ship IS the point: keep the heading it already has.
|
||||
const h = this.rotation - this.artOffset;
|
||||
nx = Math.cos(h);
|
||||
ny = Math.sin(h);
|
||||
}
|
||||
this.target = null;
|
||||
this.thrustDir = { x: nx, y: ny };
|
||||
this.setState('thrust', 'move');
|
||||
}
|
||||
|
||||
/** Stop steering and brake immediately. */
|
||||
stop() {
|
||||
this.target = null;
|
||||
this.thrustDir = null;
|
||||
this.body.acceleration.set(0, 0);
|
||||
this.body.velocity.set(0, 0);
|
||||
}
|
||||
|
|
@ -208,6 +248,39 @@ export class Ship extends Phaser.Physics.Arcade.Sprite {
|
|||
if (!body) return;
|
||||
const dt = Math.min(delta, 64) / 1000;
|
||||
|
||||
// THROTTLE LOCK (the Shift+click hold): full throttle along the
|
||||
// committed heading — no arrival check, no stopping — until the
|
||||
// player steers the ship somewhere else (setTarget → 'normal' + a
|
||||
// target, which takes over below) or brakes it (stop() clears
|
||||
// thrustDir, dropping the ship to the coast branch).
|
||||
if (this.state === 'thrust' && this.thrustDir) {
|
||||
body.acceleration.set(
|
||||
this.thrustDir.x * this.thrust,
|
||||
this.thrustDir.y * this.thrust,
|
||||
);
|
||||
|
||||
// The same drag + hard speed cap as the click-to-fly steering.
|
||||
body.velocity.scale(Math.max(0, 1 - this.drag * dt));
|
||||
const v = body.velocity.length();
|
||||
if (v > this.maxSpeed) body.velocity.scale(this.maxSpeed / v);
|
||||
|
||||
// Heading follows the actual direction of motion (so it never yaws
|
||||
// "backwards" while turning); standing still, it points down the
|
||||
// committed heading. Sprite rotation = heading + artOffset (the
|
||||
// sheet art may not face east).
|
||||
const speed = body.velocity.length();
|
||||
const wanted =
|
||||
(speed > 10
|
||||
? Phaser.Math.Angle.Wrap(Math.atan2(body.velocity.y, body.velocity.x))
|
||||
: Phaser.Math.Angle.Wrap(
|
||||
Math.atan2(this.thrustDir.y, this.thrustDir.x),
|
||||
)) + this.artOffset;
|
||||
const current = this.rotation;
|
||||
const step = Math.min(Math.abs(wanted - current), this.rotSpeed * dt);
|
||||
this.rotation = Phaser.Math.Angle.RotateTo(current, wanted, step);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.target) {
|
||||
const dx = this.target.x - this.x;
|
||||
const dy = this.target.y - this.y;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ const HUD_AUTO_COLLAPSE_MS = 10000;
|
|||
* The game world (v0.3: the current system — the central body at the
|
||||
* origin (the player's home world in the starting system, the system's
|
||||
* star in every other one) plus the system's other worlds scattered
|
||||
* around it, in open space). Click anywhere to fly there.
|
||||
* around it, in open space). Click anywhere to fly there — holding
|
||||
* SHIFT on the click THROTTLE-LOCKS the ship: it commits to that heading
|
||||
* and keeps flying the way until you steer it somewhere else.
|
||||
*
|
||||
* Discovery: come within discovery distance (data/game.json) of a world's
|
||||
* edge and it is DISCOVERED (state in this.discovery). Discovered worlds
|
||||
|
|
@ -744,9 +746,14 @@ export class GameScene extends Phaser.Scene {
|
|||
// it does not touch the mining state (no move ⇒ a live beam keeps
|
||||
// running). Any OTHER click moves the ship — which ends the mining
|
||||
// state (the beam retracts as the ship goes; a mid-reach arm
|
||||
// aborts). A click BEYOND the player's tether range clamps to the
|
||||
// union boundary — the target marker lands on the barrier line
|
||||
// itself.
|
||||
// aborts). Holding SHIFT on that click = THROTTLE LOCK: the ship
|
||||
// commits to the heading toward the click point and keeps flying it
|
||||
// — past the point, full throttle — until the player steers it
|
||||
// somewhere else (a plain click flies there and stops; another
|
||||
// Shift+click re-aims the heading). A click BEYOND the player's
|
||||
// tether range clamps to the union boundary — the target marker
|
||||
// lands on the barrier line itself (and while throttle-locked, the
|
||||
// ship drives into the line instead of resting on it).
|
||||
this.input.on('pointerdown', (pointer) => {
|
||||
// A jump clip is in flight — it owns the whole screen. A DOUBLE-CLICK
|
||||
// (two quick presses) skips the rest of the clip (the same window as
|
||||
|
|
@ -890,11 +897,29 @@ export class GameScene extends Phaser.Scene {
|
|||
// The state exit is signalled via ship.onStateChange above.
|
||||
if (this.mining.isActive) this.mining.stop();
|
||||
|
||||
// The stop point a plain click would have taken: off every solid's
|
||||
// keep-out rim, clamped to the player's tether range.
|
||||
let aim = { x: pointer.worldX, y: pointer.worldY };
|
||||
for (const s of this.solids) {
|
||||
aim = s.aimPoint(aim.x, aim.y, this.ship.radius);
|
||||
}
|
||||
aim = this.tetherField.clampPoint(aim.x, aim.y);
|
||||
|
||||
// SHIFT HELD = THROTTLE LOCK: the ship commits to the heading
|
||||
// toward the click point (raw — the direction the player pointed,
|
||||
// not the stop point) and keeps flying that way — past the point
|
||||
// and on, full throttle — until the player steers it somewhere
|
||||
// else. The world still owns the motion: solids and the tether
|
||||
// rim hold it back as it drives into them (postupdate constraint).
|
||||
// (pointer.event is the DOM pointerdown — the press that just
|
||||
// landed, with its modifier keys.)
|
||||
if (pointer.event?.shiftKey) {
|
||||
this.showTargetMarker(aim.x, aim.y); // the aim feedback
|
||||
this.ship.thrustToward(pointer.worldX, pointer.worldY);
|
||||
this.hideHint();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showTargetMarker(aim.x, aim.y);
|
||||
this.ship.setTarget(aim.x, aim.y);
|
||||
this.hideHint();
|
||||
|
|
@ -2003,9 +2028,12 @@ export class GameScene extends Phaser.Scene {
|
|||
this.setMiningLoop(true); // the beam is live — the hum runs until the sequence ends
|
||||
} else if (phase === 'stopped') {
|
||||
this.setMiningLoop(false); // the arm pulls back — the hum ends with it (no one-shot sound)
|
||||
// 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');
|
||||
// Back to normal — but ONLY when the ship is still in its mining
|
||||
// state (the beam-out / full-hold ending: the ship is parked). When
|
||||
// the player moved the ship out of 'mining' (a click-to-fly, or a
|
||||
// Shift+click throttle lock), that movement already owns the
|
||||
// ship's state ('normal' or 'thrust') — don't stomp it.
|
||||
if (this.ship.state === 'mining') this.ship.setState('normal', 'mining-ended');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue