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:
parent
67223cd392
commit
4706c4b8c1
56
data/default_formations.json
Normal file
56
data/default_formations.json
Normal 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
|
||||
}
|
||||
]
|
||||
@ -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>();
|
||||
|
||||
|
||||
42
src/CounterDrone.Core/DefaultFormations.cs
Normal file
42
src/CounterDrone.Core/DefaultFormations.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
@ -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 },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user