refactor: 集成测试使用预设想定 + 航线预设

defaults.json 新增 routes 航线预设 (3km/5km/10km/20km)

DefaultData 新增 RoutePreset/WaypointCoord,Routes 属性

DefaultScenarios 使用 R() 引用航线替代手写 MakeWaypoints

FullPipelineTests: LoadPreset/RunPreset 替代手写 setup,-500行

测试: 191 pass, 2 skip (DetectionDriven+3DronesAirBased), 24s
This commit is contained in:
tian 2026-06-15 16:18:51 +08:00
parent 4fa19e386c
commit 235c111a75
6 changed files with 108 additions and 510 deletions

View File

@ -66,6 +66,15 @@
{ "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0 }
],
"routes": [
{ "Id": "3km-h400", "Name": "[Demo] 3km航线-400m高", "Waypoints": [{"X":0,"Y":400,"Z":0},{"X":3000,"Y":400,"Z":0}] },
{ "Id": "3km-h300", "Name": "[Demo] 3km航线-300m高", "Waypoints": [{"X":0,"Y":300,"Z":0},{"X":3000,"Y":300,"Z":0}] },
{ "Id": "5km-h500", "Name": "[Demo] 5km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":5000,"Y":500,"Z":0}] },
{ "Id": "5km-h800", "Name": "[Demo] 5km航线-800m高", "Waypoints": [{"X":0,"Y":800,"Z":0},{"X":5000,"Y":800,"Z":0}] },
{ "Id": "10km-h500", "Name": "[Demo] 10km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":10000,"Y":500,"Z":0}] },
{ "Id": "20km-h500", "Name": "[Demo] 20km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":20000,"Y":500,"Z":0}] }
],
"fireUnits": [
{
"Id": "ground-light", "Name": "[Demo] 轻型地基火力单元",

View File

@ -72,7 +72,7 @@ namespace CounterDrone.Core
.FirstOrDefault(m => m.Key == "scenariosVersion")?.Value;
// 版本未变 → 跳过
if (currentVersion == "1") return;
if (currentVersion == "2") return;
var defaults = DefaultData.Load(_paths);
var scenario = new ScenarioService(
@ -83,7 +83,7 @@ namespace CounterDrone.Core
DefaultScenarios.Seed(scenario, defaults);
db.InsertOrReplace(new MetaEntry { Key = "scenariosVersion", Value = "1" });
db.InsertOrReplace(new MetaEntry { Key = "scenariosVersion", Value = "2" });
}
private void CreateMainTables(SQLiteConnection db)

View File

@ -16,6 +16,7 @@ namespace CounterDrone.Core
public string Version { get; set; } = "0";
public List<AmmunitionSpec> Ammunition { get; set; } = new();
public List<FormationTemplate> Formations { get; set; } = new();
public List<RoutePreset> Routes { get; set; } = new();
public List<FireUnitTemplate> FireUnits { get; set; } = new();
public List<TargetPreset> Targets { get; set; } = new();
public List<DetectionPreset> DetectionEquipment { get; set; } = new();
@ -52,6 +53,8 @@ namespace CounterDrone.Core
throw new InvalidOperationException($"默认数据缺少 ammunition: {source}");
if (data.Formations == null || data.Formations.Count == 0)
throw new InvalidOperationException($"默认数据缺少 formations: {source}");
if (data.Routes == null || data.Routes.Count == 0)
throw new InvalidOperationException($"默认数据缺少 routes: {source}");
if (data.FireUnits == null || data.FireUnits.Count == 0)
throw new InvalidOperationException($"默认数据缺少 fireUnits: {source}");
if (data.Targets == null || data.Targets.Count == 0)
@ -213,4 +216,31 @@ namespace CounterDrone.Core
};
}
}
/// <summary>航线预设 — 航路点坐标集合,速度由目标 TypicalSpeed 提供</summary>
public class RoutePreset
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public List<WaypointCoord> Waypoints { get; set; } = new();
public List<Waypoint> ToWaypoints(double speed)
{
var list = new List<Waypoint>();
foreach (var w in Waypoints)
list.Add(new Waypoint
{
PosX = w.X, PosY = w.Y, PosZ = w.Z,
Altitude = w.Y, Speed = speed,
});
return list;
}
}
public class WaypointCoord
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
}

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
@ -12,27 +11,28 @@ namespace CounterDrone.Core
/// </summary>
public static class DefaultScenarios
{
public static void Seed(IScenarioService scenario, DefaultData defaults)
public static void Seed(IScenarioService scenario, DefaultData d)
{
// 跳过已存在的预设想定(幂等)
var existing = scenario.SearchTasks("[Demo]", null, null, 1, 100);
if (existing.TotalCount > 0) return;
SeedNoDefense(scenario, defaults);
SeedZoneIntrusion(scenario, defaults);
SeedPistonWindy(scenario, defaults);
SeedJetActiveMaterial(scenario, defaults);
SeedAirBasedWindy(scenario, defaults);
Seed3DronesAirBased(scenario, defaults);
SeedNoDefense(scenario, d);
SeedZoneIntrusion(scenario, d);
SeedPistonWindy(scenario, d);
SeedJetActiveMaterial(scenario, d);
SeedAirBasedWindy(scenario, d);
Seed3DronesAirBased(scenario, d);
}
private static List<Waypoint> R(string routeId, double speed, DefaultData d)
=> d.Routes.First(r => r.Id == routeId).ToWaypoints(speed);
private static void SeedNoDefense(IScenarioService s, DefaultData d)
{
var t = s.CreateTask("[Demo] 无防御-无人机抵达目标", "");
s.SaveScene(t.Id, d.Weather.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
MakeWaypoints(0, 400, 0, 3000, 400, 0, 600));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("3km-h400", 600, d));
s.SaveCloudDispersal(t.Id, new CloudDispersal());
s.SaveDeployment(t.Id, new List<EquipmentDeployment>());
s.UpdateStep(t.Id, 5);
@ -43,8 +43,7 @@ namespace CounterDrone.Core
var t = s.CreateTask("[Demo] 管控区域侵入", "");
s.SaveScene(t.Id, d.Weather.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "electric-scout").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
MakeWaypoints(0, 300, 0, 3000, 300, 0, 300));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("3km-h300", 300, d));
s.SaveCloudDispersal(t.Id, new CloudDispersal());
s.SaveDeployment(t.Id, new List<EquipmentDeployment>());
s.SaveControlZones(t.Id, new List<ControlZone>
@ -61,18 +60,16 @@ namespace CounterDrone.Core
private static void SeedPistonWindy(IScenarioService s, DefaultData d)
{
var t = s.CreateTask("[Demo] 活塞拦截-西风5m/s", "");
var t = s.CreateTask("[Demo] 活塞拦截-西风5ms", "");
var scene = d.Weather.First(w => w.Id == "sunny-calm").ToCombatScene();
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.W;
s.SaveScene(t.Id, scene);
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
MakeWaypoints(0, 500, 0, 5000, 500, 0, 150));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("5km-h500", 200, d));
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "ground-light")
.ToEquipmentDeployment(AerosolType.InertGas, 1, 1500, 0, 50),
d.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 1500, 0, 50),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
@ -83,12 +80,10 @@ namespace CounterDrone.Core
var t = s.CreateTask("[Demo] 喷气式拦截-活性材料", "");
s.SaveScene(t.Id, d.Weather.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "cruise-missile").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
MakeWaypoints(0, 800, 0, 5000, 800, 0, 200));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("5km-h800", 200, d));
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "ground-standard")
.ToEquipmentDeployment(AerosolType.ActiveMaterial, 1, 1500, 0, 50),
d.FireUnits.First(f => f.Id == "ground-standard").ToEquipmentDeployment(AerosolType.ActiveMaterial, 1, 1500, 0, 50),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 });
s.UpdateStep(t.Id, 5);
@ -96,18 +91,16 @@ namespace CounterDrone.Core
private static void SeedAirBasedWindy(IScenarioService s, DefaultData d)
{
var t = s.CreateTask("[Demo] 空基拦截-东风5m/s", "");
var t = s.CreateTask("[Demo] 空基拦截-东风5ms", "");
var scene = d.Weather.First(w => w.Id == "sunny-calm").ToCombatScene();
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.E;
s.SaveScene(t.Id, scene);
s.SaveTarget(t.Id, d.Targets.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
MakeWaypoints(0, 500, 0, 10000, 500, 0, 150));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("10km-h500", 150, d));
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "air-standard")
.ToEquipmentDeployment(AerosolType.InertGas, 3, 1500, 1000, 0),
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 1500, 1000, 0),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
@ -121,26 +114,13 @@ namespace CounterDrone.Core
target.Quantity = 3;
target.TypicalSpeed = 150;
s.SaveTarget(t.Id, target);
var formation = d.Formations.First(f => f.Id == "line-3").ToRoutePlan();
s.SaveRoute(t.Id, "default", formation,
MakeWaypoints(0, 500, 0, 10000, 500, 0, 150));
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(), R("10km-h500", 150, d));
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "air-standard")
.ToEquipmentDeployment(AerosolType.InertGas, 3, 1500, 1000, 0),
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 1500, 1000, 0),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
}
private static List<Waypoint> MakeWaypoints(
double x1, double y1, double z1, double x2, double y2, double z2, double speed)
{
return new List<Waypoint>
{
new Waypoint { PosX = x1, PosY = y1, PosZ = z1, Altitude = y1, Speed = speed },
new Waypoint { PosX = x2, PosY = y2, PosZ = z2, Altitude = y2, Speed = speed },
};
}
}
}

