Files
space-game/apps/backend/Simulation/SimulationEngine.MovementSystem.cs

310 lines
12 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.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(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)
{
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";
}
// Resolve live position each frame — entities like stations orbit celestials and move every tick
var targetPosition = ResolveCurrentTargetPosition(world, task);
var targetCelestial = ResolveTravelTargetCelestial(world, task, targetPosition);
var distance = ship.Position.DistanceTo(targetPosition);
ship.TargetPosition = targetPosition;
if (ship.SystemId != task.TargetSystemId)
{
if (!HasShipCapabilities(ship.Definition, "ftl"))
{
ship.State = ShipState.Idle;
return "none";
}
var destinationEntryCelestial = ResolveSystemEntryCelestial(world, task.TargetSystemId);
var destinationEntryPosition = destinationEntryCelestial?.Position ?? Vector3.Zero;
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryCelestial);
}
var currentCelestial = ResolveCurrentCelestial(world, ship);
if (targetCelestial is not null && currentCelestial is not null && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
{
if (!HasShipCapabilities(ship.Definition, "warp"))
{
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
}
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
}
if (targetCelestial is not null
&& distance > WarpEngageDistanceKilometers
&& HasShipCapabilities(ship.Definition, "warp"))
{
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
}
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
}
private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ControllerTaskRuntime task)
{
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
{
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (station is not null)
{
return station.Position;
}
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (celestial is not null)
{
return celestial.Position;
}
}
return task.TargetPosition!.Value;
}
private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
{
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
{
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (station?.CelestialId is not null)
{
return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId);
}
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (celestial is not null)
{
return celestial;
}
}
return world.Celestials
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
.FirstOrDefault();
}
private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship)
{
if (ship.SpatialState.CurrentCelestialId is not null)
{
return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId);
}
return world.Celestials
.Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
.FirstOrDefault();
}
private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
world.Celestials.FirstOrDefault(candidate =>
candidate.SystemId == systemId &&
candidate.Kind == SpatialNodeKind.Star);
private string UpdateLocalTravel(
ShipRuntime ship,
SimulationWorld world,
float deltaSeconds,
string targetSystemId,
Vector3 targetPosition,
CelestialRuntime? targetCelestial,
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 = targetCelestial?.Id;
if (distance <= threshold)
{
ship.ActionTimer = 0f;
ship.Position = targetPosition;
ship.TargetPosition = ship.Position;
ship.SystemId = targetSystemId;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
ship.State = ShipState.Arriving;
return "arrived";
}
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, CelestialRuntime targetCelestial)
{
var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetCelestial.Id)
{
transit = new ShipTransitRuntime
{
Regime = MovementRegimeKinds.Warp,
OriginNodeId = ship.SpatialState.CurrentCelestialId,
DestinationNodeId = targetCelestial.Id,
StartedAtUtc = world.GeneratedAtUtc,
};
ship.SpatialState.Transit = transit;
}
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
ship.SpatialState.CurrentCelestialId = null;
ship.SpatialState.DestinationNodeId = targetCelestial.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;
}
ship.State = ShipState.SpoolingWarp;
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
{
return "none";
}
ship.State = ShipState.Warping;
}
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
? ship.Position.DistanceTo(targetPosition)
: (world.Celestials.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, targetCelestial.SystemId, targetPosition, targetCelestial)
: "none";
}
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
{
var destinationNodeId = targetCelestial?.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.CurrentCelestialId,
DestinationNodeId = destinationNodeId,
StartedAtUtc = world.GeneratedAtUtc,
};
ship.SpatialState.Transit = transit;
}
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
ship.SpatialState.CurrentCelestialId = null;
ship.SpatialState.DestinationNodeId = destinationNodeId;
if (ship.State != ShipState.Ftl)
{
if (ship.State != ShipState.SpoolingFtl)
{
ship.ActionTimer = 0f;
}
ship.State = ShipState.SpoolingFtl;
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
{
return "none";
}
ship.State = ShipState.Ftl;
}
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, targetCelestial)
: "none";
}
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
{
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.CurrentCelestialId = targetCelestial?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
ship.State = ShipState.Arriving;
return "arrived";
}
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
{
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.CurrentCelestialId = targetCelestial?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
ship.State = ShipState.Arriving;
return "none";
}
}