Refine ship orders and viewer controls
This commit is contained in:
@@ -6,27 +6,12 @@ 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 =>
|
||||
ship.OrderQueue.RemoveWhere(order =>
|
||||
order.SourceKind == ShipOrderSourceKind.Behavior
|
||||
&& order.Id.StartsWith("behavior-", StringComparison.Ordinal)
|
||||
&& (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)));
|
||||
|
||||
if (desiredOrder is null)
|
||||
@@ -34,10 +19,10 @@ public sealed partial class ShipAiService
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = ship.OrderQueue.FirstOrDefault(order => string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal));
|
||||
var existing = ship.OrderQueue.FindById(desiredOrder.Id);
|
||||
if (existing is null)
|
||||
{
|
||||
ship.OrderQueue.Add(desiredOrder);
|
||||
ship.OrderQueue.AddOrReplaceManagedOrder(desiredOrder);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,8 +31,7 @@ public sealed partial class ShipAiService
|
||||
return;
|
||||
}
|
||||
|
||||
ship.OrderQueue.Remove(existing);
|
||||
ship.OrderQueue.Add(desiredOrder);
|
||||
ship.OrderQueue.AddOrReplaceManagedOrder(desiredOrder);
|
||||
}
|
||||
|
||||
private ShipOrderRuntime? BuildManagedBehaviorOrder(SimulationWorld world, ShipRuntime ship)
|
||||
@@ -76,7 +60,7 @@ public sealed partial class ShipAiService
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, DockAndWait, StringComparison.Ordinal))
|
||||
if (string.Equals(behaviorKind, DockAtStation, StringComparison.Ordinal))
|
||||
{
|
||||
var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? ship.DefaultBehavior.HomeStationId);
|
||||
if (station is null)
|
||||
@@ -88,38 +72,36 @@ public sealed partial class ShipAiService
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-dock-and-wait",
|
||||
Kind = ShipOrderKinds.DockAndWait,
|
||||
Id = $"behavior-{ship.Id}-dock-at-station",
|
||||
Kind = ShipOrderKinds.DockAtStation,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Dock and wait at {station.Label}",
|
||||
Label = $"Dock 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))
|
||||
if (string.Equals(behaviorKind, Move, StringComparison.Ordinal))
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-fly-and-wait",
|
||||
Kind = ShipOrderKinds.FlyAndWait,
|
||||
Id = $"behavior-{ship.Id}-move",
|
||||
Kind = ShipOrderKinds.Move,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = "Fly and wait",
|
||||
Label = "Fly to 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,
|
||||
@@ -306,13 +288,12 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedFlyAndWaitOrder(
|
||||
return CreateManagedMoveOrder(
|
||||
ship,
|
||||
behaviorKind,
|
||||
"Protect position",
|
||||
targetSystemId,
|
||||
targetPosition,
|
||||
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
MathF.Max(6f, ship.DefaultBehavior.Radius));
|
||||
}
|
||||
|
||||
@@ -365,13 +346,12 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedFlyAndWaitOrder(
|
||||
return CreateManagedMoveOrder(
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -410,7 +390,7 @@ public sealed partial class ShipAiService
|
||||
&& SelectKnownStationVisit(world, ship, homeStation) is { } visitStation)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedDockAndWaitOrder(ship, behaviorKind, visitStation, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Revisit {visitStation.Label}");
|
||||
return CreateManagedDockAtStationOrder(ship, behaviorKind, visitStation, $"Revisit {visitStation.Label}");
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = "no-trade-route";
|
||||
@@ -641,7 +621,7 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
|
||||
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");
|
||||
return CreateManagedMoveOrder(ship, sourceKind, "Patrol waypoint", targetSystemId, targetPosition, MathF.Max(6f, ship.DefaultBehavior.Radius), orderIdSuffix: "patrol-move");
|
||||
}
|
||||
|
||||
private static ShipOrderRuntime CreateManagedAttackOrder(
|
||||
@@ -687,11 +667,11 @@ public sealed partial class ShipAiService
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
|
||||
private static ShipOrderRuntime CreateManagedDockAndWaitOrder(ShipRuntime ship, string behaviorKind, StationRuntime station, float waitSeconds, string label) =>
|
||||
private static ShipOrderRuntime CreateManagedDockAtStationOrder(ShipRuntime ship, string behaviorKind, StationRuntime station, string label) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}-dock-and-wait",
|
||||
Kind = ShipOrderKinds.DockAndWait,
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}-dock-at-station",
|
||||
Kind = ShipOrderKinds.DockAtStation,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
@@ -700,25 +680,23 @@ public sealed partial class ShipAiService
|
||||
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(
|
||||
private static ShipOrderRuntime CreateManagedMoveOrder(
|
||||
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,
|
||||
Kind = ShipOrderKinds.Move,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
@@ -726,7 +704,6 @@ public sealed partial class ShipAiService
|
||||
Label = label,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = targetPosition,
|
||||
WaitSeconds = waitSeconds,
|
||||
Radius = radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace SpaceGame.Api.Ships.AI;
|
||||
|
||||
public sealed partial class ShipAiService
|
||||
{
|
||||
private SubTaskOutcome UpdateSubTask(SimulationWorld world, ShipRuntime ship, ShipPlanStepRuntime step, ShipSubTaskRuntime subTask, float deltaSeconds)
|
||||
private SubTaskOutcome UpdateSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
|
||||
{
|
||||
return subTask.Kind switch
|
||||
{
|
||||
@@ -636,12 +636,13 @@ public sealed partial class ShipAiService
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.DestinationAnchorId = targetAnchor?.Id ?? currentAnchor?.Id;
|
||||
subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f);
|
||||
var localSystemOffset = SimulationUnits.MetersToKilometers(ship.Position);
|
||||
ship.SpatialState.SystemPosition = currentAnchor is null
|
||||
? ship.Position
|
||||
? localSystemOffset
|
||||
: new Vector3(
|
||||
currentAnchor.Position.X + ship.Position.X,
|
||||
currentAnchor.Position.Y + ship.Position.Y,
|
||||
currentAnchor.Position.Z + ship.Position.Z);
|
||||
currentAnchor.Position.X + localSystemOffset.X,
|
||||
currentAnchor.Position.Y + localSystemOffset.Y,
|
||||
currentAnchor.Position.Z + localSystemOffset.Z);
|
||||
|
||||
if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold))
|
||||
{
|
||||
@@ -650,12 +651,13 @@ public sealed partial class ShipAiService
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.CurrentSystemId = targetSystemId;
|
||||
ship.SpatialState.CurrentAnchorId = targetAnchor?.Id ?? currentAnchor?.Id;
|
||||
var arrivalSystemOffset = SimulationUnits.MetersToKilometers(targetPosition);
|
||||
ship.SpatialState.SystemPosition = targetAnchor is null
|
||||
? targetPosition
|
||||
? arrivalSystemOffset
|
||||
: new Vector3(
|
||||
targetAnchor.Position.X + targetPosition.X,
|
||||
targetAnchor.Position.Y + targetPosition.Y,
|
||||
targetAnchor.Position.Z + targetPosition.Z);
|
||||
targetAnchor.Position.X + arrivalSystemOffset.X,
|
||||
targetAnchor.Position.Y + arrivalSystemOffset.Y,
|
||||
targetAnchor.Position.Z + arrivalSystemOffset.Z);
|
||||
ship.State = ShipState.Arriving;
|
||||
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
|
||||
}
|
||||
@@ -663,12 +665,13 @@ public sealed partial class ShipAiService
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.SpatialState.CurrentAnchorId = currentAnchor?.Id;
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
var movedSystemOffset = SimulationUnits.MetersToKilometers(ship.Position);
|
||||
ship.SpatialState.SystemPosition = currentAnchor is null
|
||||
? ship.Position
|
||||
? movedSystemOffset
|
||||
: new Vector3(
|
||||
currentAnchor.Position.X + ship.Position.X,
|
||||
currentAnchor.Position.Y + ship.Position.Y,
|
||||
currentAnchor.Position.Z + ship.Position.Z);
|
||||
currentAnchor.Position.X + movedSystemOffset.X,
|
||||
currentAnchor.Position.Y + movedSystemOffset.Y,
|
||||
currentAnchor.Position.Z + movedSystemOffset.Z);
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
@@ -822,12 +825,13 @@ public sealed partial class ShipAiService
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
||||
ship.SpatialState.CurrentAnchorId = targetAnchor?.Id;
|
||||
ship.SpatialState.DestinationAnchorId = targetAnchor?.Id;
|
||||
var arrivalSystemOffset = SimulationUnits.MetersToKilometers(targetPosition);
|
||||
ship.SpatialState.SystemPosition = targetAnchor is null
|
||||
? targetPosition
|
||||
? arrivalSystemOffset
|
||||
: new Vector3(
|
||||
targetAnchor.Position.X + targetPosition.X,
|
||||
targetAnchor.Position.Y + targetPosition.Y,
|
||||
targetAnchor.Position.Z + targetPosition.Z);
|
||||
targetAnchor.Position.X + arrivalSystemOffset.X,
|
||||
targetAnchor.Position.Y + arrivalSystemOffset.Y,
|
||||
targetAnchor.Position.Z + arrivalSystemOffset.Z);
|
||||
ship.State = ShipState.Arriving;
|
||||
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
@@ -185,13 +185,14 @@ public sealed partial class ShipAiService
|
||||
{
|
||||
if (station.AnchorId is not null && ResolveAnchor(world, station.AnchorId) is { } anchor)
|
||||
{
|
||||
var localOffset = SimulationUnits.MetersToKilometers(station.Position);
|
||||
return new Vector3(
|
||||
anchor.Position.X + station.Position.X,
|
||||
anchor.Position.Y + station.Position.Y,
|
||||
anchor.Position.Z + station.Position.Z);
|
||||
anchor.Position.X + localOffset.X,
|
||||
anchor.Position.Y + localOffset.Y,
|
||||
anchor.Position.Z + localOffset.Z);
|
||||
}
|
||||
|
||||
return station.Position;
|
||||
return SimulationUnits.MetersToKilometers(station.Position);
|
||||
}
|
||||
|
||||
private static Vector3 ResolveNodeSystemPosition(SimulationWorld world, ResourceNodeRuntime node)
|
||||
@@ -216,17 +217,18 @@ public sealed partial class ShipAiService
|
||||
|
||||
if (ResolveCurrentAnchor(world, ship) is { } anchor)
|
||||
{
|
||||
var localOffset = SimulationUnits.MetersToKilometers(ship.Position);
|
||||
return new Vector3(
|
||||
anchor.Position.X + ship.Position.X,
|
||||
anchor.Position.Y + ship.Position.Y,
|
||||
anchor.Position.Z + ship.Position.Z);
|
||||
anchor.Position.X + localOffset.X,
|
||||
anchor.Position.Y + localOffset.Y,
|
||||
anchor.Position.Z + localOffset.Z);
|
||||
}
|
||||
|
||||
return ship.Position;
|
||||
return SimulationUnits.MetersToKilometers(ship.Position);
|
||||
}
|
||||
|
||||
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed) * GetSkillFactor(ship.Skills.Navigation);
|
||||
ship.Definition.Speed * GetSkillFactor(ship.Skills.Navigation);
|
||||
|
||||
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed) * GetSkillFactor(ship.Skills.Navigation);
|
||||
@@ -997,9 +999,6 @@ public sealed partial class ShipAiService
|
||||
? null
|
||||
: world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId)?.Assignment;
|
||||
|
||||
private static ShipPlanStepRuntime? GetCurrentStep(ShipPlanRuntime? plan) =>
|
||||
plan is null || plan.CurrentStepIndex >= plan.Steps.Count ? null : plan.Steps[plan.CurrentStepIndex];
|
||||
|
||||
private static StationRuntime? ResolveSupportStation(SimulationWorld world, ShipRuntime ship, ConstructionSiteRuntime site)
|
||||
{
|
||||
return ResolveStation(world, ResolveAssignment(world, ship)?.HomeStationId ?? ship.DefaultBehavior.HomeStationId)
|
||||
@@ -1032,41 +1031,40 @@ public sealed partial class ShipAiService
|
||||
|
||||
private static void TrackHistory(ShipRuntime ship)
|
||||
{
|
||||
var plan = ship.ActivePlan;
|
||||
var step = GetCurrentStep(plan);
|
||||
var subTask = step is null || step.CurrentSubTaskIndex >= step.SubTasks.Count ? null : step.SubTasks[step.CurrentSubTaskIndex];
|
||||
var signature = $"{ship.State.ToContractValue()}|{plan?.Kind ?? "none"}|{step?.Kind ?? "none"}|{subTask?.Kind ?? "none"}|{GetShipCargoAmount(ship):0.0}";
|
||||
var orderId = ship.ActiveOrderId ?? "none";
|
||||
var subTask = ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count ? null : ship.ActiveSubTasks[ship.ActiveSubTaskIndex];
|
||||
var signature = $"{ship.State.ToContractValue()}|{orderId}|{subTask?.Kind ?? "none"}|{GetShipCargoAmount(ship):0.0}";
|
||||
if (ship.LastSignature == signature)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ship.LastSignature = signature;
|
||||
ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} plan={plan?.Kind ?? "none"} step={step?.Kind ?? "none"} subTask={subTask?.Kind ?? "none"} cargo={GetShipCargoAmount(ship):0.#}");
|
||||
ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} order={orderId} task={subTask?.Kind ?? "none"} cargo={GetShipCargoAmount(ship):0.#}");
|
||||
if (ship.History.Count > 24)
|
||||
{
|
||||
ship.History.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitStateEvents(ShipRuntime ship, ShipState previousState, string? previousPlanId, string? previousStepId, ICollection<SimulationEventRecord> events)
|
||||
private static void EmitStateEvents(ShipRuntime ship, ShipState previousState, string? previousOrderId, string? previousTaskId, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var currentPlanId = ship.ActivePlan?.Id;
|
||||
var currentStepId = GetCurrentStep(ship.ActivePlan)?.Id;
|
||||
var currentOrderId = ship.ActiveOrderId;
|
||||
var currentTaskId = ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count ? null : ship.ActiveSubTasks[ship.ActiveSubTaskIndex].Id;
|
||||
var occurredAtUtc = DateTimeOffset.UtcNow;
|
||||
if (previousState != ship.State)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Name} state {previousState.ToContractValue()} -> {ship.State.ToContractValue()}.", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (!string.Equals(previousPlanId, currentPlanId, StringComparison.Ordinal))
|
||||
if (!string.Equals(previousOrderId, currentOrderId, StringComparison.Ordinal))
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "plan-changed", $"{ship.Definition.Name} switched active plan.", occurredAtUtc));
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "order-changed", $"{ship.Definition.Name} switched active order.", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (!string.Equals(previousStepId, currentStepId, StringComparison.Ordinal))
|
||||
if (!string.Equals(previousTaskId, currentTaskId, StringComparison.Ordinal))
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "step-changed", $"{ship.Definition.Name} advanced plan step.", occurredAtUtc));
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Name} advanced active task.", occurredAtUtc));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,323 +1,179 @@
|
||||
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 ShipPlanRuntime BuildBehaviorFallbackPlan(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var (behaviorKind, sourceId) = ResolveBehaviorSource(world, ship);
|
||||
var failureReason = ship.LastAccessFailureReason;
|
||||
if (string.Equals(behaviorKind, Idle, StringComparison.Ordinal))
|
||||
{
|
||||
return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "Idle");
|
||||
}
|
||||
|
||||
if (IsBehaviorBlockingFailure(behaviorKind, failureReason))
|
||||
{
|
||||
return CreateBlockedPlan(
|
||||
ship,
|
||||
AiPlanSourceKind.DefaultBehavior,
|
||||
sourceId,
|
||||
DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason),
|
||||
failureReason!);
|
||||
}
|
||||
|
||||
return CreateIdlePlan(
|
||||
ship,
|
||||
AiPlanSourceKind.DefaultBehavior,
|
||||
sourceId,
|
||||
DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason));
|
||||
}
|
||||
|
||||
private static bool IsBehaviorBlockingFailure(string behaviorKind, string? failureReason) => failureReason switch
|
||||
{
|
||||
"missing-item" => true,
|
||||
"no-suitable-buyer" => true,
|
||||
"no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => true,
|
||||
"no-mineable-node" when string.Equals(behaviorKind, ShipBehaviorKinds.LocalAutoMine, StringComparison.Ordinal) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static string DescribeBehaviorFallbackSummary(SimulationWorld world, ShipRuntime ship, string behaviorKind, string? failureReason)
|
||||
private static (string BehaviorKind, string SourceId) ResolveBehaviorSource(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var assignment = ResolveAssignment(world, ship);
|
||||
var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
|
||||
var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId ?? "resource";
|
||||
|
||||
return failureReason switch
|
||||
{
|
||||
"missing-item" => "No mining ware configured",
|
||||
"no-suitable-buyer" => $"No buyer for {itemId} in {systemId}",
|
||||
"no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => $"No {itemId} to mine in {systemId}",
|
||||
"no-mineable-node" => "No mineable node",
|
||||
"no-home-station" => "No home station",
|
||||
"no-trade-route" => "No trade route",
|
||||
"no-fleet-to-supply" => "No fleet to supply",
|
||||
"station-missing" => "No station to dock",
|
||||
"target-ship-missing" => "No ship to follow",
|
||||
"target-missing" => "No object target",
|
||||
"no-salvage-target" => "No salvage target",
|
||||
"no-repeat-orders" => "No repeat orders",
|
||||
"no-construction-site" => "No construction site",
|
||||
"support-station-missing" => "No support station",
|
||||
_ => "Idle",
|
||||
};
|
||||
return assignment is null
|
||||
? (ship.DefaultBehavior.Kind, ship.DefaultBehavior.Kind)
|
||||
: (assignment.BehaviorKind, assignment.ObjectiveId);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime BuildTradePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, TradeRoutePlan route, string summary)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildTradeSubTasks(ShipRuntime ship, TradeRoutePlan route)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
ShipOrderKinds.TradeRoute,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-acquire", "acquire-cargo", $"Load {route.ItemId} at {route.SourceStation.Label}",
|
||||
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)
|
||||
]),
|
||||
CreateStep("step-deliver", "deliver-cargo", $"Deliver {route.ItemId} to {route.DestinationStation.Label}",
|
||||
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<ShipSubTaskRuntime> BuildFleetSupplySubTasks(FleetSupplyPlan plan)
|
||||
{
|
||||
return
|
||||
[
|
||||
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)
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime BuildFleetSupplyPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, FleetSupplyPlan plan)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildConstructionSubTasks(ConstructionSiteRuntime site, StationRuntime supportStation)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
SupplyFleet,
|
||||
plan.Summary,
|
||||
[
|
||||
CreateStep("step-fleet-acquire", "acquire-cargo", $"Load {plan.ItemId} at {plan.SourceStation.Label}",
|
||||
var targetPosition = supportStation.Position;
|
||||
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),
|
||||
]),
|
||||
CreateStep("step-fleet-deliver", "deliver-fleet", $"Deliver {plan.ItemId} to {plan.TargetShip.Definition.Name}",
|
||||
[
|
||||
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),
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime BuildConstructionPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ConstructionSiteRuntime site, StationRuntime supportStation, string summary)
|
||||
private static IReadOnlyList<ShipSubTaskRuntime> BuildAttackSubTasks(string targetEntityId, string? targetSystemId, string summary)
|
||||
{
|
||||
var targetPosition = site.StationId is null ? supportStation.Position : supportStation.Position;
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
"construction-support",
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}",
|
||||
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)
|
||||
]),
|
||||
CreateStep("step-construction-build", "build-site", $"Build {site.Id}",
|
||||
CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? string.Empty, Vector3.Zero, targetEntityId, 26f, 0f),
|
||||
];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ShipSubTaskRuntime> BuildFlyToObjectSubTasks(string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary)
|
||||
{
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, supportStation.Position, site.Id, 12f, 0f)
|
||||
])
|
||||
]);
|
||||
CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f),
|
||||
];
|
||||
}
|
||||
|
||||
private ShipPlanRuntime BuildAttackPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string? targetSystemId, string summary)
|
||||
private static IReadOnlyList<ShipSubTaskRuntime> BuildFollowShipSubTasks(ShipRuntime targetShip, float radius, float durationSeconds, string summary) =>
|
||||
BuildFollowSubTasks(targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary);
|
||||
|
||||
private static IReadOnlyList<ShipSubTaskRuntime> BuildFollowSubTasks(string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
ShipOrderKinds.AttackTarget,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-attack", ShipOrderKinds.AttackTarget, summary,
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? ship.SystemId, ship.Position, targetEntityId, 26f, 0f)
|
||||
])
|
||||
]);
|
||||
CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds),
|
||||
];
|
||||
}
|
||||
|
||||
private ShipPlanRuntime BuildDockAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime station, float waitSeconds, string summary)
|
||||
private static IReadOnlyList<ShipSubTaskRuntime> BuildHoldSubTasks(ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
ShipOrderKinds.DockAndWait,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-dock-wait-travel", "travel", $"Travel to {station.Label}",
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-dock-wait-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(station.Radius + 12f, 12f), 0f),
|
||||
CreateSubTask("sub-dock-wait-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f),
|
||||
CreateSubTask("sub-dock-wait-hold", ShipTaskKinds.HoldPosition, $"Wait at {station.Label}", station.SystemId, station.Position, station.Id, 0f, waitSeconds),
|
||||
])
|
||||
]);
|
||||
CreateSubTask("sub-hold", ShipTaskKinds.HoldPosition, order.Label ?? "Hold position", order.TargetSystemId ?? ship.SystemId, order.TargetPosition ?? ship.Position, order.TargetEntityId, 0f, 3f),
|
||||
];
|
||||
}
|
||||
|
||||
private ShipPlanRuntime BuildFlyAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, float waitSeconds, string summary)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildMiningSubTasks(ShipRuntime ship, ResourceNodeRuntime node, StationRuntime homeStation)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
ShipOrderKinds.FlyAndWait,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-fly-wait", ShipOrderKinds.FlyAndWait, summary,
|
||||
var deposit = SelectMiningDeposit(node, ship.Id);
|
||||
var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, ship.Id, 20f);
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-fly-wait-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, null, 6f, 0f),
|
||||
CreateSubTask("sub-fly-wait-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, null, 0f, waitSeconds),
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime BuildFlyToObjectPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildLocalMiningSubTasks(ShipRuntime ship, ResourceNodeRuntime node)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
ShipOrderKinds.FlyToObject,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-fly-object", ShipOrderKinds.FlyToObject, summary,
|
||||
var deposit = SelectMiningDeposit(node, ship.Id);
|
||||
var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, ship.Id, 20f);
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f),
|
||||
CreateSubTask("sub-fly-object-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, targetEntityId, 0f, MathF.Max(1f, ship.DefaultBehavior.WaitSeconds)),
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime BuildFollowShipPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ShipRuntime targetShip, float radius, float durationSeconds, string summary)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildLocalMiningDeliverySubTasks(ShipRuntime ship, StationRuntime buyer, string itemId)
|
||||
{
|
||||
return BuildFollowPlan(ship, sourceKind, sourceId, targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime BuildFollowPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
ShipOrderKinds.FollowShip,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-follow", "follow-target", summary,
|
||||
var amount = MathF.Max(1f, GetInventoryAmount(ship.Inventory, itemId));
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds),
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime CreateIdlePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildSalvageSubTasks(ShipRuntime ship, WreckRuntime wreck, StationRuntime homeStation, Vector3 approach)
|
||||
{
|
||||
return CreatePlan(
|
||||
ship,
|
||||
sourceKind,
|
||||
sourceId,
|
||||
Idle,
|
||||
summary,
|
||||
[
|
||||
CreateStep("step-idle", ShipOrderKinds.HoldPosition, summary,
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-idle", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 1.5f)
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime CreateBlockedPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary, string blockingReason)
|
||||
{
|
||||
var subTask = CreateSubTask("sub-blocked", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 0f);
|
||||
subTask.Status = WorkStatus.Blocked;
|
||||
subTask.BlockingReason = blockingReason;
|
||||
|
||||
var step = CreateStep("step-blocked", "blocked", summary, [subTask]);
|
||||
step.Status = AiPlanStepStatus.Blocked;
|
||||
step.BlockingReason = blockingReason;
|
||||
|
||||
var plan = CreatePlan(ship, sourceKind, sourceId, "blocked", summary, [step]);
|
||||
plan.Status = AiPlanStatus.Blocked;
|
||||
plan.FailureReason = blockingReason;
|
||||
return plan;
|
||||
}
|
||||
|
||||
private static ShipPlanRuntime CreatePlan(
|
||||
ShipRuntime ship,
|
||||
AiPlanSourceKind sourceKind,
|
||||
string sourceId,
|
||||
string kind,
|
||||
string summary,
|
||||
IReadOnlyList<ShipPlanStepRuntime> steps)
|
||||
{
|
||||
var plan = new ShipPlanRuntime
|
||||
{
|
||||
Id = $"plan-{ship.Id}-{Guid.NewGuid():N}",
|
||||
SourceKind = sourceKind,
|
||||
SourceId = sourceId,
|
||||
Kind = kind,
|
||||
Summary = summary,
|
||||
};
|
||||
plan.Steps.AddRange(steps);
|
||||
return plan;
|
||||
}
|
||||
|
||||
private static ShipPlanStepRuntime CreateStep(string id, string kind, string summary, IReadOnlyList<ShipSubTaskRuntime> subTasks)
|
||||
{
|
||||
var step = new ShipPlanStepRuntime
|
||||
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,
|
||||
};
|
||||
step.SubTasks.AddRange(subTasks);
|
||||
return step;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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;
|
||||
|
||||
@@ -7,7 +6,7 @@ namespace SpaceGame.Api.Ships.AI;
|
||||
|
||||
public sealed partial class ShipAiService
|
||||
{
|
||||
private ShipPlanRuntime? BuildEmergencyPlan(SimulationWorld world, ShipRuntime ship)
|
||||
private ShipOrderRuntime? BuildEmergencyOrder(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var policy = ResolvePolicy(world, ship.PolicySetId);
|
||||
if (policy is null)
|
||||
@@ -37,86 +36,75 @@ public sealed partial class ShipAiService
|
||||
.ThenBy(station => station.Position.DistanceTo(ship.Position))
|
||||
.FirstOrDefault();
|
||||
|
||||
var plan = new ShipPlanRuntime
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"plan-{ship.Id}-safety-{Guid.NewGuid():N}",
|
||||
SourceKind = AiPlanSourceKind.Rule,
|
||||
Id = $"rule-{ship.Id}-flee",
|
||||
Kind = ShipOrderKinds.Flee,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = ShipOrderKinds.Flee,
|
||||
Kind = "safety-flee",
|
||||
Summary = "Emergency retreat",
|
||||
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),
|
||||
};
|
||||
|
||||
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)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildOrderSubTasks(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),
|
||||
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 static (string BehaviorKind, string SourceId) ResolveBehaviorSource(SimulationWorld world, ShipRuntime ship)
|
||||
private IReadOnlyList<ShipSubTaskRuntime> BuildFleeSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
var assignment = ResolveAssignment(world, ship);
|
||||
return assignment is null
|
||||
? (ship.DefaultBehavior.Kind, ship.DefaultBehavior.Kind)
|
||||
: (assignment.BehaviorKind, assignment.ObjectiveId);
|
||||
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 ShipPlanRuntime BuildMovePlan(ShipRuntime ship, ShipOrderRuntime order)
|
||||
private static IReadOnlyList<ShipSubTaskRuntime> BuildMoveSubTasks(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",
|
||||
return
|
||||
[
|
||||
CreateSubTask("sub-move-travel", ShipTaskKinds.Travel, order.Label ?? "Travel", targetSystemId, targetPosition, order.TargetEntityId, 0f, 0f)
|
||||
])
|
||||
]);
|
||||
CreateSubTask("sub-move-travel", ShipTaskKinds.Travel, order.Label ?? "Travel", targetSystemId, targetPosition, order.TargetEntityId, MathF.Max(0f, order.Radius), 0f),
|
||||
];
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildDockOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildDockOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId);
|
||||
if (station is null)
|
||||
@@ -125,25 +113,14 @@ public sealed partial class ShipAiService
|
||||
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}",
|
||||
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)
|
||||
]),
|
||||
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)
|
||||
])
|
||||
]);
|
||||
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 ShipPlanRuntime? BuildTradeOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildTradeOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
if (order.SourceStationId is null || order.DestinationStationId is null || order.ItemId is null)
|
||||
{
|
||||
@@ -158,10 +135,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildTradePlan(ship, AiPlanSourceKind.Order, order.Id, route, order.Label ?? route.Summary);
|
||||
return BuildTradeSubTasks(ship, route);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildMineOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildMineOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
var systemId = order.TargetSystemId ?? ship.SystemId;
|
||||
var itemId = order.ItemId;
|
||||
@@ -198,10 +175,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildLocalMiningPlan(world, ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {itemId} in {systemId}");
|
||||
return BuildLocalMiningSubTasks(ship, node);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildMineLocalOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildMineLocalOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId);
|
||||
var node = ResolveNode(world, order.TargetEntityId)
|
||||
@@ -212,10 +189,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildLocalMiningPlan(world, ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {node.ItemId}");
|
||||
return BuildLocalMiningSubTasks(ship, node);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildMineAndDeliverRunOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildMineAndDeliverRunOrderSubTasks(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
{
|
||||
var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId);
|
||||
var node = ResolveNode(world, order.TargetEntityId)
|
||||
@@ -229,10 +206,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildMiningPlan(world, ship, AiPlanSourceKind.Order, order.Id, node, buyer, order.Label ?? $"Mine {node.ItemId} for {buyer.Label}");
|
||||
return BuildMiningSubTasks(ship, node, buyer);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildSellMinedCargoOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
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))
|
||||
@@ -241,10 +218,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildLocalMiningDeliveryPlan(ship, AiPlanSourceKind.Order, order.Id, buyer, order.ItemId, order.Label ?? $"Sell {order.ItemId}");
|
||||
return BuildLocalMiningDeliverySubTasks(ship, buyer, order.ItemId);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildAutoSalvageOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
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);
|
||||
@@ -255,29 +232,10 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
|
||||
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),
|
||||
])
|
||||
]);
|
||||
return BuildSalvageSubTasks(ship, wreck, homeStation, approach);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildSupplyFleetOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
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);
|
||||
@@ -303,10 +261,10 @@ public sealed partial class ShipAiService
|
||||
amount,
|
||||
MathF.Max(16f, order.Radius),
|
||||
order.Label ?? $"Supply {targetShip.Definition.Name} with {order.ItemId}");
|
||||
return BuildFleetSupplyPlan(ship, AiPlanSourceKind.Order, order.Id, plan);
|
||||
return BuildFleetSupplySubTasks(plan);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildBuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
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)
|
||||
@@ -322,10 +280,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildConstructionPlan(ship, AiPlanSourceKind.Order, order.Id, site, supportStation, order.Label ?? $"Build {site.BlueprintId}");
|
||||
return BuildConstructionSubTasks(site, supportStation);
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildAttackOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildAttackOrderSubTasks(ShipOrderRuntime order)
|
||||
{
|
||||
var targetId = order.TargetEntityId;
|
||||
if (targetId is null)
|
||||
@@ -334,45 +292,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildAttackPlan(ship, AiPlanSourceKind.Order, order.Id, targetId, order.TargetSystemId, order.Label ?? "Attack target");
|
||||
return BuildAttackSubTasks(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)
|
||||
private IReadOnlyList<ShipSubTaskRuntime>? BuildFlyToObjectOrderSubTasks(SimulationWorld world, ShipOrderRuntime order)
|
||||
{
|
||||
var targetEntityId = order.TargetEntityId;
|
||||
if (targetEntityId is null)
|
||||
@@ -388,10 +311,10 @@ public sealed partial class ShipAiService
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildFlyToObjectPlan(ship, AiPlanSourceKind.Order, order.Id, objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, order.Label ?? $"Fly to {targetEntityId}");
|
||||
return BuildFlyToObjectSubTasks(objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, order.Label ?? $"Fly to {targetEntityId}");
|
||||
}
|
||||
|
||||
private ShipPlanRuntime? BuildFollowShipOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
|
||||
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)
|
||||
@@ -400,71 +323,6 @@ public sealed partial class ShipAiService
|
||||
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(SimulationWorld world, ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, StationRuntime homeStation, string summary)
|
||||
{
|
||||
var deposit = SelectMiningDeposit(node, ship.Id);
|
||||
var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, 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} 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)
|
||||
]),
|
||||
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(SimulationWorld world, ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, string summary)
|
||||
{
|
||||
var deposit = SelectMiningDeposit(node, ship.Id);
|
||||
var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, 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} 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 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)
|
||||
])
|
||||
]);
|
||||
return BuildFollowShipSubTasks(targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,191 +26,195 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
|
||||
var previousState = ship.State;
|
||||
var previousPlanId = ship.ActivePlan?.Id;
|
||||
var previousStepId = GetCurrentStep(ship.ActivePlan)?.Id;
|
||||
|
||||
EnsurePlan(world, ship, events);
|
||||
ExecutePlan(world, ship, deltaSeconds, events);
|
||||
TrackHistory(ship);
|
||||
EmitStateEvents(ship, previousState, previousPlanId, previousStepId, events);
|
||||
}
|
||||
|
||||
private void EnsurePlan(SimulationWorld world, ShipRuntime ship, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var emergencyPlan = BuildEmergencyPlan(world, ship);
|
||||
if (emergencyPlan is not null)
|
||||
{
|
||||
ship.LastReplanReason = "rule-safety";
|
||||
ReplacePlan(ship, emergencyPlan, "rule-safety", events);
|
||||
return;
|
||||
}
|
||||
var previousOrderId = ship.ActiveOrderId;
|
||||
var previousTaskId = GetCurrentSubTask(ship)?.Id;
|
||||
|
||||
SyncEmergencyOrders(world, ship);
|
||||
SyncBehaviorOrders(world, ship);
|
||||
var topOrder = GetTopOrder(ship);
|
||||
if (topOrder is not null && topOrder.Status == OrderStatus.Queued)
|
||||
{
|
||||
topOrder.Status = OrderStatus.Active;
|
||||
}
|
||||
|
||||
var desiredSourceKind = topOrder is null ? AiPlanSourceKind.DefaultBehavior : AiPlanSourceKind.Order;
|
||||
var desiredSourceId = topOrder?.Id ?? ResolveBehaviorSource(world, ship).SourceId;
|
||||
var currentPlan = ship.ActivePlan;
|
||||
|
||||
if (currentPlan is not null
|
||||
&& currentPlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed and not AiPlanStatus.Interrupted
|
||||
&& currentPlan.SourceKind == desiredSourceKind
|
||||
&& string.Equals(currentPlan.SourceId, desiredSourceId, StringComparison.Ordinal)
|
||||
&& !ship.NeedsReplan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ship.ReplanCooldownSeconds > 0f && currentPlan is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ShipPlanRuntime? nextPlan = desiredSourceKind == AiPlanSourceKind.Order
|
||||
? BuildOrderPlan(world, ship, topOrder!)
|
||||
: BuildBehaviorFallbackPlan(world, ship);
|
||||
|
||||
if (nextPlan is null)
|
||||
{
|
||||
nextPlan = CreateIdlePlan(ship, desiredSourceKind, desiredSourceId, "No viable plan");
|
||||
}
|
||||
|
||||
if (nextPlan.Kind != Idle)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
}
|
||||
|
||||
ReplacePlan(ship, nextPlan, "replanned", events);
|
||||
EnsureOrderExecution(world, ship, events);
|
||||
ExecuteOrder(world, ship, deltaSeconds, events);
|
||||
TrackHistory(ship);
|
||||
EmitStateEvents(ship, previousState, previousOrderId, previousTaskId, events);
|
||||
}
|
||||
|
||||
private void ExecutePlan(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
private void EnsureOrderExecution(SimulationWorld world, ShipRuntime ship, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var plan = ship.ActivePlan;
|
||||
if (plan is null)
|
||||
var currentOrder = ship.OrderQueue.GetCurrentOrder();
|
||||
if (currentOrder is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
ClearActiveOrder(ship);
|
||||
ApplyIdleOrBlockedState(world, ship);
|
||||
return;
|
||||
}
|
||||
|
||||
if (plan.CurrentStepIndex >= plan.Steps.Count)
|
||||
if (currentOrder.Status == OrderStatus.Queued)
|
||||
{
|
||||
currentOrder.Status = OrderStatus.Active;
|
||||
}
|
||||
|
||||
if (!ship.NeedsReplan
|
||||
&& string.Equals(ship.ActiveOrderId, currentOrder.Id, StringComparison.Ordinal)
|
||||
&& ship.ActiveSubTaskIndex < ship.ActiveSubTasks.Count)
|
||||
{
|
||||
CompletePlan(ship, plan, events);
|
||||
return;
|
||||
}
|
||||
|
||||
plan.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
var step = plan.Steps[plan.CurrentStepIndex];
|
||||
if (step.Status == AiPlanStepStatus.Planned)
|
||||
if (ship.ReplanCooldownSeconds > 0f && !string.Equals(ship.ActiveOrderId, currentOrder.Id, StringComparison.Ordinal))
|
||||
{
|
||||
step.Status = AiPlanStepStatus.Running;
|
||||
}
|
||||
|
||||
if (step.CurrentSubTaskIndex >= step.SubTasks.Count)
|
||||
{
|
||||
CompleteStep(plan, step);
|
||||
return;
|
||||
}
|
||||
|
||||
var subTask = step.SubTasks[step.CurrentSubTaskIndex];
|
||||
var subTasks = BuildOrderSubTasks(world, ship, currentOrder);
|
||||
if (subTasks is null || subTasks.Count == 0)
|
||||
{
|
||||
FailOrder(ship, currentOrder, currentOrder.FailureReason ?? "order-unavailable");
|
||||
ClearActiveOrder(ship);
|
||||
ship.NeedsReplan = true;
|
||||
ship.ReplanCooldownSeconds = 0.1f;
|
||||
ship.LastReplanReason = currentOrder.FailureReason ?? "order-unavailable";
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "order-failed", $"{ship.Definition.Name} failed {currentOrder.Label ?? currentOrder.Kind}.", DateTimeOffset.UtcNow));
|
||||
ApplyIdleOrBlockedState(world, ship);
|
||||
return;
|
||||
}
|
||||
|
||||
BeginOrderExecution(ship, currentOrder, subTasks);
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "order-started", $"{ship.Definition.Name} started {currentOrder.Label ?? currentOrder.Kind}.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
private void ExecuteOrder(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var order = ship.ActiveOrderId is null ? null : ship.OrderQueue.FindById(ship.ActiveOrderId);
|
||||
if (order is null)
|
||||
{
|
||||
ClearActiveOrder(ship);
|
||||
ApplyIdleOrBlockedState(world, ship);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count)
|
||||
{
|
||||
CompleteOrderExecution(ship, order, events);
|
||||
return;
|
||||
}
|
||||
|
||||
var subTask = ship.ActiveSubTasks[ship.ActiveSubTaskIndex];
|
||||
if (subTask.Status == WorkStatus.Pending)
|
||||
{
|
||||
subTask.Status = WorkStatus.Active;
|
||||
}
|
||||
else if (subTask.Status == WorkStatus.Blocked)
|
||||
{
|
||||
step.Status = AiPlanStepStatus.Blocked;
|
||||
step.BlockingReason = subTask.BlockingReason;
|
||||
plan.Status = AiPlanStatus.Blocked;
|
||||
ship.State = ShipState.Blocked;
|
||||
ship.TargetPosition = subTask.TargetPosition ?? ship.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
plan.Status = AiPlanStatus.Running;
|
||||
|
||||
var outcome = UpdateSubTask(world, ship, step, subTask, deltaSeconds);
|
||||
var outcome = UpdateSubTask(world, ship, subTask, deltaSeconds);
|
||||
switch (outcome)
|
||||
{
|
||||
case SubTaskOutcome.Active:
|
||||
step.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStepStatus.Blocked : AiPlanStepStatus.Running;
|
||||
plan.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStatus.Blocked : AiPlanStatus.Running;
|
||||
return;
|
||||
case SubTaskOutcome.Completed:
|
||||
subTask.Status = WorkStatus.Completed;
|
||||
subTask.Progress = 1f;
|
||||
step.CurrentSubTaskIndex += 1;
|
||||
step.BlockingReason = null;
|
||||
if (step.CurrentSubTaskIndex >= step.SubTasks.Count)
|
||||
ship.ActiveSubTaskIndex += 1;
|
||||
if (ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count)
|
||||
{
|
||||
CompleteStep(plan, step);
|
||||
CompleteOrderExecution(ship, order, events);
|
||||
}
|
||||
|
||||
return;
|
||||
case SubTaskOutcome.Failed:
|
||||
subTask.Status = WorkStatus.Failed;
|
||||
step.Status = AiPlanStepStatus.Failed;
|
||||
plan.Status = AiPlanStatus.Failed;
|
||||
plan.FailureReason = subTask.BlockingReason ?? "subtask-failed";
|
||||
ship.NeedsReplan = true;
|
||||
ship.ReplanCooldownSeconds = 0.5f;
|
||||
ship.LastReplanReason = plan.FailureReason;
|
||||
FailOrderExecution(ship, order, subTask.BlockingReason ?? "subtask-failed", events);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CompleteStep(ShipPlanRuntime plan, ShipPlanStepRuntime step)
|
||||
private static void BeginOrderExecution(ShipRuntime ship, ShipOrderRuntime order, IReadOnlyList<ShipSubTaskRuntime> subTasks)
|
||||
{
|
||||
step.Status = AiPlanStepStatus.Completed;
|
||||
step.BlockingReason = null;
|
||||
plan.CurrentStepIndex += 1;
|
||||
if (plan.CurrentStepIndex >= plan.Steps.Count)
|
||||
{
|
||||
plan.Status = AiPlanStatus.Completed;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CompletePlan(ShipRuntime ship, ShipPlanRuntime plan, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
plan.Status = AiPlanStatus.Completed;
|
||||
var completedOrder = plan.SourceKind == AiPlanSourceKind.Order
|
||||
? ship.OrderQueue.FirstOrDefault(order => order.Id == plan.SourceId)
|
||||
: null;
|
||||
if (completedOrder is not null)
|
||||
{
|
||||
completedOrder.Status = OrderStatus.Completed;
|
||||
ship.OrderQueue.RemoveAll(order => order.Id == completedOrder.Id);
|
||||
if (completedOrder.SourceKind == ShipOrderSourceKind.Behavior
|
||||
&& string.Equals(completedOrder.SourceId, RepeatOrders, StringComparison.Ordinal)
|
||||
&& ship.DefaultBehavior.RepeatOrders.Count > 0)
|
||||
{
|
||||
ship.DefaultBehavior.RepeatIndex = (ship.DefaultBehavior.RepeatIndex + 1) % ship.DefaultBehavior.RepeatOrders.Count;
|
||||
}
|
||||
}
|
||||
ship.ActivePlan = null;
|
||||
ship.NeedsReplan = true;
|
||||
ship.ReplanCooldownSeconds = 0.25f;
|
||||
ship.LastReplanReason = "plan-completed";
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "plan-completed", $"{ship.Definition.Name} completed {plan.Kind}.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
private void ReplacePlan(ShipRuntime ship, ShipPlanRuntime nextPlan, string reason, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
if (ship.ActivePlan is not null && ship.ActivePlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed)
|
||||
{
|
||||
ship.ActivePlan.Status = AiPlanStatus.Interrupted;
|
||||
ship.ActivePlan.InterruptReason = reason;
|
||||
}
|
||||
|
||||
ship.ActivePlan = nextPlan;
|
||||
ship.ActiveOrderId = order.Id;
|
||||
ship.ActiveSubTaskIndex = 0;
|
||||
ship.ActiveSubTasks.Clear();
|
||||
ship.ActiveSubTasks.AddRange(subTasks);
|
||||
ship.NeedsReplan = false;
|
||||
ship.ReplanCooldownSeconds = 0f;
|
||||
ship.LastReplanReason = reason;
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "plan-updated", $"{ship.Definition.Name} planned {nextPlan.Kind}.", DateTimeOffset.UtcNow));
|
||||
ship.LastReplanReason = "order-execution-started";
|
||||
ship.LastDeltaSignature = string.Empty;
|
||||
}
|
||||
|
||||
private static void ClearActiveOrder(ShipRuntime ship)
|
||||
{
|
||||
ship.ActiveOrderId = null;
|
||||
ship.ActiveSubTaskIndex = 0;
|
||||
ship.ActiveSubTasks.Clear();
|
||||
}
|
||||
|
||||
private void CompleteOrderExecution(ShipRuntime ship, ShipOrderRuntime order, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
ship.OrderQueue.TryCompleteOrder(order.Id);
|
||||
if (order.SourceKind == ShipOrderSourceKind.Behavior
|
||||
&& string.Equals(order.SourceId, RepeatOrders, StringComparison.Ordinal)
|
||||
&& ship.DefaultBehavior.RepeatOrders.Count > 0)
|
||||
{
|
||||
ship.DefaultBehavior.RepeatIndex = (ship.DefaultBehavior.RepeatIndex + 1) % ship.DefaultBehavior.RepeatOrders.Count;
|
||||
}
|
||||
|
||||
ClearActiveOrder(ship);
|
||||
ship.NeedsReplan = true;
|
||||
ship.ReplanCooldownSeconds = 0.25f;
|
||||
ship.LastReplanReason = "order-completed";
|
||||
ship.LastDeltaSignature = string.Empty;
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "order-completed", $"{ship.Definition.Name} completed {order.Label ?? order.Kind}.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
private void FailOrderExecution(ShipRuntime ship, ShipOrderRuntime order, string failureReason, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
FailOrder(ship, order, failureReason);
|
||||
ClearActiveOrder(ship);
|
||||
ship.NeedsReplan = true;
|
||||
ship.ReplanCooldownSeconds = 0.5f;
|
||||
ship.LastReplanReason = failureReason;
|
||||
ship.LastDeltaSignature = string.Empty;
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "order-failed", $"{ship.Definition.Name} failed {order.Label ?? order.Kind}.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
private static void FailOrder(ShipRuntime ship, ShipOrderRuntime order, string failureReason)
|
||||
{
|
||||
ship.OrderQueue.TryFailOrder(order.Id, failureReason);
|
||||
ship.LastDeltaSignature = string.Empty;
|
||||
}
|
||||
|
||||
private static ShipSubTaskRuntime? GetCurrentSubTask(ShipRuntime ship) =>
|
||||
ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count ? null : ship.ActiveSubTasks[ship.ActiveSubTaskIndex];
|
||||
|
||||
private void ApplyIdleOrBlockedState(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var (behaviorKind, _) = ResolveBehaviorSource(world, ship);
|
||||
if (IsBehaviorBlockingFailure(behaviorKind, ship.LastAccessFailureReason))
|
||||
{
|
||||
ship.State = ShipState.Blocked;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
}
|
||||
|
||||
private void SyncEmergencyOrders(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var desiredOrder = BuildEmergencyOrder(world, ship);
|
||||
ship.OrderQueue.RemoveWhere(order =>
|
||||
order.SourceKind == ShipOrderSourceKind.Behavior
|
||||
&& string.Equals(order.SourceId, ShipOrderKinds.Flee, StringComparison.Ordinal)
|
||||
&& (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)));
|
||||
|
||||
if (desiredOrder is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ship.OrderQueue.AddOrReplaceManagedOrderAtFront(desiredOrder);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user