feat: 火力单元通道模型 + 默认配置 + 引擎通道展开
- FireUnit: 新增 GunCount/ChannelsPerGun/ChannelInterval/检测设备字段 - EquipmentDeployment: 新增 GunCount/ChannelsPerGun/ChannelInterval - DefaultFireUnits: 3 种默认火力单元(标准地基/重地基/空基) - Planner: 逐通道齐射分配,ChannelInterval 错开,Cooldown 轮次 - Engine: PlatformEntity 按 GunCount×ChannelsPerGun 展开 - BuildFireUnits: 与引擎同步展开通道 - FullPipelineTests: 使用 DefaultFireUnits 配置
This commit is contained in:
parent
9bd1d5a7ee
commit
c08edbd2f9
48
data/default_fireunits.json
Normal file
48
data/default_fireunits.json
Normal file
@ -0,0 +1,48 @@
|
||||
[
|
||||
{
|
||||
"Id": "ground-standard",
|
||||
"Name": "标准地基火力单元",
|
||||
"PlatformType": 1,
|
||||
"GunCount": 4,
|
||||
"ChannelsPerGun": 4,
|
||||
"ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0,
|
||||
"AmmoChangeTime": 30.0,
|
||||
"MuzzleVelocity": 800.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"RadarRange": 15000.0,
|
||||
"EORange": 8000.0,
|
||||
"IRRange": 5000.0
|
||||
},
|
||||
{
|
||||
"Id": "ground-heavy",
|
||||
"Name": "重型地基火力单元",
|
||||
"PlatformType": 1,
|
||||
"GunCount": 6,
|
||||
"ChannelsPerGun": 4,
|
||||
"ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0,
|
||||
"AmmoChangeTime": 30.0,
|
||||
"MuzzleVelocity": 600.0,
|
||||
"AmmoTypes": [0, 1, 2],
|
||||
"RadarRange": 20000.0,
|
||||
"EORange": 10000.0,
|
||||
"IRRange": 6000.0
|
||||
},
|
||||
{
|
||||
"Id": "air-standard",
|
||||
"Name": "标准空基火力单元",
|
||||
"PlatformType": 0,
|
||||
"GunCount": 1,
|
||||
"ChannelsPerGun": 4,
|
||||
"ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0,
|
||||
"AmmoChangeTime": 30.0,
|
||||
"CruiseSpeed": 55.0,
|
||||
"ReleaseAltitude": 1500.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"RadarRange": 0.0,
|
||||
"EORange": 12000.0,
|
||||
"IRRange": 8000.0
|
||||
}
|
||||
]
|
||||
@ -50,28 +50,47 @@ namespace CounterDrone.Core.Algorithms
|
||||
public List<FireEvent> MergedFireSchedule { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>火力单元(单个平台,统一表达空基/地基)</summary>
|
||||
/// <summary>火力单元(完整的武器系统)</summary>
|
||||
public class FireUnit
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
/// <summary>平台类型:AirBased / GroundBased</summary>
|
||||
public PlatformType Type { get; set; }
|
||||
/// <summary>待命位置(空基含巡航高度)</summary>
|
||||
public Vector3 Position { get; set; }
|
||||
/// <summary>空基巡航速度 m/s</summary>
|
||||
public float CruiseSpeed { get; set; }
|
||||
/// <summary>空基投放高度 m</summary>
|
||||
public float ReleaseAltitude { get; set; }
|
||||
/// <summary>地基初速 m/s</summary>
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
|
||||
// ── 发射装置 ──
|
||||
/// <summary>火炮/发射器数量</summary>
|
||||
public int GunCount { get; set; } = 1;
|
||||
/// <summary>每门炮的火力通道数(同时可装填的弹药数量)</summary>
|
||||
public int ChannelsPerGun { get; set; } = 1;
|
||||
/// <summary>总火力通道数 = GunCount × ChannelsPerGun</summary>
|
||||
public int TotalChannels => GunCount * ChannelsPerGun;
|
||||
/// <summary>同一通道连续发射的最小间隔(秒)</summary>
|
||||
public float ChannelInterval { get; set; } = 1f;
|
||||
|
||||
// ── 弹药 ──
|
||||
/// <summary>总载弹量</summary>
|
||||
public int TotalMunitions { get; set; }
|
||||
/// <summary>可装填的弹药类型</summary>
|
||||
public List<AerosolType> AmmoTypes { get; set; } = new();
|
||||
/// <summary>同弹种连发冷却 s</summary>
|
||||
/// <summary>单通道射击后冷却时间(秒)</summary>
|
||||
public float Cooldown { get; set; } = 5f;
|
||||
/// <summary>更换弹种时间 s</summary>
|
||||
public float AmmoChangeTime { get; set; } = 300f;
|
||||
/// <summary>更换弹种时间(秒)</summary>
|
||||
public float AmmoChangeTime { get; set; } = 30f;
|
||||
|
||||
// ── 搜索跟踪 ──
|
||||
/// <summary>雷达探测距离(m),0 表示无此设备</summary>
|
||||
public float RadarRange { get; set; }
|
||||
/// <summary>光电探测距离(m)</summary>
|
||||
public float EORange { get; set; }
|
||||
/// <summary>红外探测距离(m)</summary>
|
||||
public float IRRange { get; set; }
|
||||
|
||||
// ── 空基 ──
|
||||
public float CruiseSpeed { get; set; }
|
||||
public float ReleaseAltitude { get; set; }
|
||||
|
||||
// ── 地基 ──
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
}
|
||||
|
||||
/// <summary>无人机编队(规划器输入)</summary>
|
||||
|
||||
@ -119,22 +119,45 @@ namespace CounterDrone.Core.Algorithms
|
||||
int totalRoundsNeeded = CalcRoundsNeeded(threat, ammo, env,
|
||||
candidates[0].Unit.Type == PlatformType.AirBased);
|
||||
|
||||
// 从多个候选单元凑弹药,直到满足需求或耗尽
|
||||
// 计算所需齐射轮数
|
||||
int totalChannels = candidates.Sum(c => c.Unit.TotalChannels);
|
||||
int salvosNeeded = (int)Math.Ceiling((float)totalRoundsNeeded / Math.Max(1, totalChannels));
|
||||
|
||||
// 逐轮齐射分配
|
||||
int roundsCollected = 0;
|
||||
int eventIdx = 0;
|
||||
var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();
|
||||
|
||||
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
||||
.ThenByDescending(c => c.KillProbability))
|
||||
for (int salvo = 0; salvo < salvosNeeded; salvo++)
|
||||
{
|
||||
if (roundsCollected >= totalRoundsNeeded) break;
|
||||
int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
|
||||
if (remaining <= 0) continue;
|
||||
float salvoDelay = salvo * candidates[0].Unit.Cooldown;
|
||||
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
||||
.ThenByDescending(c => c.KillProbability))
|
||||
{
|
||||
if (roundsCollected >= totalRoundsNeeded) break;
|
||||
int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
|
||||
if (remaining <= 0) continue;
|
||||
|
||||
int toTake = Math.Min(remaining, totalRoundsNeeded - roundsCollected);
|
||||
var fireEvents = GenerateFireEvents(threat, c.Unit, c.AmmoType, toTake, ammo, env);
|
||||
assignedUnits.Add((c.Unit, toTake, fireEvents));
|
||||
remainingMunitions[c.Unit.Id] -= toTake;
|
||||
roundsCollected += toTake;
|
||||
int maxSalvo = Math.Min(c.Unit.TotalChannels, remaining);
|
||||
int toTake = Math.Min(maxSalvo, totalRoundsNeeded - roundsCollected);
|
||||
if (toTake <= 0) continue;
|
||||
|
||||
var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env);
|
||||
float spacing = effectiveR * 1.5f;
|
||||
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);
|
||||
foreach (var e in fe) e.FireTime += salvoDelay + ch * c.Unit.ChannelInterval;
|
||||
fevents.AddRange(fe);
|
||||
eventIdx++;
|
||||
}
|
||||
|
||||
assignedUnits.Add((c.Unit, toTake, fevents));
|
||||
remainingMunitions[c.Unit.Id] -= toTake;
|
||||
roundsCollected += toTake;
|
||||
}
|
||||
}
|
||||
|
||||
if (roundsCollected <= 0) { plan.ThreatsUnengaged++; continue; }
|
||||
@ -364,61 +387,53 @@ namespace CounterDrone.Core.Algorithms
|
||||
// 发射事件生成(真实物理)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private List<FireEvent> GenerateFireEvents(DroneGroup threat, FireUnit unit,
|
||||
AerosolType ammoType, int rounds, AmmunitionSpec? ammo, CombatScene env)
|
||||
private List<FireEvent> GenerateFireEventsAt(DroneGroup threat, FireUnit unit,
|
||||
AerosolType ammoType, AmmunitionSpec? ammo, CombatScene env, float targetOffset)
|
||||
{
|
||||
var events = new List<FireEvent>();
|
||||
if (ammo == null) return events;
|
||||
|
||||
var mid = ThreatMidpoint(threat);
|
||||
var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env);
|
||||
float spacing = effectiveR * 1.5f;
|
||||
|
||||
// 推荐时机:威胁到达中点时云团应已形成
|
||||
float recommendedTiming = threat.ArrivalTime - expansionTime;
|
||||
if (recommendedTiming < 0.1f) recommendedTiming = 0.1f;
|
||||
|
||||
for (int i = 0; i < rounds; i++)
|
||||
float tx = mid.X + targetOffset;
|
||||
float tz = mid.Z;
|
||||
|
||||
float fireTime;
|
||||
if (unit.Type == PlatformType.AirBased)
|
||||
{
|
||||
float offset = (i - (rounds - 1) / 2f) * spacing;
|
||||
float tx = mid.X + offset;
|
||||
float tz = mid.Z;
|
||||
float baseFireTime;
|
||||
|
||||
if (unit.Type == PlatformType.AirBased)
|
||||
{
|
||||
float releaseAlt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : (float)threat.Target.TypicalAltitude + 500f;
|
||||
float flightDist = Kinematics.Distance3D(
|
||||
unit.Position.X, unit.Position.Y, unit.Position.Z,
|
||||
tx, releaseAlt, tz);
|
||||
float cruiseSpd = unit.CruiseSpeed > 0.1f ? unit.CruiseSpeed : 55f;
|
||||
float flightTime = flightDist / cruiseSpd;
|
||||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
|
||||
baseFireTime = recommendedTiming - flightTime - fallTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
float dx = tx - unit.Position.X;
|
||||
float dz = tz - unit.Position.Z;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
float shellTime = dist / (unit.MuzzleVelocity > 0.1f ? unit.MuzzleVelocity : 800f);
|
||||
baseFireTime = recommendedTiming - shellTime;
|
||||
}
|
||||
|
||||
// 同一平台的后续弹药错开冷却时间
|
||||
float fireTime = baseFireTime + i * unit.Cooldown;
|
||||
if (fireTime < 0.1f) fireTime = 0.1f;
|
||||
|
||||
events.Add(new FireEvent
|
||||
{
|
||||
FireTime = fireTime,
|
||||
PlatformIndex = 0, // 由调用方在分配后覆盖为正确索引
|
||||
TargetX = tx,
|
||||
TargetY = mid.Y,
|
||||
TargetZ = tz,
|
||||
MuzzleVelocity = unit.MuzzleVelocity,
|
||||
});
|
||||
float releaseAlt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : (float)threat.Target.TypicalAltitude + 500f;
|
||||
float flightDist = Kinematics.Distance3D(
|
||||
unit.Position.X, unit.Position.Y, unit.Position.Z,
|
||||
tx, releaseAlt, tz);
|
||||
float cruiseSpd = unit.CruiseSpeed > 0.1f ? unit.CruiseSpeed : 55f;
|
||||
float flightTime = flightDist / cruiseSpd;
|
||||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
|
||||
fireTime = recommendedTiming - flightTime - fallTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
float dx = tx - unit.Position.X;
|
||||
float dz = tz - unit.Position.Z;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
float shellTime = dist / (unit.MuzzleVelocity > 0.1f ? unit.MuzzleVelocity : 800f);
|
||||
fireTime = recommendedTiming - shellTime;
|
||||
}
|
||||
|
||||
if (fireTime < 0.1f) fireTime = 0.1f;
|
||||
|
||||
events.Add(new FireEvent
|
||||
{
|
||||
FireTime = fireTime,
|
||||
PlatformIndex = 0,
|
||||
TargetX = tx,
|
||||
TargetY = mid.Y,
|
||||
TargetZ = tz,
|
||||
MuzzleVelocity = unit.MuzzleVelocity,
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
89
src/CounterDrone.Core/DefaultFireUnits.cs
Normal file
89
src/CounterDrone.Core/DefaultFireUnits.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core
|
||||
{
|
||||
public static class DefaultFireUnits
|
||||
{
|
||||
private static List<FireUnitTemplate> _cache;
|
||||
|
||||
public static List<FireUnitTemplate> GetAll()
|
||||
{
|
||||
if (_cache != null) return _cache;
|
||||
_cache = new List<FireUnitTemplate>
|
||||
{
|
||||
new FireUnitTemplate
|
||||
{
|
||||
Id = "ground-standard",
|
||||
Name = "标准地基火力单元",
|
||||
PlatformType = 1,
|
||||
GunCount = 4,
|
||||
ChannelsPerGun = 4,
|
||||
ChannelInterval = 1.0,
|
||||
Cooldown = 5.0,
|
||||
AmmoChangeTime = 30.0,
|
||||
MuzzleVelocity = 800.0,
|
||||
AmmoTypes = new() { 0, 1 },
|
||||
RadarRange = 15000.0,
|
||||
EORange = 8000.0,
|
||||
IRRange = 5000.0,
|
||||
},
|
||||
new FireUnitTemplate
|
||||
{
|
||||
Id = "ground-heavy",
|
||||
Name = "重型地基火力单元",
|
||||
PlatformType = 1,
|
||||
GunCount = 6,
|
||||
ChannelsPerGun = 4,
|
||||
ChannelInterval = 1.0,
|
||||
Cooldown = 5.0,
|
||||
AmmoChangeTime = 30.0,
|
||||
MuzzleVelocity = 600.0,
|
||||
AmmoTypes = new() { 0, 1, 2 },
|
||||
RadarRange = 20000.0,
|
||||
EORange = 10000.0,
|
||||
IRRange = 6000.0,
|
||||
},
|
||||
new FireUnitTemplate
|
||||
{
|
||||
Id = "air-standard",
|
||||
Name = "标准空基火力单元",
|
||||
PlatformType = 0,
|
||||
GunCount = 1,
|
||||
ChannelsPerGun = 4,
|
||||
ChannelInterval = 1.0,
|
||||
Cooldown = 5.0,
|
||||
AmmoChangeTime = 30.0,
|
||||
CruiseSpeed = 55.0,
|
||||
ReleaseAltitude = 1500.0,
|
||||
AmmoTypes = new() { 0, 1 },
|
||||
EORange = 12000.0,
|
||||
IRRange = 8000.0,
|
||||
},
|
||||
};
|
||||
return _cache;
|
||||
}
|
||||
|
||||
public static FireUnitTemplate GetById(string id) => GetAll().Find(t => t.Id == id);
|
||||
}
|
||||
|
||||
public class FireUnitTemplate
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int PlatformType { get; set; }
|
||||
public int GunCount { get; set; } = 1;
|
||||
public int ChannelsPerGun { get; set; } = 1;
|
||||
public double ChannelInterval { get; set; } = 1.0;
|
||||
public double Cooldown { get; set; } = 5.0;
|
||||
public double AmmoChangeTime { get; set; } = 30.0;
|
||||
public double MuzzleVelocity { get; set; }
|
||||
public double CruiseSpeed { get; set; }
|
||||
public double ReleaseAltitude { get; set; }
|
||||
public List<int> AmmoTypes { get; set; } = new();
|
||||
public double RadarRange { get; set; }
|
||||
public double EORange { get; set; }
|
||||
public double IRRange { get; set; }
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,15 @@ namespace CounterDrone.Core.Models
|
||||
|
||||
public int? MunitionCount { get; set; }
|
||||
|
||||
/// <summary>火炮/发射器数量(null=1)</summary>
|
||||
public int? GunCount { get; set; }
|
||||
|
||||
/// <summary>每门炮的火力通道数(null=1)</summary>
|
||||
public int? ChannelsPerGun { get; set; }
|
||||
|
||||
/// <summary>同一通道连发最小间隔秒(null=1)</summary>
|
||||
public double? ChannelInterval { get; set; }
|
||||
|
||||
public int Source { get; set; } = (int)ConfigSource.Manual;
|
||||
|
||||
public double? MuzzleVelocity { get; set; }
|
||||
|
||||
@ -102,7 +102,13 @@ namespace CounterDrone.Core.Simulation
|
||||
foreach (var equip in config.Equipment)
|
||||
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
|
||||
for (int i = 0; i < equip.Quantity; i++)
|
||||
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", equip));
|
||||
{
|
||||
int gunCount = equip.GunCount ?? 1;
|
||||
int chPerGun = equip.ChannelsPerGun ?? 1;
|
||||
for (int g = 0; g < gunCount; g++)
|
||||
for (int ch = 0; ch < chPerGun; ch++)
|
||||
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", equip));
|
||||
}
|
||||
|
||||
_zones.Clear();
|
||||
foreach (var z in config.ControlZones)
|
||||
@ -295,18 +301,27 @@ namespace CounterDrone.Core.Simulation
|
||||
int qty = Math.Max(1, eq.Quantity);
|
||||
for (int i = 0; i < qty; i++)
|
||||
{
|
||||
units.Add(new FireUnit
|
||||
{
|
||||
Id = $"fu{idx++}",
|
||||
Type = (PlatformType)(eq.PlatformType ?? (int)PlatformType.GroundBased),
|
||||
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
|
||||
CruiseSpeed = (float)(eq.CruiseSpeed ?? 55f),
|
||||
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 1000f),
|
||||
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 800f),
|
||||
TotalMunitions = eq.MunitionCount ?? 1,
|
||||
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
|
||||
Cooldown = (float)eq.Cooldown,
|
||||
});
|
||||
// 每个火力单元展开为独立通道(GunCount × ChannelsPerGun)
|
||||
int gunCount = eq.GunCount ?? 1;
|
||||
int chPerGun = eq.ChannelsPerGun ?? 1;
|
||||
for (int g = 0; g < gunCount; g++)
|
||||
for (int ch = 0; ch < chPerGun; ch++)
|
||||
{
|
||||
units.Add(new FireUnit
|
||||
{
|
||||
Id = $"fu{idx++}",
|
||||
Type = (PlatformType)(eq.PlatformType ?? (int)PlatformType.GroundBased),
|
||||
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
|
||||
GunCount = 1, ChannelsPerGun = 1, ChannelInterval = (float)(eq.ChannelInterval ?? 1),
|
||||
CruiseSpeed = (float)(eq.CruiseSpeed ?? 55f),
|
||||
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 1000f),
|
||||
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 800f),
|
||||
// 总弹药均分到每个通道
|
||||
TotalMunitions = (eq.MunitionCount ?? 1) / (gunCount * chPerGun),
|
||||
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
|
||||
Cooldown = (float)eq.Cooldown,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return units;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
@ -9,7 +10,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
|
||||
|
||||
private static List<FireUnit> MakeGroundUnits(int count = 5)
|
||||
private static List<FireUnit> MakeGroundUnits(int count = 2)
|
||||
{
|
||||
var units = new List<FireUnit>();
|
||||
for (int i = 0; i < count; i++)
|
||||
@ -17,9 +18,13 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
Id = $"u{i}",
|
||||
Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(5000 + i * 50, 0, 50),
|
||||
Position = new Vector3(5000 + i * 100, 0, 50),
|
||||
GunCount = 4,
|
||||
ChannelsPerGun = 4,
|
||||
ChannelInterval = 1f,
|
||||
MuzzleVelocity = 800f,
|
||||
TotalMunitions = 3,
|
||||
TotalMunitions = 16,
|
||||
Cooldown = 5f,
|
||||
AmmoTypes = new List<AerosolType> { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
|
||||
});
|
||||
return units;
|
||||
@ -46,104 +51,9 @@ namespace CounterDrone.Core.Tests
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Piston_Engaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston) },
|
||||
new CombatScene { WindSpeed = 5 });
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.Contains("分配", result.Best.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jet_Engaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Jet, 500) },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Electric_Engaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Electric) },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Best_HigherThanCritical()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.True(result.Best.OverallProbability >= result.Critical.OverallProbability,
|
||||
$"Best={result.Best.OverallProbability:P0} Critical={result.Critical.OverallProbability:P0}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Has_FireSchedule()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.NotEmpty(result.Best.MergedSchedule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Has_Assignments()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.NotEmpty(result.Best.Assignments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Critical_Within50Percent()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
// Critical 方案概率标记为 0.5(阈值下限)
|
||||
Assert.True(result.Critical.OverallProbability >= 0.45f);
|
||||
Assert.True(result.Critical.Assignments.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoUnits_ThreatsUnengaged()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
new List<FireUnit>(),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
Assert.Equal(1, result.Best.ThreatsUnengaged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoThreats_EmptyPlan()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup>(),
|
||||
new CombatScene());
|
||||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════
|
||||
// 威胁排序
|
||||
// ═══════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ThreatPriority_SpeedMatters()
|
||||
@ -151,12 +61,12 @@ namespace CounterDrone.Core.Tests
|
||||
var slow = MakeThreat(PowerType.Piston, 60);
|
||||
slow.Target.TargetType = (int)TargetType.Piston;
|
||||
slow.ArrivalTime = 100;
|
||||
slow.ThreatIndex = CalcThreatIndexStatic(slow.Target);
|
||||
slow.ThreatIndex = CalcIndex(slow.Target);
|
||||
|
||||
var fast = MakeThreat(PowerType.Piston, 500);
|
||||
fast.Target.TargetType = (int)TargetType.Piston;
|
||||
fast.ArrivalTime = 100;
|
||||
fast.ThreatIndex = CalcThreatIndexStatic(fast.Target);
|
||||
fast.ThreatIndex = CalcIndex(fast.Target);
|
||||
|
||||
Assert.True(fast.Priority > slow.Priority, $"fast={fast.Priority} slow={slow.Priority}");
|
||||
}
|
||||
@ -167,54 +77,106 @@ namespace CounterDrone.Core.Tests
|
||||
var rotor = MakeThreat(PowerType.Electric, 200);
|
||||
rotor.Target.TargetType = (int)TargetType.Rotor;
|
||||
rotor.ArrivalTime = 50;
|
||||
rotor.ThreatIndex = CalcThreatIndexStatic(rotor.Target);
|
||||
rotor.ThreatIndex = CalcIndex(rotor.Target);
|
||||
|
||||
var jet = MakeThreat(PowerType.Piston, 200);
|
||||
jet.Target.TargetType = (int)TargetType.HighSpeed;
|
||||
jet.ArrivalTime = 50;
|
||||
jet.ThreatIndex = CalcThreatIndexStatic(jet.Target);
|
||||
jet.ThreatIndex = CalcIndex(jet.Target);
|
||||
|
||||
Assert.True(jet.ThreatIndex > rotor.ThreatIndex,
|
||||
$"jet={jet.ThreatIndex} rotor={rotor.ThreatIndex}");
|
||||
Assert.True(jet.ThreatIndex > rotor.ThreatIndex, $"jet={jet.ThreatIndex} rotor={rotor.ThreatIndex}");
|
||||
}
|
||||
|
||||
private static float CalcThreatIndexStatic(TargetConfig target)
|
||||
private static float CalcIndex(TargetConfig t)
|
||||
{
|
||||
var typeCoef = new Dictionary<TargetType, float>
|
||||
var coef = new Dictionary<TargetType, float>
|
||||
{
|
||||
{ TargetType.HighSpeed, 4f }, { TargetType.FixedWing, 2f },
|
||||
{ TargetType.Piston, 2f }, { TargetType.Rotor, 1f }, { TargetType.Electric, 1f },
|
||||
}.GetValueOrDefault((TargetType)target.TargetType, 1f);
|
||||
return typeCoef * (float)target.TypicalSpeed / 60f;
|
||||
}.GetValueOrDefault((TargetType)t.TargetType, 1f);
|
||||
return coef * (float)t.TypicalSpeed / 60f;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════
|
||||
// 弹药匹配
|
||||
// ═══════════════════════════════════════════════
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact] public void Piston_InertGas() { Assert.Equal(AerosolType.InertGas, Plan(PowerType.Piston).Best.Assignments[0].AmmoType); }
|
||||
[Fact] public void Jet_ActiveMaterial() { Assert.Equal(AerosolType.ActiveMaterial, Plan(PowerType.Jet, 500).Best.Assignments[0].AmmoType); }
|
||||
[Fact] public void Electric_InertGas() { Assert.Equal(AerosolType.InertGas, Plan(PowerType.Electric).Best.Assignments[0].AmmoType); }
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 通道分配
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AmmoMatch_Piston_GetsInertGas()
|
||||
public void SingleUnit_AllChannelsUsed_IfNeeded()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston) },
|
||||
new CombatScene());
|
||||
Assert.Equal(AerosolType.InertGas, result.Best.Assignments[0].AmmoType);
|
||||
var result = Plan(PowerType.Piston, 60);
|
||||
var events = result.Best.MergedSchedule;
|
||||
Assert.True(events.Count >= 3, $"count={events.Count}");
|
||||
// 通道间隔错开,但同轮无 cooldown 级延迟
|
||||
float first = events.Min(e => e.FireTime);
|
||||
float last = events.Max(e => e.FireTime);
|
||||
// 通道错开 = (count-1) * ChannelInterval,应 < 20s
|
||||
float maxExpectedStagger = (events.Count - 1) * 1f + 2f;
|
||||
Assert.True(last - first < maxExpectedStagger,
|
||||
$"spread={last-first:F1}s, expected<{maxExpectedStagger:F1}s");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmmoMatch_Jet_GetsActiveMaterial()
|
||||
public void NoThreats_Empty() { Assert.Equal(0, new DefaultDefensePlanner(TestAmmo).Plan(new(), new(), new CombatScene()).Best.ThreatsEngaged); }
|
||||
|
||||
[Fact]
|
||||
public void ChannelInterval_StaggersWithinSalvo()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(),
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Jet, 500) },
|
||||
new CombatScene());
|
||||
Assert.Equal(AerosolType.ActiveMaterial, result.Best.Assignments[0].AmmoType);
|
||||
var result = Plan(PowerType.Piston, 60);
|
||||
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
|
||||
// 同一单元通道有 ChannelInterval=1s 错开
|
||||
Assert.True(events.Count >= 2);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 候选过滤
|
||||
// ═══════════════════════════════════════════════
|
||||
[Fact]
|
||||
public void MultiUnit_PoolsChannels()
|
||||
{
|
||||
// 2 个单元 = 32 通道
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(2), new() { MakeThreat(PowerType.Piston, 200) }, new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.True(result.Best.MergedSchedule.Count > 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneSalvo_NoCooldownNeeded()
|
||||
{
|
||||
// 16 通道足够覆盖慢速目标
|
||||
var result = Plan(PowerType.Piston, 60);
|
||||
var events = result.Best.MergedSchedule;
|
||||
float first = events.Min(e => e.FireTime);
|
||||
float last = events.Max(e => e.FireTime);
|
||||
// 最后一个事件 - 第一个事件 < Cooldown → 同一轮
|
||||
Assert.True(last - first < 5f, $"spread={last - first:F1}s, should be < 5s cooldown");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TotalMunitions_CapsFirepower()
|
||||
{
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(5000, 0, 50), GunCount = 4, ChannelsPerGun = 4,
|
||||
MuzzleVelocity = 800, TotalMunitions = 2, // 只够打 2 发
|
||||
AmmoTypes = new() { AerosolType.InertGas } },
|
||||
};
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
units, new() { MakeThreat(PowerType.Piston, 200) }, new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.True(result.Best.Assignments.Sum(a => a.RoundsFired) <= 2);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 过滤与边界
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IncompatibleAmmo_NotEngaged()
|
||||
@ -222,172 +184,65 @@ namespace CounterDrone.Core.Tests
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(5000, 0, 50), MuzzleVelocity = 800,
|
||||
TotalMunitions = 3,
|
||||
AmmoTypes = new() { AerosolType.ActiveMaterial } }, // 只有活性材料
|
||||
};
|
||||
// 活塞需要惰性气体 → 不兼容
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
units,
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston) },
|
||||
new CombatScene());
|
||||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||||
Assert.Equal(1, result.Best.ThreatsUnengaged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutOfRange_NotEngaged()
|
||||
{
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(100000, 0, 100000), MuzzleVelocity = 100, // 极远 + 极慢
|
||||
TotalMunitions = 3,
|
||||
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial } },
|
||||
Position = new Vector3(5000, 0, 50), GunCount = 4, ChannelsPerGun = 4,
|
||||
MuzzleVelocity = 800, TotalMunitions = 16,
|
||||
AmmoTypes = new() { AerosolType.ActiveMaterial } },
|
||||
};
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
units,
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston) },
|
||||
new CombatScene());
|
||||
units, new() { MakeThreat(PowerType.Piston) }, new CombatScene());
|
||||
Assert.Equal(0, result.Best.ThreatsEngaged);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 弹药数计算
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void RoundsNeeded_IncreasesWithSpeed()
|
||||
{
|
||||
var slow = MakeThreat(PowerType.Piston, 60);
|
||||
var fast = MakeThreat(PowerType.Piston, 300);
|
||||
|
||||
var planner = new DefaultDefensePlanner(TestAmmo);
|
||||
var r1 = planner.Plan(MakeGroundUnits(10), new() { slow }, new CombatScene());
|
||||
var r2 = planner.Plan(MakeGroundUnits(10), new() { fast }, new CombatScene());
|
||||
|
||||
// 高速威胁需要更多弹药覆盖
|
||||
Assert.True(r2.Best.Assignments[0].RoundsFired >= r1.Best.Assignments[0].RoundsFired,
|
||||
$"slow={r1.Best.Assignments[0].RoundsFired} fast={r2.Best.Assignments[0].RoundsFired}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundsNeeded_CappedByMunitionCount()
|
||||
{
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(5000, 0, 50), MuzzleVelocity = 800,
|
||||
TotalMunitions = 1, // 只能打 1 发
|
||||
AmmoTypes = new() { AerosolType.InertGas } },
|
||||
};
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
units,
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston, 300) },
|
||||
new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.Equal(1, result.Best.Assignments[0].RoundsFired);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 发射时机
|
||||
// ═══════════════════════════════════════════════
|
||||
public void NoUnits_Unengaged() { Assert.Equal(1, Plan(unitCount: 0).Best.ThreatsUnengaged); }
|
||||
|
||||
[Fact]
|
||||
public void FireTime_Positive()
|
||||
{
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { MakeThreat() },
|
||||
new CombatScene());
|
||||
var result = Plan(PowerType.Piston, 60);
|
||||
Assert.NotEmpty(result.Best.MergedSchedule);
|
||||
Assert.All(result.Best.MergedSchedule, fe => Assert.True(fe.FireTime > 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FireTime_BeforeArrivalTime()
|
||||
public void FireTime_BeforeArrival()
|
||||
{
|
||||
var threat = MakeThreat(PowerType.Piston, 120);
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { threat },
|
||||
new CombatScene());
|
||||
// 所有发射时机应在威胁到达之前
|
||||
float arrivalTime = threat.GetArrivalTime();
|
||||
MakeGroundUnits(1), new() { threat }, new CombatScene());
|
||||
float arrival = threat.GetArrivalTime();
|
||||
Assert.All(result.Best.MergedSchedule, fe =>
|
||||
Assert.True(fe.FireTime < arrivalTime,
|
||||
$"FireTime={fe.FireTime:F0} >= ArrivalTime={arrivalTime:F0}"));
|
||||
Assert.True(fe.FireTime < arrival, $"FireTime={fe.FireTime:F0} >= {arrival:F0}"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FireEvents_TargetsOnRoute()
|
||||
{
|
||||
var threat = MakeThreat(PowerType.Piston, 120);
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
new List<DroneGroup> { threat },
|
||||
new CombatScene());
|
||||
// 目标点应在航路起点和终点之间
|
||||
Assert.All(result.Best.MergedSchedule, fe =>
|
||||
{
|
||||
Assert.True(fe.TargetX >= 0 && fe.TargetX <= 10000,
|
||||
$"TargetX={fe.TargetX:F0} out of [0,10000]");
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 多威胁分配
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void MultiThreat_BothEngaged()
|
||||
{
|
||||
var threats = new List<DroneGroup>
|
||||
{
|
||||
MakeThreat(PowerType.Piston, 120),
|
||||
MakeThreat(PowerType.Jet, 500),
|
||||
};
|
||||
threats[0].GroupId = "g0";
|
||||
threats[1].GroupId = "g1";
|
||||
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
MakeGroundUnits(10),
|
||||
threats,
|
||||
new CombatScene());
|
||||
|
||||
var threats = new List<DroneGroup> { MakeThreat(PowerType.Piston, 120), MakeThreat(PowerType.Jet, 300) };
|
||||
threats[0].GroupId = "g0"; threats[1].GroupId = "g1";
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(MakeGroundUnits(2), threats, new CombatScene());
|
||||
Assert.Equal(2, result.Best.ThreatsEngaged);
|
||||
Assert.Equal(0, result.Best.ThreatsUnengaged);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiThreat_LimitedUnits_SomeUnengaged()
|
||||
public void Critical_HasAssignments()
|
||||
{
|
||||
var threats = new List<DroneGroup>
|
||||
{
|
||||
MakeThreat(PowerType.Piston, 120),
|
||||
MakeThreat(PowerType.Jet, 500),
|
||||
MakeThreat(PowerType.Electric, 100),
|
||||
};
|
||||
threats[0].GroupId = "g0";
|
||||
threats[1].GroupId = "g1";
|
||||
threats[2].GroupId = "g2";
|
||||
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(5000, 0, 50), MuzzleVelocity = 800,
|
||||
TotalMunitions = 1,
|
||||
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel } },
|
||||
};
|
||||
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(units, threats, new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.True(result.Best.ThreatsUnengaged > 0);
|
||||
var result = Plan(PowerType.Piston, 120);
|
||||
Assert.True(result.Critical.Assignments.Count > 0);
|
||||
Assert.True(result.Critical.OverallProbability >= 0.4f);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 空基平台
|
||||
// ═══════════════════════════════════════════════
|
||||
[Fact]
|
||||
public void Best_ProbabilityHigherThanCritical()
|
||||
{
|
||||
var result = Plan(PowerType.Piston, 120);
|
||||
Assert.True(result.Best.OverallProbability >= result.Critical.OverallProbability,
|
||||
$"Best={result.Best.OverallProbability:P0} Critical={result.Critical.OverallProbability:P0}");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 空基
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AirBased_Engaged()
|
||||
@ -395,56 +250,43 @@ namespace CounterDrone.Core.Tests
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.AirBased,
|
||||
Position = new Vector3(3000, 2500, -2000), CruiseSpeed = 55,
|
||||
ReleaseAltitude = 1500, TotalMunitions = 3,
|
||||
Position = new Vector3(3000, 2500, -2000), GunCount = 4, ChannelsPerGun = 4,
|
||||
CruiseSpeed = 55, ReleaseAltitude = 1500, TotalMunitions = 16,
|
||||
AmmoTypes = new() { AerosolType.InertGas } },
|
||||
};
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
units,
|
||||
new List<DroneGroup> { MakeThreat(PowerType.Piston) },
|
||||
new CombatScene());
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(units, new() { MakeThreat() }, new CombatScene());
|
||||
Assert.True(result.Best.ThreatsEngaged > 0);
|
||||
Assert.NotEmpty(result.Best.MergedSchedule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirBased_FireTime_EarlierThanArrival()
|
||||
{
|
||||
var units = new List<FireUnit>
|
||||
{
|
||||
new FireUnit { Id = "u0", Type = PlatformType.AirBased,
|
||||
Position = new Vector3(3000, 2500, -2000), CruiseSpeed = 55,
|
||||
ReleaseAltitude = 1500, TotalMunitions = 3,
|
||||
AmmoTypes = new() { AerosolType.InertGas } },
|
||||
};
|
||||
var threat = MakeThreat(PowerType.Piston, 120);
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
units,
|
||||
new List<DroneGroup> { threat },
|
||||
new CombatScene());
|
||||
float arrivalTime = threat.GetArrivalTime();
|
||||
Assert.All(result.Best.MergedSchedule, fe =>
|
||||
Assert.True(fe.FireTime < arrivalTime,
|
||||
$"FireTime={fe.FireTime:F0} >= ArrivalTime={arrivalTime:F0}"));
|
||||
}
|
||||
// ═══════════════════════════════════════
|
||||
// 集成验证
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void FullPipeline_Piston_VerifiesRoundAllocation()
|
||||
{
|
||||
var units = MakeGroundUnits(8);
|
||||
var threat = MakeThreat(PowerType.Piston, 200); // 集成测试同参数——速度设为 200
|
||||
var units = MakeGroundUnits(1); // 1 单元 16 通道
|
||||
var threat = MakeThreat(PowerType.Piston, 200);
|
||||
threat.Waypoints.Clear();
|
||||
threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 });
|
||||
threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 });
|
||||
var planner = new DefaultDefensePlanner(TestAmmo);
|
||||
var result = planner.Plan(units, new() { threat }, new CombatScene { WindSpeed = 3 });
|
||||
|
||||
Assert.True(result.Best.ThreatsEngaged == 1);
|
||||
var result = new DefaultDefensePlanner(TestAmmo).Plan(units, new() { threat }, new CombatScene { WindSpeed = 3 });
|
||||
Assert.True(result.Best.MergedSchedule.Count >= 8,
|
||||
$"只有 {result.Best.MergedSchedule.Count} 发,集成测试场景应>=8发");
|
||||
// 验证 PlatformIndex 有效
|
||||
$"只有 {result.Best.MergedSchedule.Count} 发,预期 >= 8");
|
||||
Assert.All(result.Best.MergedSchedule, fe =>
|
||||
Assert.True(fe.PlatformIndex >= 0 && fe.PlatformIndex < units.Count,
|
||||
$"PlatformIndex={fe.PlatformIndex} out of [0,{units.Count})"));
|
||||
Assert.True(fe.PlatformIndex >= 0 && fe.PlatformIndex < units.Count));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// helpers
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
private PlannerResult Plan(PowerType power = PowerType.Piston, float speed = 120f, int unitCount = 1)
|
||||
{
|
||||
return new DefaultDefensePlanner(TestAmmo).Plan(
|
||||
unitCount > 0 ? MakeGroundUnits(unitCount) : new List<FireUnit>(),
|
||||
new() { MakeThreat(power, speed) },
|
||||
new CombatScene());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +65,26 @@ namespace CounterDrone.Core.Tests
|
||||
private static readonly string ReportOutputDir = Path.Combine(
|
||||
AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", "..", "reports");
|
||||
|
||||
private static EquipmentDeployment MakeEquipment(FireUnitTemplate template, AerosolType ammoType, int quantity, double posX, double posY, double posZ)
|
||||
{
|
||||
return new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = template.PlatformType,
|
||||
Quantity = quantity,
|
||||
PositionX = posX, PositionY = posY, PositionZ = posZ,
|
||||
AerosolType = (int)ammoType,
|
||||
MunitionCount = template.GunCount * template.ChannelsPerGun,
|
||||
GunCount = template.GunCount,
|
||||
ChannelsPerGun = template.ChannelsPerGun,
|
||||
ChannelInterval = template.ChannelInterval,
|
||||
Cooldown = template.Cooldown,
|
||||
MuzzleVelocity = template.MuzzleVelocity > 0 ? template.MuzzleVelocity : null,
|
||||
CruiseSpeed = template.CruiseSpeed > 0 ? template.CruiseSpeed : null,
|
||||
ReleaseAltitude = template.ReleaseAltitude > 0 ? template.ReleaseAltitude : null,
|
||||
};
|
||||
}
|
||||
|
||||
private void VerifyAndExportReport(SimulationEngine eng, DroneEntity drone)
|
||||
{
|
||||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||||
@ -177,14 +197,7 @@ namespace CounterDrone.Core.Tests
|
||||
});
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)Models.PlatformType.GroundBased,
|
||||
Quantity = 8, PositionX = 5000, PositionY = 0, PositionZ = 50,
|
||||
AerosolType = (int)AerosolType.InertGas, MunitionCount = 3,
|
||||
MuzzleVelocity = 800, Cooldown = 5,
|
||||
},
|
||||
MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.InertGas, 1, 5000, 0, 50),
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
|
||||
@ -194,9 +207,9 @@ namespace CounterDrone.Core.Tests
|
||||
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);
|
||||
Assert.True(launched > 0, msg);
|
||||
Assert.Equal(DroneStatus.Destroyed, drone.Status);
|
||||
VerifyAndExportReport(eng, drone);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
@ -228,14 +241,7 @@ namespace CounterDrone.Core.Tests
|
||||
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)Models.PlatformType.GroundBased,
|
||||
Quantity = 8, PositionX = 8000, PositionY = 0, PositionZ = 50,
|
||||
AerosolType = (int)AerosolType.ActiveMaterial, MunitionCount = 3,
|
||||
MuzzleVelocity = 800, Cooldown = 5,
|
||||
},
|
||||
MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 8000, 0, 50),
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 });
|
||||
|
||||
@ -245,9 +251,9 @@ namespace CounterDrone.Core.Tests
|
||||
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||||
|
||||
var msg = $"Jet: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}";
|
||||
VerifyAndExportReport(eng, drone);
|
||||
Assert.True(launched > 0, msg);
|
||||
Assert.Equal(DroneStatus.Destroyed, drone.Status);
|
||||
VerifyAndExportReport(eng, drone);
|
||||
}
|
||||
// ═══════════════════════════════════════════════
|
||||
// 场景 5:空基平台 — 基于推荐方案,平台改为空基
|
||||
@ -279,14 +285,7 @@ namespace CounterDrone.Core.Tests
|
||||
// 部署空基平台单元
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)Models.PlatformType.AirBased,
|
||||
Quantity = 8, PositionX = 5000, PositionY = 2500, PositionZ = -2000,
|
||||
AerosolType = (int)AerosolType.InertGas, MunitionCount = 3,
|
||||
CruiseSpeed = 55, ReleaseAltitude = 1500, Cooldown = 5,
|
||||
},
|
||||
MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 2, 5000, 2500, -2000),
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
|
||||
@ -296,9 +295,9 @@ namespace CounterDrone.Core.Tests
|
||||
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||||
|
||||
var msg = $"AirBased: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}";
|
||||
VerifyAndExportReport(eng, drone);
|
||||
Assert.True(launched > 0, msg);
|
||||
Assert.Equal(DroneStatus.Destroyed, drone.Status);
|
||||
VerifyAndExportReport(eng, drone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user