orbit/js/entities/Star.js

149 lines
5.8 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).
*/
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;
}
}
/**
* 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();
}
}