157 lines
6.1 KiB
JavaScript
157 lines
6.1 KiB
JavaScript
import Phaser from '../vendor/phaser.js';
|
|
import { config } from '../config/Config.js';
|
|
import { toColor } from '../utils/Color.js';
|
|
import { Planet } from './Planet.js';
|
|
|
|
/**
|
|
* A STAR — the central body of every NON-HOME system (content.star from
|
|
* the generator: name + spectral class). The starting system's central
|
|
* body is the player's home world (js/entities/Planet.js) and only there
|
|
* — "Home World" exists in exactly one system in the galaxy.
|
|
*
|
|
* Solid like a planet: the keep-out circle is the disc rim (half the
|
|
* configured size — the halo is visual only), the ship keeps
|
|
* `planets.shipClearance` px (edge-to-edge) off it and can never move
|
|
* through it (the same plain-circle rule, GameScene.solids). It is
|
|
* discoverable (the 'home' NAV point of the chart — every system has a
|
|
* central body) but NOT a comms target: there is no surface to land on.
|
|
*
|
|
* The look is procedural (data/planets.json → star): an opaque body —
|
|
* a white core melting into the spectral class's hue — plus a soft halo,
|
|
* band-filled with Graphics (same pattern as js/visuals/Starfield.js, so
|
|
* no canvas API and no art asset). One texture per spectral class,
|
|
* generated on first use.
|
|
*/
|
|
export class Star extends Phaser.GameObjects.Image {
|
|
/**
|
|
* @param {Phaser.Scene} scene
|
|
* @param {number} x — world x of the star's center
|
|
* @param {number} y — world y
|
|
* @param {object} [star={}] — content.star ({ name, class })
|
|
*/
|
|
constructor(scene, x, y, star = {}) {
|
|
const cls = String(star?.class ?? 'G').toUpperCase();
|
|
const key = `star-${cls.toLowerCase()}`;
|
|
if (!scene.textures.exists(key)) Star.makeTexture(scene, key, cls);
|
|
super(scene, x, y, key);
|
|
scene.add.existing(this);
|
|
|
|
this.starClass = cls;
|
|
this.name = star?.name ?? ''; // display name (the scene stamps discoveryName)
|
|
|
|
// The solid disc: diameter from data/planets.json → star.size (the
|
|
// halo extends past the rim but is not part of the keep-out).
|
|
this.size = Math.max(64, Number(config.get('planets.star.size', 1536)) || 1536);
|
|
this.setScale(1);
|
|
this.radius = this.size / 2;
|
|
// Edge-to-edge gap the ship may close in on the rim (never less).
|
|
this.clearance = config.get('planets.shipClearance', 50);
|
|
}
|
|
|
|
/** Minimum allowed center-to-center distance for a ship of `shipRadius`. */
|
|
minCenterDistance(shipRadius = 0) {
|
|
return this.radius + this.clearance + shipRadius;
|
|
}
|
|
|
|
/**
|
|
* A world point `gap` px (edge-to-edge) off this star's rim at `angle`
|
|
* radians — e.g. a spawn or approach point.
|
|
*/
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* A world point the ship may be sent to: a point inside the keep-out
|
|
* circle (e.g. a click on the star itself, or through it) is projected
|
|
* out onto the rim, along the ray from the center — so the ship always
|
|
* has a reachable destination and is never told to go inside.
|
|
*/
|
|
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 }; // dead center: +x
|
|
return { x: this.x + (dx / dist) * minDist, y: this.y + (dy / dist) * minDist };
|
|
}
|
|
|
|
/**
|
|
* Keep a ship out of the star: same plain-circle rule as Planet —
|
|
* push the center out to `minCenterDistance`, strip the inward part of
|
|
* velocity and acceleration (the tangential part slides along the rim).
|
|
* Returns true when it actually touched the ship (contact — see
|
|
* Planet.constrainShip).
|
|
*/
|
|
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,
|
|
);
|
|
const touched =
|
|
r.x !== ship.x || r.y !== ship.y ||
|
|
r.vx !== body.velocity.x || r.vy !== body.velocity.y ||
|
|
(body.acceleration &&
|
|
(r.ax !== body.acceleration.x || r.ay !== body.acceleration.y));
|
|
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;
|
|
}
|
|
return touched;
|
|
}
|
|
|
|
/**
|
|
* The star's texture for one spectral class: an opaque body (a white
|
|
* core melting into the class hue, data/planets.json →
|
|
* star.classColor) + a soft halo (star.glow). Band-filled circles,
|
|
* outer → inner, via Graphics — no canvas API, one texture per class.
|
|
*/
|
|
static makeTexture(scene, key, cls) {
|
|
const coreR = Math.max(32, Math.floor(config.get('planets.star.size', 1536) / 2));
|
|
const glowFactor = Math.max(1, Number(config.get('planets.star.glow.radiusFactor', 1.55)) || 1.55);
|
|
const glowAlpha = Math.min(1, Math.max(0, Number(config.get('planets.star.glow.alpha', 0.5)) || 0));
|
|
const color = toColor(config.get(`planets.star.classColor.${cls}`, '#ffe9b0'));
|
|
|
|
const R = Math.ceil(coreR * glowFactor);
|
|
const T = Math.ceil(R * 2);
|
|
const c = T / 2;
|
|
|
|
// Blend the class hue toward white (t = 1 → white).
|
|
const mix = (t) => {
|
|
const ch = (sh) => {
|
|
const v = (color >> sh) & 255;
|
|
return Math.round(v + (255 - v) * t);
|
|
};
|
|
return (ch(16) << 16) | (ch(8) << 8) | ch(0);
|
|
};
|
|
|
|
const g = scene.make.graphics({ add: false });
|
|
// Halo: rim → edge, alpha growing to the rim (0 at the edge).
|
|
const H = 14;
|
|
for (let i = 0; i < H; i++) {
|
|
const t = i / (H - 1); // 0 = edge, 1 = rim
|
|
g.fillStyle(color, glowAlpha * t * t);
|
|
g.fillCircle(c, c, R - (R - coreR) * t);
|
|
}
|
|
// Body: rim → core, opaque; the hue melts into a white core.
|
|
const B = 24;
|
|
for (let i = 0; i < B; i++) {
|
|
const t = i / (B - 1); // 0 = rim, 1 = core
|
|
g.fillStyle(mix(Math.pow(t, 1.4)), 1);
|
|
g.fillCircle(c, c, coreR * (1 - t * 0.999));
|
|
}
|
|
g.generateTexture(key, T, T);
|
|
g.destroy();
|
|
}
|
|
}
|