Refactor universe sim and observer HUD
This commit is contained in:
353
src/game/world/universeGenerator.ts
Normal file
353
src/game/world/universeGenerator.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
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: "trade-hub", systemId: capital.id, factionId: faction.id, planetIndex: 1, lagrangeSide: 1 },
|
||||
{
|
||||
constructibleId: "farm-ring",
|
||||
systemId: capital.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: 0,
|
||||
lagrangeSide: -1,
|
||||
seedStock: { gas: 120, water: 160 },
|
||||
},
|
||||
{
|
||||
constructibleId: "manufactory",
|
||||
systemId: capital.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: Math.min(2, capital.planets.length - 1),
|
||||
lagrangeSide: 1,
|
||||
seedStock: { "refined-metals": 200, water: 100, "ship-equipment": 40, "naval-guns": 24 },
|
||||
},
|
||||
{
|
||||
constructibleId: "shipyard",
|
||||
systemId: capital.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: Math.min(3, capital.planets.length - 1),
|
||||
lagrangeSide: -1,
|
||||
seedStock: { "ship-parts": 80, "ammo-crates": 70, "hull-sections": 100, "ship-equipment": 40 },
|
||||
},
|
||||
{ constructibleId: "defense-grid", systemId: capital.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
||||
{
|
||||
constructibleId: "refinery",
|
||||
systemId: mining.id,
|
||||
factionId: faction.id,
|
||||
planetIndex: 0,
|
||||
lagrangeSide: 1,
|
||||
seedStock: { ore: 240, "refined-metals": 80 },
|
||||
},
|
||||
{ constructibleId: "defense-grid", systemId: mining.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
||||
);
|
||||
|
||||
shipFormations.push(
|
||||
{ shipId: "frigate", count: 1, center: localPoint(capital, 180, 120), systemId: capital.id, factionId: faction.id },
|
||||
{ shipId: "hauler", count: 1, center: localPoint(capital, 280, -120), systemId: capital.id, factionId: faction.id },
|
||||
{ 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": 100, "ship-parts": 30, "ammo-crates": 30 },
|
||||
},
|
||||
{ constructibleId: "defense-grid", systemId: base.id, factionId: faction.id, planetIndex: 1, lagrangeSide: -1 },
|
||||
);
|
||||
shipFormations.push(
|
||||
{ shipId: "frigate", count: 4, center: localPoint(base, 180, 60), systemId: base.id, factionId: faction.id },
|
||||
{ shipId: "destroyer", count: 2, center: localPoint(base, 250, 120), systemId: base.id, factionId: faction.id },
|
||||
{ shipId: "hauler", count: 1, center: localPoint(base, 320, -90), 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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user