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 RefinedMetalsAvailableStock { get; set; } public float RefinedMetalsUsageRate { get; set; } public float RefinedMetalsProjectedProductionRate { get; set; } public float RefinedMetalsProjectedNetRate { get; set; } public float RefinedMetalsLevelSeconds { get; set; } public string RefinedMetalsLevel { get; set; } = "unknown"; public float HullpartsAvailableStock { get; set; } public float HullpartsUsageRate { get; set; } public float HullpartsProjectedProductionRate { get; set; } public float HullpartsProjectedNetRate { get; set; } public float HullpartsLevelSeconds { get; set; } public string HullpartsLevel { get; set; } = "unknown"; public float ClaytronicsAvailableStock { get; set; } public float ClaytronicsUsageRate { get; set; } public float ClaytronicsProjectedProductionRate { get; set; } public float ClaytronicsProjectedNetRate { get; set; } public float ClaytronicsLevelSeconds { get; set; } public string ClaytronicsLevel { get; set; } = "unknown"; public float WaterAvailableStock { get; set; } public float WaterUsageRate { get; set; } public float WaterProjectedProductionRate { get; set; } public float WaterProjectedNetRate { get; set; } public float WaterLevelSeconds { get; set; } public string WaterLevel { get; set; } = "unknown"; public bool HasRefinedMetalsProduction => RefinedMetalsProjectedProductionRate > 0.01f; public bool HasHullpartsProduction => HullpartsProjectedProductionRate > 0.01f; public bool HasClaytronicsProduction => ClaytronicsProjectedProductionRate > 0.01f; public bool HasWaterProduction => WaterProjectedProductionRate > 0.01f; public bool HasWarIndustrySupplyChain => IsCommodityOperational(RefinedMetalsProjectedProductionRate, RefinedMetalsProjectedNetRate, RefinedMetalsLevelSeconds, RefinedMetalsLevel, 240f) && IsCommodityOperational(HullpartsProjectedProductionRate, HullpartsProjectedNetRate, HullpartsLevelSeconds, HullpartsLevel, 240f) && IsCommodityOperational(ClaytronicsProjectedProductionRate, ClaytronicsProjectedNetRate, ClaytronicsLevelSeconds, ClaytronicsLevel, 240f); 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)); } internal static bool IsCommodityOperational( float projectedProductionRate, float projectedNetRate, float levelSeconds, string level, float targetLevelSeconds) => projectedProductionRate > 0.01f && projectedNetRate >= -0.01f && levelSeconds >= targetLevelSeconds && (string.Equals(level, "stable", StringComparison.OrdinalIgnoreCase) || string.Equals(level, "surplus", StringComparison.OrdinalIgnoreCase)); internal static float ComputeCommodityNeed( float projectedProductionRate, float usageRate, float projectedNetRate, float levelSeconds, string level, float targetLevelSeconds) { var levelWeight = level switch { "critical" => 140f, "low" => 80f, "stable" => 20f, _ => 0f, }; var rateDeficit = MathF.Max(0f, usageRate - projectedProductionRate); var levelDeficit = MathF.Max(0f, targetLevelSeconds - levelSeconds) / MathF.Max(targetLevelSeconds, 1f); var instability = projectedNetRate < 0f ? MathF.Abs(projectedNetRate) * 80f : 0f; return levelWeight + (rateDeficit * 140f) + (levelDeficit * 120f) + instability; } } // ─── 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 = (FactionPlanningState.IsCommodityOperational(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f) ? 0 : 1) + (FactionPlanningState.IsCommodityOperational(state.HullpartsProjectedProductionRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f) ? 0 : 1) + (FactionPlanningState.IsCommodityOperational(state.ClaytronicsProjectedProductionRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f) ? 0 : 1) + (state.HasShipFactory ? 0 : 1); var supplyNeed = FactionPlanningState.ComputeCommodityNeed(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsUsageRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f) + FactionPlanningState.ComputeCommodityNeed(state.HullpartsProjectedProductionRate, state.HullpartsUsageRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f) + FactionPlanningState.ComputeCommodityNeed(state.ClaytronicsProjectedProductionRate, state.ClaytronicsUsageRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f); return missingStages <= 0 && supplyNeed <= 0.01f ? 0f : 110f + (missingStages * 22f) + (supplyNeed * 0.18f); } } public sealed class EnsureWaterSecurityGoal : GoapGoal { public override string Name => "ensure-water-security"; public override bool IsSatisfied(FactionPlanningState state) => FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f); public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander) { if (FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f)) { return 0f; } return 55f + FactionPlanningState.ComputeCommodityNeed( state.WaterProjectedProductionRate, state.WaterUsageRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f) * 0.25f; } } 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); } }