From 8eeb43f0e8181843fcd024b10b2c494d38704a73 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 15 Jun 2026 15:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9E=B6=E6=9E=84=20(defaults.json=20+=20Def?= =?UTF-8?q?aultData)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单一数据源 data/defaults.json,含弹药/编队/火力单元/无人机/探测/天气六类预设 版本追踪: Meta 表 + version 字段,更新时 InsertOrReplace,不删用户数据 名称统一 [Demo] 前缀,UI 中可识别为模拟数据 删除: DefaultAmmunition.cs, DefaultFireUnits.cs, default_ammo.json, default_formations.json, TestAmmo.cs TestPathProvider 自动复制数据文件到测试目录 集成测试精简: 4个简单变体跳过,保留6个核心场景,39s --- data/default_ammo.json | 56 ----- data/default_formations.json | 56 ----- data/defaults.json | 147 ++++++++++++ .../Algorithms/DefaultAmmunition.cs | 59 ----- src/CounterDrone.Core/DatabaseManager.cs | 18 +- src/CounterDrone.Core/DefaultData.cs | 216 ++++++++++++++++++ src/CounterDrone.Core/DefaultFireUnits.cs | 103 --------- src/CounterDrone.Core/Models/MetaEntry.cs | 13 ++ .../Simulation/SimulationEngine.cs | 8 +- src/CounterDrone.Core/TestPathProvider.cs | 32 +++ .../Scripts/Managers/ManagerVerification.cs | 2 +- .../Scripts/Managers/SimulationBootstrap.cs | 2 +- .../Scripts/Managers/SimulationRunner.cs | 2 +- .../AmmunitionSpecRepositoryTests.cs | 6 +- .../DatabaseManagerTests.cs | 2 +- .../DefensePlannerTests.cs | 2 +- .../DispersionModelTests.cs | 4 +- .../CounterDrone.Core.Tests/EdgeCaseTests.cs | 7 +- .../FullPipelineTests.cs | 29 ++- .../SimulationEngineTests.cs | 3 +- test/unit/CounterDrone.Core.Tests/TestAmmo.cs | 14 -- test/unit/CounterDrone.Core.Tests/TestData.cs | 41 ++++ 22 files changed, 495 insertions(+), 327 deletions(-) delete mode 100644 data/default_ammo.json delete mode 100644 data/default_formations.json create mode 100644 data/defaults.json delete mode 100644 src/CounterDrone.Core/Algorithms/DefaultAmmunition.cs create mode 100644 src/CounterDrone.Core/DefaultData.cs delete mode 100644 src/CounterDrone.Core/DefaultFireUnits.cs create mode 100644 src/CounterDrone.Core/Models/MetaEntry.cs delete mode 100644 test/unit/CounterDrone.Core.Tests/TestAmmo.cs create mode 100644 test/unit/CounterDrone.Core.Tests/TestData.cs diff --git a/data/default_ammo.json b/data/default_ammo.json deleted file mode 100644 index 5b6fe29..0000000 --- a/data/default_ammo.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "Id": "default-inert", - "AerosolType": 0, - "Name": "惰性气体弹(发烟罐型)", - "InitialRadius": 3.8, - "InitialVolume": 14000.0, - "CoreDensity": 1.5, - "EdgeDensity": 0.1, - "InitialTemperature": 1800.0, - "BuoyancyFactor": 0.3, - "EffectiveConcentration": 0.0001, - "MaxRadius": 100.0, - "MaxDuration": 120.0, - "SourceStrength": 10.0, - "BurstChargeKg": 1.5, - "TurbulentExpansionK": 3.0, - "ParticlesJson": "{}" - }, - { - "Id": "default-active", - "AerosolType": 1, - "Name": "活性材料弹(爆炸分散型)", - "InitialRadius": 5.0, - "InitialVolume": 30000.0, - "CoreDensity": 2.0, - "EdgeDensity": 0.2, - "InitialTemperature": 2400.0, - "BuoyancyFactor": 0.6, - "EffectiveConcentration": 0.0001, - "MaxRadius": 80.0, - "MaxDuration": 90.0, - "SourceStrength": 12.0, - "BurstChargeKg": 4.0, - "TurbulentExpansionK": 4.0, - "ParticlesJson": "{}" - }, - { - "Id": "default-fuel", - "AerosolType": 2, - "Name": "活性燃料弹(抛射分散型)", - "InitialRadius": 3.8, - "InitialVolume": 14000.0, - "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": "{}" - } -] diff --git a/data/default_formations.json b/data/default_formations.json deleted file mode 100644 index bb01361..0000000 --- a/data/default_formations.json +++ /dev/null @@ -1,56 +0,0 @@ -[ - { - "Id": "single", - "Name": "单机", - "FormationMode": 0, - "LateralCount": 1, - "LongitudinalCount": 1, - "LateralSpacing": 0, - "LongitudinalSpacing": 0 - }, - { - "Id": "line-abreast-3", - "Name": "3机横队", - "FormationMode": 1, - "LateralCount": 3, - "LongitudinalCount": 1, - "LateralSpacing": 50, - "LongitudinalSpacing": 0 - }, - { - "Id": "line-abreast-5", - "Name": "5机横队", - "FormationMode": 1, - "LateralCount": 5, - "LongitudinalCount": 1, - "LateralSpacing": 50, - "LongitudinalSpacing": 0 - }, - { - "Id": "column-3", - "Name": "3机纵队", - "FormationMode": 1, - "LateralCount": 1, - "LongitudinalCount": 3, - "LateralSpacing": 0, - "LongitudinalSpacing": 100 - }, - { - "Id": "box-2x2", - "Name": "2×2方队", - "FormationMode": 1, - "LateralCount": 2, - "LongitudinalCount": 2, - "LateralSpacing": 50, - "LongitudinalSpacing": 100 - }, - { - "Id": "swarm-10", - "Name": "蜂群(10架)", - "FormationMode": 2, - "LateralCount": 10, - "LongitudinalCount": 1, - "LateralSpacing": 30, - "LongitudinalSpacing": 0 - } -] diff --git a/data/defaults.json b/data/defaults.json new file mode 100644 index 0000000..994a2f0 --- /dev/null +++ b/data/defaults.json @@ -0,0 +1,147 @@ +{ + "version": "1", + "ammunition": [ + { + "Id": "inert", + "AerosolType": 0, + "Name": "[Demo] 惰性气体弹(发烟罐型)", + "InitialRadius": 3.8, + "InitialVolume": 14000.0, + "CoreDensity": 1.5, + "EdgeDensity": 0.1, + "InitialTemperature": 1800.0, + "BuoyancyFactor": 0.3, + "EffectiveConcentration": 0.0001, + "MaxRadius": 100.0, + "MaxDuration": 120.0, + "SourceStrength": 10.0, + "BurstChargeKg": 1.5, + "TurbulentExpansionK": 3.0, + "ParticlesJson": "{}" + }, + { + "Id": "active", + "AerosolType": 1, + "Name": "[Demo] 活性材料弹(爆炸分散型)", + "InitialRadius": 5.0, + "InitialVolume": 30000.0, + "CoreDensity": 2.0, + "EdgeDensity": 0.2, + "InitialTemperature": 2400.0, + "BuoyancyFactor": 0.6, + "EffectiveConcentration": 0.0001, + "MaxRadius": 80.0, + "MaxDuration": 90.0, + "SourceStrength": 12.0, + "BurstChargeKg": 4.0, + "TurbulentExpansionK": 4.0, + "ParticlesJson": "{}" + }, + { + "Id": "fuel", + "AerosolType": 2, + "Name": "[Demo] 活性燃料弹(抛射分散型)", + "InitialRadius": 3.8, + "InitialVolume": 14000.0, + "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": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0 }, + { "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 }, + { "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0 }, + { "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100 }, + { "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100 }, + { "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0 } + ], + + "fireUnits": [ + { + "Id": "ground-light", "Name": "[Demo] 轻型地基火力单元", + "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 + }, + { + "Id": "ground-standard", "Name": "[Demo] 标准地基火力单元", + "PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1, + "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": "[Demo] 重型地基火力单元", + "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": "[Demo] 标准空基火力单元", + "PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0, + "Cooldown": 5.0, "AmmoChangeTime": 30.0, + "CruiseSpeed": 55.0, "ReleaseAltitude": 1500.0, + "AmmoTypes": [0, 1], + "EORange": 12000.0, "IRRange": 8000.0 + } + ], + + "targets": [ + { "Id": "quadcopter", "Name": "[Demo] 小型四旋翼(DJI类)", + "TargetType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 }, + { "Id": "electric-scout", "Name": "[Demo] 电推侦察无人机", + "TargetType": 2, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 }, + { "Id": "fixed-piston", "Name": "[Demo] 固定翼活塞(Orlan类)", + "TargetType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 }, + { "Id": "shahed", "Name": "[Demo] 活塞巡飞弹(Shahed类)", + "TargetType": 3, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 }, + { "Id": "tb2", "Name": "[Demo] 中空长航时(TB2类)", + "TargetType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 }, + { "Id": "cruise-missile", "Name": "[Demo] 巡航导弹(喷气式)", + "TargetType": 4, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 300.0, "TypicalAltitude": 2000.0 } + ], + + "detectionEquipment": [ + { "Id": "radar-mr", "Name": "[Demo] 中程防空雷达", + "RadarRange": 20000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 30.0 }, + { "Id": "radar-sr", "Name": "[Demo] 近程防空雷达", + "RadarRange": 10000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 50.0 }, + { "Id": "eo-station", "Name": "[Demo] 光电跟踪站", + "RadarRange": 0.0, "EORange": 15000.0, "IRRange": 8000.0, "Accuracy": 20.0 }, + { "Id": "ir-sentry", "Name": "[Demo] 红外哨", + "RadarRange": 0.0, "EORange": 0.0, "IRRange": 10000.0, "Accuracy": 40.0 } + ], + + "weather": [ + { "Id": "sunny-calm", "Name": "[Demo] 晴天无风", + "WeatherType": 0, "WindSpeed": 3.0, "WindDirection": 0, + "Temperature": 25.0, "Humidity": 50.0, "Pressure": 1013.0, "Visibility": 8000.0 }, + { "Id": "sunny-windy", "Name": "[Demo] 晴天大风", + "WeatherType": 0, "WindSpeed": 10.0, "WindDirection": 4, + "Temperature": 28.0, "Humidity": 40.0, "Pressure": 1010.0, "Visibility": 10000.0 }, + { "Id": "overcast", "Name": "[Demo] 阴天", + "WeatherType": 1, "WindSpeed": 5.0, "WindDirection": 2, + "Temperature": 18.0, "Humidity": 70.0, "Pressure": 1015.0, "Visibility": 5000.0 }, + { "Id": "fog", "Name": "[Demo] 雾天", + "WeatherType": 2, "WindSpeed": 2.0, "WindDirection": 1, + "Temperature": 12.0, "Humidity": 95.0, "Pressure": 1020.0, "Visibility": 500.0 }, + { "Id": "rain", "Name": "[Demo] 雨天", + "WeatherType": 3, "WindSpeed": 8.0, "WindDirection": 3, + "Temperature": 15.0, "Humidity": 90.0, "Pressure": 1005.0, "Visibility": 3000.0 }, + { "Id": "night", "Name": "[Demo] 夜间", + "WeatherType": 4, "WindSpeed": 2.0, "WindDirection": 5, + "Temperature": 10.0, "Humidity": 65.0, "Pressure": 1018.0, "Visibility": 2000.0 } + ] +} diff --git a/src/CounterDrone.Core/Algorithms/DefaultAmmunition.cs b/src/CounterDrone.Core/Algorithms/DefaultAmmunition.cs deleted file mode 100644 index 4d1dfa1..0000000 --- a/src/CounterDrone.Core/Algorithms/DefaultAmmunition.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using CounterDrone.Core.Models; - -namespace CounterDrone.Core.Algorithms -{ - /// 默认弹药规格 — 基于发烟罐/烟幕弹工程数据 - /// - /// 参考:10kg烟幕剂,抛射药/烟幕剂质量比 5%-20%,TNT当量 1.5-2kg(典型值) - /// 初始半径 R₀ = 3.3 × W^0.32 ≈ 3.8~4.1m - /// - public static class DefaultAmmunition - { - public static List GetAll() - { - return new List - { - new AmmunitionSpec - { - Id = "default-inert", AerosolType = (int)AerosolType.InertGas, - Name = "惰性气体弹(发烟罐型)", - InitialRadius = 3.8, - CoreDensity = 1.5, EdgeDensity = 0.1, - InitialTemperature = 1800, BuoyancyFactor = 0.3, - EffectiveConcentration = 0.0001, - MaxRadius = 100.0, MaxDuration = 120.0, - SourceStrength = 10.0, - BurstChargeKg = 1.5, TurbulentExpansionK = 3.0, - }, - new AmmunitionSpec - { - Id = "default-active", AerosolType = (int)AerosolType.ActiveMaterial, - Name = "活性材料弹(爆炸分散型)", - InitialRadius = 5.0, - CoreDensity = 2.0, EdgeDensity = 0.2, - InitialTemperature = 2400, BuoyancyFactor = 0.6, - EffectiveConcentration = 0.0001, - MaxRadius = 80.0, MaxDuration = 90.0, - SourceStrength = 12.0, - BurstChargeKg = 4.0, TurbulentExpansionK = 4.0, - }, - new AmmunitionSpec - { - Id = "default-fuel", AerosolType = (int)AerosolType.ActiveFuel, - Name = "活性燃料弹(抛射分散型)", - InitialRadius = 3.8, - CoreDensity = 1.8, EdgeDensity = 0.15, - InitialTemperature = 1900, BuoyancyFactor = 0.4, - EffectiveConcentration = 0.0001, - MaxRadius = 90.0, MaxDuration = 100.0, - SourceStrength = 10.0, - BurstChargeKg = 1.5, TurbulentExpansionK = 3.0, - }, - }; - } - - public static AmmunitionSpec GetByType(AerosolType type) - => GetAll().Find(a => a.AerosolType == (int)type) ?? GetAll()[0]; - } -} diff --git a/src/CounterDrone.Core/DatabaseManager.cs b/src/CounterDrone.Core/DatabaseManager.cs index c24496d..f408f2f 100644 --- a/src/CounterDrone.Core/DatabaseManager.cs +++ b/src/CounterDrone.Core/DatabaseManager.cs @@ -47,14 +47,20 @@ namespace CounterDrone.Core private void SeedDefaultData(SQLiteConnection db) { - if (db.Table().Count() > 0) return; + db.CreateTable(); - var jsonPath = Path.Combine(_paths.GetDataRoot(), "default_ammo.json"); - if (!File.Exists(jsonPath)) return; + var defaults = DefaultData.Load(_paths); + var currentVersion = db.Table() + .FirstOrDefault(m => m.Key == "defaultsVersion")?.Value; - var specs = JsonSerializer.Deserialize>(File.ReadAllText(jsonPath)); - if (specs != null) - foreach (var s in specs) db.Insert(s); + // 版本未变且数据已存在 → 跳过 + if (currentVersion == defaults.Version && db.Table().Count() > 0) + return; + + // 插入或更新默认数据(按 Id 覆盖,不删除用户自建数据) + foreach (var a in defaults.Ammunition) db.InsertOrReplace(a); + + db.InsertOrReplace(new MetaEntry { Key = "defaultsVersion", Value = defaults.Version }); } private void CreateMainTables(SQLiteConnection db) diff --git a/src/CounterDrone.Core/DefaultData.cs b/src/CounterDrone.Core/DefaultData.cs new file mode 100644 index 0000000..8b15293 --- /dev/null +++ b/src/CounterDrone.Core/DefaultData.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core +{ + /// + /// 统一默认数据 — 弹药、编队、火力单元、无人机、探测设备、天气的预设库。 + /// 所有默认数据集中在 data/defaults.json,通过本类统一加载。 + /// + public class DefaultData + { + public string Version { get; set; } = "0"; + public List Ammunition { get; set; } = new(); + public List Formations { get; set; } = new(); + public List FireUnits { get; set; } = new(); + public List Targets { get; set; } = new(); + public List DetectionEquipment { get; set; } = new(); + public List Weather { get; set; } = new(); + + /// 从 data/defaults.json 加载。文件缺失或解析失败即抛异常。 + public static DefaultData Load(IPathProvider paths) + { + var jsonPath = Path.Combine(paths.GetDataRoot(), "defaults.json"); + if (!File.Exists(jsonPath)) + throw new FileNotFoundException($"默认数据文件未找到: {jsonPath}"); + + var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var data = JsonSerializer.Deserialize(File.ReadAllText(jsonPath), opts) + ?? throw new InvalidOperationException($"默认数据文件解析失败: {jsonPath}"); + + Validate(data, jsonPath); + return data; + } + + /// 从 JSON 字符串解析(测试用) + public static DefaultData FromJson(string json) + { + var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var data = JsonSerializer.Deserialize(json, opts) + ?? throw new InvalidOperationException("默认数据 JSON 解析失败"); + Validate(data, ""); + return data; + } + + private static void Validate(DefaultData data, string source) + { + if (data.Ammunition == null || data.Ammunition.Count == 0) + throw new InvalidOperationException($"默认数据缺少 ammunition: {source}"); + if (data.Formations == null || data.Formations.Count == 0) + throw new InvalidOperationException($"默认数据缺少 formations: {source}"); + if (data.FireUnits == null || data.FireUnits.Count == 0) + throw new InvalidOperationException($"默认数据缺少 fireUnits: {source}"); + if (data.Targets == null || data.Targets.Count == 0) + throw new InvalidOperationException($"默认数据缺少 targets: {source}"); + if (data.Weather == null || data.Weather.Count == 0) + throw new InvalidOperationException($"默认数据缺少 weather: {source}"); + if (data.DetectionEquipment == null) + data.DetectionEquipment = new(); + } + } + + /// 编队模板 + public class FormationTemplate + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public int FormationMode { get; set; } + public int LateralCount { get; set; } = 1; + public int LongitudinalCount { get; set; } = 1; + public double LateralSpacing { get; set; } + public double LongitudinalSpacing { get; set; } + + public RoutePlan ToRoutePlan() + { + return new RoutePlan + { + FormationMode = FormationMode, + LateralCount = LateralCount, + LongitudinalCount = LongitudinalCount, + LateralSpacing = LateralSpacing, + LongitudinalSpacing = LongitudinalSpacing, + }; + } + } + + /// 火力单元模板 + 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 AmmoTypes { get; set; } = new(); + public double RadarRange { get; set; } + public double EORange { get; set; } + public double IRRange { get; set; } + + public EquipmentDeployment ToEquipmentDeployment(AerosolType ammoType, int quantity, + double posX, double posY, double posZ) + { + return new EquipmentDeployment + { + EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform, + PlatformType = PlatformType, + Quantity = quantity, + PositionX = posX, + PositionY = posY, + PositionZ = posZ, + AerosolType = (int)ammoType, + MunitionCount = GunCount * ChannelsPerGun * quantity, + GunCount = GunCount, + ChannelsPerGun = ChannelsPerGun, + ChannelInterval = ChannelInterval, + Cooldown = Cooldown, + MuzzleVelocity = MuzzleVelocity > 0 ? MuzzleVelocity : null, + CruiseSpeed = CruiseSpeed > 0 ? CruiseSpeed : null, + ReleaseAltitude = ReleaseAltitude > 0 ? ReleaseAltitude : null, + RadarRange = RadarRange > 0 ? RadarRange : null, + EORange = EORange > 0 ? EORange : null, + IRRange = IRRange > 0 ? IRRange : null, + }; + } + } + + /// 无人机/威胁目标预设 + public class TargetPreset + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public int TargetType { get; set; } + public int PowerType { get; set; } + public double Wingspan { get; set; } + public double TypicalSpeed { get; set; } + public double TypicalAltitude { get; set; } + + public TargetConfig ToTargetConfig(string waveId = "default") + { + return new TargetConfig + { + WaveId = waveId, + TargetType = TargetType, + PowerType = PowerType, + Quantity = 1, + Wingspan = Wingspan, + TypicalSpeed = TypicalSpeed, + TypicalAltitude = TypicalAltitude, + }; + } + } + + /// 独立探测设备预设 + public class DetectionPreset + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public double RadarRange { get; set; } + public double EORange { get; set; } + public double IRRange { get; set; } + public double Accuracy { get; set; } = 50.0; + + public EquipmentDeployment ToEquipmentDeployment(double posX, double posY, double posZ) + { + return new EquipmentDeployment + { + EquipmentRole = (int)Models.EquipmentRole.Detection, + Quantity = 1, + PositionX = posX, + PositionY = posY, + PositionZ = posZ, + RadarRange = RadarRange > 0 ? RadarRange : null, + EORange = EORange > 0 ? EORange : null, + IRRange = IRRange > 0 ? IRRange : null, + DetectionAccuracy = Accuracy > 0 ? Accuracy : null, + }; + } + } + + /// 天气/场景预设 + public class WeatherPreset + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public int WeatherType { get; set; } + public double WindSpeed { get; set; } + public int WindDirection { get; set; } + public double Temperature { get; set; } = 20.0; + public double Humidity { get; set; } = 60.0; + public double Pressure { get; set; } = 1013.0; + public double Visibility { get; set; } = 5000.0; + + public CombatScene ToCombatScene() + { + return new CombatScene + { + WeatherType = WeatherType, + WindSpeed = WindSpeed, + WindDirection = WindDirection, + Temperature = Temperature, + Humidity = Humidity, + Pressure = Pressure, + Visibility = Visibility, + }; + } + } +} diff --git a/src/CounterDrone.Core/DefaultFireUnits.cs b/src/CounterDrone.Core/DefaultFireUnits.cs deleted file mode 100644 index 7afd5c6..0000000 --- a/src/CounterDrone.Core/DefaultFireUnits.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; - -namespace CounterDrone.Core -{ - public static class DefaultFireUnits - { - private static List? _cache; - - public static List GetAll() - { - if (_cache != null) return _cache; - _cache = new List - { - new FireUnitTemplate - { - Id = "ground-light", - Name = "轻型地基火力单元", - PlatformType = 1, - GunCount = 4, - ChannelsPerGun = 4, - ChannelInterval = 0.1, - Cooldown = 5.0, - AmmoChangeTime = 30.0, - MuzzleVelocity = 800.0, - AmmoTypes = new() { 0, 1 }, - RadarRange = 10000.0, - EORange = 6000.0, - IRRange = 3000.0, - }, - new FireUnitTemplate - { - Id = "ground-standard", - Name = "标准地基火力单元", - PlatformType = 1, - GunCount = 4, - ChannelsPerGun = 4, - ChannelInterval = 0.1, - 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 = 8, - 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 AmmoTypes { get; set; } = new(); - public double RadarRange { get; set; } - public double EORange { get; set; } - public double IRRange { get; set; } - } -} diff --git a/src/CounterDrone.Core/Models/MetaEntry.cs b/src/CounterDrone.Core/Models/MetaEntry.cs new file mode 100644 index 0000000..eb694f1 --- /dev/null +++ b/src/CounterDrone.Core/Models/MetaEntry.cs @@ -0,0 +1,13 @@ +using SQLite; + +namespace CounterDrone.Core.Models +{ + /// 键值对元数据表(版本追踪等) + [Table("Meta")] + internal class MetaEntry + { + [PrimaryKey] + public string Key { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + } +} diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index a83440b..d856830 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -81,9 +81,13 @@ namespace CounterDrone.Core.Simulation _tickInterval = 1.0f / _tickRate; using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath())) + { _ammoSpec = ammoDb.Table() - .FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType) - ?? DefaultAmmunition.GetByType((AerosolType)config.Cloud.AerosolType); + .FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType); + } + if (_ammoSpec == null || _ammoSpec.Id == null) + _ammoSpec = DefaultData.Load(_paths).Ammunition + .First(a => a.AerosolType == config.Cloud.AerosolType); _drones.Clear(); foreach (var target in config.Targets) diff --git a/src/CounterDrone.Core/TestPathProvider.cs b/src/CounterDrone.Core/TestPathProvider.cs index 9f8e4c8..dda5542 100644 --- a/src/CounterDrone.Core/TestPathProvider.cs +++ b/src/CounterDrone.Core/TestPathProvider.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Reflection; namespace CounterDrone.Core { @@ -12,6 +13,37 @@ namespace CounterDrone.Core { _root = rootDir; Directory.CreateDirectory(_root); + CopyDataFiles(); + } + + /// 从项目 data/ 目录复制 defaults.json 和 planner_config.json 到测试根目录 + private void CopyDataFiles() + { + var sourceDir = FindProjectDataDir(); + if (sourceDir == null) return; + foreach (var file in new[] { "defaults.json", "planner_config.json" }) + { + var src = Path.Combine(sourceDir, file); + var dst = Path.Combine(_root, file); + if (File.Exists(src) && !File.Exists(dst)) + File.Copy(src, dst); + } + } + + private static string? FindProjectDataDir() + { + var asmDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (asmDir == null) return null; + // 从 bin/Debug/netX.X 往上走到仓库根 + var dir = asmDir; + for (int i = 0; i < 10; i++) + { + var candidate = Path.Combine(dir, "data/defaults.json"); + if (File.Exists(candidate)) return Path.Combine(dir, "data"); + dir = Path.GetDirectoryName(dir); + if (dir == null) break; + } + return null; } public string GetDataRoot() => _root; diff --git a/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs b/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs index 7f7d93d..f1102bb 100644 --- a/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs +++ b/src/Unity/Assets/Scripts/Managers/ManagerVerification.cs @@ -50,7 +50,7 @@ namespace CounterDrone.Unity var db = new DatabaseManager(paths).OpenMainDb(); SqliteConnectionTracker.Track(db); - foreach (var a in DefaultAmmunition.GetAll()) db.Insert(a); + foreach (var a in DefaultData.Load(paths).Ammunition) db.Insert(a); var ammoCatalog = db.Table().ToList(); var detail = scenarioMgr.GetDetail(taskId); diff --git a/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs b/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs index 26fe2e9..5bd66ef 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs @@ -34,7 +34,7 @@ namespace CounterDrone.Unity var dbm = new DatabaseManager(paths); var db = dbm.OpenMainDb(); SqliteConnectionTracker.Track(db); - foreach (var a in DefaultAmmunition.GetAll()) db.Insert(a); + foreach (var a in DefaultData.Load(paths).Ammunition) db.Insert(a); // 创建想定 var scenario = new ScenarioService( diff --git a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs index 78ee901..1e1bddb 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs @@ -48,7 +48,7 @@ namespace CounterDrone.Unity new RoutePlanRepository(_db), new WaypointRepository(_db)); _frameStore = new FrameDataStore(_paths); - _engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(DefaultAmmunition.GetAll(), PlannerConfig.Load(_paths))); + _engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths))); } public void LoadAndStart(string taskId) diff --git a/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs b/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs index e4e370f..61554fc 100644 --- a/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs +++ b/test/unit/CounterDrone.Core.Tests/AmmunitionSpecRepositoryTests.cs @@ -76,10 +76,10 @@ namespace CounterDrone.Core.Tests }); var inertSpecs = _repo.GetByAerosolType((int)AerosolType.InertGas); - Assert.Equal(2, inertSpecs.Count); + Assert.Equal(3, inertSpecs.Count); var activeSpecs = _repo.GetByAerosolType((int)AerosolType.ActiveMaterial); - Assert.Single(activeSpecs); + Assert.Equal(2, activeSpecs.Count); } [Fact] @@ -95,7 +95,7 @@ namespace CounterDrone.Core.Tests _repo.InsertAll(specs); var all = _repo.GetAll(); - Assert.Equal(2, all.Count); + Assert.Equal(5, all.Count); Assert.Contains(all, s => s.Name == "惰性气体弹"); Assert.Contains(all, s => s.Name == "活性材料弹"); } diff --git a/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs b/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs index 8180035..b4a49c5 100644 --- a/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DatabaseManagerTests.cs @@ -31,7 +31,7 @@ namespace CounterDrone.Core.Tests { var db = _dbManager.OpenMainDb(); - // 验证所有 11 张表存在 + // 验证所有 12 张表存在(含 Meta) var tables = new[] { "ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene", diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index c5daec1..7eb0ae8 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -13,7 +13,7 @@ namespace CounterDrone.Core.Tests private readonly ITestOutputHelper _output; public DefensePlannerTests(ITestOutputHelper output) { _output = output; } - private static readonly List TestAmmo = DefaultAmmunition.GetAll(); + private static readonly List TestAmmo = TestData.Defaults.Ammunition; private static FireUnit MakeGroundUnit(string id, float posX, int munitions = 16) => new() diff --git a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs index fac37c3..629bbe1 100644 --- a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs @@ -6,7 +6,7 @@ namespace CounterDrone.Core.Tests { public class DispersionModelTests { - private static AmmunitionSpec Ammo() => DefaultAmmunition.GetByType(AerosolType.InertGas); + private static AmmunitionSpec Ammo() => TestData.Defaults.Ammunition.First(a => a.AerosolType == (int)AerosolType.InertGas); [Fact] public void Phase1_InitialRadius_FromBurstCharge() @@ -64,7 +64,7 @@ namespace CounterDrone.Core.Tests [Fact] public void DefaultAmmo_ParametersInReasonableRange() { - var inert = DefaultAmmunition.GetByType(AerosolType.InertGas); + var inert = TestData.Defaults.Ammunition.First(a => a.AerosolType == (int)AerosolType.InertGas); Assert.True(inert.BurstChargeKg is > 0.01 and < 5, "爆发药应 0.01~5kg"); Assert.True(inert.TurbulentExpansionK is > 1 and < 20, "湍流系数应 1~20"); Assert.True(inert.EffectiveConcentration is >= 0.0001 and < 0.1, "有效浓度阈值应 ≥ 0.0001"); diff --git a/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs index cd8292f..5d69799 100644 --- a/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs +++ b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs @@ -25,7 +25,6 @@ namespace CounterDrone.Core.Tests var paths = new TestPathProvider(_testDir); var dbm = new DatabaseManager(paths); _db = dbm.OpenMainDb(); - foreach (var a in DefaultAmmunition.GetAll()) _db.Insert(a); _scenario = new ScenarioService( new SimTaskRepository(_db), new CombatSceneRepository(_db), @@ -65,7 +64,7 @@ namespace CounterDrone.Core.Tests }); var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), - new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); + new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = 4f; @@ -107,7 +106,7 @@ namespace CounterDrone.Core.Tests }); var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), - new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); + new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = 4f; @@ -148,7 +147,7 @@ namespace CounterDrone.Core.Tests }); var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), - new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); + new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = 4f; diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index 4dc2ee0..faf22c3 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -29,8 +29,7 @@ namespace CounterDrone.Core.Tests _paths = new TestPathProvider(_testDir); var dbm = new DatabaseManager(_paths); _mainDb = dbm.OpenMainDb(); - foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a); - _ammoCatalog = new List(DefaultAmmunition.GetAll()); + _ammoCatalog = new List(TestData.Defaults.Ammunition); _scenario = new ScenarioService( new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb), @@ -180,7 +179,7 @@ namespace CounterDrone.Core.Tests // 场景 3:活塞式 200km/h,惰性气体拦截 // ═══════════════════════════════════════════════ - [Fact] + [Fact(Skip = "被 Piston_Windy 替代:保留有风偏版本作为核心拦截测试")] public void Scenario_Piston_InertGasIntercept() { var task = _scenario.CreateTask("拦截活塞式无人机", ""); @@ -208,7 +207,7 @@ namespace CounterDrone.Core.Tests }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50), + 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 }); @@ -292,7 +291,7 @@ namespace CounterDrone.Core.Tests }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50), + 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 }); @@ -349,7 +348,7 @@ namespace CounterDrone.Core.Tests _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 1500, 0, 50), + 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 }); @@ -368,7 +367,7 @@ namespace CounterDrone.Core.Tests // 场景 5:空基平台 — 基于推荐方案,平台改为空基 // ═══════════════════════════════════════════════ - [Fact] + [Fact(Skip = "被 AirBased_Windy 替代:保留有风偏版本作为空基测试")] public void Scenario_AirBased_PlatformFliesAndDrops() { var task = _scenario.CreateTask("空基平台拦截测试", ""); @@ -394,7 +393,7 @@ namespace CounterDrone.Core.Tests // 部署空基平台单元 _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0), + 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 }); @@ -459,7 +458,7 @@ namespace CounterDrone.Core.Tests _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0), + 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 }); @@ -493,7 +492,7 @@ namespace CounterDrone.Core.Tests // 验证 planner 给每个车道分配云团、3 架都被击毁 // ═══════════════════════════════════════════════ - [Fact] + [Fact(Skip = "被 3DronesAirBased 替代:保留空基多目标版本")] public void Scenario_3DronesFormation_AllDestroyed() { var task = _scenario.CreateTask("3架编队拦截测试", ""); @@ -524,7 +523,7 @@ namespace CounterDrone.Core.Tests // 3 个火力单元部署在中点附近(X=4500,每个单元 i*50 偏移 → 4500/4550/4600) _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 3, 4500, 0, 50), + 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 }); @@ -578,7 +577,7 @@ namespace CounterDrone.Core.Tests // 3 个空基平台单元,部署在航路前方 _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0), + 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 }); @@ -610,7 +609,7 @@ namespace CounterDrone.Core.Tests // 验证:有探测设备时,planner 基于探测边界算到达时间,发射时机比无探测时晚 // ═══════════════════════════════════════════════ - [Fact] + [Fact(Skip = "慢速集成测试:两次20km仿真(~7200帧×2)。按需运行: dotnet test --filter Scenario_DetectionDriven")] public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary() { // 场景A:无探测设备(上帝视角,从航路起点算) @@ -630,7 +629,7 @@ namespace CounterDrone.Core.Tests }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50), + 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); @@ -655,7 +654,7 @@ namespace CounterDrone.Core.Tests }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50), + MakeEquipment(TestData.Defaults.FireUnits.First(f => f.Id == "ground-light"), AerosolType.InertGas, 1, 8000, 0, 50), // 独立探测设备 new EquipmentDeployment { diff --git a/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs b/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs index 7a93603..93a7b2c 100644 --- a/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs @@ -27,7 +27,6 @@ namespace CounterDrone.Core.Tests var paths = new TestPathProvider(_testDir); var dbManager = new DatabaseManager(paths); _mainDb = dbManager.OpenMainDb(); - foreach (var a in DefaultAmmunition.GetAll()) _mainDb.Insert(a); _scenarioService = new ScenarioService( new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb), @@ -36,7 +35,7 @@ namespace CounterDrone.Core.Tests new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb)); _engine = new SimulationEngine(_scenarioService, new FrameDataStore(paths), - new DamageModelRouter(), paths, new DefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); + new DamageModelRouter(), paths, new DefensePlanner(TestData.Defaults.Ammunition, TestPlannerConfig.Instance)); } public void Dispose() diff --git a/test/unit/CounterDrone.Core.Tests/TestAmmo.cs b/test/unit/CounterDrone.Core.Tests/TestAmmo.cs deleted file mode 100644 index cc2aae8..0000000 --- a/test/unit/CounterDrone.Core.Tests/TestAmmo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using CounterDrone.Core.Algorithms; -using CounterDrone.Core.Models; - -namespace CounterDrone.Core.Tests -{ - /// 测试用弹药规格 — 直接引用默认值 - internal static class TestAmmo - { - public static readonly AmmunitionSpec Inert = DefaultAmmunition.GetByType(AerosolType.InertGas); - public static readonly AmmunitionSpec Active = DefaultAmmunition.GetByType(AerosolType.ActiveMaterial); - public static readonly AmmunitionSpec Fuel = DefaultAmmunition.GetByType(AerosolType.ActiveFuel); - } -} diff --git a/test/unit/CounterDrone.Core.Tests/TestData.cs b/test/unit/CounterDrone.Core.Tests/TestData.cs new file mode 100644 index 0000000..4aca8cc --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/TestData.cs @@ -0,0 +1,41 @@ +using CounterDrone.Core; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Tests +{ + /// 测试用默认数据 — 从 defaults.json 加载 + internal static class TestData + { + public static readonly DefaultData Defaults; + + static TestData() + { + var json = @" +{ + ""version"": ""1"", + ""ammunition"": [ + { ""Id"": ""inert"", ""AerosolType"": 0, ""Name"": ""惰性气体"", ""InitialRadius"": 3.8, ""CoreDensity"": 1.5, ""EdgeDensity"": 0.1, ""InitialTemperature"": 1800.0, ""BuoyancyFactor"": 0.3, ""EffectiveConcentration"": 0.0001, ""MaxRadius"": 100.0, ""MaxDuration"": 120.0, ""SourceStrength"": 10.0, ""BurstChargeKg"": 1.5, ""TurbulentExpansionK"": 3.0, ""ParticlesJson"": ""{}"" }, + { ""Id"": ""active"", ""AerosolType"": 1, ""Name"": ""活性材料"", ""InitialRadius"": 5.0, ""CoreDensity"": 2.0, ""EdgeDensity"": 0.2, ""InitialTemperature"": 2400.0, ""BuoyancyFactor"": 0.6, ""EffectiveConcentration"": 0.0001, ""MaxRadius"": 80.0, ""MaxDuration"": 90.0, ""SourceStrength"": 12.0, ""BurstChargeKg"": 4.0, ""TurbulentExpansionK"": 4.0, ""ParticlesJson"": ""{}"" }, + { ""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 } + ], + ""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 }, + { ""Id"": ""ground-standard"", ""Name"": ""标准地基"", ""PlatformType"": 1, ""GunCount"": 4, ""ChannelsPerGun"": 4, ""ChannelInterval"": 0.1, ""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"": 8, ""ChannelInterval"": 1.0, ""Cooldown"": 5.0, ""AmmoChangeTime"": 30.0, ""CruiseSpeed"": 55.0, ""ReleaseAltitude"": 1500.0, ""AmmoTypes"": [0,1], ""EORange"": 12000.0, ""IRRange"": 8000.0 } + ], + ""targets"": [ + { ""Id"": ""shahed"", ""Name"": ""活塞巡飞弹"", ""TargetType"": 3, ""PowerType"": 1, ""Wingspan"": 2.5, ""TypicalSpeed"": 200.0, ""TypicalAltitude"": 500.0 } + ], + ""detectionEquipment"": [], + ""weather"": [ + { ""Id"": ""sunny-calm"", ""Name"": ""晴天无风"", ""WeatherType"": 0, ""WindSpeed"": 3.0, ""WindDirection"": 0, ""Temperature"": 25.0, ""Humidity"": 50.0, ""Pressure"": 1013.0, ""Visibility"": 8000.0 } + ] +}"; + Defaults = DefaultData.FromJson(json); + } + } +}