orbit/js/scenes/GameScene.js

165 lines
5.4 KiB
JavaScript

import Phaser from '../vendor/phaser.js';
import { config } from '../config/Config.js';
import { toColor } from '../utils/Color.js';
import { Rng } from '../utils/Rng.js';
import { Galaxy } from '../galaxy/Galaxy.js';
import { Ship } from '../entities/Ship.js';
import { Starfield } from '../visuals/Starfield.js';
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
/**
* The game world (v0.2: one ship in the current system's open space).
* Click anywhere to fly there.
*/
export class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
}
create() {
// Camera: smoothly follows the ship (updateCamera below). This motion
// is what drives the parallax starfield — ship flies, view trails.
this.cameraFollowShip = config.get('game.camera.followShip', true);
this.cameraFollowRate = config.get('game.camera.followRate', 3.0); // 1/s
// The ship — spawns at the world origin in open, unbounded space.
this.ship = new Ship(this, 0, 0);
this.ship.setDepth(10);
// We are, after all, in a system. Show the player which one.
this.createSystemHud();
// Center the camera on the ship from the very first frame.
this.cameras.main.setScroll(-this.scale.width / 2, -this.scale.height / 2);
// Background (stars are placed around the current camera view).
this.starfield = new Starfield(this);
this.starfield.create();
// Hint
this.hint = this.add
.text(this.scale.width / 2, this.scale.height - 26, config.get('game.hintText', ''), {
fontFamily: FONT_FALLBACK,
fontSize: '14px',
color: '#54608a',
})
.setOrigin(0.5)
.setScrollFactor(0); // UI: pinned to the screen, not the world
// Input: click = fly there
this.input.on('pointerdown', (pointer) => {
this.showTargetMarker(pointer.worldX, pointer.worldY);
this.ship.setTarget(pointer.worldX, pointer.worldY);
this.hideHint();
});
}
/**
* Top-left HUD: the current system's name and identity.
*
* The galaxy comes from the shared registry (built by the menu from the
* chosen seed). Dev boots that skip the menu (dev/test-game.html) get a
* fresh dev galaxy so the scene always works standalone.
*
* The system's CONTENTS are generated here, on arrival — the first touch
* of the lazy level-2 generation. (Roster/positions were fixed at the
* menu's New Game click; this is just the "build the room" part.)
*/
createSystemHud() {
this.galaxy = this.registry.get('galaxy') ?? null;
if (!this.galaxy) {
const seed = Rng.randomSeedString(8);
this.galaxy = Galaxy.create(seed);
this.registry.set('galaxy', this.galaxy);
this.registry.set('seed', seed);
console.warn(`[orbit] no galaxy in the registry — generated a dev galaxy (seed "${seed}")`);
}
const current = this.galaxy.currentSystem();
const content = this.galaxy.ensureContent(current.id);
const typeDef = this.galaxy.typeDefs?.[current.type] ?? {};
const fam = FONT_FALLBACK;
this.add
.text(16, 14, content.name, {
fontFamily: fam,
fontSize: '17px',
fontStyle: 'bold',
color: toColor(typeDef.theme?.color ?? '#9fb4e8'),
})
.setOrigin(0, 0)
.setScrollFactor(0) // UI: pinned to the screen, not the world
.setDepth(30);
const label = typeDef.label ?? current.type;
this.add
.text(16, 38, `${label} \u00b7 star ${content.star.class} \u00b7 ${content.planets.length} planets \u00b7 seed ${this.galaxy.seed}`, {
fontFamily: fam,
fontSize: '12px',
color: '#8fa0c9',
})
.setOrigin(0, 0)
.setScrollFactor(0)
.setDepth(30);
}
update(_time, delta) {
this.ship.update(_time, delta);
this.updateCamera(delta);
this.starfield.update(); // after the camera, so it sees this frame's motion
}
/**
* The camera chases the ship with a frame-rate-independent ease:
* scroll += (shipCenter - scroll) * (1 - e^(-rate·dt))
* While the ship flies, the camera trails it and the starfield streams
* past in the opposite direction (real parallax). When the ship stops,
* the camera keeps easing until it is centered on the ship again — a
* slow recenter (≈1.5 s at followRate 3) with the stars parallaxing
* along for the ride.
*/
updateCamera(delta) {
if (!this.cameraFollowShip || !this.ship) return;
const rate = this.cameraFollowRate;
if (!(rate > 0)) return;
const cam = this.cameras.main;
const k = 1 - Math.exp(-rate * (Math.min(delta, 64) / 1000));
cam.setScroll(
Phaser.Math.Linear(cam.scrollX, this.ship.x - this.scale.width / 2, k),
Phaser.Math.Linear(cam.scrollY, this.ship.y - this.scale.height / 2, k),
);
}
showTargetMarker(x, y) {
const color = toColor(config.get('game.markerColor', '#41c7ff'));
const marker = this.add.circle(x, y, 10, color, 0.8).setDepth(5);
this.tweens.add({
targets: marker,
scale: 2.4,
alpha: 0,
duration: 450,
ease: 'Sine.easeOut',
onComplete: () => marker.destroy(),
});
}
hideHint() {
if (!this.hint || !this.hint.active) return;
this.tweens.add({
targets: this.hint,
alpha: 0,
duration: 400,
onComplete: () => {
this.hint.destroy();
this.hint = null;
},
});
}
shutdown() {
this.starfield?.destroy();
}
}