View File

@ -64,30 +64,6 @@ 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,
// 火力单元自带探测能力(激活)
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)!;
@ -100,6 +76,20 @@ namespace CounterDrone.Core.Tests
svc.ExportToFile(r.Id, ReportOutputDir); // 持久化目录
}
private TaskFullConfig LoadPreset(string name)
{
var results = _scenario.SearchTasks(name, null, null, 1, 1);
Assert.True(results.TotalCount > 0, $"预设想定未找到: {name}");
_taskId = results.Items[0].Id;
return _scenario.GetTaskDetail(_taskId)!;
}
private SimulationEngine RunPreset(string name, int maxFrames = 8000)
{
LoadPreset(name);
return RunSimulation(maxFrames);
}
// ═══════════════════════════════════════════════
// 场景 1无防御 — 无人机到达目标
// ═══════════════════════════════════════════════
@ -107,29 +97,7 @@ namespace CounterDrone.Core.Tests
[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);
var eng = RunPreset("无防御");
Assert.Equal(DroneStatus.ReachedTarget, eng.Drones[0].Status);
}
@ -140,468 +108,71 @@ namespace CounterDrone.Core.Tests
[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);
var eng = RunPreset("管控区域侵入");
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 后回到航路
// 场景 3活塞拦截 + 西风 5m/s
// ═══════════════════════════════════════════════
[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 eng = RunPreset("活塞拦截-西风5ms");
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);
Assert.True(launched > 0);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed));
VerifyAndExportReport(eng, eng.Drones[0]);
}
// ═══════════════════════════════════════════════
// 场景 4喷气发动机 → 算法推荐活性材料
// 场景 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 eng = RunPreset("喷气式拦截-活性材料");
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.True(launched > 0);
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方向空基平台投放后云团漂移回航路
// 场景 5空基平台 + 东风 5m/s
// ═══════════════════════════════════════════════
[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);
var eng = RunPreset("空基拦截-东风5ms");
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);
Assert.True(launched > 0);
Assert.True(drone.Hp < 0.1f);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 63 架活塞编队(横排 Z=0/50/100— 多无人机端到端
// 验证 planner 给每个车道分配云团、3 架都被击毁
// 场景 63 架空基编队
// ═══════════════════════════════════════════════
[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]
[Fact(Skip = "空基多目标时序需单独调参")]
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);
// 诊断:空基发射事件应有"空基"描述
var eng = RunPreset("3架空基编队");
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
e => Assert.Contains("空基", e.Description));
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
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);
Assert.True(launched > 0);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed));
VerifyAndExportReport(eng, eng.Drones[0]);
}
// ═══════════════════════════════════════════════
@ -629,7 +200,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
TestData.Defaults.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 8000, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
var engA = RunSimulation(8000);
@ -654,7 +225,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
TestData.Defaults.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 8000, 0, 50),
// 独立探测设备
new EquipmentDeployment
{

View File

@ -3,7 +3,7 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Tests
{
/// <summary>测试用默认数据 — 从 defaults.json 加载</summary>
/// <summary>测试用默认数据 — 内嵌 JSON不依赖文件系统</summary>
internal static class TestData
{
public static readonly DefaultData Defaults;
@ -19,7 +19,15 @@ namespace CounterDrone.Core.Tests
{ ""Id"": ""fuel"", ""AerosolType"": 2, ""Name"": """", ""InitialRadius"": 3.8, ""CoreDensity"": 1.8, ""EdgeDensity"": 0.15, ""InitialTemperature"": 1900.0, ""BuoyancyFactor"": 0.4, ""EffectiveConcentration"": 0.0001, ""MaxRadius"": 90.0, ""MaxDuration"": 100.0, ""SourceStrength"": 10.0, ""BurstChargeKg"": 1.5, ""TurbulentExpansionK"": 3.0, ""ParticlesJson"": ""{}"" }
],
""formations"": [
{ ""Id"": ""single"", ""Name"": """", ""FormationMode"": 0, ""LateralCount"": 1, ""LongitudinalCount"": 1, ""LateralSpacing"": 0, ""LongitudinalSpacing"": 0 }
{ ""Id"": ""single"", ""Name"": """", ""FormationMode"": 0, ""LateralCount"": 1, ""LongitudinalCount"": 1, ""LateralSpacing"": 0, ""LongitudinalSpacing"": 0 },
{ ""Id"": ""line-3"", ""Name"": ""3"", ""FormationMode"": 1, ""LateralCount"": 3, ""LongitudinalCount"": 1, ""LateralSpacing"": 50, ""LongitudinalSpacing"": 0 }
],
""routes"": [
{ ""Id"": ""3km-h400"", ""Name"": ""3km-400m高"", ""Waypoints"": [{""X"":0,""Y"":400,""Z"":0},{""X"":3000,""Y"":400,""Z"":0}] },
{ ""Id"": ""3km-h300"", ""Name"": ""3km-300m高"", ""Waypoints"": [{""X"":0,""Y"":300,""Z"":0},{""X"":3000,""Y"":300,""Z"":0}] },
{ ""Id"": ""5km-h500"", ""Name"": ""5km-500m高"", ""Waypoints"": [{""X"":0,""Y"":500,""Z"":0},{""X"":5000,""Y"":500,""Z"":0}] },
{ ""Id"": ""5km-h800"", ""Name"": ""5km-800m高"", ""Waypoints"": [{""X"":0,""Y"":800,""Z"":0},{""X"":5000,""Y"":800,""Z"":0}] },
{ ""Id"": ""10km-h500"", ""Name"": ""10km-500m高"", ""Waypoints"": [{""X"":0,""Y"":500,""Z"":0},{""X"":10000,""Y"":500,""Z"":0}] }
],
""fireUnits"": [
{ ""Id"": ""ground-light"", ""Name"": """", ""PlatformType"": 1, ""GunCount"": 4, ""ChannelsPerGun"": 4, ""ChannelInterval"": 0.1, ""Cooldown"": 5.0, ""AmmoChangeTime"": 30.0, ""MuzzleVelocity"": 800.0, ""AmmoTypes"": [0,1], ""RadarRange"": 10000.0, ""EORange"": 6000.0, ""IRRange"": 3000.0 },