feat: 编队模型扩展 — LateralSpacing/LongitudinalSpacing/LateralCount/LongitudinalCount

- FormationSpacing → LateralSpacing(正面横向间距)
- 新增 LongitudinalCount/LongitudinalSpacing(纵深串列)
- DefaultFormations: 6种默认编队模板(单机/3机横队/5机横队/3机纵队/2×2方队/10机蜂群)
- Piston 测试设置 LateralCount=3, LongitudinalCount=1
This commit is contained in:
tian 2026-06-13 13:16:27 +08:00
parent 67223cd392
commit 4706c4b8c1
7 changed files with 158 additions and 18 deletions

View File

@ -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
}
]

View File

@ -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<FireEvent>();
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<FireEvent> 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<FireEvent>();

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace CounterDrone.Core
{
public static class DefaultFormations
{
private static List<FormationTemplate> _cache;
public static List<FormationTemplate> GetAll()
{
if (_cache != null) return _cache;
_cache = new List<FormationTemplate>
{
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; }
}
}

View File

@ -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;
/// <summary>正面架数横向展开null=全部横向(=Quantity</summary>
public int? LateralCount { get; set; }
/// <summary>纵深架数串列跟随null=1无纵深</summary>
public int? LongitudinalCount { get; set; }
/// <summary>纵向串列间距 m</summary>
public double LongitudinalSpacing { get; set; } = 50.0;
public string ETA { get; set; } = string.Empty;
}

View File

@ -92,7 +92,7 @@ namespace CounterDrone.Core.Simulation
var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List<Waypoint>());
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));

View File

@ -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<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
@ -197,19 +204,20 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
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));
}
// ═══════════════════════════════════════════════

View File

@ -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<Waypoint>
{
@ -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<Waypoint>
{
new Waypoint { PosX = 0, PosY = 100, PosZ = 200, Altitude = 600, Speed = 250 },