using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class ScenarioLoader { private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system) { var nodes = new List(); var bubbles = new List(); var lagrangeNodesByPlanetIndex = new Dictionary>(); var starNode = AddSpatialNode( nodes, bubbles, id: $"node-{system.Definition.Id}-star", systemId: system.Definition.Id, kind: SpatialNodeKind.Star, position: Vector3.Zero, radius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, 180f)); 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 planetNode = AddSpatialNode( nodes, bubbles, id: planetNodeId, systemId: system.Definition.Id, kind: SpatialNodeKind.Planet, position: planetPosition, radius: MathF.Max(planet.Size + PlanetBubbleRadiusPadding, 120f), parentNodeId: starNode.Id); var lagrangeNodes = new Dictionary(StringComparer.Ordinal); foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet)) { var lagrangeNode = AddSpatialNode( nodes, bubbles, id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}", systemId: system.Definition.Id, kind: SpatialNodeKind.LagrangePoint, position: point.Position, radius: LagrangeBubbleRadius, parentNodeId: planetNode.Id, orbitReferenceId: point.Designation); lagrangeNodes[point.Designation] = lagrangeNode; } lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes; if (planet.MoonCount <= 0) { continue; } var moonOrbitRadius = MathF.Max(planet.Size + 48f, 42f); for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1) { var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex); AddSpatialNode( nodes, bubbles, id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}", systemId: system.Definition.Id, kind: SpatialNodeKind.Moon, position: moonPosition, radius: MoonBubbleRadiusPadding + 24f, parentNodeId: planetNode.Id); moonOrbitRadius += 30f; } } return new SystemSpatialGraph(system.Definition.Id, nodes, bubbles, lagrangeNodesByPlanetIndex); } private static NodeRuntime AddSpatialNode( ICollection nodes, ICollection bubbles, string id, string systemId, SpatialNodeKind kind, Vector3 position, float radius, string? parentNodeId = null, string? orbitReferenceId = null) { var bubbleId = $"bubble-{id}"; var node = new NodeRuntime { Id = id, SystemId = systemId, Kind = kind, Position = position, BubbleId = bubbleId, ParentNodeId = parentNodeId, OrbitReferenceId = orbitReferenceId, }; nodes.Add(node); bubbles.Add(new LocalBubbleRuntime { Id = bubbleId, NodeId = id, SystemId = systemId, Radius = radius, }); return node; } 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 existingNodes) { if (plan.PlanetIndex is int planetIndex && graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes)) { var designation = ResolveLagrangeDesignation(plan.LagrangeSide); if (lagrangeNodes.TryGetValue(designation, out var lagrangeNode)) { return new StationPlacement(lagrangeNode, lagrangeNode.Position); } } if (plan.Position is { Length: 3 }) { var targetPosition = NormalizeScenarioPoint(system, plan.Position); var preferredNode = existingNodes .Where((node) => node.SystemId == system.Definition.Id && node.Kind == SpatialNodeKind.LagrangePoint) .OrderBy((node) => node.Position.DistanceTo(targetPosition)) .FirstOrDefault() ?? existingNodes .Where((node) => node.SystemId == system.Definition.Id) .OrderBy((node) => node.Position.DistanceTo(targetPosition)) .First(); return new StationPlacement(preferredNode, preferredNode.Position); } var fallbackNode = graph.Nodes .FirstOrDefault((node) => node.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(node.OccupyingStructureId)) ?? graph.Nodes.First((node) => node.Kind == SpatialNodeKind.Planet); return new StationPlacement(fallbackNode, fallbackNode.Position); } private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch { < 0 => "L4", > 0 => "L5", _ => "L1", }; private static NodeRuntime? 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.Nodes.FirstOrDefault((node) => node.Id == moonNodeId); } var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}"; return graph.Nodes.FirstOrDefault((node) => node.Id == planetNodeId); } private static Vector3 ComputeResourceNodePosition(NodeRuntime? anchorNode, 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 (anchorNode is null) { return new Vector3(offset.X, yPlane + offset.Y, offset.Z); } return Add(anchorNode.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, float orbitRadius, int moonIndex, int planetIndex) { var angle = ((MathF.PI * 2f) / MathF.Max(1, moonIndex + 3)) * (moonIndex + 1) + (planetIndex * 0.37f); return Add(planetPosition, new Vector3(MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius)); } private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection nodes) { var nearestNode = nodes .Where((node) => node.SystemId == systemId) .OrderBy((node) => node.Position.DistanceTo(position)) .FirstOrDefault(); return new ShipSpatialStateRuntime { CurrentSystemId = systemId, SpaceLayer = SpaceLayerKinds.LocalSpace, CurrentNodeId = nearestNode?.Id, CurrentBubbleId = nearestNode?.BubbleId, LocalPosition = position, SystemPosition = position, MovementRegime = MovementRegimeKinds.LocalFlight, }; } private sealed record SystemSpatialGraph( string SystemId, List Nodes, List Bubbles, Dictionary> LagrangeNodesByPlanetIndex); private sealed record LagrangePointPlacement(string Designation, Vector3 Position); private sealed record StationPlacement(NodeRuntime AnchorNode, Vector3 Position); }