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>
314 lines
14 KiB
C#
314 lines
14 KiB
C#
namespace SpaceGame.Simulation.Api.Simulation;
|
|
|
|
public sealed partial class SimulationEngine
|
|
{
|
|
private const float WarpEngageDistanceKilometers = 250_000f;
|
|
|
|
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
|
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
|
|
|
|
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
|
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
|
|
|
|
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
|
|
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
|
|
?? Vector3.Zero;
|
|
|
|
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
{
|
|
var task = ship.ControllerTask;
|
|
return task.Kind switch
|
|
{
|
|
ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Refuel => UpdateRefuel(ship, world, deltaSeconds),
|
|
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
|
|
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds),
|
|
ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds),
|
|
ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds),
|
|
ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds),
|
|
ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
|
|
_ => UpdateIdle(ship, world, deltaSeconds),
|
|
};
|
|
}
|
|
|
|
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
{
|
|
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
|
|
ship.State = ShipState.Idle;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
{
|
|
var task = ship.ControllerTask;
|
|
if (task.TargetPosition is null || task.TargetSystemId is null)
|
|
{
|
|
ship.State = ShipState.Idle;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
var targetPosition = task.TargetPosition.Value;
|
|
var targetNode = ResolveTravelTargetNode(world, task, targetPosition);
|
|
ship.TargetPosition = targetPosition;
|
|
|
|
if (ship.SystemId != task.TargetSystemId)
|
|
{
|
|
var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
|
|
var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
|
|
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
|
|
}
|
|
|
|
var currentNode = ResolveCurrentNode(world, ship);
|
|
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
|
|
{
|
|
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
|
|
}
|
|
|
|
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
|
|
}
|
|
|
|
private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
|
{
|
|
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
if (station?.NodeId is not null)
|
|
{
|
|
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId);
|
|
}
|
|
|
|
var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
if (node is not null)
|
|
{
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return world.SpatialNodes
|
|
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
|
|
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship)
|
|
{
|
|
if (ship.SpatialState.CurrentNodeId is not null)
|
|
{
|
|
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId);
|
|
}
|
|
|
|
return world.SpatialNodes
|
|
.Where(candidate => candidate.SystemId == ship.SystemId)
|
|
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) =>
|
|
world.SpatialNodes.FirstOrDefault(candidate =>
|
|
candidate.SystemId == systemId &&
|
|
candidate.Kind == SpatialNodeKind.Star);
|
|
|
|
private string UpdateLocalTravel(
|
|
ShipRuntime ship,
|
|
SimulationWorld world,
|
|
float deltaSeconds,
|
|
string targetSystemId,
|
|
Vector3 targetPosition,
|
|
NodeRuntime? targetNode,
|
|
float threshold)
|
|
{
|
|
var distance = ship.Position.DistanceTo(targetPosition);
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
|
ship.SpatialState.Transit = null;
|
|
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
|
|
|
if (distance <= threshold)
|
|
{
|
|
ship.ActionTimer = 0f;
|
|
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
|
|
ship.Position = targetPosition;
|
|
ship.TargetPosition = ship.Position;
|
|
ship.SystemId = targetSystemId;
|
|
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
|
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
|
ship.State = ShipState.Arriving;
|
|
return "arrived";
|
|
}
|
|
|
|
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
|
{
|
|
ship.State = ShipState.CapacitorStarved;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
ship.ActionTimer = 0f;
|
|
ship.State = ShipState.LocalFlight;
|
|
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
return "none";
|
|
}
|
|
|
|
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode)
|
|
{
|
|
var transit = ship.SpatialState.Transit;
|
|
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id)
|
|
{
|
|
transit = new ShipTransitRuntime
|
|
{
|
|
Regime = MovementRegimeKinds.Warp,
|
|
OriginNodeId = ship.SpatialState.CurrentNodeId,
|
|
DestinationNodeId = targetNode.Id,
|
|
StartedAtUtc = world.GeneratedAtUtc,
|
|
};
|
|
ship.SpatialState.Transit = transit;
|
|
}
|
|
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
|
|
ship.SpatialState.CurrentNodeId = null;
|
|
ship.SpatialState.CurrentBubbleId = null;
|
|
ship.SpatialState.DestinationNodeId = targetNode.Id;
|
|
|
|
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
|
if (ship.State != ShipState.Warping)
|
|
{
|
|
if (ship.State != ShipState.SpoolingWarp)
|
|
{
|
|
ship.ActionTimer = 0f;
|
|
}
|
|
|
|
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
|
|
{
|
|
ship.State = ShipState.CapacitorStarved;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
ship.State = ShipState.SpoolingWarp;
|
|
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
|
|
{
|
|
return "none";
|
|
}
|
|
|
|
ship.State = ShipState.Warping;
|
|
}
|
|
|
|
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
|
|
{
|
|
ship.State = ShipState.CapacitorStarved;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
|
? ship.Position.DistanceTo(targetPosition)
|
|
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
|
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
|
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
|
return ship.Position.DistanceTo(targetPosition) <= 18f
|
|
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
|
|
: "none";
|
|
}
|
|
|
|
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
|
{
|
|
var destinationNodeId = targetNode?.Id;
|
|
var transit = ship.SpatialState.Transit;
|
|
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
|
|
{
|
|
transit = new ShipTransitRuntime
|
|
{
|
|
Regime = MovementRegimeKinds.FtlTransit,
|
|
OriginNodeId = ship.SpatialState.CurrentNodeId,
|
|
DestinationNodeId = destinationNodeId,
|
|
StartedAtUtc = world.GeneratedAtUtc,
|
|
};
|
|
ship.SpatialState.Transit = transit;
|
|
}
|
|
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
|
|
ship.SpatialState.CurrentNodeId = null;
|
|
ship.SpatialState.CurrentBubbleId = null;
|
|
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
|
|
|
if (ship.State != ShipState.Ftl)
|
|
{
|
|
if (ship.State != ShipState.SpoolingFtl)
|
|
{
|
|
ship.ActionTimer = 0f;
|
|
}
|
|
|
|
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
|
|
{
|
|
ship.State = ShipState.CapacitorStarved;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
ship.State = ShipState.SpoolingFtl;
|
|
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
|
|
{
|
|
return "none";
|
|
}
|
|
|
|
ship.State = ShipState.Ftl;
|
|
}
|
|
|
|
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
|
|
{
|
|
ship.State = ShipState.CapacitorStarved;
|
|
ship.TargetPosition = ship.Position;
|
|
return "none";
|
|
}
|
|
|
|
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
|
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
|
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
|
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
|
|
return transit.Progress >= 0.999f
|
|
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
|
|
: "none";
|
|
}
|
|
|
|
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
|
{
|
|
ship.ActionTimer = 0f;
|
|
ship.Position = targetPosition;
|
|
ship.TargetPosition = targetPosition;
|
|
ship.SystemId = targetSystemId;
|
|
ship.SpatialState.Transit = null;
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
|
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
|
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
|
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
|
ship.State = ShipState.Arriving;
|
|
return "arrived";
|
|
}
|
|
|
|
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
|
{
|
|
ship.ActionTimer = 0f;
|
|
ship.Position = targetPosition;
|
|
ship.TargetPosition = targetPosition;
|
|
ship.SystemId = targetSystemId;
|
|
ship.SpatialState.Transit = null;
|
|
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
|
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
|
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
|
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
|
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
|
ship.State = ShipState.Arriving;
|
|
return "none";
|
|
}
|
|
}
|