Files
space-game/apps/backend/Simulation/Systems/OrbitalStateUpdater.cs

296 lines
11 KiB
C#

using SpaceGame.Api.Data;
using SpaceGame.Api.Simulation.Model;
using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService;
namespace SpaceGame.Api.Simulation.Systems;
internal sealed class OrbitalStateUpdater
{
private readonly OrbitalSimulationOptions _orbitalSimulation;
internal OrbitalStateUpdater(OrbitalSimulationOptions orbitalSimulation)
{
_orbitalSimulation = orbitalSimulation;
}
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<LagrangePointPlacement> 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;
}
}
internal void Update(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;
}
}
internal 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);
}