ScenarioDrone/ScenarioUnit 表精简:外键引用基础数据,去冗余字段

- ScenarioDrone: 仅保留 Id/ScenarioId/DroneSpecId/WaveId/Quantity
- ScenarioUnit: 仅保留 Id/ScenarioId/FireUnitSpecId/SensorSpecId/Position 等想定字段
- DroneEntity/PlatformEntity 构造函数改为接收 DroneSpec/FireUnitSpec
- SimulationEngine 加载 spec 字典,build 方法通过 FK 查 spec
- ReportGenerator/ReportService 通过 spec 字典取值
- DefaultLaneDivider.Divide 从 DroneWave.Quantity 取值
- Core 编译通过,测试待修复
This commit is contained in:
tian 2026-06-18 16:47:30 +08:00
parent 537671ab3a
commit 69ec9ed0e2
11 changed files with 151 additions and 172 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
@ -51,7 +52,8 @@ namespace CounterDrone.Core.Algorithms
public class DroneWave
{
public string WaveId { get; set; } = string.Empty;
public ScenarioDrone Profile { get; set; } = new();
public DroneSpec Profile { get; set; } = new();
public int Quantity { get; set; } = 1;
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();

View File

@ -7,14 +7,14 @@ namespace CounterDrone.Core.Algorithms
{
public (int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius)
{
if (wave.Route == null || wave.Profile.Quantity <= 1)
if (wave.Route == null || wave.Quantity <= 1)
return (1, 0);
var mode = (FormationMode)wave.Route.FormationMode;
if (mode != FormationMode.Formation)
return (1, 0);
int latCount = wave.Route.LateralCount ?? wave.Profile.Quantity;
int latCount = wave.Route.LateralCount ?? wave.Quantity;
int longCount = wave.Route.LongitudinalCount ?? 1;
float latWidth = (latCount - 1) * (float)wave.Route.LateralSpacing;
float longDepth = (longCount - 1) * (float)wave.Route.LongitudinalSpacing;
@ -22,7 +22,7 @@ namespace CounterDrone.Core.Algorithms
// 任一维度超过云团半径,则无法一个云团覆盖
if (latWidth > cloudRadius || longDepth > cloudRadius)
return (wave.Profile.Quantity, (float)wave.Route.LateralSpacing);
return (wave.Quantity, (float)wave.Route.LateralSpacing);
return (1, 0);
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
@ -90,10 +91,10 @@ namespace CounterDrone.Core.Algorithms
// Step 1: 威胁指数
// ═══════════════════════════════════════════════
private static float CalcThreatIndex(PlannerConfig config, ScenarioDrone target)
private static float CalcThreatIndex(PlannerConfig config, DroneSpec spec)
{
float typeCoef = config.TypeCoefficient.GetValueOrDefault((DroneType)target.DroneType, 1f);
float speedCoef = (float)target.TypicalSpeed / 60f;
float typeCoef = config.TypeCoefficient.GetValueOrDefault((DroneType)spec.DroneType, 1f);
float speedCoef = (float)spec.TypicalSpeed / 60f;
return typeCoef * speedCoef;
}

View File

@ -130,28 +130,14 @@ namespace CounterDrone.Core
{
return new ScenarioUnit
{
FireUnitSpecId = Id,
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,
MinElevation = MinElevation,
MaxElevation = MaxElevation,
MinDetectAlt = MinDetectAlt,
MaxDetectAlt = MaxDetectAlt,
};
}
}
@ -175,15 +161,9 @@ namespace CounterDrone.Core
{
return new ScenarioDrone
{
DroneSpecId = Id,
WaveId = waveId,
DroneType = DroneType,
Model = Model,
Description = Description,
PowerType = PowerType,
Quantity = 1,
Wingspan = Wingspan,
TypicalSpeed = TypicalSpeed,
TypicalAltitude = TypicalAltitude,
};
}
}
@ -209,19 +189,12 @@ namespace CounterDrone.Core
{
return new ScenarioUnit
{
SensorSpecId = Id,
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,
MinElevation = MinElevation,
MaxElevation = MaxElevation,
MinDetectAlt = MinDetectAlt,
MaxDetectAlt = MaxDetectAlt,
};
}
}

View File

