diff --git a/README.md b/README.md index a3b61a7..2201054 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,12 @@ python3 -m http.server 8080 - 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) - **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`) + start in: a 1024 px disc rendered 1:1 from the `assets/images/planets.png` + spritesheet. Which Terran face it shows (frames 0–2) is a + seed-deterministic pick, so the same galaxy always yields the same home + world. The ship spawns ~150 px (edge-to-edge) off its rim in a + 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 while it flies and the view slowly recenters (≈1.5 s) once the ship comes to rest diff --git a/assets/fonts/Ethnocentric-Regular.otf b/assets/fonts/Ethnocentric-Regular.otf new file mode 100644 index 0000000..c1dadc8 Binary files /dev/null and b/assets/fonts/Ethnocentric-Regular.otf differ diff --git a/assets/fonts/FontsFree-Net-Centauri.ttf b/assets/fonts/FontsFree-Net-Centauri.ttf new file mode 100644 index 0000000..eb263b4 Binary files /dev/null and b/assets/fonts/FontsFree-Net-Centauri.ttf differ diff --git a/data/planets.json b/data/planets.json index a205f5d..64d10d6 100644 --- a/data/planets.json +++ b/data/planets.json @@ -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", "frameWidth": 1024, "frameHeight": 1024, "scale": 1.0, "frames": { - "terran": 0 + "terran": [0, 1, 2] }, "homePlanet": "terran", "shipClearance": 50, diff --git a/dev/planet.test.mjs b/dev/planet.test.mjs index a8ec670..be50a32 100644 --- a/dev/planet.test.mjs +++ b/dev/planet.test.mjs @@ -47,6 +47,7 @@ const check = (label, cond) => { }; 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). 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})`, 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 ---------------------------- { const s = ship(planet.x + minDist + 10, planet.y); diff --git a/js/entities/Planet.js b/js/entities/Planet.js index bccff64..ef90cb4 100644 --- a/js/entities/Planet.js +++ b/js/entities/Planet.js @@ -4,7 +4,8 @@ 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). + * 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 * (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 { 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 {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) + * @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); scene.add.existing(this); + this.name = name; const scale = config.get('planets.scale', 1); this.setScale(scale); diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index c261bde..02a88ba 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -21,8 +21,8 @@ export class GameScene extends Phaser.Scene { } preload() { - // The planet spritesheet: frameWidth×frameHeight frames, frame 0 = the - // Terran home world (more worlds slot into later frames). + // 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'), @@ -44,10 +44,12 @@ export class GameScene extends Phaser.Scene { this.createSystemHud(); // 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 homeFrame = config.get(`planets.frames.${homeName}`, 0); - this.planet = new Planet(this, 0, 0, homeFrame); + 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