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 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 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 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 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 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 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); } }