using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; 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 ShipOrderRuntime? BuildEmergencyOrder(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(); return new ShipOrderRuntime { Id = $"rule-{ship.Id}-flee", Kind = ShipOrderKinds.Flee, SourceKind = ShipOrderSourceKind.Behavior, SourceId = ShipOrderKinds.Flee, Priority = 1000, InterruptCurrentPlan = true, Label = "Emergency retreat", TargetEntityId = safeStation?.Id, TargetSystemId = safeStation?.SystemId ?? ship.SystemId, TargetPosition = safeStation?.Position ?? ship.Position, DestinationStationId = safeStation?.Id, Radius = safeStation is null ? 0f : MathF.Max(balance.ArrivalThreshold, safeStation.Radius + 12f), }; } private IReadOnlyList? BuildOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { return order.Kind switch { var kind when string.Equals(kind, ShipOrderKinds.Flee, StringComparison.Ordinal) => BuildFleeSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.Move, StringComparison.Ordinal) => BuildMoveSubTasks(ship, order), var kind when string.Equals(kind, ShipOrderKinds.DockAtStation, StringComparison.Ordinal) => BuildDockOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.FlyToObject, StringComparison.Ordinal) => BuildFlyToObjectOrderSubTasks(world, order), var kind when string.Equals(kind, ShipOrderKinds.FollowShip, StringComparison.Ordinal) => BuildFollowShipOrderSubTasks(world, order), var kind when string.Equals(kind, ShipOrderKinds.TradeRoute, StringComparison.Ordinal) => BuildTradeOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliver, StringComparison.Ordinal) => BuildMineOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.MineLocal, StringComparison.Ordinal) => BuildMineLocalOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliverRun, StringComparison.Ordinal) => BuildMineAndDeliverRunOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.SellMinedCargo, StringComparison.Ordinal) => BuildSellMinedCargoOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.SupplyFleetRun, StringComparison.Ordinal) => BuildSupplyFleetOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.SalvageRun, StringComparison.Ordinal) => BuildAutoSalvageOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.BuildAtSite, StringComparison.Ordinal) => BuildBuildOrderSubTasks(world, ship, order), var kind when string.Equals(kind, ShipOrderKinds.AttackTarget, StringComparison.Ordinal) => BuildAttackOrderSubTasks(order), var kind when string.Equals(kind, ShipOrderKinds.HoldPosition, StringComparison.Ordinal) => BuildHoldSubTasks(ship, order), _ => null, }; } private IReadOnlyList BuildFleeSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var safeStation = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId); if (safeStation is null) { return [ CreateSubTask("sub-flee-hold", ShipTaskKinds.HoldPosition, "Hold safe position", ship.SystemId, ship.Position, null, 0f, 3f), ]; } return [ CreateSubTask("sub-flee-travel", ShipTaskKinds.Travel, $"Travel to {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, MathF.Max(balance.ArrivalThreshold, safeStation.Radius + 12f), 0f), CreateSubTask("sub-flee-dock", ShipTaskKinds.Dock, $"Dock at {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, 4f, 0f), ]; } private static IReadOnlyList BuildMoveSubTasks(ShipRuntime ship, ShipOrderRuntime order) { var targetSystemId = order.TargetSystemId ?? ship.SystemId; var targetPosition = order.TargetPosition ?? ship.Position; return [ CreateSubTask("sub-move-travel", ShipTaskKinds.Travel, order.Label ?? "Travel", targetSystemId, targetPosition, order.TargetEntityId, MathF.Max(0f, order.Radius), 0f), ]; } private IReadOnlyList? BuildDockOrderSubTasks(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 [ CreateSubTask("sub-dock-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(balance.ArrivalThreshold, station.Radius + 12f), 0f), CreateSubTask("sub-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f), ]; } private IReadOnlyList? BuildTradeOrderSubTasks(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 BuildTradeSubTasks(ship, route); } private IReadOnlyList? BuildMineOrderSubTasks(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 anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId); var node = ResolveNode(world, order.TargetEntityId); 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, anchor?.Id); } if (node is null) { order.FailureReason = "mine-order-node-missing"; return null; } return BuildLocalMiningSubTasks(ship, node); } private IReadOnlyList? BuildMineLocalOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId); var node = ResolveNode(world, order.TargetEntityId) ?? SelectLocalMiningNode(world, ship, order.TargetSystemId ?? ship.SystemId, order.ItemId ?? ship.DefaultBehavior.ItemId ?? string.Empty, anchor?.Id); if (node is null) { order.FailureReason = "mine-order-incomplete"; return null; } return BuildLocalMiningSubTasks(ship, node); } private IReadOnlyList? BuildMineAndDeliverRunOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) { var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId); var node = ResolveNode(world, order.TargetEntityId) ?? (string.IsNullOrWhiteSpace(order.ItemId) ? null : SelectLocalMiningNode(world, ship, order.TargetSystemId ?? ship.SystemId, order.ItemId, anchor?.Id)); var buyer = ResolveStation(world, order.DestinationStationId); if (node is null || buyer is null) { order.FailureReason = "mine-and-deliver-order-incomplete"; return null; } return BuildMiningSubTasks(ship, node, buyer); } private IReadOnlyList? BuildSellMinedCargoOrderSubTasks(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 BuildLocalMiningDeliverySubTasks(ship, buyer, order.ItemId); } private IReadOnlyList? BuildAutoSalvageOrderSubTasks(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 BuildSalvageSubTasks(ship, wreck, homeStation, approach); } private IReadOnlyList? BuildSupplyFleetOrderSubTasks(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 BuildFleetSupplySubTasks(plan); } private IReadOnlyList? BuildBuildOrderSubTasks(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 BuildConstructionSubTasks(site, supportStation); } private IReadOnlyList? BuildAttackOrderSubTasks(ShipOrderRuntime order) { var targetId = order.TargetEntityId; if (targetId is null) { order.FailureReason = "attack-target-missing"; return null; } return BuildAttackSubTasks(targetId, order.TargetSystemId, order.Label ?? "Attack target"); } private IReadOnlyList? BuildFlyToObjectOrderSubTasks(SimulationWorld world, 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 BuildFlyToObjectSubTasks(objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, order.Label ?? $"Fly to {targetEntityId}"); } private IReadOnlyList? BuildFollowShipOrderSubTasks(SimulationWorld world, 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 BuildFollowShipSubTasks(targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Name}"); } }