namespace SpaceGame.Api.Factions.AI; // ─── Planning State ──────────────────────────────────────────────────────────── public sealed class FactionPlanningState { public int MilitaryShipCount { get; set; } public int MinerShipCount { get; set; } public int TransportShipCount { get; set; } public int ConstructorShipCount { get; set; } public int ControlledSystemCount { get; set; } public int TargetSystemCount { get; set; } public bool HasShipFactory { get; set; } public int EnemyFactionCount { get; set; } public int EnemyShipCount { get; set; } public int EnemyStationCount { get; set; } public float OreStockpile { get; set; } public float RefinedMetalsStockpile { get; set; } public float RefinedMetalsProductionRate { get; set; } public float RefinedMetalsShortageHorizonSeconds { get; set; } public float HullpartsStockpile { get; set; } public float HullpartsProductionRate { get; set; } public float HullpartsShortageHorizonSeconds { get; set; } public float ClaytronicsStockpile { get; set; } public float ClaytronicsProductionRate { get; set; } public float ClaytronicsShortageHorizonSeconds { get; set; } public float WaterStockpile { get; set; } public float WaterProductionRate { get; set; } public float WaterShortageHorizonSeconds { get; set; } public bool HasRefinedMetalsProduction => RefinedMetalsProductionRate > 0.01f; public bool HasHullpartsProduction => HullpartsProductionRate > 0.01f; public bool HasClaytronicsProduction => ClaytronicsProductionRate > 0.01f; public bool HasWaterProduction => WaterProductionRate > 0.01f; public bool HasWarIndustrySupplyChain => HasRefinedMetalsProduction && HasHullpartsProduction && HasClaytronicsProduction; public FactionPlanningState Clone() => (FactionPlanningState)MemberwiseClone(); internal static int ComputeTargetWarships(FactionPlanningState state) { var expansionDeficit = Math.Max(0, state.TargetSystemCount - state.ControlledSystemCount); return Math.Max(3, (state.ControlledSystemCount * 2) + (expansionDeficit * 3) + Math.Min(4, state.EnemyFactionCount + state.EnemyStationCount)); } } // ─── Goals ───────────────────────────────────────────────────────────────────── public sealed class EnsureWarIndustryGoal : GoapGoal { public override string Name => "ensure-war-industry"; public override bool IsSatisfied(FactionPlanningState state) => state.EnemyFactionCount <= 0 || (state.HasWarIndustrySupplyChain && state.HasShipFactory); public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { if (state.EnemyFactionCount <= 0) { return 0f; } var missingStages = (state.HasRefinedMetalsProduction ? 0 : 1) + (state.HasHullpartsProduction ? 0 : 1) + (state.HasClaytronicsProduction ? 0 : 1) + (state.HasShipFactory ? 0 : 1); return missingStages <= 0 ? 0f : 125f + (missingStages * 18f); } } public sealed class EnsureWaterSecurityGoal : GoapGoal { public override string Name => "ensure-water-security"; public override bool IsSatisfied(FactionPlanningState state) => state.HasWaterProduction && state.WaterShortageHorizonSeconds >= 300f; public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { if (state.HasWaterProduction && state.WaterShortageHorizonSeconds >= 300f) { return 0f; } if (float.IsPositiveInfinity(state.WaterShortageHorizonSeconds)) { return state.HasWaterProduction ? 0f : 85f; } return 55f + MathF.Max(0f, 300f - state.WaterShortageHorizonSeconds) * 0.2f; } } public sealed class EnsureWarFleetGoal : GoapGoal { public override string Name => "ensure-war-fleet"; public override bool IsSatisfied(FactionPlanningState state) => state.MilitaryShipCount >= FactionPlanningState.ComputeTargetWarships(state); public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { var deficit = FactionPlanningState.ComputeTargetWarships(state) - state.MilitaryShipCount; return deficit <= 0 ? 0f : 50f + (deficit * 10f); } } public sealed class ExterminateRivalGoal : GoapGoal { public override string Name => "exterminate-rival"; public override bool IsSatisfied(FactionPlanningState state) => state.EnemyFactionCount <= 0 || (state.EnemyShipCount <= 0 && state.EnemyStationCount <= 0); public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { if (state.EnemyFactionCount <= 0) { return 0f; } return 140f + (state.EnemyStationCount * 25f) + (state.EnemyShipCount * 6f); } } public sealed class ExpandTerritoryGoal : GoapGoal { public override string Name => "expand-territory"; public override bool IsSatisfied(FactionPlanningState state) => state.ControlledSystemCount >= state.TargetSystemCount; public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { var deficit = state.TargetSystemCount - state.ControlledSystemCount; return deficit <= 0 ? 0f : 80f + (deficit * 15f); } } public sealed class EnsureMiningCapacityGoal : GoapGoal { private const int MinMiners = 2; public override string Name => "ensure-mining-capacity"; public override bool IsSatisfied(FactionPlanningState state) => state.MinerShipCount >= MinMiners; public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { var deficit = MinMiners - state.MinerShipCount; return deficit <= 0 ? 0f : 70f + (deficit * 12f); } } public sealed class EnsureConstructionCapacityGoal : GoapGoal { private const int MinConstructors = 1; public override string Name => "ensure-construction-capacity"; public override bool IsSatisfied(FactionPlanningState state) => state.ConstructorShipCount >= MinConstructors; public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { var deficit = MinConstructors - state.ConstructorShipCount; return deficit <= 0 ? 0f : 60f + (deficit * 10f); } } // ─── Actions ─────────────────────────────────────────────────────────────────── public sealed class OrderShipProductionAction : GoapAction { private readonly string shipKind; private readonly string shipId; public OrderShipProductionAction(string shipKind, string shipId) { this.shipKind = shipKind; this.shipId = shipId; } public override string Name => $"order-{shipId}-production"; public override float Cost => 1f; public override bool CheckPreconditions(FactionPlanningState state) => state.HasShipFactory && state.HasWarIndustrySupplyChain; public override FactionPlanningState ApplyEffects(FactionPlanningState state) { switch (shipKind) { case "military": state.MilitaryShipCount++; break; case "mining": state.MinerShipCount++; break; case "transport": state.TransportShipCount++; break; case "construction": state.ConstructorShipCount++; break; } return state; } public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { commander.ActiveDirectives.Add($"produce-{shipKind}-ships"); } } public sealed class PlanWarIndustryAction : GoapAction { public override string Name => "plan-war-industry"; public override float Cost => 2f; public override bool CheckPreconditions(FactionPlanningState state) => state.EnemyFactionCount > 0 && (!state.HasWarIndustrySupplyChain || !state.HasShipFactory); public override FactionPlanningState ApplyEffects(FactionPlanningState state) { state.RefinedMetalsProductionRate = MathF.Max(state.RefinedMetalsProductionRate, 1f); state.HullpartsProductionRate = MathF.Max(state.HullpartsProductionRate, 1f); state.ClaytronicsProductionRate = MathF.Max(state.ClaytronicsProductionRate, 1f); state.HasShipFactory = true; return state; } public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { commander.ActiveDirectives.Add("bootstrap-war-industry"); if (FactionIndustryPlanner.AnalyzeShipyardNeed(world, commander.FactionId) is not { } project) { return; } FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project); commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}"); } } public sealed class PlanCommoditySupplyAction : GoapAction { private readonly string commodityId; public PlanCommoditySupplyAction(string commodityId) { this.commodityId = commodityId; } public override string Name => $"plan-{commodityId}-supply"; public override float Cost => 2f; public override bool CheckPreconditions(FactionPlanningState state) => commodityId switch { "water" => !state.HasWaterProduction || state.WaterShortageHorizonSeconds < 300f, _ => false, }; public override FactionPlanningState ApplyEffects(FactionPlanningState state) { if (string.Equals(commodityId, "water", StringComparison.Ordinal)) { state.WaterProductionRate = MathF.Max(state.WaterProductionRate, 1f); state.WaterShortageHorizonSeconds = MathF.Max(state.WaterShortageHorizonSeconds, 600f); } return state; } public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { if (FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, commodityId) is not { } project) { return; } FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project); commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}"); } } public sealed class ExpandToSystemAction : GoapAction { public override string Name => "expand-to-system"; public override float Cost => 3f; public override bool CheckPreconditions(FactionPlanningState state) => state.ConstructorShipCount > 0 && state.MilitaryShipCount >= 2 && state.HasWarIndustrySupplyChain; public override FactionPlanningState ApplyEffects(FactionPlanningState state) { state.ControlledSystemCount++; return state; } public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { commander.ActiveDirectives.Add("expand-territory"); } } public sealed class LaunchExterminationCampaignAction : GoapAction { public override string Name => "launch-extermination-campaign"; public override float Cost => 1f; public override bool CheckPreconditions(FactionPlanningState state) => state.EnemyFactionCount > 0 && state.HasShipFactory && state.MilitaryShipCount >= Math.Max(2, FactionPlanningState.ComputeTargetWarships(state) / 2); public override FactionPlanningState ApplyEffects(FactionPlanningState state) { state.EnemyShipCount = 0; state.EnemyStationCount = 0; state.EnemyFactionCount = 0; return state; } public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander) { commander.ActiveDirectives.Add("attack-rival"); commander.ActiveDirectives.Add("produce-military-ships"); } }