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 ShipPlanRuntime BuildBehaviorFallbackPlan(SimulationWorld world, ShipRuntime ship) { var (behaviorKind, sourceId) = ResolveBehaviorSource(world, ship); var failureReason = ship.LastAccessFailureReason; if (string.Equals(behaviorKind, Idle, StringComparison.Ordinal)) { return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "Idle"); } if (IsBehaviorBlockingFailure(behaviorKind, failureReason)) { return CreateBlockedPlan( ship, AiPlanSourceKind.DefaultBehavior, sourceId, DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason), failureReason!); } return CreateIdlePlan( ship, AiPlanSourceKind.DefaultBehavior, sourceId, DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason)); } private static bool IsBehaviorBlockingFailure(string behaviorKind, string? failureReason) => failureReason switch { "missing-item" => true, "no-suitable-buyer" => true, "no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => true, _ => false, }; private static string DescribeBehaviorFallbackSummary(SimulationWorld world, ShipRuntime ship, string behaviorKind, string? failureReason) { var assignment = ResolveAssignment(world, ship); var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId ?? "resource"; return failureReason switch { "missing-item" => "No mining ware configured", "no-suitable-buyer" => $"No buyer for {itemId} in {systemId}", "no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => $"No {itemId} to mine in {systemId}", "no-mineable-node" => "No mineable node", "no-home-station" => "No home station", "no-trade-route" => "No trade route", "no-fleet-to-supply" => "No fleet to supply", "station-missing" => "No station to dock", "target-ship-missing" => "No ship to follow", "target-missing" => "No object target", "no-salvage-target" => "No salvage target", "no-repeat-orders" => "No repeat orders", "no-construction-site" => "No construction site", "support-station-missing" => "No support station", _ => "Idle", }; } private ShipPlanRuntime BuildTradePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, TradeRoutePlan route, string summary) { return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.TradeRoute, summary, [ CreateStep("step-acquire", "acquire-cargo", $"Load {route.ItemId} at {route.SourceStation.Label}", [ CreateSubTask("sub-acquire-travel", ShipTaskKinds.Travel, $"Travel to {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(route.SourceStation.Radius + 12f, 12f), 0f), CreateSubTask("sub-acquire-dock", ShipTaskKinds.Dock, $"Dock at {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 4f, 0f), CreateSubTask("sub-acquire-load", ShipTaskKinds.LoadCargo, $"Load {route.ItemId}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: route.ItemId), CreateSubTask("sub-acquire-undock", ShipTaskKinds.Undock, $"Undock from {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(4f, 12f), 0f) ]), CreateStep("step-deliver", "deliver-cargo", $"Deliver {route.ItemId} to {route.DestinationStation.Label}", [ CreateSubTask("sub-route-travel", ShipTaskKinds.Travel, $"Travel to {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(route.DestinationStation.Radius + 12f, 12f), 0f), CreateSubTask("sub-route-dock", ShipTaskKinds.Dock, $"Dock at {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 4f, 0f), CreateSubTask("sub-route-unload", ShipTaskKinds.UnloadCargo, $"Unload {route.ItemId}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: route.ItemId), CreateSubTask("sub-route-undock", ShipTaskKinds.Undock, $"Undock from {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(4f, 12f), 0f) ]) ]); } private ShipPlanRuntime BuildFleetSupplyPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, FleetSupplyPlan plan) { return CreatePlan( ship, sourceKind, sourceId, SupplyFleet, plan.Summary, [ CreateStep("step-fleet-acquire", "acquire-cargo", $"Load {plan.ItemId} at {plan.SourceStation.Label}", [ CreateSubTask("sub-fleet-source-travel", ShipTaskKinds.Travel, $"Travel to {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, MathF.Max(plan.SourceStation.Radius + 12f, 12f), 0f), CreateSubTask("sub-fleet-source-dock", ShipTaskKinds.Dock, $"Dock at {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 4f, 0f), CreateSubTask("sub-fleet-source-load", ShipTaskKinds.LoadCargo, $"Load {plan.ItemId}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 0f, plan.Amount, itemId: plan.ItemId), CreateSubTask("sub-fleet-source-undock", ShipTaskKinds.Undock, $"Undock from {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 12f, 0f), ]), CreateStep("step-fleet-deliver", "deliver-fleet", $"Deliver {plan.ItemId} to {plan.TargetShip.Definition.Name}", [ CreateSubTask("sub-fleet-follow", ShipTaskKinds.FollowTarget, $"Rendezvous with {plan.TargetShip.Definition.Name}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, 6f), CreateSubTask("sub-fleet-transfer", ShipTaskKinds.TransferCargoToShip, $"Transfer {plan.ItemId}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, plan.Amount, itemId: plan.ItemId), ]) ]); } private ShipPlanRuntime BuildConstructionPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ConstructionSiteRuntime site, StationRuntime supportStation, string summary) { var targetPosition = site.StationId is null ? supportStation.Position : supportStation.Position; return CreatePlan( ship, sourceKind, sourceId, "construction-support", summary, [ CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}", [ CreateSubTask("sub-construction-travel", ShipTaskKinds.Travel, $"Travel to support station {supportStation.Label}", supportStation.SystemId, targetPosition, supportStation.Id, MathF.Max(supportStation.Radius + 18f, 18f), 0f), CreateSubTask("sub-construction-deliver", ShipTaskKinds.DeliverConstruction, $"Deliver materials to {site.Id}", site.SystemId, supportStation.Position, site.Id, 12f, 0f) ]), CreateStep("step-construction-build", "build-site", $"Build {site.Id}", [ CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, supportStation.Position, site.Id, 12f, 0f) ]) ]); } private ShipPlanRuntime BuildAttackPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string? targetSystemId, string summary) { return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.AttackTarget, summary, [ CreateStep("step-attack", ShipOrderKinds.AttackTarget, summary, [ CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? ship.SystemId, ship.Position, targetEntityId, 26f, 0f) ]) ]); } private ShipPlanRuntime BuildDockAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime station, float waitSeconds, string summary) { return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.DockAndWait, summary, [ CreateStep("step-dock-wait-travel", "travel", $"Travel to {station.Label}", [ CreateSubTask("sub-dock-wait-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(station.Radius + 12f, 12f), 0f), CreateSubTask("sub-dock-wait-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f), CreateSubTask("sub-dock-wait-hold", ShipTaskKinds.HoldPosition, $"Wait at {station.Label}", station.SystemId, station.Position, station.Id, 0f, waitSeconds), ]) ]); } private ShipPlanRuntime BuildFlyAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, float waitSeconds, string summary) { return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.FlyAndWait, summary, [ CreateStep("step-fly-wait", ShipOrderKinds.FlyAndWait, summary, [ CreateSubTask("sub-fly-wait-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, null, 6f, 0f), CreateSubTask("sub-fly-wait-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, null, 0f, waitSeconds), ]) ]); } private ShipPlanRuntime BuildFlyToObjectPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary) { return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.FlyToObject, summary, [ CreateStep("step-fly-object", ShipOrderKinds.FlyToObject, summary, [ CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f), CreateSubTask("sub-fly-object-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, targetEntityId, 0f, MathF.Max(1f, ship.DefaultBehavior.WaitSeconds)), ]) ]); } private ShipPlanRuntime BuildFollowShipPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ShipRuntime targetShip, float radius, float durationSeconds, string summary) { return BuildFollowPlan(ship, sourceKind, sourceId, targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary); } private ShipPlanRuntime BuildFollowPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary) { return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.FollowShip, summary, [ CreateStep("step-follow", "follow-target", summary, [ CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds), ]) ]); } private ShipPlanRuntime CreateIdlePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary) { return CreatePlan( ship, sourceKind, sourceId, Idle, summary, [ CreateStep("step-idle", ShipOrderKinds.HoldPosition, summary, [ CreateSubTask("sub-idle", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 1.5f) ]) ]); } private ShipPlanRuntime CreateBlockedPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary, string blockingReason) { var subTask = CreateSubTask("sub-blocked", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 0f); subTask.Status = WorkStatus.Blocked; subTask.BlockingReason = blockingReason; var step = CreateStep("step-blocked", "blocked", summary, [subTask]); step.Status = AiPlanStepStatus.Blocked; step.BlockingReason = blockingReason; var plan = CreatePlan(ship, sourceKind, sourceId, "blocked", summary, [step]); plan.Status = AiPlanStatus.Blocked; plan.FailureReason = blockingReason; return plan; } private static ShipPlanRuntime CreatePlan( ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string kind, string summary, IReadOnlyList steps) { var plan = new ShipPlanRuntime { Id = $"plan-{ship.Id}-{Guid.NewGuid():N}", SourceKind = sourceKind, SourceId = sourceId, Kind = kind, Summary = summary, }; plan.Steps.AddRange(steps); return plan; } private static ShipPlanStepRuntime CreateStep(string id, string kind, string summary, IReadOnlyList subTasks) { var step = new ShipPlanStepRuntime { Id = id, Kind = kind, Summary = summary, }; step.SubTasks.AddRange(subTasks); return step; } private static ShipSubTaskRuntime CreateSubTask( string id, string kind, string summary, string targetSystemId, Vector3 targetPosition, string? targetEntityId, float threshold, float amount, string? itemId = null, string? moduleId = null, string? targetAnchorId = null, string? targetResourceNodeId = null, string? targetResourceDepositId = null) => new() { Id = id, Kind = kind, Summary = summary, TargetSystemId = targetSystemId, TargetPosition = targetPosition, TargetEntityId = targetEntityId, TargetAnchorId = targetAnchorId, TargetResourceNodeId = targetResourceNodeId, TargetResourceDepositId = targetResourceDepositId, ItemId = itemId, ModuleId = moduleId, Threshold = threshold, Amount = amount, }; }