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(MoonDefinition moon, float timeSeconds) { var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch) + (timeSeconds * moon.OrbitSpeed); var local = new Vector3( MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius); local = RotateAroundX(local, DegreesToRadians(moon.OrbitInclination)); local = RotateAroundY(local, DegreesToRadians(moon.OrbitLongitudeOfAscendingNode)); return local; } 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 celestialsById = world.Celestials.ToDictionary(c => c.Id, StringComparer.Ordinal); foreach (var system in world.Systems) { for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1) { var star = system.Definition.Stars[starIndex]; var starNodeId = $"node-{system.Definition.Id}-star-{starIndex + 1}"; if (!celestialsById.TryGetValue(starNodeId, out var starNode)) { continue; } if (star.OrbitRadius <= 0f) { starNode.Position = Vector3.Zero; } else { var angle = DegreesToRadians(star.OrbitPhaseAtEpoch) + (worldTimeSeconds * star.OrbitSpeed); starNode.Position = new Vector3(MathF.Cos(angle) * star.OrbitRadius, 0f, MathF.Sin(angle) * star.OrbitRadius); } } 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 (!celestialsById.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 (celestialsById.TryGetValue(lagrangeId, out var lagrangeNode)) { lagrangeNode.Position = lagrange.Position; } } for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1) { var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}"; if (!celestialsById.TryGetValue(moonId, out var moonNode)) { continue; } moonNode.Position = Add(planetPosition, ComputeMoonOffset(planet.Moons[moonIndex], worldTimeSeconds)); } } } foreach (var station in world.Stations) { if (station.CelestialId is null || !celestialsById.TryGetValue(station.CelestialId, out var anchorCelestial)) { continue; } station.Position = anchorCelestial.Position; } foreach (var node in world.Nodes) { if (node.CelestialId is null || !celestialsById.TryGetValue(node.CelestialId, out var anchorCelestial)) { continue; } node.Position = Add(anchorCelestial.Position, ComputeResourceNodeOffset(node, worldTimeSeconds)); } 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 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.CurrentCelestialId = null; continue; } ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; var nearestCelestial = world.Celestials .Where(candidate => candidate.SystemId == ship.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) .FirstOrDefault(); ship.SpatialState.CurrentCelestialId = nearestCelestial?.Id; if (ship.DockedStationId is null) { continue; } var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (station?.CelestialId is not null) { ship.SpatialState.CurrentCelestialId = station.CelestialId; } } } private readonly record struct LagrangePointPlacement(string Designation, Vector3 Position); }