diff --git a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs index 593954e..b52e27f 100644 --- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs @@ -401,11 +401,13 @@ namespace CounterDrone.Core.Algorithms float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z); float crossArc = midArc + targetOffset; - // 编队横向偏移:按车道分布在航路法向上 + // 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。 + // DroneEntity: latOffset = formationIndex * lateralSpacing(0/50/100...) // 法向 = 切向旋转 90°,用 RouteGeometry.TangentAt 取穿越点处航路方向 var (tanX, tanZ) = RouteGeometry.TangentAt(wps, crossArc); float normalX = -tanZ, normalZ = tanX; // 切向逆时针 90° = 左侧法向 - float laneOffset = yLanes > 1 ? (yLane - (yLanes - 1) / 2f) * (formationWidth / Math.Max(1, yLanes - 1)) : 0f; + float laneSpacing = yLanes > 1 ? formationWidth / Math.Max(1, yLanes - 1) : 0f; + float laneOffset = yLane * laneSpacing; // 穿越点 = 航路上 crossArc 处 + 横向车道偏移 var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc); diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index eb86487..8f2608f 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -483,5 +483,60 @@ namespace CounterDrone.Core.Tests Assert.True(drone.Hp < 0.1f, msg); VerifyAndExportReport(eng, drone); } + + // ═══════════════════════════════════════════════ + // 场景 6:3 架活塞编队(横排 Z=0/50/100)— 多无人机端到端 + // 验证 planner 给每个车道分配云团、3 架都被击毁 + // ═══════════════════════════════════════════════ + + [Fact] + public void Scenario_3DronesFormation_AllDestroyed() + { + var task = _scenario.CreateTask("3架编队拦截测试", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + GroupId = "default", TargetType = (int)TargetType.Piston, + PowerType = (int)PowerType.Piston, + Quantity = 3, + TypicalSpeed = 200, TypicalAltitude = 500, + }); + // 3 架横排编队:Formation 模式,横向间距 50m + // 引擎会生成 3 架无人机,Z 偏移 0/50/100(latIdx × LateralSpacing) + _scenario.SaveRoute(_taskId, "default", new RoutePlan + { + FormationMode = (int)FormationMode.Formation, + LateralSpacing = 50, + LateralCount = 3, + LongitudinalCount = 1, + }, + new List + { + new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, + new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 }, + }); + // 3 个火力单元部署在中点附近(X=4500,每个单元 i*50 偏移 → 4500/4550/4600) + _scenario.SaveDeployment(_taskId, new List + { + MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 3, 4500, 0, 50), + }); + _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); + + var eng = RunSimulation(8000); + var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); + var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); + var destroyed = eng.Events.Count(e => e.Type == SimEventType.DroneDestroyed); + + // 验证:3 架无人机都被击毁 + var msg = $"3Drones: launched={launched} clouds={clouds} destroyed={destroyed} drones={eng.Drones.Count}"; + foreach (var d in eng.Drones) + msg += $" [{d.Status} hp={d.Hp:F2} pos=({d.PosX:F0},{d.PosZ:F0})]"; + VerifyAndExportReport(eng, eng.Drones[0]); + Assert.Equal(3, eng.Drones.Count); + Assert.True(launched > 0, msg); + Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg); + } } }