Add seed-deterministic planet frame pools
- Allow planet kinds to specify multiple sheet frames and pick one with a seed-derived Rng so the same galaxy always gets the same home world. - Add Ethnocentric and Centauri font assets.
This commit is contained in:
parent
e7b52cc33d
commit
02d689e1bb
11
README.md
11
README.md
|
|
@ -43,11 +43,12 @@ python3 -m http.server 8080
|
||||||
- Game screen with a basic top-down ship: **click anywhere to fly there**
|
- Game screen with a basic top-down ship: **click anywhere to fly there**
|
||||||
in the current system's open 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
|
- **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
|
start in: a 1024 px disc rendered 1:1 from the `assets/images/planets.png`
|
||||||
`assets/images/planets.png`. The ship spawns ~150 px (edge-to-edge) off
|
spritesheet. Which Terran face it shows (frames 0–2) is a
|
||||||
its rim in a seed-derived direction (same galaxy ⇒ same start), may fly
|
seed-deterministic pick, so the same galaxy always yields the same home
|
||||||
in as close as 50 px from the rim, and can never cross it — a planet is
|
world. The ship spawns ~150 px (edge-to-edge) off its rim in a
|
||||||
solid (tuning in `data/planets.json`)
|
seed-derived direction, 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
|
- Camera gently trails the ship; the **parallax starfield** streams past
|
||||||
while it flies and the view slowly recenters (≈1.5 s) once the ship
|
while it flies and the view slowly recenters (≈1.5 s) once the ship
|
||||||
comes to rest
|
comes to rest
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"_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.",
|
"_comment": "Planet visuals + home-planet rules. texture is a spritesheet of frameWidth×frameHeight frames (frame 0 = top-left); frames maps a planet name to the sheet frames it may be drawn as — the generator picks one per planet (seed-deterministic). 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",
|
"texture": "assets/images/planets.png",
|
||||||
"frameWidth": 1024,
|
"frameWidth": 1024,
|
||||||
"frameHeight": 1024,
|
"frameHeight": 1024,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"frames": {
|
"frames": {
|
||||||
"terran": 0
|
"terran": [0, 1, 2]
|
||||||
},
|
},
|
||||||
"homePlanet": "terran",
|
"homePlanet": "terran",
|
||||||
"shipClearance": 50,
|
"shipClearance": 50,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ const check = (label, cond) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { Planet } = await import(pathToFileURL(join(__dirname, '../js/entities/Planet.js')).href);
|
const { Planet } = await import(pathToFileURL(join(__dirname, '../js/entities/Planet.js')).href);
|
||||||
|
const { Rng } = await import(pathToFileURL(join(__dirname, '../js/utils/Rng.js')).href);
|
||||||
|
|
||||||
// Off-origin on purpose: proves nothing is baked to (0, 0).
|
// Off-origin on purpose: proves nothing is baked to (0, 0).
|
||||||
const planet = new Planet({ add: { existing: (o) => o } }, 100, -50, 0);
|
const planet = new Planet({ add: { existing: (o) => o } }, 100, -50, 0);
|
||||||
|
|
@ -64,6 +65,33 @@ check(`texture key is the spritesheet (got "${planet.key}")`, planet.key === 'pl
|
||||||
check(`keep-out = radius + clearance + ship radius (got ${minDist}, want ${512 + clearance + shipRadius})`,
|
check(`keep-out = radius + clearance + ship radius (got ${minDist}, want ${512 + clearance + shipRadius})`,
|
||||||
minDist === 512 + clearance + shipRadius);
|
minDist === 512 + clearance + shipRadius);
|
||||||
|
|
||||||
|
// --- 1b. Frame pools: terran has 3 faces, pick is seed-deterministic ------
|
||||||
|
{
|
||||||
|
const pool = config.get('planets.frames.terran', []);
|
||||||
|
check('terran frame pool is [0, 1, 2] (got ' + JSON.stringify(pool) + ')',
|
||||||
|
JSON.stringify(pool) === JSON.stringify([0, 1, 2]));
|
||||||
|
|
||||||
|
// Same seed ⇒ same pick (worldgen determinism), and every pick is a
|
||||||
|
// member of the pool.
|
||||||
|
const a = Planet.frameFor('terran', Rng.derive('seed-x', 'planet', 'home'));
|
||||||
|
const b = Planet.frameFor('terran', Rng.derive('seed-x', 'planet', 'home'));
|
||||||
|
check(`frameFor is deterministic per seed (got ${a} and ${b})`, a === b && pool.includes(a));
|
||||||
|
|
||||||
|
// Across seeds, all three faces actually get picked (uniform pool).
|
||||||
|
const seen = new Set();
|
||||||
|
for (let i = 0; i < 90; i++) {
|
||||||
|
const f = Planet.frameFor('terran', Rng.derive('seed' + i, 'planet', 'home'));
|
||||||
|
if (!pool.includes(f)) { check(`frameFor stays in the pool (seed${i} got ${f})`, false); break; }
|
||||||
|
seen.add(f);
|
||||||
|
}
|
||||||
|
check(`frameFor covers the whole pool over seeds (got ${[...seen].sort().join(',')})`,
|
||||||
|
seen.size === pool.length);
|
||||||
|
|
||||||
|
// Unknown kinds / bad pools fall back to frame 0.
|
||||||
|
const fallback = Planet.frameFor('gasGiant', Rng.derive('seed-x', 'planet', 'home'));
|
||||||
|
check(`frameFor unknown kind falls back to 0 (got ${fallback})`, fallback === 0);
|
||||||
|
}
|
||||||
|
|
||||||
// --- 2. Outside the keep-out circle: untouched ----------------------------
|
// --- 2. Outside the keep-out circle: untouched ----------------------------
|
||||||
{
|
{
|
||||||
const s = ship(planet.x + minDist + 10, planet.y);
|
const s = ship(planet.x + minDist + 10, planet.y);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import { config } from '../config/Config.js';
|
||||||
/**
|
/**
|
||||||
* A planet: a static world rendered from the shared spritesheet
|
* A planet: a static world rendered from the shared spritesheet
|
||||||
* (data/planets.json → texture; `frameWidth`×`frameHeight` frames, frame 0
|
* (data/planets.json → texture; `frameWidth`×`frameHeight` frames, frame 0
|
||||||
* is the top-left, and `frames` maps names like "terran" to frame indices).
|
* is the top-left, and `frames` maps names like "terran" to the sheet
|
||||||
|
* frames that kind may be drawn as — the generator picks one per planet).
|
||||||
*
|
*
|
||||||
* A planet is a SOLID DISC. Its collision radius is half the scaled frame
|
* 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
|
* (the world fills its frame — a Terran world is 1024 px across at
|
||||||
|
|
@ -20,15 +21,30 @@ import { config } from '../config/Config.js';
|
||||||
export class Planet extends Phaser.GameObjects.Sprite {
|
export class Planet extends Phaser.GameObjects.Sprite {
|
||||||
static TEXTURE_KEY = 'planets';
|
static TEXTURE_KEY = 'planets';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick a sheet frame for a planet of `name` from its pool in
|
||||||
|
* data/planets.json (`frames`), using the given Rng so the choice is
|
||||||
|
* seed-deterministic (same seed ⇒ same world). A pool that is just a
|
||||||
|
* single number (or a missing/empty pool) falls back to that number,
|
||||||
|
* else 0.
|
||||||
|
*/
|
||||||
|
static frameFor(name, rng) {
|
||||||
|
const pool = config.get(`planets.frames.${name}`);
|
||||||
|
if (Array.isArray(pool) && pool.length > 0) return rng.pick(pool);
|
||||||
|
return Number.isFinite(pool) ? pool : 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Phaser.Scene} scene
|
* @param {Phaser.Scene} scene
|
||||||
* @param {number} x — world x of the planet's center
|
* @param {number} x — world x of the planet's center
|
||||||
* @param {number} y — world y
|
* @param {number} y — world y
|
||||||
* @param {number} [frame=0] — spritesheet frame index (data/planets.json → frames)
|
* @param {number} [frame=0] — spritesheet frame index (usually from Planet.frameFor)
|
||||||
|
* @param {string} [name=''] — planet kind, e.g. 'terran' (data/planets.json → frames)
|
||||||
*/
|
*/
|
||||||
constructor(scene, x, y, frame = 0) {
|
constructor(scene, x, y, frame = 0, name = '') {
|
||||||
super(scene, x, y, Planet.TEXTURE_KEY, frame);
|
super(scene, x, y, Planet.TEXTURE_KEY, frame);
|
||||||
scene.add.existing(this);
|
scene.add.existing(this);
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
const scale = config.get('planets.scale', 1);
|
const scale = config.get('planets.scale', 1);
|
||||||
this.setScale(scale);
|
this.setScale(scale);
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export class GameScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
preload() {
|
preload() {
|
||||||
// The planet spritesheet: frameWidth×frameHeight frames, frame 0 = the
|
// The planet spritesheet: frameWidth×frameHeight frames; the first
|
||||||
// Terran home world (more worlds slot into later frames).
|
// frames are Terran worlds (see data/planets.json → frames).
|
||||||
this.load.spritesheet(
|
this.load.spritesheet(
|
||||||
Planet.TEXTURE_KEY,
|
Planet.TEXTURE_KEY,
|
||||||
config.get('planets.texture', 'assets/images/planets.png'),
|
config.get('planets.texture', 'assets/images/planets.png'),
|
||||||
|
|
@ -44,10 +44,12 @@ export class GameScene extends Phaser.Scene {
|
||||||
this.createSystemHud();
|
this.createSystemHud();
|
||||||
|
|
||||||
// The home planet — the player's Terran world, always present in the
|
// The home planet — the player's Terran world, always present in the
|
||||||
// system they start in. It sits at the world origin.
|
// 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 homeName = config.get('planets.homePlanet', 'terran');
|
||||||
const homeFrame = config.get(`planets.frames.${homeName}`, 0);
|
const homeRng = Rng.derive(this.galaxy.seed, 'planet', 'home');
|
||||||
this.planet = new Planet(this, 0, 0, homeFrame);
|
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)
|
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
|
// The ship — a short hop (~150 px, edge-to-edge) from the home world's
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue