using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; internal sealed class SpatialBuilder { internal ScenarioSpatialLayout BuildLayout(IReadOnlyList systems, BalanceDefinition balance) { var systemGraphs = systems.ToDictionary( system => system.Definition.Id, BuildSystemSpatialGraph, StringComparer.Ordinal); var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList(); var nodes = new List(); var nodeIdCounter = 0; foreach (var system in systems) { var systemGraph = systemGraphs[system.Definition.Id]; foreach (var node in system.Definition.ResourceNodes) { var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node); nodes.Add(new ResourceNodeRuntime { Id = $"node-{++nodeIdCounter}", SystemId = system.Definition.Id, Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane), SourceKind = node.SourceKind, ItemId = node.ItemId, CelestialId = anchorCelestial?.Id, OrbitRadius = node.RadiusOffset, OrbitPhase = node.Angle, OrbitInclination = DegreesToRadians(node.InclinationDegrees), OreRemaining = node.OreAmount, MaxOre = node.OreAmount, }); } } return new ScenarioSpatialLayout(systemGraphs, celestials, nodes); } 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); } 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; } internal 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 (!string.IsNullOrWhiteSpace(definition.AnchorReference)) { var anchorId = definition.AnchorReference.ToLowerInvariant() switch { var reference when reference.StartsWith("star-", StringComparison.Ordinal) => $"node-{graph.SystemId}-{reference}", var reference when reference.StartsWith("planet-", StringComparison.Ordinal) => $"node-{graph.SystemId}-{reference}", _ => null, }; if (anchorId is not null) { return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal)); } } 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); return new Vector3(MathF.Cos(angle) * orbitRadiusKm, 0f, MathF.Sin(angle) * orbitRadiusKm); } 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); } internal 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 = SpaceLayerKind.LocalSpace, CurrentCelestialId = nearestCelestial?.Id, LocalPosition = position, SystemPosition = position, MovementRegime = MovementRegimeKinds.LocalFlight, }; } } internal sealed record ScenarioSpatialLayout( IReadOnlyDictionary SystemGraphs, List Celestials, List Nodes); internal sealed record SystemSpatialGraph( string SystemId, List Celestials, Dictionary> LagrangeNodesByPlanetIndex); internal sealed record LagrangePointPlacement(string Designation, Vector3 Position); internal sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position);