using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Ships.Simulation; internal sealed partial class ShipTaskExecutionService { 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; internal 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"; } }