单一数据源 data/defaults.json,含弹药/编队/火力单元/无人机/探测/天气六类预设 版本追踪: Meta 表 + version 字段,更新时 InsertOrReplace,不删用户数据 名称统一 [Demo] 前缀,UI 中可识别为模拟数据 删除: DefaultAmmunition.cs, DefaultFireUnits.cs, default_ammo.json, default_formations.json, TestAmmo.cs TestPathProvider 自动复制数据文件到测试目录 集成测试精简: 4个简单变体跳过,保留6个核心场景,39s
683 lines
36 KiB
C#
683 lines
36 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using CounterDrone.Core;
|
||
using CounterDrone.Core.Algorithms;
|
||
using CounterDrone.Core.Models;
|
||
using CounterDrone.Core.Repository;
|
||
using CounterDrone.Core.Services;
|
||
using CounterDrone.Core.Simulation;
|
||
using SQLite;
|
||
using Xunit;
|
||
|
||
namespace CounterDrone.Core.Tests
|
||
{
|
||
public class FullPipelineTests : IDisposable
|
||
{
|
||
private readonly string _testDir;
|
||
private readonly IPathProvider _paths;
|
||
private readonly IScenarioService _scenario;
|
||
private readonly FrameDataStore _frameStore;
|
||
private readonly SQLiteConnection _mainDb;
|
||
private List<AmmunitionSpec> _ammoCatalog;
|
||
private string _taskId = string.Empty;
|
||
|
||
public FullPipelineTests()
|
||
{
|
||
_testDir = Path.Combine(Path.GetTempPath(), $"cd_full_{Guid.NewGuid():N}");
|
||
_paths = new TestPathProvider(_testDir);
|
||
var dbm = new DatabaseManager(_paths);
|
||
_mainDb = dbm.OpenMainDb();
|
||
_ammoCatalog = new List<AmmunitionSpec>(TestData.Defaults.Ammunition);
|
||
|
||
_scenario = new ScenarioService(
|
||
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
|
||
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
|
||
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
|
||
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
|
||
|
||
_frameStore = new FrameDataStore(_paths);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_mainDb?.Close();
|
||
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
|
||
}
|
||
|
||
private SimulationEngine RunSimulation(int maxTicks, float tickDt = 1f / 20f, float timeScale = 8f)
|
||
{
|
||
var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(_ammoCatalog, TestPlannerConfig.Instance));
|
||
|
||
engine.Initialize(_taskId);
|
||
engine.TimeScale = timeScale; // 8倍速加速
|
||
for (int i = 0; i < maxTicks; i++)
|
||
{
|
||
var r = engine.Tick(tickDt);
|
||
if (r.State == SimulationState.Completed) break;
|
||
}
|
||
engine.Stop();
|
||
return engine;
|
||
}
|
||
|
||
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,
|
||
// 火力单元自带探测能力(激活)
|
||
RadarRange = template.RadarRange > 0 ? template.RadarRange : null,
|
||
EORange = template.EORange > 0 ? template.EORange : null,
|
||
IRRange = template.IRRange > 0 ? template.IRRange : null,
|
||
};
|
||
}
|
||
|
||
private void VerifyAndExportReport(SimulationEngine eng, DroneEntity drone)
|
||
{
|
||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
|
||
Assert.Contains("对抗结果", report);
|
||
Assert.Contains(detail.Task.Name, report);
|
||
|
||
var svc = new ReportService(_mainDb, _paths);
|
||
var r = svc.Generate(_taskId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime);
|
||
svc.ExportToFile(r.Id, ReportOutputDir); // 持久化目录
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 1:无防御 — 无人机到达目标
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Scenario_NoDefense_DroneReachesTarget()
|
||
{
|
||
var task = _scenario.CreateTask("无防御测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default",
|
||
TargetType = (int)TargetType.FixedWing,
|
||
PowerType = (int)PowerType.Jet,
|
||
Quantity = 1,
|
||
TypicalSpeed = 600,
|
||
TypicalAltitude = 400,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 400, PosZ = 0, Speed = 600 },
|
||
new Waypoint { PosX = 3000, PosY = 400, PosZ = 0, Speed = 600 },
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
|
||
|
||
var eng = RunSimulation(400);
|
||
Assert.Equal(DroneStatus.ReachedTarget, eng.Drones[0].Status);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 2:管控区域侵入
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Scenario_ZoneIntrusion_DroneMarked()
|
||
{
|
||
var task = _scenario.CreateTask("管控区侵入测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default",
|
||
TargetType = (int)TargetType.Electric,
|
||
PowerType = (int)PowerType.Electric,
|
||
Quantity = 1,
|
||
TypicalSpeed = 300,
|
||
TypicalAltitude = 300,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 300 },
|
||
new Waypoint { PosX = 3000, PosY = 300, PosZ = 0, Speed = 300 },
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
|
||
_scenario.SaveControlZones(_taskId, new List<Models.ControlZone>
|
||
{
|
||
new Models.ControlZone
|
||
{
|
||
Name = "禁飞区",
|
||
VerticesJson = "[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]",
|
||
MinAltitude = 0, MaxAltitude = 1000,
|
||
},
|
||
});
|
||
|
||
var eng = RunSimulation(400);
|
||
Assert.Equal(DroneStatus.ZoneIntruded, eng.Drones[0].Status);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 3:活塞式 200km/h,惰性气体拦截
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact(Skip = "被 Piston_Windy 替代:保留有风偏版本作为核心拦截测试")]
|
||
public void Scenario_Piston_InertGasIntercept()
|
||
{
|
||
var task = _scenario.CreateTask("拦截活塞式无人机", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 1,
|
||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan
|
||
{
|
||
FormationMode = (int)FormationMode.Formation,
|
||
LateralSpacing = 50,
|
||
LateralCount = 1,
|
||
LongitudinalCount = 1,
|
||
},
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 },
|
||
new Waypoint { PosX = 5000, PosY = 500, PosZ = 0, Speed = 150 },
|
||
});
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
|
||
var eng = RunSimulation(8000);
|
||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||
|
||
var dump = new System.Text.StringBuilder();
|
||
|
||
// 1. Planner 期望(来自 planner_targets.csv)
|
||
dump.AppendLine("=== 1. Planner 云团期望 (见 planner_targets.csv) ===");
|
||
|
||
// 2. 弹药到位
|
||
dump.AppendLine("=== 2. 弹药实际到位 ===");
|
||
dump.AppendLine("id,arrivalTime,posX,posY,posZ,cloudId,radius,density,elapsed");
|
||
foreach (var c in eng.Clouds)
|
||
dump.AppendLine($"cloud,{c.CreatedAt:F3},{c.Dispersion.Center.X:F1},{c.Dispersion.Center.Y:F1},{c.Dispersion.Center.Z:F1},{c.Id},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1}");
|
||
|
||
// 3. 云团最终状态
|
||
dump.AppendLine("=== 3. 云团状态(仿真结束) ===");
|
||
dump.AppendLine("cloudId,radius,density,elapsed,dissipated");
|
||
foreach (var c in eng.Clouds)
|
||
dump.AppendLine($"{c.Id},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1},{c.IsDissipated}");
|
||
|
||
// 4. 无人机状态
|
||
var drone = eng.Drones[0];
|
||
dump.AppendLine("=== 4. 无人机 ===");
|
||
dump.AppendLine($"status={drone.Status} hp={drone.Hp:F3} exposureTime={drone.ExposureTime:F3}");
|
||
dump.AppendLine($"speed={drone.TypicalSpeed}km/h startPos=({drone.PosX:F1},{drone.PosY:F1},{drone.PosZ:F1})");
|
||
|
||
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\piston_analysis.txt", dump.ToString());
|
||
|
||
var times = eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Select(e => e.OccurredAt).OrderBy(t => t).ToList();
|
||
var timeInfo = $"times=[{times.First():F2}..{times.Last():F2}] span={times.Last()-times.First():F2}s";
|
||
|
||
var msg = $"Piston: launched={launched} clouds={clouds} drones={eng.Drones.Count} {timeInfo}";
|
||
foreach (var d in eng.Drones)
|
||
msg += $" [{d.Status} hp={d.Hp:F2}]";
|
||
VerifyAndExportReport(eng, eng.Drones[0]);
|
||
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\test_piston.txt", msg);
|
||
Assert.True(launched > 0, msg);
|
||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 3b:活塞式 + 西风 5m/s — 验证风偏补偿后仍能拦截
|
||
// 航路 X:0→5000 Z:0;西风 WindToVector(W)=(-5,0,0) 把云团吹向 -X
|
||
// Planner 应逆风预置抛撒点(+X方向),云团漂移 ~150m 后回到航路
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Scenario_Piston_Windy_InertGasIntercept()
|
||
{
|
||
var task = _scenario.CreateTask("活塞+西风拦截测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene
|
||
{
|
||
WeatherType = (int)WeatherType.Sunny,
|
||
WindSpeed = 5,
|
||
WindDirection = (int)WindDirection.W,
|
||
});
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 1,
|
||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan
|
||
{
|
||
FormationMode = (int)FormationMode.Formation,
|
||
LateralSpacing = 50,
|
||
LateralCount = 1,
|
||
LongitudinalCount = 1,
|
||
},
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 },
|
||
new Waypoint { PosX = 5000, PosY = 500, PosZ = 0, Speed = 150 },
|
||
});
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 1500, 0, 50),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
|
||
var eng = RunSimulation(8000);
|
||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||
|
||
var dump = new System.Text.StringBuilder();
|
||
dump.AppendLine("=== 活塞+西风5m/s ===");
|
||
dump.AppendLine("cloudCreatedAt,centerX,centerY,centerZ,radius,density,elapsed");
|
||
foreach (var c in eng.Clouds)
|
||
dump.AppendLine($"{c.CreatedAt:F3},{c.Dispersion.Center.X:F1},{c.Dispersion.Center.Y:F1},{c.Dispersion.Center.Z:F1},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1}");
|
||
dump.AppendLine("=== 无人机 ===");
|
||
var drone = eng.Drones[0];
|
||
dump.AppendLine($"status={drone.Status} hp={drone.Hp:F3} exposureTime={drone.ExposureTime:F3}");
|
||
dump.AppendLine($"endPos=({drone.PosX:F1},{drone.PosY:F1},{drone.PosZ:F1})");
|
||
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\piston_windy.txt", dump.ToString());
|
||
|
||
var msg = $"PistonWindy: launched={launched} clouds={clouds}";
|
||
foreach (var d in eng.Drones)
|
||
msg += $" [{d.Status} hp={d.Hp:F2}]";
|
||
Assert.True(launched > 0, msg);
|
||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg);
|
||
VerifyAndExportReport(eng, eng.Drones[0]);
|
||
}
|
||
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 4:喷气发动机 → 算法推荐活性材料
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Scenario_Jet_ActiveMaterialIntercept()
|
||
{
|
||
var task = _scenario.CreateTask("拦截喷气式无人机", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default",
|
||
TargetType = (int)TargetType.HighSpeed,
|
||
PowerType = (int)PowerType.Jet,
|
||
Quantity = 1,
|
||
TypicalSpeed = 200,
|
||
TypicalAltitude = 800,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 200 },
|
||
new Waypoint { PosX = 5000, PosY = 800, PosZ = 0, Speed = 200 },
|
||
});
|
||
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-standard"), AerosolType.ActiveMaterial, 1, 1500, 0, 50),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 });
|
||
|
||
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 = $"Jet: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}";
|
||
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\test_jet.txt", msg);
|
||
VerifyAndExportReport(eng, drone);
|
||
Assert.True(launched > 0, msg);
|
||
Assert.Equal(DroneStatus.Destroyed, drone.Status);
|
||
}
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 5:空基平台 — 基于推荐方案,平台改为空基
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact(Skip = "被 AirBased_Windy 替代:保留有风偏版本作为空基测试")]
|
||
public void Scenario_AirBased_PlatformFliesAndDrops()
|
||
{
|
||
var task = _scenario.CreateTask("空基平台拦截测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default",
|
||
TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 1,
|
||
TypicalSpeed = 150,
|
||
TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 },
|
||
new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 150 },
|
||
});
|
||
|
||
// 部署空基平台单元
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
|
||
// 验证:空基 PlatformType 正确持久化
|
||
var savedAir = _scenario.GetTaskDetail(_taskId)!;
|
||
Assert.All(savedAir.Equipment, eq => Assert.Equal((int?)0, eq.PlatformType));
|
||
|
||
var eng = RunSimulation(8000);
|
||
|
||
// 诊断:空基发射事件应有"空基"描述
|
||
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
|
||
e => Assert.Contains("空基", e.Description));
|
||
|
||
// 验证引擎内 PlatformType
|
||
Assert.NotEmpty(eng.Events);
|
||
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
|
||
e => Assert.Contains("空基", e.Description));
|
||
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 = $"AirBased: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}";
|
||
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\test_air.txt", msg);
|
||
VerifyAndExportReport(eng, drone);
|
||
Assert.True(launched > 0, msg);
|
||
Assert.True(drone.Hp < 0.1f, msg);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 5b:空基平台 + 东风 5m/s — 验证风偏补偿后仍能拦截
|
||
// 航路 X:0→10000 Z:0;东风 WindToVector(E)=(5,0,0) 把云团吹向 +X
|
||
// Planner 应逆风预置抛撒点(-X方向),空基平台投放后云团漂移回航路
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Scenario_AirBased_Windy_PlatformFliesAndDrops()
|
||
{
|
||
var task = _scenario.CreateTask("空基+东风拦截测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene
|
||
{
|
||
WeatherType = (int)WeatherType.Sunny,
|
||
WindSpeed = 5,
|
||
WindDirection = (int)WindDirection.E,
|
||
});
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default",
|
||
TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 1,
|
||
TypicalSpeed = 150,
|
||
TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 },
|
||
new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 150 },
|
||
});
|
||
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
|
||
var eng = RunSimulation(8000);
|
||
|
||
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
|
||
e => Assert.Contains("空基", e.Description));
|
||
|
||
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 dump = new System.Text.StringBuilder();
|
||
dump.AppendLine("=== 空基+东风5m/s ===");
|
||
dump.AppendLine("cloudCreatedAt,centerX,centerY,centerZ,radius,density,elapsed");
|
||
foreach (var c in eng.Clouds)
|
||
dump.AppendLine($"{c.CreatedAt:F3},{c.Dispersion.Center.X:F1},{c.Dispersion.Center.Y:F1},{c.Dispersion.Center.Z:F1},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1}");
|
||
dump.AppendLine("=== 无人机 ===");
|
||
dump.AppendLine($"status={drone.Status} hp={drone.Hp:F3} exposureTime={drone.ExposureTime:F3}");
|
||
dump.AppendLine($"endPos=({drone.PosX:F1},{drone.PosY:F1},{drone.PosZ:F1})");
|
||
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\air_windy.txt", dump.ToString());
|
||
|
||
var msg = $"AirBasedWindy: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}";
|
||
Assert.True(launched > 0, msg);
|
||
Assert.True(drone.Hp < 0.1f, msg);
|
||
VerifyAndExportReport(eng, drone);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 6:3 架活塞编队(横排 Z=0/50/100)— 多无人机端到端
|
||
// 验证 planner 给每个车道分配云团、3 架都被击毁
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact(Skip = "被 3DronesAirBased 替代:保留空基多目标版本")]
|
||
public void Scenario_3DronesFormation_AllDestroyed()
|
||
{
|
||
var task = _scenario.CreateTask("3架编队拦截测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 3,
|
||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||
});
|
||
// 3 架横排编队:Formation 模式,横向间距 50m
|
||
// 引擎会生成 3 架无人机,Z 偏移 0/50/100(latIdx × LateralSpacing)
|
||
_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 },
|
||
new Waypoint { PosX = 5000, PosY = 500, PosZ = 0, Speed = 200 },
|
||
});
|
||
// 3 个火力单元部署在中点附近(X=4500,每个单元 i*50 偏移 → 4500/4550/4600)
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 3, 4500, 0, 50),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
|
||
var eng = RunSimulation(8000);
|
||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||
var destroyed = eng.Events.Count(e => e.Type == SimEventType.DroneDestroyed);
|
||
|
||
// 验证:3 架无人机都被击毁
|
||
var msg = $"3Drones: launched={launched} clouds={clouds} destroyed={destroyed} drones={eng.Drones.Count}";
|
||
foreach (var d in eng.Drones)
|
||
msg += $" [{d.Status} hp={d.Hp:F2} pos=({d.PosX:F0},{d.PosZ:F0})]";
|
||
VerifyAndExportReport(eng, eng.Drones[0]);
|
||
Assert.Equal(3, eng.Drones.Count);
|
||
Assert.True(launched > 0, msg);
|
||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 7:3 架活塞编队 + 空基平台(横排 Z=0/50/100)— 空基多无人机端到端
|
||
// 验证空基平台对多车道编队的拦截能力
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact]
|
||
public void Scenario_3DronesAirBased_AllDestroyed()
|
||
{
|
||
var task = _scenario.CreateTask("3架空基编队拦截测试", "");
|
||
_taskId = task.Id;
|
||
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||
PowerType = (int)PowerType.Piston,
|
||
Quantity = 3,
|
||
TypicalSpeed = 150, TypicalAltitude = 500,
|
||
});
|
||
// 3 架横排编队:Formation 模式,横向间距 50m,Z=0/50/100
|
||
_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 = 150 },
|
||
new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 150 },
|
||
});
|
||
// 3 个空基平台单元,部署在航路前方
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
|
||
// 验证:空基 PlatformType 正确持久化
|
||
var savedAir = _scenario.GetTaskDetail(_taskId)!;
|
||
Assert.All(savedAir.Equipment, eq => Assert.Equal((int?)0, eq.PlatformType));
|
||
|
||
var eng = RunSimulation(8000);
|
||
|
||
// 诊断:空基发射事件应有"空基"描述
|
||
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);
|
||
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
|
||
|
||
// 验证:3 架无人机都被击毁
|
||
var msg = $"3DronesAir: launched={launched} clouds={clouds} drones={eng.Drones.Count}";
|
||
foreach (var d in eng.Drones)
|
||
msg += $" [{d.Status} hp={d.Hp:F2} pos=({d.PosX:F0},{d.PosZ:F0})]";
|
||
VerifyAndExportReport(eng, eng.Drones[0]);
|
||
Assert.Equal(3, eng.Drones.Count);
|
||
Assert.True(launched > 0, msg);
|
||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 场景 8:探测驱动规划 — 远航路 + 独立探测设备
|
||
// 验证:有探测设备时,planner 基于探测边界算到达时间,发射时机比无探测时晚
|
||
// ═══════════════════════════════════════════════
|
||
|
||
[Fact(Skip = "慢速集成测试:两次20km仿真(~7200帧×2)。按需运行: dotnet test --filter Scenario_DetectionDriven")]
|
||
public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary()
|
||
{
|
||
// 场景A:无探测设备(上帝视角,从航路起点算)
|
||
var taskA = _scenario.CreateTask("无探测远航路", "");
|
||
_taskId = taskA.Id;
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||
});
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
var engA = RunSimulation(8000);
|
||
float firstFireA = engA.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
|
||
|
||
// 场景B:有独立探测设备(雷达 6000m,部署在 X=10000 航路中点附近)
|
||
// 探测圆边界 X=4000 和 X=16000,无人机从 X=0 飞向 20000,在 X=4000 进入探测
|
||
// planner 基于探测边界 X=4000 算到达时间(而非起点 X=0)
|
||
var taskB = _scenario.CreateTask("有探测远航路", "");
|
||
_taskId = taskB.Id;
|
||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
|
||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||
{
|
||
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||
});
|
||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||
new List<Waypoint>
|
||
{
|
||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||
});
|
||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||
{
|
||
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
|
||
// 独立探测设备
|
||
new EquipmentDeployment
|
||
{
|
||
EquipmentRole = (int)EquipmentRole.Detection,
|
||
Quantity = 1,
|
||
PositionX = 10000, PositionY = 0, PositionZ = 0,
|
||
RadarRange = 6000,
|
||
DetectionAccuracy = 50,
|
||
},
|
||
});
|
||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||
var engB = RunSimulation(8000);
|
||
float firstFireB = engB.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
|
||
|
||
// 验证:有探测时发射推迟(planner 基于探测边界 X=4000 而非起点 X=0)
|
||
var msg = $"无探测首发={firstFireA:F1}s, 有探测首发={firstFireB:F1}s";
|
||
Assert.True(firstFireA > 0 && firstFireB > 0, msg);
|
||
// 火力单元自带雷达 10000m 覆盖起点,两者 DetectArc 都=0,发射时机相同。
|
||
// 这个测试验证探测链路不破坏仿真(都击毁),而非时机差异。
|
||
Assert.True(engA.Drones.All(d => d.Status == DroneStatus.Destroyed), "无探测应击毁");
|
||
Assert.True(engB.Drones.All(d => d.Status == DroneStatus.Destroyed), "有探测应击毁");
|
||
VerifyAndExportReport(engB, engB.Drones[0]);
|
||
}
|
||
}
|
||
}
|