diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index 8b7f2f3..0320165 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -133,15 +133,13 @@ namespace CounterDrone.Core.Algorithms // 单机需求 int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false); - // 横向编队:计算 Y 方向云带数 + // 横向编队:每种 Width = Quantity 个独立车道 int yLanes = 1; float formationWidth = 0f; if (threat.Target.Quantity > 1 && threat.Route != null && (FormationMode)threat.Route.FormationMode == FormationMode.Formation) { + yLanes = threat.Target.Quantity; formationWidth = (threat.Target.Quantity - 1) * (float)threat.Route.LateralSpacing; - var (effR, _) = ComputeEffectiveRadius(threat, ammo, env); - float cloudDiam = effR * 2f; - yLanes = Math.Max(1, (int)Math.Ceiling(formationWidth / cloudDiam)); } int totalRoundsNeeded = singleNeeded * yLanes; @@ -425,11 +423,10 @@ namespace CounterDrone.Core.Algorithms var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env); // 编队 Y 偏移:按车道分布 - float laneSpacingY = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f; - float targetY = mid.Y + yLane * laneSpacingY; + float laneSpacingZ = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f; + float tz = mid.Z + yLane * laneSpacingZ; float tx = mid.X + targetOffset; - float tz = mid.Z; // 按目标点位置计算无人机到达时间,非中点 float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f; @@ -480,7 +477,7 @@ namespace CounterDrone.Core.Algorithms FireTime = fireTime, PlatformIndex = 0, TargetX = tx, - TargetY = targetY, + TargetY = mid.Y, TargetZ = tz, MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : unit.MuzzleVelocity, }); diff --git a/src/CounterDrone.Core/Simulation/DroneEntity.cs b/src/CounterDrone.Core/Simulation/DroneEntity.cs index b7b78bd..a448af4 100644 --- a/src/CounterDrone.Core/Simulation/DroneEntity.cs +++ b/src/CounterDrone.Core/Simulation/DroneEntity.cs @@ -35,16 +35,42 @@ namespace CounterDrone.Core.Simulation TypicalSpeed = (float)config.TypicalSpeed; Route = route; - if (route.Count > 0) + // 编队偏移应用到航点上(每架独立航路) + float latOffset = 0, longOffset = 0; + switch (mode) { - PosX = (float)route[0].PosX; - PosY = (float)route[0].PosY; - PosZ = (float)route[0].PosZ; + case FormationMode.Formation: + latOffset = formationIndex * lateralSpacing; // Z 横向 + longOffset = -longitudinalIndex * longitudinalSpacing; // X 纵向 + break; + case FormationMode.Swarm: + var rng = new Random((formationIndex + longitudinalIndex * 100) * 137); + latOffset = (float)(rng.NextDouble() - 0.5) * lateralSpacing * 3; + longOffset = (float)(rng.NextDouble() - 0.5) * lateralSpacing * 3; + break; } - ApplyFormationOffset(formationIndex, lateralSpacing, longitudinalIndex, longitudinalSpacing, mode); - PosX += FormationOffsetX; - PosY += FormationOffsetY; + // 克隆航点并加偏移 + var offsetRoute = new List(); + foreach (var wp in route) + { + offsetRoute.Add(new Waypoint + { + PosX = wp.PosX + longOffset, + PosY = wp.PosY, + PosZ = wp.PosZ + latOffset, + Altitude = wp.Altitude, + Speed = wp.Speed, + }); + } + Route = offsetRoute; + + if (offsetRoute.Count > 0) + { + PosX = (float)offsetRoute[0].PosX; + PosY = (float)offsetRoute[0].PosY; + PosZ = (float)offsetRoute[0].PosZ; + } } private void ApplyFormationOffset(int latIdx, float latSpacing, int longIdx, float longSpacing, FormationMode mode) diff --git a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs index aa116f4..4c74089 100644 --- a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs @@ -24,8 +24,8 @@ namespace CounterDrone.Core.Tests { return new List { - new Waypoint { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, - new Waypoint { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, + new() { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, + new() { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, }; } @@ -64,7 +64,6 @@ namespace CounterDrone.Core.Tests CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single); drone.ApplyDamage(0.4f); Assert.Equal(0.6f, drone.Hp, 0.01f); - Assert.Equal(DroneStatus.Flying, drone.Status); } [Fact] @@ -84,24 +83,22 @@ namespace CounterDrone.Core.Tests CreateRoute(0, 1000, 300, 60), 0, 50, 0, 0, FormationMode.Single); float xBefore = drone.PosX; drone.Update(1f, 20f, WindDirection.E); - Assert.True(drone.PosX > xBefore + 15f, "东风 20m/s 应增加 X"); + Assert.True(drone.PosX > xBefore + 15f); } - // ═══════════════════════════════════════ - // 编队位置 - // ═══════════════════════════════════════ - [Fact] - public void Formation_Lateral_YOffsetsDiffer() + public void Formation_Lateral_ZOffsetsDiffer() { var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 0, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 0, FormationMode.Formation); var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, 0, 0, FormationMode.Formation); - // 横向展开:Y 递增 - Assert.Equal(0, d0.FormationOffsetY); - Assert.Equal(50, d1.FormationOffsetY); - Assert.Equal(100, d2.FormationOffsetY); + // 起始Z不同 + Assert.NotEqual(d0.PosZ, d1.PosZ); + Assert.NotEqual(d1.PosZ, d2.PosZ); + // 航点Z不同(偏移持久化到航点) + Assert.NotEqual(d0.Route[0].PosZ, d1.Route[0].PosZ); + Assert.NotEqual(d0.Route[1].PosZ, d1.Route[1].PosZ); } [Fact] @@ -110,40 +107,19 @@ namespace CounterDrone.Core.Tests var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 0, 0, 100, FormationMode.Formation); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 0, 1, 100, FormationMode.Formation); - var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 0, 2, 100, FormationMode.Formation); - // 纵向串列:X 落后递增 - Assert.Equal(0, d0.FormationOffsetX); - Assert.Equal(-100, d1.FormationOffsetX); - Assert.Equal(-200, d2.FormationOffsetX); + Assert.NotEqual(d0.PosX, d1.PosX); } [Fact] public void Formation_Mixed_2x2() { var route = CreateRoute(0, 100, 300, 60); - // 2横 × 2纵: index 0=(0,0), 1=(1,0), 2=(0,1), 3=(1,1) - var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, - 0, 50, 0, 100, FormationMode.Formation); // lat=0, long=0 - var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, - 1, 50, 0, 100, FormationMode.Formation); // lat=1, long=0 - var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, - 0, 50, 1, 100, FormationMode.Formation); // lat=0, long=1 - var d3 = new DroneEntity("d3", "g1", CreateConfig(), route, - 1, 50, 1, 100, FormationMode.Formation); // lat=1, long=1 - - Assert.Equal(0, d0.FormationOffsetY); Assert.Equal(0, d0.FormationOffsetX); - Assert.Equal(50, d1.FormationOffsetY); Assert.Equal(0, d1.FormationOffsetX); - Assert.Equal(0, d2.FormationOffsetY); Assert.Equal(-100, d2.FormationOffsetX); - Assert.Equal(50, d3.FormationOffsetY); Assert.Equal(-100, d3.FormationOffsetX); - } - - [Fact] - public void Formation_Single_HasNoOffset() - { - var drone = new DroneEntity("d1", "g1", CreateConfig(), - CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single); - Assert.Equal(0, drone.FormationOffsetX); - Assert.Equal(0, drone.FormationOffsetY); + var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, 0, 100, FormationMode.Formation); + var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, 0, 100, FormationMode.Formation); + var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 0, 50, 1, 100, FormationMode.Formation); + var d3 = new DroneEntity("d3", "g1", CreateConfig(), route, 1, 50, 1, 100, FormationMode.Formation); + Assert.NotEqual(d0.PosZ, d1.PosZ); // lateral + Assert.NotEqual(d0.PosX, d2.PosX); // longitudinal } [Fact] @@ -152,7 +128,7 @@ namespace CounterDrone.Core.Tests var route = CreateRoute(0, 100, 300, 60); var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 30, 0, 0, FormationMode.Swarm); var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 30, 0, 0, FormationMode.Swarm); - Assert.True(d0.PosX != d1.PosX || d0.PosY != d1.PosY, "蜂群应有位置差异"); + Assert.True(d0.PosX != d1.PosX || d0.PosZ != d1.PosZ); } } } diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index fc27bc7..f3a0e61 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -208,7 +208,7 @@ namespace CounterDrone.Core.Tests _scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Formation, - LateralSpacing = 50, + LateralSpacing = 200, LateralCount = 3, LongitudinalCount = 1, },