Rework solar system layout to orbit rings with minSpacing/maxNeighbor ru
- Replace the annular-band scatter (minOrbit/maxOrbit/minEdgeGap) with a ring-based placement: objects sit on one or two orbits around the home world, governed by center-to-center `minSpacing` and `maxNeighbor` - Free-space stations (deep-space stations, waypoints) are now layout objects alongside planets; planet-bound settlements stay features of their planet - Handle the N=11 case (9 planets + 2 stations) with an inner 3-ring + outer 8-ring; chain outward defensively for any larger N - Update tests to verify the new spacing invariants, the two-orbit case, and determinism including station positions - Refresh PROJECT_NOTES and the data-file comment to describe the orbit model
This commit is contained in:
parent
fb9722f028
commit
48dfc7cd8f
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "Planet visuals + home-planet rules + the solar system's layout. texture is a spritesheet of frameWidth×frameHeight frames (frame 0 = top-left); frames maps a planet kind 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 at the origin of the system they start 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. classScale = size multiplier per planet class (1.0 = 1024 px across); classTint = optional canvas tint per class (multiplicative — the terran art stands in for every kind until real art lands). solarSystem = the top-down layout band: the other worlds of a system sit minOrbit–maxOrbit px from the origin, at least minEdgeGap px (edge-to-edge) apart from each other and the origin.",
|
||||
"_comment": "Planet visuals + home-planet rules + the solar system's layout. texture is a spritesheet of frameWidth×frameHeight frames (frame 0 = top-left); frames maps a planet kind 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 at the origin of the system they start 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. classScale = size multiplier per planet class (1.0 = 1024 px across); classTint = optional canvas tint per class (multiplicative — the terran art stands in for every kind until real art lands). solarSystem = the top-down layout: the system's other objects (planets + free-space stations) sit on one or two orbits (rings) around the origin — center-to-center at least minSpacing px from every other object (incl. the origin's home world), and, whenever the system holds more than one object, every object within maxNeighbor px of at least one other object. minSpacing/maxNeighbor are center-to-center px (the same unit as the 1024 px world disc).",
|
||||
"texture": "assets/images/planets.png",
|
||||
"frameWidth": 1024,
|
||||
"frameHeight": 1024,
|
||||
|
|
@ -37,8 +37,7 @@
|
|||
"spawnDistanceFromEdge": 150,
|
||||
"solarSystem": {
|
||||
"enabled": true,
|
||||
"minOrbit": 2600,
|
||||
"maxOrbit": 8800,
|
||||
"minEdgeGap": 1200
|
||||
"minSpacing": 6144,
|
||||
"maxNeighbor": 10240
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@
|
|||
* - the DISCOVERY RULE (data/game.json → discovery.distance): within that
|
||||
* many px of an object's EDGE ⇒ discovered, exactly once, per system;
|
||||
* state round-trips through toJSON/fromJSON (saves-ready);
|
||||
* - the SOLAR SYSTEM LAYOUT (data/planets.json → solarSystem): every
|
||||
* system's worlds sit at finite, in-band positions around the origin,
|
||||
* at least minEdgeGap px apart edge-to-edge (incl. the origin's home
|
||||
* world), scaled by class — deterministically (same seed ⇒ same layout,
|
||||
* different seed ⇒ different);
|
||||
* - the SOLAR SYSTEM LAYOUT (data/planets.json → solarSystem): the
|
||||
* system's planets + free-space stations (+ the origin's home world)
|
||||
* sit on orbits around the origin, no pair closer than minSpacing
|
||||
* (center-to-center) and — whenever a system holds more than one
|
||||
* object — every object within maxNeighbor of at least one other,
|
||||
* planets scaled by class — deterministically (same seed ⇒ same
|
||||
* layout, different seed ⇒ different);
|
||||
* - the COMPASS geometry (js/ui/DiscoveryCompass.js): edgeAnchor lands on
|
||||
* the screen-edge rect (edges AND corners), circleInView is exact,
|
||||
* lerpAngle always takes the short arc;
|
||||
|
|
@ -106,62 +108,94 @@ check('toJSON/fromJSON round-trips (saves-ready)', restored.distance === D && re
|
|||
// --- The solar system layout ---------------------------------------------
|
||||
|
||||
const band = config.get('planets.solarSystem');
|
||||
const { minOrbit, maxOrbit, minEdgeGap } = band;
|
||||
const baseR = (config.get('planets.frameWidth', 1024) * config.get('planets.scale', 1)) / 2;
|
||||
const MIN_SEP = band.minSpacing;
|
||||
const MAX_NBR = band.maxNeighbor;
|
||||
|
||||
const g = Galaxy.create('discovery-layout-test');
|
||||
let layoutOk = true;
|
||||
let layoutWhy = '';
|
||||
let sawTwoOrbits = false; // the N = 11 case (9 planets + 2 stations)
|
||||
const probe = (systems) => {
|
||||
for (const rec of systems) {
|
||||
const c = g.ensureContent(rec.id);
|
||||
const placed = [{ x: 0, y: 0, r: baseR }]; // the origin's home world
|
||||
for (const p of c.planets) {
|
||||
const r = baseR * p.scale;
|
||||
const d0 = Math.hypot(p.x, p.y);
|
||||
const want = config.get(`planets.classScale.${p.class}`, 1);
|
||||
if (!Number.isFinite(p.x) || !Number.isFinite(p.y) || !Number.isFinite(p.scale)) {
|
||||
layoutOk = false;
|
||||
layoutWhy = `${rec.id}: non-finite layout`;
|
||||
continue;
|
||||
}
|
||||
const want = config.get(`planets.classScale.${p.class}`, 1);
|
||||
if (Math.abs(p.scale - want) > 1e-9) {
|
||||
layoutOk = false;
|
||||
layoutWhy = `${rec.id}: ${p.class} world scaled ${p.scale} ≠ classScale ${want}`;
|
||||
}
|
||||
if (d0 < minOrbit - 1e-6) {
|
||||
}
|
||||
// Layout objects: the home world (origin) + planets + free-space
|
||||
// stations (planet-bound settlements sit ON their planet — not
|
||||
// layout objects of their own).
|
||||
const objs = [{ x: 0, y: 0 }];
|
||||
for (const p of c.planets) objs.push({ x: p.x, y: p.y });
|
||||
for (const s of c.settlements ?? []) {
|
||||
if (s.anchor?.type !== 'space') continue;
|
||||
if (typeof s.x !== 'number' || typeof s.y !== 'number' || !Number.isFinite(s.x) || !Number.isFinite(s.y)) {
|
||||
layoutOk = false;
|
||||
layoutWhy = `${rec.id}: world at ${d0.toFixed(1)} px < minOrbit ${minOrbit}`;
|
||||
layoutWhy = `${rec.id}: free-space station "${s.name}" has no valid position`;
|
||||
continue;
|
||||
}
|
||||
for (const q of placed) {
|
||||
const gap = Math.hypot(p.x - q.x, p.y - q.y) - q.r - r;
|
||||
if (gap < minEdgeGap - 1e-6) {
|
||||
objs.push({ x: s.x, y: s.y });
|
||||
}
|
||||
if (objs.length - 1 === 11) sawTwoOrbits = true;
|
||||
// 1) No two objects closer than minSpacing (center to center).
|
||||
for (let i = 0; i < objs.length && layoutOk; i++) {
|
||||
for (let j = i + 1; j < objs.length; j++) {
|
||||
const d = Math.hypot(objs[i].x - objs[j].x, objs[i].y - objs[j].y);
|
||||
if (d < MIN_SEP - 1e-6) {
|
||||
layoutOk = false;
|
||||
layoutWhy = `${rec.id}: worlds ${minEdgeGap - gap}px closer than minEdgeGap ${minEdgeGap}`;
|
||||
layoutWhy = `${rec.id}: two objects ${Math.round(MIN_SEP - d)} px closer than minSpacing ${MIN_SEP}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2) Every object has a neighbor within maxNeighbor (the home world is
|
||||
// always present, so "more than one object" ⇔ objs.length ≥ 2).
|
||||
if (layoutOk && objs.length >= 2) {
|
||||
for (let i = 0; i < objs.length; i++) {
|
||||
const near = objs.some(
|
||||
(o, j) => j !== i && Math.hypot(objs[i].x - o.x, objs[i].y - o.y) <= MAX_NBR + 1e-6,
|
||||
);
|
||||
if (!near) {
|
||||
layoutOk = false;
|
||||
layoutWhy = `${rec.id}: an object with no neighbor within maxNeighbor ${MAX_NBR}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
placed.push({ x: p.x, y: p.y, r });
|
||||
}
|
||||
}
|
||||
};
|
||||
probe(g.records.slice(0, 500));
|
||||
probe(g.records.slice(0, 5000));
|
||||
check(
|
||||
`layout: 500 systems' worlds finite, scaled by class, in-band (≥${minOrbit}px), ≥${minEdgeGap}px apart edge-to-edge${layoutOk ? '' : ' — ' + layoutWhy}`,
|
||||
`layout: 5000 systems obey minSpacing (no pair <${MIN_SEP}px center-to-center) & maxNeighbor (every object ≤${MAX_NBR}px from a neighbor)${layoutOk ? '' : ' — ' + layoutWhy}`,
|
||||
layoutOk,
|
||||
);
|
||||
check('layout: the 11-object two-orbit case (9 planets + 2 stations) occurs in the sample', sawTwoOrbits);
|
||||
|
||||
const rec0 = g.records[0].id;
|
||||
// Determinism on a system that has BOTH planet and station objects.
|
||||
const rec0 = g.records.find((r) =>
|
||||
g.ensureContent(r.id).settlements.some((s) => s.anchor?.type === 'space'),
|
||||
).id;
|
||||
const la = Galaxy.create('discovery-layout-test').ensureContent(rec0);
|
||||
const lb = Galaxy.create('discovery-layout-test').ensureContent(rec0);
|
||||
const stationXY = (c) => (c.settlements ?? []).filter((s) => s.anchor?.type === 'space').map((s) => [s.x, s.y]);
|
||||
const same =
|
||||
la.planets.length === lb.planets.length &&
|
||||
la.planets.every((p, i) => p.x === lb.planets[i].x && p.y === lb.planets[i].y && p.scale === lb.planets[i].scale);
|
||||
check('layout is deterministic (same seed ⇒ same x/y/scale)', same);
|
||||
la.planets.every((p, i) => p.x === lb.planets[i].x && p.y === lb.planets[i].y && p.scale === lb.planets[i].scale) &&
|
||||
JSON.stringify(stationXY(la)) === JSON.stringify(stationXY(lb));
|
||||
check('layout is deterministic (same seed ⇒ same planet + station x/y/scale)', same);
|
||||
|
||||
const lc = Galaxy.create('discovery-layout-OTHER').ensureContent(rec0);
|
||||
const diff =
|
||||
lc.planets.length !== la.planets.length ||
|
||||
la.planets.some((p, i) => p.x !== lc.planets[i]?.x || p.y !== lc.planets[i]?.y);
|
||||
la.planets.some((p, i) => p.x !== lc.planets[i]?.x || p.y !== lc.planets[i]?.y) ||
|
||||
JSON.stringify(stationXY(lc)) !== JSON.stringify(stationXY(la));
|
||||
check('different seed ⇒ different layout', diff);
|
||||
|
||||
// --- The compass geometry ---------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -171,15 +171,22 @@ unclaimed". Model & seams:
|
|||
The player starts in a solar system, and the other worlds **exist in the
|
||||
world** — solid, rendered, flyable-to. Rules and seams:
|
||||
|
||||
- **World layout** — `SystemGenerator.layoutSystemPlanets(seed, systemId,
|
||||
planets)` (pure, `js/galaxy/SystemGenerator.js`) places each generated
|
||||
world in an annular band around the home world (origin), enforcing
|
||||
edge-to-edge separation from every other disc. Draws come from the
|
||||
dedicated fork `Rng.derive(seed, 'system', id, 'layout')` — layout
|
||||
results are seed-deterministic *and* don't perturb the content stream
|
||||
(lazy === eager is preserved). Band params live in
|
||||
`data/planets.json → solarSystem` (enabled, minOrbit, maxOrbit,
|
||||
minEdgeGap); class sizes/tints in `classScale`/`classTint`.
|
||||
- **World layout** — `SystemGenerator.layoutSystem(seed, systemId, planets,
|
||||
freeSpace)` (pure, `js/galaxy/SystemGenerator.js`) places each system's
|
||||
planets and free-space stations (deep-space stations, waypoints) on one
|
||||
or two orbits (rings) around the home world (origin), enforcing the hard
|
||||
spacing rules in `data/planets.json → solarSystem`: `minSpacing` (6144 px,
|
||||
center-to-center, between ANY two objects — planets, stations, the home
|
||||
world) and `maxNeighbor` (10240 px — whenever a system holds more than
|
||||
one object, every object is within it of at least one other; the inner
|
||||
orbit's near neighbor is the home world at the origin). ≤ 10 objects
|
||||
share one orbit; the 11-object maximum (9 planets + 2 stations) splits
|
||||
into a 3-ring + 8-ring. Planet-bound settlements (colonies, mining
|
||||
stations, cloud bases) are features of their planet, not layout objects.
|
||||
Draws come from the dedicated fork `Rng.derive(seed, 'system', id,
|
||||
'layout')` — layout results are seed-deterministic *and* don't perturb
|
||||
the content stream (lazy === eager is preserved). Class sizes/tints in
|
||||
`classScale`/`classTint`.
|
||||
- **Discovery** — `js/galaxy/Discovery.js` (pure, no Phaser — Node-
|
||||
testable, save-ready: `toJSON()`/`fromJSON()`). Rule: the ship within
|
||||
`game.discovery.distance` (data/game.json, default 540 px) of an
|
||||
|
|
@ -241,9 +248,11 @@ The ship starts with **one level-1 tether** anchored on the home world
|
|||
visual/feel parameter.
|
||||
|
||||
**Upgrade economics (planned):** radius(level) = 5120 × 1.25^(level−1) —
|
||||
level 4 = 10000 px, which reaches the far system orbit (maxOrbit 8800)
|
||||
plus rim clearance. Extra tethers anchored on planets/stations will be the
|
||||
"expand your reach" verb; the seams are in place.
|
||||
level 4 = 10000 px, which reaches every object of a ≤ 10-object system
|
||||
(all within 10240 px of the home world); the far 8-ring of the rare
|
||||
11-object system (up to ~13380 px out) wants one level more. Extra
|
||||
tethers anchored on planets/stations will be the "expand your reach" verb;
|
||||
the seams are in place.
|
||||
|
||||
**Phaser v4 note:** the barrier is two `Graphics` layers redrawn per frame
|
||||
(`clear()` → one `strokePath` per pass) — no textures, no physics bodies,
|
||||
|
|
|
|||
|
|
@ -90,8 +90,6 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
habitable: pclass === 'rocky' && rng.chance(attr.habitability ?? 0.1),
|
||||
});
|
||||
}
|
||||
layoutSystemPlanets(galaxy.seed, record.id, planets);
|
||||
|
||||
// --- Settlements (the lived-in layer) ---------------------------------
|
||||
const settlements = generateSettlements({
|
||||
rng,
|
||||
|
|
@ -104,6 +102,12 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
density: settlementDensity(galaxy, record),
|
||||
});
|
||||
|
||||
// --- Layout: orbits around the home world -----------------------------
|
||||
// Planets and free-space stations are the system's layout objects — each
|
||||
// gets an x/y (see layoutSystem for the spacing rules and the N=11 case).
|
||||
const freeSpace = settlements.filter((s) => s.anchor?.type === 'space');
|
||||
layoutSystem(galaxy.seed, record.id, planets, freeSpace);
|
||||
|
||||
// --- Debris belt & system-level hazard --------------------------------
|
||||
const belt = {
|
||||
present: rng.chance(attr.beltChance ?? 0.35),
|
||||
|
|
@ -125,59 +129,138 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Top-down layout of a system's worlds (the play field around the origin,
|
||||
* where the player's home world sits).
|
||||
* Top-down layout of a system's objects — its planets and its free-space
|
||||
* stations — as one or two ORBITS (rings) around the home world, which
|
||||
* always sits at the system origin (the game renders a solid world there;
|
||||
* see GameScene).
|
||||
*
|
||||
* Each planet record gains:
|
||||
* x, y — world position (the game renders it there),
|
||||
* scale — size multiplier (data/planets.json → classScale, so gas
|
||||
* giants read bigger than rockies).
|
||||
* Each object gains x, y (world position). Planets also gain scale (size
|
||||
* multiplier, data/planets.json → classScale — the spacing rules below are
|
||||
* center-to-center, but the rendered discs still scale by class).
|
||||
*
|
||||
* Worlds are scattered in the annulus minOrbit–maxOrbit px from the
|
||||
* origin and kept at least minEdgeGap px (edge-to-edge) apart from each
|
||||
* other and from the origin (data/planets.json → solarSystem).
|
||||
* The hard spacing rules (data/planets.json → solarSystem):
|
||||
* minSpacing — no two objects (planets, space stations, the home world)
|
||||
* may be closer than this, center to center;
|
||||
* maxNeighbor — whenever a system holds more than one object, every
|
||||
* object must be within this of at least one other.
|
||||
*
|
||||
* Determinism: draws come from a DEDICATED fork — (seed, 'system', id,
|
||||
* 'layout') — so layout never perturbs the star/planet/settlement draws
|
||||
* A ring of k objects at radius R around the origin satisfies both at once:
|
||||
* - object ⇄ home-world distance is R, so R ∈ [minSpacing, maxNeighbor]
|
||||
* takes care of the home world's own pair of constraints;
|
||||
* - the closest object-object pair on a regular k-gon is an edge,
|
||||
* 2·R·sin(π/k) — an edge is the shortest chord (vertices further around
|
||||
* are further), so edge ≥ minSpacing covers every pair;
|
||||
* - every object's nearest neighbor is then the home world, at
|
||||
* R ≤ maxNeighbor.
|
||||
* That radius range is non-empty for k ≤ 10 (k = 11 would need
|
||||
* R ≥ 10905, which already strands the home world beyond maxNeighbor). So
|
||||
* the 11-object maximum (9 planets + 2 stations — the most the current
|
||||
* data can produce) gets two orbits: an inner 3-ring and an outer 8-ring.
|
||||
* The outer ring's near neighbor is its ring-mate (edge stays in
|
||||
* [minSpacing, maxNeighbor]); the inner ring keeps the home world within
|
||||
* maxNeighbor; and any inner/outer pair is at least R_outer − R_inner ≥
|
||||
* minSpacing apart. Smaller N all share one orbit. (Defensively, beyond
|
||||
* 11 objects the orbits chain outward — rings of ≤ 10, and a lone world
|
||||
* always fits radially outside the previous orbit — so this terminates
|
||||
* for any N.)
|
||||
*
|
||||
* What counts as an object: planets (all of them) and free-space
|
||||
* settlements (anchor.type 'space' — deep-space stations, waypoints).
|
||||
* Planet-bound settlements are features OF their planet (a colony sits on
|
||||
* it) and so are not layout objects of their own.
|
||||
*
|
||||
* Determinism: draws come from the dedicated fork (seed, 'system', id,
|
||||
* 'layout') — layout never perturbs the star/planet/settlement draws
|
||||
* above, and lazy (on-arrival) === eager (generateAll) is preserved.
|
||||
*/
|
||||
function layoutSystemPlanets(seed, systemId, planets) {
|
||||
function layoutSystem(seed, systemId, planets, freeSpace) {
|
||||
const band = config.get('planets.solarSystem', {});
|
||||
const minOrbit = band.minOrbit ?? 2600;
|
||||
const maxOrbit = band.maxOrbit ?? 8800;
|
||||
const minGap = band.minEdgeGap ?? 1200;
|
||||
const baseR = (config.get('planets.frameWidth', 1024) * config.get('planets.scale', 1)) / 2;
|
||||
|
||||
const lay = Rng.derive(seed, 'system', systemId, 'layout');
|
||||
// The origin is occupied by a world (the home world) — respect it.
|
||||
const placed = [{ x: 0, y: 0, r: baseR }];
|
||||
if (band.enabled === false) return;
|
||||
const MIN_SEP = band.minSpacing ?? 6144;
|
||||
const MAX_NBR = band.maxNeighbor ?? 10240;
|
||||
|
||||
// Rendered size per class (visual only — the spacing rules are
|
||||
// center-to-center, not edge-to-edge).
|
||||
for (const p of planets) {
|
||||
const scale = config.get(`planets.classScale.${p.class}`, 1) ?? 1;
|
||||
const r = baseR * scale;
|
||||
let ok = false;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
for (let attempt = 0; attempt < 24 && !ok; attempt++) {
|
||||
const ang = lay.range(0, Math.PI * 2);
|
||||
const d = lay.range(minOrbit, maxOrbit);
|
||||
x = d * Math.cos(ang);
|
||||
y = d * Math.sin(ang);
|
||||
ok = placed.every((q) => Math.hypot(x - q.x, y - q.y) >= q.r + r + minGap);
|
||||
p.scale = config.get(`planets.classScale.${p.class}`, 1) ?? 1;
|
||||
}
|
||||
|
||||
// Slot assignment order: planets in orbital order, then free-space
|
||||
// stations in generation order (deep-space station, then waypoint) —
|
||||
// stable per system, so lazy === eager.
|
||||
const objects = [...planets, ...freeSpace];
|
||||
const N = objects.length;
|
||||
if (N === 0) return;
|
||||
|
||||
const TAU = Math.PI * 2;
|
||||
const lay = Rng.derive(seed, 'system', systemId, 'layout');
|
||||
const edgeFactor = (k) => (k <= 1 ? 1 : 2 * Math.sin(Math.PI / k));
|
||||
const slots = [];
|
||||
let prevR = 0;
|
||||
let prevAngle = 0;
|
||||
|
||||
const placeRing = (k, lo, hi) => {
|
||||
const R = lay.range(lo, hi);
|
||||
// A LONE object on an outer orbit sits radially outside an
|
||||
// already-placed one: its guaranteed neighbor is then exactly
|
||||
// R − prevR away, whereas a random angle could leave it more than
|
||||
// maxNeighbor from every inner object.
|
||||
const th0 = k === 1 && prevR > 0 ? prevAngle : lay.range(0, TAU);
|
||||
for (let i = 0; i < k; i++) {
|
||||
const th = th0 + (k === 1 ? 0 : (TAU * i) / k);
|
||||
slots.push({ x: R * Math.cos(th), y: R * Math.sin(th) });
|
||||
}
|
||||
if (!ok) {
|
||||
// The band is (nearly) full — spiral outward. A deterministic
|
||||
// escape hatch: with the default band and ≤ 9 worlds per system
|
||||
// this never actually triggers.
|
||||
const ang = lay.range(0, Math.PI * 2);
|
||||
const d = maxOrbit + r + minGap + placed.length * (r + minGap);
|
||||
x = d * Math.cos(ang);
|
||||
y = d * Math.sin(ang);
|
||||
prevR = R;
|
||||
prevAngle = th0;
|
||||
};
|
||||
|
||||
if (N <= 10) {
|
||||
// One orbit around the home world (k = 1: a single object, whose only
|
||||
// neighbor is the home world — fine, it's still within [MIN_SEP, MAX_NBR]).
|
||||
const f = edgeFactor(N);
|
||||
placeRing(N, Math.max(MIN_SEP, MIN_SEP / f), MAX_NBR);
|
||||
} else if (N === 11) {
|
||||
// Inner 3-ring: the home world must stay within MAX_NBR of it (R1 ≤
|
||||
// MAX_NBR), and the outer 8-ring must sit at least MIN_SEP beyond it
|
||||
// at a radius where its edge is still ≤ MAX_NBR:
|
||||
// R1 ≤ MAX_NBR / edgeFactor(8) − MIN_SEP.
|
||||
const f8 = edgeFactor(8);
|
||||
placeRing(
|
||||
3,
|
||||
Math.max(MIN_SEP, MIN_SEP / edgeFactor(3)),
|
||||
Math.min(MAX_NBR, MAX_NBR / f8 - MIN_SEP),
|
||||
);
|
||||
const R1 = prevR;
|
||||
// Outer 8-ring: ring-mates are its near neighbors (edge in
|
||||
// [MIN_SEP, MAX_NBR]); every inner object is ≥ R2 − R1 ≥ MIN_SEP away.
|
||||
placeRing(
|
||||
8,
|
||||
Math.max(MIN_SEP / f8, R1 + MIN_SEP),
|
||||
Math.min(MAX_NBR / f8, R1 + MAX_NBR),
|
||||
);
|
||||
} else {
|
||||
// Beyond the data maximum: chain orbits outward. A ring of ≤ 10
|
||||
// objects always has a valid radius as ring 1, a lone world always
|
||||
// fits on any outer orbit — the loop terminates for any N.
|
||||
let rem = N;
|
||||
while (rem > 0) {
|
||||
const k = Math.min(rem, 10);
|
||||
const f = edgeFactor(k);
|
||||
const lo = prevR === 0 ? Math.max(MIN_SEP, MIN_SEP / f) : Math.max(MIN_SEP / f, prevR + MIN_SEP);
|
||||
const hi = prevR === 0 ? MAX_NBR : Math.min(MAX_NBR / f, prevR + MAX_NBR);
|
||||
if (lo <= hi) {
|
||||
placeRing(k, lo, hi);
|
||||
rem -= k;
|
||||
} else {
|
||||
placeRing(1, prevR + MIN_SEP, prevR + MAX_NBR);
|
||||
rem -= 1;
|
||||
}
|
||||
}
|
||||
p.scale = scale;
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
placed.push({ x, y, r });
|
||||
}
|
||||
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
objects[i].x = slots[i].x;
|
||||
objects[i].y = slots[i].y;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,8 +100,8 @@ export class GameScene extends Phaser.Scene {
|
|||
this.planet.setDepth(5); // above the starfield (depths 0–2), below the ship (10)
|
||||
|
||||
// The rest of the solar system — the generated worlds, placed by the
|
||||
// generator (data/planets.json → solarSystem) in a band around the home
|
||||
// world. Same solid-disc rules as home: fly close, never through.
|
||||
// generator (data/planets.json → solarSystem) on orbits around the
|
||||
// home world. Same solid-disc rules as home: fly close, never through.
|
||||
this.systemPlanets = [];
|
||||
if (config.get('planets.solarSystem.enabled', true)) {
|
||||
for (const rec of this.systemContent.planets ?? []) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue