using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; namespace SpaceGame.Api.Ships.AI; public sealed partial class ShipAiService { private static bool IsBehaviorBlockingFailure(string behaviorKind, string? failureReason) => failureReason switch { "missing-item" => true, "no-suitable-buyer" => true, "no-mineable-node" when string.Equals(behaviorKind, ShipBehaviorKinds.LocalAutoMine, StringComparison.Ordinal) => true, _ => false, }; private static (string BehaviorKind, string SourceId) ResolveBehaviorSource(SimulationWorld world, ShipRuntime ship) { var assignment = ResolveAssignment(world, ship); return assignment is null ? (ship.DefaultBehavior.Kind, ship.DefaultBehavior.Kind) : (assignment.BehaviorKind, assignment.ObjectiveId); } private IReadOnlyList BuildTradeSubTasks(ShipRuntime ship, TradeRoutePlan route) { return [ 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), 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 IReadOnlyList BuildFleetSupplySubTasks(FleetSupplyPlan plan) { return [ 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), 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 IReadOnlyList BuildConstructionSubTasks(ConstructionSiteRuntime site, StationRuntime supportStation) { var targetPosition = supportStation.Position; return [ 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), CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, supportStation.Position, site.Id, 12f, 0f), ]; } private static IReadOnlyList BuildAttackSubTasks(string targetEntityId, string? targetSystemId, string summary) { return [ CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? string.Empty, Vector3.Zero, targetEntityId, 26f, 0f), ]; } private static IReadOnlyList BuildFlyToObjectSubTasks(string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary) { return [ CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f), ]; } private static IReadOnlyList BuildFollowShipSubTasks(ShipRuntime targetShip, float radius, float durationSeconds, string summary) => BuildFollowSubTasks(targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary); private static IReadOnlyList BuildFollowSubTasks(string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary) { return [ CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds), ]; } private static IReadOnlyList BuildHoldSubTasks(ShipRuntime ship, ShipOrderRuntime order) { return [ CreateSubTask("sub-hold", ShipTaskKinds.HoldPosition, order.Label ?? "Hold position", order.TargetSystemId ?? ship.SystemId, order.TargetPosition ?? ship.Position, order.TargetEntityId, 0f, 3f), ]; } private IReadOnlyList BuildMiningSubTasks(ShipRuntime ship, ResourceNodeRuntime node, StationRuntime homeStation) { var deposit = SelectMiningDeposit(node, ship.Id); var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, ship.Id, 20f); return [ CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} field", node.SystemId, Vector3.Zero, node.AnchorId, 8f, 0f, targetAnchorId: node.AnchorId), CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId, targetAnchorId: node.AnchorId, targetResourceNodeId: node.Id, targetResourceDepositId: deposit?.Id), CreateSubTask("sub-deliver-travel", ShipTaskKinds.Travel, $"Travel to {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(homeStation.Radius + 12f, 12f), 0f), CreateSubTask("sub-deliver-dock", ShipTaskKinds.Dock, $"Dock at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 4f, 0f), CreateSubTask("sub-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 0f, ship.Definition.GetTotalCargoCapacity()), CreateSubTask("sub-deliver-undock", ShipTaskKinds.Undock, $"Undock from {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(4f, 12f), 0f), ]; } private IReadOnlyList BuildLocalMiningSubTasks(ShipRuntime ship, ResourceNodeRuntime node) { var deposit = SelectMiningDeposit(node, ship.Id); var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, ship.Id, 20f); return [ CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} field", node.SystemId, Vector3.Zero, node.AnchorId, 8f, 0f, targetAnchorId: node.AnchorId), CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId, targetAnchorId: node.AnchorId, targetResourceNodeId: node.Id, targetResourceDepositId: deposit?.Id), ]; } private IReadOnlyList BuildLocalMiningDeliverySubTasks(ShipRuntime ship, StationRuntime buyer, string itemId) { var amount = MathF.Max(1f, GetInventoryAmount(ship.Inventory, itemId)); return [ CreateSubTask("sub-deliver-travel", ShipTaskKinds.Travel, $"Travel to {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, MathF.Max(buyer.Radius + 12f, 12f), 0f), CreateSubTask("sub-deliver-dock", ShipTaskKinds.Dock, $"Dock at {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, 4f, 0f), CreateSubTask("sub-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload {itemId} at {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, 0f, amount, itemId: itemId), CreateSubTask("sub-deliver-undock", ShipTaskKinds.Undock, $"Undock from {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, MathF.Max(4f, 12f), 0f), ]; } private IReadOnlyList BuildSalvageSubTasks(ShipRuntime ship, WreckRuntime wreck, StationRuntime homeStation, Vector3 approach) { return [ CreateSubTask("sub-salvage-travel", ShipTaskKinds.Travel, $"Travel to wreck {wreck.Id}", wreck.SystemId, approach, wreck.Id, 8f, 0f), CreateSubTask("sub-salvage-work", ShipTaskKinds.SalvageWreck, $"Salvage {wreck.ItemId}", wreck.SystemId, approach, wreck.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: wreck.ItemId), CreateSubTask("sub-salvage-deliver-travel", ShipTaskKinds.Travel, $"Travel to {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(homeStation.Radius + 12f, 12f), 0f), CreateSubTask("sub-salvage-deliver-dock", ShipTaskKinds.Dock, $"Dock at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 4f, 0f), CreateSubTask("sub-salvage-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload salvage at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: wreck.ItemId), CreateSubTask("sub-salvage-deliver-undock", ShipTaskKinds.Undock, $"Undock from {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 12f, 0f), ]; } 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, }; }