Add the home planet: a solid world the ship can approach but not enter
Adds the player's home planet to the starting system, rendered from the new planets.png spritesheet. The planet is a solid disc: the ship may come within shipClearance of its rim but can never cross it, and clicks on the planet clamp to the keep-out rim so the ship always has a reachable destination. - js/entities/Planet.js: solid-disc entity with a circle keep-out constraint (constrainShip/resolve, pure and scene-free) plus edgePoint/aimPoint helpers - data/planets.json (+ manifest entry): texture, frame size, scale, homePlanet, shipClearance, spawnDistanceFromEdge - Ship.js: exposes a radius (half the hull width) for the keep-out math - GameScene.js: loads the spritesheet, spawns the home world at the origin, starts the ship ~150 px off its rim in a seed-derived direction, and applies the constraint each frame after the ship moves - dev/planet.test.mjs: Node tests of the constraint against real config; dev/test-game.html gains a <base href> so asset paths resolve from /dev
This commit is contained in:
parent
9d40103cce
commit
e7b52cc33d
11
README.md
11
README.md
|
|
@ -41,7 +41,13 @@ python3 -m http.server 8080
|
|||
The current system's dossier (name, identity, what's there) shows
|
||||
top-left in the game scene.
|
||||
- Game screen with a basic top-down ship: **click anywhere to fly there**
|
||||
in infinite, unbounded space (system boundaries/jumps come next)
|
||||
in the current system's open space (system boundaries/jumps come next)
|
||||
- **The home planet** — the player's Terran world — in the system you
|
||||
start in: a 1024 px disc rendered 1:1 from frame 0 of
|
||||
`assets/images/planets.png`. The ship spawns ~150 px (edge-to-edge) off
|
||||
its rim in a seed-derived direction (same galaxy ⇒ same start), may fly
|
||||
in as close as 50 px from the rim, and can never cross it — a planet is
|
||||
solid (tuning in `data/planets.json`)
|
||||
- Camera gently trails the ship; the **parallax starfield** streams past
|
||||
while it flies and the view slowly recenters (≈1.5 s) once the ship
|
||||
comes to rest
|
||||
|
|
@ -61,12 +67,13 @@ orbit/
|
|||
│ ├── systems.json # system archetypes: theme, attributes, distribution
|
||||
│ ├── settlements.json # the lived-in layer: settlement kinds & populations
|
||||
│ └── naming.json # syllable pools for names
|
||||
├── assets/images/ # art: planets.png (1024×1024 spritesheet frames)
|
||||
├── lib/ # vendored third-party libs (Phaser 4.2.1)
|
||||
├── js/
|
||||
│ ├── main.js # entry point: load config → boot Phaser
|
||||
│ ├── config/ # Config singleton, ConfigLoader, game config
|
||||
│ ├── scenes/ # MenuScene, GameScene (thin, orchestration)
|
||||
│ ├── entities/ # Ship (own behavior)
|
||||
│ ├── entities/ # Ship (own behavior), Planet (home world, solid)
|
||||
│ ├── galaxy/ # Galaxy (seeded world model), SystemGenerator, SystemReport
|
||||
│ ├── ui/ # MenuButton (reusable)
|
||||
│ ├── visuals/ # Starfield (decorative)
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
Binary file not shown.
|
|
@ -3,6 +3,7 @@
|
|||
"game.json",
|
||||
"menu.json",
|
||||
"ship.json",
|
||||
"planets.json",
|
||||
"galaxy.json",
|
||||
"systems.json",
|
||||
"naming.json"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"_comment": "Planet visuals + home-planet rules. texture is a spritesheet of frameWidth×frameHeight frames (frame 0 = top-left); frames maps a planet name to its frame index. homePlanet is the player's world — always present in the system the player starts in. scale = world pixels per sheet pixel (1.0 = 1:1, so a Terran world is 1024 px across on screen). A planet is a solid disc: the ship may approach to shipClearance px (edge-to-edge) from its rim but can never cross it. spawnDistanceFromEdge = how far (edge-to-edge) the ship starts from the home world's rim.",
|
||||
"texture": "assets/images/planets.png",
|
||||
"frameWidth": 1024,
|
||||
"frameHeight": 1024,
|
||||
"scale": 1.0,
|
||||
"frames": {
|
||||
"terran": 0
|
||||
},
|
||||
"homePlanet": "terran",
|
||||
"shipClearance": 50,
|
||||
"spawnDistanceFromEdge": 150
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* Planet test (dev tool, run with Node — no browser needed):
|
||||
*
|
||||
* node dev/planet.test.mjs
|
||||
*
|
||||
* Stubs just enough of Phaser to construct the REAL Planet from
|
||||
* js/entities/Planet.js against the real data/planets.json + ship.json
|
||||
* config, then asserts the home-world rules:
|
||||
* - the Terran world is 1024 px across at 1:1 (rim 512 px from center);
|
||||
* - the ship can come to shipClearance px (edge-to-edge) from the rim,
|
||||
* but never closer, and never moves through the planet;
|
||||
* - a graze keeps its tangential velocity (slides along the rim);
|
||||
* - edgePoint() places the spawn exactly spawnDistanceFromEdge off.
|
||||
*/
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// --- Phaser stub: just the Sprite base class -----------------------------
|
||||
class Sprite {
|
||||
constructor(scene, x, y, key, frame) {
|
||||
this.scene = scene; this.x = x; this.y = y;
|
||||
this.key = key; this.frame = frame;
|
||||
this.scaleX = 1; this.scaleY = 1;
|
||||
}
|
||||
setScale(s) { this.scaleX = s; this.scaleY = s; return this; }
|
||||
}
|
||||
globalThis.window = { Phaser: { GameObjects: { Sprite } } }; // js/vendor/phaser.js reads this
|
||||
|
||||
// --- Load the real config (data/*.json) into the config singleton --------
|
||||
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
|
||||
const fs = await import('node:fs');
|
||||
const dataDir = join(__dirname, '../data');
|
||||
const configData = {};
|
||||
for (const f of fs.readdirSync(dataDir)) {
|
||||
if (!f.endsWith('.json') || f === 'manifest.json') continue;
|
||||
configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8'));
|
||||
}
|
||||
config.init(configData);
|
||||
console.log('config loaded from data/:', Object.keys(configData).join(', '));
|
||||
|
||||
let failures = 0;
|
||||
const check = (label, cond) => {
|
||||
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
|
||||
if (!cond) failures++;
|
||||
};
|
||||
|
||||
const { Planet } = await import(pathToFileURL(join(__dirname, '../js/entities/Planet.js')).href);
|
||||
|
||||
// Off-origin on purpose: proves nothing is baked to (0, 0).
|
||||
const planet = new Planet({ add: { existing: (o) => o } }, 100, -50, 0);
|
||||
|
||||
const shipRadius = (config.get('ship.size', 46) * config.get('ship.scale', 1)) / 2;
|
||||
const clearance = planet.clearance;
|
||||
const minDist = planet.minCenterDistance(shipRadius);
|
||||
const ship = (x, y, vx = 0, vy = 0) => ({ x, y, body: { velocity: { x: vx, y: vy }, acceleration: { x: 0, y: 0 } } });
|
||||
const dist = (p, c = planet) => Math.hypot(p.x - c.x, p.y - c.y);
|
||||
|
||||
// --- 1. Real config: 1:1 Terran world + keep-out distances ---------------
|
||||
check(`1:1 world: planet radius is 512 px (got ${planet.radius})`, planet.radius === 512);
|
||||
check(`frame 0 is the Terran world (got frame ${planet.frame})`, planet.frame === 0);
|
||||
check(`texture key is the spritesheet (got "${planet.key}")`, planet.key === 'planets');
|
||||
check(`keep-out = radius + clearance + ship radius (got ${minDist}, want ${512 + clearance + shipRadius})`,
|
||||
minDist === 512 + clearance + shipRadius);
|
||||
|
||||
// --- 2. Outside the keep-out circle: untouched ----------------------------
|
||||
{
|
||||
const s = ship(planet.x + minDist + 10, planet.y);
|
||||
planet.constrainShip(s, shipRadius);
|
||||
check('outside the keep-out circle: ship untouched',
|
||||
s.x === planet.x + minDist + 10 && s.y === planet.y);
|
||||
}
|
||||
|
||||
// Exactly on the boundary: allowed ("within 50 px" includes 50 px).
|
||||
{
|
||||
const s = ship(planet.x + minDist, planet.y, -100, 0); // on the line, moving in
|
||||
planet.constrainShip(s, shipRadius);
|
||||
const vn = s.body.velocity.x; // normal here is +x
|
||||
check('on the boundary with inward velocity: position held, inward velocity removed',
|
||||
dist(s) === minDist && vn >= 0);
|
||||
}
|
||||
|
||||
// --- 3. Inside: pushed out to exactly the keep-out line -------------------
|
||||
{
|
||||
const s = ship(planet.x + 100, planet.y);
|
||||
planet.constrainShip(s, shipRadius);
|
||||
check(`inside: pushed out to the keep-out line (dist=${dist(s).toFixed(2)}, want ${minDist})`,
|
||||
Math.abs(dist(s) - minDist) < 1e-9);
|
||||
}
|
||||
|
||||
// --- 4. Inward velocity removed, tangential kept (a graze slides) ---------
|
||||
{
|
||||
const s = ship(planet.x + 200, planet.y, -100, -300); // inside, heading into the planet
|
||||
planet.constrainShip(s, shipRadius);
|
||||
check('inside + inward velocity: pushed out, inward component removed (vx=0)',
|
||||
Math.abs(dist(s) - minDist) < 1e-9 && s.body.velocity.x === 0);
|
||||
check('tangential velocity kept (vy=-300)', s.body.velocity.y === -300);
|
||||
}
|
||||
|
||||
// --- 4b. Inward ACCELERATION removed too (the world integrates it after) --
|
||||
{
|
||||
// The arcade world steps AFTER this constraint runs, so an inward
|
||||
// acceleration would push the ship back inside a fraction of a pixel on
|
||||
// the very next step. It must be stripped, tangential kept.
|
||||
const s = ship(planet.x + 200, planet.y, 0, 0);
|
||||
s.body.acceleration.x = -500; // throttling into the planet
|
||||
s.body.acceleration.y = -300; // …and a tangential component
|
||||
planet.constrainShip(s, shipRadius);
|
||||
check('inward acceleration stripped (ax=0), tangential kept (ay=-300)',
|
||||
s.body.acceleration.x === 0 && s.body.acceleration.y === -300);
|
||||
// …so the next physics step can only carry the ship along the rim.
|
||||
const step = 16.67 / 1000;
|
||||
s.x += s.body.velocity.x * step + s.body.acceleration.x * step * step;
|
||||
s.y += s.body.velocity.y * step + s.body.acceleration.y * step * step;
|
||||
check('next physics step keeps the ship at the clearance or farther',
|
||||
dist(s) >= minDist - 1e-9);
|
||||
}
|
||||
|
||||
// --- 4c. aimPoint(): clicks inside the planet clamp to the keep-out rim ---
|
||||
{
|
||||
// Inside → projected out onto the rim, along the ray from the center.
|
||||
const inAim = planet.aimPoint(planet.x + 100, planet.y + 300, shipRadius);
|
||||
check('aimPoint inside: lands exactly on the keep-out line',
|
||||
Math.abs(dist(inAim) - minDist) < 1e-9);
|
||||
const dirOk = Math.abs((inAim.x - planet.x) / 100 - (inAim.y - planet.y) / 300) < 1e-12;
|
||||
check('aimPoint inside: stays on the ray from the center', dirOk);
|
||||
|
||||
// Outside → unchanged.
|
||||
const outAim = planet.aimPoint(planet.x + minDist + 5, planet.y - 20, shipRadius);
|
||||
check('aimPoint outside: passes through unchanged',
|
||||
outAim.x === planet.x + minDist + 5 && outAim.y === planet.y - 20);
|
||||
|
||||
// Dead center → pushed out along +x, no NaN.
|
||||
const cAim = planet.aimPoint(planet.x, planet.y, shipRadius);
|
||||
check('aimPoint at the center: +x on the rim, no NaN',
|
||||
Number.isFinite(cAim.x) && Math.abs(dist(cAim) - minDist) < 1e-9 && cAim.x === planet.x + minDist);
|
||||
}
|
||||
|
||||
// --- 5. Full approach at max ship speed, frame by frame ------------------
|
||||
{
|
||||
// One 60 fps frame at the ship's maxSpeed (data/ship.json), closing in
|
||||
// dead-on: the ship must never end a frame closer than the clearance,
|
||||
// and must come to rest exactly on the keep-out line.
|
||||
let s = ship(planet.x + 1500, planet.y, -480, 0);
|
||||
let worst = Infinity;
|
||||
for (let i = 0; i < 200 && s.body.velocity.x < 0; i++) {
|
||||
s.x += s.body.velocity.x * (16.67 / 1000); // world integration
|
||||
s.y += s.body.velocity.y * (16.67 / 1000);
|
||||
planet.constrainShip(s, shipRadius);
|
||||
worst = Math.min(worst, dist(s) - planet.radius - shipRadius);
|
||||
}
|
||||
check(`frame-by-frame approach at max speed: never closer than the clearance (worst edge gap ${worst.toFixed(2)} px)`,
|
||||
worst >= clearance - 1e-9);
|
||||
check('...and comes to rest on the keep-out line with no inward speed',
|
||||
Math.abs(dist(s) - minDist) < 1e-9 && s.body.velocity.x === 0);
|
||||
}
|
||||
|
||||
// --- 6. Degenerate: ship dead at the planet's center ----------------------
|
||||
{
|
||||
const s = ship(planet.x, planet.y);
|
||||
planet.constrainShip(s, shipRadius);
|
||||
check('at the center: pushed out along +x, no NaN',
|
||||
Number.isFinite(s.x) && Number.isFinite(s.y) && Math.abs(dist(s) - minDist) < 1e-9
|
||||
&& s.x === planet.x + minDist && s.y === planet.y);
|
||||
}
|
||||
|
||||
// --- 7. edgePoint(): spawn is exactly spawnDistanceFromEdge off the rim ---
|
||||
{
|
||||
const gap = config.get('planets.spawnDistanceFromEdge', 150);
|
||||
let ok = true;
|
||||
for (const a of [0, 0.7, 2.4, 4.2]) {
|
||||
const p = planet.edgePoint(a, gap, shipRadius);
|
||||
const edgeGap = dist(p) - planet.radius - shipRadius;
|
||||
if (Math.abs(edgeGap - gap) > 1e-9) ok = false;
|
||||
}
|
||||
check(`edgePoint puts the ship exactly ${gap} px (edge-to-edge) off the rim, any direction`, ok);
|
||||
}
|
||||
|
||||
console.log(failures === 0 ? '\nAll planet tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
|
||||
process.exit(failures === 0 ? 0 : 1);
|
||||
|
|
@ -3,14 +3,18 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev smoke test (GameScene)</title>
|
||||
<!-- This page lives in /dev, but the game's relative asset paths are rooted
|
||||
at the project root — resolve them against it. ("../" keeps this
|
||||
working even if the project is served from a subdirectory.) -->
|
||||
<base href="../" />
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="../lib/phaser.min.js"></script>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="smoke-game.mjs"></script>
|
||||
<script type="module" src="dev/smoke-game.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
import Phaser from '../vendor/phaser.js';
|
||||
import { config } from '../config/Config.js';
|
||||
|
||||
/**
|
||||
* A planet: a static world rendered from the shared spritesheet
|
||||
* (data/planets.json → texture; `frameWidth`×`frameHeight` frames, frame 0
|
||||
* is the top-left, and `frames` maps names like "terran" to frame indices).
|
||||
*
|
||||
* A planet is a SOLID DISC. Its collision radius is half the scaled frame
|
||||
* (the world fills its frame — a Terran world is 1024 px across at
|
||||
* scale 1.0), and the ship is kept `shipClearance` px (edge-to-edge) off
|
||||
* the rim: it can come that close, but it can never move through the
|
||||
* planet or any closer.
|
||||
*
|
||||
* The keep-out rule is a plain circle test (constrainShip → static
|
||||
* resolve) applied to the ship after it moves, so it never interferes
|
||||
* with the ship's own flight model and stays testable in Node
|
||||
* (dev/planet.test.mjs) without a scene.
|
||||
*/
|
||||
export class Planet extends Phaser.GameObjects.Sprite {
|
||||
static TEXTURE_KEY = 'planets';
|
||||
|
||||
/**
|
||||
* @param {Phaser.Scene} scene
|
||||
* @param {number} x — world x of the planet's center
|
||||
* @param {number} y — world y
|
||||
* @param {number} [frame=0] — spritesheet frame index (data/planets.json → frames)
|
||||
*/
|
||||
constructor(scene, x, y, frame = 0) {
|
||||
super(scene, x, y, Planet.TEXTURE_KEY, frame);
|
||||
scene.add.existing(this);
|
||||
|
||||
const scale = config.get('planets.scale', 1);
|
||||
this.setScale(scale);
|
||||
|
||||
// Collision circle: frames are square and the world fills its frame,
|
||||
// so the rim is half the (scaled) frame width from the center.
|
||||
this.radius = (config.get('planets.frameWidth', 1024) * scale) / 2;
|
||||
// Edge-to-edge gap the ship may close in on the rim (never less).
|
||||
this.clearance = config.get('planets.shipClearance', 50);
|
||||
}
|
||||
|
||||
/** Minimum allowed center-to-center distance for a ship of `shipRadius`. */
|
||||
minCenterDistance(shipRadius = 0) {
|
||||
return this.radius + this.clearance + shipRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* A world point `gap` px (edge-to-edge) off this planet's rim at
|
||||
* `angle` radians — e.g. where to spawn the ship near the home world.
|
||||
*/
|
||||
edgePoint(angle, gap, shipRadius = 0) {
|
||||
const d = this.radius + gap + shipRadius;
|
||||
return { x: this.x + Math.cos(angle) * d, y: this.y + Math.sin(angle) * d };
|
||||
}
|
||||
|
||||
/**
|
||||
* A world point the ship may be sent to: a point inside the keep-out
|
||||
* circle (e.g. a click on the planet itself, or through it) is projected
|
||||
* out onto the rim, along the ray from the center — so the ship always
|
||||
* has a reachable destination and is never told to go inside. Points
|
||||
* already outside pass through unchanged.
|
||||
*/
|
||||
aimPoint(wx, wy, shipRadius = 0) {
|
||||
const minDist = this.minCenterDistance(shipRadius);
|
||||
const dx = wx - this.x;
|
||||
const dy = wy - this.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist >= minDist) return { x: wx, y: wy };
|
||||
if (dist === 0) return { x: this.x + minDist, y: this.y }; // dead center: +x
|
||||
return { x: this.x + (dx / dist) * minDist, y: this.y + (dy / dist) * minDist };
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep a ship (anything with x, y and body.velocity) out of the planet:
|
||||
* if it is inside the keep-out circle its center is moved out to
|
||||
* `minCenterDistance` and the inward part of its velocity AND
|
||||
* acceleration is removed — the tangential part is kept, so a near-miss
|
||||
* slides along the rim instead of sticking. Stripping the acceleration
|
||||
* matters: the arcade world integrates it AFTER this runs, so without
|
||||
* it the ship would be pushed back inside a fraction of a pixel on the
|
||||
* very next physics step. A ship already outside is untouched; one
|
||||
* riding exactly on the circle keeps its position but loses its inward
|
||||
* speed, so contact is clean (no in/out jitter).
|
||||
*/
|
||||
constrainShip(ship, shipRadius = 0) {
|
||||
const minDist = this.minCenterDistance(shipRadius);
|
||||
const body = ship.body;
|
||||
const r = Planet.resolve(
|
||||
this.x, this.y, minDist,
|
||||
ship.x, ship.y,
|
||||
body.velocity.x, body.velocity.y,
|
||||
body.acceleration ? body.acceleration.x : 0,
|
||||
body.acceleration ? body.acceleration.y : 0,
|
||||
);
|
||||
ship.x = r.x;
|
||||
ship.y = r.y;
|
||||
body.velocity.x = r.vx;
|
||||
body.velocity.y = r.vy;
|
||||
if (body.acceleration) {
|
||||
body.acceleration.x = r.ax;
|
||||
body.acceleration.y = r.ay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The pure circle constraint (static so it can be tested without a
|
||||
* scene): clamps a point at least `minDist` from (cx, cy) and removes
|
||||
* the components of velocity and acceleration pointing into the circle.
|
||||
*/
|
||||
static resolve(cx, cy, minDist, x, y, vx, vy, ax = 0, ay = 0) {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist > minDist) return { x, y, vx, vy, ax, ay };
|
||||
let nx;
|
||||
let ny;
|
||||
if (dist === 0) { nx = 1; ny = 0; } // dead center: push out along +x
|
||||
else { nx = dx / dist; ny = dy / dist; }
|
||||
const ox = cx + nx * minDist;
|
||||
const oy = cy + ny * minDist;
|
||||
// Strip any component pointing into the circle (keep the tangential).
|
||||
const strip = (v) => {
|
||||
const vn = v[0] * nx + v[1] * ny;
|
||||
return vn < 0 ? [v[0] - vn * nx, v[1] - vn * ny] : [v[0], v[1]];
|
||||
};
|
||||
const [rvx, rvy] = strip([vx, vy]);
|
||||
const [rax, ray] = strip([ax, ay]);
|
||||
return { x: ox, y: oy, vx: rvx, vy: rvy, ax: rax, ay: ray };
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,9 @@ export class Ship extends Phaser.Physics.Arcade.Sprite {
|
|||
this.arriveRadius = config.get('ship.arriveRadius', 8); // px
|
||||
this.arriveSpeed = config.get('ship.arriveSpeed', 50); // px/s
|
||||
this.setScale(config.get('ship.scale', 1));
|
||||
// Collision radius (half the hull's width, at scale) — planets keep
|
||||
// the ship this far off their rims (see Planet.constrainShip).
|
||||
this.radius = (config.get('ship.size', 46) * config.get('ship.scale', 1)) / 2;
|
||||
|
||||
this.target = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ 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.2: one ship in the current system's open space).
|
||||
* 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 {
|
||||
|
|
@ -18,21 +20,52 @@ export class GameScene extends Phaser.Scene {
|
|||
super({ key: 'GameScene' });
|
||||
}
|
||||
|
||||
preload() {
|
||||
// The planet spritesheet: frameWidth×frameHeight frames, frame 0 = the
|
||||
// Terran home world (more worlds slot into later 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
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
const homeName = config.get('planets.homePlanet', 'terran');
|
||||
const homeFrame = config.get(`planets.frames.${homeName}`, 0);
|
||||
this.planet = new Planet(this, 0, 0, homeFrame);
|
||||
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.scale.width / 2, -this.scale.height / 2);
|
||||
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);
|
||||
|
|
@ -48,10 +81,12 @@ export class GameScene extends Phaser.Scene {
|
|||
.setOrigin(0.5)
|
||||
.setScrollFactor(0); // UI: pinned to the screen, not the world
|
||||
|
||||
// Input: click = fly there
|
||||
// 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) => {
|
||||
this.showTargetMarker(pointer.worldX, pointer.worldY);
|
||||
this.ship.setTarget(pointer.worldX, pointer.worldY);
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
|
@ -112,6 +147,9 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue