142 lines
4.7 KiB
JavaScript
142 lines
4.7 KiB
JavaScript
/**
|
|
* binary companion star — a distant "other star" wandering slowly.
|
|
*
|
|
* Content layer (two world-space sprites: a bright core + a soft halo),
|
|
* NOT a filter: Canvas-safe and immune to the HUD. It sits FAR away
|
|
* (very low parallax ~0.08, so it barely moves as you fly — a distant
|
|
* light source) and drifts along a slow elliptical arc, seeded per system
|
|
* so each binary's companion wanders its own way. 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).
|
|
*
|
|
* 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
|
|
|
|
/** 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.
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
/** Build the core + halo (idempotent). */
|
|
create() {
|
|
const scene = this.scene;
|
|
if (this.core) return;
|
|
const {
|
|
coreSize = [40, 70],
|
|
haloSize = [120, 200],
|
|
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 starting point (upper third of the view — a star in the sky)
|
|
// and a slow elliptical arc (the "wander").
|
|
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 baseX = w * (0.2 + 0.6 * phase);
|
|
const baseY = h * (0.15 + 0.3 * angle);
|
|
const ampX = w * (0.05 + 0.12 * drift);
|
|
const ampY = h * (0.04 + 0.10 * drift);
|
|
const omega = ((speed[0] + (speed[1] - speed[0]) * drift) / w) * Math.PI * 2; // rad/s (slow)
|
|
|
|
this.core = scene.add
|
|
.image(baseX, baseY, coreKey)
|
|
.setDepth(DEPTH)
|
|
.setTint(this.color)
|
|
.setAlpha(0.95)
|
|
.setBlendMode(Phaser.BlendModes.ADD);
|
|
const cs = coreSize[0] + (coreSize[1] - coreSize[0]) * drift;
|
|
this.core.setScale(cs / 64);
|
|
|
|
this.halo = scene.add
|
|
.image(baseX, baseY, haloKey)
|
|
.setDepth(DEPTH)
|
|
.setTint(this.color)
|
|
.setAlpha(0.8)
|
|
.setBlendMode(Phaser.BlendModes.ADD);
|
|
const hs = haloSize[0] + (haloSize[1] - haloSize[0]) * drift;
|
|
this.halo.setScale(hs / 128);
|
|
|
|
this.baseX = baseX;
|
|
this.baseY = baseY;
|
|
this.ampX = ampX;
|
|
this.ampY = ampY;
|
|
this.omega = omega;
|
|
this.phase0 = angle * Math.PI * 2;
|
|
this.p = parallax;
|
|
}
|
|
|
|
/** Per-frame: slow elliptical wander + camera parallax. */
|
|
update(nowMs, dtMs) {
|
|
if (!this.core) return;
|
|
this.tSec += (dtMs || 0) / 1000;
|
|
const t = this.tSec;
|
|
const a = this.omega * t + this.phase0;
|
|
let x = this.baseX + this.ampX * Math.cos(a);
|
|
let y = this.baseY + this.ampY * Math.sin(a);
|
|
// very low parallax (distant): shifts a little as the camera flies
|
|
const cam = this.scene.cameras.main;
|
|
const camX = cam?.worldView?.x ?? 0;
|
|
const camY = cam?.worldView?.y ?? 0;
|
|
x += (camX - this.scene.scale.width / 2) * this.p;
|
|
y += (camY - this.scene.scale.height / 2) * this.p;
|
|
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;
|
|
}
|
|
}
|