Refactor backend into domain-first slices
This commit is contained in:
293
apps/backend/Universe/Simulation/OrbitalStateUpdater.cs
Normal file
293
apps/backend/Universe/Simulation/OrbitalStateUpdater.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user