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 { formatSystemReport } from '../galaxy/SystemReport.js'; import { Ship } from '../entities/Ship.js'; import { Planet } from '../entities/Planet.js'; import { Starfield } from '../visuals/Starfield.js'; const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif"; /** * The game world (v0.3: the home planet — the player's Terran world — * in the current system's open space). * Click anywhere to fly there. */ export class GameScene extends Phaser.Scene { constructor() { super({ key: 'GameScene' }); } preload() { // The planet spritesheet: frameWidth×frameHeight frames; the first // frames are Terran worlds (see data/planets.json → frames). this.load.spritesheet( Planet.TEXTURE_KEY, config.get('planets.texture', 'assets/images/planets.png'), { frameWidth: config.get('planets.frameWidth', 1024), frameHeight: config.get('planets.frameHeight', 1024), }, ); } 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 // We are, after all, in a system. Show the player which one. (This // also establishes the galaxy — and its seed — for what follows.) this.createSystemHud(); // The home planet — the player's Terran world, always present in the // system they start in. It sits at the world origin. Which Terran face // it shows is a seed-deterministic pick from the terran pool, so the // same galaxy always yields the same home world. const homeName = config.get('planets.homePlanet', 'terran'); const homeRng = Rng.derive(this.galaxy.seed, 'planet', 'home'); this.planet = new Planet(this, 0, 0, Planet.frameFor(homeName, homeRng), homeName); this.planet.setDepth(5); // above the starfield (depths 0–2), below the ship (10) // The ship — a short hop (~150 px, edge-to-edge) from the home world's // rim, in a seed-derived direction: same galaxy ⇒ same start. this.ship = new Ship(this, 0, 0); this.ship.setDepth(10); const spawn = this.planet.edgePoint( Rng.derive(this.galaxy.seed, 'spawn', 'ship').range(0, Math.PI * 2), config.get('planets.spawnDistanceFromEdge', 150), this.ship.radius, ); this.ship.setPosition(spawn.x, spawn.y); // Center the camera on the ship from the very first frame. this.cameras.main.setScroll( this.ship.x - this.scale.width / 2, this.ship.y - 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. A click inside the planet clamps to the // keep-out rim — the ship can stop at the clearance, never inside. this.input.on('pointerdown', (pointer) => { const aim = this.planet.aimPoint(pointer.worldX, pointer.worldY, this.ship.radius); this.showTargetMarker(aim.x, aim.y); this.ship.setTarget(aim.x, aim.y); this.hideHint(); }); } /** * Top-left HUD: the current system's dossier — name, identity, and what *'s ALREADY THERE: colonies, mining stations, cloud bases, stations adrift * in open space (or "charted · unclaimed" when nobody's settled here). * Formatted by the pure SystemReport helper; this method only renders. * * 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 report = formatSystemReport(content); const fam = FONT_FALLBACK; let y = 14; const line = (text, style) => { this.add .text(16, y, text, style) .setOrigin(0, 0) .setScrollFactor(0) // UI: pinned to the screen, not the world .setDepth(30); y += 20 + (style.fontSize === '17px' ? 6 : 0); }; line(report.title, { fontFamily: fam, fontSize: '17px', fontStyle: 'bold', color: toColor(this.galaxy.typeDefs?.[current.type]?.theme?.color ?? '#9fb4e8'), }); line(report.subtitle, { fontFamily: fam, fontSize: '12px', color: '#8fa0c9' }); y += 2; for (const s of report.settlements) { line(s.text, { fontFamily: fam, fontSize: '13px', color: toColor(s.color, 0x8fa0c9) }); } line(report.summary, { fontFamily: fam, fontSize: '12px', color: '#54608a' }); line(`seed ${this.galaxy.seed}`, { fontFamily: fam, fontSize: '11px', color: '#3d476b' }); } update(_time, delta) { this.ship.update(_time, delta); // The home world is solid: the ship may come within the clearance in // data/planets.json of its rim, but never closer (or through it). this.planet.constrainShip(this.ship, this.ship.radius); 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(); } }