228 lines
8.3 KiB
C#
228 lines
8.3 KiB
C#
|
|
namespace SpaceGame.Api.Ships.AI;
|
|
|
|
// ─── Planning State ────────────────────────────────────────────────────────────
|
|
|
|
public sealed class ShipPlanningState
|
|
{
|
|
public string ShipKind { get; set; } = string.Empty;
|
|
public bool HasMiningCapability { get; set; }
|
|
public bool FactionWantsOre { get; set; }
|
|
public bool FactionWantsExpansion { get; set; }
|
|
public bool FactionWantsCombat { get; set; }
|
|
public bool FactionNeedsShipyard { get; set; }
|
|
public string? TargetEnemySystemId { get; set; }
|
|
public string? TargetEnemyEntityId { get; set; }
|
|
public string? TradeItemId { get; set; }
|
|
public string? TradeSourceStationId { get; set; }
|
|
public string? TradeDestinationStationId { get; set; }
|
|
public string? CurrentObjective { get; set; }
|
|
|
|
public ShipPlanningState Clone() => (ShipPlanningState)MemberwiseClone();
|
|
}
|
|
|
|
// ─── Goals ─────────────────────────────────────────────────────────────────────
|
|
|
|
// A ship should always have an assigned objective. The planner picks the best one.
|
|
public sealed class AssignObjectiveGoal : GoapGoal<ShipPlanningState>
|
|
{
|
|
public override string Name => "assign-objective";
|
|
|
|
public override bool IsSatisfied(ShipPlanningState state) => state.CurrentObjective is not null;
|
|
|
|
public override float ComputePriority(ShipPlanningState state, SimulationWorld world, CommanderRuntime commander) =>
|
|
100f;
|
|
}
|
|
|
|
// ─── Actions ───────────────────────────────────────────────────────────────────
|
|
|
|
public sealed class SetMiningObjectiveAction : GoapAction<ShipPlanningState>
|
|
{
|
|
public override string Name => "set-mining-objective";
|
|
public override float Cost => 1f;
|
|
|
|
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
state.HasMiningCapability && state.FactionWantsOre;
|
|
|
|
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
{
|
|
state.CurrentObjective = "auto-mine";
|
|
return state;
|
|
}
|
|
|
|
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
{
|
|
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "auto-mine", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.DefaultBehavior.Kind = "auto-mine";
|
|
ship.DefaultBehavior.Phase = null;
|
|
ship.DefaultBehavior.NodeId = null;
|
|
}
|
|
}
|
|
|
|
public sealed class SetPatrolObjectiveAction : GoapAction<ShipPlanningState>
|
|
{
|
|
public override string Name => "set-patrol-objective";
|
|
public override float Cost => 2f;
|
|
|
|
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
string.Equals(state.ShipKind, "military", StringComparison.Ordinal);
|
|
|
|
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
{
|
|
state.CurrentObjective = "patrol";
|
|
return state;
|
|
}
|
|
|
|
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
{
|
|
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "patrol", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
|
|
{
|
|
var station = world.Stations.FirstOrDefault(s =>
|
|
s.FactionId == ship.FactionId &&
|
|
string.Equals(s.SystemId, ship.SystemId, StringComparison.Ordinal));
|
|
|
|
if (station is not null)
|
|
{
|
|
var radius = station.Radius + 90f;
|
|
ship.DefaultBehavior.PatrolPoints.AddRange(
|
|
[
|
|
new Vector3(station.Position.X + radius, station.Position.Y, station.Position.Z),
|
|
new Vector3(station.Position.X, station.Position.Y, station.Position.Z + radius),
|
|
new Vector3(station.Position.X - radius, station.Position.Y, station.Position.Z),
|
|
new Vector3(station.Position.X, station.Position.Y, station.Position.Z - radius),
|
|
]);
|
|
}
|
|
}
|
|
|
|
ship.DefaultBehavior.Kind = "patrol";
|
|
}
|
|
}
|
|
|
|
public sealed class SetAttackObjectiveAction : GoapAction<ShipPlanningState>
|
|
{
|
|
public override string Name => "set-attack-objective";
|
|
public override float Cost => 1f;
|
|
|
|
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
string.Equals(state.ShipKind, "military", StringComparison.Ordinal)
|
|
&& state.FactionWantsCombat
|
|
&& state.TargetEnemyEntityId is not null;
|
|
|
|
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
{
|
|
state.CurrentObjective = "attack-target";
|
|
return state;
|
|
}
|
|
|
|
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
{
|
|
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
if (ship is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.DefaultBehavior.Kind = "attack-target";
|
|
ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior?.AreaSystemId ?? ship.DefaultBehavior.AreaSystemId;
|
|
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
|
|
ship.DefaultBehavior.Phase = null;
|
|
}
|
|
}
|
|
|
|
public sealed class SetConstructionObjectiveAction : GoapAction<ShipPlanningState>
|
|
{
|
|
public override string Name => "set-construction-objective";
|
|
public override float Cost => 1f;
|
|
|
|
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
string.Equals(state.ShipKind, "construction", StringComparison.Ordinal)
|
|
&& (state.FactionWantsExpansion || state.FactionNeedsShipyard);
|
|
|
|
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
{
|
|
state.CurrentObjective = "construct-station";
|
|
return state;
|
|
}
|
|
|
|
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
{
|
|
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "construct-station", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.DefaultBehavior.Kind = "construct-station";
|
|
ship.DefaultBehavior.Phase = null;
|
|
}
|
|
}
|
|
|
|
public sealed class SetTradeObjectiveAction : GoapAction<ShipPlanningState>
|
|
{
|
|
public override string Name => "set-trade-objective";
|
|
public override float Cost => 1f;
|
|
|
|
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
string.Equals(state.ShipKind, "transport", StringComparison.Ordinal)
|
|
&& state.TradeItemId is not null
|
|
&& state.TradeSourceStationId is not null
|
|
&& state.TradeDestinationStationId is not null;
|
|
|
|
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
{
|
|
state.CurrentObjective = "trade-haul";
|
|
return state;
|
|
}
|
|
|
|
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
{
|
|
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
if (ship is null || commander.ActiveBehavior is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.DefaultBehavior.Kind = "trade-haul";
|
|
ship.DefaultBehavior.ItemId = commander.ActiveBehavior.ItemId;
|
|
ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId;
|
|
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior.TargetEntityId;
|
|
ship.DefaultBehavior.Phase ??= "travel-to-source";
|
|
}
|
|
}
|
|
|
|
public sealed class SetIdleObjectiveAction : GoapAction<ShipPlanningState>
|
|
{
|
|
public override string Name => "set-idle-objective";
|
|
public override float Cost => 10f;
|
|
|
|
public override bool CheckPreconditions(ShipPlanningState state) => true;
|
|
|
|
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
{
|
|
state.CurrentObjective = "idle";
|
|
return state;
|
|
}
|
|
|
|
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
{
|
|
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "idle", StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.DefaultBehavior.Kind = "idle";
|
|
}
|
|
}
|