From 4706c4b8c1c929741301c94dc8651aeed4642174 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 13 Jun 2026 13:16:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BC=96=E9=98=9F=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=89=A9=E5=B1=95=20=E2=80=94=20LateralSpacing/LongitudinalSpa?= =?UTF-8?q?cing/LateralCount/LongitudinalCount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FormationSpacing → LateralSpacing(正面横向间距) - 新增 LongitudinalCount/LongitudinalSpacing(纵深串列) - DefaultFormations: 6种默认编队模板(单机/3机横队/5机横队/3机纵队/2×2方队/10机蜂群) - Piston 测试设置 LateralCount=3, LongitudinalCount=1 --- data/default_formations.json | 56 +++++++++++++++++++ .../Algorithms/DefaultDefensePlanner.cs | 39 ++++++++++--- src/CounterDrone.Core/DefaultFormations.cs | 42 ++++++++++++++ src/CounterDrone.Core/Models/RoutePlan.cs | 11 +++- .../Simulation/SimulationEngine.cs | 2 +- .../FullPipelineTests.cs | 22 +++++--- .../ScenarioServiceTests.cs | 4 +- 7 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 data/default_formations.json create mode 100644 src/CounterDrone.Core/DefaultFormations.cs diff --git a/data/default_formations.json b/data/default_formations.json new file mode 100644 index 0000000..bb01361 --- /dev/null +++ b/data/default_formations.json @@ -0,0 +1,56 @@ +[ + { + "Id": "single", + "Name": "单机", + "FormationMode": 0, + "LateralCount": 1, + "LongitudinalCount": 1, + "LateralSpacing": 0, + "LongitudinalSpacing": 0 + }, + { + "Id": "line-abreast-3", + "Name": "3机横队", + "FormationMode": 1, + "LateralCount": 3, + "LongitudinalCount": 1, + "LateralSpacing": 50, + "LongitudinalSpacing": 0 + }, + { + "Id": "line-abreast-5", + "Name": "5机横队", + "FormationMode": 1, + "LateralCount": 5, + "LongitudinalCount": 1, + "LateralSpacing": 50, + "LongitudinalSpacing": 0 + }, + { + "Id": "column-3", + "Name": "3机纵队", + "FormationMode": 1, + "LateralCount": 1, + "LongitudinalCount": 3, + "LateralSpacing": 0, + "LongitudinalSpacing": 100 + }, + { + "Id": "box-2x2", + "Name": "2×2方队", + "FormationMode": 1, + "LateralCount": 2, + "LongitudinalCount": 2, + "LateralSpacing": 50, + "LongitudinalSpacing": 100 + }, + { + "Id": "swarm-10", + "Name": "蜂群(10架)", + "FormationMode": 2, + "LateralCount": 10, + "LongitudinalCount": 1, + "LateralSpacing": 30, + "LongitudinalSpacing": 0 + } +] diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index 5f42f63..e016d92 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -128,9 +128,22 @@ namespace CounterDrone.Core.Algorithms // 计算总弹药需求 var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType); - var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)neededAmmo); - int totalRoundsNeeded = CalcRoundsNeeded(threat, ammo, env, - candidates[0].Unit.Type == PlatformType.AirBased); + var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo); + + // 单机需求 + int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false); + + // 横向编队:计算 Y 方向云带数 + int yLanes = 1; + float formationWidth = 0f; + if (threat.Target.Quantity > 1 && threat.Route != null && (FormationMode)threat.Route.FormationMode == FormationMode.Formation) + { + 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; // 计算所需齐射轮数 int totalChannels = candidates.Sum(c => c.Unit.TotalChannels); @@ -166,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); + var feTemplate = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, centerOffset, spacing, totalRoundsNeeded); 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); + var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, spacing, totalRoundsNeeded); foreach (var e in fe) { e.FireTime = baseFireTime + salvoDelay + eventIdx * physicsInterval; @@ -386,7 +399,18 @@ namespace CounterDrone.Core.Algorithms var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; - float requiredCoverage = neededExposure * avgSpeed; + float singleCoverage = neededExposure * avgSpeed; + + // 多架编队:覆盖范围需加上编队展开宽度 + int quantity = Math.Max(1, threat.Target.Quantity); + float formationSpread = 0f; + if (quantity > 1 && threat.Route != null) + { + var mode = (FormationMode)threat.Route.FormationMode; + if (mode != FormationMode.Single) + formationSpread = (quantity - 1) * (float)threat.Route.LateralSpacing; + } + float requiredCoverage = singleCoverage + formationSpread; return Math.Max(1, (int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1); @@ -412,7 +436,8 @@ namespace CounterDrone.Core.Algorithms // ═══════════════════════════════════════════════ private List GenerateFireEventsAt(DroneGroup threat, FireUnit unit, - AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset) + AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset, + float formationY) { var events = new List(); diff --git a/src/CounterDrone.Core/DefaultFormations.cs b/src/CounterDrone.Core/DefaultFormations.cs new file mode 100644 index 0000000..d0fb979 --- /dev/null +++ b/src/CounterDrone.Core/DefaultFormations.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace CounterDrone.Core +{ + public static class DefaultFormations + { + private static List _cache; + + public static List GetAll() + { + if (_cache != null) return _cache; + _cache = new List + { + new() { Id = "single", Name = "单机", FormationMode = 0, LateralCount = 1, LongitudinalCount = 1 }, + new() { Id = "line-abreast-3", Name = "3机横队", FormationMode = 1, LateralCount = 3, LongitudinalCount = 1, + LateralSpacing = 50 }, + new() { Id = "line-abreast-5", Name = "5机横队", FormationMode = 1, LateralCount = 5, LongitudinalCount = 1, + LateralSpacing = 50 }, + new() { Id = "column-3", Name = "3机纵队", FormationMode = 1, LateralCount = 1, LongitudinalCount = 3, + LongitudinalSpacing = 100 }, + new() { Id = "box-2x2", Name = "2×2方队", FormationMode = 1, LateralCount = 2, LongitudinalCount = 2, + LateralSpacing = 50, LongitudinalSpacing = 100 }, + new() { Id = "swarm-10", Name = "蜂群(10架)", FormationMode = 2, LateralCount = 10, LongitudinalCount = 1, + LateralSpacing = 30 }, + }; + return _cache; + } + + public static FormationTemplate GetById(string id) => GetAll().Find(t => t.Id == id); + } + + public class FormationTemplate + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public int FormationMode { get; set; } + public int LateralCount { get; set; } = 1; + public int LongitudinalCount { get; set; } = 1; + public double LateralSpacing { get; set; } + public double LongitudinalSpacing { get; set; } + } +} diff --git a/src/CounterDrone.Core/Models/RoutePlan.cs b/src/CounterDrone.Core/Models/RoutePlan.cs index b457dd0..13b6c28 100644 --- a/src/CounterDrone.Core/Models/RoutePlan.cs +++ b/src/CounterDrone.Core/Models/RoutePlan.cs @@ -18,7 +18,16 @@ namespace CounterDrone.Core.Models public int FormationMode { get; set; } = (int)Models.FormationMode.Single; - public double FormationSpacing { get; set; } = 50.0; + public double LateralSpacing { get; set; } = 50.0; + + /// 正面架数(横向展开),null=全部横向(=Quantity) + public int? LateralCount { get; set; } + + /// 纵深架数(串列跟随),null=1(无纵深) + public int? LongitudinalCount { get; set; } + + /// 纵向串列间距 m + public double LongitudinalSpacing { get; set; } = 50.0; public string ETA { get; set; } = string.Empty; } diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index a22d44f..d014bfe 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -92,7 +92,7 @@ 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.FormationSpacing; + var spacing = (float)route.LateralSpacing; for (int i = 0; i < target.Quantity; i++) _drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId, target, wps, i, spacing, mode)); diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index 739adb4..84ed2c6 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -186,10 +186,17 @@ namespace CounterDrone.Core.Tests _scenario.SaveTarget(_taskId, new TargetConfig { GroupId = "default", TargetType = (int)TargetType.Piston, - PowerType = (int)PowerType.Piston, Quantity = 1, + PowerType = (int)PowerType.Piston, + Quantity = 3, // 单批次 3 架 TypicalSpeed = 200, TypicalAltitude = 500, }); - _scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, + _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 }, @@ -197,19 +204,20 @@ namespace CounterDrone.Core.Tests }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 2, 5000, 0, 50), + MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 4, 5000, 0, 50), }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); var eng = RunSimulation(8000); - var drone = eng.Drones[0]; var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); - var msg = $"Piston: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}"; - VerifyAndExportReport(eng, drone); + var msg = $"Piston: launched={launched} clouds={clouds} drones={eng.Drones.Count}"; + foreach (var d in eng.Drones) + msg += $" [{d.Status} hp={d.Hp:F2}]"; + VerifyAndExportReport(eng, eng.Drones[0]); Assert.True(launched > 0, msg); - Assert.Equal(DroneStatus.Destroyed, drone.Status); + Assert.All(eng.Drones, d => Assert.Equal(DroneStatus.Destroyed, d.Status)); } // ═══════════════════════════════════════════════ diff --git a/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs b/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs index b183481..6d2e382 100644 --- a/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs +++ b/test/unit/CounterDrone.Core.Tests/ScenarioServiceTests.cs @@ -258,7 +258,7 @@ namespace CounterDrone.Core.Tests var route = new RoutePlan { FormationMode = (int)FormationMode.Formation, - FormationSpacing = 60.0, + LateralSpacing = 60.0, }; var waypoints = new List { @@ -439,7 +439,7 @@ namespace CounterDrone.Core.Tests _service.SaveRoute(task.Id, "default", new RoutePlan { FormationMode = (int)FormationMode.Swarm, - FormationSpacing = 30.0, + LateralSpacing = 30.0, }, new List { new Waypoint { PosX = 0, PosY = 100, PosZ = 200, Altitude = 600, Speed = 250 },