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);
+ }
+ }
+}