import Phaser from '../vendor/phaser.js'; import { config } from '../config/Config.js'; import { Planet } from './Planet.js'; /** * A SPACE STATION — a free-space settlement (SystemGenerator: * `anchor.type === 'space'`, kinds `deepSpaceStation` / `waypoint`) * rendered as a world object: a weathered hub + slowly turning ring + * solar wings (deep-space station) or a small nav beacon (waypoint). * * Solid like a planet (the ship keeps its clearance — the same plain * circle rule, GameScene.solids), discoverable (a compass arrow + the * discovery toast, at the scale of its keepout), and a comms target — * click it and the comms panel opens there (the click is NOT a * fly-here — the ship stays put; GameScene.openCommsPanel). * * update(time) turns the ring and breathes the beacon — driven by * GameScene.update, like the asteroid clusters. */ export class Station extends Phaser.GameObjects.Container { /** * @param {Phaser.Scene} scene * @param {object} settlement — the content record: { id, kind, name, * anchor: { type: 'space' }, x, y, population, owner } * @param {object} [o] { depth } */ constructor(scene, settlement, o = {}) { super(scene, settlement.x, settlement.y); this.scene.add.existing(this); // v4: new'd containers are not on the display list this.settlement = settlement; this.kind = settlement.kind ?? 'deepSpaceStation'; // Discovery bookkeeping (id = the settlement id — the rep key). this.discoveryId = settlement.id; this.discoveryName = settlement.name; this.size = config.get(`stations.kinds.${this.kind}.size`, this.kind === 'waypoint' ? 46 : 108); this.radius = this.size; // the keepout circle's radius this.bound = this.size; // the discovery radius (the compass/toast scale) this.clearance = config.get('stations.shipClearance', 50); this.ringSpeed = config.get(`stations.kinds.${this.kind}.ringSpeed`, 0.08); this.ringBody = null; this.setDepth(o.depth ?? 5); this.build(); } /** Procedural build — the same worn hardware every game. */ build() { const scene = this.scene; const S = this.size; const g = scene.add.graphics(); this.add(g); if (this.kind === 'waypoint') { // A small nav beacon: a base, a mast, a breathing light. g.fillStyle(0x4a3a2c, 1); g.fillRect(-S * 0.22, S * 0.28, S * 0.44, S * 0.16); // the base g.fillStyle(0x57493a, 1); g.fillRect(-S * 0.06, -S * 0.45, S * 0.12, S * 0.78); // the mast g.fillStyle(0x6e3a1f, 0.5); // a rust band g.fillRect(-S * 0.06, 0, S * 0.12, S * 0.18); g.lineStyle(1.5, 0x2c2118, 1); g.strokeCircle(0, -S * 0.5, S * 0.13); g.fillStyle(0x3a2f26, 1); g.fillCircle(0, -S * 0.5, S * 0.1); } else { // ---- Deep-space station ----------------------------------------- const wingX0 = S * 0.24; const wingX1 = S * 0.85; const wingH = S * 0.18; // Solar wings — weathered blue-gray panels in a rusty frame. for (const dir of [-1, 1]) { const x0 = dir === 1 ? wingX0 : -wingX1; const w = wingX1 - wingX0; g.lineStyle(3, 0x57493a, 1); g.lineBetween(dir * S * 0.1, 0, dir * wingX0, 0); // the truss g.fillStyle(0x3f5468, 1); g.fillRect(x0, -wingH / 2, w, wingH); g.fillStyle(0x6e3a1f, 0.35); // rust streak on the outer edge g.fillRect(dir === 1 ? wingX1 - 6 : -wingX1, -wingH / 2, 6, wingH); g.lineStyle(2, 0x22303e, 1); g.strokeRect(x0, -wingH / 2, w, wingH); g.lineStyle(1, 0x22303e, 0.9); for (let i = 1; i <= 4; i++) { const x = x0 + (w * i) / 5; g.lineBetween(x, -wingH / 2, x, wingH / 2); } } // The rotating ring (spokes + pods) — its own container so // update() can spin it. this.ringBody = new Phaser.GameObjects.Container(scene, 0, 0); const rg = scene.add.graphics(); const ringR = S * 0.48; rg.lineStyle(S * 0.065, 0x6a5a48, 0.95); rg.strokeCircle(0, 0, ringR); rg.lineStyle(S * 0.03, 0x57493a, 1); for (let i = 0; i < 3; i++) { const a = (i / 3) * Math.PI * 2; rg.lineBetween( Math.cos(a) * S * 0.12, Math.sin(a) * S * 0.12, Math.cos(a) * ringR, Math.sin(a) * ringR, ); rg.fillStyle(0x8a6a48, 1); rg.fillCircle(Math.cos(a) * ringR, Math.sin(a) * ringR, S * 0.037); } this.ringBody.add(rg); this.add(this.ringBody); // The hub (over the ring): a weathered core + rust. const hubR = S * 0.205; g.fillStyle(0x57493a, 1); g.fillCircle(0, 0, hubR); g.lineStyle(2, 0x2c2118, 1); g.strokeCircle(0, 0, hubR); g.fillStyle(0x3a2f26, 1); g.fillCircle(0, 0, hubR * 0.55); g.fillStyle(0x8a6a48, 1); g.fillCircle(0, 0, hubR * 0.22); g.fillStyle(0x6e3a1f, 0.5); g.fillCircle(hubR * 0.5, -hubR * 0.35, 3); g.fillCircle(-hubR * 0.4, hubR * 0.5, 2.5); } // The beacon light (both kinds) — the thing that says "occupied". const hy = this.kind === 'waypoint' ? -S * 0.5 : -S * 0.26; const hue = this.kind === 'waypoint' ? 0xa8ffc4 : 0xff7a5c; const glow = this.kind === 'waypoint' ? 0x7dffb0 : 0xff5a3c; this.beaconGlow = scene.add.circle(0, hy, this.kind === 'waypoint' ? S * 0.24 : 7, glow, 0.2); this.beacon = scene.add.circle(0, hy, this.kind === 'waypoint' ? S * 0.07 : 2.6, hue, 1); this.add([this.beaconGlow, this.beacon]); } /** The ring turns; the beacon breathes. (GameScene.update drives this.) */ update(time) { const t = time / 1000; if (this.ringBody) this.ringBody.rotation = t * this.ringSpeed; const pulse = 0.5 + 0.5 * Math.sin(t * 2.6); this.beaconGlow.setAlpha(0.12 + 0.3 * pulse); this.beacon.setAlpha(0.6 + 0.4 * pulse); } // ---- SOLID (the same contract as Planet / AsteroidCluster) --------- minCenterDistance(shipRadius = 0) { return this.radius + this.clearance + shipRadius; } /** A point `gap` past the surface toward (wx, wy) — the ship's approach stop. */ edgePoint(angle, gap, shipRadius = 0) { const d = this.radius + gap + shipRadius; return { x: this.x + Math.cos(angle) * d, y: this.y + Math.sin(angle) * d, }; } /** Clamp a target to at least clearance outside the surface (the ship can stop anywhere at the edge). */ aimPoint(wx, wy, shipRadius = 0) { const minDist = this.minCenterDistance(shipRadius); const dx = wx - this.x; const dy = wy - this.y; const dist = Math.hypot(dx, dy); if (dist >= minDist) return { x: wx, y: wy }; if (dist === 0) return { x: this.x + minDist, y: this.y }; return { x: this.x + (dx / dist) * minDist, y: this.y + (dy / dist) * minDist, }; } /** Hard constraint — push the ship outside the keepout circle (position + velocity + acceleration). */ constrainShip(ship, shipRadius = 0) { const body = ship.body; const r = Planet.resolve( this.x, this.y, this.minCenterDistance(shipRadius), ship.x, ship.y, body.velocity.x, body.velocity.y, body.acceleration ? body.acceleration.x : 0, body.acceleration ? body.acceleration.y : 0, ); ship.x = r.x; ship.y = r.y; body.velocity.x = r.vx; body.velocity.y = r.vy; if (body.acceleration) { body.acceleration.x = r.ax; body.acceleration.y = r.ay; } } destroy() { // v4: Container no longer has removeChildren() — removeAll(true) is // the equivalent (detach the art and destroy it); v4's super.destroy() // would do the same for whatever was still attached. this.removeAll(true); super.destroy(); } }