Files
space-game/apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs

786 lines
34 KiB
C#

using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
namespace SpaceGame.Api.Ships.AI;
public sealed partial class ShipAiService
{
private static ShipOrderRuntime? GetTopOrder(ShipRuntime ship) =>
ship.OrderQueue
.Where(order => order.Status is OrderStatus.Queued or OrderStatus.Active)
.OrderByDescending(GetOrderSourcePriority)
.ThenByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.FirstOrDefault();
private static int GetOrderSourcePriority(ShipOrderRuntime order) => order.SourceKind switch
{
ShipOrderSourceKind.Player => 300,
ShipOrderSourceKind.Commander => 200,
ShipOrderSourceKind.Behavior => 100,
_ => 0,
};
private void SyncBehaviorOrders(SimulationWorld world, ShipRuntime ship)
{
var desiredOrder = BuildManagedBehaviorOrder(world, ship);
ship.OrderQueue.RemoveAll(order =>
order.SourceKind == ShipOrderSourceKind.Behavior
&& (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)));
if (desiredOrder is null)
{
return;
}
var existing = ship.OrderQueue.FirstOrDefault(order => string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal));
if (existing is null)
{
ship.OrderQueue.Add(desiredOrder);
return;
}
if (ManagedOrdersEqual(existing, desiredOrder))
{
return;
}
ship.OrderQueue.Remove(existing);
ship.OrderQueue.Add(desiredOrder);
}
private ShipOrderRuntime? BuildManagedBehaviorOrder(SimulationWorld world, ShipRuntime ship)
{
var assignment = ResolveAssignment(world, ship);
var behaviorKind = assignment?.BehaviorKind ?? ship.DefaultBehavior.Kind;
var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
if (string.Equals(behaviorKind, HoldPosition, StringComparison.Ordinal))
{
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-hold-position",
Kind = ShipOrderKinds.HoldPosition,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = "Hold position",
TargetSystemId = systemId,
TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position,
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
Radius = ship.DefaultBehavior.Radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, DockAndWait, StringComparison.Ordinal))
{
var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? ship.DefaultBehavior.HomeStationId);
if (station is null)
{
ship.LastAccessFailureReason = "station-missing";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-dock-and-wait",
Kind = ShipOrderKinds.DockAndWait,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = $"Dock and wait at {station.Label}",
TargetEntityId = station.Id,
TargetSystemId = station.SystemId,
DestinationStationId = station.Id,
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
Radius = ship.DefaultBehavior.Radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, FlyAndWait, StringComparison.Ordinal))
{
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-fly-and-wait",
Kind = ShipOrderKinds.FlyAndWait,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = "Fly and wait",
TargetSystemId = systemId,
TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position,
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
Radius = ship.DefaultBehavior.Radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, FollowShip, StringComparison.Ordinal))
{
var targetShip = world.Ships.FirstOrDefault(candidate =>
candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId)
&& candidate.Health > 0f);
if (targetShip is null)
{
ship.LastAccessFailureReason = "target-ship-missing";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-follow-ship",
Kind = ShipOrderKinds.FollowShip,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = $"Follow {targetShip.Definition.Name}",
TargetEntityId = targetShip.Id,
TargetSystemId = targetShip.SystemId,
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
Radius = MathF.Max(16f, ship.DefaultBehavior.Radius),
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, FlyToObject, StringComparison.Ordinal))
{
var targetEntityId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
var target = ResolveObjectTarget(world, targetEntityId);
if (target is null)
{
ship.LastAccessFailureReason = "target-missing";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-fly-to-object",
Kind = ShipOrderKinds.FlyToObject,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = "Fly to object",
TargetEntityId = targetEntityId,
TargetSystemId = target.Value.SystemId,
TargetPosition = target.Value.Position,
WaitSeconds = MathF.Max(1f, ship.DefaultBehavior.WaitSeconds),
Radius = MathF.Max(8f, ship.DefaultBehavior.Radius),
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, Patrol, StringComparison.Ordinal))
{
return BuildManagedPatrolOrder(world, ship, assignment, behaviorKind);
}
if (string.Equals(behaviorKind, AttackTarget, StringComparison.Ordinal))
{
var targetEntityId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
if (string.IsNullOrWhiteSpace(targetEntityId))
{
return BuildManagedPatrolOrder(world, ship, assignment, behaviorKind);
}
var target = ResolveObjectTarget(world, targetEntityId);
ship.LastAccessFailureReason = target is null ? "target-missing" : null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-attack-target",
Kind = ShipOrderKinds.AttackTarget,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = "Attack target",
TargetEntityId = targetEntityId,
TargetSystemId = target?.SystemId ?? assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId,
TargetPosition = target?.Position ?? ship.Position,
WaitSeconds = 0f,
Radius = 26f,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, ConstructStation, StringComparison.Ordinal))
{
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.PreferredConstructionSiteId))
?? world.ConstructionSites
.Where(candidate => candidate.FactionId == ship.FactionId && candidate.State is ConstructionSiteStateKinds.Active or ConstructionSiteStateKinds.Planned)
.OrderBy(candidate => candidate.Id, StringComparer.Ordinal)
.FirstOrDefault();
if (site is null)
{
ship.LastAccessFailureReason = "no-construction-site";
return null;
}
if (ResolveSupportStation(world, ship, site) is null)
{
ship.LastAccessFailureReason = "support-station-missing";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-construct-station",
Kind = ShipOrderKinds.BuildAtSite,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = $"Build {site.BlueprintId}",
TargetEntityId = site.Id,
TargetSystemId = site.SystemId,
ConstructionSiteId = site.Id,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, AdvancedAutoMine, StringComparison.Ordinal)
|| string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal))
{
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
if (homeStation is null)
{
ship.LastAccessFailureReason = "no-home-station";
return null;
}
var opportunity = SelectMiningOpportunity(world, ship, homeStation, assignment, behaviorKind);
if (opportunity is null)
{
ship.LastAccessFailureReason = "no-mineable-node";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-{behaviorKind}-mine-and-deliver",
Kind = ShipOrderKinds.MineAndDeliverRun,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = opportunity.Summary,
TargetEntityId = opportunity.Node.Id,
TargetSystemId = opportunity.Node.SystemId,
DestinationStationId = opportunity.DropOffStation.Id,
ItemId = opportunity.Node.ItemId,
AnchorId = opportunity.Node.AnchorId,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, ProtectPosition, StringComparison.Ordinal))
{
var targetSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
var targetPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position;
var threat = SelectThreatTarget(world, ship, targetSystemId, targetPosition, MathF.Max(90f, ship.DefaultBehavior.Radius));
if (threat is not null)
{
ship.LastAccessFailureReason = null;
return CreateManagedAttackOrder(ship, behaviorKind, "Protect position", threat.EntityId, threat.SystemId, threat.Position);
}
ship.LastAccessFailureReason = null;
return CreateManagedFlyAndWaitOrder(
ship,
behaviorKind,
"Protect position",
targetSystemId,
targetPosition,
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
MathF.Max(6f, ship.DefaultBehavior.Radius));
}
if (string.Equals(behaviorKind, ProtectShip, StringComparison.Ordinal))
{
var guardTarget = world.Ships.FirstOrDefault(candidate =>
candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId)
&& candidate.Health > 0f);
if (guardTarget is null)
{
return BuildManagedPatrolOrder(world, ship, assignment, Patrol);
}
var threat = SelectThreatTarget(
world,
ship,
guardTarget.SystemId,
guardTarget.Position,
MathF.Max(90f, ship.DefaultBehavior.Radius),
excludeEntityId: guardTarget.Id);
if (threat is not null)
{
ship.LastAccessFailureReason = null;
return CreateManagedAttackOrder(ship, behaviorKind, $"Protect {guardTarget.Definition.Name}", threat.EntityId, threat.SystemId, threat.Position);
}
ship.LastAccessFailureReason = null;
return CreateManagedFollowShipOrder(
ship,
behaviorKind,
$"Escort {guardTarget.Definition.Name}",
guardTarget,
MathF.Max(18f, ship.DefaultBehavior.Radius * 0.5f),
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds));
}
if (string.Equals(behaviorKind, ProtectStation, StringComparison.Ordinal))
{
var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
if (station is null)
{
return BuildManagedPatrolOrder(world, ship, assignment, Patrol);
}
var threat = SelectThreatTarget(world, ship, station.SystemId, station.Position, MathF.Max(station.Radius + 80f, ship.DefaultBehavior.Radius));
if (threat is not null)
{
ship.LastAccessFailureReason = null;
return CreateManagedAttackOrder(ship, behaviorKind, $"Protect {station.Label}", threat.EntityId, threat.SystemId, threat.Position);
}
ship.LastAccessFailureReason = null;
return CreateManagedFlyAndWaitOrder(
ship,
behaviorKind,
$"Guard {station.Label}",
station.SystemId,
GetFormationPosition(station.Position, ship.Id, MathF.Max(station.Radius + 18f, ship.DefaultBehavior.Radius)),
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
MathF.Max(6f, ship.DefaultBehavior.Radius));
}
if (string.Equals(behaviorKind, Police, StringComparison.Ordinal))
{
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
var policeSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? homeStation?.SystemId ?? ship.SystemId;
var areaPosition = homeStation?.Position ?? ship.DefaultBehavior.TargetPosition ?? ship.Position;
var contact = SelectPoliceContact(world, ship, policeSystemId, areaPosition, MathF.Max(80f, ship.DefaultBehavior.Radius));
if (contact is null)
{
return BuildManagedPatrolOrder(world, ship, assignment, Patrol);
}
ship.LastAccessFailureReason = null;
return contact.Engage
? CreateManagedAttackOrder(ship, behaviorKind, "Police engage", contact.EntityId, contact.SystemId, contact.Position)
: CreateManagedFollowTargetOrder(ship, behaviorKind, "Police inspect", contact.EntityId, contact.SystemId, contact.Position, MathF.Max(14f, ship.DefaultBehavior.Radius * 0.5f), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds));
}
if (string.Equals(behaviorKind, LocalAutoTrade, StringComparison.Ordinal)
|| string.Equals(behaviorKind, AdvancedAutoTrade, StringComparison.Ordinal)
|| string.Equals(behaviorKind, FillShortages, StringComparison.Ordinal)
|| string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal)
|| string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal))
{
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
var route = SelectTradeRoute(world, ship, homeStation, behaviorKind, ship.DefaultBehavior.KnownStationsOnly);
if (route is not null)
{
ship.LastAccessFailureReason = null;
return CreateManagedTradeRouteOrder(ship, behaviorKind, route);
}
if (string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal)
&& SelectKnownStationVisit(world, ship, homeStation) is { } visitStation)
{
ship.LastAccessFailureReason = null;
return CreateManagedDockAndWaitOrder(ship, behaviorKind, visitStation, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Revisit {visitStation.Label}");
}
ship.LastAccessFailureReason = "no-trade-route";
return null;
}
if (string.Equals(behaviorKind, SupplyFleet, StringComparison.Ordinal))
{
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
var plan = SelectFleetSupplyPlan(world, ship, homeStation);
if (plan is null)
{
ship.LastAccessFailureReason = "no-fleet-to-supply";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-supply-fleet",
Kind = ShipOrderKinds.SupplyFleetRun,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = plan.Summary,
TargetEntityId = plan.TargetShip.Id,
TargetSystemId = plan.TargetShip.SystemId,
SourceStationId = plan.SourceStation.Id,
ItemId = plan.ItemId,
Radius = plan.Radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, AutoSalvage, StringComparison.Ordinal))
{
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
if (homeStation is null)
{
ship.LastAccessFailureReason = "no-home-station";
return null;
}
var salvage = SelectSalvageOpportunity(world, ship, homeStation);
if (salvage is null)
{
ship.LastAccessFailureReason = "no-salvage-target";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-auto-salvage",
Kind = ShipOrderKinds.SalvageRun,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = salvage.Summary,
TargetEntityId = salvage.Wreck.Id,
TargetSystemId = salvage.Wreck.SystemId,
TargetPosition = salvage.Wreck.Position,
SourceStationId = homeStation.Id,
ItemId = salvage.Wreck.ItemId,
Radius = MathF.Max(8f, ship.DefaultBehavior.Radius * 0.25f),
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
if (string.Equals(behaviorKind, RepeatOrders, StringComparison.Ordinal))
{
if (ship.DefaultBehavior.RepeatOrders.Count == 0)
{
ship.LastAccessFailureReason = "no-repeat-orders";
return null;
}
var template = ship.DefaultBehavior.RepeatOrders[ship.DefaultBehavior.RepeatIndex % ship.DefaultBehavior.RepeatOrders.Count];
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-repeat-{ship.DefaultBehavior.RepeatIndex}",
Kind = template.Kind,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = template.Label,
TargetEntityId = template.TargetEntityId,
TargetSystemId = template.TargetSystemId,
TargetPosition = template.TargetPosition,
SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId,
AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId,
WaitSeconds = template.WaitSeconds,
Radius = template.Radius,
MaxSystemRange = template.MaxSystemRange ?? ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = template.KnownStationsOnly,
};
}
if (!string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal))
{
return null;
}
var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId;
if (string.IsNullOrWhiteSpace(itemId))
{
ship.LastAccessFailureReason = "missing-item";
return null;
}
if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f)
{
var buyer = SelectLocalAutoMineBuyer(world, ship, systemId, itemId);
if (buyer is null)
{
ship.LastAccessFailureReason = "no-suitable-buyer";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-local-auto-mine-sell",
Kind = ShipOrderKinds.SellMinedCargo,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = $"Sell {itemId} in {systemId}",
TargetEntityId = buyer.Id,
TargetSystemId = buyer.SystemId,
DestinationStationId = buyer.Id,
ItemId = itemId,
WaitSeconds = 0f,
Radius = 0f,
MaxSystemRange = 0,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
var node = SelectLocalMiningNode(world, ship, systemId, itemId, ship.DefaultBehavior.PreferredAnchorId);
if (node is null)
{
ship.LastAccessFailureReason = "no-mineable-node";
return null;
}
ship.LastAccessFailureReason = null;
return new ShipOrderRuntime
{
Id = $"behavior-{ship.Id}-local-auto-mine-mine",
Kind = ShipOrderKinds.MineLocal,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = $"Mine {itemId} in {systemId}",
TargetEntityId = node.Id,
TargetSystemId = node.SystemId,
AnchorId = node.AnchorId,
ItemId = node.ItemId,
WaitSeconds = 0f,
Radius = 0f,
MaxSystemRange = 0,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}
private static bool ManagedOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) =>
string.Equals(left.Id, right.Id, StringComparison.Ordinal)
&& string.Equals(left.Kind, right.Kind, StringComparison.Ordinal)
&& left.SourceKind == right.SourceKind
&& string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal)
&& left.Priority == right.Priority
&& left.InterruptCurrentPlan == right.InterruptCurrentPlan
&& string.Equals(left.Label, right.Label, StringComparison.Ordinal)
&& string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal)
&& string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal)
&& left.TargetPosition == right.TargetPosition
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.AnchorId, right.AnchorId, StringComparison.Ordinal)
&& left.WaitSeconds.Equals(right.WaitSeconds)
&& left.Radius.Equals(right.Radius)
&& left.MaxSystemRange == right.MaxSystemRange
&& left.KnownStationsOnly == right.KnownStationsOnly;
private ShipOrderRuntime BuildManagedPatrolOrder(SimulationWorld world, ShipRuntime ship, CommanderAssignmentRuntime? assignment, string sourceKind)
{
var patrolSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
var protectPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position;
var patrolThreat = SelectThreatTarget(world, ship, patrolSystemId, protectPosition, MathF.Max(60f, ship.DefaultBehavior.Radius));
if (patrolThreat is not null)
{
ship.LastAccessFailureReason = null;
return CreateManagedAttackOrder(ship, sourceKind, "Patrol intercept", patrolThreat.EntityId, patrolThreat.SystemId, patrolThreat.Position, orderIdSuffix: "patrol-attack");
}
Vector3 targetPosition;
string targetSystemId;
if (ship.DefaultBehavior.PatrolPoints.Count > 0)
{
var index = ship.DefaultBehavior.PatrolIndex % ship.DefaultBehavior.PatrolPoints.Count;
targetPosition = ship.DefaultBehavior.PatrolPoints[index];
ship.DefaultBehavior.PatrolIndex = (index + 1) % ship.DefaultBehavior.PatrolPoints.Count;
targetSystemId = ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
}
else if (ResolveStation(world, ship.DefaultBehavior.HomeStationId ?? assignment?.HomeStationId) is { } homeStation)
{
var patrolRadius = homeStation.Radius + 90f;
targetPosition = new Vector3(homeStation.Position.X + patrolRadius, homeStation.Position.Y, homeStation.Position.Z);
targetSystemId = homeStation.SystemId;
}
else
{
targetPosition = ship.Position;
targetSystemId = ship.SystemId;
}
ship.LastAccessFailureReason = null;
return CreateManagedFlyAndWaitOrder(ship, sourceKind, "Patrol waypoint", targetSystemId, targetPosition, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), MathF.Max(6f, ship.DefaultBehavior.Radius), orderIdSuffix: "patrol-fly-and-wait");
}
private static ShipOrderRuntime CreateManagedAttackOrder(
ShipRuntime ship,
string behaviorKind,
string label,
string targetEntityId,
string targetSystemId,
Vector3 targetPosition,
string? orderIdSuffix = null) =>
new()
{
Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}",
Kind = ShipOrderKinds.AttackTarget,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = label,
TargetEntityId = targetEntityId,
TargetSystemId = targetSystemId,
TargetPosition = targetPosition,
WaitSeconds = 0f,
Radius = 26f,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
private static ShipOrderRuntime CreateManagedTradeRouteOrder(ShipRuntime ship, string behaviorKind, TradeRoutePlan route) =>
new()
{
Id = $"behavior-{ship.Id}-{behaviorKind}-trade-route",
Kind = ShipOrderKinds.TradeRoute,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = route.Summary,
SourceStationId = route.SourceStation.Id,
DestinationStationId = route.DestinationStation.Id,
ItemId = route.ItemId,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
private static ShipOrderRuntime CreateManagedDockAndWaitOrder(ShipRuntime ship, string behaviorKind, StationRuntime station, float waitSeconds, string label) =>
new()
{
Id = $"behavior-{ship.Id}-{behaviorKind}-dock-and-wait",
Kind = ShipOrderKinds.DockAndWait,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = label,
TargetEntityId = station.Id,
TargetSystemId = station.SystemId,
DestinationStationId = station.Id,
WaitSeconds = waitSeconds,
Radius = ship.DefaultBehavior.Radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
private static ShipOrderRuntime CreateManagedFlyAndWaitOrder(
ShipRuntime ship,
string behaviorKind,
string label,
string targetSystemId,
Vector3 targetPosition,
float waitSeconds,
float radius,
string? orderIdSuffix = null) =>
new()
{
Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}",
Kind = ShipOrderKinds.FlyAndWait,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = label,
TargetSystemId = targetSystemId,
TargetPosition = targetPosition,
WaitSeconds = waitSeconds,
Radius = radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
private static ShipOrderRuntime CreateManagedFollowShipOrder(
ShipRuntime ship,
string behaviorKind,
string label,
ShipRuntime targetShip,
float radius,
float waitSeconds) =>
new()
{
Id = $"behavior-{ship.Id}-{behaviorKind}",
Kind = ShipOrderKinds.FollowShip,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = label,
TargetEntityId = targetShip.Id,
TargetSystemId = targetShip.SystemId,
WaitSeconds = waitSeconds,
Radius = radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
private static ShipOrderRuntime CreateManagedFollowTargetOrder(
ShipRuntime ship,
string behaviorKind,
string label,
string targetEntityId,
string targetSystemId,
Vector3 targetPosition,
float radius,
float waitSeconds) =>
new()
{
Id = $"behavior-{ship.Id}-{behaviorKind}",
Kind = ShipOrderKinds.FollowShip,
SourceKind = ShipOrderSourceKind.Behavior,
SourceId = behaviorKind,
Priority = 0,
InterruptCurrentPlan = false,
Label = label,
TargetEntityId = targetEntityId,
TargetSystemId = targetSystemId,
TargetPosition = targetPosition,
WaitSeconds = waitSeconds,
Radius = radius,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
};
}