Files
space-game/apps/backend/Universe/Scenario/SpatialBuilder.cs

321 lines
13 KiB
C#

using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario;
public sealed class SpatialBuilder(IBalanceService balance)
{
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems)
{
var systemGraphs = systems.ToDictionary(
system => system.Definition.Id,
BuildSystemSpatialGraph,
StringComparer.Ordinal);
var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList();
var nodes = new List<ResourceNodeRuntime>();
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<CelestialRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
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<string, CelestialRuntime>(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<CelestialRuntime> 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<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;
}
internal static StationPlacement ResolveStationPlacement(
InitialStationDefinition plan,
SystemRuntime system,
SystemSpatialGraph graph,
IReadOnlyCollection<CelestialRuntime> 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<CelestialRuntime> 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 = MovementRegimeKind.LocalFlight,
};
}
}
public sealed record ScenarioSpatialLayout(
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
List<CelestialRuntime> Celestials,
List<ResourceNodeRuntime> Nodes);
public sealed record SystemSpatialGraph(
string SystemId,
List<CelestialRuntime> Celestials,
Dictionary<int, Dictionary<string, CelestialRuntime>> LagrangeNodesByPlanetIndex);
internal sealed record LagrangePointPlacement(string Designation, Vector3 Position);
internal sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position);