221 lines
8.4 KiB
C#
221 lines
8.4 KiB
C#
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
|
|
|
|
namespace SpaceGame.Api.Ships.AI;
|
|
|
|
public sealed partial class ShipAiService
|
|
{
|
|
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 readonly IBalanceService balance;
|
|
|
|
public ShipAiService(IBalanceService balance)
|
|
{
|
|
this.balance = balance;
|
|
}
|
|
|
|
internal void UpdateShip(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
if (ship.ReplanCooldownSeconds > 0f)
|
|
{
|
|
ship.ReplanCooldownSeconds = MathF.Max(0f, ship.ReplanCooldownSeconds - deltaSeconds);
|
|
}
|
|
|
|
var previousState = ship.State;
|
|
var previousOrderId = ship.ActiveOrderId;
|
|
var previousTaskId = GetCurrentSubTask(ship)?.Id;
|
|
|
|
SyncEmergencyOrders(world, ship);
|
|
SyncBehaviorOrders(world, ship);
|
|
EnsureOrderExecution(world, ship, events);
|
|
ExecuteOrder(world, ship, deltaSeconds, events);
|
|
TrackHistory(ship);
|
|
EmitStateEvents(ship, previousState, previousOrderId, previousTaskId, events);
|
|
}
|
|
|
|
private void EnsureOrderExecution(SimulationWorld world, ShipRuntime ship, ICollection<SimulationEventRecord> events)
|
|
{
|
|
var currentOrder = ship.OrderQueue.GetCurrentOrder();
|
|
if (currentOrder is null)
|
|
{
|
|
ClearActiveOrder(ship);
|
|
ApplyIdleOrBlockedState(world, ship);
|
|
return;
|
|
}
|
|
|
|
if (currentOrder.Status == OrderStatus.Queued)
|
|
{
|
|
currentOrder.Status = OrderStatus.Active;
|
|
}
|
|
|
|
if (!ship.NeedsReplan
|
|
&& string.Equals(ship.ActiveOrderId, currentOrder.Id, StringComparison.Ordinal)
|
|
&& ship.ActiveSubTaskIndex < ship.ActiveSubTasks.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ship.ReplanCooldownSeconds > 0f && !string.Equals(ship.ActiveOrderId, currentOrder.Id, StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var subTasks = BuildOrderSubTasks(world, ship, currentOrder);
|
|
if (subTasks is null || subTasks.Count == 0)
|
|
{
|
|
FailOrder(ship, currentOrder, currentOrder.FailureReason ?? "order-unavailable");
|
|
ClearActiveOrder(ship);
|
|
ship.NeedsReplan = true;
|
|
ship.ReplanCooldownSeconds = 0.1f;
|
|
ship.LastReplanReason = currentOrder.FailureReason ?? "order-unavailable";
|
|
events.Add(new SimulationEventRecord("ship", ship.Id, "order-failed", $"{ship.Definition.Name} failed {currentOrder.Label ?? currentOrder.Kind}.", DateTimeOffset.UtcNow));
|
|
ApplyIdleOrBlockedState(world, ship);
|
|
return;
|
|
}
|
|
|
|
BeginOrderExecution(ship, currentOrder, subTasks);
|
|
events.Add(new SimulationEventRecord("ship", ship.Id, "order-started", $"{ship.Definition.Name} started {currentOrder.Label ?? currentOrder.Kind}.", DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
private void ExecuteOrder(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
var order = ship.ActiveOrderId is null ? null : ship.OrderQueue.FindById(ship.ActiveOrderId);
|
|
if (order is null)
|
|
{
|
|
ClearActiveOrder(ship);
|
|
ApplyIdleOrBlockedState(world, ship);
|
|
return;
|
|
}
|
|
|
|
if (ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count)
|
|
{
|
|
CompleteOrderExecution(ship, order, events);
|
|
return;
|
|
}
|
|
|
|
var subTask = ship.ActiveSubTasks[ship.ActiveSubTaskIndex];
|
|
if (subTask.Status == WorkStatus.Pending)
|
|
{
|
|
subTask.Status = WorkStatus.Active;
|
|
}
|
|
else if (subTask.Status == WorkStatus.Blocked)
|
|
{
|
|
ship.State = ShipState.Blocked;
|
|
ship.TargetPosition = subTask.TargetPosition ?? ship.Position;
|
|
return;
|
|
}
|
|
|
|
var outcome = UpdateSubTask(world, ship, subTask, deltaSeconds);
|
|
switch (outcome)
|
|
{
|
|
case SubTaskOutcome.Active:
|
|
return;
|
|
case SubTaskOutcome.Completed:
|
|
subTask.Status = WorkStatus.Completed;
|
|
subTask.Progress = 1f;
|
|
ship.ActiveSubTaskIndex += 1;
|
|
if (ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count)
|
|
{
|
|
CompleteOrderExecution(ship, order, events);
|
|
}
|
|
|
|
return;
|
|
case SubTaskOutcome.Failed:
|
|
subTask.Status = WorkStatus.Failed;
|
|
FailOrderExecution(ship, order, subTask.BlockingReason ?? "subtask-failed", events);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private static void BeginOrderExecution(ShipRuntime ship, ShipOrderRuntime order, IReadOnlyList<ShipSubTaskRuntime> subTasks)
|
|
{
|
|
ship.ActiveOrderId = order.Id;
|
|
ship.ActiveSubTaskIndex = 0;
|
|
ship.ActiveSubTasks.Clear();
|
|
ship.ActiveSubTasks.AddRange(subTasks);
|
|
ship.NeedsReplan = false;
|
|
ship.ReplanCooldownSeconds = 0f;
|
|
ship.LastReplanReason = "order-execution-started";
|
|
ship.LastDeltaSignature = string.Empty;
|
|
}
|
|
|
|
private static void ClearActiveOrder(ShipRuntime ship)
|
|
{
|
|
ship.ActiveOrderId = null;
|
|
ship.ActiveSubTaskIndex = 0;
|
|
ship.ActiveSubTasks.Clear();
|
|
}
|
|
|
|
private void CompleteOrderExecution(ShipRuntime ship, ShipOrderRuntime order, ICollection<SimulationEventRecord> events)
|
|
{
|
|
ship.OrderQueue.TryCompleteOrder(order.Id);
|
|
if (order.SourceKind == ShipOrderSourceKind.Behavior
|
|
&& string.Equals(order.SourceId, RepeatOrders, StringComparison.Ordinal)
|
|
&& ship.DefaultBehavior.RepeatOrders.Count > 0)
|
|
{
|
|
ship.DefaultBehavior.RepeatIndex = (ship.DefaultBehavior.RepeatIndex + 1) % ship.DefaultBehavior.RepeatOrders.Count;
|
|
}
|
|
|
|
ClearActiveOrder(ship);
|
|
ship.NeedsReplan = true;
|
|
ship.ReplanCooldownSeconds = 0.25f;
|
|
ship.LastReplanReason = "order-completed";
|
|
ship.LastDeltaSignature = string.Empty;
|
|
events.Add(new SimulationEventRecord("ship", ship.Id, "order-completed", $"{ship.Definition.Name} completed {order.Label ?? order.Kind}.", DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
private void FailOrderExecution(ShipRuntime ship, ShipOrderRuntime order, string failureReason, ICollection<SimulationEventRecord> events)
|
|
{
|
|
FailOrder(ship, order, failureReason);
|
|
ClearActiveOrder(ship);
|
|
ship.NeedsReplan = true;
|
|
ship.ReplanCooldownSeconds = 0.5f;
|
|
ship.LastReplanReason = failureReason;
|
|
ship.LastDeltaSignature = string.Empty;
|
|
events.Add(new SimulationEventRecord("ship", ship.Id, "order-failed", $"{ship.Definition.Name} failed {order.Label ?? order.Kind}.", DateTimeOffset.UtcNow));
|
|
}
|
|
|
|
private static void FailOrder(ShipRuntime ship, ShipOrderRuntime order, string failureReason)
|
|
{
|
|
ship.OrderQueue.TryFailOrder(order.Id, failureReason);
|
|
ship.LastDeltaSignature = string.Empty;
|
|
}
|
|
|
|
private static ShipSubTaskRuntime? GetCurrentSubTask(ShipRuntime ship) =>
|
|
ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count ? null : ship.ActiveSubTasks[ship.ActiveSubTaskIndex];
|
|
|
|
private void ApplyIdleOrBlockedState(SimulationWorld world, ShipRuntime ship)
|
|
{
|
|
var (behaviorKind, _) = ResolveBehaviorSource(world, ship);
|
|
if (IsBehaviorBlockingFailure(behaviorKind, ship.LastAccessFailureReason))
|
|
{
|
|
ship.State = ShipState.Blocked;
|
|
ship.TargetPosition = ship.Position;
|
|
return;
|
|
}
|
|
|
|
ship.State = ShipState.Idle;
|
|
ship.TargetPosition = ship.Position;
|
|
}
|
|
|
|
private void SyncEmergencyOrders(SimulationWorld world, ShipRuntime ship)
|
|
{
|
|
var desiredOrder = BuildEmergencyOrder(world, ship);
|
|
ship.OrderQueue.RemoveWhere(order =>
|
|
order.SourceKind == ShipOrderSourceKind.Behavior
|
|
&& string.Equals(order.SourceId, ShipOrderKinds.Flee, StringComparison.Ordinal)
|
|
&& (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)));
|
|
|
|
if (desiredOrder is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.OrderQueue.AddOrReplaceManagedOrderAtFront(desiredOrder);
|
|
}
|
|
}
|