/** * binary companion star — a distant "other star" wandering slowly in the sky. * * Content layer (two world-space sprites: a bright core + a soft halo), * NOT a filter: Canvas-safe and immune to the HUD. It is a FAR, distant * light source: very low parallax (~0.08), so it barely drifts as you * fly, and it wanders a slow elliptical arc seeded per system (each * binary's companion walks its own path). Both sprites use additive * blend — it is a source of light, so it glows, never occludes. * Depth 3 keeps it behind the art (planets 5, ship 10). * * Placement (the fix for "it was off-screen"): the star is ANCHORED TO * THE CAMERA VIEW, not to a fixed box near the system star. Each frame its * view-space position is (home + wander + drift), wrapped back into the * view (Starfield pattern) so it stays in the sky no matter how far the * ship flies — then converted to world space as camera + offset. The * drift trails the camera at (1 − parallax), so on screen it slowly * wanders opposite to travel — a distant star, never a lens sticker. * * Config: data/systems.json → types.binary.effect.wanderer. */ import Phaser from '../vendor/phaser.js'; import { canvasTexture } from '../utils/Textures.js'; import { toColor } from '../utils/Color.js'; const DEPTH = 3; // behind the art, in front of the starfield const MARGIN = 160; // keep the star's center this far inside the view, each side /** A tight, bright core — the star's body. White (tint gives the color). */ function drawCore(ctx, size) { const c = size / 2; const g = ctx.createRadialGradient(c, c, 0, c, c, c); g.addColorStop(0.0, 'rgba(255,255,255,1.0)'); g.addColorStop(0.5, 'rgba(255,255,255,0.9)'); g.addColorStop(1.0, 'rgba(255,255,255,0.0)'); ctx.fillStyle = g; ctx.fillRect(0, 0, size, size); } /** A wide, soft halo — the star's corona. White (tint gives the color). */ function drawHalo(ctx, size) { const c = size / 2; const g = ctx.createRadialGradient(c, c, 0, c, c, c); g.addColorStop(0.0, 'rgba(255,255,255,0.55)'); g.addColorStop(0.4, 'rgba(255,255,255,0.28)'); g.addColorStop(1.0, 'rgba(255,255,255,0.0)'); ctx.fillStyle = g; ctx.fillRect(0, 0, size, size); } /** * The companion star (core + halo) wandering a slow seeded arc, anchored * to the current camera view. * * @param {object} scene Phaser scene * @param {string} color star color (CSS hex) * @param {object} cfg data/systems.json → effect.wanderer * @param {{phase?:number, angle?:number, drift?:number}} fx per-system seeded 0..1 variation */ export class WandererStar { constructor(scene, color, cfg, fx = {}) { this.scene = scene; this.color = toColor(color, 0x9fd0ff); this.cfg = cfg ?? {}; this.fx = fx ?? {}; this.core = null; this.halo = null; this.tSec = 0; this.driftX = 0; this.driftY = 0; this.lastScrollX = 0; this.lastScrollY = 0; } /** Build the core + halo (idempotent). */ create() { const scene = this.scene; if (this.core) return; const { coreSize = [60, 90], haloSize = [200, 300], parallax = 0.08, speed = [3, 8], } = this.cfg; const coreKey = canvasTexture(scene, 'fx-wanderer-core', 64, 64, (ctx) => drawCore(ctx, 64)); const haloKey = canvasTexture(scene, 'fx-wanderer-halo', 128, 128, (ctx) => drawHalo(ctx, 128)); // Seeded HOME point in VIEW space (upper third of the view — a star in // the sky) and a slow elliptical WANDER around it. const fx = this.fx; const phase = fx.phase ?? 0.5; const angle = fx.angle ?? 0.5; const drift = fx.drift ?? 0.5; const w = scene.scale.width; const h = scene.scale.height; const homeX = w * (0.2 + 0.6 * phase); const homeY = h * (0.12 + 0.3 * angle); const ampX = w * (0.05 + 0.12 * drift); const ampY = h * (0.04 + 0.1 * drift); const omega = ((speed[0] + (speed[1] - speed[0]) * drift) / w) * Math.PI * 2; // rad/s (slow) this.homeX = homeX; this.homeY = homeY; this.ampX = ampX; this.ampY = ampY; this.omega = omega; this.phase0 = angle * Math.PI * 2; this.p = parallax; // Start at the home point in world space (camera + view offset). const cam = scene.cameras.main; const x0 = (cam?.scrollX ?? 0) + homeX; const y0 = (cam?.scrollY ?? 0) + homeY; this.core = scene.add .image(x0, y0, coreKey) .setDepth(DEPTH) .setTint(this.color) .setAlpha(1.0) .setBlendMode(Phaser.BlendModes.ADD); const cs = coreSize[0] + (coreSize[1] - coreSize[0]) * drift; this.core.setScale(cs / 64); this.halo = scene.add .image(x0, y0, haloKey) .setDepth(DEPTH) .setTint(this.color) .setAlpha(0.85) .setBlendMode(Phaser.BlendModes.ADD); const hs = haloSize[0] + (haloSize[1] - haloSize[0]) * drift; this.halo.setScale(hs / 128); this.lastScrollX = cam?.scrollX ?? 0; this.lastScrollY = cam?.scrollY ?? 0; } /** * Per-frame: anchor to the current view, add the slow elliptical wander, * trail the camera at (1 − parallax), and wrap back into the view so the * star never escapes the sky. Call after the camera has moved. */ update(nowMs, dtMs) { if (!this.core) return; const cam = this.scene.cameras.main; const camX = cam?.scrollX ?? 0; const camY = cam?.scrollY ?? 0; // The camera delta; the star trails it at (1 − p), so on screen it // drifts by −p of the motion (opposite to travel, like Starfield). const dx = camX - this.lastScrollX; const dy = camY - this.lastScrollY; this.lastScrollX = camX; this.lastScrollY = camY; this.driftX -= dx * this.p; this.driftY -= dy * this.p; this.tSec += (dtMs || 0) / 1000; const a = this.omega * this.tSec + this.phase0; const w = this.scene.scale.width; const h = this.scene.scale.height; // View-space position, wrapped into the view (+ margin) — a distant // star that wanders, but stays in the sky. const vx = wrapIn(this.homeX + this.ampX * Math.cos(a) + this.driftX, -MARGIN, w + 2 * MARGIN); const vy = wrapIn(this.homeY + this.ampY * Math.sin(a) + this.driftY, -MARGIN, h + 2 * MARGIN); // World = camera + view offset. const x = camX + vx; const y = camY + vy; this.core.setPosition(x, y); this.halo.setPosition(x, y); } /** Tear down. */ destroy() { this.core?.destroy(); this.halo?.destroy(); this.core = null; this.halo = null; } } /** Maps any coordinate into [left, left + span) — safe for huge deltas too. */ function wrapIn(v, left, span) { return left + ((((v - left) % span) + span) % span); }