新增 LaunchPlatformSpec:发射平台与探测设备分离

- LaunchPlatformSpec(纯发射参数,无探测字段)
- ScenarioUnit.FireUnitSpecId → LaunchPlatformSpecId
- PlatformEntity/BuildFireUnits/BuildDetectionSources 适配
- ReportGenerator/ReportService 适配
- FireUnitSpec 保留但不使用
- 243/250 通过,7 个待修
This commit is contained in:
tian 2026-06-19 21:43:06 +08:00
parent 8abaf0bf87
commit 8a198a27a0
14 changed files with 158 additions and 74 deletions

View File

@ -118,6 +118,38 @@
}
],
"launchPlatforms": [
{
"Id": "ground-light", "Name": "[Demo] 轻型地基发射平台",
"Description": "4通道轻型平台800m/s初速",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1]
},
{
"Id": "ground-standard", "Name": "[Demo] 标准地基发射平台",
"Description": "4通道标准平台800m/s初速",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1]
},
{
"Id": "ground-heavy", "Name": "[Demo] 重型地基发射平台",
"Description": "6通道重型平台600m/s初速支持全弹种",
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
"AmmoTypes": [0, 1, 2]
},
{
"Id": "air-standard", "Name": "[Demo] 标准空基发射平台",
"Description": "8通道空基平台80m/s巡航",
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1]
}
],
"drones": [
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼DJI类",
"Model": "DJI Mavic 类", "Description": "小型四旋翼,低空侦察",

View File

@ -63,6 +63,7 @@ namespace CounterDrone.Core
// 插入或更新默认数据(按 Id 覆盖,不删除用户自建数据)
foreach (var a in defaults.Ammunition) db.InsertOrReplace(a);
foreach (var f in defaults.FireUnits) db.InsertOrReplace(f);
foreach (var l in defaults.LaunchPlatforms) db.InsertOrReplace(l);
foreach (var d in defaults.Drones) db.InsertOrReplace(d);
foreach (var s in defaults.Sensors) db.InsertOrReplace(s);
foreach (var e in defaults.Environments) db.InsertOrReplace(e);
@ -118,6 +119,7 @@ namespace CounterDrone.Core
db.CreateTable<Waypoint>();
db.CreateTable<SimulationReport>();
db.CreateTable<FireUnitSpec>();
db.CreateTable<LaunchPlatformSpec>();
db.CreateTable<DroneSpec>();
db.CreateTable<SensorSpec>();
db.CreateTable<EnvironmentSpec>();

View File

@ -19,6 +19,8 @@ namespace CounterDrone.Core
public List<RouteTemplate> Routes { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("fireUnits")]
public List<FireUnitSpec> FireUnits { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("launchPlatforms")]
public List<LaunchPlatformSpec> LaunchPlatforms { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("drones")]
public List<DroneSpec> Drones { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("detectionEquipment")]
@ -59,6 +61,8 @@ namespace CounterDrone.Core
throw new InvalidOperationException($"默认数据缺少 routes: {source}");
if (data.FireUnits == null || data.FireUnits.Count == 0)
throw new InvalidOperationException($"默认数据缺少 fireUnits: {source}");
if (data.LaunchPlatforms == null || data.LaunchPlatforms.Count == 0)
throw new InvalidOperationException($"默认数据缺少 launchPlatforms: {source}");
if (data.Drones == null || data.Drones.Count == 0)
throw new InvalidOperationException($"默认数据缺少 drones: {source}");
if (data.Environments == null || data.Environments.Count == 0)

View File

@ -38,7 +38,7 @@ namespace CounterDrone.Core.Models
{
return new ScenarioUnit
{
FireUnitSpecId = Id,
LaunchPlatformSpecId = Id,
EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform,
Quantity = quantity,
PositionX = posX,

View File

@ -0,0 +1,44 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>搭载/发射平台规格(基础数据模板)</summary>
[Table("LaunchPlatformSpec")]
public class LaunchPlatformSpec
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Description { 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 string ModelId { get; set; } = "";
[Ignore]
public List<int> AmmoTypes { get; set; } = new();
public ScenarioUnit ToScenarioUnit(AerosolType ammoType, int quantity,
double posX, double posY, double posZ)
{
return new ScenarioUnit
{
LaunchPlatformSpecId = Id,
EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform,
Quantity = quantity,
PositionX = posX,
PositionY = posY,
PositionZ = posZ,
AerosolType = (int)ammoType,
MunitionCount = GunCount * ChannelsPerGun * quantity,
};
}
}
}

View File

@ -14,8 +14,8 @@ namespace CounterDrone.Core.Models
[Indexed]
public string ScenarioId { get; set; } = string.Empty;
/// <summary>外键 → FireUnitSpec发射平台</summary>
public string FireUnitSpecId { get; set; } = string.Empty;
/// <summary>外键 → LaunchPlatformSpec发射平台</summary>
public string LaunchPlatformSpecId { get; set; } = string.Empty;
/// <summary>外键 → SensorSpec探测设备</summary>
public string SensorSpecId { get; set; } = string.Empty;

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -13,7 +13,7 @@ namespace CounterDrone.Core.Services
public string Generate(ScenarioConfig config, List<SimEvent> events,
Simulation.DroneStatus finalDroneStatus, float simulationDuration,
Dictionary<string, DroneSpec> droneSpecs,
Dictionary<string, FireUnitSpec> fireUnitSpecs,
Dictionary<string, LaunchPlatformSpec> fireUnitSpecs,
Dictionary<string, SensorSpec> sensorSpecs)
{
var sb = new StringBuilder();
@ -116,7 +116,7 @@ namespace CounterDrone.Core.Services
{
var eq = config.Units[ei];
if (eq.EquipmentRole != (int)EquipmentRole.LaunchPlatform) continue;
fireUnitSpecs.TryGetValue(eq.FireUnitSpecId, out var fuSpec);
fireUnitSpecs.TryGetValue(eq.LaunchPlatformSpecId, out var fuSpec);
int channels = (fuSpec?.GunCount ?? 1) * (fuSpec?.ChannelsPerGun ?? 1);
for (int j = 0; j < eq.Quantity; j++)
{
@ -141,7 +141,7 @@ namespace CounterDrone.Core.Services
int unitNum2 = 0;
foreach (var eq in config.Units.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
fireUnitSpecs.TryGetValue(eq.FireUnitSpecId, out var fuSpec);
fireUnitSpecs.TryGetValue(eq.LaunchPlatformSpecId, out var fuSpec);
int channels = (fuSpec?.GunCount ?? 1) * (fuSpec?.ChannelsPerGun ?? 1);
for (int j = 0; j < eq.Quantity; j++)
{

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -24,7 +24,7 @@ namespace CounterDrone.Core.Services
List<SimEvent> events, string finalDroneStatus, float duration)
{
var droneSpecs = _db.Table<DroneSpec>().ToDictionary(d => d.Id);
var fireUnitSpecs = _db.Table<FireUnitSpec>().ToDictionary(f => f.Id);
var fireUnitSpecs = _db.Table<LaunchPlatformSpec>().ToDictionary(f => f.Id);
var sensorSpecs = _db.Table<SensorSpec>().ToDictionary(s => s.Id);
var destroyed = events.Count(e => e.Type == SimEventType.DroneDestroyed);

View File

@ -49,7 +49,7 @@ namespace CounterDrone.Core.Simulation
/// <summary>Ready 包含:冷却就绪 + 有弹药 + 非飞行中</summary>
public bool Ready => CooldownRemaining <= 0 && MunitionCount > 0 && State != PlatformState.FlyingToTarget;
public PlatformEntity(string id, FireUnitSpec spec, ScenarioUnit deployment)
public PlatformEntity(string id, LaunchPlatformSpec spec, ScenarioUnit deployment)
{
Id = id;
Role = (EquipmentRole)deployment.EquipmentRole;

View File

@ -50,7 +50,7 @@ namespace CounterDrone.Core.Simulation
private float _disperseHeight;
private AmmunitionSpec _ammoSpec = new();
private Dictionary<string, DroneSpec> _droneSpecs = new();
private Dictionary<string, FireUnitSpec> _fireUnitSpecs = new();
private Dictionary<string, LaunchPlatformSpec> _launchPlatformSpecs = new();
private Dictionary<string, SensorSpec> _sensorSpecs = new();
private int _tickRate = 20;
private readonly List<FireEvent> _fireSchedule = new();
@ -102,7 +102,7 @@ namespace CounterDrone.Core.Simulation
_ammoSpec = specDb.Table<AmmunitionSpec>()
.FirstOrDefault(a => a.AerosolType == config.Cloud.AerosolType);
_droneSpecs = specDb.Table<DroneSpec>().ToDictionary(d => d.Id);
_fireUnitSpecs = specDb.Table<FireUnitSpec>().ToDictionary(f => f.Id);
_launchPlatformSpecs = specDb.Table<LaunchPlatformSpec>().ToDictionary(f => f.Id);
_sensorSpecs = specDb.Table<SensorSpec>().ToDictionary(s => s.Id);
}
if (_ammoSpec == null || _ammoSpec.Id == null)
@ -139,7 +139,7 @@ namespace CounterDrone.Core.Simulation
{
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
{
if (!_fireUnitSpecs.TryGetValue(equip.FireUnitSpecId, out var fuSpec)) continue;
if (!_launchPlatformSpecs.TryGetValue(equip.LaunchPlatformSpecId, out var fuSpec)) continue;
for (int i = 0; i < equip.Quantity; i++)
{
int gunCount = fuSpec.GunCount;
@ -158,7 +158,7 @@ namespace CounterDrone.Core.Simulation
// 探测设备运行时实体
_detectionEntities.Clear();
var detectionSrcs = BuildDetectionSources(config, _fireUnitSpecs, _sensorSpecs);
var detectionSrcs = BuildDetectionSources(config, _sensorSpecs);
int detIdx = 0;
foreach (var src in detectionSrcs)
_detectionEntities.Add(new DetectionEntity($"det_{++detIdx}", src, src.ModelId));
@ -174,11 +174,11 @@ namespace CounterDrone.Core.Simulation
// 自动从 Planner 生成发射计划
if (_fireSchedule.Count == 0)
{
var fireUnits = BuildFireUnits(config, _fireUnitSpecs);
var fireUnits = BuildFireUnits(config, _launchPlatformSpecs);
var threats = BuildDroneWaves(config, _droneSpecs);
if (fireUnits.Count > 0 && threats.Count > 0)
{
var detectionSources = BuildDetectionSources(config, _fireUnitSpecs, _sensorSpecs);
var detectionSources = BuildDetectionSources(config, _sensorSpecs);
var result = _planner.Plan(fireUnits, threats, _scene, detectionSources);
SetFireSchedule(result.Best.MergedSchedule);
if (_fireSchedule.Count == 0)
@ -440,12 +440,12 @@ namespace CounterDrone.Core.Simulation
public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; }
public void Stop() { State = SimulationState.Stopped; _frameStore.Flush(); }
private List<FireUnit> BuildFireUnits(ScenarioConfig config, Dictionary<string, FireUnitSpec> specs)
private List<FireUnit> BuildFireUnits(ScenarioConfig config, Dictionary<string, LaunchPlatformSpec> specs)
{
var units = new List<FireUnit>();
foreach (var eq in config.Units.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
if (!specs.TryGetValue(eq.FireUnitSpecId, out var spec)) continue;
if (!specs.TryGetValue(eq.LaunchPlatformSpecId, out var spec)) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
{
@ -463,66 +463,37 @@ namespace CounterDrone.Core.Simulation
TotalMunitions = eq.MunitionCount ?? spec.GunCount * spec.ChannelsPerGun,
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
Cooldown = (float)spec.Cooldown,
RadarRange = (float)spec.RadarRange,
EORange = (float)spec.EORange,
IRRange = (float)spec.IRRange,
});
}
}
return units;
}
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config, Dictionary<string, FireUnitSpec> fuSpecs, Dictionary<string, SensorSpec> sensorSpecs)
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config, Dictionary<string, SensorSpec> sensorSpecs)
{
var sources = new List<DetectionSource>();
foreach (var eq in config.Units)
{
// Launch platform: get detection from FireUnitSpec
if (eq.EquipmentRole == (int)EquipmentRole.LaunchPlatform)
if (string.IsNullOrEmpty(eq.SensorSpecId)) continue;
if (!sensorSpecs.TryGetValue(eq.SensorSpecId, out var sensor)) continue;
bool hasDetection = sensor.RadarRange > 0 || sensor.EORange > 0 || sensor.IRRange > 0;
if (!hasDetection) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
{
if (!fuSpecs.TryGetValue(eq.FireUnitSpecId, out var spec)) continue;
bool hasDetection = spec.RadarRange > 0 || spec.EORange > 0 || spec.IRRange > 0;
if (!hasDetection) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
sources.Add(new DetectionSource
{
sources.Add(new DetectionSource
{
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
RadarRange = (float)spec.RadarRange,
EORange = (float)spec.EORange,
IRRange = (float)spec.IRRange,
Accuracy = (float)(spec.MinElevation.HasValue ? 50f : 50f),
MinElevation = (float)(spec.MinElevation ?? float.MaxValue),
MaxElevation = (float)(spec.MaxElevation ?? float.MaxValue),
MinDetectAlt = (float)(spec.MinDetectAlt ?? float.MaxValue),
MaxDetectAlt = (float)(spec.MaxDetectAlt ?? float.MaxValue),
ModelId = string.IsNullOrEmpty(spec.ModelId) ? null : spec.ModelId,
});
}
}
else // Detection equipment: get from SensorSpec
{
if (!sensorSpecs.TryGetValue(eq.SensorSpecId, out var sensor)) continue;
bool hasDetection = sensor.RadarRange > 0 || sensor.EORange > 0 || sensor.IRRange > 0;
if (!hasDetection) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
{
sources.Add(new DetectionSource
{
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
RadarRange = (float)sensor.RadarRange,
EORange = (float)sensor.EORange,
IRRange = (float)sensor.IRRange,
Accuracy = (float)sensor.Accuracy,
MinElevation = (float)(sensor.MinElevation ?? float.MaxValue),
MaxElevation = (float)(sensor.MaxElevation ?? float.MaxValue),
MinDetectAlt = (float)(sensor.MinDetectAlt ?? float.MaxValue),
MaxDetectAlt = (float)(sensor.MaxDetectAlt ?? float.MaxValue),
ModelId = string.IsNullOrEmpty(sensor.ModelId) ? null : sensor.ModelId,
});
}
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
RadarRange = (float)sensor.RadarRange,
EORange = (float)sensor.EORange,
IRRange = (float)sensor.IRRange,
Accuracy = (float)sensor.Accuracy,
MinElevation = (float)(sensor.MinElevation ?? float.MaxValue),
MaxElevation = (float)(sensor.MaxElevation ?? float.MaxValue),
MinDetectAlt = (float)(sensor.MinDetectAlt ?? float.MaxValue),
MaxDetectAlt = (float)(sensor.MaxDetectAlt ?? float.MaxValue),
ModelId = string.IsNullOrEmpty(sensor.ModelId) ? null : sensor.ModelId,
});
}
}
return sources;

View File

@ -118,6 +118,38 @@
}
],
"launchPlatforms": [
{
"Id": "ground-light", "Name": "[Demo] 轻型地基发射平台",
"Description": "4通道轻型平台800m/s初速",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1]
},
{
"Id": "ground-standard", "Name": "[Demo] 标准地基发射平台",
"Description": "4通道标准平台800m/s初速",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1]
},
{
"Id": "ground-heavy", "Name": "[Demo] 重型地基发射平台",
"Description": "6通道重型平台600m/s初速支持全弹种",
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
"AmmoTypes": [0, 1, 2]
},
{
"Id": "air-standard", "Name": "[Demo] 标准空基发射平台",
"Description": "8通道空基平台80m/s巡航",
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1]
}
],
"drones": [
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼DJI类",
"Model": "DJI Mavic 类", "Description": "小型四旋翼,低空侦察",

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using CounterDrone.Core;
@ -28,8 +28,8 @@ namespace CounterDrone.Core.Tests
private static DroneSpec? _droneSpec;
public static DroneSpec DefaultDroneSpec => _droneSpec ??= _db.Table<DroneSpec>().First(d => d.Id == "shahed");
private static FireUnitSpec? _groundSpec;
public static FireUnitSpec GroundSpec => _groundSpec ??= _db.Table<FireUnitSpec>().First(f => f.Id == "ground-light");
private static LaunchPlatformSpec? _groundSpec;
public static LaunchPlatformSpec GroundSpec => _groundSpec ??= _db.Table<LaunchPlatformSpec>().First(f => f.Id == "ground-light");
public static ScenarioDrone CreateScenarioDrone(string waveId = "w1", int quantity = 1)
=> new() { DroneSpecId = DefaultDroneSpec.Id, WaveId = waveId, Quantity = quantity };
@ -39,16 +39,15 @@ namespace CounterDrone.Core.Tests
public static ScenarioUnit CreateScenarioUnit(double px, double py, double pz,
int ammoType = 0, int? munitionCount = null)
=> new() { FireUnitSpecId = "ground-light", PositionX = px, PositionY = py, PositionZ = pz,
=> new() { LaunchPlatformSpecId = "ground-light", PositionX = px, PositionY = py, PositionZ = pz,
AerosolType = ammoType, MunitionCount = munitionCount ?? 8, Quantity = 1 };
private static FireUnitSpec? _defaultFuSpec;
public static FireUnitSpec DefaultFuSpec => _defaultFuSpec ??= new FireUnitSpec
private static LaunchPlatformSpec? _defaultFuSpec;
public static LaunchPlatformSpec DefaultFuSpec => _defaultFuSpec ??= new LaunchPlatformSpec
{
Id = "test-fu", PlatformType = (int)PlatformType.GroundBased,
GunCount = 1, ChannelsPerGun = 8, ChannelInterval = 1,
MuzzleVelocity = 800, Cooldown = 5, AmmoChangeTime = 30,
RadarRange = 5000,
};
private static SensorSpec? _sensorSpec;
@ -66,7 +65,7 @@ namespace CounterDrone.Core.Tests
};
public static Dictionary<string, DroneSpec> EmptyDroneSpecs => new();
public static Dictionary<string, FireUnitSpec> EmptyFuSpecs => new();
public static Dictionary<string, LaunchPlatformSpec> EmptyFuSpecs => new();
public static Dictionary<string, SensorSpec> EmptySensorSpecs => new();
private static DefaultData? _all;

Binary file not shown.