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(); } }