import { makeRng, hash32 } from './rng.js'; const SYSTEM_NAMES = [ 'Arcadia', 'Boreas', 'Cygni', 'Drakon', 'Eridu', 'Fenrir', 'Gorgon', 'Helix', 'Icarus', 'Juno', 'Kepler', 'Lyra', 'Mira', 'Nyx', 'Orion', 'Pavo', 'Quasar', 'Rigel', 'Sirius', 'Talos', 'Umbra', 'Vega', 'Wraith', 'Xandar', 'Yara', 'Zenith', 'Altair', 'Bellatrix', 'Castor', 'Deneb', ]; const PLANET_SUFFIX = ['Prime', 'II', 'III']; const PROFILES = [ { kind: 'agri', produces: { food: 10, meds: 3 }, consumes: { fuel: 4, tech: 3 } }, { kind: 'mining', produces: { ore: 12, fuel: 6 }, consumes: { food: 6, meds: 2 } }, { kind: 'industrial', produces: { tech: 6 }, consumes: { ore: 8, fuel: 5, food: 5 } }, { kind: 'refinery', produces: { fuel: 10 }, consumes: { ore: 6, food: 4 } }, { kind: 'resort', produces: { luxury: 5 }, consumes: { food: 7, meds: 4, fuel: 3 } }, ]; const ALL_GOODS = ['ore', 'food', 'fuel', 'tech', 'meds', 'luxury']; export function otherEnd(lane, systemId) { return lane.a === systemId ? lane.b : lane.a; } export function generateGalaxy(seed, { systemCount = 25 } = {}) { const rng = makeRng(hash32('galaxy:' + seed)); // 1. Place systems with minimum spacing (relaxes if placement gets tight). const systems = []; let minDist = 95, attempts = 0; while (systems.length < systemCount) { const x = 60 + rng() * 880, y = 50 + rng() * 600; if (systems.every((s) => Math.hypot(s.x - x, s.y - y) > minDist)) { systems.push({ id: systems.length, name: SYSTEM_NAMES[systems.length % SYSTEM_NAMES.length], x, y, planets: [], lanes: [] }); } if (++attempts > 2000) { minDist *= 0.9; attempts = 0; } } // 2. Lanes: each system to its 2 nearest neighbors, deduplicated. const laneKeys = new Set(); const key = (a, b) => (a < b ? `${a}-${b}` : `${b}-${a}`); for (const s of systems) { const near = systems .filter((o) => o.id !== s.id) .sort((p, q) => Math.hypot(p.x - s.x, p.y - s.y) - Math.hypot(q.x - s.x, q.y - s.y)); laneKeys.add(key(s.id, near[0].id)); laneKeys.add(key(s.id, near[1].id)); } // 3. Connectivity repair: link closest pair across components until one component. const parent = systems.map((_, i) => i); const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i]))); const union = (a, b) => { parent[find(a)] = find(b); }; for (const k of laneKeys) { const [a, b] = k.split('-').map(Number); union(a, b); } for (;;) { const roots = new Set(systems.map((s) => find(s.id))); if (roots.size === 1) break; let best = null; for (const s of systems) for (const o of systems) { if (find(s.id) === find(o.id)) continue; const d = Math.hypot(s.x - o.x, s.y - o.y); if (!best || d < best.d) best = { a: s.id, b: o.id, d }; } laneKeys.add(key(best.a, best.b)); union(best.a, best.b); } // 4. Materialize lanes with travel time and hazard. const lanes = [...laneKeys].sort().map((k, i) => { const [a, b] = k.split('-').map(Number); const dist = Math.hypot(systems[a].x - systems[b].x, systems[a].y - systems[b].y); const days = Math.min(4, Math.max(1, Math.round(dist / 80))); const hazard = rng() < 0.2 ? +(0.3 + rng() * 0.4).toFixed(3) : +(rng() * 0.15).toFixed(3); return { id: i, a, b, days, hazard }; }); for (const lane of lanes) { systems[lane.a].lanes.push(lane.id); systems[lane.b].lanes.push(lane.id); } // 5. Planets with economy profiles and a render orbit. const planets = []; for (const s of systems) { const n = 1 + Math.floor(rng() * 3); for (let i = 0; i < n; i++) { const profile = PROFILES[Math.floor(rng() * PROFILES.length)]; const stock = {}; for (const g of ALL_GOODS) { if (profile.produces[g]) stock[g] = Math.floor(150 + rng() * 150); else if (profile.consumes[g]) stock[g] = Math.floor(40 + rng() * 80); else stock[g] = Math.floor(80 + rng() * 70); } const planet = { id: planets.length, systemId: s.id, name: `${s.name} ${PLANET_SUFFIX[i]}`, kind: profile.kind, produces: { ...profile.produces }, consumes: { ...profile.consumes }, stock, stockTime: 0, orbit: { r: 90 + i * 55, a: rng() * Math.PI * 2 }, }; planets.push(planet); s.planets.push(planet.id); } } return { systems, lanes, planets }; } // All-pairs hop counts (BFS per system). hops[a][b] = lanes to cross. export function hopMatrix(galaxy) { const n = galaxy.systems.length; const m = Array.from({ length: n }, () => Array(n).fill(Infinity)); for (let src = 0; src < n; src++) { m[src][src] = 0; const queue = [src]; while (queue.length) { const cur = queue.shift(); for (const laneId of galaxy.systems[cur].lanes) { const next = otherEnd(galaxy.lanes[laneId], cur); if (m[src][next] === Infinity) { m[src][next] = m[src][cur] + 1; queue.push(next); } } } } return m; } // Dijkstra by travel days. Returns { lanes: [laneId...], days } or null. export function findRoute(galaxy, fromSys, toSys) { if (fromSys === toSys) return { lanes: [], days: 0 }; const dist = new Map([[fromSys, 0]]); const prevLane = new Map(); const visited = new Set(); for (;;) { let cur = null, best = Infinity; for (const [s, d] of dist) if (!visited.has(s) && d < best) { best = d; cur = s; } if (cur === null) return null; if (cur === toSys) break; visited.add(cur); for (const laneId of galaxy.systems[cur].lanes) { const lane = galaxy.lanes[laneId]; const next = otherEnd(lane, cur); const nd = best + lane.days; if (nd < (dist.get(next) ?? Infinity)) { dist.set(next, nd); prevLane.set(next, laneId); } } } const laneIds = []; let s = toSys; while (s !== fromSys) { const laneId = prevLane.get(s); laneIds.unshift(laneId); s = otherEnd(galaxy.lanes[laneId], s); } return { lanes: laneIds, days: dist.get(toSys) }; }