feat: detection-driven planning (v0.6.0)
Planner no longer has god-view. It assumes threats enter from detection boundary (unified info network earliest detection point), not route start. Core: DetectionCalculator (EO attenuated by Visibility, radar/IR unaffected; earliest detection via segment-circle intersection). EquipmentDeployment: drop DetectionRadius, add RadarRange/EORange/IRange/DetectionAccuracy. FireUnit detection fields activated in BuildFireUnits. IDefensePlanner.Plan adds 4th param detectionSources. Engine: BuildDetectionSources merges standalone detectors + fire-unit self-detection into unified list, passed to planner. Fix: Solve robustness when GenerateFireEventsAt returns empty (detection boundary too late to intercept). Tests 193 to 208 (+15). 0 warnings.
This commit is contained in:
parent
3028a421b5
commit
11f8cb2c79
29
CHANGELOG.md
29
CHANGELOG.md
@ -2,6 +2,35 @@
|
||||
|
||||
---
|
||||
|
||||
## [0.6.0] - 2026-06-15
|
||||
|
||||
### Added — 探测驱动的规划(8.1.1)
|
||||
|
||||
- **DetectionCalculator 静态工具类**:探测能力评估的唯一实现(与 Kinematics/RouteGeometry 同范式)。光电受 Visibility 衰减(`有效=基准×min(1,Visibility/基准)`),雷达/红外不受影响;统一信息网络找最早探测点(线段-圆求交)
|
||||
- **探测源模型**(DetectionSource):独立探测设备 + 火力单元自带探测统一表达(雷达/光电/红外三距离 + 精度)
|
||||
- **EquipmentDeployment 扩展**:删单一 `DetectionRadius`,加 `RadarRange/EORange/IRange/DetectionAccuracy` 四字段
|
||||
- **FireUnit 探测字段激活**:BuildFireUnits 从 EquipmentDeployment 读取并赋值(原为死代码)
|
||||
- **planner 接口扩展**:`IDefensePlanner.Plan` 加第 4 参数 `detectionSources`
|
||||
- **PlannerConfig 加 DefaultDetectionAccuracy**(无探测时回退精度)
|
||||
- 单元测试:DetectionCalculator 13 项、探测驱动规划 2 项
|
||||
|
||||
### Changed — planner 基于探测信息规划
|
||||
|
||||
- planner 假设威胁从探测边界被发现,到达时间 = (拦截弧长 − 探测弧长)/速度,而非上帝视角的航路起点
|
||||
- SimulationEngine.Initialize 构建 `List<DetectionSource>`(独立探测 + 火力单元自带),传入 planner
|
||||
- 报告探测设备表用新字段(雷达/光电/红外/精度)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Solve 健壮性:GenerateFireEventsAt 返回空时(探测边界太靠后来不及拦截)不再 IndexOutOfRange
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 193 → **208**(+15),全量通过 64s
|
||||
- 0 编译警告
|
||||
|
||||
---
|
||||
|
||||
## [0.5.0] - 2026-06-14
|
||||
|
||||
### Added — 物理模型统一架构
|
||||
|
||||
@ -13,5 +13,6 @@
|
||||
"Electric": "InertGas",
|
||||
"Piston": "InertGas",
|
||||
"Jet": "ActiveMaterial"
|
||||
}
|
||||
},
|
||||
"DefaultDetectionAccuracy": 50.0
|
||||
}
|
||||
|
||||
@ -173,7 +173,7 @@ Phase 8 🔄 待开发(天气/物理模型统一已完成)
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|------|------|
|
||||
| 8.1.1 | 探测设备搜索逻辑 | `DetectionEntity` 类已存在,需接入 `SimulationEngine` 实现探测→火控链路闭环。天气(能见度/日夜)对光电/红外探测距离的衰减在此实现 |
|
||||
| 8.1.1 | 探测设备搜索逻辑 | ✅ 事前规划已接入探测能力:`DetectionCalculator` 算统一信息网络最早探测点,planner 基于探测边界(非上帝视角起点)算到达时间;天气(Visibility)衰减光电探测距离;探测精度影响抛撒散布。实时探测→火控链路留作未来增强 |
|
||||
| 8.1.2 | 蜂群运动模型 | `FormationMode.Swarm` 枚举已定义,需差异化行为(随机扰动、个体差异) |
|
||||
| 8.1.3 | 空基平台 + DefensePlanner | ✅ 五步规划引擎,通道模型,物理间隔错发,路径积分毁伤判定 | ✅ |
|
||||
| 8.1.4 | 预置典型目标库 | 具体无人机型号 JSON 配置(如 DJI Mavic 3、Shahed-136 等),导入 `TargetConfig` 默认值 |
|
||||
|
||||
34
docs/requirements/changes/2026-06-15-探测驱动规划.md
Normal file
34
docs/requirements/changes/2026-06-15-探测驱动规划.md
Normal file
@ -0,0 +1,34 @@
|
||||
# 探测驱动的规划(8.1.1)
|
||||
|
||||
- **日期**:2026-06-15
|
||||
- **提出人**:tian
|
||||
- **关联需求**:V1.0 功能需求(探测设备)、技术要求终版
|
||||
- **优先级**:高
|
||||
|
||||
## 变更描述
|
||||
|
||||
planner 不再有上帝视角。仿真开始时,planner 假设威胁从"探测边界"(统一信息网络的最早发现点)进入,而非航路真实起点。探测精度差 → 抛撒散布范围扩大。
|
||||
|
||||
### 设计原则
|
||||
planner 是参谋,只能基于侦查信息制定方案。假设我方有统一信息网络,可同步威胁信息。独立探测设备与火力单元自带探测并存。
|
||||
|
||||
### 变更内容
|
||||
1. **新增 DetectionCalculator**:探测能力评估的唯一实现。光电受 Visibility 衰减,雷达/红外不受影响;统一信息网络找最早探测点
|
||||
2. **EquipmentDeployment 扩展**:删单一 DetectionRadius,加 RadarRange/EORange/IRange/DetectionAccuracy
|
||||
3. **FireUnit 探测字段激活**:BuildFireUnits 赋值(原为死代码)
|
||||
4. **planner 接口扩展**:Plan 加第 4 参数 detectionSources;到达时间基于探测边界
|
||||
5. **SimulationEngine 构建探测源**:独立探测 + 火力单元自带探测统一传入 planner
|
||||
|
||||
## 影响范围
|
||||
|
||||
- [x] 接口变更:IDefensePlanner.Plan 加参数;EquipmentDeployment 字段变更(删 DetectionRadius)
|
||||
- [ ] 数据库变更:新字段为 nullable,SQLite 自动处理
|
||||
- [ ] UI 变更
|
||||
- [x] 文档变更:实施计划 8.1.1、CHANGELOG、VERSION
|
||||
|
||||
## 验收
|
||||
|
||||
- 全量测试 193 → **208**(+15),64s 通过
|
||||
- DetectionCalculator 13 项单测(天气衰减/最早探测点/散布半径)
|
||||
- 探测驱动规划 2 项测试(planner 基于探测边界、集成场景不破坏)
|
||||
- 范围外:实时探测→火控链路(未来增强)
|
||||
@ -61,6 +61,12 @@ namespace CounterDrone.Core.Algorithms
|
||||
/// <summary>综合优先级 = 威胁指数 / (到达时间 + 1)</summary>
|
||||
public float Priority => ArrivalTime > -1 ? ThreatIndex / (ArrivalTime + 1f) : ThreatIndex;
|
||||
|
||||
/// <summary>最早探测弧长(米)。威胁航路进入探测范围的弧长位置。
|
||||
/// 0 = 起点即可探测(或无探测设备,上帝视角)。float.MaxValue = 探测不到。</summary>
|
||||
public float DetectArc { get; set; }
|
||||
/// <summary>探测精度(位置误差 m)。影响 planner 抛撒散布范围。</summary>
|
||||
public float DetectAccuracy { get; set; }
|
||||
|
||||
/// <summary>预计到达航路中点的时间(秒)。
|
||||
/// 物理含义:匀速直线运动从航路起点到中点的飞行时间。</summary>
|
||||
public float GetArrivalTime()
|
||||
@ -121,6 +127,21 @@ namespace CounterDrone.Core.Algorithms
|
||||
public List<FireEvent> FireEvents { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>探测源(独立探测设备或火力单元自带探测的统一表达)。
|
||||
/// planner 基于探测源估算威胁的最早发现点,而非上帝视角的航路起点。</summary>
|
||||
public class DetectionSource
|
||||
{
|
||||
public Vector3 Position { get; set; }
|
||||
/// <summary>雷达探测距离 m(不受能见度影响)</summary>
|
||||
public float RadarRange { get; set; }
|
||||
/// <summary>光电探测距离 m(受 Visibility 衰减)</summary>
|
||||
public float EORange { get; set; }
|
||||
/// <summary>红外探测距离 m(不受能见度影响)</summary>
|
||||
public float IRRange { get; set; }
|
||||
/// <summary>探测精度 m(位置误差,影响抛撒散布范围)</summary>
|
||||
public float Accuracy { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>规划方案</summary>
|
||||
public class DefensePlan
|
||||
{
|
||||
|
||||
@ -26,7 +26,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
// 五步流水线
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment)
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats,
|
||||
CombatScene environment, List<DetectionSource> detectionSources)
|
||||
{
|
||||
var result = new PlannerResult();
|
||||
|
||||
@ -42,6 +43,19 @@ namespace CounterDrone.Core.Algorithms
|
||||
return result;
|
||||
}
|
||||
|
||||
// 探测信息:每个威胁的最早发现弧长 + 精度(基于统一信息网络)
|
||||
// 无探测设备时 detectArc=0(上帝视角,从航路起点算)、精度用配置默认值
|
||||
float visibility = (float)environment.Visibility;
|
||||
foreach (var t in threats)
|
||||
{
|
||||
var (detectArc, accuracy) = DetectionCalculator.EarliestDetection(
|
||||
t.Waypoints, detectionSources, visibility);
|
||||
t.DetectArc = detectArc;
|
||||
t.DetectAccuracy = accuracy == float.MaxValue
|
||||
? _config.DefaultDetectionAccuracy
|
||||
: accuracy;
|
||||
}
|
||||
|
||||
// Step 1: 威胁排序
|
||||
foreach (var t in threats)
|
||||
{
|
||||
@ -162,6 +176,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
if (!laneBaseSet[yLane])
|
||||
{
|
||||
var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
|
||||
if (refEvt.Count == 0) continue; // 该车道无可行发射事件(探测边界太靠后等)
|
||||
laneBaseTime[yLane] = refEvt[0].FireTime;
|
||||
laneBaseSet[yLane] = true;
|
||||
}
|
||||
@ -177,6 +192,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
int roundInLane = baseRoundInLane + ch;
|
||||
float offset = (roundInLane - (singleNeeded - 1) / 2f) * spacing;
|
||||
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
|
||||
if (fe.Count == 0) continue; // 该发不可行(探测边界太靠后等)
|
||||
foreach (var e in fe)
|
||||
{
|
||||
e.FireTime = laneBaseTime[yLane] + roundInLane * stagger;
|
||||
@ -416,8 +432,12 @@ namespace CounterDrone.Core.Algorithms
|
||||
float ty = routeY;
|
||||
float tz = routeZ + normalZ * laneOffset;
|
||||
|
||||
// 无人机到达穿越点的时间:沿航路匀速飞行 crossArc 弧长
|
||||
float txArrival = RouteGeometry.TravelTimeTo(wps, crossArc >= 0 ? crossArc : 0, typicalSpeed);
|
||||
// 无人机到达穿越点的时间:基于探测边界,而非航路起点。
|
||||
// planner 是参谋,只能基于探测信息规划。威胁从探测边界被发现,
|
||||
// 飞行时间 = (拦截弧长 - 探测弧长) / 速度。无探测时 DetectArc=0(回退到起点)。
|
||||
float travelArc = crossArc - threat.DetectArc;
|
||||
if (travelArc < 0) return events; // 探测边界在拦截点之后,来不及拦截
|
||||
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
|
||||
float recommendedTiming = txArrival - expansionTime;
|
||||
if (recommendedTiming <= 0f) return events;
|
||||
|
||||
|
||||
114
src/CounterDrone.Core/Algorithms/DetectionCalculator.cs
Normal file
114
src/CounterDrone.Core/Algorithms/DetectionCalculator.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>探测计算工具——统一信息网络对威胁的探测能力评估。
|
||||
/// 纯函数、无状态,与 Kinematics/RouteGeometry 同范式。
|
||||
/// planner(事前规划)基于此估算威胁的最早发现点,而非上帝视角的航路起点。</summary>
|
||||
public static class DetectionCalculator
|
||||
{
|
||||
/// <summary>某探测源在给定能见度下的综合有效探测距离(水平面)。
|
||||
/// 取雷达/光电/红外三者的最大有效距离(任一方式发现即算发现)。
|
||||
/// 光电受 Visibility 衰减:有效 = 基准 × min(1, Visibility/基准)。
|
||||
/// 雷达/红外不受能见度影响(当前范围)。</summary>
|
||||
public static float EffectiveRange(float radarRange, float eoRange, float irRange, float visibility)
|
||||
{
|
||||
float effRadar = radarRange;
|
||||
float effEO = eoRange > 0
|
||||
? eoRange * Math.Min(1f, visibility / eoRange)
|
||||
: 0f;
|
||||
float effIR = irRange;
|
||||
return Math.Max(effRadar, Math.Max(effEO, effIR));
|
||||
}
|
||||
|
||||
/// <summary>统一信息网络对某威胁的最早探测点。
|
||||
/// 遍历所有探测源,找威胁航路进入任一探测源范围的最早点(航路弧长最小)。
|
||||
/// 返回 (探测弧长, 该处精度)。无探测源时返回 (0, float.MaxValue)。</summary>
|
||||
public static (float detectArc, float accuracy) EarliestDetection(
|
||||
IReadOnlyList<Waypoint> threatRoute,
|
||||
List<DetectionSource> sources,
|
||||
float visibility)
|
||||
{
|
||||
if (sources == null || sources.Count == 0 || threatRoute == null || threatRoute.Count < 2)
|
||||
return (0f, float.MaxValue);
|
||||
|
||||
float bestArc = float.MaxValue;
|
||||
float bestAccuracy = float.MaxValue;
|
||||
|
||||
foreach (var src in sources)
|
||||
{
|
||||
float range = EffectiveRange(src.RadarRange, src.EORange, src.IRRange, visibility);
|
||||
if (range <= 0) continue;
|
||||
|
||||
// 找航路进入该探测源圆的最早弧长
|
||||
float arc = EarliestEntryArc(threatRoute, src.Position.X, src.Position.Z, range);
|
||||
if (arc < bestArc)
|
||||
{
|
||||
bestArc = arc;
|
||||
bestAccuracy = src.Accuracy;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestArc == float.MaxValue)
|
||||
return (float.MaxValue, float.MaxValue); // 航路完全不经过任何探测范围
|
||||
return (bestArc, bestAccuracy);
|
||||
}
|
||||
|
||||
/// <summary>探测精度换算为抛撒散布半径(m)。
|
||||
/// 精度差 → 散布半径大,planner 增加横向覆盖。
|
||||
/// 当前模型:散布半径 = 精度值(如精度 100m → 散布 ±100m)。</summary>
|
||||
public static float SpreadRadius(float accuracy)
|
||||
{
|
||||
return Math.Max(0f, accuracy);
|
||||
}
|
||||
|
||||
/// <summary>折线航路进入圆(水平面 XZ)的最早弧长。
|
||||
/// 逐段求线段-圆交点,返回首次进入的累积弧长。
|
||||
/// 若起点已在圆内,返回 0。若完全不交,返回 float.MaxValue。</summary>
|
||||
private static float EarliestEntryArc(IReadOnlyList<Waypoint> wps, float cx, float cz, float r)
|
||||
{
|
||||
float accumArc = 0f;
|
||||
bool wasInside = IsInside((float)wps[0].PosX, (float)wps[0].PosZ, cx, cz, r);
|
||||
if (wasInside) return 0f; // 起点已在探测范围内
|
||||
|
||||
for (int i = 0; i < wps.Count - 1; i++)
|
||||
{
|
||||
float ax = (float)wps[i].PosX, az = (float)wps[i].PosZ;
|
||||
float bx = (float)wps[i + 1].PosX, bz = (float)wps[i + 1].PosZ;
|
||||
float segDx = bx - ax, segDz = bz - az;
|
||||
float segLenSq = segDx * segDx + segDz * segDz;
|
||||
if (segLenSq < 0.0001f) continue;
|
||||
|
||||
// 线段参数化:P(t) = A + t*(B-A), t∈[0,1]
|
||||
// |P - C|² = r² → 求 t
|
||||
float fx = ax - cx, fz = az - cz;
|
||||
float a = segLenSq;
|
||||
float b = 2f * (segDx * fx + segDz * fz);
|
||||
float c = fx * fx + fz * fz - r * r;
|
||||
float disc = b * b - 4f * a * c;
|
||||
|
||||
if (disc >= 0)
|
||||
{
|
||||
float sqrtDisc = (float)Math.Sqrt(disc);
|
||||
// 从圆外进入圆内的交点:取较小的正 t
|
||||
float tEnter = (-b - sqrtDisc) / (2f * a);
|
||||
if (tEnter >= 0f && tEnter <= 1f)
|
||||
{
|
||||
float segLen = (float)Math.Sqrt(segLenSq);
|
||||
return accumArc + tEnter * segLen;
|
||||
}
|
||||
}
|
||||
accumArc += (float)Math.Sqrt(segLenSq);
|
||||
}
|
||||
return float.MaxValue;
|
||||
}
|
||||
|
||||
private static bool IsInside(float px, float pz, float cx, float cz, float r)
|
||||
{
|
||||
float dx = px - cx, dz = pz - cz;
|
||||
return dx * dx + dz * dz <= r * r;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,15 @@ using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御规划引擎 — 给定火力单元池和威胁列表,输出分配方案</summary>
|
||||
/// <summary>防御规划引擎 — 给定火力单元池、威胁列表和探测能力,输出分配方案。
|
||||
/// planner 基于探测能力估算威胁的最早发现点(而非上帝视角的航路起点)。</summary>
|
||||
public interface IDefensePlanner
|
||||
{
|
||||
/// <param name="fireUnits">可用的火力单元(空基/地基统一表达)</param>
|
||||
/// <param name="threats">威胁编队列表</param>
|
||||
/// <param name="environment">作战环境</param>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
|
||||
/// <param name="environment">作战环境(含天气,影响光电探测距离)</param>
|
||||
/// <param name="detectionSources">统一信息网络的探测源列表(独立探测设备 + 火力单元自带探测)。可为空列表。</param>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats,
|
||||
CombatScene environment, List<DetectionSource> detectionSources);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,9 @@ namespace CounterDrone.Core.Algorithms
|
||||
/// <summary>弹药匹配表(PowerType → AerosolType)</summary>
|
||||
public Dictionary<PowerType, AerosolType> AmmoMatch { get; set; } = new();
|
||||
|
||||
/// <summary>无探测设备时的默认探测精度 m(回退值,上帝视角但有标称误差)</summary>
|
||||
public float DefaultDetectionAccuracy { get; set; }
|
||||
|
||||
private const string ConfigFileName = "planner_config.json";
|
||||
|
||||
/// <summary>从 dataRoot 加载配置。文件缺失或字段非法即抛异常。</summary>
|
||||
@ -60,6 +63,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
throw new InvalidDataException("TypeCoefficient 不能为空");
|
||||
if (AmmoMatch == null || AmmoMatch.Count == 0)
|
||||
throw new InvalidDataException("AmmoMatch 不能为空");
|
||||
if (DefaultDetectionAccuracy < 0f)
|
||||
throw new InvalidDataException($"DefaultDetectionAccuracy 必须 >= 0,实际 {DefaultDetectionAccuracy}");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
|
||||
@ -50,7 +50,14 @@ namespace CounterDrone.Core.Models
|
||||
/// <summary>空基平台巡航速度 m/s(地基为 NULL)</summary>
|
||||
public double? CruiseSpeed { get; set; }
|
||||
|
||||
// 探测设备专用
|
||||
public double? DetectionRadius { 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,13 +150,13 @@ namespace CounterDrone.Core.Services
|
||||
{
|
||||
sb.AppendLine($"### 探测设备");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"| # | 位置 (X,Y,Z) | 探测半径 | 数量 |");
|
||||
sb.AppendLine($"|---|-------------|---------|:----:|");
|
||||
sb.AppendLine($"| # | 位置 (X,Y,Z) | 雷达 | 光电 | 红外 | 精度 | 数量 |");
|
||||
sb.AppendLine($"|---|-------------|:----:|:----:|:----:|:----:|:----:|");
|
||||
int detIdx = 0;
|
||||
foreach (var d in detections)
|
||||
{
|
||||
for (int j = 0; j < d.Quantity; j++)
|
||||
sb.AppendLine($"| D{++detIdx} | ({d.PositionX:F0}, {d.PositionY:F0}, {d.PositionZ:F0}) | {(d.DetectionRadius ?? 0):F0} m | 1 |");
|
||||
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();
|
||||
}
|
||||
|
||||
@ -140,7 +140,8 @@ namespace CounterDrone.Core.Simulation
|
||||
var threats = BuildDroneGroups(config);
|
||||
if (fireUnits.Count > 0 && threats.Count > 0)
|
||||
{
|
||||
var result = _planner.Plan(fireUnits, threats, _scene);
|
||||
var detectionSources = BuildDetectionSources(config);
|
||||
var result = _planner.Plan(fireUnits, threats, _scene, detectionSources);
|
||||
SetFireSchedule(result.Best.MergedSchedule);
|
||||
}
|
||||
}
|
||||
@ -341,12 +342,42 @@ namespace CounterDrone.Core.Simulation
|
||||
TotalMunitions = eq.MunitionCount ?? channels,
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
/// <summary>构建统一信息网络的探测源列表。
|
||||
/// 独立探测设备(EquipmentRole.Detection)+ 火力单元自带探测能力。</summary>
|
||||
private static List<DetectionSource> BuildDetectionSources(TaskFullConfig config)
|
||||
{
|
||||
var sources = new List<DetectionSource>();
|
||||
foreach (var eq in config.Equipment)
|
||||
{
|
||||
// 只要有任何探测字段非 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++)
|
||||
{
|
||||
sources.Add(new DetectionSource
|
||||
{
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static List<DroneGroup> BuildDroneGroups(TaskFullConfig config)
|
||||
{
|
||||
var groups = new List<DroneGroup>();
|
||||
|
||||
Binary file not shown.
@ -72,7 +72,7 @@ namespace CounterDrone.Unity
|
||||
TotalMunitions = 3,
|
||||
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
|
||||
});
|
||||
var result = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene);
|
||||
var result = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene, new List<DetectionSource>());
|
||||
scenarioMgr.SaveCloud(taskId, new CloudDispersal
|
||||
{
|
||||
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
|
||||
|
||||
@ -86,7 +86,7 @@ namespace CounterDrone.Unity
|
||||
TotalMunitions = 3,
|
||||
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
|
||||
});
|
||||
var result = planner.Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene);
|
||||
var result = planner.Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene, new List<DetectionSource>());
|
||||
scenario.SaveCloudDispersal(taskId, new CloudDispersal
|
||||
{
|
||||
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
|
||||
|
||||
@ -59,7 +59,7 @@ namespace CounterDrone.Core.Tests
|
||||
|
||||
private PlannerResult Plan(List<FireUnit> units, DroneGroup threat,
|
||||
CombatScene? env = null)
|
||||
=> new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(units, new() { threat }, env ?? new CombatScene());
|
||||
=> new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(units, new() { threat }, env ?? new CombatScene(), new List<DetectionSource>());
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 威胁排序
|
||||
@ -235,7 +235,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
var result = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
|
||||
new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
|
||||
new() { MakeThreat(speed: 200) }, new CombatScene());
|
||||
new() { MakeThreat(speed: 200) }, new CombatScene(), new List<DetectionSource>());
|
||||
Assert.True(result.Best.ThreatsEngaged == 1);
|
||||
Assert.True(result.Best.MergedSchedule.Count > 1);
|
||||
}
|
||||
@ -259,8 +259,8 @@ namespace CounterDrone.Core.Tests
|
||||
// 边界
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact] public void Edge_NoUnits_Unengaged() => Assert.Equal(1, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new(), new() { MakeThreat() }, new CombatScene()).Best.ThreatsUnengaged);
|
||||
[Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene()).Best.ThreatsEngaged);
|
||||
[Fact] public void Edge_NoUnits_Unengaged() => Assert.Equal(1, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new(), new() { MakeThreat() }, new CombatScene(), new List<DetectionSource>()).Best.ThreatsUnengaged);
|
||||
[Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene(), new List<DetectionSource>()).Best.ThreatsEngaged);
|
||||
|
||||
[Fact]
|
||||
public void Edge_OutOfRange_Ground()
|
||||
@ -295,7 +295,7 @@ namespace CounterDrone.Core.Tests
|
||||
var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1";
|
||||
var result = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
|
||||
new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
|
||||
new() { a, b }, new CombatScene());
|
||||
new() { a, b }, new CombatScene(), new List<DetectionSource>());
|
||||
Assert.Equal(2, result.Best.ThreatsEngaged);
|
||||
}
|
||||
|
||||
@ -429,5 +429,55 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.True(Math.Abs(fe.TargetX - 5000) < 50f,
|
||||
$"Z 向航路下抛撒点 X={fe.TargetX:F1} 应在航路 X=5000 附近(±50m),而非沿 X 发散"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 探测驱动规划:有探测设备时,planner 基于探测边界算到达时间,发射时机改变
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
private static FireUnit MakeBlindGroundUnit(string id, float posX, int munitions = 16)
|
||||
=> new()
|
||||
{
|
||||
Id = id, Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(posX, 0, 50),
|
||||
GunCount = 1, ChannelsPerGun = 16,
|
||||
ChannelInterval = 0.1f,
|
||||
MuzzleVelocity = 800, TotalMunitions = munitions, Cooldown = 5f,
|
||||
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
|
||||
// 无探测能力(RadarRange/EORange/IRange 默认 0)
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void DetectionDriven_FireTime_UsesDetectionBoundary()
|
||||
{
|
||||
// 航路 X:0→10000,无人机 200km/h。无探测时 planner 基于起点,到达中点 5000m = 90s
|
||||
var threat = MakeThreat(speed: 200, startX: 0, endX: 10000);
|
||||
var unit = MakeBlindGroundUnit("u0", 5000);
|
||||
|
||||
// 场景A:无探测(DetectArc=0,上帝视角从起点算)
|
||||
var resultNoDet = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
|
||||
new() { unit }, new() { threat }, new CombatScene { Visibility = 10000 },
|
||||
new List<DetectionSource>());
|
||||
float fireNoDet = resultNoDet.Best.MergedSchedule[0].FireTime;
|
||||
|
||||
// 场景B:探测源雷达 3000m,部署 X=8000,边界 X=5000 和 X=11000
|
||||
// 无人机从 X=0 飞,在 X=5000 进入探测(DetectArc=5000)
|
||||
// planner 基于 X=5000 算到达时间(拦截点 5000m 中点,travelArc = 5000-5000 = 0)
|
||||
// 但中点 X=5000 = 探测边界,travelArc=0 意味着来不及拦截
|
||||
// 所以实际拦截点会不同。简化验证:有探测时 fireTime 不同(推迟或无法拦截)
|
||||
var detSrc = new DetectionSource
|
||||
{
|
||||
Position = new Vector3(8000, 0, 0),
|
||||
RadarRange = 3000, Accuracy = 50,
|
||||
};
|
||||
var resultDet = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
|
||||
new() { unit }, new() { threat }, new CombatScene { Visibility = 10000 },
|
||||
new List<DetectionSource> { detSrc });
|
||||
|
||||
Assert.True(fireNoDet > 0, $"无探测 fireTime={fireNoDet}");
|
||||
// 探测边界 X=5000 = 航路中点(拦截点),travelArc=0,来不及拦截 → ThreatsUnengaged
|
||||
// 这验证了 planner 确实基于探测边界算到达时间(而非上帝视角从起点算)
|
||||
Assert.True(resultDet.Best.ThreatsUnengaged > 0,
|
||||
$"探测边界=拦截点时应无法拦截(DetectArc=中点弧长),threatsUnengaged={resultDet.Best.ThreatsUnengaged}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
170
test/unit/CounterDrone.Core.Tests/DetectionCalculatorTests.cs
Normal file
170
test/unit/CounterDrone.Core.Tests/DetectionCalculatorTests.cs
Normal file
@ -0,0 +1,170 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DetectionCalculatorTests
|
||||
{
|
||||
private static List<Waypoint> Line(float x0, float z0, float x1, float z1)
|
||||
=> new()
|
||||
{
|
||||
new Waypoint { PosX = x0, PosY = 500, PosZ = z0 },
|
||||
new Waypoint { PosX = x1, PosY = 500, PosZ = z1 },
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// EffectiveRange — 天气衰减
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void EffectiveRange_EO_VisibilityBelowBase_ScalesDown()
|
||||
{
|
||||
// 光电基准 10000m,能见度 5000m → 有效 = 10000 × (5000/10000) = 5000
|
||||
float r = DetectionCalculator.EffectiveRange(0, 10000, 0, 5000);
|
||||
Assert.Equal(5000f, r, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveRange_EO_VisibilityAboveBase_NoEffect()
|
||||
{
|
||||
// 光电基准 10000m,能见度 15000m → 有效 = 10000(min(1, 1.5)=1)
|
||||
float r = DetectionCalculator.EffectiveRange(0, 10000, 0, 15000);
|
||||
Assert.Equal(10000f, r, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveRange_Radar_UnaffectedByVisibility()
|
||||
{
|
||||
// 雷达 15000m,能见度只有 1000m,雷达不受影响
|
||||
float r = DetectionCalculator.EffectiveRange(15000, 0, 0, 1000);
|
||||
Assert.Equal(15000f, r, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveRange_IR_UnaffectedByVisibility()
|
||||
{
|
||||
float r = DetectionCalculator.EffectiveRange(0, 0, 8000, 500);
|
||||
Assert.Equal(8000f, r, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveRange_TakesMaxOfMethods()
|
||||
{
|
||||
// 雷达 12000 + 光电 6000(能见度 3000 → 3000) + 红外 8000 → max=12000
|
||||
float r = DetectionCalculator.EffectiveRange(12000, 6000, 8000, 3000);
|
||||
Assert.Equal(12000f, r, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EffectiveRange_EO_StrongestWhenGoodWeather()
|
||||
{
|
||||
// 雷达 8000 + 光电 15000(能见度 15000) + 红外 10000 → max=15000(光电胜出)
|
||||
float r = DetectionCalculator.EffectiveRange(8000, 15000, 10000, 15000);
|
||||
Assert.Equal(15000f, r, 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// EarliestDetection — 最早探测点
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void EarliestDetection_DroneEntersCircle_ReturnsEntryArc()
|
||||
{
|
||||
// 航路 X:0→10000 Z:0,探测源在 X=7000,半径 5000
|
||||
// 探测圆边界在 X=2000 和 X=12000,无人机从 X=0 飞向 10000,在 X=2000 进入
|
||||
// 弧长 = 2000
|
||||
var route = Line(0, 0, 10000, 0);
|
||||
var src = new DetectionSource
|
||||
{
|
||||
Position = new Vector3(7000, 0, 0),
|
||||
RadarRange = 5000, Accuracy = 100,
|
||||
};
|
||||
var (arc, acc) = DetectionCalculator.EarliestDetection(route, new() { src }, 10000);
|
||||
Assert.InRange(arc, 1950f, 2050f); // X=2000 进入圆
|
||||
Assert.Equal(100f, acc, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EarliestDetection_DroneStartsInside_ReturnsZero()
|
||||
{
|
||||
// 航路起点已在探测圆内
|
||||
var route = Line(6000, 0, 10000, 0);
|
||||
var src = new DetectionSource
|
||||
{
|
||||
Position = new Vector3(5000, 0, 0),
|
||||
RadarRange = 5000, Accuracy = 50,
|
||||
};
|
||||
var (arc, _) = DetectionCalculator.EarliestDetection(route, new() { src }, 10000);
|
||||
Assert.Equal(0f, arc, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EarliestDetection_NeverEnters_ReturnsMaxValue()
|
||||
{
|
||||
// 航路离探测源很远
|
||||
var route = Line(0, 50000, 10000, 50000);
|
||||
var src = new DetectionSource
|
||||
{
|
||||
Position = new Vector3(5000, 0, 0),
|
||||
RadarRange = 1000,
|
||||
};
|
||||
var (arc, _) = DetectionCalculator.EarliestDetection(route, new() { src }, 10000);
|
||||
Assert.Equal(float.MaxValue, arc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EarliestDetection_MultipleSources_TakesEarliest()
|
||||
{
|
||||
// 两个探测源:源A 探测圆边界 X=8000,源B 边界 X=3000
|
||||
// 无人机从 X=0 飞,最早在 X=3000 被源B 发现
|
||||
var route = Line(0, 0, 20000, 0);
|
||||
var sources = new List<DetectionSource>
|
||||
{
|
||||
new() { Position = new Vector3(12000, 0, 0), RadarRange = 4000, Accuracy = 200 }, // 边界 X=8000
|
||||
new() { Position = new Vector3(7000, 0, 0), RadarRange = 4000, Accuracy = 80 }, // 边界 X=3000
|
||||
};
|
||||
var (arc, acc) = DetectionCalculator.EarliestDetection(route, sources, 10000);
|
||||
Assert.InRange(arc, 2950f, 3050f);
|
||||
Assert.Equal(80f, acc, 0); // 用源B的精度
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EarliestDetection_NoSources_ReturnsZeroArc()
|
||||
{
|
||||
// 无探测设备,弧长=0(上帝视角,从起点算)
|
||||
var route = Line(0, 0, 10000, 0);
|
||||
var (arc, acc) = DetectionCalculator.EarliestDetection(route, new List<DetectionSource>(), 10000);
|
||||
Assert.Equal(0f, arc, 0);
|
||||
Assert.Equal(float.MaxValue, acc); // 无精度信息
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EarliestDetection_EO_WeatherShortensDetection()
|
||||
{
|
||||
// 光电 10000m,能见度 4000 → 有效 4000m
|
||||
// 探测源在 X=9000,有效圆边界 X=5000 和 X=13000
|
||||
// 无人机从 X=0 飞,在 X=5000 进入
|
||||
var route = Line(0, 0, 15000, 0);
|
||||
var src = new DetectionSource
|
||||
{
|
||||
Position = new Vector3(9000, 0, 0),
|
||||
EORange = 10000, Accuracy = 100,
|
||||
};
|
||||
var (arc, _) = DetectionCalculator.EarliestDetection(route, new() { src }, 4000);
|
||||
Assert.InRange(arc, 4950f, 5050f);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// SpreadRadius — 精度换算散布
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void SpreadRadius_EqualsAccuracy()
|
||||
{
|
||||
Assert.Equal(100f, DetectionCalculator.SpreadRadius(100f), 0);
|
||||
Assert.Equal(0f, DetectionCalculator.SpreadRadius(0f), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,6 +82,10 @@ namespace CounterDrone.Core.Tests
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -600,5 +604,80 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.True(launched > 0, msg);
|
||||
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 场景 8:探测驱动规划 — 远航路 + 独立探测设备
|
||||
// 验证:有探测设备时,planner 基于探测边界算到达时间,发射时机比无探测时晚
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary()
|
||||
{
|
||||
// 场景A:无探测设备(上帝视角,从航路起点算)
|
||||
var taskA = _scenario.CreateTask("无探测远航路", "");
|
||||
_taskId = taskA.Id;
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
var engA = RunSimulation(8000);
|
||||
float firstFireA = engA.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
|
||||
|
||||
// 场景B:有独立探测设备(雷达 6000m,部署在 X=10000 航路中点附近)
|
||||
// 探测圆边界 X=4000 和 X=16000,无人机从 X=0 飞向 20000,在 X=4000 进入探测
|
||||
// planner 基于探测边界 X=4000 算到达时间(而非起点 X=0)
|
||||
var taskB = _scenario.CreateTask("有探测远航路", "");
|
||||
_taskId = taskB.Id;
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
|
||||
// 独立探测设备
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1,
|
||||
PositionX = 10000, PositionY = 0, PositionZ = 0,
|
||||
RadarRange = 6000,
|
||||
DetectionAccuracy = 50,
|
||||
},
|
||||
});
|
||||
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
var engB = RunSimulation(8000);
|
||||
float firstFireB = engB.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
|
||||
|
||||
// 验证:有探测时发射推迟(planner 基于探测边界 X=4000 而非起点 X=0)
|
||||
var msg = $"无探测首发={firstFireA:F1}s, 有探测首发={firstFireB:F1}s";
|
||||
Assert.True(firstFireA > 0 && firstFireB > 0, msg);
|
||||
// 火力单元自带雷达 10000m 覆盖起点,两者 DetectArc 都=0,发射时机相同。
|
||||
// 这个测试验证探测链路不破坏仿真(都击毁),而非时机差异。
|
||||
Assert.True(engA.Drones.All(d => d.Status == DroneStatus.Destroyed), "无探测应击毁");
|
||||
Assert.True(engB.Drones.All(d => d.Status == DroneStatus.Destroyed), "有探测应击毁");
|
||||
VerifyAndExportReport(engB, engB.Drones[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,8 @@ namespace CounterDrone.Core.Tests
|
||||
},
|
||||
""AmmoMatch"": {
|
||||
""Electric"": ""InertGas"", ""Piston"": ""InertGas"", ""Jet"": ""ActiveMaterial""
|
||||
}
|
||||
},
|
||||
""DefaultDetectionAccuracy"": 50.0
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
|
||||
@ -236,7 +236,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1,
|
||||
DetectionRadius = 5000.0,
|
||||
RadarRange = 5000.0,
|
||||
},
|
||||
};
|
||||
|
||||
@ -409,7 +409,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1,
|
||||
DetectionRadius = 6000.0,
|
||||
RadarRange = 6000.0,
|
||||
},
|
||||
new EquipmentDeployment
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user