@ -3,7 +3,7 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤2目标配置</summary>
/// <summary>想定数据:无人机批次</summary>
[Table("ScenarioDrone")]
public class ScenarioDrone
{
@ -13,24 +13,11 @@ namespace CounterDrone.Core.Models
[Indexed]
public string ScenarioId { get; set; } = string.Empty;
/// <summary>外键 → DroneSpec</summary>
public string DroneSpecId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public int DroneType { get; set; } = (int)Models.DroneType.Rotor;
/// <summary>型号</summary>
public string Model { get; set; } = string.Empty;
/// <summary>描述/用途</summary>
public string Description { get; set; } = string.Empty;
public int Quantity { get; set; } = 1;
public int PowerType { get; set; } = (int)Models.PowerType.Electric;
public double Wingspan { get; set; } = 1.2;
public double TypicalSpeed { get; set; } = 60.0;
public double TypicalAltitude { get; set; } = 300.0;
}
}

View File

@ -3,7 +3,7 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤3:装备部署</summary>
/// <summary>想定数据:装备部署</summary>
[Table("ScenarioUnit")]
public class ScenarioUnit
{
@ -13,6 +13,12 @@ namespace CounterDrone.Core.Models
[Indexed]
public string ScenarioId { get; set; } = string.Empty;
/// <summary>外键 → FireUnitSpec发射平台</summary>
public string FireUnitSpecId { get; set; } = string.Empty;
/// <summary>外键 → SensorSpec探测设备</summary>
public string SensorSpecId { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform;
@ -21,9 +27,6 @@ namespace CounterDrone.Core.Models
public string WaveId { get; set; } = string.Empty;
// 发射平台专用
public int? PlatformType { get; set; }
public double PositionX { get; set; }
public double PositionY { get; set; }
public double PositionZ { get; set; }
@ -32,44 +35,6 @@ namespace CounterDrone.Core.Models
public int? MunitionCount { get; set; }
/// <summary>火炮/发射器数量null=1</summary>
public int? GunCount { get; set; }
/// <summary>每门炮的火力通道数null=1</summary>
public int? ChannelsPerGun { get; set; }
/// <summary>同一通道连发最小间隔秒null=1</summary>
public double? ChannelInterval { get; set; }
public int Source { get; set; } = (int)ConfigSource.Manual;
public double? MuzzleVelocity { get; set; }
public double? ReleaseAltitude { get; set; }
public double Cooldown { get; set; } = 5.0;
/// <summary>空基平台巡航速度 m/s地基为 NULL</summary>
public double? CruiseSpeed { get; set; }
// ── 探测能力(独立探测设备和火力单元自带探测统一表达)──
/// <summary>雷达探测距离 m雨雾不衰减发射平台也可有</summary>
public double? RadarRange { get; set; }
/// <summary>光电探测距离 m受 Visibility 衰减)</summary>
public double? EORange { get; set; }
/// <summary>红外探测距离 m不受 Visibility 影响)</summary>
public double? IRRange { get; set; }
/// <summary>探测精度 m位置误差影响抛撒散布范围</summary>
public double? DetectionAccuracy { get; set; }
// ── 三维球冠几何参数(探测范围约束)──
/// <summary>俯仰角下限NULL=无限制(等价全向)</summary>
public double? MinElevation { get; set; }
/// <summary>俯仰角上限NULL=无限制</summary>
public double? MaxElevation { get; set; }
/// <summary>可探测高度下界 mNULL=无限制</summary>
public double? MinDetectAlt { get; set; }
/// <summary>可探测高度上界 mNULL=无限制</summary>
public double? MaxDetectAlt { get; set; }
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CounterDrone.Core;
using CounterDrone.Core.Models;
using CounterDrone.Core.Simulation;
@ -10,7 +11,10 @@ namespace CounterDrone.Core.Services
public class ReportGenerator
{
public string Generate(ScenarioConfig config, List<SimEvent> events,
Simulation.DroneStatus finalDroneStatus, float simulationDuration)
Simulation.DroneStatus finalDroneStatus, float simulationDuration,
Dictionary<string, DroneSpec> droneSpecs,
Dictionary<string, FireUnitSpec> fireUnitSpecs,
Dictionary<string, SensorSpec> sensorSpecs)
{
var sb = new StringBuilder();
var platforms = config.Units.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList();
@ -70,12 +74,14 @@ namespace CounterDrone.Core.Services
sb.AppendLine();
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 类型 | {TranslateTarget((DroneType)(target?.DroneType ?? 0))} |");
sb.AppendLine($"| 动力 | {TranslatePower((PowerType)(target?.PowerType ?? 0))} |");
// Resolve specs
droneSpecs.TryGetValue(target?.DroneSpecId ?? "", out var droneSpec);
sb.AppendLine($"| 类型 | {TranslateTarget((DroneType)(droneSpec?.DroneType ?? 0))} |");
sb.AppendLine($"| 动力 | {TranslatePower((PowerType)(droneSpec?.PowerType ?? 0))} |");
sb.AppendLine($"| 数量 | {totalDrones} 架 |");
sb.AppendLine($"| 翼展 | {target?.Wingspan ?? 0:F1} m |");
sb.AppendLine($"| 速度 | {target?.TypicalSpeed ?? 0:F0} km/h |");
sb.AppendLine($"| 高度 | {target?.TypicalAltitude ?? 0:F0} m |");
sb.AppendLine($"| 翼展 | {droneSpec?.Wingspan ?? 0:F1} m |");
sb.AppendLine($"| 速度 | {droneSpec?.TypicalSpeed ?? 0:F0} km/h |");
sb.AppendLine($"| 高度 | {droneSpec?.TypicalAltitude ?? 0:F0} m |");
sb.AppendLine();
// === 四、云团抛撒配置 ===
@ -110,7 +116,8 @@ namespace CounterDrone.Core.Services
{
var eq = config.Units[ei];
if (eq.EquipmentRole != (int)EquipmentRole.LaunchPlatform) continue;
int channels = (eq.GunCount ?? 1) * (eq.ChannelsPerGun ?? 1);
fireUnitSpecs.TryGetValue(eq.FireUnitSpecId, out var fuSpec);
int channels = (fuSpec?.GunCount ?? 1) * (fuSpec?.ChannelsPerGun ?? 1);
for (int j = 0; j < eq.Quantity; j++)
{
unitNum++;
@ -134,12 +141,13 @@ namespace CounterDrone.Core.Services
int unitNum2 = 0;
foreach (var eq in config.Units.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
int channels = (eq.GunCount ?? 1) * (eq.ChannelsPerGun ?? 1);
fireUnitSpecs.TryGetValue(eq.FireUnitSpecId, out var fuSpec);
int channels = (fuSpec?.GunCount ?? 1) * (fuSpec?.ChannelsPerGun ?? 1);
for (int j = 0; j < eq.Quantity; j++)
{
unitNum2++;
int fired = unitLaunchCounts.GetValueOrDefault(unitNum2, 0);
string ptype = ((PlatformType?)(eq.PlatformType ?? 0)) == PlatformType.AirBased ? "空基" : "地基";
string ptype = ((PlatformType?)(fuSpec?.PlatformType ?? 0)) == PlatformType.AirBased ? "空基" : "地基";
string ammo = eq.AerosolType.HasValue ? TranslateAerosol((AerosolType)eq.AerosolType) : "-";
sb.AppendLine($"| #{unitNum2} | {ptype} | ({eq.PositionX + j * 50:F0}, {eq.PositionY:F0}, {eq.PositionZ:F0}) | {ammo} | {eq.MunitionCount ?? channels} | {fired} |");
}
@ -156,8 +164,9 @@ namespace CounterDrone.Core.Services
int detIdx = 0;
foreach (var d in detections)
{
sensorSpecs.TryGetValue(d.SensorSpecId, out var sensor);
for (int j = 0; j < d.Quantity; j++)
sb.AppendLine($"| D{++detIdx} | ({d.PositionX:F0}, {d.PositionY:F0}, {d.PositionZ:F0}) | {(d.RadarRange ?? 0):F0} | {(d.EORange ?? 0):F0} | {(d.IRRange ?? 0):F0} | {(d.DetectionAccuracy ?? 0):F0} m | 1 |");
sb.AppendLine($"| D{++detIdx} | ({d.PositionX:F0}, {d.PositionY:F0}, {d.PositionZ:F0}) | {(sensor?.RadarRange ?? 0):F0} | {(sensor?.EORange ?? 0):F0} | {(sensor?.IRRange ?? 0):F0} | {(sensor?.Accuracy ?? 0):F0} m | 1 |");
}
sb.AppendLine();
}

View File

@ -23,6 +23,10 @@ namespace CounterDrone.Core.Services
public SimulationReport Generate(string scenarioId, ScenarioConfig config,
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 sensorSpecs = _db.Table<SensorSpec>().ToDictionary(s => s.Id);
var destroyed = events.Count(e => e.Type == SimEventType.DroneDestroyed);
var reached = events.Count(e => e.Type == SimEventType.DroneReachedTarget);
var intruded = events.Count(e => e.Type == SimEventType.ZoneIntruded);
@ -38,7 +42,8 @@ namespace CounterDrone.Core.Services
TargetCount = total,
EquipmentCount = config.Units.Count,
InterceptResult = (int)result,
Content = _generator.Generate(config, events, Simulation.DroneStatus.Flying, duration),
Content = _generator.Generate(config, events, Simulation.DroneStatus.Flying, duration,
droneSpecs, fireUnitSpecs, sensorSpecs),
};
_db.Insert(report);

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
@ -38,15 +39,15 @@ namespace CounterDrone.Core.Simulation
/// 查找弧长 s 所在段:最大的 i 使 _cumArc[i] >= s。</summary>
private readonly float[] _cumArc;
public DroneEntity(string id, string waveId, ScenarioDrone config, List<Waypoint> route,
public DroneEntity(string id, string waveId, DroneSpec spec, List<Waypoint> route,
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing,
FormationMode mode, int lateralAxis = 2, int longitudinalAxis = 0)
{
Id = id;
WaveId = waveId;
DroneType = (DroneType)config.DroneType;
PowerType = (PowerType)config.PowerType;
Wingspan = (float)config.Wingspan;
DroneType = (DroneType)spec.DroneType;
PowerType = (PowerType)spec.PowerType;
Wingspan = (float)spec.Wingspan;
float spd = (float)route[0].Speed;
if (spd <= 0) throw new InvalidOperationException($"无人机 {id}: 航路点速度必须 > 0");
CruiseSpeed = spd;

View File

@ -1,4 +1,5 @@
using System;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
@ -46,20 +47,20 @@ namespace CounterDrone.Core.Simulation
/// <summary>Ready 包含:冷却就绪 + 有弹药 + 非飞行中</summary>
public bool Ready => CooldownRemaining <= 0 && MunitionCount > 0 && State != PlatformState.FlyingToTarget;
public PlatformEntity(string id, ScenarioUnit config)
public PlatformEntity(string id, FireUnitSpec spec, ScenarioUnit deployment)
{
Id = id;
Role = (EquipmentRole)config.EquipmentRole;
PlatformType = config.PlatformType.HasValue ? (PlatformType?)config.PlatformType.Value : null;
PosX = (float)config.PositionX;
PosY = (float)config.PositionY;
PosZ = (float)config.PositionZ;
AerosolType = config.AerosolType.HasValue ? (AerosolType?)config.AerosolType.Value : null;
MunitionCount = config.MunitionCount ?? 1;
Cooldown = (float)config.Cooldown;
MuzzleVelocity = (float)(config.MuzzleVelocity ?? 0f);
CruiseSpeed = (float)(config.CruiseSpeed ?? 0f);
ReleaseAltitude = (float)(config.ReleaseAltitude ?? 0f);
Role = (EquipmentRole)deployment.EquipmentRole;
PlatformType = (PlatformType?)spec.PlatformType;
PosX = (float)deployment.PositionX;
PosY = (float)deployment.PositionY;
PosZ = (float)deployment.PositionZ;
AerosolType = deployment.AerosolType.HasValue ? (AerosolType?)deployment.AerosolType.Value : null;
MunitionCount = deployment.MunitionCount ?? 1;
Cooldown = (float)spec.Cooldown;
MuzzleVelocity = (float)spec.MuzzleVelocity;
CruiseSpeed = (float)spec.CruiseSpeed;
ReleaseAltitude = (float)spec.ReleaseAltitude;
PatrolX = PosX;
PatrolY = PosY;

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using CounterDrone.Core.Services;
@ -48,6 +49,9 @@ namespace CounterDrone.Core.Simulation
private CombatScene _scene = new();
private float _disperseHeight;
private AmmunitionSpec _ammoSpec = new();
private Dictionary<string, DroneSpec> _droneSpecs = new();
private Dictionary<string, FireUnitSpec> _fireUnitSpecs = new();
private Dictionary<string, SensorSpec> _sensorSpecs = new();
private int _tickRate = 20;
private readonly List<FireEvent> _fireSchedule = new();
private int _nextFireIndex;
@ -93,10 +97,13 @@ namespace CounterDrone.Core.Simulation
_tickRate = config.Info.TickRate;
_tickInterval = 1.0f / _tickRate;
using (var ammoDb = new SQLiteConnection(_paths.GetMainDbPath()))
using (var specDb = new SQLiteConnection(_paths.GetMainDbPath()))
{
_ammoSpec = ammoDb.Table<AmmunitionSpec>()
_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);
_sensorSpecs = specDb.Table<SensorSpec>().ToDictionary(s => s.Id);
}
if (_ammoSpec == null || _ammoSpec.Id == null)
_ammoSpec = DefaultData.Load(_paths).Ammunition
@ -105,6 +112,7 @@ namespace CounterDrone.Core.Simulation
_drones.Clear();
foreach (var target in config.Drones)
{
if (!_droneSpecs.TryGetValue(target.DroneSpecId, out var droneSpec)) continue;
var route = config.Routes.FirstOrDefault(r => r.WaveId == target.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(target.WaveId, new List<Waypoint>());
if (route == null || wps.Count == 0) continue;
@ -118,7 +126,7 @@ namespace CounterDrone.Core.Simulation
int latIdx = i % latCount;
int longIdx = i / latCount;
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId,
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode,
droneSpec, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode,
route.LateralAxis, route.LongitudinalAxis));
}
}
@ -128,15 +136,20 @@ namespace CounterDrone.Core.Simulation
_platforms.Clear();
foreach (var equip in config.Units)
{
if (equip.EquipmentRole != (int)EquipmentRole.Detection)
{
if (!_fireUnitSpecs.TryGetValue(equip.FireUnitSpecId, out var fuSpec)) continue;
for (int i = 0; i < equip.Quantity; i++)
{
int gunCount = equip.GunCount ?? 1;
int chPerGun = equip.ChannelsPerGun ?? 1;
int gunCount = fuSpec.GunCount;
int chPerGun = fuSpec.ChannelsPerGun;
for (int g = 0; g < gunCount; g++)
for (int ch = 0; ch < chPerGun; ch++)
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", equip));
_platforms.Add(new PlatformEntity($"plat_{++_entityCounter}", fuSpec, equip));
}
}
}
_zones.Clear();
foreach (var z in config.ControlZones)
@ -145,7 +158,7 @@ namespace CounterDrone.Core.Simulation
// 探测设备运行时实体
_detectionEntities.Clear();
var detectionSrcs = BuildDetectionSources(config);
var detectionSrcs = BuildDetectionSources(config, _fireUnitSpecs, _sensorSpecs);
int detIdx = 0;
foreach (var src in detectionSrcs)
_detectionEntities.Add(new DetectionEntity($"det_{++detIdx}", src));
@ -161,11 +174,11 @@ namespace CounterDrone.Core.Simulation
// 自动从 Planner 生成发射计划
if (_fireSchedule.Count == 0)
{
var fireUnits = BuildFireUnits(config);
var threats = BuildDroneWaves(config);
var fireUnits = BuildFireUnits(config, _fireUnitSpecs);
var threats = BuildDroneWaves(config, _droneSpecs);
if (fireUnits.Count > 0 && threats.Count > 0)
{
var detectionSources = BuildDetectionSources(config);
var detectionSources = BuildDetectionSources(config, _fireUnitSpecs, _sensorSpecs);
var result = _planner.Plan(fireUnits, threats, _scene, detectionSources);
SetFireSchedule(result.Best.MergedSchedule);
if (_fireSchedule.Count == 0)
@ -427,83 +440,105 @@ namespace CounterDrone.Core.Simulation
public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; }
public void Stop() { State = SimulationState.Stopped; _frameStore.Flush(); }
private static List<FireUnit> BuildFireUnits(ScenarioConfig config)
private List<FireUnit> BuildFireUnits(ScenarioConfig config, Dictionary<string, FireUnitSpec> 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;
int qty = Math.Max(1, eq.Quantity);
int gunCount = eq.GunCount ?? 1;
int chPerGun = eq.ChannelsPerGun ?? 1;
int channels = gunCount * chPerGun;
for (int i = 0; i < qty; i++)
{
units.Add(new FireUnit
{
Id = $"fu{units.Count}",
Type = (PlatformType)(eq.PlatformType ?? (int)PlatformType.GroundBased),
Type = (PlatformType)spec.PlatformType,
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
GunCount = gunCount,
ChannelsPerGun = chPerGun,
ChannelInterval = (float)(eq.ChannelInterval ?? 1),
CruiseSpeed = (float)(eq.CruiseSpeed ?? 0f),
ReleaseAltitude = (float)(eq.ReleaseAltitude ?? 0f),
MuzzleVelocity = (float)(eq.MuzzleVelocity ?? 0f),
TotalMunitions = eq.MunitionCount ?? channels,
GunCount = spec.GunCount,
ChannelsPerGun = spec.ChannelsPerGun,
ChannelInterval = (float)spec.ChannelInterval,
CruiseSpeed = (float)spec.CruiseSpeed,
ReleaseAltitude = (float)spec.ReleaseAltitude,
MuzzleVelocity = (float)spec.MuzzleVelocity,
TotalMunitions = eq.MunitionCount ?? spec.GunCount * spec.ChannelsPerGun,
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
Cooldown = (float)eq.Cooldown,
// 探测能力(火力单元自带探测,激活原死字段)
RadarRange = (float)(eq.RadarRange ?? 0f),
EORange = (float)(eq.EORange ?? 0f),
IRRange = (float)(eq.IRRange ?? 0f),
Cooldown = (float)spec.Cooldown,
RadarRange = (float)spec.RadarRange,
EORange = (float)spec.EORange,
IRRange = (float)spec.IRRange,
});
}
}
return units;
}
/// <summary>构建统一信息网络的探测源列表。
/// 独立探测设备EquipmentRole.Detection+ 火力单元自带探测能力。</summary>
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config)
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config, Dictionary<string, FireUnitSpec> fuSpecs, Dictionary<string, SensorSpec> sensorSpecs)
{
var sources = new List<DetectionSource>();
foreach (var eq in config.Units)
{
// 只要有任何探测字段非 null 且 > 0就是有效探测源无论是否独立探测设备
bool hasDetection = (eq.RadarRange ?? 0) > 0 || (eq.EORange ?? 0) > 0 || (eq.IRRange ?? 0) > 0;
if (!hasDetection) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
// Launch platform: get detection from FireUnitSpec
if (eq.EquipmentRole == (int)EquipmentRole.LaunchPlatform)
{
sources.Add(new DetectionSource
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++)
{
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
RadarRange = (float)(eq.RadarRange ?? 0f),
EORange = (float)(eq.EORange ?? 0f),
IRRange = (float)(eq.IRRange ?? 0f),
Accuracy = (float)(eq.DetectionAccuracy ?? 0f),
// 3D 球冠几何:缺失时退化为无限制(等价 2D保证存量数据平滑过渡
MinElevation = (float)(eq.MinElevation ?? float.MaxValue),
MaxElevation = (float)(eq.MaxElevation ?? float.MaxValue),
MinDetectAlt = (float)(eq.MinDetectAlt ?? float.MaxValue),
MaxDetectAlt = (float)(eq.MaxDetectAlt ?? float.MaxValue),
});
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), // default accuracy
MinElevation = (float)(spec.MinElevation ?? float.MaxValue),
MaxElevation = (float)(spec.MaxElevation ?? float.MaxValue),
MinDetectAlt = (float)(spec.MinDetectAlt ?? float.MaxValue),
MaxDetectAlt = (float)(spec.MaxDetectAlt ?? float.MaxValue),
});
}
}
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),
});
}
}
}
return sources;
}
private static List<DroneWave> BuildDroneWaves(ScenarioConfig config)
private static List<DroneWave> BuildDroneWaves(ScenarioConfig config, Dictionary<string, DroneSpec> specs)
{
var groups = new List<DroneWave>();
foreach (var t in config.Drones)
{
if (!specs.TryGetValue(t.DroneSpecId, out var droneSpec)) continue;
var route = config.Routes.FirstOrDefault(r => r.WaveId == t.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(t.WaveId, new List<Waypoint>());
groups.Add(new DroneWave
{
WaveId = t.WaveId,
Profile = t,
Profile = droneSpec,
Quantity = t.Quantity,
Route = route ?? new RoutePlan(),
Waypoints = wps,
});