using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class ScenarioLoader { private const string SolSystemId = "sol"; private const string DevelopmentCompanionSystemId = "helios"; private static List InjectSpecialSystems( IReadOnlyList authoredSystems, bool includeSolSystem) { var systems = authoredSystems .Select(CloneSystemDefinition) .ToList(); if (includeSolSystem && systems.All((system) => system.Id != "sol")) { systems.Add(CreateSolSystem()); } return systems; } private static List ExpandSystems( IReadOnlyList authoredSystems, int targetSystemCount) { var systems = authoredSystems .Select(CloneSystemDefinition) .ToList(); if (targetSystemCount <= 0) { return []; } if (systems.Count > targetSystemCount) { return TrimSystemsToTarget(systems, targetSystemCount); } if (systems.Count >= targetSystemCount || authoredSystems.Count == 0) { return systems; } var existingIds = systems .Select((system) => system.Id) .ToHashSet(StringComparer.Ordinal); var generatedPositions = BuildGalaxyPositions( authoredSystems.Select((system) => ToVector(system.Position)).ToList(), targetSystemCount - systems.Count); for (var index = systems.Count; index < targetSystemCount; index += 1) { var template = authoredSystems[index % authoredSystems.Count]; var name = GeneratedSystemNames[(index - authoredSystems.Count) % GeneratedSystemNames.Length]; var id = BuildGeneratedSystemId(name, index + 1); while (!existingIds.Add(id)) { id = $"{id}-x"; } systems.Add(CreateGeneratedSystem(template, name, id, index - authoredSystems.Count, generatedPositions[index - authoredSystems.Count])); } return systems; } private static List TrimSystemsToTarget( IReadOnlyList systems, int targetSystemCount) { var selected = new List(targetSystemCount); void AddById(string systemId) { var system = systems.FirstOrDefault((candidate) => string.Equals(candidate.Id, systemId, StringComparison.Ordinal)); if (system is not null && selected.All((candidate) => !string.Equals(candidate.Id, systemId, StringComparison.Ordinal))) { selected.Add(system); } } AddById(SolSystemId); AddById(DevelopmentCompanionSystemId); foreach (var system in systems) { if (selected.Count >= targetSystemCount) { break; } if (selected.Any((candidate) => string.Equals(candidate.Id, system.Id, StringComparison.Ordinal))) { continue; } selected.Add(system); } if (selected.Count > 0 && selected.Count <= 4) { ApplyCompactGalaxyLayout(selected); } return selected; } private static void ApplyCompactGalaxyLayout(IReadOnlyList systems) { var compactPositions = new[] { new[] { 0f, 0f, 0f }, new[] { 2.6f, 0.02f, -0.42f }, new[] { -2.4f, -0.04f, 0.56f }, new[] { 0.52f, 0.04f, 2.48f }, }; for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1) { systems[index].Position = compactPositions[index]; } } private static SolarSystemDefinition CreateGeneratedSystem( SolarSystemDefinition template, string label, string id, int generatedIndex, Vector3 position) { var starProfile = SelectStarProfile(generatedIndex); var planets = BuildGeneratedPlanets(template, generatedIndex); var resourceNodes = BuildProceduralResourceNodes(template, planets, generatedIndex) .Select((node) => new ResourceNodeDefinition { SourceKind = node.SourceKind, Angle = node.Angle, RadiusOffset = node.RadiusOffset, InclinationDegrees = node.InclinationDegrees, AnchorPlanetIndex = node.AnchorPlanetIndex, AnchorMoonIndex = node.AnchorMoonIndex, OreAmount = node.OreAmount, ItemId = node.ItemId, ShardCount = node.ShardCount, }) .ToList(); return new SolarSystemDefinition { Id = id, Label = label, Position = [position.X, position.Y, position.Z], StarKind = starProfile.Kind, StarCount = starProfile.StarCount, StarColor = starProfile.StarColor, StarGlow = starProfile.StarGlow, StarSize = starProfile.BaseSize + ((generatedIndex % 4) * 2f), GravityWellRadius = template.GravityWellRadius + ((generatedIndex % 3) * 12f), AsteroidField = new AsteroidFieldDefinition { DecorationCount = template.AsteroidField.DecorationCount + ((generatedIndex % 5) * 10), RadiusOffset = template.AsteroidField.RadiusOffset + ((generatedIndex % 4) * 18000f), RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12000f), HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4000f), }, ResourceNodes = resourceNodes, Planets = planets, }; } private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition) { return new SolarSystemDefinition { Id = definition.Id, Label = definition.Label, Position = definition.Position.ToArray(), StarKind = definition.StarKind, StarCount = definition.StarCount, StarColor = definition.StarColor, StarGlow = definition.StarGlow, StarSize = definition.StarSize, GravityWellRadius = definition.GravityWellRadius, AsteroidField = new AsteroidFieldDefinition { DecorationCount = definition.AsteroidField.DecorationCount, RadiusOffset = definition.AsteroidField.RadiusOffset, RadiusVariance = definition.AsteroidField.RadiusVariance, HeightVariance = definition.AsteroidField.HeightVariance, }, ResourceNodes = definition.ResourceNodes .Select((node) => new ResourceNodeDefinition { SourceKind = node.SourceKind, Angle = node.Angle, RadiusOffset = node.RadiusOffset, InclinationDegrees = node.InclinationDegrees, AnchorPlanetIndex = node.AnchorPlanetIndex, AnchorMoonIndex = node.AnchorMoonIndex, OreAmount = node.OreAmount, ItemId = node.ItemId, ShardCount = node.ShardCount, }) .ToList(), Planets = definition.Planets .Select((planet) => new PlanetDefinition { Label = planet.Label, PlanetType = planet.PlanetType, Shape = planet.Shape, MoonCount = planet.MoonCount, OrbitRadius = planet.OrbitRadius, OrbitSpeed = planet.OrbitSpeed, OrbitEccentricity = planet.OrbitEccentricity, OrbitInclination = planet.OrbitInclination, OrbitLongitudeOfAscendingNode = planet.OrbitLongitudeOfAscendingNode, OrbitArgumentOfPeriapsis = planet.OrbitArgumentOfPeriapsis, OrbitPhaseAtEpoch = planet.OrbitPhaseAtEpoch, Size = planet.Size, Color = planet.Color, Tilt = planet.Tilt, HasRing = planet.HasRing, }) .ToList(), }; } private static List BuildProceduralResourceNodes( SolarSystemDefinition template, IReadOnlyList planets, int generatedIndex) { var nodes = new List(); if (template.ResourceNodes.Count > 0) { nodes.AddRange(template.ResourceNodes.Select((node) => new ResourceNodeDefinition { SourceKind = node.SourceKind, Angle = node.Angle, RadiusOffset = node.RadiusOffset, InclinationDegrees = node.InclinationDegrees, AnchorPlanetIndex = node.AnchorPlanetIndex, AnchorMoonIndex = node.AnchorMoonIndex, OreAmount = node.OreAmount, ItemId = node.ItemId, ShardCount = node.ShardCount, })); } nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets)); return nodes; } private static List BuildGalaxyPositions(IReadOnlyCollection occupiedPositions, int count) { var allPositions = occupiedPositions.ToList(); var generated = new List(count); for (var index = 0; index < count; index += 1) { Vector3? accepted = null; for (var attempt = 0; attempt < 64; attempt += 1) { var candidate = ComputeGeneratedSystemPosition(index, attempt); if (allPositions.All((existing) => existing.DistanceTo(candidate) >= MinimumSystemSeparation)) { accepted = candidate; break; } } accepted ??= ComputeFallbackGeneratedSystemPosition(index); generated.Add(accepted.Value); allPositions.Add(accepted.Value); } return generated; } private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt) { const int armCount = 4; const float baseInnerRadius = 9f; const float radiusStep = 0.54f; const float armOffset = MathF.PI * 2f / armCount; var armIndex = (generatedIndex + attempt) % armCount; var armDepth = generatedIndex / armCount; var radius = baseInnerRadius + (armDepth * radiusStep) + Jitter(generatedIndex * 17 + attempt, 0, 0.9f); var angle = (armIndex * armOffset) + (radius / 8.2f) + Jitter(generatedIndex, 1 + attempt, 0.16f); var x = MathF.Cos(angle) * radius; var z = MathF.Sin(angle) * radius * 0.58f; var y = ComputeSystemHeight(radius, generatedIndex, attempt); return new Vector3(x, y, z); } private static Vector3 ComputeFallbackGeneratedSystemPosition(int generatedIndex) { const int ringCount = 5; const float fallbackRadius = 42f; var angle = (generatedIndex % ringCount) * (MathF.PI * 2f / ringCount) + (generatedIndex / ringCount) * 0.22f; var radius = fallbackRadius + (generatedIndex / ringCount) * 1.8f; return new Vector3( MathF.Cos(angle) * radius, ComputeSystemHeight(radius, generatedIndex, 99), MathF.Sin(angle) * radius * 0.6f); } private static string BuildGeneratedSystemId(string label, int ordinal) { var slug = string.Concat(label .ToLowerInvariant() .Select((character) => char.IsLetterOrDigit(character) ? character : '-')) .Trim('-'); return $"gen-{ordinal}-{slug}"; } private static IEnumerable BuildAsteroidBeltNodes(int generatedIndex, IReadOnlyList planets) { var nodeCount = 4 + (generatedIndex % 4); var oreAmount = 1000f; for (var index = 0; index < nodeCount; index += 1) { yield return new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f), RadiusOffset = 120000f + Jitter(generatedIndex, 200 + index, 36000f), InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f), AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets), OreAmount = oreAmount, ItemId = "ore", ShardCount = 6 + (index % 4), }; } } private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList planets) { if (planets.Count == 0) { return 0; } var gasGiantIndex = -1; for (var index = 0; index < planets.Count; index += 1) { if (planets[index].PlanetType is "gas-giant" or "ice-giant") { gasGiantIndex = index; break; } } if (gasGiantIndex > 0) { return gasGiantIndex - 1; } return Math.Clamp((planets.Count / 2) - 1, 0, planets.Count - 1); } private static List BuildGeneratedPlanets( SolarSystemDefinition template, int generatedIndex) { var planetCount = 2 + (int)MathF.Floor(Hash01(generatedIndex, 2) * 7f); var planets = new List(planetCount); var orbitRadius = 0.24f + (Hash01(generatedIndex, 3) * 0.12f); var sourcePlanets = template.Planets.Count > 0 ? template.Planets : null; for (var index = 0; index < planetCount; index += 1) { var profile = SelectPlanetProfile(generatedIndex, index); var templatePlanet = sourcePlanets is not null && sourcePlanets.Count > 0 ? sourcePlanets[index % sourcePlanets.Count] : null; orbitRadius += profile.OrbitGapMin + (Hash01(generatedIndex, 10 + index) * (profile.OrbitGapMax - profile.OrbitGapMin)); var orbitEccentricity = 0.01f + (Hash01(generatedIndex, 20 + index) * 0.16f); var orbitInclination = -9f + (Hash01(generatedIndex, 30 + index) * 18f); var moonVariance = (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f); planets.Add(new PlanetDefinition { Label = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}", PlanetType = profile.Type, Shape = profile.Shape, MoonCount = profile.BaseMoonCount + moonVariance, OrbitRadius = orbitRadius, OrbitSpeed = 0.11f / MathF.Sqrt(MathF.Max(orbitRadius * orbitRadius * orbitRadius, 0.02f)), OrbitEccentricity = orbitEccentricity, OrbitInclination = orbitInclination, OrbitLongitudeOfAscendingNode = Hash01(generatedIndex, 120 + index) * 360f, OrbitArgumentOfPeriapsis = Hash01(generatedIndex, 140 + index) * 360f, OrbitPhaseAtEpoch = Hash01(generatedIndex, 160 + index) * 360f, Size = profile.BaseSize + (Hash01(generatedIndex, 50 + index) * (profile.BaseSize * 0.35f)), Color = templatePlanet?.Color ?? profile.Color, Tilt = -0.45f + (Hash01(generatedIndex, 60 + index) * 0.9f), HasRing = profile.CanHaveRing && Hash01(generatedIndex, 70 + index) > 0.55f, }); } return planets; } private static StarProfile SelectStarProfile(int generatedIndex) { var value = Hash01(generatedIndex, 80); return value switch { < 0.32f => StarProfiles[0], < 0.54f => StarProfiles[1], < 0.68f => StarProfiles[5], < 0.8f => StarProfiles[2], < 0.9f => StarProfiles[3], < 0.97f => StarProfiles[6], _ => StarProfiles[4], }; } private static PlanetProfile SelectPlanetProfile(int generatedIndex, int planetIndex) { var value = Hash01(generatedIndex, 90 + planetIndex); return value switch { < 0.14f => PlanetProfiles[7], < 0.28f => PlanetProfiles[0], < 0.46f => PlanetProfiles[3], < 0.62f => PlanetProfiles[1], < 0.74f => PlanetProfiles[2], < 0.86f => PlanetProfiles[4], < 0.94f => PlanetProfiles[6], _ => PlanetProfiles[5], }; } private static string BuildPlanetBaseName(int generatedIndex, int planetIndex) { var source = GeneratedSystemNames[generatedIndex % GeneratedSystemNames.Length] .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0]; return source[..Math.Min(source.Length, 6)]; } private static float ComputeSystemHeight(float radius, int generatedIndex, int salt) { var normalized = MathF.Min(1f, MathF.Max(0f, (radius - 8f) / 28f)); var band = 0.22f + (normalized * 0.76f); return (Hash01(generatedIndex, 100 + salt) * 2f - 1f) * band; } private static float Jitter(int index, int salt, float amplitude) => (Hash01(index, salt) * 2f - 1f) * amplitude; private static float Hash01(int index, int salt) { uint value = (uint)(index + 1); value ^= (uint)(salt + 0x9e3779b9); value *= 0x85ebca6b; value ^= value >> 13; value *= 0xc2b2ae35; value ^= value >> 16; return (value & 0x00ffffff) / 16777215f; } private sealed record StarProfile( string Kind, string StarColor, string StarGlow, float BaseSize, int StarCount); private sealed record PlanetProfile( string Type, string Shape, string Color, float BaseSize, float OrbitGapMin, int BaseMoonCount, bool CanHaveRing) { public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f); } private static SolarSystemDefinition CreateSolSystem() { var mercuryOrbitAu = 0.3871f; var venusOrbitAu = 0.7233f; var earthOrbitAu = 1.000f; var marsOrbitAu = 1.5237f; var jupiterOrbitAu = 5.203f; var saturnOrbitAu = 9.582f; var uranusOrbitAu = 19.201f; var neptuneOrbitAu = 30.047f; return new SolarSystemDefinition { Id = "sol", Label = "Sol", Position = [18.2f, 0.02f, -11.8f], StarKind = "main-sequence", StarCount = 1, StarColor = "#fff1b8", StarGlow = "#ffd35a", StarSize = 696340f, GravityWellRadius = 240f, AsteroidField = new AsteroidFieldDefinition { DecorationCount = 240, RadiusOffset = 422000000f, RadiusVariance = 180000000f, HeightVariance = 22000000f, }, ResourceNodes = [ new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 0.2f, RadiusOffset = 126000f, InclinationDegrees = 4f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, ], Planets = [ CreateSolPlanet("Mercury", "barren", "sphere", 0, mercuryOrbitAu, 0.2056f, 7.0f, 48f, 29f, 252f, "#b7a08f", 0.03f, false), CreateSolPlanet("Venus", "desert", "sphere", 0, venusOrbitAu, 0.0067f, 3.4f, 76f, 54f, 181f, "#d9b38c", 2.64f, false), CreateSolPlanet("Earth", "terrestrial", "sphere", 1, earthOrbitAu, 0.0167f, 0.0f, 0f, 114f, 100f, "#4f84c4", 0.41f, false), CreateSolPlanet("Mars", "desert", "sphere", 2, marsOrbitAu, 0.0934f, 1.85f, 49f, 286f, 54f, "#c56e52", 0.44f, false), CreateSolPlanet("Jupiter", "gas-giant", "oblate", 95, jupiterOrbitAu, 0.0489f, 1.3f, 100f, 275f, 34f, "#d9b06f", 0.05f, true), CreateSolPlanet("Saturn", "gas-giant", "oblate", 146, saturnOrbitAu, 0.0565f, 2.49f, 113f, 339f, 200f, "#dfc27d", 0.47f, true), CreateSolPlanet("Uranus", "ice-giant", "oblate", 28, uranusOrbitAu, 0.046f, 0.77f, 74f, 97f, 130f, "#9fd3df", 1.71f, true), CreateSolPlanet("Neptune", "ice-giant", "oblate", 16, neptuneOrbitAu, 0.009f, 1.77f, 132f, 273f, 256f, "#4c79c9", 0.49f, true) ], }; } private static PlanetDefinition CreateSolPlanet( string label, string planetType, string shape, int moonCount, float orbitRadiusAu, float orbitEccentricity, float orbitInclination, float ascendingNode, float argumentOfPeriapsis, float phaseAtEpoch, string color, float tilt, bool hasRing) { return new PlanetDefinition { Label = label, PlanetType = planetType, Shape = shape, MoonCount = moonCount, OrbitRadius = orbitRadiusAu, OrbitSpeed = ComputeSolOrbitSpeed(orbitRadiusAu), OrbitEccentricity = orbitEccentricity, OrbitInclination = orbitInclination, OrbitLongitudeOfAscendingNode = ascendingNode, OrbitArgumentOfPeriapsis = argumentOfPeriapsis, OrbitPhaseAtEpoch = phaseAtEpoch, Size = planetType switch { "gas-giant" => label == "Saturn" ? 58232f : 69911f, "ice-giant" => label == "Uranus" ? 25362f : 24622f, _ => label switch { "Mercury" => 2440f, "Venus" => 6052f, "Earth" => 6371f, "Mars" => 3390f, _ => 5000f, }, }, Color = color, Tilt = tilt, HasRing = hasRing, }; } private static float ComputeSolOrbitSpeed(float orbitRadiusAu) { const float earthAngularSpeed = 0.11f; return earthAngularSpeed / MathF.Sqrt(orbitRadiusAu * orbitRadiusAu * orbitRadiusAu); } }