using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; using static SpaceGame.Api.Stations.Simulation.StationSimulationService; namespace SpaceGame.Api.Ships.AI; public sealed partial class ShipAiService { private ShipPlanRuntime? BuildEmergencyPlan(SimulationWorld world, ShipRuntime ship) { var policy = ResolvePolicy(world, ship.PolicySetId); if (policy is null) { return null; } var hullRatio = ship.Definition.Hull <= 0.01f ? 1f : ship.Health / ship.Definition.Hull; if (hullRatio > policy.FleeHullRatio) { return null; } var hostileNearby = world.Ships.Any(candidate => candidate.Health > 0f && candidate.FactionId != ship.FactionId && candidate.SystemId == ship.SystemId && candidate.Position.DistanceTo(ship.Position) <= 200f); if (!hostileNearby) { return null; } var safeStation = world.Stations .Where(station => station.FactionId == ship.FactionId) .OrderByDescending(station => station.SystemId == ship.SystemId ? 1 : 0) .ThenBy(station => station.Position.DistanceTo(ship.Position)) .FirstOrDefault(); var plan = new ShipPlanRuntime { Id = $"plan-{ship.Id}-safety-{Guid.NewGuid():N}", SourceKind = AiPlanSourceKind.Rule, SourceId = ShipOrderKinds.Flee, Kind = "safety-flee", Summary = "Emergency retreat", }; if (safeStation is null) { plan.Steps.Add(CreateStep("step-flee-hold", ShipOrderKinds.HoldPosition, "Hold position away from hostiles", [ CreateSubTask("sub-flee-hold", ShipTaskKinds.HoldPosition, "Hold safe position", ship.SystemId, ship.Position, null, 0f, 3f) ])); return plan; } plan.Steps.Add(CreateStep("step-flee-travel", "travel", "Travel to safe station", [ CreateSubTask("sub-flee-travel", ShipTaskKinds.Travel, $"Travel to {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, MathF.Max(balance.ArrivalThreshold, safeStation.Radius + 12f), 0f) ])); plan.Steps.Add(CreateStep("step-flee-dock", "dock", "Dock at safe station", [ CreateSubTask("sub-flee-dock", ShipTaskKinds.Dock, $"Dock at {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, 4f, 0f) ])); return plan; } private ShipPlanRuntime? BuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { return order.Kind switch { var kind when string.Equals(kind, ShipOrderKinds.Move, StringComparison.Ordinal) => BuildMovePlan(ship, order), var kind when string.Equals(kind, ShipOrderKinds.DockAtStation, StringComparison.Ordinal) => BuildDockOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.DockAndWait, StringComparison.Ordinal) => BuildDockAndWaitOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.FlyAndWait, StringComparison.Ordinal) => BuildFlyAndWaitOrderPlan(ship, order), var kind when string.Equals(kind, ShipOrderKinds.FlyToObject, StringComparison.Ordinal) => BuildFlyToObjectOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.FollowShip, StringComparison.Ordinal) => BuildFollowShipOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.TradeRoute, StringComparison.Ordinal) => BuildTradeOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliver, StringComparison.Ordinal) => BuildMineOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.MineLocal, StringComparison.Ordinal) => BuildMineLocalOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliverRun, StringComparison.Ordinal) => BuildMineAndDeliverRunOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.SellMinedCargo, StringComparison.Ordinal) => BuildSellMinedCargoOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.SupplyFleetRun, StringComparison.Ordinal) => BuildSupplyFleetOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.SalvageRun, StringComparison.Ordinal) => BuildAutoSalvageOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.BuildAtSite, StringComparison.Ordinal) => BuildBuildOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.AttackTarget, StringComparison.Ordinal) => BuildAttackOrderPlan(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.HoldPosition, StringComparison.Ordinal) => BuildHoldOrderPlan(ship, order), _ => null, }; } 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 ShipPlanRuntime BuildMovePlan(ShipRuntime ship, ShipOrderRuntime order) { var targetSystemId = order.TargetSystemId ?? ship.SystemId; var targetPosition = order.TargetPosition ?? ship.Position; return CreatePlan( ship, AiPlanSourceKind.Order, order.Id, ShipOrderKinds.Move, order.Label ?? "Move order", [ CreateStep("step-move", "travel", order.Label ?? "Travel", [ CreateSubTask("sub-move-travel", ShipTaskKinds.Travel, order.Label ?? "Travel", targetSystemId, targetPosition, order.TargetEntityId, 0f, 0f) ]) ]); } private ShipPlanRuntime? BuildDockOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId); if (station is null) { order.FailureReason = "station-missing"; return null; } return CreatePlan( ship, AiPlanSourceKind.Order, order.Id, "dock-at-station", order.Label ?? $"Dock at {station.Label}", [ CreateStep("step-dock-travel", "travel", $"Travel to {station.Label}", [ CreateSubTask("sub-dock-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(balance.ArrivalThreshold, station.Radius + 12f), 0f) ]), CreateStep("step-dock", "dock", $"Dock at {station.Label}", [ CreateSubTask("sub-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f) ]) ]); } private ShipPlanRuntime? BuildTradeOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { if (order.SourceStationId is null || order.DestinationStationId is null || order.ItemId is null) { order.FailureReason = "trade-order-incomplete"; return null; } var route = ResolveTradeRoute(world, order.ItemId, order.SourceStationId, order.DestinationStationId); if (route is null) { order.FailureReason = "trade-route-missing"; return null; } return BuildTradePlan(ship, AiPlanSourceKind.Order, order.Id, route, order.Label ?? route.Summary); } private ShipPlanRuntime? BuildMineOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var systemId = order.TargetSystemId ?? ship.SystemId; var itemId = order.ItemId; if (string.IsNullOrWhiteSpace(itemId)) { order.FailureReason = "mine-order-item-missing"; return null; } var node = ResolveNode(world, order.NodeId); if (node is not null) { if (!string.Equals(node.SystemId, systemId, StringComparison.Ordinal)) { order.FailureReason = "mine-order-node-system-mismatch"; return null; } if (!string.Equals(node.ItemId, itemId, StringComparison.Ordinal)) { order.FailureReason = "mine-order-node-item-mismatch"; return null; } } else { node = SelectLocalMiningNode(world, ship, systemId, itemId); } if (node is null) { order.FailureReason = "mine-order-node-missing"; return null; } return BuildLocalMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {itemId} in {systemId}"); } private ShipPlanRuntime? BuildMineLocalOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var node = ResolveNode(world, order.NodeId); if (node is null) { order.FailureReason = "mine-order-incomplete"; return null; } return BuildLocalMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {node.ItemId}"); } private ShipPlanRuntime? BuildMineAndDeliverRunOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var node = ResolveNode(world, order.NodeId); var buyer = ResolveStation(world, order.DestinationStationId); if (node is null || buyer is null) { order.FailureReason = "mine-and-deliver-order-incomplete"; return null; } return BuildMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, buyer, order.Label ?? $"Mine {node.ItemId} for {buyer.Label}"); } private ShipPlanRuntime? BuildSellMinedCargoOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var buyer = ResolveStation(world, order.DestinationStationId ?? order.TargetEntityId); if (buyer is null || string.IsNullOrWhiteSpace(order.ItemId)) { order.FailureReason = "sell-order-incomplete"; return null; } return BuildLocalMiningDeliveryPlan(ship, AiPlanSourceKind.Order, order.Id, buyer, order.ItemId, order.Label ?? $"Sell {order.ItemId}"); } private ShipPlanRuntime? BuildAutoSalvageOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var homeStation = ResolveStation(world, order.SourceStationId ?? ship.DefaultBehavior.HomeStationId); var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.RemainingAmount > 0.01f); if (homeStation is null || wreck is null) { order.FailureReason = "salvage-order-incomplete"; return null; } var approach = GetFormationPosition(wreck.Position, ship.Id, MathF.Max(8f, order.Radius > 0f ? order.Radius : ship.DefaultBehavior.Radius * 0.25f)); return CreatePlan( ship, AiPlanSourceKind.Order, order.Id, AutoSalvage, order.Label ?? $"Salvage {wreck.ItemId}", [ CreateStep("step-salvage-collect", "salvage", $"Salvage {wreck.ItemId}", [ 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), ]), CreateStep("step-salvage-deliver", "deliver-salvage", $"Deliver salvage to {homeStation.Label}", [ 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 ShipPlanRuntime? BuildSupplyFleetOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var sourceStation = ResolveStation(world, order.SourceStationId); var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.Health > 0f); if (sourceStation is null || targetShip is null || string.IsNullOrWhiteSpace(order.ItemId)) { order.FailureReason = "supply-fleet-order-incomplete"; return null; } var amount = MathF.Min( MathF.Max(10f, ship.Definition.GetTotalCargoCapacity() * 0.5f), GetInventoryAmount(sourceStation.Inventory, order.ItemId)); if (amount <= 0.01f) { order.FailureReason = "supply-item-unavailable"; return null; } var plan = new FleetSupplyPlan( sourceStation, targetShip, order.ItemId, amount, MathF.Max(16f, order.Radius), order.Label ?? $"Supply {targetShip.Definition.Name} with {order.ItemId}"); return BuildFleetSupplyPlan(ship, AiPlanSourceKind.Order, order.Id, plan); } private ShipPlanRuntime? BuildBuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (order.ConstructionSiteId ?? order.TargetEntityId)); if (site is null) { order.FailureReason = "construction-site-missing"; return null; } var supportStation = ResolveSupportStation(world, ship, site); if (supportStation is null) { order.FailureReason = "support-station-missing"; return null; } return BuildConstructionPlan(ship, AiPlanSourceKind.Order, order.Id, site, supportStation, order.Label ?? $"Build {site.BlueprintId}"); } private ShipPlanRuntime? BuildAttackOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var targetId = order.TargetEntityId; if (targetId is null) { order.FailureReason = "attack-target-missing"; return null; } return BuildAttackPlan(ship, AiPlanSourceKind.Order, order.Id, targetId, order.TargetSystemId, order.Label ?? "Attack target"); } private ShipPlanRuntime BuildHoldOrderPlan(ShipRuntime ship, ShipOrderRuntime order) { return CreatePlan( ship, AiPlanSourceKind.Order, order.Id, ShipOrderKinds.HoldPosition, order.Label ?? "Hold position", [ CreateStep("step-hold", ShipOrderKinds.HoldPosition, order.Label ?? "Hold position", [ CreateSubTask("sub-hold", ShipTaskKinds.HoldPosition, order.Label ?? "Hold position", order.TargetSystemId ?? ship.SystemId, order.TargetPosition ?? ship.Position, order.TargetEntityId, 0f, 3f) ]) ]); } private ShipPlanRuntime? BuildDockAndWaitOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId ?? ship.DefaultBehavior.HomeStationId); if (station is null) { order.FailureReason = "station-missing"; return null; } return BuildDockAndWaitPlan(ship, AiPlanSourceKind.Order, order.Id, station, MathF.Max(1f, order.WaitSeconds), order.Label ?? $"Dock and wait at {station.Label}"); } private ShipPlanRuntime BuildFlyAndWaitOrderPlan(ShipRuntime ship, ShipOrderRuntime order) { var systemId = order.TargetSystemId ?? ship.SystemId; var targetPosition = order.TargetPosition ?? ship.Position; return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.Order, order.Id, systemId, targetPosition, MathF.Max(1f, order.WaitSeconds), order.Label ?? "Fly and wait"); } private ShipPlanRuntime? BuildFlyToObjectOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var targetEntityId = order.TargetEntityId; if (targetEntityId is null) { order.FailureReason = "target-missing"; return null; } var objectTarget = ResolveObjectTarget(world, targetEntityId); if (objectTarget is null) { order.FailureReason = "target-missing"; return null; } return BuildFlyToObjectPlan(ship, AiPlanSourceKind.Order, order.Id, objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, order.Label ?? $"Fly to {targetEntityId}"); } private ShipPlanRuntime? BuildFollowShipOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.Health > 0f); if (targetShip is null) { order.FailureReason = "target-ship-missing"; return null; } return BuildFollowShipPlan(ship, AiPlanSourceKind.Order, order.Id, targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Name}"); } private ShipPlanRuntime BuildMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, StationRuntime homeStation, string summary) { var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.MineAndDeliver, summary, [ CreateStep("step-mine", "mine", $"Mine {node.ItemId}", [ CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity()) ]), CreateStep("step-deliver", "deliver", $"Deliver {node.ItemId} to {homeStation.Label}", [ 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 ShipPlanRuntime BuildLocalMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, string summary) { var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.MineLocal, summary, [ CreateStep("step-mine", "mine", $"Mine {node.ItemId}", [ CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId) ]) ]); } private ShipPlanRuntime BuildLocalMiningDeliveryPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime buyer, string itemId, string summary) { var amount = MathF.Max(1f, GetInventoryAmount(ship.Inventory, itemId)); return CreatePlan( ship, sourceKind, sourceId, ShipOrderKinds.SellMinedCargo, summary, [ CreateStep("step-deliver", "deliver", $"Deliver {itemId} to {buyer.Label}", [ 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) ]) ]); } }