diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index e016d92..d235e40 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -179,14 +179,14 @@ namespace CounterDrone.Core.Algorithms // 同单元所有弹药以中心点计算飞行时间,仅通过 eventIdx 错开间隔 float centerOffset = (eventIdx + (toTake - 1) / 2f - (totalRoundsNeeded - 1) / 2f) * spacing; - var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset, spacing, totalRoundsNeeded); + var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset); float baseFireTime = feTemplate[0].FireTime; var fevents = new List(); for (int ch = 0; ch < toTake; ch++) { float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing; - var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, spacing, totalRoundsNeeded); + var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset); foreach (var e in fe) { e.FireTime = baseFireTime + salvoDelay + eventIdx * physicsInterval; @@ -436,8 +436,7 @@ namespace CounterDrone.Core.Algorithms // ═══════════════════════════════════════════════ private List GenerateFireEventsAt(DroneGroup threat, FireUnit unit, - AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset, - float formationY) + AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset) { var events = new List(); diff --git a/src/CounterDrone.Core/Simulation/DroneEntity.cs b/src/CounterDrone.Core/Simulation/DroneEntity.cs index e4f2df3..b7b78bd 100644 --- a/src/CounterDrone.Core/Simulation/DroneEntity.cs +++ b/src/CounterDrone.Core/Simulation/DroneEntity.cs @@ -12,7 +12,7 @@ namespace CounterDrone.Core.Simulation public TargetType TargetType { get; } public PowerType PowerType { get; } public float Wingspan { get; } - public float TypicalSpeed { get; } // km/h + public float TypicalSpeed { get; } public List Route { get; } public int CurrentWaypointIndex { get; private set; } public float PosX { get; private set; } @@ -20,11 +20,12 @@ namespace CounterDrone.Core.Simulation public float PosZ { get; private set; } public float Hp { get; private set; } = 1.0f; public DroneStatus Status { get; private set; } = DroneStatus.Flying; - public float ExposureTime { get; set; } // 在云团中的累计暴露时间 + public float ExposureTime { get; set; } public float FormationOffsetX { get; set; } public float FormationOffsetY { get; set; } - public DroneEntity(string id, string groupId, TargetConfig config, List route, int formationIndex, float spacing, FormationMode mode) + public DroneEntity(string id, string groupId, TargetConfig config, List route, + int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode) { Id = id; GroupId = groupId; @@ -41,25 +42,24 @@ namespace CounterDrone.Core.Simulation PosZ = (float)route[0].PosZ; } - // 编队偏移 - ApplyFormationOffset(formationIndex, spacing, mode); + ApplyFormationOffset(formationIndex, lateralSpacing, longitudinalIndex, longitudinalSpacing, mode); PosX += FormationOffsetX; PosY += FormationOffsetY; } - private void ApplyFormationOffset(int index, float spacing, FormationMode mode) + private void ApplyFormationOffset(int latIdx, float latSpacing, int longIdx, float longSpacing, FormationMode mode) { - if (index == 0) return; - var offset = spacing * index; + if (latIdx == 0 && longIdx == 0) return; switch (mode) { case FormationMode.Formation: - FormationOffsetY = offset; // 沿 Y 轴横队排列 + FormationOffsetY = latIdx * latSpacing; + FormationOffsetX = -longIdx * longSpacing; break; case FormationMode.Swarm: - var rng = new Random(index * 137); - FormationOffsetX = (float)(rng.NextDouble() - 0.5) * spacing * 3; - FormationOffsetY = (float)(rng.NextDouble() - 0.5) * spacing * 3; + var rng = new Random((latIdx + longIdx * 100) * 137); + FormationOffsetX = (float)(rng.NextDouble() - 0.5) * latSpacing * 3; + FormationOffsetY = (float)(rng.NextDouble() - 0.5) * latSpacing * 3; break; } } @@ -93,13 +93,11 @@ namespace CounterDrone.Core.Simulation return; } - // 移动方向 var ratio = step / dist; PosX += dx * ratio; PosY += dy * ratio; PosZ += dz * ratio; - // 风偏 (float wx, float wy, float wz) = Kinematics.WindToVector(windDir, windSpeed); PosX += wx * deltaTime; PosY += wy * deltaTime; diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index d014bfe..702b5e8 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -92,10 +92,17 @@ namespace CounterDrone.Core.Simulation var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List()); if (route == null || wps.Count == 0) continue; var mode = (FormationMode)route.FormationMode; - var spacing = (float)route.LateralSpacing; + var lateralSpacing = (float)route.LateralSpacing; + var longSpacing = (float)route.LongitudinalSpacing; + int latCount = route.LateralCount ?? target.Quantity; + int longCount = route.LongitudinalCount ?? 1; for (int i = 0; i < target.Quantity; i++) + { + int latIdx = i % latCount; + int longIdx = i / latCount; _drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId, - target, wps, i, spacing, mode)); + target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode)); + } } _platforms.Clear(); diff --git a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs index f1abc8b..aa116f4 100644 --- a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs @@ -20,105 +20,139 @@ namespace CounterDrone.Core.Tests }; } - private List CreateRoute(float fromX, float toX, float altitude, float speed) + private List CreateRoute(float fromX, float toX, float alt, float speed) { return new List { - new Waypoint { PosX = fromX, PosY = altitude, PosZ = 0, Altitude = altitude, Speed = speed }, - new Waypoint { PosX = toX, PosY = altitude, PosZ = 0, Altitude = altitude, Speed = speed }, + new Waypoint { PosX = fromX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, + new Waypoint { PosX = toX, PosY = alt, PosZ = 0, Altitude = alt, Speed = speed }, }; } [Fact] public void InitialPosition_IsFirstWaypoint() { - var route = CreateRoute(100, 500, 300, 120); - var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single); - + var drone = new DroneEntity("d1", "g1", CreateConfig(), + CreateRoute(100, 500, 300, 120), 0, 50, 0, 0, FormationMode.Single); Assert.Equal(100, drone.PosX, 1); - Assert.Equal(300, drone.PosY, 1); // 高度 = waypoint.PosY + Assert.Equal(300, drone.PosY, 1); } [Fact] public void Update_MovesTowardTarget() { - var route = CreateRoute(0, 1000, 300, 120); - var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single); - + var drone = new DroneEntity("d1", "g1", CreateConfig(), + CreateRoute(0, 1000, 300, 120), 0, 50, 0, 0, FormationMode.Single); float prevX = drone.PosX; drone.Update(1f, 0, WindDirection.N); - - Assert.True(drone.PosX > prevX, "无人机应向目标移动"); + Assert.True(drone.PosX > prevX); } [Fact] public void Update_ReachesTarget_StatusChanges() { - var route = CreateRoute(0, 100, 300, 360); // 360 km/h = 100 m/s - var drone = new DroneEntity("d1", "g1", CreateConfig(360), route, 0, 50, FormationMode.Single); - - // 100m 距离, 100m/s 速度, 1s 后应到达 + var drone = new DroneEntity("d1", "g1", CreateConfig(360), + CreateRoute(0, 100, 300, 360), 0, 50, 0, 0, FormationMode.Single); drone.Update(2f, 0, WindDirection.N); - Assert.Equal(DroneStatus.ReachedTarget, drone.Status); } [Fact] - public void ApplyDamage_ReducesHp() + public void Damage_ReducesHp() { var drone = new DroneEntity("d1", "g1", CreateConfig(), - CreateRoute(0, 100, 300, 60), 0, 50, FormationMode.Single); - + 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] - public void ApplyDamage_ExceedingHp_DestroysDrone() + public void Damage_ExceedingHp_Destroys() { var drone = new DroneEntity("d1", "g1", CreateConfig(), - CreateRoute(0, 100, 300, 60), 0, 50, FormationMode.Single); - + CreateRoute(0, 100, 300, 60), 0, 50, 0, 0, FormationMode.Single); drone.ApplyDamage(1.5f); Assert.True(drone.Hp <= 0); Assert.Equal(DroneStatus.Destroyed, drone.Status); } [Fact] - public void Formation_YOffsetsDiffer() + public void Wind_AffectsPosition() + { + var drone = new DroneEntity("d1", "g1", CreateConfig(), + 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"); + } + + // ═══════════════════════════════════════ + // 编队位置 + // ═══════════════════════════════════════ + + [Fact] + public void Formation_Lateral_YOffsetsDiffer() { var route = CreateRoute(0, 100, 300, 60); - var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, FormationMode.Formation); - var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, FormationMode.Formation); + 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); + } - Assert.NotEqual(d0.PosY, d1.PosY); + [Fact] + public void Formation_Longitudinal_XOffsetsDiffer() + { + 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); + } + + [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); } [Fact] public void Swarm_ProducesRandomOffsets() { var route = CreateRoute(0, 100, 300, 60); - var d0 = new DroneEntity("d0", "g1", CreateConfig(), route, 0, 50, FormationMode.Swarm); - var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 50, FormationMode.Swarm); - var d2 = new DroneEntity("d2", "g1", CreateConfig(), route, 2, 50, FormationMode.Swarm); - - // 蜂群模式:三架无人机位置应各不相同 + 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(d1.PosX != d2.PosX || d1.PosY != d2.PosY, "蜂群应有位置差异"); - } - - [Fact] - public void Wind_AffectsPosition() - { - var route = CreateRoute(0, 1000, 300, 60); - var drone = new DroneEntity("d1", "g1", CreateConfig(), route, 0, 50, FormationMode.Single); - - float xBefore = drone.PosX; - drone.Update(1f, 20f, WindDirection.E); // 东风 20m/s - - // 东风 → +X 方向偏移 - Assert.True(drone.PosX > xBefore + 15f, "东风应显著增加 X 位移"); } } }