using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { private static Vector3 ComputePlanetPosition(PlanetDefinition planet, float timeSeconds) { var eccentricity = Math.Clamp(planet.OrbitEccentricity, 0f, 0.85f); var meanAnomaly = DegreesToRadians(planet.OrbitPhaseAtEpoch) + (timeSeconds * planet.OrbitSpeed); var eccentricAnomaly = meanAnomaly + (eccentricity * MathF.Sin(meanAnomaly)) + (0.5f * eccentricity * eccentricity * MathF.Sin(2f * meanAnomaly)); var semiMajorAxis = SimulationUnits.AuToKilometers(planet.OrbitRadius); var semiMinorAxis = semiMajorAxis * MathF.Sqrt(MathF.Max(1f - (eccentricity * eccentricity), 0.05f)); var local = new Vector3( semiMajorAxis * (MathF.Cos(eccentricAnomaly) - eccentricity), 0f, semiMinorAxis * MathF.Sin(eccentricAnomaly)); local = RotateAroundY(local, DegreesToRadians(planet.OrbitArgumentOfPeriapsis)); local = RotateAroundX(local, DegreesToRadians(planet.OrbitInclination)); local = RotateAroundY(local, DegreesToRadians(planet.OrbitLongitudeOfAscendingNode)); return local; } private static Vector3 ComputeMoonOffset(PlanetDefinition planet, int moonIndex, float timeSeconds) { var orbitRadius = ComputeMoonOrbitRadius(planet, moonIndex); var speed = ComputeMoonOrbitSpeed(planet, moonIndex); var phase = HashUnit($"{planet.Label}:{moonIndex}:phase") * MathF.PI * 2f; var inclination = DegreesToRadians((HashUnit($"{planet.Label}:{moonIndex}:inclination") - 0.5f) * 28f); var ascendingNode = DegreesToRadians(HashUnit($"{planet.Label}:{moonIndex}:node") * 360f); var angle = phase + (timeSeconds * speed); var local = new Vector3( MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius); local = RotateAroundX(local, inclination); local = RotateAroundY(local, ascendingNode); return local; } private static float ComputeMoonOrbitRadius(PlanetDefinition planet, int moonIndex) { var spacing = planet.Size * 1.4f; var variance = HashUnit($"{planet.Label}:{moonIndex}:radius") * planet.Size * 0.9f; return (planet.Size * 1.8f) + (moonIndex * spacing) + variance; } private static float ComputeMoonOrbitSpeed(PlanetDefinition planet, int moonIndex) { var radius = ComputeMoonOrbitRadius(planet, moonIndex); return 0.9f / MathF.Sqrt(MathF.Max(radius, 1f)) + (moonIndex * 0.003f); } private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node) { var baseSpeed = 0.24f; return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f)); } private static Vector3 ComputeResourceNodeOffset(ResourceNodeRuntime node, float timeSeconds) { var angle = node.OrbitPhase + (timeSeconds * ComputeResourceNodeOrbitSpeed(node)); var orbit = new Vector3( MathF.Cos(angle) * node.OrbitRadius, 0f, MathF.Sin(angle) * node.OrbitRadius); return RotateAroundX(orbit, node.OrbitInclination); } 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; } private static Vector3 NormalizeOrFallback(Vector3 value, Vector3 fallback) { var length = MathF.Sqrt(value.LengthSquared()); if (length <= 0.0001f) { return fallback; } return value.Divide(length); } private static Vector3 RotateAroundX(Vector3 value, float angle) { var cos = MathF.Cos(angle); var sin = MathF.Sin(angle); return new Vector3( value.X, (value.Y * cos) - (value.Z * sin), (value.Y * sin) + (value.Z * cos)); } private static Vector3 RotateAroundY(Vector3 value, float angle) { var cos = MathF.Cos(angle); var sin = MathF.Sin(angle); return new Vector3( (value.X * cos) + (value.Z * sin), value.Y, (-value.X * sin) + (value.Z * cos)); } private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z); private static Vector3 Scale(Vector3 value, float scalar) => new(value.X * scalar, value.Y * scalar, value.Z * scalar); private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f); private static float HashUnit(string input) { unchecked { var hash = 2166136261u; foreach (var character in input) { hash ^= character; hash *= 16777619u; } return (hash & 0x00FFFFFF) / (float)0x01000000; } } private void UpdateOrbitalState(SimulationWorld world) { var worldTimeSeconds = (float)world.OrbitalTimeSeconds; var spatialNodesById = world.SpatialNodes.ToDictionary(node => node.Id, StringComparer.Ordinal); foreach (var system in world.Systems) { var starNodeId = $"node-{system.Definition.Id}-star"; if (spatialNodesById.TryGetValue(starNodeId, out var starNode)) { starNode.Position = Vector3.Zero; } 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}"; if (!spatialNodesById.TryGetValue(planetNodeId, out var planetNode)) { continue; } var planetPosition = ComputePlanetPosition(planet, worldTimeSeconds); planetNode.Position = planetPosition; foreach (var lagrange in EnumeratePlanetLagrangePoints(planetPosition, planet)) { var lagrangeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{lagrange.Designation.ToLowerInvariant()}"; if (spatialNodesById.TryGetValue(lagrangeId, out var lagrangeNode)) { lagrangeNode.Position = lagrange.Position; } } for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1) { var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}"; if (!spatialNodesById.TryGetValue(moonId, out var moonNode)) { continue; } moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet, moonIndex, worldTimeSeconds)); } } } foreach (var station in world.Stations) { if (station.AnchorNodeId is null || !spatialNodesById.TryGetValue(station.AnchorNodeId, out var anchorNode)) { continue; } station.Position = anchorNode.Position; if (station.NodeId is not null && spatialNodesById.TryGetValue(station.NodeId, out var stationNode)) { stationNode.Position = station.Position; } } foreach (var node in world.Nodes) { if (node.AnchorNodeId is null || !spatialNodesById.TryGetValue(node.AnchorNodeId, out var anchorNode)) { continue; } node.Position = Add(anchorNode.Position, ComputeResourceNodeOffset(node, worldTimeSeconds)); if (spatialNodesById.TryGetValue(node.Id, out var resourceNode)) { resourceNode.Position = node.Position; } } foreach (var ship in world.Ships.Where(ship => ship.DockedStationId is not null)) { var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station is null) { continue; } var dockedPosition = GetShipDockedPosition(ship, station); ship.Position = dockedPosition; ship.TargetPosition = dockedPosition; } } private static void SyncSpatialState(SimulationWorld world) { foreach (var bubble in world.LocalBubbles) { bubble.OccupantShipIds.Clear(); } foreach (var ship in world.Ships) { ship.SpatialState.CurrentSystemId = ship.SystemId; ship.SpatialState.LocalPosition = ship.Position; ship.SpatialState.SystemPosition = ship.Position; if (ship.SpatialState.Transit is not null) { ship.SpatialState.CurrentNodeId = null; ship.SpatialState.CurrentBubbleId = null; continue; } ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; var nearestNode = world.SpatialNodes .Where(candidate => candidate.SystemId == ship.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) .FirstOrDefault(); ship.SpatialState.CurrentNodeId = nearestNode?.Id; ship.SpatialState.CurrentBubbleId = nearestNode?.BubbleId; if (nearestNode is not null) { var nearestBubble = world.LocalBubbles.FirstOrDefault(candidate => candidate.Id == nearestNode.BubbleId); nearestBubble?.OccupantShipIds.Add(ship.Id); } if (ship.DockedStationId is null) { continue; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station?.BubbleId is null) { continue; } ship.SpatialState.CurrentNodeId = station.NodeId; ship.SpatialState.CurrentBubbleId = station.BubbleId; var bubble = world.LocalBubbles.FirstOrDefault(candidate => candidate.Id == station.BubbleId); bubble?.OccupantShipIds.Add(ship.Id); } } private readonly record struct LagrangePointPlacement(string Designation, Vector3 Position); }