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. * * 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; * retracting the beam is pulling back rock → ship; the ship is already * BACK TO 'normal' (FREE) — 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 arm's sequence is live: mid-extension OR beam live. */ get isActive() { 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) { // 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); 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; } }