129 lines
4.3 KiB
JavaScript
129 lines
4.3 KiB
JavaScript
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);
|
||
}
|