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 previousPlanId = ship.ActivePlan?.Id; var previousStepId = GetCurrentStep(ship.ActivePlan)?.Id; EnsurePlan(world, ship, events); ExecutePlan(world, ship, deltaSeconds, events); TrackHistory(ship); EmitStateEvents(ship, previousState, previousPlanId, previousStepId, events); } private void EnsurePlan(SimulationWorld world, ShipRuntime ship, ICollection events) { var emergencyPlan = BuildEmergencyPlan(world, ship); if (emergencyPlan is not null) { ship.LastReplanReason = "rule-safety"; ReplacePlan(ship, emergencyPlan, "rule-safety", events); return; } SyncBehaviorOrders(world, ship); var topOrder = GetTopOrder(ship); if (topOrder is not null && topOrder.Status == OrderStatus.Queued) { topOrder.Status = OrderStatus.Active; } var desiredSourceKind = topOrder is null ? AiPlanSourceKind.DefaultBehavior : AiPlanSourceKind.Order; var desiredSourceId = topOrder?.Id ?? ResolveBehaviorSource(world, ship).SourceId; var currentPlan = ship.ActivePlan; if (currentPlan is not null && currentPlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed and not AiPlanStatus.Interrupted && currentPlan.SourceKind == desiredSourceKind && string.Equals(currentPlan.SourceId, desiredSourceId, StringComparison.Ordinal) && !ship.NeedsReplan) { return; } if (ship.ReplanCooldownSeconds > 0f && currentPlan is null) { return; } ShipPlanRuntime? nextPlan = desiredSourceKind == AiPlanSourceKind.Order ? BuildOrderPlan(world, ship, topOrder!) : BuildBehaviorFallbackPlan(world, ship); if (nextPlan is null) { nextPlan = CreateIdlePlan(ship, desiredSourceKind, desiredSourceId, "No viable plan"); } if (nextPlan.Kind != Idle) { ship.LastAccessFailureReason = null; } ReplacePlan(ship, nextPlan, "replanned", events); } private void ExecutePlan(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection events) { var plan = ship.ActivePlan; if (plan is null) { ship.State = ShipState.Idle; ship.TargetPosition = ship.Position; return; } if (plan.CurrentStepIndex >= plan.Steps.Count) { CompletePlan(ship, plan, events); return; } plan.UpdatedAtUtc = DateTimeOffset.UtcNow; var step = plan.Steps[plan.CurrentStepIndex]; if (step.Status == AiPlanStepStatus.Planned) { step.Status = AiPlanStepStatus.Running; } if (step.CurrentSubTaskIndex >= step.SubTasks.Count) { CompleteStep(plan, step); return; } var subTask = step.SubTasks[step.CurrentSubTaskIndex]; if (subTask.Status == WorkStatus.Pending) { subTask.Status = WorkStatus.Active; } else if (subTask.Status == WorkStatus.Blocked) { step.Status = AiPlanStepStatus.Blocked; step.BlockingReason = subTask.BlockingReason; plan.Status = AiPlanStatus.Blocked; ship.State = ShipState.Blocked; ship.TargetPosition = subTask.TargetPosition ?? ship.Position; return; } plan.Status = AiPlanStatus.Running; var outcome = UpdateSubTask(world, ship, step, subTask, deltaSeconds); switch (outcome) { case SubTaskOutcome.Active: step.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStepStatus.Blocked : AiPlanStepStatus.Running; plan.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStatus.Blocked : AiPlanStatus.Running; return; case SubTaskOutcome.Completed: subTask.Status = WorkStatus.Completed; subTask.Progress = 1f; step.CurrentSubTaskIndex += 1; step.BlockingReason = null; if (step.CurrentSubTaskIndex >= step.SubTasks.Count) { CompleteStep(plan, step); } return; case SubTaskOutcome.Failed: subTask.Status = WorkStatus.Failed; step.Status = AiPlanStepStatus.Failed; plan.Status = AiPlanStatus.Failed; plan.FailureReason = subTask.BlockingReason ?? "subtask-failed"; ship.NeedsReplan = true; ship.ReplanCooldownSeconds = 0.5f; ship.LastReplanReason = plan.FailureReason; return; } } private static void CompleteStep(ShipPlanRuntime plan, ShipPlanStepRuntime step) { step.Status = AiPlanStepStatus.Completed; step.BlockingReason = null; plan.CurrentStepIndex += 1; if (plan.CurrentStepIndex >= plan.Steps.Count) { plan.Status = AiPlanStatus.Completed; } } private static void CompletePlan(ShipRuntime ship, ShipPlanRuntime plan, ICollection events) { plan.Status = AiPlanStatus.Completed; var completedOrder = plan.SourceKind == AiPlanSourceKind.Order ? ship.OrderQueue.FirstOrDefault(order => order.Id == plan.SourceId) : null; if (completedOrder is not null) { completedOrder.Status = OrderStatus.Completed; ship.OrderQueue.RemoveAll(order => order.Id == completedOrder.Id); if (completedOrder.SourceKind == ShipOrderSourceKind.Behavior && string.Equals(completedOrder.SourceId, RepeatOrders, StringComparison.Ordinal) && ship.DefaultBehavior.RepeatOrders.Count > 0) { ship.DefaultBehavior.RepeatIndex = (ship.DefaultBehavior.RepeatIndex + 1) % ship.DefaultBehavior.RepeatOrders.Count; } } ship.ActivePlan = null; ship.NeedsReplan = true; ship.ReplanCooldownSeconds = 0.25f; ship.LastReplanReason = "plan-completed"; events.Add(new SimulationEventRecord("ship", ship.Id, "plan-completed", $"{ship.Definition.Name} completed {plan.Kind}.", DateTimeOffset.UtcNow)); } private void ReplacePlan(ShipRuntime ship, ShipPlanRuntime nextPlan, string reason, ICollection events) { if (ship.ActivePlan is not null && ship.ActivePlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed) { ship.ActivePlan.Status = AiPlanStatus.Interrupted; ship.ActivePlan.InterruptReason = reason; } ship.ActivePlan = nextPlan; ship.NeedsReplan = false; ship.ReplanCooldownSeconds = 0f; ship.LastReplanReason = reason; events.Add(new SimulationEventRecord("ship", ship.Id, "plan-updated", $"{ship.Definition.Name} planned {nextPlan.Kind}.", DateTimeOffset.UtcNow)); } }