204 lines
7.6 KiB
JavaScript
204 lines
7.6 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 (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();
|
|
}
|
|
}
|