393 lines
15 KiB
C#
393 lines
15 KiB
C#
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 const float FrigateDps = 7f;
|
|
private const float DestroyerDps = 12f;
|
|
private const float CruiserDps = 18f;
|
|
private const float CapitalDps = 26f;
|
|
|
|
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.AttackTarget => UpdateAttackTarget(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;
|
|
return UpdateTravel(ship, world, deltaSeconds, task);
|
|
}
|
|
|
|
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ControllerTaskRuntime task)
|
|
{
|
|
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 string UpdateAttackTarget(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
{
|
|
var task = ship.ControllerTask;
|
|
if (string.IsNullOrWhiteSpace(task.TargetEntityId))
|
|
{
|
|
ship.State = ShipState.Idle;
|
|
ship.TargetPosition = ship.Position;
|
|
return "target-lost";
|
|
}
|
|
|
|
var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId && candidate.Health > 0f);
|
|
var hostileStation = hostileShip is null
|
|
? world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId)
|
|
: null;
|
|
|
|
if ((hostileShip is not null && string.Equals(hostileShip.FactionId, ship.FactionId, StringComparison.Ordinal))
|
|
|| (hostileStation is not null && string.Equals(hostileStation.FactionId, ship.FactionId, StringComparison.Ordinal)))
|
|
{
|
|
return "target-lost";
|
|
}
|
|
|
|
if (hostileShip is null && hostileStation is null)
|
|
{
|
|
ship.State = ShipState.Idle;
|
|
ship.TargetPosition = ship.Position;
|
|
return "target-lost";
|
|
}
|
|
|
|
var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId;
|
|
var targetPosition = hostileShip?.Position ?? hostileStation!.Position;
|
|
var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f;
|
|
var attackTask = new ControllerTaskRuntime
|
|
{
|
|
Kind = ControllerTaskKind.Travel,
|
|
TargetEntityId = task.TargetEntityId,
|
|
TargetSystemId = targetSystemId,
|
|
TargetPosition = targetPosition,
|
|
Threshold = attackRange,
|
|
};
|
|
|
|
if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange)
|
|
{
|
|
return UpdateTravel(ship, world, deltaSeconds, attackTask);
|
|
}
|
|
|
|
ship.State = ShipState.EngagingTarget;
|
|
ship.TargetPosition = targetPosition;
|
|
ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f));
|
|
var damage = GetShipDamagePerSecond(ship) * deltaSeconds;
|
|
|
|
if (hostileShip is not null)
|
|
{
|
|
hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage);
|
|
return hostileShip.Health <= 0f ? "target-destroyed" : "none";
|
|
}
|
|
|
|
hostileStation!.Health = MathF.Max(0f, hostileStation.Health - damage * 0.6f);
|
|
return hostileStation.Health <= 0f ? "target-destroyed" : "none";
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
private static float GetShipDamagePerSecond(ShipRuntime ship) =>
|
|
ship.Definition.Class switch
|
|
{
|
|
"frigate" => FrigateDps,
|
|
"destroyer" => DestroyerDps,
|
|
"cruiser" => CruiserDps,
|
|
"capital" => CapitalDps,
|
|
_ => 4f,
|
|
};
|
|
}
|