fix: planner formation lane offset + add 3-drone integration test

Bug: planner used symmetric lane offset (Z=-50/0/+50) but DroneEntity uses start-anchored (Z=0/50/100). 3rd drone at Z=100 got no clouds.

Fix: planner laneOffset = yLane * laneSpacing (matches DroneEntity formationIndex * lateralSpacing).

Test: Scenario_3DronesFormation - 3 drones in Formation (Z=0/50/100), 3 fire units, all destroyed. 192 tests pass.
This commit is contained in:
tian 2026-06-14 21:58:05 +08:00
parent 87e82727f2
commit 008df4a86c
2 changed files with 59 additions and 2 deletions

View File

@ -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 * lateralSpacing0/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);

View File

@ -483,5 +483,60 @@ namespace CounterDrone.Core.Tests
Assert.True(drone.Hp < 0.1f, msg);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 63 架活塞编队(横排 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/100latIdx × LateralSpacing
_scenario.SaveRoute(_taskId, "default", new RoutePlan
{
FormationMode = (int)FormationMode.Formation,
LateralSpacing = 50,
LateralCount = 3,
LongitudinalCount = 1,
},
new List<Waypoint>
{
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<EquipmentDeployment>
{
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);
}
}
}