320 lines
15 KiB
C#
320 lines
15 KiB
C#
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
|
|
|
|
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,
|
|
_ => false,
|
|
};
|
|
|
|
private static string DescribeBehaviorFallbackSummary(SimulationWorld world, ShipRuntime ship, string behaviorKind, string? failureReason)
|
|
{
|
|
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",
|
|
};
|
|
}
|
|
|
|
private ShipPlanRuntime BuildTradePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, TradeRoutePlan route, string summary)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
ShipOrderKinds.TradeRoute,
|
|
summary,
|
|
[
|
|
CreateStep("step-acquire", "acquire-cargo", $"Load {route.ItemId} at {route.SourceStation.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)
|
|
]),
|
|
CreateStep("step-deliver", "deliver-cargo", $"Deliver {route.ItemId} to {route.DestinationStation.Label}",
|
|
[
|
|
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 ShipPlanRuntime BuildFleetSupplyPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, FleetSupplyPlan plan)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
SupplyFleet,
|
|
plan.Summary,
|
|
[
|
|
CreateStep("step-fleet-acquire", "acquire-cargo", $"Load {plan.ItemId} at {plan.SourceStation.Label}",
|
|
[
|
|
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),
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime BuildConstructionPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ConstructionSiteRuntime site, StationRuntime supportStation, 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}",
|
|
[
|
|
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, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f)
|
|
]),
|
|
CreateStep("step-construction-build", "build-site", $"Build {site.Id}",
|
|
[
|
|
CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f)
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime BuildAttackPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string? targetSystemId, string summary)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
ShipOrderKinds.AttackTarget,
|
|
summary,
|
|
[
|
|
CreateStep("step-attack", ShipOrderKinds.AttackTarget, summary,
|
|
[
|
|
CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? ship.SystemId, ship.Position, targetEntityId, 26f, 0f)
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime BuildDockAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime station, float waitSeconds, string summary)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
ShipOrderKinds.DockAndWait,
|
|
summary,
|
|
[
|
|
CreateStep("step-dock-wait-travel", "travel", $"Travel to {station.Label}",
|
|
[
|
|
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),
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime BuildFlyAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, float waitSeconds, string summary)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
ShipOrderKinds.FlyAndWait,
|
|
summary,
|
|
[
|
|
CreateStep("step-fly-wait", ShipOrderKinds.FlyAndWait, summary,
|
|
[
|
|
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),
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime BuildFlyToObjectPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
ShipOrderKinds.FlyToObject,
|
|
summary,
|
|
[
|
|
CreateStep("step-fly-object", ShipOrderKinds.FlyToObject, summary,
|
|
[
|
|
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)),
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime BuildFollowShipPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ShipRuntime targetShip, float radius, float durationSeconds, string summary)
|
|
{
|
|
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,
|
|
[
|
|
CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds),
|
|
])
|
|
]);
|
|
}
|
|
|
|
private ShipPlanRuntime CreateIdlePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary)
|
|
{
|
|
return CreatePlan(
|
|
ship,
|
|
sourceKind,
|
|
sourceId,
|
|
Idle,
|
|
summary,
|
|
[
|
|
CreateStep("step-idle", ShipOrderKinds.HoldPosition, summary,
|
|
[
|
|
CreateSubTask("sub-idle", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 1.5f)
|
|
])
|
|
]);
|
|
}
|
|
|
|
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
|
|
{
|
|
Id = id,
|
|
Kind = kind,
|
|
Summary = summary,
|
|
};
|
|
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? targetNodeId = null) =>
|
|
new()
|
|
{
|
|
Id = id,
|
|
Kind = kind,
|
|
Summary = summary,
|
|
TargetSystemId = targetSystemId,
|
|
TargetPosition = targetPosition,
|
|
TargetEntityId = targetEntityId,
|
|
TargetNodeId = targetNodeId,
|
|
ItemId = itemId,
|
|
ModuleId = moduleId,
|
|
Threshold = threshold,
|
|
Amount = amount,
|
|
};
|
|
}
|