CounterDroneBackend/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs
tian 8eeb43f0e8 feat: 统一默认数据架构 (defaults.json + DefaultData)
单一数据源 data/defaults.json,含弹药/编队/火力单元/无人机/探测/天气六类预设

版本追踪: Meta 表 + version 字段,更新时 InsertOrReplace,不删用户数据

名称统一 [Demo] 前缀,UI 中可识别为模拟数据

删除: DefaultAmmunition.cs, DefaultFireUnits.cs, default_ammo.json, default_formations.json, TestAmmo.cs

TestPathProvider 自动复制数据文件到测试目录

集成测试精简: 4个简单变体跳过,保留6个核心场景,39s
2026-06-15 15:46:20 +08:00

683 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
// ═══════════════════════════════════════════════
// 场景 63 架活塞编队(横排 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/100latIdx × 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);
}
// ═══════════════════════════════════════════════
// 场景 73 架活塞编队 + 空基平台(横排 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 模式,横向间距 50mZ=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]);
}
}
}