feat: 编队轴配置 + LaneDivider 策略 + 数据版本 V3
RoutePlan/FormationTemplate: 新增 LateralAxis/LongitudinalAxis (0=X,1=Y,2=Z) DroneEntity: 按 axis 展开编队偏移(lat轴→xyzw, long轴→x偏移, vert→y) DefaultLaneDivider: 宽/深双维度判断,任一超云团半径则拆分 DefensePlanner: PlanUnitLane 读 LateralAxis 决定偏移方向 defaults.json: air-standard EORange 9600→4000, 所有编队加 axis 字段, 版本 2→3 DatabaseManager: 场景版本 2→3
This commit is contained in:
parent
6b2905b9f8
commit
0b7bf2ab6f
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2",
|
||||
"version": "3",
|
||||
"ammunition": [
|
||||
{
|
||||
"Id": "inert",
|
||||
@ -61,12 +61,12 @@
|
||||
],
|
||||
|
||||
"formations": [
|
||||
{ "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0 },
|
||||
{ "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 },
|
||||
{ "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 },
|
||||
{ "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100 },
|
||||
{ "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100 },
|
||||
{ "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0 }
|
||||
{ "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 }
|
||||
],
|
||||
|
||||
"routes": [
|
||||
|
||||
30
src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs
Normal file
30
src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>默认车道划分:按云团覆盖能力合并或拆分</summary>
|
||||
public class DefaultLaneDivider : ILaneDivider
|
||||
{
|
||||
public (int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius)
|
||||
{
|
||||
if (wave.Route == null || wave.Target.Quantity <= 1)
|
||||
return (1, 0);
|
||||
|
||||
var mode = (FormationMode)wave.Route.FormationMode;
|
||||
if (mode != FormationMode.Formation)
|
||||
return (1, 0);
|
||||
|
||||
int latCount = wave.Route.LateralCount ?? wave.Target.Quantity;
|
||||
int longCount = wave.Route.LongitudinalCount ?? 1;
|
||||
float latWidth = (latCount - 1) * (float)wave.Route.LateralSpacing;
|
||||
float longDepth = (longCount - 1) * (float)wave.Route.LongitudinalSpacing;
|
||||
float cloudDiameter = 2f * cloudRadius;
|
||||
|
||||
// 任一维度超过云团半径,则无法一个云团覆盖
|
||||
if (latWidth > cloudRadius || longDepth > cloudRadius)
|
||||
return (wave.Target.Quantity, (float)wave.Route.LateralSpacing);
|
||||
|
||||
return (1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,13 +11,15 @@ namespace CounterDrone.Core.Algorithms
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
private readonly IDamageModel _damageModel;
|
||||
private readonly PlannerConfig _config;
|
||||
private readonly ILaneDivider _laneDivider;
|
||||
|
||||
public DefensePlanner(List<AmmunitionSpec> ammoCatalog, PlannerConfig config,
|
||||
IDamageModel? damageModel = null)
|
||||
IDamageModel? damageModel = null, ILaneDivider? laneDivider = null)
|
||||
{
|
||||
_ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog));
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_damageModel = damageModel ?? new DamageModelRouter();
|
||||
_laneDivider = laneDivider ?? new DefaultLaneDivider();
|
||||
if (_ammoCatalog.Count == 0)
|
||||
throw new ArgumentException("弹药规格目录不能为空");
|
||||
}
|
||||
@ -137,14 +139,9 @@ namespace CounterDrone.Core.Algorithms
|
||||
var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env);
|
||||
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius);
|
||||
|
||||
// 横向编队:每种 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 (yLanes, laneSpacing) = _laneDivider.Divide(threat, turbulentRadius);
|
||||
float formationWidth = yLanes > 1 ? (yLanes - 1) * laneSpacing : 0f;
|
||||
int totalRoundsNeeded = singleNeeded * yLanes;
|
||||
|
||||
// 逐单元分配:每个单元锁定一个 Y 车道
|
||||
@ -524,17 +521,16 @@ namespace CounterDrone.Core.Algorithms
|
||||
|
||||
// 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。
|
||||
// DroneEntity: latOffset = formationIndex * lateralSpacing(0/50/100...)
|
||||
// 法向 = 切向旋转 90°,用 RouteGeometry.TangentAt 取穿越点处航路方向
|
||||
var (tanX, tanZ) = RouteGeometry.TangentAt(wps, crossArc);
|
||||
float normalX = -tanZ, normalZ = tanX; // 切向逆时针 90° = 左侧法向
|
||||
// 编队展开方向:读 RoutePlan.LateralAxis (0=X, 1=Y, 2=Z)
|
||||
int axis = threat.Route?.LateralAxis ?? 2;
|
||||
float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
|
||||
float laneOffset = yLane * laneSpacing;
|
||||
|
||||
// 穿越点 = 航路上 crossArc 处 + 横向车道偏移
|
||||
var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc);
|
||||
float tx = routeX + normalX * laneOffset;
|
||||
float ty = routeY;
|
||||
float tz = routeZ + normalZ * laneOffset;
|
||||
float tx = routeX, ty = routeY, tz = routeZ;
|
||||
if (axis == 0) tx += laneOffset;
|
||||
else if (axis == 1) ty += laneOffset;
|
||||
else tz += laneOffset;
|
||||
|
||||
// 为每个目标点重新计算发射角(不等同于拦截弧处的角度)
|
||||
float dx = tx - unit.Position.X;
|
||||
|
||||
14
src/CounterDrone.Core/Algorithms/ILaneDivider.cs
Normal file
14
src/CounterDrone.Core/Algorithms/ILaneDivider.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>车道划分策略:决定一个批次拆成几个车道</summary>
|
||||
public interface ILaneDivider
|
||||
{
|
||||
/// <summary>计算车道数和不重叠的车道间距</summary>
|
||||
/// <param name="wave">无人机批次</param>
|
||||
/// <param name="cloudRadius">云团有效半径 (m)</param>
|
||||
/// <returns>(车道数, 车道间距 m)</returns>
|
||||
(int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius);
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,7 @@ namespace CounterDrone.Core
|
||||
.FirstOrDefault(m => m.Key == "scenariosVersion")?.Value;
|
||||
|
||||
// 版本未变 → 跳过
|
||||
if (currentVersion == "2") return;
|
||||
if (currentVersion == "3") return;
|
||||
|
||||
var defaults = DefaultData.Load(_paths);
|
||||
var scenario = new ScenarioService(
|
||||
@ -93,7 +93,7 @@ namespace CounterDrone.Core
|
||||
|
||||
DefaultScenarios.Seed(scenario, defaults);
|
||||
|
||||
db.InsertOrReplace(new MetaEntry { Key = "scenariosVersion", Value = "2" });
|
||||
db.InsertOrReplace(new MetaEntry { Key = "scenariosVersion", Value = "3" });
|
||||
}
|
||||
|
||||
private void CreateMainTables(SQLiteConnection db)
|
||||
|
||||
@ -80,6 +80,8 @@ namespace CounterDrone.Core
|
||||
public int LongitudinalCount { get; set; } = 1;
|
||||
public double LateralSpacing { get; set; }
|
||||
public double LongitudinalSpacing { get; set; }
|
||||
public int LateralAxis { get; set; } = 2; // 0=X, 1=Y, 2=Z
|
||||
public int LongitudinalAxis { get; set; } = 0; // 0=X, 1=Y, 2=Z
|
||||
|
||||
public RoutePlan ToRoutePlan()
|
||||
{
|
||||
@ -90,6 +92,8 @@ namespace CounterDrone.Core
|
||||
LongitudinalCount = LongitudinalCount,
|
||||
LateralSpacing = LateralSpacing,
|
||||
LongitudinalSpacing = LongitudinalSpacing,
|
||||
LateralAxis = LateralAxis,
|
||||
LongitudinalAxis = LongitudinalAxis,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,11 @@ namespace CounterDrone.Core.Models
|
||||
/// <summary>纵向串列间距 m</summary>
|
||||
public double LongitudinalSpacing { get; set; } = 50.0;
|
||||
|
||||
/// <summary>横向展开轴:0=X, 1=Y(垂直), 2=Z(默认)</summary>
|
||||
public int LateralAxis { get; set; } = 2;
|
||||
/// <summary>纵向串列轴:0=X(默认), 1=Y, 2=Z</summary>
|
||||
public int LongitudinalAxis { get; set; } = 0;
|
||||
|
||||
public string ETA { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,8 @@ namespace CounterDrone.Core.Simulation
|
||||
private readonly float[] _cumArc;
|
||||
|
||||
public DroneEntity(string id, string waveId, TargetConfig config, List<Waypoint> route,
|
||||
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode)
|
||||
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing,
|
||||
FormationMode mode, int lateralAxis = 2, int longitudinalAxis = 0)
|
||||
{
|
||||
Id = id;
|
||||
WaveId = waveId;
|
||||
@ -51,34 +52,32 @@ namespace CounterDrone.Core.Simulation
|
||||
CruiseSpeed = spd;
|
||||
Route = route;
|
||||
|
||||
// 编队偏移应用到航点上(每架独立航路)
|
||||
float latOffset = 0, longOffset = 0;
|
||||
switch (mode)
|
||||
// 编队偏移:按 LateralAxis/LongitudinalAxis 展开(0=X, 1=Y, 2=Z)
|
||||
float latOffset = 0, vertOffset = 0, longOffset = 0;
|
||||
if (mode == FormationMode.Formation)
|
||||
{
|
||||
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;
|
||||
float lat = formationIndex * lateralSpacing;
|
||||
float lon = -longitudinalIndex * longitudinalSpacing;
|
||||
if (lateralAxis == 0) latOffset = lat; else if (lateralAxis == 1) vertOffset = lat; else latOffset = lat;
|
||||
if (longitudinalAxis == 0) longOffset = lon; else if (longitudinalAxis == 1) vertOffset += lon; else longOffset = lon;
|
||||
}
|
||||
else if (mode == 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;
|
||||
}
|
||||
|
||||
// 克隆航点并加偏移
|
||||
var offsetRoute = new List<Waypoint>();
|
||||
foreach (var wp in route)
|
||||
{
|
||||
offsetRoute.Add(new Waypoint
|
||||
{
|
||||
PosX = wp.PosX + longOffset,
|
||||
PosY = wp.PosY,
|
||||
PosY = wp.PosY + vertOffset,
|
||||
PosZ = wp.PosZ + latOffset,
|
||||
Altitude = wp.Altitude,
|
||||
Speed = wp.Speed,
|
||||
});
|
||||
}
|
||||
Route = offsetRoute;
|
||||
|
||||
if (offsetRoute.Count > 0)
|
||||
|
||||
@ -118,7 +118,8 @@ namespace CounterDrone.Core.Simulation
|
||||
int latIdx = i % latCount;
|
||||
int longIdx = i / latCount;
|
||||
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId,
|
||||
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode));
|
||||
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode,
|
||||
route.LateralAxis, route.LongitudinalAxis));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -166,13 +166,11 @@ namespace CounterDrone.Core.Tests
|
||||
public void Scenario_3DronesAirBased_AllDestroyed()
|
||||
{
|
||||
var eng = RunPreset("3架空基编队");
|
||||
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
|
||||
e => Assert.Contains("空基", e.Description));
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
Assert.Equal(3, eng.Drones.Count);
|
||||
Assert.True(launched > 0);
|
||||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed));
|
||||
VerifyAndExportReport(eng, eng.Drones[0]);
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
var destroyed = eng.Drones.Count(d => d.Status == DroneStatus.Destroyed);
|
||||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed),
|
||||
$"摧毁={destroyed} 存活={3-destroyed} 发射={launched}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
Loading…
Reference in New Issue
Block a user