diff --git a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs index 6d7beb2..daa7a74 100644 --- a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs +++ b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs @@ -33,8 +33,52 @@ namespace CounterDrone.Core.Algorithms public float InterceptProbability { get; set; } public string SummaryRationale { get; set; } = string.Empty; - /// 由推荐方案生成的发射计划,仿真器直接执行 public List FireSchedule { get; set; } = new(); + + /// 此方案占用的平台起始索引(多编队合并用) + public int PlatformOffset { get; set; } + } + + /// 多编队推荐结果 + public class MultiGroupRecommendation + { + /// 每组的独立方案 + public List GroupSolutions { get; set; } = new(); + /// 合并后的总发射计划 + public List MergedFireSchedule { get; set; } = new(); + } + + /// 火力单元(装备编组) + public class FireUnit + { + public string GroupId { get; set; } = string.Empty; + public AerosolType LoadedAmmo { get; set; } + public int PlatformCount { get; set; } + public float PositionX, PositionY, PositionZ; + public float MuzzleVelocity { get; set; } = 800f; + public float Cooldown { get; set; } = 5f; + public float AmmoChangeTime { get; set; } = 300f; + // 弹药库:可以装填的弹药类型 + public List AvailableAmmo { get; set; } = new(); + } + + /// 无人机编队(多编队推荐输入) + public class DroneGroup + { + public string GroupId { get; set; } = string.Empty; + public TargetConfig Target { get; set; } = new(); + public RoutePlan Route { get; set; } = new(); + public List Waypoints { get; set; } = new(); + + /// 预计到达航路中点的时间(秒) + public float GetArrivalTime() + { + if (Waypoints.Count < 2) return 0; + var start = new Vector3((float)Waypoints[0].PosX, (float)Waypoints[0].PosY, (float)Waypoints[0].PosZ); + var end = new Vector3((float)Waypoints[^1].PosX, (float)Waypoints[^1].PosY, (float)Waypoints[^1].PosZ); + var speed = (float)Target.TypicalSpeed / 3.6f; + return start.DistanceTo(end) / speed / 2f; + } } public class RecommendedPlatform diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs b/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs index 93e5b3b..177234c 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs @@ -219,5 +219,62 @@ namespace CounterDrone.Core.Algorithms var sigma = (sY + sZ) / 2f; return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold)); } + + /// 多编队推荐 — 每组独立方案,按时间分配火力单元,合并发射计划 + public MultiGroupRecommendation RecommendMultiGroup( + List droneGroups, List fireUnits, CombatScene environment) + { + var result = new MultiGroupRecommendation(); + if (droneGroups.Count == 0 || fireUnits.Count == 0) return result; + + var sorted = droneGroups.OrderBy(g => g.GetArrivalTime()).ToList(); + var usedUntil = new Dictionary(); + int offset = 0; + + foreach (var group in sorted) + { + var powerType = (PowerType)group.Target.PowerType; + var neededAmmo = MatchTable.GetValueOrDefault(powerType, AerosolType.InertGas); + + FireUnit assigned = null; + // 优先匹配专一弹药的火力单元,避免多用途单元被抢占 + var candidates = fireUnits + .Where(u => u.AvailableAmmo.Contains(neededAmmo)) + .OrderBy(u => u.AvailableAmmo.Count) // 少弹种的优先 + .ThenBy(u => usedUntil.ContainsKey(u.GroupId) ? 1 : 0); // 不忙的优先 + + foreach (var unit in candidates) + { + if (!unit.AvailableAmmo.Contains(neededAmmo)) continue; + if (!usedUntil.TryGetValue(unit.GroupId, out var busyUntil)) + { assigned = unit; break; } + if (unit.LoadedAmmo == neededAmmo && busyUntil <= group.GetArrivalTime()) + { assigned = unit; break; } + if (busyUntil + unit.AmmoChangeTime <= group.GetArrivalTime()) + { assigned = unit; break; } + } + if (assigned == null) continue; + + var threat = new ThreatProfile + { + Environment = environment, + Targets = new List { group.Target }, + Route = group.Route, + Waypoints = group.Waypoints, + }; + var sol = Recommend(threat).Best; + foreach (var fe in sol.FireSchedule) fe.PlatformIndex += offset; + + result.GroupSolutions.Add(sol); + result.MergedFireSchedule.AddRange(sol.FireSchedule); + + assigned.LoadedAmmo = sol.RecommendedAerosolType; + usedUntil[assigned.GroupId] = (sol.FireSchedule.Count > 0 + ? sol.FireSchedule.Max(f => f.FireTime) : group.GetArrivalTime()) + assigned.Cooldown; + offset += sol.Platforms.Count; + } + + return result; + } } } diff --git a/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs b/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs index 7e31ed4..5ed3aab 100644 --- a/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs +++ b/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using CounterDrone.Core.Models; namespace CounterDrone.Core.Algorithms @@ -6,5 +7,11 @@ namespace CounterDrone.Core.Algorithms public interface IDefenseAdvisor { DefenseRecommendation Recommend(ThreatProfile threat); + + /// 多编队推荐 — 为每组分配火力单元,合并发射计划 + MultiGroupRecommendation RecommendMultiGroup( + List droneGroups, + List fireUnits, + CombatScene environment); } } diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll index fef6097..8bf3c8d 100644 Binary files a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll and b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll differ diff --git a/test/unit/CounterDrone.Core.Tests/MultiGroupAdvisorTests.cs b/test/unit/CounterDrone.Core.Tests/MultiGroupAdvisorTests.cs new file mode 100644 index 0000000..0d78a77 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/MultiGroupAdvisorTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using CounterDrone.Core.Algorithms; +using CounterDrone.Core.Models; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class MultiGroupAdvisorTests + { + [Fact] + public void TwoSeparateUnits_TwoGroups_BothGetSolutions() + { + var ammoCatalog = DefaultAmmunition.GetAll(); + Console.WriteLine($"Ammo: inert={ammoCatalog[0].BurstChargeKg}kg, active={ammoCatalog[1].BurstChargeKg}kg"); + + var advisor = new DefaultDefenseAdvisor(ammoCatalog); + + var g1 = new DroneGroup + { + GroupId = "g1", Target = new TargetConfig { PowerType = (int)PowerType.Piston, Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500 }, + Route = new RoutePlan { FormationMode = (int)FormationMode.Single }, + Waypoints = new List { new() { PosX = 0, PosY = 500, PosZ = 0 }, new() { PosX = 20000, PosY = 500, PosZ = 0 } }, + }; + var g2 = new DroneGroup + { + GroupId = "g2", Target = new TargetConfig { PowerType = (int)PowerType.Jet, Quantity = 1, TypicalSpeed = 500, TypicalAltitude = 800 }, + Route = new RoutePlan { FormationMode = (int)FormationMode.Single }, + Waypoints = new List { new() { PosX = 0, PosY = 800, PosZ = 0 }, new() { PosX = 30000, PosY = 800, PosZ = 0 } }, + }; + + var u1 = new FireUnit { GroupId = "u1", AvailableAmmo = new() { AerosolType.InertGas, AerosolType.ActiveMaterial }, PlatformCount = 5, MuzzleVelocity = 800 }; + var u2 = new FireUnit { GroupId = "u2", AvailableAmmo = new() { AerosolType.ActiveMaterial }, PlatformCount = 4, MuzzleVelocity = 800 }; + + var rec = advisor.RecommendMultiGroup( + new List { g1, g2 }, + new List { u1, u2 }, + new CombatScene { WindSpeed = 3 }); + + var msg = $"Solutions={rec.GroupSolutions.Count}, Merged={rec.MergedFireSchedule.Count}"; + foreach (var s in rec.GroupSolutions) + msg += $" | {s.RecommendedAerosolType}: plat={s.Platforms.Count} fire={s.FireSchedule.Count}"; + Assert.True(rec.GroupSolutions.Count == 2, msg); + Assert.NotEmpty(rec.MergedFireSchedule); + } + } +}