using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class ScenarioLoader { private static List InjectSpecialSystems(IReadOnlyList authoredSystems) { var systems = authoredSystems .Select(CloneSystemDefinition) .ToList(); if (systems.All((system) => system.Id != "sol")) { systems.Add(CreateSolSystem()); } return systems; } private static List ExpandSystems(IReadOnlyList authoredSystems) { var systems = authoredSystems .Select(CloneSystemDefinition) .ToList(); 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 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, 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) * 18f), RadiusVariance = template.AsteroidField.RadiusVariance + ((generatedIndex % 3) * 12f), HeightVariance = template.AsteroidField.HeightVariance + ((generatedIndex % 4) * 4f), }, 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 { Angle = node.Angle, RadiusOffset = node.RadiusOffset, 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, OreAmount = node.OreAmount, ItemId = node.ItemId, ShardCount = node.ShardCount, })); } nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets)); nodes.AddRange(BuildGasCloudNodes(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 = 9000f; const float radiusStep = 540f; 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, 900f); var angle = (armIndex * armOffset) + (radius / 8200f) + 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 = 42000f; var angle = (generatedIndex % ringCount) * (MathF.PI * 2f / ringCount) + (generatedIndex / ringCount) * 0.22f; var radius = fallbackRadius + (generatedIndex / ringCount) * 1800f; 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 beltRadius = ResolveAsteroidBeltRadius(planets, generatedIndex); var nodeCount = 4 + (generatedIndex % 4); var oreAmount = 2800f + ((generatedIndex % 5) * 320f); 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 = beltRadius + Jitter(generatedIndex, 200 + index, 80f), OreAmount = oreAmount, ItemId = "ore", ShardCount = 6 + (index % 4), }; } } private static IEnumerable BuildGasCloudNodes(int generatedIndex, IReadOnlyList planets) { var gasAnchor = planets .Where((planet) => planet.PlanetType is "gas-giant" or "ice-giant") .OrderByDescending((planet) => planet.OrbitRadius) .FirstOrDefault(); if (gasAnchor is null) { yield break; } var nodeCount = 2 + (generatedIndex % 3); var gasAmount = 2200f + ((generatedIndex % 4) * 260f); for (var index = 0; index < nodeCount; index += 1) { yield return new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = gasAnchor.OrbitPhaseAtEpoch * (MathF.PI / 180f) + (((MathF.PI * 2f) / nodeCount) * index) + Jitter(generatedIndex, 240 + index, 0.18f), RadiusOffset = gasAnchor.OrbitRadius + 90f + Jitter(generatedIndex, 260 + index, 70f), OreAmount = gasAmount, ItemId = "gas", ShardCount = 10 + index, }; } } private static float ResolveAsteroidBeltRadius(IReadOnlyList planets, int generatedIndex) { var gap = planets .Zip(planets.Skip(1), (left, right) => (LeftOrbitRadius: left.OrbitRadius, RightOrbitRadius: right.OrbitRadius, Gap: right.OrbitRadius - left.OrbitRadius)) .OrderByDescending((entry) => entry.Gap) .FirstOrDefault(); if (gap.Gap > 1f) { return gap.LeftOrbitRadius + (gap.Gap * 0.52f); } return 420f + ((generatedIndex % 5) * 60f); } 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 = 140f + (Hash01(generatedIndex, 3) * 35f); 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.22f / MathF.Sqrt(MathF.Max(1f, orbitRadius / 120f)), 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) * 10f), 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 - 8000f) / 28000f)); var band = 220f + (normalized * 760f); 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 + 44f; } private static SolarSystemDefinition CreateSolSystem() { return new SolarSystemDefinition { Id = "sol", Label = "Sol", Position = [18200f, 24f, -11800f], StarKind = "main-sequence", StarCount = 1, StarColor = "#fff1b8", StarGlow = "#ffd35a", StarSize = 58f, GravityWellRadius = 240f, AsteroidField = new AsteroidFieldDefinition { DecorationCount = 240, RadiusOffset = 780f, RadiusVariance = 180f, HeightVariance = 22f, }, ResourceNodes = [ new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 0.2f, RadiusOffset = 720f, OreAmount = 4200f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 760f, OreAmount = 4200f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 810f, OreAmount = 4200f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 780f, OreAmount = 4200f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 0.9f, RadiusOffset = 1650f, OreAmount = 2800f, ItemId = "gas", ShardCount = 12 }, new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 2.7f, RadiusOffset = 1710f, OreAmount = 2800f, ItemId = "gas", ShardCount = 12 }, new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 4.8f, RadiusOffset = 2140f, OreAmount = 2600f, ItemId = "gas", ShardCount = 10 }, ], Planets = [ CreateSolPlanet("Mercury", "barren", "sphere", 0, 180f, 0.19f, 0.2056f, 7.0f, 48f, 29f, 252f, "#b7a08f", 0.03f, false), CreateSolPlanet("Venus", "desert", "sphere", 0, 270f, 0.14f, 0.0067f, 3.4f, 76f, 54f, 181f, "#d9b38c", 2.64f, false), CreateSolPlanet("Earth", "terrestrial", "sphere", 1, 380f, 0.11f, 0.0167f, 0.0f, 0f, 114f, 100f, "#4f84c4", 0.41f, false), CreateSolPlanet("Mars", "desert", "sphere", 2, 500f, 0.09f, 0.0934f, 1.85f, 49f, 286f, 54f, "#c56e52", 0.44f, false), CreateSolPlanet("Jupiter", "gas-giant", "oblate", 95, 980f, 0.05f, 0.0489f, 1.3f, 100f, 275f, 34f, "#d9b06f", 0.05f, true), CreateSolPlanet("Saturn", "gas-giant", "oblate", 146, 1380f, 0.035f, 0.0565f, 2.49f, 113f, 339f, 200f, "#dfc27d", 0.47f, true), CreateSolPlanet("Uranus", "ice-giant", "oblate", 28, 1760f, 0.026f, 0.046f, 0.77f, 74f, 97f, 130f, "#9fd3df", 1.71f, true), CreateSolPlanet("Neptune", "ice-giant", "oblate", 16, 2140f, 0.021f, 0.009f, 1.77f, 132f, 273f, 256f, "#4c79c9", 0.49f, true) ], }; } private static PlanetDefinition CreateSolPlanet( string label, string planetType, string shape, int moonCount, float orbitRadius, float orbitSpeed, 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 = orbitRadius, OrbitSpeed = orbitSpeed, OrbitEccentricity = orbitEccentricity, OrbitInclination = orbitInclination, OrbitLongitudeOfAscendingNode = ascendingNode, OrbitArgumentOfPeriapsis = argumentOfPeriapsis, OrbitPhaseAtEpoch = phaseAtEpoch, Size = planetType switch { "gas-giant" => label == "Saturn" ? 66f : 72f, "ice-giant" => 48f, _ => label == "Earth" ? 28f : label == "Mars" ? 22f : label == "Venus" ? 26f : 20f, }, Color = color, Tilt = tilt, HasRing = hasRing, }; } }