diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index ee43d27..daf78d5 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -173,6 +173,7 @@ namespace CounterDrone.Core.Algorithms int yLane = currentLane; var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth); float unitBaseTime = refEvt[0].FireTime; + float stagger = Kinematics.CloudCoverInterval(effR2 * 2f, (float)threat.Target.TypicalSpeed, c.Unit.ChannelInterval); var fevents = new List(); for (int ch = 0; ch < toTake; ch++) @@ -180,7 +181,7 @@ namespace CounterDrone.Core.Algorithms float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing; var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth); foreach (var e in fe) - e.FireTime = unitBaseTime + ch * c.Unit.ChannelInterval; + e.FireTime = unitBaseTime + ch * stagger; fevents.AddRange(fe); eventIdx++; } diff --git a/src/CounterDrone.Core/Algorithms/Kinematics.cs b/src/CounterDrone.Core/Algorithms/Kinematics.cs index 703d5ff..bb70d03 100644 --- a/src/CounterDrone.Core/Algorithms/Kinematics.cs +++ b/src/CounterDrone.Core/Algorithms/Kinematics.cs @@ -113,6 +113,17 @@ namespace CounterDrone.Core.Algorithms return (dx * s, dy * s, dz * s); } + /// 云团覆盖间隔:无人机穿越单个云团的时间 = 云团直径 / 无人机速度 + /// 云团有效直径 m + /// 目标速度 km/h + /// 硬件最小间隔 s + public static float CloudCoverInterval(float cloudDiameter, float targetSpeedKmh, float minInterval = 1f) + { + float speedMs = targetSpeedKmh / 3.6f; + if (speedMs <= 0.1f) return minInterval; + return Math.Max(minInterval, cloudDiameter / speedMs); + } + /// 点到矩形距离(简化判定) public static float Distance2D(float x1, float z1, float x2, float z2) { diff --git a/src/CounterDrone.Core/DefaultFireUnits.cs b/src/CounterDrone.Core/DefaultFireUnits.cs index 67fc059..b96ae13 100644 --- a/src/CounterDrone.Core/DefaultFireUnits.cs +++ b/src/CounterDrone.Core/DefaultFireUnits.cs @@ -16,7 +16,7 @@ namespace CounterDrone.Core Id = "ground-light", Name = "轻型地基火力单元", PlatformType = 1, - GunCount = 2, + GunCount = 4, ChannelsPerGun = 4, ChannelInterval = 1.0, Cooldown = 5.0, diff --git a/src/CounterDrone.Core/Services/ReportGenerator.cs b/src/CounterDrone.Core/Services/ReportGenerator.cs index d1cfc36..4447785 100644 --- a/src/CounterDrone.Core/Services/ReportGenerator.cs +++ b/src/CounterDrone.Core/Services/ReportGenerator.cs @@ -137,7 +137,7 @@ namespace CounterDrone.Core.Services SimEventType.SimulationEnd => "仿真结束", _ => e.Description, }; - sb.AppendLine($"| {e.OccurredAt:F1} | {TranslateEvent(e.Type)} | {desc} | {unitCell} |"); + sb.AppendLine($"| {e.OccurredAt:F2} | {TranslateEvent(e.Type)} | {desc} | {unitCell} |"); } sb.AppendLine(); diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index 99927a8..29ee7c2 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -111,32 +111,28 @@ namespace CounterDrone.Core.Tests } [Fact] - public void MultiUnit_Parallel_ShorterThanSingle() + public void MultiLane_3Drones_3Lanes_ExactOutput() { + // 3 架无人机横向编队,应生成 3 车道各 11 发 var threat = MakeThreat(speed: 200); + threat.Target.Quantity = 3; + threat.Route = new RoutePlan { FormationMode = (int)Models.FormationMode.Formation, LateralSpacing = 50 }; threat.Waypoints.Clear(); threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }); threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }); - // 每个单元只有 6 通道,单机需求 11 发 → 一个不够,必须两个并射 - var unitSmall = new FireUnit { Id = "u0", Type = PlatformType.GroundBased, - Position = new Vector3(5000, 0, 50), GunCount = 1, ChannelsPerGun = 6, - MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f, - AmmoTypes = new() { AerosolType.InertGas } }; - var unitSmall2 = new FireUnit { Id = "u1", Type = PlatformType.GroundBased, - Position = new Vector3(5100, 0, 50), GunCount = 1, ChannelsPerGun = 6, - MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f, - AmmoTypes = new() { AerosolType.InertGas } }; + var result = Plan(new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5000), MakeGroundUnit("u2", 5000) }, threat); - var r1 = Plan(new() { unitSmall }, threat); - var r2 = Plan(new() { unitSmall, unitSmall2 }, threat); + var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); - float s1 = r1.Best.MergedSchedule.Max(e => e.FireTime) - r1.Best.MergedSchedule.Min(e => e.FireTime); - float s2 = r2.Best.MergedSchedule.Max(e => e.FireTime) - r2.Best.MergedSchedule.Min(e => e.FireTime); + // 打印所有事件 + var lines = events.Select(e => $"{e.FireTime:F2}s Y={e.TargetY:F0} u={e.PlatformIndex}").ToList(); + var dump = string.Join("\n", lines.Take(15)) + $"\n... ({events.Count} total)"; - // 2 单元并射应短于 1 单元 - var msg = $"2单元={s2:F1}s({r2.Best.MergedSchedule.Count}发) 1单元={s1:F1}s({r1.Best.MergedSchedule.Count}发)"; - Assert.True(s2 < s1, msg); + Assert.True(events.Count >= 30, $"count={events.Count}\n{dump}"); + // 验证时间不等(有错开) + float first = events[0].FireTime, last = events[^1].FireTime; + Assert.True(last - first > 2f, $"spread={last-first:F1}s, 应有错开\n{dump}"); } [Fact] @@ -179,12 +175,12 @@ namespace CounterDrone.Core.Tests var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); Assert.True(events.Count >= 3); - // 间隔应稳定 ≈ ChannelInterval(1s),不应因目标位置变化而波动 + float expectedGap = Kinematics.CloudCoverInterval(40f, 120f, 1f); for (int i = 1; i < events.Count; i++) { float gap = events[i].FireTime - events[i - 1].FireTime; - Assert.True(Math.Abs(gap - 1.0f) < 0.2f, - $"gap[{i}]={gap:F3}, expected 1.0±0.2"); + Assert.True(Math.Abs(gap - expectedGap) < 0.3f, + $"gap[{i}]={gap:F3}, expected {expectedGap:F2}±0.3"); } }