using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class ScenarioLoader { private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system) { var celestials = new List(); var lagrangeNodesByPlanetIndex = new Dictionary>(); for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1) { AddCelestial( celestials, id: $"node-{system.Definition.Id}-star-{starIndex + 1}", systemId: system.Definition.Id, kind: SpatialNodeKind.Star, position: Vector3.Zero, localSpaceRadius: LocalSpaceRadius); } var primaryStarNodeId = $"node-{system.Definition.Id}-star-1"; for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1) { var planet = system.Definition.Planets[planetIndex]; var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}"; var planetPosition = ComputePlanetPosition(planet); var planetCelestial = AddCelestial( celestials, id: planetNodeId, systemId: system.Definition.Id, kind: SpatialNodeKind.Planet, position: planetPosition, localSpaceRadius: LocalSpaceRadius, parentNodeId: primaryStarNodeId); var lagrangeNodes = new Dictionary(StringComparer.Ordinal); foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet)) { var lagrangeCelestial = AddCelestial( celestials, id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}", systemId: system.Definition.Id, kind: SpatialNodeKind.LagrangePoint, position: point.Position, localSpaceRadius: LocalSpaceRadius, parentNodeId: planetCelestial.Id, orbitReferenceId: point.Designation); lagrangeNodes[point.Designation] = lagrangeCelestial; } lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes; for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1) { var moon = planet.Moons[moonIndex]; var moonPosition = ComputeMoonPosition(planetPosition, moon); AddCelestial( celestials, id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}", systemId: system.Definition.Id, kind: SpatialNodeKind.Moon, position: moonPosition, localSpaceRadius: LocalSpaceRadius, parentNodeId: planetCelestial.Id); } } return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex); } private static CelestialRuntime AddCelestial( ICollection celestials, string id, string systemId, SpatialNodeKind kind, Vector3 position, float localSpaceRadius, string? parentNodeId = null, string? orbitReferenceId = null) { var celestial = new CelestialRuntime { Id = id, SystemId = systemId, Kind = kind, Position = position, LocalSpaceRadius = localSpaceRadius, ParentNodeId = parentNodeId, OrbitReferenceId = orbitReferenceId, }; celestials.Add(celestial); return celestial; } private static IEnumerable EnumeratePlanetLagrangePoints( Vector3 planetPosition, PlanetDefinition planet) { var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f)); var tangential = new Vector3(-radial.Z, 0f, radial.X); var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z); var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet); var triangularAngle = MathF.PI / 3f; yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset))); yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset))); yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm)); yield return new LagrangePointPlacement( "L4", Add( Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)), Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle)))); yield return new LagrangePointPlacement( "L5", Add( Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)), Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle)))); } private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet) { var planetMassProxy = EstimatePlanetMassRatio(planet); var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f)); var minimumOffset = MathF.Max(planet.Size * 4f, 25000f); return MathF.Max(minimumOffset, hillLikeOffset); } // The simulation does not track physical masses yet, so use a size/density proxy. private static float EstimatePlanetMassRatio(PlanetDefinition planet) { var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f); var densityFactor = planet.PlanetType switch { "gas-giant" => 0.24f, "ice-giant" => 0.18f, "oceanic" => 0.95f, "ice" => 0.7f, _ => 1f, }; var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor; return earthMasses / 332_946f; } private static StationPlacement ResolveStationPlacement( InitialStationDefinition plan, SystemRuntime system, SystemSpatialGraph graph, IReadOnlyCollection existingCelestials) { if (plan.PlanetIndex is int planetIndex && graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes)) { var designation = ResolveLagrangeDesignation(plan.LagrangeSide); if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial)) { return new StationPlacement(lagrangeCelestial, lagrangeCelestial.Position); } } if (plan.Position is { Length: 3 }) { var targetPosition = NormalizeScenarioPoint(system, plan.Position); var preferredCelestial = existingCelestials .Where((c) => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint) .OrderBy((c) => c.Position.DistanceTo(targetPosition)) .FirstOrDefault() ?? existingCelestials .Where((c) => c.SystemId == system.Definition.Id) .OrderBy((c) => c.Position.DistanceTo(targetPosition)) .First(); return new StationPlacement(preferredCelestial, preferredCelestial.Position); } var fallbackCelestial = graph.Celestials .FirstOrDefault((c) => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId)) ?? graph.Celestials.First((c) => c.Kind == SpatialNodeKind.Planet); return new StationPlacement(fallbackCelestial, fallbackCelestial.Position); } private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch { < 0 => "L4", > 0 => "L5", _ => "L1", }; private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition) { if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0) { return null; } if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0) { var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}"; return graph.Celestials.FirstOrDefault((c) => c.Id == moonNodeId); } var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}"; return graph.Celestials.FirstOrDefault((c) => c.Id == planetNodeId); } private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane) { var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f); var offset = new Vector3( MathF.Cos(definition.Angle) * definition.RadiusOffset, verticalOffset, MathF.Sin(definition.Angle) * definition.RadiusOffset); if (anchorCelestial is null) { return new Vector3(offset.X, yPlane + offset.Y, offset.Z); } return Add(anchorCelestial.Position, offset); } private static Vector3 ComputePlanetPosition(PlanetDefinition planet) { var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch); var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius); var x = MathF.Cos(angle) * orbitRadiusKm; var z = MathF.Sin(angle) * orbitRadiusKm; return new Vector3(x, 0f, z); } private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon) { var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch); var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius); return Add(planetPosition, local); } private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection celestials) { var nearestCelestial = celestials .Where((c) => c.SystemId == systemId) .OrderBy((c) => c.Position.DistanceTo(position)) .FirstOrDefault(); return new ShipSpatialStateRuntime { CurrentSystemId = systemId, SpaceLayer = SpaceLayerKinds.LocalSpace, CurrentCelestialId = nearestCelestial?.Id, LocalPosition = position, SystemPosition = position, MovementRegime = MovementRegimeKinds.LocalFlight, }; } private sealed record SystemSpatialGraph( string SystemId, List Celestials, Dictionary> LagrangeNodesByPlanetIndex); private sealed record LagrangePointPlacement(string Designation, Vector3 Position); private sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position); }