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; } }