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; private readonly IDamageModel _damageModel; public DefaultDefensePlanner(List ammoCatalog, IDamageModel damageModel = null) { _ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog)); _damageModel = damageModel ?? new DamageModelRouter(); if (_ammoCatalog.Count == 0) throw new ArgumentException("弹药规格目录不能为空"); } 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: 贪心分配求解 // 预先检查:所有需要的弹药类型都在目录中 var neededTypes = sorted .Select(t => MatchAmmo((PowerType)t.Target.PowerType)) .Distinct() .ToList(); foreach (var t in neededTypes) { if (!_ammoCatalog.Any(a => a.AerosolType == (int)t)) throw new InvalidOperationException($"弹药规格目录中缺少类型: {t}"); } 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.First(a => a.AerosolType == (int)neededAmmo); // 单机需求 var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(threat, ammo, env); int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius); // 横向编队:每种 Width = Quantity 个独立车道 int yLanes = 1; float formationWidth = 0f; if (threat.Target.Quantity > 1 && threat.Route != null && (FormationMode)threat.Route.FormationMode == FormationMode.Formation) { yLanes = threat.Target.Quantity; formationWidth = (threat.Target.Quantity - 1) * (float)threat.Route.LateralSpacing; } int totalRoundsNeeded = singleNeeded * yLanes; // 逐单元分配:每个单元锁定一个 Y 车道 float spacing = 2f * turbulentRadius; int[] laneNeeded = new int[yLanes]; float[] laneBaseTime = new float[yLanes]; bool[] laneBaseSet = new bool[yLanes]; for (int l = 0; l < yLanes; l++) laneNeeded[l] = singleNeeded; int currentLane = 0; int roundsCollected = 0; int eventIdx = 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; while (currentLane < yLanes && laneNeeded[currentLane] <= 0) currentLane++; if (currentLane >= yLanes) break; int toTake = Math.Min(Math.Min(c.Unit.TotalChannels, remaining), laneNeeded[currentLane]); if (toTake <= 0) continue; int yLane = currentLane; var mid = ThreatMidpoint(threat); // 车道第一个单元设基准时间,后续单元以此为准保证间距均匀 if (!laneBaseSet[yLane]) { var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth); laneBaseTime[yLane] = refEvt[0].FireTime; laneBaseSet[yLane] = true; } float stagger = Kinematics.CloudCoverInterval(turbulentRadius * 2f, (float)threat.Target.TypicalSpeed, c.Unit.ChannelInterval); var fevents = new List(); int unitIdx = fireUnits.IndexOf(c.Unit); for (int ch = 0; ch < toTake; ch++) { float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing; var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth); int baseRoundInLane = singleNeeded - laneNeeded[currentLane]; foreach (var e in fe) { e.FireTime = laneBaseTime[yLane] + (baseRoundInLane + ch) * stagger; e.TargetX = mid.X + offset; e.PlatformIndex = unitIdx * c.Unit.TotalChannels + ch; } fevents.AddRange(fe); eventIdx++; } assignedUnits.Add((c.Unit, toTake, fevents)); remainingMunitions[c.Unit.Id] -= toTake; laneNeeded[currentLane] -= 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.First(a => a.AerosolType == (int)neededAmmo); 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 effectiveR, 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); if (unit.MuzzleVelocity <= 0) return null; float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f; if (dist > maxRange) return null; float shellTime = dist / unit.MuzzleVelocity; float interceptTime = threat.ArrivalTime; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType); float actualExposure = effectiveR * 2f / avgSpeed; float prob = Math.Min(0.95f, actualExposure / neededExposure); return new InterceptCandidate { Unit = unit, AmmoType = ammoType, EarliestInterceptTime = interceptTime, CoverageDuration = effectiveR * 2f, KillProbability = prob, FlightTime = shellTime, ShellFlightTime = shellTime, }; } private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo, float effectiveR, CombatScene env) { if (unit.ReleaseAltitude <= 0 || unit.CruiseSpeed <= 0) return null; var mid = ThreatMidpoint(threat); float releaseAlt = unit.ReleaseAltitude; float flightDist = Kinematics.Distance3D( unit.Position.X, unit.Position.Y, unit.Position.Z, mid.X, releaseAlt, mid.Z); float flightTime = flightDist / unit.CruiseSpeed; float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude); float totalTime = flightTime + fallTime; float interceptTime = threat.ArrivalTime; if (totalTime > interceptTime) return null; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType); float actualExposure = effectiveR * 2f / avgSpeed; float prob = Math.Min(0.95f, actualExposure / neededExposure); return new InterceptCandidate { Unit = unit, AmmoType = ammoType, EarliestInterceptTime = interceptTime, CoverageDuration = effectiveR * 2f, KillProbability = prob, FlightTime = totalTime, ShellFlightTime = fallTime, }; } // ═══════════════════════════════════════════════ // 弹药计算(使用 AmmunitionSpec + 环境参数) // ═══════════════════════════════════════════════ private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius( DroneGroup threat, AmmunitionSpec ammo, CombatScene env) { var model = new CloudExpansionModel(ammo, env); float tPhase2 = 30f; float rPhase2 = model.RadiusAt(tPhase2); float expansionTime = model.TimeToReach(rPhase2); float halfTime = threat.ArrivalTime * 2f; // 总飞行时间的一半 float effectiveR = model.RadiusAt(halfTime); return (effectiveR, expansionTime, rPhase2); } private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec ammo, CombatScene env, bool isAirBased, float turbulentRadius) { var cloudModel = new CloudExpansionModel(ammo, env); float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = _damageModel.RequiredExposureSeconds( (TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType); float requiredCoverage = neededExposure * avgSpeed; return cloudModel.RoundsNeeded(requiredCoverage); } private float ComputeInterceptProbability(DroneGroup threat, AmmunitionSpec ammo, int rounds, CombatScene env) { float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; var (effectiveR, _, _) = ComputeEffectiveRadius(threat, ammo, env); float spacing = 2f * effectiveR; float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR; float actualExposure = actualCoverage / avgSpeed; var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType); return Math.Min(0.95f, actualExposure / neededExposure); } // ═══════════════════════════════════════════════ // 发射事件生成(真实物理) // ═══════════════════════════════════════════════ private List GenerateFireEventsAt(DroneGroup threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset, int yLane, int yLanes, float formationWidth) { var events = new List(); var mid = ThreatMidpoint(threat); var cloudModel = new CloudExpansionModel(ammo, env); // 云龄 = expansionTime,expansionTime = TimeToReach(RadiusAt(30s)) ≈ 30s // 考虑弹间间隔,最后云的龄 = expansionTime - (rounds-1)*stagger // 用保守值:取 expansionTime 的 90%(实际间距导致龄差 ~3s) float effectiveAge = cloudModel.TimeToReach(cloudModel.TurbulentRadius) * 0.9f; float effectiveR = cloudModel.RadiusAt(effectiveAge); float expansionTime = cloudModel.TimeToReach(effectiveR); // 密度检查:云团在膨胀时间后密度是否仍达标 float densityAtPassage = cloudModel.DensityAt(expansionTime); if (densityAtPassage < (float)ammo.EffectiveConcentration) return events; // 云团到达时已稀释失效 // 编队 Y 偏移:按车道分布 float laneSpacingZ = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f; float tz = mid.Z + yLane * laneSpacingZ; float tx = mid.X + targetOffset; // 无人机到达目标点时间 float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f; if (droneSpeed <= 0) return events; float startX = (float)threat.Waypoints[0].PosX; float startZ = (float)threat.Waypoints[0].PosZ; float endX = (float)threat.Waypoints[^1].PosX; float endZ = (float)threat.Waypoints[^1].PosZ; float totalDist = Kinematics.Distance2D(startX, startZ, endX, endZ); if (totalDist <= 0) return events; float distToTx = Kinematics.Distance2D(startX, startZ, tx, tz); distToTx = Math.Max(0, Math.Min(totalDist, distToTx)); float txArrival = distToTx / droneSpeed; float recommendedTiming = txArrival - expansionTime; if (recommendedTiming <= 0f) return events; float fireTime; if (unit.Type == PlatformType.AirBased) { if (unit.ReleaseAltitude <= 0 || unit.CruiseSpeed <= 0) throw new InvalidOperationException($"空基单元 {unit.Id}: ReleaseAltitude={unit.ReleaseAltitude}, CruiseSpeed={unit.CruiseSpeed} 必须>0"); float releaseAlt = unit.ReleaseAltitude; float cruiseSpd = unit.CruiseSpeed; float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude); // 载具飞向云端方向,漂移距离 = 巡航速度 × 落体时间 float driftDist = cruiseSpd * fallTime; float distToCloud = Kinematics.Distance3D( unit.Position.X, unit.Position.Y, unit.Position.Z, tx, releaseAlt, tz); // 投放点间距 = 总距离 - 漂移,投后弹药滑翔至预期云位 float flightDist = Math.Max(0f, distToCloud - driftDist); float flightTime = flightDist / cruiseSpd; fireTime = recommendedTiming - flightTime - fallTime; } else { if (unit.MuzzleVelocity <= 0) throw new InvalidOperationException($"地基单元 {unit.Id}: MuzzleVelocity={unit.MuzzleVelocity} 必须>0"); float mv = unit.MuzzleVelocity; float dx = tx - unit.Position.X; float dz = tz - unit.Position.Z; float dist = (float)Math.Sqrt(dx * dx + dz * dz); float heightDiff = (float)threat.Target.TypicalAltitude - unit.Position.Y; float shellTime = Kinematics.ParabolicShellTime(dist, heightDiff, mv); fireTime = recommendedTiming - shellTime; } if (fireTime <= 0f) return events; // 平台来不及到达 events.Add(new FireEvent { FireTime = fireTime, PlatformIndex = 0, TargetX = tx, TargetY = mid.Y, TargetZ = tz, MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : 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); } } }