feat: migrate simulation to physically-based unit system

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>
This commit is contained in:
2026-03-16 14:21:20 -04:00
parent 5ba1287f85
commit 5df5111463
34 changed files with 1558 additions and 456 deletions

View File

@@ -35,7 +35,7 @@ public sealed partial class ScenarioLoader
parentNodeId: starNode.Id);
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet.OrbitRadius, planet.Size, planetIndex))
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
{
var lagrangeNode = AddSpatialNode(
nodes,
@@ -113,39 +113,52 @@ public sealed partial class ScenarioLoader
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
Vector3 planetPosition,
float orbitRadius,
float planetSize,
int planetIndex)
PlanetDefinition planet)
{
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
var tangential = new Vector3(-radial.Z, 0f, radial.X);
var offset = ComputePlanetLocalLagrangeOffset(orbitRadius, planetSize, planetIndex);
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", Add(planetPosition, Scale(radial, -(offset * 1.2f))));
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
yield return new LagrangePointPlacement(
"L4",
Add(
planetPosition,
Add(
Scale(radial, offset * MathF.Cos(triangularAngle)),
Scale(tangential, offset * MathF.Sin(triangularAngle)))));
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
yield return new LagrangePointPlacement(
"L5",
Add(
planetPosition,
Add(
Scale(radial, offset * MathF.Cos(triangularAngle)),
Scale(tangential, -offset * MathF.Sin(triangularAngle)))));
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
}
private static float ComputePlanetLocalLagrangeOffset(float orbitRadius, float planetSize, int planetIndex)
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
{
var orbitalScale = MathF.Min(orbitRadius * 0.016f, 96f + (planetIndex * 4f));
var sizeScale = (planetSize * 1.9f) + 10f;
return MathF.Max(22f + (planetIndex * 2f), MathF.Max(orbitalScale, sizeScale));
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(
@@ -210,7 +223,7 @@ public sealed partial class ScenarioLoader
private static Vector3 ComputeResourceNodePosition(NodeRuntime? anchorNode, ResourceNodeDefinition definition, float yPlane)
{
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.18f, 28f);
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,
@@ -227,8 +240,9 @@ public sealed partial class ScenarioLoader
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
{
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
var x = MathF.Cos(angle) * planet.OrbitRadius;
var z = MathF.Sin(angle) * planet.OrbitRadius;
var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
var x = MathF.Cos(angle) * orbitRadiusKm;
var z = MathF.Sin(angle) * orbitRadiusKm;
return new Vector3(x, 0f, z);
}