Replace arbitrary game units with real-world measurements throughout the simulation and viewer: planet orbits in AU, sizes in km, galaxy positions in light-years. Add SimulationUnits helpers for conversions, separate WarpSpeed from FtlSpeed for ships, fix FTL transit progress to use galaxy-space distances, overhaul Lagrange point placement with Hill sphere approximation, and update the viewer to scale and format all distances correctly. Ships in FTL transit now render in galaxy view. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
284 lines
10 KiB
C#
284 lines
10 KiB
C#
using SpaceGame.Simulation.Api.Data;
|
|
|
|
namespace SpaceGame.Simulation.Api.Simulation;
|
|
|
|
public sealed partial class ScenarioLoader
|
|
{
|
|
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
|
|
{
|
|
var nodes = new List<NodeRuntime>();
|
|
var bubbles = new List<LocalBubbleRuntime>();
|
|
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, NodeRuntime>>();
|
|
|
|
var starNode = AddSpatialNode(
|
|
nodes,
|
|
bubbles,
|
|
id: $"node-{system.Definition.Id}-star",
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.Star,
|
|
position: Vector3.Zero,
|
|
radius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, 180f));
|
|
|
|
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 planetNode = AddSpatialNode(
|
|
nodes,
|
|
bubbles,
|
|
id: planetNodeId,
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.Planet,
|
|
position: planetPosition,
|
|
radius: MathF.Max(planet.Size + PlanetBubbleRadiusPadding, 120f),
|
|
parentNodeId: starNode.Id);
|
|
|
|
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
|
|
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
|
{
|
|
var lagrangeNode = AddSpatialNode(
|
|
nodes,
|
|
bubbles,
|
|
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.LagrangePoint,
|
|
position: point.Position,
|
|
radius: LagrangeBubbleRadius,
|
|
parentNodeId: planetNode.Id,
|
|
orbitReferenceId: point.Designation);
|
|
lagrangeNodes[point.Designation] = lagrangeNode;
|
|
}
|
|
|
|
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
|
|
|
|
if (planet.MoonCount <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var moonOrbitRadius = MathF.Max(planet.Size + 48f, 42f);
|
|
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
|
|
{
|
|
var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex);
|
|
AddSpatialNode(
|
|
nodes,
|
|
bubbles,
|
|
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
|
|
systemId: system.Definition.Id,
|
|
kind: SpatialNodeKind.Moon,
|
|
position: moonPosition,
|
|
radius: MoonBubbleRadiusPadding + 24f,
|
|
parentNodeId: planetNode.Id);
|
|
moonOrbitRadius += 30f;
|
|
}
|
|
}
|
|
|
|
return new SystemSpatialGraph(system.Definition.Id, nodes, bubbles, lagrangeNodesByPlanetIndex);
|
|
}
|
|
|
|
private static NodeRuntime AddSpatialNode(
|
|
ICollection<NodeRuntime> nodes,
|
|
ICollection<LocalBubbleRuntime> bubbles,
|
|
string id,
|
|
string systemId,
|
|
SpatialNodeKind kind,
|
|
Vector3 position,
|
|
float radius,
|
|
string? parentNodeId = null,
|
|
string? orbitReferenceId = null)
|
|
{
|
|
var bubbleId = $"bubble-{id}";
|
|
var node = new NodeRuntime
|
|
{
|
|
Id = id,
|
|
SystemId = systemId,
|
|
Kind = kind,
|
|
Position = position,
|
|
BubbleId = bubbleId,
|
|
ParentNodeId = parentNodeId,
|
|
OrbitReferenceId = orbitReferenceId,
|
|
};
|
|
|
|
nodes.Add(node);
|
|
bubbles.Add(new LocalBubbleRuntime
|
|
{
|
|
Id = bubbleId,
|
|
NodeId = id,
|
|
SystemId = systemId,
|
|
Radius = radius,
|
|
});
|
|
return node;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// The simulation does not track physical masses yet, so use a size/density proxy.
|
|
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 StationPlacement ResolveStationPlacement(
|
|
InitialStationDefinition plan,
|
|
SystemRuntime system,
|
|
SystemSpatialGraph graph,
|
|
IReadOnlyCollection<NodeRuntime> existingNodes)
|
|
{
|
|
if (plan.PlanetIndex is int planetIndex &&
|
|
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
|
|
{
|
|
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
|
|
if (lagrangeNodes.TryGetValue(designation, out var lagrangeNode))
|
|
{
|
|
return new StationPlacement(lagrangeNode, lagrangeNode.Position);
|
|
}
|
|
}
|
|
|
|
if (plan.Position is { Length: 3 })
|
|
{
|
|
var targetPosition = NormalizeScenarioPoint(system, plan.Position);
|
|
var preferredNode = existingNodes
|
|
.Where((node) => node.SystemId == system.Definition.Id && node.Kind == SpatialNodeKind.LagrangePoint)
|
|
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
|
|
.FirstOrDefault()
|
|
?? existingNodes
|
|
.Where((node) => node.SystemId == system.Definition.Id)
|
|
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
|
|
.First();
|
|
return new StationPlacement(preferredNode, preferredNode.Position);
|
|
}
|
|
|
|
var fallbackNode = graph.Nodes
|
|
.FirstOrDefault((node) => node.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(node.OccupyingStructureId))
|
|
?? graph.Nodes.First((node) => node.Kind == SpatialNodeKind.Planet);
|
|
return new StationPlacement(fallbackNode, fallbackNode.Position);
|
|
}
|
|
|
|
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
|
|
{
|
|
< 0 => "L4",
|
|
> 0 => "L5",
|
|
_ => "L1",
|
|
};
|
|
|
|
private static NodeRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
|
|
{
|
|
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.Nodes.FirstOrDefault((node) => node.Id == moonNodeId);
|
|
}
|
|
|
|
var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
|
|
return graph.Nodes.FirstOrDefault((node) => node.Id == planetNodeId);
|
|
}
|
|
|
|
private static Vector3 ComputeResourceNodePosition(NodeRuntime? anchorNode, 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 (anchorNode is null)
|
|
{
|
|
return new Vector3(offset.X, yPlane + offset.Y, offset.Z);
|
|
}
|
|
|
|
return Add(anchorNode.Position, offset);
|
|
}
|
|
|
|
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
|
|
{
|
|
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
|
|
var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
|
|
var x = MathF.Cos(angle) * orbitRadiusKm;
|
|
var z = MathF.Sin(angle) * orbitRadiusKm;
|
|
return new Vector3(x, 0f, z);
|
|
}
|
|
|
|
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, float orbitRadius, int moonIndex, int planetIndex)
|
|
{
|
|
var angle = ((MathF.PI * 2f) / MathF.Max(1, moonIndex + 3)) * (moonIndex + 1) + (planetIndex * 0.37f);
|
|
return Add(planetPosition, new Vector3(MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius));
|
|
}
|
|
|
|
private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<NodeRuntime> nodes)
|
|
{
|
|
var nearestNode = nodes
|
|
.Where((node) => node.SystemId == systemId)
|
|
.OrderBy((node) => node.Position.DistanceTo(position))
|
|
.FirstOrDefault();
|
|
|
|
return new ShipSpatialStateRuntime
|
|
{
|
|
CurrentSystemId = systemId,
|
|
SpaceLayer = SpaceLayerKinds.LocalSpace,
|
|
CurrentNodeId = nearestNode?.Id,
|
|
CurrentBubbleId = nearestNode?.BubbleId,
|
|
LocalPosition = position,
|
|
SystemPosition = position,
|
|
MovementRegime = MovementRegimeKinds.LocalFlight,
|
|
};
|
|
}
|
|
|
|
private sealed record SystemSpatialGraph(
|
|
string SystemId,
|
|
List<NodeRuntime> Nodes,
|
|
List<LocalBubbleRuntime> Bubbles,
|
|
Dictionary<int, Dictionary<string, NodeRuntime>> LagrangeNodesByPlanetIndex);
|
|
|
|
private sealed record LagrangePointPlacement(string Designation, Vector3 Position);
|
|
|
|
private sealed record StationPlacement(NodeRuntime AnchorNode, Vector3 Position);
|
|
}
|