import Phaser from '../vendor/phaser.js'; import { config } from '../config/Config.js'; import { toColor } from '../utils/Color.js'; const STAR_KEY = '__star'; const MIN_SCALE = 0.4; // smallest star const MAX_SCALE = 1.5; // largest star const MARGIN = 8; // px of starfield beyond the camera view, each side /** * Decorative parallax starfield for infinite space. * * All motion is camera-driven (the camera follows the ship — see * GameScene.updateCamera). Each frame the field is shifted by (1 − p) of * the camera delta, so on screen the stars stream opposite to the * direction of travel, near ("bigger") stars faster than far ones * (p = the star's parallax factor). Ship still → stars still. * * Because the world is unbounded, stars are kept in a window around the * *current camera view* and wrapped back in when they leave it — the * field stays full and evenly distributed no matter how far the ship * flies. * * Tuned by data/game.json → starfield { enabled, count, parallax, colors }. */ export class Starfield { constructor(scene) { this.scene = scene; this.cfg = config.get('game.starfield', {}); const p = this.cfg.parallax; this.parallax = Array.isArray(p) && p.length === 2 ? p : [0.15, 0.85]; this.stars = []; this.lastScrollX = 0; this.lastScrollY = 0; } create() { if (!this.cfg.enabled) return; const scene = this.scene; const width = scene.scale.width; const height = scene.scale.height; const cam = scene.cameras.main; // Tiny 4x4 white dot, shared by every star. if (!scene.textures.exists(STAR_KEY)) { const g = scene.make.graphics({ add: false }); g.fillStyle(0xffffff, 1); g.fillCircle(2, 2, 2); g.generateTexture(STAR_KEY, 4, 4); g.destroy(); } const count = this.cfg.count ?? 200; const colors = (this.cfg.colors ?? ['#ffffff']).map((c) => toColor(c)); const [parMin, parMax] = this.parallax; // Stars start in a window around the current camera view; update() // keeps them wrapped into it as the camera flies. const left = cam.scrollX - MARGIN; const top = cam.scrollY - MARGIN; for (let i = 0; i < count; i++) { const star = scene.add.image( Phaser.Math.FloatBetween(left, left + width + 2 * MARGIN), Phaser.Math.FloatBetween(top, top + height + 2 * MARGIN), STAR_KEY, ); // Bigger stars are "closer" → they parallax more. const s = Phaser.Math.FloatBetween(MIN_SCALE, MAX_SCALE); const t = (s - MIN_SCALE) / (MAX_SCALE - MIN_SCALE); const parallax = Phaser.Math.Linear(parMin, parMax, t); star.parallax = parallax; star .setScale(s) .setAlpha(Phaser.Math.FloatBetween(0.2, 0.85)) .setTint(colors[Phaser.Math.Between(0, colors.length - 1)]) .setDepth(Math.round(2 * t)); // 0 = far … 2 = near (ship draws above, depth 10) this.stars.push(star); } this.lastScrollX = cam.scrollX; this.lastScrollY = cam.scrollY; } /** * Shift the field opposite to the camera motion and wrap it back into * the current view. Call once per frame, after the camera has moved. */ update() { if (!this.cfg.enabled || this.stars.length === 0) return; const cam = this.scene.cameras.main; const dx = cam.scrollX - this.lastScrollX; const dy = cam.scrollY - this.lastScrollY; this.lastScrollX = cam.scrollX; this.lastScrollY = cam.scrollY; if (dx === 0 && dy === 0) return; const spanX = this.scene.scale.width + 2 * MARGIN; const spanY = this.scene.scale.height + 2 * MARGIN; const left = cam.scrollX - MARGIN; const top = cam.scrollY - MARGIN; for (const star of this.stars) { // Move by (1 − p) of the camera delta, so on screen the star drifts // by exactly −p of it: opposite to travel, near stars faster. star.x += dx * (1 - star.parallax); star.y += dy * (1 - star.parallax); // Keep the field full: wrap into the current camera window. star.x = wrapIn(star.x, left, spanX); star.y = wrapIn(star.y, top, spanY); } } destroy() { for (const star of this.stars) star.destroy(); this.stars = []; } } /** Maps any coordinate into [left, left + span) — safe for huge deltas too. */ function wrapIn(v, left, span) { return left + ((((v - left) % span) + span) % span); }