using System; using System.Collections.Generic; using System.Linq; using CounterDrone.Core.Models; namespace CounterDrone.Core.Algorithms { /// 默认防御规划器 — 五步流水线,全部使用真实物理计算 public class DefaultDefensePlanner : IDefensePlanner { private readonly List _ammoCatalog; public DefaultDefensePlanner(List ammoCatalog) { _ammoCatalog = ammoCatalog ?? new List(); } private static readonly Dictionary MatchTable = new() { { PowerType.Electric, AerosolType.InertGas }, { PowerType.Piston, AerosolType.InertGas }, { PowerType.Jet, AerosolType.ActiveMaterial }, }; private static readonly Dictionary TypeCoefficient = new() { { TargetType.HighSpeed, 4f }, { TargetType.FixedWing, 2f }, { TargetType.Piston, 2f }, { TargetType.Rotor, 1f }, { TargetType.Electric, 1f }, }; // ═══════════════════════════════════════════════ // 五步流水线 // ═══════════════════════════════════════════════ public PlannerResult Plan(List fireUnits, List threats, CombatScene environment) { var result = new PlannerResult(); if (threats.Count == 0) { result.Best.Summary = "无威胁目标"; return result; } if (fireUnits.Count == 0) { result.Best.Summary = "无可用火力单元"; result.Best.ThreatsUnengaged = threats.Count; return result; } // Step 1: 威胁排序 foreach (var t in threats) { t.ArrivalTime = t.GetArrivalTime(); t.ThreatIndex = CalcThreatIndex(t.Target); } var sorted = threats.OrderByDescending(t => t.Priority).ToList(); // Step 2-4: 贪心分配求解 result.Best = Solve(sorted, fireUnits, environment); // Step 5: 临界方案 result.Critical = DeriveCritical(result.Best); return result; } // ═══════════════════════════════════════════════ // Step 1: 威胁指数 // ═══════════════════════════════════════════════ private static float CalcThreatIndex(TargetConfig target) { float typeCoef = TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f); float speedCoef = (float)target.TypicalSpeed / 60f; return typeCoef * speedCoef; } // ═══════════════════════════════════════════════ // Step 2: 弹药匹配 // ═══════════════════════════════════════════════ private static AerosolType MatchAmmo(PowerType power) { return MatchTable.GetValueOrDefault(power, AerosolType.InertGas); } // ═══════════════════════════════════════════════ // Step 3-4: 候选生成 → 贪心分配 // ═══════════════════════════════════════════════ private DefensePlan Solve(List sortedThreats, List fireUnits, CombatScene env) { var plan = new DefensePlan(); var remainingMunitions = new Dictionary(); foreach (var u in fireUnits) remainingMunitions[u.Id] = u.TotalMunitions; foreach (var threat in sortedThreats) { var available = fireUnits .Where(u => remainingMunitions.GetValueOrDefault(u.Id, 0) > 0) .ToList(); var candidates = GenerateCandidates(threat, available, env); if (candidates.Count == 0) { plan.ThreatsUnengaged++; continue; } // 计算总弹药需求 var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType); var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)neededAmmo); int totalRoundsNeeded = CalcRoundsNeeded(threat, ammo, env, candidates[0].Unit.Type == PlatformType.AirBased); // 从多个候选单元凑弹药,直到满足需求或耗尽 int roundsCollected = 0; var assignedUnits = new List<(FireUnit unit, int rounds, List events)>(); foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime) .ThenByDescending(c => c.KillProbability)) { if (roundsCollected >= totalRoundsNeeded) break; int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0); if (remaining <= 0) continue; int toTake = Math.Min(remaining, totalRoundsNeeded - roundsCollected); var fireEvents = GenerateFireEvents(threat, c.Unit, c.AmmoType, toTake, ammo, env); assignedUnits.Add((c.Unit, toTake, fireEvents)); remainingMunitions[c.Unit.Id] -= toTake; roundsCollected += toTake; } if (roundsCollected <= 0) { plan.ThreatsUnengaged++; continue; } foreach (var (unit, rounds, fireEvents) in assignedUnits) { plan.Assignments.Add(new UnitAssignment { FireUnitId = unit.Id, DroneGroupId = threat.GroupId, AmmoType = neededAmmo, RoundsFired = rounds, FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0, FireEvents = fireEvents, }); plan.MergedSchedule.AddRange(fireEvents); } plan.ThreatsEngaged++; } plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime)); // 按威胁汇总概率 var threatProbs = new List(); foreach (var threat in sortedThreats) { var a2 = _ammoCatalog.FirstOrDefault(s => s.AerosolType == (int)MatchAmmo((PowerType)threat.Target.PowerType)); int totalRounds = plan.Assignments .Where(a => a.DroneGroupId == threat.GroupId) .Sum(a => a.RoundsFired); if (totalRounds > 0) threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env)); } plan.OverallProbability = threatProbs.Count > 0 ? threatProbs.Average() : 0f; plan.Summary = plan.ThreatsEngaged > 0 ? $"分配 {plan.ThreatsEngaged} 个威胁,{plan.ThreatsUnengaged} 个无方案" : "无威胁被分配拦截方案"; return plan; } // ═══════════════════════════════════════════════ // 候选生成(使用真实物理) // ═══════════════════════════════════════════════ private List GenerateCandidates(DroneGroup threat, List availableUnits, CombatScene env) { var candidates = new List(); var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType); var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)neededAmmo); if (ammo == null) return candidates; var ammoEff = ComputeEffectiveRadius(threat, ammo, env); foreach (var unit in availableUnits) { if (!unit.AmmoTypes.Contains(neededAmmo)) continue; if (unit.Type == PlatformType.AirBased) { var c = BuildAirBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, env); if (c != null) candidates.Add(c); } else { var c = BuildGroundBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, env); if (c != null) candidates.Add(c); } } return candidates; } private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo, (float radius, float expTime) ammoEff, CombatScene env) { var mid = ThreatMidpoint(threat); float dx = mid.X - unit.Position.X; float dz = mid.Z - unit.Position.Z; float dist = (float)Math.Sqrt(dx * dx + dz * dz); float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f; if (dist > maxRange * 0.8f) return null; float shellTime = dist / unit.MuzzleVelocity; float interceptTime = threat.ArrivalTime; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f; float actualExposure = ammoEff.radius * 2f / avgSpeed; float prob = Math.Min(0.95f, actualExposure / neededExposure); return new InterceptCandidate { Unit = unit, AmmoType = ammoType, EarliestInterceptTime = interceptTime, CoverageDuration = ammoEff.radius * 2f, KillProbability = prob, FlightTime = shellTime, ShellFlightTime = shellTime, }; } private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo, (float radius, float expTime) ammoEff, CombatScene env) { var mid = ThreatMidpoint(threat); float releaseAlt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : (float)threat.Target.TypicalAltitude + 500f; float flightDist = Kinematics.Distance3D( unit.Position.X, unit.Position.Y, unit.Position.Z, mid.X, releaseAlt, mid.Z); float cruiseSpd = unit.CruiseSpeed > 0.1f ? unit.CruiseSpeed : 55f; float flightTime = flightDist / cruiseSpd; float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude); float totalTime = flightTime + fallTime; float interceptTime = threat.ArrivalTime; if (totalTime > interceptTime * 1.5f) return null; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f; float actualExposure = ammoEff.radius * 2f / avgSpeed; float prob = Math.Min(0.95f, actualExposure / neededExposure); return new InterceptCandidate { Unit = unit, AmmoType = ammoType, EarliestInterceptTime = interceptTime, CoverageDuration = ammoEff.radius * 2f, KillProbability = prob, FlightTime = totalTime, ShellFlightTime = fallTime, }; } // ═══════════════════════════════════════════════ // 弹药计算(使用 AmmunitionSpec + 环境参数) // ═══════════════════════════════════════════════ private (float effectiveRadius, float expansionTime) ComputeEffectiveRadius( DroneGroup threat, AmmunitionSpec ammo, CombatScene env) { float totalFlightTime = threat.ArrivalTime * 2f; float halfTime = totalFlightTime / 2f; float windSpeed = (float)env.WindSpeed; var weather = (WeatherType)env.WeatherType; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; // Phase 1 初始半径(基于爆发药 TNT 当量) float r0 = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)ammo.BurstChargeKg), 0.32); float k = (float)ammo.TurbulentExpansionK; float maxDur = (float)ammo.MaxDuration; // Phase 2 湍流膨胀后的半径 float phase2Time = Math.Min(halfTime, 30f); float rPhase2 = r0 + k * (float)Math.Sqrt(Math.Max(0, phase2Time)); float expansionTime = (float)Math.Pow((rPhase2 - r0) / Math.Max(k, 0.01f), 2); float effectiveR = rPhase2; // Phase 3 高斯扩散 if (halfTime > 30f) { float x = Math.Max(1f, windSpeed * (halfTime - 30f)); var cls = Kinematics.GetStabilityClass(weather, windSpeed); float sY = Kinematics.SigmaY(cls, x); float sZ = Kinematics.SigmaZ(cls, x); float peakC = Kinematics.GaussianPeakConcentration((float)ammo.SourceStrength, sY, sZ); float threshold = (float)ammo.EffectiveConcentration; if (peakC > threshold) { float sigma = (sY + sZ) / 2f; effectiveR = rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold)); } } return (effectiveR, expansionTime); } private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec? ammo, CombatScene env, bool isAirBased) { if (ammo == null) return 1; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env); float spacing = effectiveR * 1.5f; var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; float requiredCoverage = neededExposure * avgSpeed; return Math.Max(1, (int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1); } private float ComputeInterceptProbability(DroneGroup threat, AmmunitionSpec? ammo, int rounds, CombatScene env) { if (ammo == null) return 0f; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env); float spacing = effectiveR * 1.5f; float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR; float actualExposure = actualCoverage / avgSpeed; var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; return Math.Min(0.95f, actualExposure / neededExposure); } // ═══════════════════════════════════════════════ // 发射事件生成(真实物理) // ═══════════════════════════════════════════════ private List GenerateFireEvents(DroneGroup threat, FireUnit unit, AerosolType ammoType, int rounds, AmmunitionSpec? ammo, CombatScene env) { var events = new List(); if (ammo == null) return events; var mid = ThreatMidpoint(threat); var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env); float spacing = effectiveR * 1.5f; // 推荐时机:威胁到达中点时云团应已形成 float recommendedTiming = threat.ArrivalTime - expansionTime; if (recommendedTiming < 0.1f) recommendedTiming = 0.1f; for (int i = 0; i < rounds; i++) { float offset = (i - (rounds - 1) / 2f) * spacing; float tx = mid.X + offset; float tz = mid.Z; float fireTime; if (unit.Type == PlatformType.AirBased) { float releaseAlt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : (float)threat.Target.TypicalAltitude + 500f; float flightDist = Kinematics.Distance3D( unit.Position.X, unit.Position.Y, unit.Position.Z, tx, releaseAlt, tz); float cruiseSpd = unit.CruiseSpeed > 0.1f ? unit.CruiseSpeed : 55f; float flightTime = flightDist / cruiseSpd; float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude); fireTime = recommendedTiming - flightTime - fallTime; } else { float dx = tx - unit.Position.X; float dz = tz - unit.Position.Z; float dist = (float)Math.Sqrt(dx * dx + dz * dz); float shellTime = dist / (unit.MuzzleVelocity > 0.1f ? unit.MuzzleVelocity : 800f); fireTime = recommendedTiming - shellTime; } if (fireTime < 0.1f) fireTime = 0.1f; events.Add(new FireEvent { FireTime = fireTime, PlatformIndex = i % rounds, TargetX = tx, TargetY = mid.Y, TargetZ = tz, MuzzleVelocity = unit.MuzzleVelocity, }); } return events; } // ═══════════════════════════════════════════════ // Step 5: 临界方案(概率阈值 50%) // ═══════════════════════════════════════════════ private DefensePlan DeriveCritical(DefensePlan best) { var critical = new DefensePlan { ThreatsEngaged = best.ThreatsEngaged, ThreatsUnengaged = best.ThreatsUnengaged, }; foreach (var assignment in best.Assignments) { int rounds = assignment.RoundsFired; while (rounds > 1) { // 简化:弹药减半 → 概率减半 if ((float)rounds / assignment.RoundsFired < 0.5f) break; rounds--; } var reduced = new UnitAssignment { FireUnitId = assignment.FireUnitId, DroneGroupId = assignment.DroneGroupId, AmmoType = assignment.AmmoType, RoundsFired = rounds, FirstFireTime = assignment.FirstFireTime, FireEvents = assignment.FireEvents.Take(rounds).ToList(), }; critical.Assignments.Add(reduced); critical.MergedSchedule.AddRange(reduced.FireEvents); } critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime)); critical.OverallProbability = 0.5f; critical.Summary = $"临界方案:刚好满足 50% 拦截概率"; return critical; } // ═══════════════════════════════════════════════ // 辅助 // ═══════════════════════════════════════════════ private static Vector3 ThreatMidpoint(DroneGroup threat) { if (threat.Waypoints.Count < 2) return new Vector3(0, (float)threat.Target.TypicalAltitude, 0); var s = threat.Waypoints[0]; var e = threat.Waypoints[^1]; return new Vector3( (float)(s.PosX + e.PosX) / 2f, (float)(s.PosY + e.PosY) / 2f, (float)(s.PosZ + e.PosZ) / 2f); } } }