329 lines
14 KiB
C#
329 lines
14 KiB
C#
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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime> 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<ShipSubTaskRuntime> 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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<ShipSubTaskRuntime>? 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}");
|
|
}
|
|
}
|