217 lines
8.0 KiB
C#
217 lines
8.0 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 const float WarpEngageDistanceKilometers = 250_000f;
|
|
private const float FrigateDps = 7f;
|
|
private const float DestroyerDps = 12f;
|
|
private const float CruiserDps = 18f;
|
|
private const float CapitalDps = 26f;
|
|
|
|
private readonly IBalanceService balance;
|
|
|
|
public ShipAiService(IBalanceService balance)
|
|
{
|
|
this.balance = balance;
|
|
}
|
|
|
|
internal void UpdateShip(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
if (ship.ReplanCooldownSeconds > 0f)
|
|
{
|
|
ship.ReplanCooldownSeconds = MathF.Max(0f, ship.ReplanCooldownSeconds - deltaSeconds);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private void ExecutePlan(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
var plan = ship.ActivePlan;
|
|
if (plan is null)
|
|
{
|
|
ship.State = ShipState.Idle;
|
|
ship.TargetPosition = ship.Position;
|
|
return;
|
|
}
|
|
|
|
if (plan.CurrentStepIndex >= plan.Steps.Count)
|
|
{
|
|
CompletePlan(ship, plan, events);
|
|
return;
|
|
}
|
|
|
|
plan.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
|
|
|
var step = plan.Steps[plan.CurrentStepIndex];
|
|
if (step.Status == AiPlanStepStatus.Planned)
|
|
{
|
|
step.Status = AiPlanStepStatus.Running;
|
|
}
|
|
|
|
if (step.CurrentSubTaskIndex >= step.SubTasks.Count)
|
|
{
|
|
CompleteStep(plan, step);
|
|
return;
|
|
}
|
|
|
|
var subTask = step.SubTasks[step.CurrentSubTaskIndex];
|
|
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);
|
|
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)
|
|
{
|
|
CompleteStep(plan, step);
|
|
}
|
|
|
|
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;
|
|
return;
|
|
}
|
|
}
|
|
|
|
private static void CompleteStep(ShipPlanRuntime plan, ShipPlanStepRuntime step)
|
|
{
|
|
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.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));
|
|
}
|
|
}
|