import { solarSystemDefinitions } from "../data/catalog"; import type { AsteroidFieldDefinition, FactionDefinition, PatrolRouteDefinition, PlanetDefinition, ResourceNodeDefinition, ScenarioDefinition, SolarSystemDefinition, UniverseDefinition, } from "../types"; const TOTAL_SYSTEMS = 28; const STAR_PALETTES = [ { starColor: "#ffd27a", starGlow: "#ffb14a" }, { starColor: "#9dc6ff", starGlow: "#66a0ff" }, { starColor: "#ffb7a1", starGlow: "#ff7d66" }, { starColor: "#f3f0ff", starGlow: "#b49cff" }, { starColor: "#b6ffe0", starGlow: "#5ed6b1" }, { starColor: "#ffe49a", starGlow: "#ffc14a" }, ]; const PLANET_COLORS = ["#d4a373", "#58a36c", "#6ea7d4", "#6958a8", "#c48f6a", "#4f84c4", "#8f8fb0", "#d46e8a"]; const FRONTIER_PREFIXES = ["Aquila", "Draco", "Lyra", "Cygnus", "Orion", "Vela", "Carina", "Pavo", "Vesper", "Altair"]; const FRONTIER_SUFFIXES = ["Reach", "Gate", "Crown", "Run", "March", "Drift", "Anchor", "Span", "Wake", "Vale"]; const EMPIRE_ARCHETYPES = [ { id: "solar-dominion", label: "Solar Dominion", color: "#f0c36d", accent: "#ffefb0" }, { id: "aegis-state", label: "Aegis State", color: "#72b7ff", accent: "#d5ecff" }, { id: "verdant-combine", label: "Verdant Combine", color: "#77dd8c", accent: "#d7ffe2" }, { id: "iron-clans", label: "Iron Clans", color: "#ff926c", accent: "#ffd8c9" }, ]; const PIRATE_ARCHETYPES = [ { id: "black-flag", label: "Black Flag Cartel", color: "#ff5a6f", accent: "#ffd0d6" }, { id: "void-rats", label: "Void Rats", color: "#9a7cff", accent: "#e7dcff" }, { id: "grim-sons", label: "Grim Sons", color: "#ff8d54", accent: "#ffe1d1" }, { id: "night-jackals", label: "Night Jackals", color: "#a0ff7f", accent: "#e8ffd8" }, { id: "red-knives", label: "Red Knives", color: "#ff6a8c", accent: "#ffd7e2" }, { id: "dust-serpents", label: "Dust Serpents", color: "#c2a56f", accent: "#f0e1c3" }, ]; export function generateUniverse(seed = Math.floor(Math.random() * 0x7fffffff)): UniverseDefinition { const rng = createRng(seed); const systems: SolarSystemDefinition[] = []; const empires: FactionDefinition[] = []; const pirates: FactionDefinition[] = []; const centralSystems = Array.from({ length: 3 }, (_, index) => createCentralSystem(index, rng)); systems.push(...centralSystems); EMPIRE_ARCHETYPES.forEach((archetype, index) => { const angle = (index / EMPIRE_ARCHETYPES.length) * Math.PI * 2; const capitalSystem = createEmpireCapitalSystem(archetype.label, archetype.id, angle, rng); const miningSystem = createEmpireMiningSystem(archetype.label, archetype.id, angle + 0.22, rng); systems.push(capitalSystem, miningSystem); empires.push({ id: archetype.id, label: archetype.label, kind: "empire", color: archetype.color, accent: archetype.accent, homeSystemId: capitalSystem.id, miningSystemId: miningSystem.id, targetSystemIds: centralSystems.map((system) => system.id), rivals: EMPIRE_ARCHETYPES.filter((_, rivalIndex) => rivalIndex !== index).map((rival) => rival.id), }); }); PIRATE_ARCHETYPES.forEach((archetype, index) => { const targetEmpire = empires[index % empires.length]; const secondaryEmpire = empires[(index + 1) % empires.length]; const pirateSystem = createPirateBaseSystem(archetype.label, archetype.id, index, rng); systems.push(pirateSystem); pirates.push({ id: archetype.id, label: archetype.label, kind: "pirate", color: archetype.color, accent: archetype.accent, homeSystemId: pirateSystem.id, targetSystemIds: [targetEmpire.homeSystemId, targetEmpire.miningSystemId ?? targetEmpire.homeSystemId], rivals: [targetEmpire.id, secondaryEmpire.id], pirateForFactionId: targetEmpire.id, }); }); while (systems.length < TOTAL_SYSTEMS) { systems.push(createFrontierSystem(systems.length, rng)); } const factions = [...empires, ...pirates]; empires.forEach((empire, index) => { empire.rivals.push( pirates[index].id, pirates[(index + 4) % pirates.length].id, ); }); return { seed, label: `Autonomous Cluster ${seed.toString(16).toUpperCase()}`, systems, scenario: createScenario(systems, factions), }; } function createScenario(systems: SolarSystemDefinition[], factions: FactionDefinition[]): ScenarioDefinition { const empires = factions.filter((faction) => faction.kind === "empire"); const pirates = factions.filter((faction) => faction.kind === "pirate"); const initialStations: ScenarioDefinition["initialStations"] = []; const shipFormations: ScenarioDefinition["shipFormations"] = []; const patrolRoutes: PatrolRouteDefinition[] = []; const centralSystemIds = systems.filter((system) => system.id.startsWith("central-")).map((system) => system.id); empires.forEach((faction) => { const capital = systems.find((system) => system.id === faction.homeSystemId); const mining = systems.find((system) => system.id === faction.miningSystemId); if (!capital || !mining) { return; } initialStations.push( { constructibleId: "manufactory", systemId: capital.id, factionId: faction.id, planetIndex: Math.min(1, capital.planets.length - 1), lagrangeSide: 1, seedStock: { water: 40 }, }, { constructibleId: "refinery", systemId: mining.id, factionId: faction.id, planetIndex: 0, lagrangeSide: 1, seedStock: { ore: 60 }, }, ); shipFormations.push({ shipId: "miner", count: 1, center: localPoint(mining, 180, 100), systemId: mining.id, factionId: faction.id, }); patrolRoutes.push(createPatrolRoute(capital), createPatrolRoute(mining)); }); pirates.forEach((faction) => { const base = systems.find((system) => system.id === faction.homeSystemId); if (!base) { return; } initialStations.push( { constructibleId: "trade-hub", systemId: base.id, factionId: faction.id, planetIndex: 0, lagrangeSide: 1, seedStock: { "refined-metals": 60, "ammo-crates": 12 }, }, ); shipFormations.push({ shipId: "frigate", count: 1, center: localPoint(base, 180, 60), systemId: base.id, factionId: faction.id, }); patrolRoutes.push(createPatrolRoute(base)); }); const firstEmpire = empires[0]; return { initialStations, shipFormations, patrolRoutes, miningDefaults: { nodeSystemId: firstEmpire.miningSystemId ?? firstEmpire.homeSystemId, refinerySystemId: firstEmpire.homeSystemId, }, factions, centralSystemIds, }; } function createEmpireCapitalSystem(label: string, factionId: string, angle: number, rng: () => number): SolarSystemDefinition { const base = solarSystemDefinitions[0]; const radius = 6200 + Math.floor(rng() * 700); return { ...base, id: `${factionId}-capital`, label: `${label} Prime`, position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)], starSize: 48 + Math.floor(rng() * 10), gravityWellRadius: 210 + Math.floor(rng() * 18), asteroidField: createAsteroidFieldDefinition(rng, false), resourceNodes: [], planets: createPlanets(4 + Math.floor(rng() * 2), rng), }; } function createEmpireMiningSystem(label: string, factionId: string, angle: number, rng: () => number): SolarSystemDefinition { const base = solarSystemDefinitions[1]; const radius = 7700 + Math.floor(rng() * 900); return { ...base, id: `${factionId}-belt`, label: `${label} Belt`, position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)], starSize: 42 + Math.floor(rng() * 10), gravityWellRadius: 220 + Math.floor(rng() * 20), asteroidField: createAsteroidFieldDefinition(rng, true), resourceNodes: createResourceNodes(4 + Math.floor(rng() * 2), rng, 3600, 5200), planets: createPlanets(3 + Math.floor(rng() * 2), rng), }; } function createCentralSystem(index: number, rng: () => number): SolarSystemDefinition { const palette = STAR_PALETTES[(index + 1) % STAR_PALETTES.length]; const angle = (index / 3) * Math.PI * 2 + rng() * 0.3; const radius = 900 + Math.floor(rng() * 500); return { id: `central-${index + 1}`, label: ["Crown Basin", "Throne Verge", "Golden Axis"][index] ?? `Central ${index + 1}`, position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)], starColor: palette.starColor, starGlow: palette.starGlow, starSize: 50 + Math.floor(rng() * 14), gravityWellRadius: 240 + Math.floor(rng() * 28), asteroidField: createAsteroidFieldDefinition(rng, true), resourceNodes: createResourceNodes(6 + Math.floor(rng() * 3), rng, 5200, 7600), planets: createPlanets(4 + Math.floor(rng() * 2), rng), }; } function createPirateBaseSystem(label: string, factionId: string, index: number, rng: () => number): SolarSystemDefinition { const palette = STAR_PALETTES[(index + 3) % STAR_PALETTES.length]; const angle = (index / PIRATE_ARCHETYPES.length) * Math.PI * 2 + 0.35; const radius = 9800 + Math.floor(rng() * 1200); return { id: `${factionId}-den`, label: `${label} Den`, position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)], starColor: palette.starColor, starGlow: palette.starGlow, starSize: 36 + Math.floor(rng() * 10), gravityWellRadius: 180 + Math.floor(rng() * 30), asteroidField: createAsteroidFieldDefinition(rng, true), resourceNodes: createResourceNodes(2 + Math.floor(rng() * 2), rng, 1600, 2600), planets: createPlanets(2 + Math.floor(rng() * 2), rng), }; } function createFrontierSystem(index: number, rng: () => number): SolarSystemDefinition { const angle = index * 2.399963229728653 + rng() * 0.4; const radius = 3600 + 900 * Math.sqrt(index) + rng() * 600; const palette = STAR_PALETTES[Math.floor(rng() * STAR_PALETTES.length)]; const hasResources = rng() > 0.45; return { id: `frontier-${index + 1}`, label: `${FRONTIER_PREFIXES[index % FRONTIER_PREFIXES.length]} ${FRONTIER_SUFFIXES[Math.floor(rng() * FRONTIER_SUFFIXES.length)]}`, position: [Math.round(Math.cos(angle) * radius), 0, Math.round(Math.sin(angle) * radius)], starColor: palette.starColor, starGlow: palette.starGlow, starSize: 34 + Math.round(rng() * 18), gravityWellRadius: 185 + Math.round(rng() * 60), asteroidField: createAsteroidFieldDefinition(rng, hasResources), resourceNodes: hasResources ? createResourceNodes(1 + Math.floor(rng() * 3), rng, 1800, 3400) : [], planets: createPlanets(2 + Math.floor(rng() * 3), rng), }; } function createAsteroidFieldDefinition(rng: () => number, dense: boolean): AsteroidFieldDefinition { return { decorationCount: dense ? 180 + Math.floor(rng() * 70) : 90 + Math.floor(rng() * 70), radiusOffset: 290 + Math.floor(rng() * 100), radiusVariance: 70 + Math.floor(rng() * 80), heightVariance: 12 + Math.floor(rng() * 12), }; } function createPlanets(count: number, rng: () => number): PlanetDefinition[] { const planets: PlanetDefinition[] = []; let orbitRadius = 150 + Math.floor(rng() * 40); for (let index = 0; index < count; index += 1) { orbitRadius += 120 + Math.floor(rng() * 90); planets.push({ label: `${String.fromCharCode(65 + index)}-${Math.floor(rng() * 90 + 10)}`, orbitRadius, orbitSpeed: Number((0.05 + rng() * 0.14).toFixed(3)), size: 18 + Math.floor(rng() * 30), color: PLANET_COLORS[Math.floor(rng() * PLANET_COLORS.length)], tilt: Number(((rng() - 0.5) * 0.8).toFixed(2)), hasRing: rng() > 0.72, }); } return planets; } function createResourceNodes(count: number, rng: () => number, minOre: number, maxOre: number): ResourceNodeDefinition[] { return Array.from({ length: count }, (_, index) => ({ angle: Number((((index / count) * Math.PI * 2 + rng() * 0.7) % (Math.PI * 2)).toFixed(6)), radiusOffset: 300 + Math.floor(rng() * 140), oreAmount: minOre + Math.floor(rng() * (maxOre - minOre)), itemId: "ore", shardCount: 5 + Math.floor(rng() * 5), })); } function createPatrolRoute(system: SolarSystemDefinition): PatrolRouteDefinition { return { systemId: system.id, points: [ localPoint(system, 160, 90), localPoint(system, 340, -180), localPoint(system, 560, 210), localPoint(system, 240, 340), ], }; } function localPoint(system: SolarSystemDefinition, x: number, z: number): [number, number, number] { return [system.position[0] + x, 0, system.position[2] + z]; } function createRng(seed: number) { let value = seed >>> 0; return () => { value += 0x6d2b79f5; let result = Math.imul(value ^ (value >>> 15), 1 | value); result ^= result + Math.imul(result ^ (result >>> 7), 61 | result); return ((result ^ (result >>> 14)) >>> 0) / 4294967296; }; }