orbit/js/entities/Station.js

201 lines
7.4 KiB
JavaScript

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 while the ship flies to its
* edge (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() {
this.removeChildren();
super.destroy();
}
}