122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
import { config } from '../config/Config.js';
|
|
import { MiningBeam } from './MiningBeam.js';
|
|
|
|
/**
|
|
* 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 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
|
|
* 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
|
|
* pattern as TetherField's onChange.
|
|
*/
|
|
export class Mining {
|
|
/**
|
|
* @param {Phaser.Scene} scene
|
|
* @param {object} [o]
|
|
* @param {Function} [o.onPhase] — (phase) => void, phase ∈ 'extending' | 'mining' | 'stopped'
|
|
*/
|
|
constructor(scene, o = {}) {
|
|
this.scene = scene;
|
|
this.onPhase = typeof o.onPhase === 'function' ? o.onPhase : null;
|
|
this.state = 'idle'; // 'idle' | 'extending' | 'mining' | 'retracting'
|
|
this.cluster = null;
|
|
this.member = null;
|
|
this.beam = null;
|
|
this.stateT0 = 0;
|
|
this.armExtendMs = config.get('asteroids.mining.armExtendMs', 1500);
|
|
}
|
|
|
|
/** The ship may not fly: mid-extension OR beam live. */
|
|
get isLocked() {
|
|
return this.state === 'extending' || this.state === 'mining';
|
|
}
|
|
|
|
/**
|
|
* Start mining `cluster`'s rock `member` (from the pop-up's
|
|
* "Mine Asteroids"). Cuts any live/retracting beam first (retarget).
|
|
* Returns false if already extending — the arm is committed.
|
|
*/
|
|
begin(cluster, member) {
|
|
if (this.state === 'extending') return false;
|
|
if (this.state === 'mining' || this.state === 'retracting') {
|
|
this.beam?.destroy();
|
|
this.beam = null;
|
|
}
|
|
this.cluster = cluster;
|
|
this.member = member;
|
|
this.state = 'extending';
|
|
this.stateT0 = this.scene.time.now;
|
|
this._phase('extending');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Stop mining (the pop-up's "Stop Mining", a world click, or ESC):
|
|
* mid-extension → abort; beam live → it retracts (retracting).
|
|
*/
|
|
stop() {
|
|
if (this.state === 'extending') {
|
|
this.state = 'idle';
|
|
this._phase('stopped');
|
|
return;
|
|
}
|
|
if (this.state === 'mining' && this.beam && !this.beam.outRunning) {
|
|
this.state = 'retracting';
|
|
this.beam.beginOut(this.scene.time.now);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Per-frame (GameScene.update, after the clusters have refreshed their
|
|
* rock positions — the beam tracks them). Drives the extending →
|
|
* mining hand-off and reaps a finished retract.
|
|
*/
|
|
update(time, delta) {
|
|
if (this.state === 'extending' && time >= this.stateT0 + this.armExtendMs) {
|
|
this.state = 'mining';
|
|
this.beam = new MiningBeam(this.scene, this);
|
|
this._phase('mining');
|
|
}
|
|
if (this.beam) {
|
|
this.beam.update(time, delta);
|
|
if (this.beam.finished) {
|
|
this.beam.destroy();
|
|
this.beam = null;
|
|
if (this.state === 'retracting') {
|
|
this.state = 'idle';
|
|
this._phase('stopped');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_phase(phase) {
|
|
try {
|
|
this.onPhase?.(phase);
|
|
} catch (err) {
|
|
console.error('[mining] onPhase handler failed', err);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.beam?.destroy();
|
|
this.beam = null;
|
|
this.state = 'idle';
|
|
this.cluster = null;
|
|
this.member = null;
|
|
this.onPhase = null;
|
|
}
|
|
}
|