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 { 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 { 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 { 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 { 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 { 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 { 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 { 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"; } }