using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; public sealed class SystemGenerationService { internal List PrepareAuthoredSystems(IReadOnlyList authoredSystems) => authoredSystems .Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index)) .ToList(); internal 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); } } foreach (var preferredSystemId in SystemSelectionPolicy.PreferredSystemIds) { AddById(preferredSystemId); } 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, AnchorReference = node.AnchorReference, 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 EnsureStrategicResourceCoverage(new SolarSystemDefinition { Id = id, Label = label, Position = [position.X, position.Y, position.Z], Stars = [ new StarDefinition { Kind = starProfile.Kind, Color = starProfile.StarColor, Glow = starProfile.StarGlow, Size = starProfile.BaseSize + ((generatedIndex % 4) * 2f), }, ], 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, }, generatedIndex + 1024); } private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition) { return new SolarSystemDefinition { Id = definition.Id, Label = definition.Label, Position = definition.Position.ToArray(), Stars = definition.Stars.Select(s => new StarDefinition { Kind = s.Kind, Color = s.Color, Glow = s.Glow, Size = s.Size, OrbitRadius = s.OrbitRadius, OrbitSpeed = s.OrbitSpeed, OrbitPhaseAtEpoch = s.OrbitPhaseAtEpoch }).ToList(), 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, AnchorReference = node.AnchorReference, 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, Moons = planet.Moons.Select(moon => new MoonDefinition { Label = moon.Label, Size = moon.Size, Color = moon.Color, OrbitRadius = moon.OrbitRadius, OrbitSpeed = moon.OrbitSpeed, OrbitPhaseAtEpoch = moon.OrbitPhaseAtEpoch, OrbitInclination = moon.OrbitInclination, OrbitLongitudeOfAscendingNode = moon.OrbitLongitudeOfAscendingNode }).ToList(), 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, AnchorReference = node.AnchorReference, Angle = node.Angle, RadiusOffset = node.RadiusOffset, InclinationDegrees = node.InclinationDegrees, AnchorPlanetIndex = node.AnchorPlanetIndex, AnchorMoonIndex = node.AnchorMoonIndex, OreAmount = node.OreAmount, ItemId = node.ItemId, ShardCount = node.ShardCount, })); } return nodes; } private static SolarSystemDefinition EnsureStrategicResourceCoverage(SolarSystemDefinition system, int seed) { for (var index = 0; index < system.ResourceNodes.Count; index += 1) { system.ResourceNodes[index] = SanitizeResourceNode(system.ResourceNodes[index], system.Planets, seed, index); } var requiredItems = new[] { "ore", "silicon", "ice", "hydrogen", "helium", "methane" }; foreach (var itemId in requiredItems) { if (system.ResourceNodes.Any(node => string.Equals(node.ItemId, itemId, StringComparison.Ordinal))) { continue; } system.ResourceNodes.Add(BuildStrategicResourceNode(itemId, system.Planets, seed, system.ResourceNodes.Count)); } return system; } 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 ResourceNodeDefinition BuildStrategicResourceNode( string itemId, IReadOnlyList planets, int seed, int ordinal) { var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets); return new ResourceNodeDefinition { SourceKind = "local-space", AnchorReference = ResolveStrategicAnchorReference(itemId, planets, ordinal), Angle = (MathF.PI * 2f * ((ordinal % 7) / 7f)) + Jitter(seed, 400 + ordinal, 0.35f), RadiusOffset = 150000f + Jitter(seed, 460 + ordinal, 42000f), InclinationDegrees = Jitter(seed, 520 + ordinal, 10f), AnchorPlanetIndex = anchorPlanetIndex, OreAmount = itemId switch { "ore" => 12000f, "silicon" => 10000f, "ice" => 9000f, _ => 8000f, }, ItemId = itemId, ShardCount = itemId switch { "ore" or "silicon" or "ice" => 8, _ => 6, }, }; } private static ResourceNodeDefinition SanitizeResourceNode( ResourceNodeDefinition node, IReadOnlyList planets, int seed, int ordinal) { node.SourceKind = "local-space"; node.AnchorReference ??= ResolveLegacyAnchorReference(node, planets, seed, ordinal); return node; } private static string ResolveLegacyAnchorReference( ResourceNodeDefinition node, IReadOnlyList planets, int seed, int ordinal) { if (node.AnchorMoonIndex is int moonIndex && node.AnchorPlanetIndex is int planetIndex && planetIndex >= 0) { return $"planet-{planetIndex + 1}-moon-{moonIndex + 1}"; } if (node.AnchorPlanetIndex is int anchoredPlanetIndex && anchoredPlanetIndex >= 0) { return $"planet-{anchoredPlanetIndex + 1}"; } return ResolveStrategicAnchorReference(node.ItemId, planets, ordinal + seed); } private static string ResolveStrategicAnchorReference(string itemId, IReadOnlyList planets, int ordinal) { if (itemId is "hydrogen" or "helium" or "methane") { var gasGiantIndex = planets .Select((planet, index) => (planet, index)) .FirstOrDefault(entry => entry.planet.PlanetType is "gas-giant" or "ice-giant") .index; return gasGiantIndex > 0 || (planets.Count > 0 && planets[0].PlanetType is "gas-giant" or "ice-giant") ? $"planet-{gasGiantIndex + 1}" : "star-1"; } if (itemId == "ice") { var moonAnchor = planets .Select((planet, index) => (planet, index)) .FirstOrDefault(entry => entry.planet.Moons.Count > 0 && entry.planet.PlanetType is "ice" or "ice-giant" or "oceanic"); if (moonAnchor.planet is not null && moonAnchor.planet.Moons.Count > 0) { return $"planet-{moonAnchor.index + 1}-moon-1"; } } var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets); var lagrange = (ordinal % 3) switch { 0 => "l1", 1 => "l4", _ => "l5", }; return $"planet-{anchorPlanetIndex + 1}-{lagrange}"; } private static int ResolveStrategicResourceAnchorPlanetIndex(string itemId, IReadOnlyList planets) { if (planets.Count == 0) { return 0; } bool MatchesPlanetType(PlanetDefinition planet) => itemId switch { "hydrogen" or "helium" or "methane" => planet.PlanetType is "gas-giant" or "ice-giant", "ice" => planet.PlanetType is "ice" or "ice-giant" or "oceanic", _ => planet.PlanetType is not "gas-giant" and not "ice-giant", }; for (var index = 0; index < planets.Count; index += 1) { if (MatchesPlanetType(planets[index])) { return index; } } return ResolveAsteroidAnchorPlanetIndex(planets); } 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 moonCount = profile.BaseMoonCount + (int)MathF.Floor(Hash01(generatedIndex, 40 + index) * 3f); var planetLabel = $"{BuildPlanetBaseName(generatedIndex, index)}-{index + 1}"; planets.Add(new PlanetDefinition { Label = planetLabel, PlanetType = profile.Type, Shape = profile.Shape, Moons = GenerateMoons(planetLabel, profile.BaseSize, moonCount), 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 static List GenerateMoons(string planetLabel, float planetSize, int moonCount) { var seed = planetLabel.Aggregate(0, (acc, c) => acc * 31 + c); var moons = new List(moonCount); for (var moonIndex = 0; moonIndex < moonCount; moonIndex += 1) { var spacing = planetSize * 1.4f; var radiusVariance = Hash01(seed, 10 + moonIndex) * planetSize * 0.9f; var orbitRadius = (planetSize * 1.8f) + (moonIndex * spacing) + radiusVariance; var orbitSpeed = 0.9f / MathF.Sqrt(MathF.Max(orbitRadius, 1f)) + (moonIndex * 0.003f); var phase = Hash01(seed, 20 + moonIndex) * 360f; var inclination = (Hash01(seed, 30 + moonIndex) - 0.5f) * 28f; var ascendingNode = Hash01(seed, 40 + moonIndex) * 360f; var sizeBase = MathF.Max(2.2f, planetSize * 0.11f); var sizeVariance = Hash01(seed, 50 + moonIndex) * MathF.Max(planetSize * 0.16f, 2.5f); var size = MathF.Min(sizeBase + sizeVariance, planetSize * 0.42f); moons.Add(new MoonDefinition { Label = $"{planetLabel}-m{moonIndex + 1}", Size = size, Color = "#c8c4bc", OrbitRadius = orbitRadius, OrbitSpeed = orbitSpeed, OrbitPhaseAtEpoch = phase, OrbitInclination = inclination, OrbitLongitudeOfAscendingNode = ascendingNode, }); } return moons; } }