Compare commits
3 Commits
3028a421b5
...
56d345189e
| Author | SHA1 | Date | |
|---|---|---|---|
| 56d345189e | |||
| 665747a846 | |||
| 11f8cb2c79 |
70
CHANGELOG.md
70
CHANGELOG.md
@ -2,6 +2,76 @@
|
||||
|
||||
---
|
||||
|
||||
## [0.7.0] - 2026-06-15
|
||||
|
||||
### Breaking — 概念升级:编组拆分 + Group 表移除
|
||||
|
||||
- **Group 表移除**:原 `Group` 表(DroneFleet / EquipmentGroup)不再创建。向后兼容保留旧库中的 Group 表,但不再主动读写
|
||||
- **GroupType 枚举删除**:不再区分 DroneFleet / EquipmentGroup
|
||||
- **GroupService / IGroupService / GroupRepository 删除**:不再需要编组管理服务
|
||||
- **Unity GroupManager 删除**:不再需要编组管理桥接
|
||||
|
||||
### Changed — 数据模型重命名
|
||||
|
||||
- **`GroupId` → `WaveId`**:TargetConfig、EquipmentDeployment、RoutePlan、Waypoint 四表的编组外键重命名为批次外键
|
||||
- **RoutePlan 索引重命名**:`(TaskId, GroupId)` → `(TaskId, WaveId)`
|
||||
- **RoutePlanRepository / WaypointRepository**:`GetByTaskAndGroup` → `GetByTaskAndWave`
|
||||
- **IScenarioService.SaveRoute**:`groupId` 参数 → `waveId`
|
||||
|
||||
### Changed — 算法类型重命名
|
||||
|
||||
- **`DroneGroup` → `DroneWave`**:AlgorithmTypes、IDefensePlanner、DefensePlanner、SimulationEngine 全部使用新类型名
|
||||
- **`DroneGroupId` → `DroneWaveId`**:UnitAssignment 中的编队引用 → 批次引用
|
||||
- **`BuildDroneGroups()` → `BuildDroneWaves()`**:SimulationEngine 构建方法重命名
|
||||
|
||||
### Changed — 文档同步
|
||||
|
||||
- 总体架构设计:编组概念升级说明
|
||||
- DefensePlanner 技术方案:DroneGroup → DroneWave
|
||||
- 实施计划:GroupService 移除、多编队 → 多批次
|
||||
- Unity 前端对接文档:DefaultDefenseAdvisor → DefensePlanner,SaveRoute 签名更新
|
||||
- 仿真器实体事件映射:BuildDroneGroups → BuildDroneWaves
|
||||
|
||||
### Removed
|
||||
|
||||
- 7 个文件:Group.cs / GroupRepository.cs / IGroupService.cs / GroupService.cs / GroupManager.cs / GroupServiceTests.cs / GroupRepositoryTests.cs
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 208 → **204**(−4,删除 Group 相关测试),全量通过 63s
|
||||
- 0 编译错误,0 编译警告
|
||||
|
||||
---
|
||||
|
||||
## [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
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# 反无人机仿真系统 — 总体架构设计
|
||||
|
||||
> **版本**:V9
|
||||
> **日期**:2026-06-13
|
||||
> **版本**:V10
|
||||
> **日期**:2026-06-15
|
||||
> **状态**:已实现
|
||||
> **Unity 版本**:22.3.62
|
||||
> **.NET 版本**:.NET Standard 2.1
|
||||
> **变更**:DefensePlanner 五步规划引擎;FireUnit 通道模型;空基平台飞控;SQLite domain reload 修复;删除所有 fallback 默认值
|
||||
> **变更**:Group 表移除;GroupId → WaveId;DroneGroup → DroneWave;编组概念升级为批次+火力单元
|
||||
|
||||
---
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
| 模块 | 核心功能 |
|
||||
|------|----------|
|
||||
| **模型管理** | 3D 模型导入/预览/删除,编组管理 |
|
||||
| **模型管理** | 3D 模型导入/预览/删除 |
|
||||
| **想定管理** | 仿真任务 CRUD,步骤化配置向导,管控区域设置,搜索分页 |
|
||||
| **仿真报告** | 报告列表,含时序图的完整报告预览,导出 PDF/Word,删除 |
|
||||
|
||||
@ -41,12 +41,12 @@
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ Unity Application Layer │
|
||||
│ ModelPanel | ScenarioWizard | SimulationRunner | │
|
||||
│ ReplayController | ReportPanel | GroupManager │
|
||||
│ ReplayController | ReportPanel │
|
||||
├──────────────────────────────────────────────────────────┤
|
||||
│ Service Layer (Pure C#) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ ModelService │ │ScenarioService│ │ SimulationEngine │ │
|
||||
│ │ GroupService │ │ DefensePlanner │ │ ├ DroneEntity │ │
|
||||
│ │ │ │ DefensePlanner │ │ ├ DroneEntity │ │
|
||||
│ └──────────────┘ └──────────────┘ │ ├ PlatformEntity │ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │ ├ MunitionEntity │ │
|
||||
│ │FrameDataStore│ │ReportService │ │ ├ CloudEntity │ │
|
||||
@ -199,9 +199,19 @@ enum TriggerMode { Time = 0, Area = 1, Manual = 2 }
|
||||
// === 毁伤状态阶段(输出到 StateData 供 Unity 渲染)===
|
||||
enum DamageStage { Normal = 0, EngineAnomaly = 1, AttitudeLoss = 2, Destroyed = 3 }
|
||||
|
||||
// === 编队 & 编组 ===
|
||||
// === 编队 ===
|
||||
enum FormationMode { Single = 0, Formation = 1, Swarm = 2 }
|
||||
enum GroupType { DroneFleet = 0, EquipmentGroup = 1 }
|
||||
|
||||
// === 核心作战概念 ===
|
||||
// 火力单元(FireUnit):能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。
|
||||
// 包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环系统。
|
||||
// 配置层 = EquipmentDeployment(LaunchPlatform),含探测字段 + 打击字段。
|
||||
// 独立探测节点(DetectionNode):只探测不打击的侦查节点(如前沿警戒雷达)。
|
||||
// 配置层 = EquipmentDeployment(Detection),仅含探测字段。
|
||||
// 两者都往统一信息网络送探测信息,planner 基于融合后的探测边界规划。
|
||||
// 无人机批次(DroneWave):有共同航路的一组无人机(攻击方)。
|
||||
// 批次关联航路但不独占(多个批次可共享同一条航路)。
|
||||
// 配置层 = TargetConfig(含 WaveId) + RoutePlan(TaskId, WaveId) + Waypoints。
|
||||
|
||||
// === 运行时 ===
|
||||
enum EntityType { Drone = 0, Platform = 1, DetectionEquip = 2, Cloud = 3, Munition = 4 }
|
||||
@ -254,12 +264,11 @@ CounterDroneBackend_Data/
|
||||
SimTask — 仿真任务主表
|
||||
CombatScene — 步骤1:作战场景(含24h时间、天气)
|
||||
ControlZone — 步骤1+:管控区域/电子围栏(NEW)
|
||||
TargetConfig — 步骤2:目标配置
|
||||
EquipmentDeployment — 步骤3:装备部署(搭载平台+探测设备)
|
||||
TargetConfig — 步骤2:目标配置(含 WaveId 关联批次)
|
||||
EquipmentDeployment — 步骤3:装备部署(火力单元含探测+打击,或独立探测节点)
|
||||
CloudDispersal — 步骤4:云团抛撒配置
|
||||
RoutePlan — 步骤5:航路规划(1:N SimTask,多编队)
|
||||
RoutePlan — 步骤5:航路规划(1:N SimTask,多批次;批次关联但不独占航路,FK 关联 TargetConfig.WaveId)
|
||||
Waypoint — 航路点
|
||||
Group — 编组(独立管理)
|
||||
SimulationReport — 仿真报告
|
||||
SimEvent — 仿真事件(时序图数据源)
|
||||
|
||||
@ -267,6 +276,12 @@ CounterDroneBackend_Data/
|
||||
SimFrameRecord — 逐帧位置记录(回放用)
|
||||
```
|
||||
|
||||
> **核心作战概念**:
|
||||
> - **火力单元(FireUnit)**:能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环。配置层 = `EquipmentDeployment(LaunchPlatform)`。
|
||||
> - **独立探测节点(DetectionNode)**:只探测不打击的侦查节点(如前沿警戒雷达)。配置层 = `EquipmentDeployment(Detection)`。
|
||||
> - **无人机批次(DroneWave)**:有共同航路的一组无人机(攻击方)。批次关联航路但不独占(多个批次可共享同一条航路)。配置层 = `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints`。
|
||||
> - 原来的 `Group`(编组)表已移除:无人机编队概念升级为"批次",装备编组概念不再需要(火力单元是独立作战单位)。
|
||||
|
||||
### 4.3 业务表详细设计
|
||||
|
||||
#### ModelInfo — 模型元数据
|
||||
@ -337,7 +352,7 @@ CounterDroneBackend_Data/
|
||||
|------|------|------|------|
|
||||
| Id | TEXT | PK | GUID |
|
||||
| TaskId | TEXT | FK | |
|
||||
| GroupId | TEXT | FK | 关联无人机编队 Group |
|
||||
| WaveId | TEXT | FK | 关联无人机批次(同一 WaveId 的无人机共享航路) |
|
||||
| TargetType | INTEGER | | 0旋翼 1固定翼 2电推 3活塞 4高速 |
|
||||
| Quantity | INTEGER | DEFAULT 1 | 目标数量 |
|
||||
| PowerType | INTEGER | | 0电推 1活塞 2喷吸气 |
|
||||
@ -355,28 +370,31 @@ CounterDroneBackend_Data/
|
||||
| 活塞 | 2.5 | 120 | 800 |
|
||||
| 高速目标 | 1.5 | 300 | 2000 |
|
||||
|
||||
#### EquipmentDeployment — 步骤3:装备部署(重构)
|
||||
#### EquipmentDeployment — 步骤3:装备部署(火力单元 + 独立探测节点)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| Id | TEXT | PK | GUID |
|
||||
| TaskId | TEXT | FK | |
|
||||
| EquipmentRole | INTEGER | | 0=探测设备 1=发射平台 |
|
||||
| EquipmentRole | INTEGER | | 0=独立探测节点 1=火力单元(发射平台) |
|
||||
| Quantity | INTEGER | DEFAULT 1 | 部署数量 |
|
||||
| GroupId | TEXT | FK | 关联装备编组 Group(火力单元,多批次时用于分配) |
|
||||
| // 以下为发射平台专用字段 |
|
||||
| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹),探测设备为 NULL |
|
||||
| WaveId | TEXT | NULLABLE | 可选关联批次(用于 UI 分组,火力单元是独立作战单位,不强依赖批次) |
|
||||
| // 以下为火力单元(发射平台)专用字段 |
|
||||
| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹),独立探测节点为 NULL |
|
||||
| PositionX | REAL | | 部署位置 |
|
||||
| PositionY | REAL | | |
|
||||
| PositionZ | REAL | | |
|
||||
| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,探测设备为 NULL |
|
||||
| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,独立探测节点为 NULL |
|
||||
| MunitionCount | INTEGER | NULLABLE | 挂载弹药数量 |
|
||||
| MuzzleVelocity | REAL | NULLABLE | **NEW** 弹药初速 m/s(地基炮弹用) |
|
||||
| ReleaseAltitude | REAL | NULLABLE | **NEW** 弹药释放高度 m(空基平台用) |
|
||||
| Cooldown | REAL | DEFAULT 5 | 发射冷却时间 s(同弹种连发间隔) |
|
||||
| AmmoChangeTime | REAL | DEFAULT 300 | **NEW** 更换弹种所需时间 s(默认 5 分钟) |
|
||||
| // 以下为探测设备专用字段 |
|
||||
| DetectionRadius | REAL | NULLABLE | 探测半径 m |
|
||||
| // 探测能力(火力单元自带 + 独立探测节点均有) |
|
||||
| RadarRange | REAL | NULLABLE | 雷达探测距离 m(雨雾不衰减) |
|
||||
| EORange | REAL | NULLABLE | 光电探测距离 m(受 Visibility 衰减) |
|
||||
| IRRange | REAL | NULLABLE | 红外探测距离 m(不受 Visibility 影响) |
|
||||
| DetectionAccuracy | REAL | NULLABLE | 探测精度 m(位置误差,影响抛撒散布范围) |
|
||||
|
||||
> **装备体系说明**:
|
||||
> - **搭载平台**(2 种):
|
||||
@ -415,12 +433,12 @@ CounterDroneBackend_Data/
|
||||
|
||||
#### RoutePlan — 步骤5:航路规划
|
||||
|
||||
> **支持多批次**:通过 `TaskId + GroupId` 唯一标识一条航路,一个任务可有多个编队各自独立航路。
|
||||
> **支持多批次**:通过 `TaskId + WaveId` 唯一标识一条航路,一个任务可有多个批次各自独立航路。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| TaskId | TEXT | PK(复合) | |
|
||||
| GroupId | TEXT | PK(复合), FK → Group | 关联编队:同一任务不同编队可有独立航路 |
|
||||
| WaveId | TEXT | PK(复合) | 关联批次:同一任务不同批次可有独立航路(多个批次可共享同一条航路) |
|
||||
| FormationMode | INTEGER | | 0单机 1编队 2蜂群 |
|
||||
| FormationSpacing | REAL | DEFAULT 50 | m |
|
||||
| ETA | TEXT | | 预计到达时间 |
|
||||
@ -438,15 +456,13 @@ CounterDroneBackend_Data/
|
||||
| Altitude | REAL | | 该航点飞行高度 m |
|
||||
| Speed | REAL | | 该段速度 km/h |
|
||||
|
||||
#### Group — 编组
|
||||
#### ~~Group~~ — 编组(已移除)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| Id | TEXT | PK | GUID |
|
||||
| Name | TEXT | NOT NULL | 编组名称 |
|
||||
| GroupType | INTEGER | | 0=无人机编队 1=装备编组 |
|
||||
| Description | TEXT | | 备注 |
|
||||
| CreatedAt | TEXT | | |
|
||||
> **概念升级**:原 Group 表的两种类型已分别升级:
|
||||
> - `DroneFleet`(无人机编队)→ **无人机批次(DroneWave)**:由 `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints` 隐式表达。批次关联航路但不独占(多个批次可共享同一条航路)。
|
||||
> - `EquipmentGroup`(装备编组)→ **不再需要**。火力单元是独立作战的最小单位,不需要上层编组。
|
||||
>
|
||||
> 数据层:`Group` 表保留但不再主动使用(向后兼容)。`TargetConfig.WaveId` 语义为"批次 ID"。
|
||||
|
||||
### 4.4 运行时表
|
||||
|
||||
@ -507,7 +523,7 @@ SimulationEngine
|
||||
├── SimulationState ← 状态(运行/暂停/结束)
|
||||
├── SceneConfig ← 配置快照(含场景边界、管控区域)
|
||||
├── List<DroneEntity> ← 无人机(目标方)
|
||||
│ └── 按编组组织,共享航路
|
||||
│ └── 按批次组织,共享航路
|
||||
├── List<PlatformEntity> ← 搭载平台(发射方)★重构
|
||||
│ ├── AirBasedPlatform ← 空基(大型无人机)
|
||||
│ └── GroundBasedPlatform ← 地基(火炮)
|
||||
@ -613,7 +629,7 @@ Unity 渲染毁伤效果(尾焰异常、姿态失控、坠落)
|
||||
|
||||
```
|
||||
属性:
|
||||
- 编组 GroupId、航路点队列 Waypoint[]
|
||||
- 批次 WaveId、航路点队列 Waypoint[]
|
||||
- 当前位置、速度、朝向
|
||||
- HP(0~1 归一化)
|
||||
- DamageStage(Normal → EngineAnomaly → AttitudeLoss → Destroyed)
|
||||
@ -1080,7 +1096,7 @@ planner 所有策略参数从 `data/planner_config.json` 读取,代码零默
|
||||
// === 防御规划 ===
|
||||
public interface IDefensePlanner
|
||||
{
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene environment);
|
||||
}
|
||||
|
||||
// ThreatProfile / DefenseRecommendation / DefenseSolution 等模型见本节上文
|
||||
@ -1265,7 +1281,7 @@ public interface IFrameDataStore
|
||||
|
||||
| 阶段 | 内容 | 数据来源 |
|
||||
|------|------|------|
|
||||
| **仿真前** | 我方配置(装备类型/数量/编组、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
|
||||
| **仿真前** | 我方配置(装备类型/数量、火力单元部署、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
|
||||
| **仿真中** | 关键事件时序表、各实体轨迹概要、云团演化数据 | SimEvent + SimFrameRecord |
|
||||
| **仿真后** | 拦截成功率、各阶段耗时、毁伤效果评估、对抗结果判定 | SimEvent 汇总统计 |
|
||||
|
||||
@ -1297,14 +1313,6 @@ interface IModelService
|
||||
ModelInfo GetModel(string id);
|
||||
}
|
||||
|
||||
// === 编组管理 ===
|
||||
interface IGroupService
|
||||
{
|
||||
Group CreateGroup(string name, GroupType type, string description);
|
||||
void DeleteGroup(string id);
|
||||
List<Group> GetGroups(GroupType? type);
|
||||
}
|
||||
|
||||
// === 想定管理 ===
|
||||
interface IScenarioService
|
||||
{
|
||||
@ -1366,7 +1374,7 @@ interface IReportService
|
||||
|
||||
### 数据模型
|
||||
|
||||
一个仿真任务可包含**多种类型、多批次**的无人机,每批次(编队)有独立的航路:
|
||||
一个仿真任务可包含**多种类型、多批次**的无人机,每批次有独立的航路:
|
||||
|
||||
```
|
||||
SimTask "城市防御演习"
|
||||
@ -1374,51 +1382,51 @@ SimTask "城市防御演习"
|
||||
├── ControlZone[](共享)
|
||||
├── CloudDispersal(共享:云团参数,具体拦截方案由算法确定)
|
||||
│
|
||||
├── 编队 A(活塞×3) ← Group.Id
|
||||
│ ├── TargetConfig(类型=活塞,数量=3)
|
||||
│ ├── RoutePlan(航路 A:北→南)
|
||||
├── 批次 A(活塞×3) ← WaveId
|
||||
│ ├── TargetConfig(WaveId=A,类型=活塞,数量=3)
|
||||
│ ├── RoutePlan(TaskId + WaveId=A,航路 A:北→南)
|
||||
│ └── Waypoint[]
|
||||
│
|
||||
├── 编队 B(喷气×2) ← Group.Id
|
||||
│ ├── TargetConfig(类型=高速,数量=2)
|
||||
│ ├── RoutePlan(航路 B:西→东)
|
||||
├── 批次 B(喷气×2) ← WaveId
|
||||
│ ├── TargetConfig(WaveId=B,类型=高速,数量=2)
|
||||
│ ├── RoutePlan(TaskId + WaveId=B,航路 B:西→东)
|
||||
│ └── Waypoint[]
|
||||
│
|
||||
└── 编队 C(旋翼×5) ← Group.Id
|
||||
├── TargetConfig(类型=旋翼,数量=5)
|
||||
├── RoutePlan(航路 C)
|
||||
└── 批次 C(旋翼×5) ← WaveId
|
||||
├── TargetConfig(WaveId=C,类型=旋翼,数量=5)
|
||||
├── RoutePlan(TaskId + WaveId=C,航路 C)
|
||||
└── Waypoint[]
|
||||
```
|
||||
|
||||
**关键变更**:`RoutePlan` 主键从 `TaskId` 改为 `(TaskId, GroupId)` 复合键。
|
||||
**关键变更**:`RoutePlan` 主键为 `(TaskId, WaveId)` 复合键。`Waypoint` 通过 `(TaskId, WaveId)` 关联航路。
|
||||
|
||||
### 推荐算法
|
||||
|
||||
算法需要同时考虑**所有无人机编队**和**可用装备火力单元**,自动匹配分配:
|
||||
算法需要同时考虑**所有无人机批次**和**可用火力单元**,自动匹配分配:
|
||||
|
||||
```csharp
|
||||
// 输入:所有无人机编队 + 已部署的火力单元
|
||||
// 输出:每个编队的拦截方案 + 合并的发射计划
|
||||
|
||||
var allSolutions = new List<DefenseSolution>();
|
||||
var usedPlatforms = new HashSet<int>();
|
||||
var usedFireUnits = new HashSet<string>();
|
||||
|
||||
foreach (var droneGroup in threat.DroneGroups)
|
||||
foreach (var droneWave in threat.DroneWaves)
|
||||
{
|
||||
// 从可用装备中选择合适的火力单元(弹药类型匹配目标动力类型)
|
||||
var availableUnits = fireUnits.Where(u => !usedPlatforms.Contains(u.Id)).ToList();
|
||||
var solution = advisor.RecommendForGroup(droneGroup, environment, availableUnits);
|
||||
// 从可用火力单元中选择合适的(弹药类型匹配目标动力类型)
|
||||
var availableUnits = fireUnits.Where(u => !usedFireUnits.Contains(u.Id)).ToList();
|
||||
var solution = advisor.RecommendForWave(droneWave, environment, availableUnits);
|
||||
|
||||
// 标记占用的平台(一组装备可同时对付一个编队)
|
||||
foreach (var idx in solution.AssignedPlatformIndices)
|
||||
usedPlatforms.Add(idx);
|
||||
// 标记占用的火力单元
|
||||
foreach (var idx in solution.AssignedFireUnitIds)
|
||||
usedFireUnits.Add(idx);
|
||||
|
||||
allSolutions.Add(solution);
|
||||
}
|
||||
|
||||
// 复用判定(不写死"一对一"或"一对多"):
|
||||
// 同种弹药 → 只需冷却时间(秒级),可直接复用
|
||||
// 异种弹药 → 需要 AmmoChangeTime(默认 5 分钟),检查编队间隔是否够
|
||||
// 异种弹药 → 需要 AmmoChangeTime(默认 5 分钟),检查批次间隔是否够
|
||||
// 弹药耗尽 → 不可复用
|
||||
|
||||
var merged = MergeFireSchedules(allSolutions);
|
||||
@ -1435,13 +1443,13 @@ engine.SetFireSchedule(merged);
|
||||
|
||||
| 表 | 变更 |
|
||||
|------|------|
|
||||
| `RoutePlan` | PK 改为 (TaskId, GroupId) 复合键 |
|
||||
| `TargetConfig` | 不变(已有 GroupId FK) |
|
||||
| `Waypoint` | 关联 `(TaskId, GroupId)` 定位航路 |
|
||||
| `RoutePlan` | PK 为 (TaskId, WaveId) 复合键 |
|
||||
| `TargetConfig` | 不变(已有 WaveId) |
|
||||
| `Waypoint` | 关联 `(TaskId, WaveId)` 定位航路 |
|
||||
|
||||
`IScenarioService` 新增:
|
||||
`IScenarioService` 的 `SaveRoute` 签名:
|
||||
```csharp
|
||||
void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wps); // groupId 参数
|
||||
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> wps); // waveId 参数
|
||||
```
|
||||
|
||||
### 报告
|
||||
@ -1450,10 +1458,10 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
|
||||
|
||||
```markdown
|
||||
## 编队汇总
|
||||
| 编队 | 目标 | 数量 | 拦截方案 | 结果 |
|
||||
| 批次 | 目标 | 数量 | 拦截方案 | 结果 |
|
||||
|------|------|------|------|------|
|
||||
| 编队A | 活塞 | 3 | 惰性气体 12发 | ✅ 全毁 |
|
||||
| 编队B | 喷气 | 2 | 活性材料 8发 | ⚠️ 1逃脱 |
|
||||
| 批次A | 活塞 | 3 | 惰性气体 12发 | ✅ 全毁 |
|
||||
| 批次B | 喷气 | 2 | 活性材料 8发 | ⚠️ 1逃脱 |
|
||||
```
|
||||
|
||||
---
|
||||
@ -1473,7 +1481,7 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
|
||||
│ └─ 多边形绘制 / 编辑 / 删除 │
|
||||
├────────────────────────────────────────────────────────┤
|
||||
│ 步骤 2:目标配置 │
|
||||
│ ├─ 编组选择(已有编队/新建编队) │
|
||||
│ ├─ 批次选择(已有批次/新建批次) │
|
||||
│ ├─ 目标类型选择(自动填充默认参数) │
|
||||
│ ├─ 数量 / 动力类型 / 翼展 / 速度 / 高度 │
|
||||
│ └─ 预设典型目标库(NEW) │
|
||||
@ -1481,7 +1489,7 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
|
||||
│ 步骤 3:装备部署(重构) │
|
||||
│ ├─ 装备角色(探测设备 / 发射平台) │
|
||||
│ ├─ 发射平台类型(空基 / 地基 NEW) │
|
||||
│ ├─ 数量 / 编组 / 部署位置 │
|
||||
│ ├─ 数量 / 部署位置 │
|
||||
│ ├─ 挂载气溶胶类型 / 弹药数量 │
|
||||
│ ├─ 弹药初速 / 释放高度 / 冷却时间 │
|
||||
│ └─ 探测半径(探测设备) │
|
||||
@ -1521,7 +1529,7 @@ void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> wp
|
||||
| 12 | 第三方数据 | **静态配置(JSON → SQLite)** | 仅提供弹药基础参数和初始扩散状态,不提供运行时算法 |
|
||||
| 13 | 运动学 | 简化运动学 + 风偏 | 航路点线性插值 + 风速矢量 |
|
||||
| 14 | 弹药弹道 | 抛物线(地基)/ 自由落体(空基) | 基于初速、角度、释放高度计算 |
|
||||
| 15 | 编组 | Group 独立实体 | 编组独立管理,任务中引用 |
|
||||
| 15 | 批次 / 火力单元 | WaveId + 火力单元独立作战 | 批次关联航路但不独占;火力单元是最小独立作战单位,不需要上层编组 |
|
||||
| 16 | 管控区域 | ControlZone 表 + Tick 判定 | 电子围栏,侵入即任务失败 |
|
||||
| 17 | 配置向导流程 | **威胁驱动三阶段** | Phase1 定义威胁 → Phase2 算法推荐 → Phase3 审阅确认 |
|
||||
| 18 | 报告导出 | 模板填充 → Markdown | PDF/Word 待调研 |
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
# DefensePlanner 防御规划引擎 — 技术方案
|
||||
|
||||
- **版本**:V3
|
||||
- **日期**:2026-06-13
|
||||
- **版本**:V4
|
||||
- **日期**:2026-06-15
|
||||
- **状态**:已实现
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁编队列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
|
||||
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁批次列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
|
||||
|
||||
**当前问题**:
|
||||
|
||||
@ -54,11 +54,11 @@ class FireUnit {
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 DroneGroup(威胁编队)
|
||||
### 2.2 DroneWave(威胁批次)
|
||||
|
||||
```csharp
|
||||
class DroneGroup {
|
||||
string GroupId; // 编队 ID
|
||||
class DroneWave {
|
||||
string WaveId; // 批次 ID
|
||||
TargetConfig Target; // 类型、数量、动力、翼展、速度、高度
|
||||
List<Waypoint> Waypoints; // 航路点
|
||||
float ArrivalTime; // 预计算:到达防御区域中点的时间 s
|
||||
@ -78,7 +78,7 @@ class DroneGroup {
|
||||
```csharp
|
||||
class UnitAssignment {
|
||||
string FireUnitId; // 分配到的火力单元
|
||||
string DroneGroupId; // 对抗的威胁编队
|
||||
string DroneWaveId; // 对抗的威胁批次
|
||||
AerosolType AmmoType; // 装填的弹药类型
|
||||
int RoundsFired; // 本次发射几发
|
||||
float FirstFireTime; // 首发发射时机 s
|
||||
@ -113,7 +113,7 @@ class PlannerResult {
|
||||
## 4. 内部流程(五步法)
|
||||
|
||||
```
|
||||
输入:List<FireUnit> + List<DroneGroup> + CombatScene
|
||||
输入:List<FireUnit> + List<DroneWave> + CombatScene
|
||||
│
|
||||
▼
|
||||
Step 1 — 威胁排序
|
||||
@ -190,7 +190,7 @@ class InterceptCandidate {
|
||||
|
||||
**候选生成逻辑**:
|
||||
|
||||
对于给定的威胁编队和火力单元:
|
||||
对于给定的威胁批次和火力单元:
|
||||
|
||||
1. **弹药兼容性**:`Unit.AmmoTypes` 包含威胁需要的弹药类型
|
||||
2. **地基可达性**:计算目标与部署点的水平距离,校验 `MuzzleVelocity` 射程
|
||||
@ -239,7 +239,7 @@ for each threat in pending:
|
||||
|
||||
> 作用:给操作员一个置信区间——最优 vs 临界,展示"再少就不够了"的底线。
|
||||
|
||||
### 5.5 多编队合并
|
||||
### 5.5 多批次合并
|
||||
|
||||
```
|
||||
Step 4 的贪心算法天然支持多威胁:
|
||||
@ -258,7 +258,7 @@ Step 4 的贪心算法天然支持多威胁:
|
||||
public interface IDefensePlanner
|
||||
{
|
||||
/// <summary>为给定火力单元池和威胁列表生成规划方案</summary>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene environment);
|
||||
}
|
||||
```
|
||||
|
||||
@ -273,16 +273,16 @@ public class DefaultDefensePlanner : IDefensePlanner
|
||||
|
||||
public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog) { ... }
|
||||
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene env)
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene env)
|
||||
{
|
||||
// Step 1-5
|
||||
}
|
||||
|
||||
// 内部方法
|
||||
private List<DroneGroup> Prioritize(List<DroneGroup> threats) { ... }
|
||||
private List<DroneWave> Prioritize(List<DroneWave> threats) { ... }
|
||||
private AerosolType MatchAmmo(PowerType power) { ... }
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat, List<FireUnit> units, float now) { ... }
|
||||
private DefensePlan Solve(List<DroneGroup> threats, List<FireUnit> units) { ... }
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneWave threat, List<FireUnit> units, float now) { ... }
|
||||
private DefensePlan Solve(List<DroneWave> threats, List<FireUnit> units) { ... }
|
||||
private DefensePlan DeriveCritical(DefensePlan best) { ... }
|
||||
}
|
||||
```
|
||||
@ -293,7 +293,7 @@ public class DefaultDefensePlanner : IDefensePlanner
|
||||
|--------|--------|------|
|
||||
| `IDefenseAdvisor.Recommend(ThreatProfile)` | `IDefensePlanner.Plan(fireUnits, threats, env)` | ✅ 已替换 |
|
||||
| `IDefenseAdvisor.GetDefenseRecommendation(taskId)` | 不再需要 | ✅ 已删除 |
|
||||
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多编队 | ✅ 已删除 |
|
||||
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多批次 | ✅ 已删除 |
|
||||
|
||||
---
|
||||
|
||||
@ -304,7 +304,7 @@ SimulationEngine.Initialize(taskId)
|
||||
│
|
||||
│ // 引擎内部组装
|
||||
├── BuildFireUnits(config) → List<FireUnit>
|
||||
├── BuildDroneGroups(config) → List<DroneGroup>
|
||||
├── BuildDroneWaves(config) → List<DroneWave>
|
||||
└── _scene (CombatScene)
|
||||
│
|
||||
▼
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
SimulationEngine.Initialize()
|
||||
│
|
||||
├─ BuildFireUnits() → List<FireUnit>
|
||||
├─ BuildDroneGroups() → List<DroneGroup>
|
||||
├─ BuildDroneWaves() → List<DroneWave>
|
||||
│
|
||||
▼
|
||||
IDefensePlanner.Plan(fireUnits, threats, scene)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# 实施计划与任务跟踪
|
||||
|
||||
> **项目**:反无人机仿真系统后端
|
||||
> **文档版本**:V1.3
|
||||
> **更新日期**:2026-06-14
|
||||
> **文档版本**:V1.4
|
||||
> **更新日期**:2026-06-15
|
||||
|
||||
---
|
||||
|
||||
@ -32,7 +32,7 @@ Phase 8 🔄 待开发(天气/物理模型统一已完成)
|
||||
|
||||
| # | 任务 | 状态 |
|
||||
|---|------|------|
|
||||
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService/GroupService、单元测试 | ✅ |
|
||||
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService、单元测试 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
@ -105,7 +105,7 @@ Phase 8 🔄 待开发(天气/物理模型统一已完成)
|
||||
| 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ✅ | `src/Unity/` 项目,16 个 DLL → `Assets/Plugins/` |
|
||||
| 6.2 | 实现 `UnityPathProvider` | 1h | ✅ | `Application.persistentDataPath` 桥接 |
|
||||
| 6.3 | 实现 `ModelManager` | 3h | ✅ | 导入/删除/查询,含 Verify |
|
||||
| 6.4 | 实现 `ScenarioManager`(5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多编队 Route |
|
||||
| 6.4 | 实现 `ScenarioManager`(5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多批次 Route |
|
||||
| 6.5 | 实现 `SimulationRunner`(Update 驱动) | 3h | ✅ | Tick 驱动 + 事件订阅 + 实体位置同步 + 炮弹轨迹可视化 |
|
||||
| 6.6 | 实现 `ReplayController`(帧加载) | 3h | ✅ | 从分库加载帧数据,TotalFrames/GetFrame |
|
||||
| 6.7 | 实现 `ReportManager` | 2h | ✅ | 生成 + Markdown 导出 |
|
||||
@ -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 基于探测边界、集成场景不破坏)
|
||||
- 范围外:实时探测→火控链路(未来增强)
|
||||
@ -1,7 +1,7 @@
|
||||
# 后端对接文档(Unity 前端)
|
||||
|
||||
> **版本**:V1.1
|
||||
> **日期**:2026-06-12
|
||||
> **版本**:V1.2
|
||||
> **日期**:2026-06-15
|
||||
> **Unity 版本**:2022.3.62f3c1
|
||||
|
||||
---
|
||||
@ -35,17 +35,21 @@ data/default_ammo.json ← 默认弹药参数(放到 persisten
|
||||
var scenario = GetComponent<ScenarioManager>();
|
||||
var task = scenario.CreateTask("拦截活塞式无人机", "");
|
||||
scenario.SaveScene(task.Id, new CombatScene { WindSpeed = 3 });
|
||||
scenario.SaveTarget(task.Id, new TargetConfig { PowerType = (int)PowerType.Piston, ... });
|
||||
scenario.SaveRoute(task.Id, routePlan, waypoints);
|
||||
scenario.SaveTarget(task.Id, new TargetConfig { WaveId = "default", PowerType = (int)PowerType.Piston, ... });
|
||||
scenario.SaveRoute(task.Id, "default", routePlan, waypoints);
|
||||
|
||||
// 推荐方案(自动从数据库读弹药规格)
|
||||
// 推荐方案(DefensePlanner 五步流水线)
|
||||
var detail = scenario.GetDetail(task.Id);
|
||||
var ammoCatalog = db.Table<AmmunitionSpec>().ToList(); // DatabaseManager 已种子
|
||||
var rec = new DefaultDefenseAdvisor(ammoCatalog).Recommend(new ThreatProfile { ... });
|
||||
var fireUnits = new List<FireUnit> { /* 从 EquipmentDeployment 构建 */ };
|
||||
var threats = new List<DroneWave> { new DroneWave { WaveId = "default", Target = detail.Targets[0], Route = detail.Routes[0], Waypoints = detail.WaypointGroups["default"] } };
|
||||
var plan = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, threats, detail.Scene, new List<DetectionSource>());
|
||||
|
||||
// 应用到想定
|
||||
scenario.SaveCloud(task.Id, rec.Best.RecommendedCloud);
|
||||
scenario.SaveDeployment(task.Id, rec.Best.Platforms.Select(p => new EquipmentDeployment { ... }).ToList());
|
||||
// 应用到想定(发射计划传给引擎,部署方案保存到配置)
|
||||
var mergedSchedule = plan.Best.MergedSchedule;
|
||||
// 部署火力单元到 EquipmentDeployment...
|
||||
scenario.SaveCloud(task.Id, new CloudDispersal { /* 从 plan.Best.Assignments 推算云团参数 */ });
|
||||
scenario.SaveDeployment(task.Id, equips);
|
||||
```
|
||||
|
||||
### 3.2 启动仿真(事件驱动)
|
||||
@ -54,7 +58,7 @@ scenario.SaveDeployment(task.Id, rec.Best.Platforms.Select(p => new EquipmentDep
|
||||
var runner = GetComponent<SimulationRunner>();
|
||||
|
||||
// 推荐方案的 FireSchedule 直接传给引擎
|
||||
runner.Engine.SetFireSchedule(rec.Best.FireSchedule);
|
||||
runner.Engine.SetFireSchedule(plan.Best.MergedSchedule);
|
||||
runner.LoadAndStart(task.Id);
|
||||
runner.Engine.TimeScale = 4f; // 加速
|
||||
|
||||
@ -93,7 +97,7 @@ mgr.SaveScene(id, combatScene);
|
||||
mgr.SaveTarget(id, targetConfig);
|
||||
mgr.SaveDeployment(id, equipmentList);
|
||||
mgr.SaveCloud(id, cloudDispersal);
|
||||
mgr.SaveRoute(id, routePlan, waypoints);
|
||||
mgr.SaveRoute(id, "default", routePlan, waypoints);
|
||||
TaskFullConfig detail = mgr.GetDetail(id);
|
||||
PagedResult<SimTask> result = mgr.Search("关键词", from, to, page, pageSize);
|
||||
```
|
||||
@ -163,4 +167,4 @@ dotnet publish -c Release -o ../../unity_plugins
|
||||
| 实体事件映射 | `docs/design/technical/仿真器实体与事件映射.md` |
|
||||
| 任务跟踪 | `docs/implementation/tasks/实施计划与任务跟踪.md` |
|
||||
| 测试报告 | `test/reports/`(每次集成测试自动生成) |
|
||||
| 测试状态 | **129 测试,95.4% 行覆盖率,12 秒** |
|
||||
| 测试状态 | **204 测试,95.4% 行覆盖率,63 秒** |
|
||||
|
||||
@ -46,10 +46,10 @@ namespace CounterDrone.Core.Algorithms
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
}
|
||||
|
||||
/// <summary>无人机编队(规划器输入)</summary>
|
||||
public class DroneGroup
|
||||
/// <summary>无人机批次(规划器输入)</summary>
|
||||
public class DroneWave
|
||||
{
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
public TargetConfig Target { get; set; } = new();
|
||||
public RoutePlan Route { get; set; } = new();
|
||||
public List<Waypoint> Waypoints { get; set; } = new();
|
||||
@ -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()
|
||||
@ -114,13 +120,28 @@ namespace CounterDrone.Core.Algorithms
|
||||
public class UnitAssignment
|
||||
{
|
||||
public string FireUnitId { get; set; } = string.Empty;
|
||||
public string DroneGroupId { get; set; } = string.Empty;
|
||||
public string DroneWaveId { get; set; } = string.Empty;
|
||||
public AerosolType AmmoType { get; set; }
|
||||
public int RoundsFired { get; set; }
|
||||
public float FirstFireTime { get; set; }
|
||||
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<DroneWave> 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)
|
||||
{
|
||||
@ -94,7 +108,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
// Step 3-4: 候选生成 → 贪心分配
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private DefensePlan Solve(List<DroneGroup> sortedThreats,
|
||||
private DefensePlan Solve(List<DroneWave> sortedThreats,
|
||||
List<FireUnit> fireUnits, CombatScene env)
|
||||
{
|
||||
var plan = new DefensePlan();
|
||||
@ -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;
|
||||
@ -199,7 +215,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
plan.Assignments.Add(new UnitAssignment
|
||||
{
|
||||
FireUnitId = unit.Id,
|
||||
DroneGroupId = threat.GroupId,
|
||||
DroneWaveId = threat.WaveId,
|
||||
AmmoType = neededAmmo,
|
||||
RoundsFired = rounds,
|
||||
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
|
||||
@ -220,7 +236,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
var a2 = _ammoCatalog.FirstOrDefault(s =>
|
||||
s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Target.PowerType));
|
||||
int totalRounds = plan.Assignments
|
||||
.Where(a => a.DroneGroupId == threat.GroupId)
|
||||
.Where(a => a.DroneWaveId == threat.WaveId)
|
||||
.Sum(a => a.RoundsFired);
|
||||
if (totalRounds > 0)
|
||||
threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
|
||||
@ -237,7 +253,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
// 候选生成(使用真实物理)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat,
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneWave threat,
|
||||
List<FireUnit> availableUnits, CombatScene env)
|
||||
{
|
||||
var candidates = new List<InterceptCandidate>();
|
||||
@ -265,7 +281,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat,
|
||||
private InterceptCandidate? BuildGroundBasedCandidate(DroneWave threat,
|
||||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||||
float effectiveR, CombatScene env)
|
||||
{
|
||||
@ -295,7 +311,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
};
|
||||
}
|
||||
|
||||
private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat,
|
||||
private InterceptCandidate? BuildAirBasedCandidate(DroneWave threat,
|
||||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||||
float effectiveR, CombatScene env)
|
||||
{
|
||||
@ -342,7 +358,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
return (effectiveR, expansionTime, rPhase2);
|
||||
}
|
||||
|
||||
private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec ammo,
|
||||
private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
|
||||
CombatScene env, bool isAirBased, float turbulentRadius)
|
||||
{
|
||||
var cloudModel = new CloudExpansionModel(ammo, env);
|
||||
@ -355,7 +371,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
return cloudModel.RoundsNeeded(requiredCoverage, spacing);
|
||||
}
|
||||
|
||||
private float ComputeInterceptProbability(DroneGroup threat,
|
||||
private float ComputeInterceptProbability(DroneWave threat,
|
||||
AmmunitionSpec ammo, int rounds, CombatScene env)
|
||||
{
|
||||
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
|
||||
@ -374,7 +390,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
// 发射事件生成(真实物理)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private List<FireEvent> GenerateFireEventsAt(DroneGroup threat, FireUnit unit,
|
||||
private List<FireEvent> GenerateFireEventsAt(DroneWave threat, FireUnit unit,
|
||||
AerosolType ammoType, AmmunitionSpec ammo, CombatScene env, float targetOffset,
|
||||
int yLane, int yLanes, float formationWidth)
|
||||
{
|
||||
@ -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;
|
||||
|
||||
@ -497,7 +517,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
var reduced = new UnitAssignment
|
||||
{
|
||||
FireUnitId = assignment.FireUnitId,
|
||||
DroneGroupId = assignment.DroneGroupId,
|
||||
DroneWaveId = assignment.DroneWaveId,
|
||||
AmmoType = assignment.AmmoType,
|
||||
RoundsFired = rounds,
|
||||
FirstFireTime = assignment.FirstFireTime,
|
||||
@ -517,7 +537,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
// 辅助
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static Vector3 ThreatMidpoint(DroneGroup threat)
|
||||
private static Vector3 ThreatMidpoint(DroneWave threat)
|
||||
{
|
||||
if (threat.Waypoints.Count < 2)
|
||||
return new Vector3(0, (float)threat.Target.TypicalAltitude, 0);
|
||||
|
||||
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="threats">威胁批次列表</param>
|
||||
/// <param name="environment">作战环境(含天气,影响光电探测距离)</param>
|
||||
/// <param name="detectionSources">统一信息网络的探测源列表(独立探测设备 + 火力单元自带探测)。可为空列表。</param>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> 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()
|
||||
|
||||
@ -68,9 +68,8 @@ namespace CounterDrone.Core
|
||||
db.CreateTable<EquipmentDeployment>();
|
||||
db.CreateTable<CloudDispersal>();
|
||||
db.CreateTable<RoutePlan>();
|
||||
db.CreateIndex("RoutePlan", new[] { "TaskId", "GroupId" }, true);
|
||||
db.CreateIndex("RoutePlan", new[] { "TaskId", "WaveId" }, true);
|
||||
db.CreateTable<Waypoint>();
|
||||
db.CreateTable<Group>();
|
||||
db.CreateTable<SimulationReport>();
|
||||
|
||||
db.CreateIndex("SimTask", "TaskNumber");
|
||||
|
||||
@ -92,7 +92,7 @@ namespace CounterDrone.Core.Models
|
||||
Destroyed = 3
|
||||
}
|
||||
|
||||
// === 编队 & 编组 ===
|
||||
// === 编队 & 批次 ===
|
||||
public enum FormationMode
|
||||
{
|
||||
Single = 0,
|
||||
@ -100,12 +100,6 @@ namespace CounterDrone.Core.Models
|
||||
Swarm = 2
|
||||
}
|
||||
|
||||
public enum GroupType
|
||||
{
|
||||
DroneFleet = 0,
|
||||
EquipmentGroup = 1
|
||||
}
|
||||
|
||||
// === 运行时 ===
|
||||
public enum EntityType
|
||||
{
|
||||
|
||||
@ -17,7 +17,7 @@ namespace CounterDrone.Core.Models
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
// 发射平台专用
|
||||
public int? PlatformType { get; set; }
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>编组</summary>
|
||||
[Table("Group")]
|
||||
public class Group
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[NotNull]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public int GroupType { get; set; } = (int)Models.GroupType.DroneFleet;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("o");
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>步骤5:航路规划 — 多编队支持:每个(任务,编队)一条航路</summary>
|
||||
/// <summary>步骤5:航路规划 — 多批次支持:每个(任务,批次)一条航路</summary>
|
||||
[Table("RoutePlan")]
|
||||
public class RoutePlan
|
||||
{
|
||||
@ -14,7 +14,7 @@ namespace CounterDrone.Core.Models
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
[Indexed]
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
public int FormationMode { get; set; } = (int)Models.FormationMode.Single;
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ namespace CounterDrone.Core.Models
|
||||
[Indexed]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
public int TargetType { get; set; } = (int)Models.TargetType.Rotor;
|
||||
|
||||
|
||||
@ -11,9 +11,9 @@ namespace CounterDrone.Core.Models
|
||||
public List<TargetConfig> Targets { get; set; } = new();
|
||||
public List<EquipmentDeployment> Equipment { get; set; } = new();
|
||||
public CloudDispersal Cloud { get; set; } = new();
|
||||
/// <summary>多编队航路(多批次支持)</summary>
|
||||
/// <summary>多批次航路(多批次支持)</summary>
|
||||
public List<RoutePlan> Routes { get; set; } = new();
|
||||
/// <summary>按 GroupId 分组的航路点</summary>
|
||||
/// <summary>按 WaveId 分组的航路点</summary>
|
||||
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
@ -13,8 +13,8 @@ namespace CounterDrone.Core.Models
|
||||
[Indexed]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>关联的编队 ID(多编队支持)</summary>
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
/// <summary>关联的批次 ID(多批次支持)</summary>
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
[NotNull]
|
||||
public int OrderIndex { get; set; }
|
||||
|
||||
@ -14,6 +14,14 @@ namespace CounterDrone.Core.Repository
|
||||
return Db.Table<EquipmentDeployment>().Where(e => e.TaskId == taskId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>按任务和角色查询装备(EquipmentRole: 0=Detection, 1=LaunchPlatform)</summary>
|
||||
public List<EquipmentDeployment> GetByTaskIdAndRole(string taskId, int equipmentRole)
|
||||
{
|
||||
return Db.Table<EquipmentDeployment>()
|
||||
.Where(e => e.TaskId == taskId && e.EquipmentRole == equipmentRole)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void DeleteByTaskId(string taskId)
|
||||
{
|
||||
var equips = GetByTaskId(taskId);
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class GroupRepository : BaseRepository<Group>
|
||||
{
|
||||
public GroupRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<Group> GetByType(int groupType)
|
||||
{
|
||||
return Db.Table<Group>()
|
||||
.Where(g => g.GroupType == groupType)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,10 +14,10 @@ namespace CounterDrone.Core.Repository
|
||||
return Db.Table<RoutePlan>().Where(r => r.TaskId == taskId).ToList();
|
||||
}
|
||||
|
||||
public RoutePlan GetByTaskAndGroup(string taskId, string groupId)
|
||||
public RoutePlan GetByTaskAndWave(string taskId, string waveId)
|
||||
{
|
||||
return Db.Table<RoutePlan>()
|
||||
.FirstOrDefault(r => r.TaskId == taskId && r.GroupId == groupId);
|
||||
.FirstOrDefault(r => r.TaskId == taskId && r.WaveId == waveId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,10 +17,10 @@ namespace CounterDrone.Core.Repository
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<Waypoint> GetByTaskAndGroup(string taskId, string groupId)
|
||||
public List<Waypoint> GetByTaskAndWave(string taskId, string waveId)
|
||||
{
|
||||
return Db.Table<Waypoint>()
|
||||
.Where(w => w.TaskId == taskId && w.GroupId == groupId)
|
||||
.Where(w => w.TaskId == taskId && w.WaveId == waveId)
|
||||
.OrderBy(w => w.OrderIndex)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
public class GroupService : IGroupService
|
||||
{
|
||||
private readonly GroupRepository _repo;
|
||||
|
||||
public GroupService(GroupRepository repo)
|
||||
{
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public Group CreateGroup(string name, GroupType type, string description)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("编组名称不能为空");
|
||||
|
||||
var group = new Group
|
||||
{
|
||||
Name = name,
|
||||
GroupType = (int)type,
|
||||
Description = description,
|
||||
};
|
||||
|
||||
_repo.Insert(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
public void DeleteGroup(string id)
|
||||
{
|
||||
_repo.Delete(id);
|
||||
}
|
||||
|
||||
public List<Group> GetGroups(GroupType? type)
|
||||
{
|
||||
if (type.HasValue)
|
||||
return _repo.GetByType((int)type.Value);
|
||||
return _repo.GetAll();
|
||||
}
|
||||
|
||||
public Group GetGroup(string id)
|
||||
{
|
||||
return _repo.GetById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
/// <summary>编组管理服务</summary>
|
||||
public interface IGroupService
|
||||
{
|
||||
Group CreateGroup(string name, GroupType type, string description);
|
||||
void DeleteGroup(string id);
|
||||
List<Group> GetGroups(GroupType? type);
|
||||
Group GetGroup(string id);
|
||||
}
|
||||
}
|
||||
@ -15,8 +15,16 @@ namespace CounterDrone.Core.Services
|
||||
void SaveControlZones(string taskId, List<ControlZone> zones);
|
||||
void SaveTarget(string taskId, TargetConfig target);
|
||||
void SaveDeployment(string taskId, List<EquipmentDeployment> equips);
|
||||
|
||||
/// <summary>添加单个探测设备(独立于 SaveDeployment,不覆盖火力单元)</summary>
|
||||
void AddDetection(string taskId, EquipmentDeployment detection);
|
||||
/// <summary>删除探测设备</summary>
|
||||
void DeleteDetection(string detectionId);
|
||||
/// <summary>获取任务的所有探测设备(EquipmentRole.Detection)</summary>
|
||||
List<EquipmentDeployment> GetDetections(string taskId);
|
||||
|
||||
void SaveCloudDispersal(string taskId, CloudDispersal cloud);
|
||||
void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> waypoints);
|
||||
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints);
|
||||
void UpdateStep(string taskId, int step);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ namespace CounterDrone.Core.Services
|
||||
Cloud = _cloudRepo.GetById(id) ?? new CloudDispersal { TaskId = id },
|
||||
Routes = _routeRepo.GetByTaskId(id),
|
||||
WaypointGroups = _waypointRepo.GetByTaskId(id)
|
||||
.GroupBy(w => w.GroupId)
|
||||
.GroupBy(w => w.WaveId)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(w => w.OrderIndex).ToList()),
|
||||
};
|
||||
}
|
||||
@ -158,6 +158,26 @@ namespace CounterDrone.Core.Services
|
||||
TouchTask(taskId);
|
||||
}
|
||||
|
||||
public void AddDetection(string taskId, EquipmentDeployment detection)
|
||||
{
|
||||
detection.TaskId = taskId;
|
||||
detection.EquipmentRole = (int)EquipmentRole.Detection;
|
||||
if (string.IsNullOrEmpty(detection.Id))
|
||||
detection.Id = Guid.NewGuid().ToString();
|
||||
_equipRepo.Insert(detection);
|
||||
TouchTask(taskId);
|
||||
}
|
||||
|
||||
public void DeleteDetection(string detectionId)
|
||||
{
|
||||
_equipRepo.Delete(detectionId);
|
||||
}
|
||||
|
||||
public List<EquipmentDeployment> GetDetections(string taskId)
|
||||
{
|
||||
return _equipRepo.GetByTaskIdAndRole(taskId, (int)EquipmentRole.Detection);
|
||||
}
|
||||
|
||||
public void SaveCloudDispersal(string taskId, CloudDispersal cloud)
|
||||
{
|
||||
cloud.TaskId = taskId;
|
||||
@ -169,11 +189,11 @@ namespace CounterDrone.Core.Services
|
||||
TouchTask(taskId);
|
||||
}
|
||||
|
||||
public void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> waypoints)
|
||||
public void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints)
|
||||
{
|
||||
route.TaskId = taskId;
|
||||
route.GroupId = groupId;
|
||||
var existing = _routeRepo.GetByTaskAndGroup(taskId, groupId);
|
||||
route.WaveId = waveId;
|
||||
var existing = _routeRepo.GetByTaskAndWave(taskId, waveId);
|
||||
if (existing != null)
|
||||
{
|
||||
route.Id = existing.Id;
|
||||
@ -182,13 +202,13 @@ namespace CounterDrone.Core.Services
|
||||
else
|
||||
_routeRepo.Insert(route);
|
||||
|
||||
// 删除该编队的旧航路点,插入新的
|
||||
var oldWps = _waypointRepo.GetByTaskAndGroup(taskId, groupId);
|
||||
// 删除该批次的旧航路点,插入新的
|
||||
var oldWps = _waypointRepo.GetByTaskAndWave(taskId, waveId);
|
||||
foreach (var w in oldWps) _waypointRepo.Delete(w.Id);
|
||||
for (int i = 0; i < waypoints.Count; i++)
|
||||
{
|
||||
waypoints[i].TaskId = taskId;
|
||||
waypoints[i].GroupId = groupId;
|
||||
waypoints[i].WaveId = waveId;
|
||||
waypoints[i].OrderIndex = i;
|
||||
if (string.IsNullOrEmpty(waypoints[i].Id))
|
||||
waypoints[i].Id = Guid.NewGuid().ToString();
|
||||
|
||||
@ -8,7 +8,7 @@ namespace CounterDrone.Core.Simulation
|
||||
public class DroneEntity
|
||||
{
|
||||
public string Id { get; }
|
||||
public string GroupId { get; }
|
||||
public string WaveId { get; }
|
||||
public TargetType TargetType { get; }
|
||||
public PowerType PowerType { get; }
|
||||
public float Wingspan { get; }
|
||||
@ -27,11 +27,11 @@ namespace CounterDrone.Core.Simulation
|
||||
/// <summary>沿航路已飞行弧长(米),由 RouteGeometry 驱动</summary>
|
||||
private float _traveledArc;
|
||||
|
||||
public DroneEntity(string id, string groupId, TargetConfig config, List<Waypoint> route,
|
||||
public DroneEntity(string id, string waveId, TargetConfig config, List<Waypoint> route,
|
||||
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode)
|
||||
{
|
||||
Id = id;
|
||||
GroupId = groupId;
|
||||
WaveId = waveId;
|
||||
TargetType = (TargetType)config.TargetType;
|
||||
PowerType = (PowerType)config.PowerType;
|
||||
Wingspan = (float)config.Wingspan;
|
||||
|
||||
@ -88,8 +88,8 @@ namespace CounterDrone.Core.Simulation
|
||||
_drones.Clear();
|
||||
foreach (var target in config.Targets)
|
||||
{
|
||||
var route = config.Routes.FirstOrDefault(r => r.GroupId == target.GroupId);
|
||||
var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List<Waypoint>());
|
||||
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;
|
||||
var mode = (FormationMode)route.FormationMode;
|
||||
var lateralSpacing = (float)route.LateralSpacing;
|
||||
@ -100,7 +100,7 @@ namespace CounterDrone.Core.Simulation
|
||||
{
|
||||
int latIdx = i % latCount;
|
||||
int longIdx = i / latCount;
|
||||
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
|
||||
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId,
|
||||
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode));
|
||||
}
|
||||
}
|
||||
@ -137,10 +137,11 @@ namespace CounterDrone.Core.Simulation
|
||||
if (_fireSchedule.Count == 0)
|
||||
{
|
||||
var fireUnits = BuildFireUnits(config);
|
||||
var threats = BuildDroneGroups(config);
|
||||
var threats = BuildDroneWaves(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,22 +342,52 @@ 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;
|
||||
}
|
||||
|
||||
private static List<DroneGroup> BuildDroneGroups(TaskFullConfig config)
|
||||
/// <summary>构建统一信息网络的探测源列表。
|
||||
/// 独立探测设备(EquipmentRole.Detection)+ 火力单元自带探测能力。</summary>
|
||||
private static List<DetectionSource> BuildDetectionSources(TaskFullConfig config)
|
||||
{
|
||||
var groups = new List<DroneGroup>();
|
||||
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<DroneWave> BuildDroneWaves(TaskFullConfig config)
|
||||
{
|
||||
var groups = new List<DroneWave>();
|
||||
foreach (var t in config.Targets)
|
||||
{
|
||||
var route = config.Routes.FirstOrDefault(r => r.GroupId == t.GroupId);
|
||||
var wps = config.WaypointGroups.GetValueOrDefault(t.GroupId, new List<Waypoint>());
|
||||
groups.Add(new DroneGroup
|
||||
var route = config.Routes.FirstOrDefault(r => r.WaveId == t.WaveId);
|
||||
var wps = config.WaypointGroups.GetValueOrDefault(t.WaveId, new List<Waypoint>());
|
||||
groups.Add(new DroneWave
|
||||
{
|
||||
GroupId = t.GroupId,
|
||||
WaveId = t.WaveId,
|
||||
Target = t,
|
||||
Route = route ?? new RoutePlan(),
|
||||
Waypoints = wps,
|
||||
|
||||
Binary file not shown.
@ -1,45 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using CounterDrone.Core.Services;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CounterDrone.Unity
|
||||
{
|
||||
/// <summary>编组管理桥接 — 无人机编队和装备编组的 CRUD</summary>
|
||||
public class GroupManager : MonoBehaviour
|
||||
{
|
||||
private IGroupService _service;
|
||||
private SQLite.SQLiteConnection _db;
|
||||
|
||||
public IGroupService Service => _service;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
var paths = new UnityPathProvider();
|
||||
_db = new DatabaseManager(paths).OpenMainDb();
|
||||
SqliteConnectionTracker.Track(_db);
|
||||
_service = new GroupService(new GroupRepository(_db));
|
||||
}
|
||||
|
||||
void OnDisable() { SqliteConnectionTracker.Untrack(_db); _db?.Dispose(); _db = null; }
|
||||
|
||||
public Group Create(string name, GroupType type, string desc = "")
|
||||
=> _service.CreateGroup(name, type, desc);
|
||||
|
||||
public void Delete(string id) => _service.DeleteGroup(id);
|
||||
|
||||
public List<Group> GetByType(GroupType? type = null) => _service.GetGroups(type);
|
||||
|
||||
public Group Get(string id) => _service.GetGroup(id);
|
||||
|
||||
[ContextMenu("Verify")]
|
||||
void Verify()
|
||||
{
|
||||
Awake();
|
||||
var g = Create("测试编队", GroupType.DroneFleet, "自动验证");
|
||||
Debug.Log($"GroupManager OK. Created: {g.Name} ({g.Id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24c080ea3d6cbcf45aa804fd10269e4a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -35,7 +35,7 @@ namespace CounterDrone.Unity
|
||||
scenarioMgr.SaveScene(taskId, new CombatScene { WindSpeed = 0 });
|
||||
scenarioMgr.SaveTarget(taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
@ -54,9 +54,9 @@ namespace CounterDrone.Unity
|
||||
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
|
||||
|
||||
var detail = scenarioMgr.GetDetail(taskId);
|
||||
var droneGroup = new DroneGroup
|
||||
var droneGroup = new DroneWave
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
Target = detail.Targets[0],
|
||||
Route = detail.Routes[0],
|
||||
Waypoints = detail.WaypointGroups["default"],
|
||||
@ -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<DroneWave> { droneGroup }, detail.Scene, new List<DetectionSource>());
|
||||
scenarioMgr.SaveCloud(taskId, new CloudDispersal
|
||||
{
|
||||
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
|
||||
|
||||
@ -53,7 +53,7 @@ namespace CounterDrone.Unity
|
||||
});
|
||||
scenario.SaveTarget(taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = _droneSpeed, TypicalAltitude = 500,
|
||||
});
|
||||
@ -68,9 +68,9 @@ namespace CounterDrone.Unity
|
||||
var detail = scenario.GetTaskDetail(taskId);
|
||||
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
|
||||
var planner = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths));
|
||||
var droneGroup = new DroneGroup
|
||||
var droneGroup = new DroneWave
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
Target = detail.Targets[0],
|
||||
Route = detail.Routes[0],
|
||||
Waypoints = detail.WaypointGroups["default"],
|
||||
@ -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<DroneWave> { droneGroup }, detail.Scene, new List<DetectionSource>());
|
||||
scenario.SaveCloudDispersal(taskId, new CloudDispersal
|
||||
{
|
||||
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
|
||||
|
||||
@ -31,12 +31,12 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
var db = _dbManager.OpenMainDb();
|
||||
|
||||
// 验证所有 12 张表存在
|
||||
// 验证所有 11 张表存在
|
||||
var tables = new[]
|
||||
{
|
||||
"ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene",
|
||||
"ControlZone", "TargetConfig", "EquipmentDeployment",
|
||||
"CloudDispersal", "RoutePlan", "Waypoint", "Group",
|
||||
"CloudDispersal", "RoutePlan", "Waypoint",
|
||||
"SimulationReport"
|
||||
};
|
||||
|
||||
|
||||
@ -37,12 +37,12 @@ namespace CounterDrone.Core.Tests
|
||||
AmmoTypes = new() { AerosolType.InertGas },
|
||||
};
|
||||
|
||||
private static DroneGroup MakeThreat(PowerType power = PowerType.Piston, float speed = 120f,
|
||||
private static DroneWave MakeThreat(PowerType power = PowerType.Piston, float speed = 120f,
|
||||
float startX = 0, float endX = 10000, float alt = 500)
|
||||
{
|
||||
var t = new DroneGroup
|
||||
var t = new DroneWave
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
Target = new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston, PowerType = (int)power,
|
||||
@ -57,9 +57,9 @@ namespace CounterDrone.Core.Tests
|
||||
return t;
|
||||
}
|
||||
|
||||
private PlannerResult Plan(List<FireUnit> units, DroneGroup threat,
|
||||
private PlannerResult Plan(List<FireUnit> units, DroneWave 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()
|
||||
@ -291,11 +291,11 @@ namespace CounterDrone.Core.Tests
|
||||
[Fact]
|
||||
public void MultiThreat_BothEngaged()
|
||||
{
|
||||
var a = MakeThreat(PowerType.Piston, 120); a.GroupId = "g0";
|
||||
var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1";
|
||||
var a = MakeThreat(PowerType.Piston, 120); a.WaveId = "g0";
|
||||
var b = MakeThreat(PowerType.Jet, 300); b.WaveId = "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);
|
||||
}
|
||||
|
||||
@ -405,9 +405,9 @@ namespace CounterDrone.Core.Tests
|
||||
// Z 向航路:从 (5000,0) 飞向 (5000,10000),即沿 +Z 方向
|
||||
// 之前 offset 写死 X 轴时,多发云团会沿 X 发散到 5000±N×spacing,偏离航路
|
||||
// 改用 RouteGeometry 后,offset 沿航路切向(+Z),云团应集中在 X=5000
|
||||
var threat = new DroneGroup
|
||||
var threat = new DroneWave
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
Target = new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,7 +49,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston,
|
||||
Quantity = 1,
|
||||
TypicalSpeed = 100,
|
||||
@ -92,7 +92,7 @@ namespace CounterDrone.Core.Tests
|
||||
});
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston,
|
||||
Quantity = 1,
|
||||
TypicalSpeed = 600, // 高速对抗强风
|
||||
@ -131,7 +131,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.HighSpeed,
|
||||
Quantity = 1,
|
||||
TypicalSpeed = 500,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -110,7 +114,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.FixedWing,
|
||||
PowerType = (int)PowerType.Jet,
|
||||
Quantity = 1,
|
||||
@ -143,7 +147,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Electric,
|
||||
PowerType = (int)PowerType.Electric,
|
||||
Quantity = 1,
|
||||
@ -185,7 +189,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston,
|
||||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1,
|
||||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
@ -269,7 +273,7 @@ namespace CounterDrone.Core.Tests
|
||||
});
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston,
|
||||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1,
|
||||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
@ -329,7 +333,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.HighSpeed,
|
||||
PowerType = (int)PowerType.Jet,
|
||||
Quantity = 1,
|
||||
@ -373,7 +377,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1,
|
||||
@ -439,7 +443,7 @@ namespace CounterDrone.Core.Tests
|
||||
});
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1,
|
||||
@ -498,7 +502,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston,
|
||||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 3,
|
||||
TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
@ -553,7 +557,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenario.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default", TargetType = (int)TargetType.Piston,
|
||||
WaveId = "default", TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
Quantity = 3,
|
||||
TypicalSpeed = 150, TypicalAltitude = 500,
|
||||
@ -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
|
||||
{
|
||||
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.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
|
||||
{
|
||||
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
|
||||
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
|
||||
});
|
||||
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using SQLite;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class GroupRepositoryTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
private readonly SQLiteConnection _db;
|
||||
private readonly GroupRepository _repo;
|
||||
|
||||
public GroupRepositoryTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}");
|
||||
var paths = new TestPathProvider(_testDir);
|
||||
var dbManager = new DatabaseManager(paths);
|
||||
_db = dbManager.OpenMainDb();
|
||||
_repo = new GroupRepository(_db);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Close();
|
||||
if (Directory.Exists(_testDir))
|
||||
Directory.Delete(_testDir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Insert_And_GetById_ReturnsGroup()
|
||||
{
|
||||
var group = new Group
|
||||
{
|
||||
Name = "Alpha Fleet",
|
||||
GroupType = (int)GroupType.DroneFleet,
|
||||
Description = "Test drone formation"
|
||||
};
|
||||
|
||||
_repo.Insert(group);
|
||||
|
||||
var result = _repo.GetById(group.Id);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Alpha Fleet", result.Name);
|
||||
Assert.Equal((int)GroupType.DroneFleet, result.GroupType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetByType_FiltersCorrectly()
|
||||
{
|
||||
_repo.Insert(new Group { Name = "Fleet A", GroupType = (int)GroupType.DroneFleet });
|
||||
_repo.Insert(new Group { Name = "Fleet B", GroupType = (int)GroupType.DroneFleet });
|
||||
_repo.Insert(new Group { Name = "Equip G1", GroupType = (int)GroupType.EquipmentGroup });
|
||||
|
||||
var fleets = _repo.GetByType((int)GroupType.DroneFleet);
|
||||
Assert.Equal(2, fleets.Count);
|
||||
|
||||
var equipGroups = _repo.GetByType((int)GroupType.EquipmentGroup);
|
||||
Assert.Single(equipGroups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delete_RemovesGroup()
|
||||
{
|
||||
var group = new Group { Name = "ToDelete" };
|
||||
_repo.Insert(group);
|
||||
_repo.Delete(group.Id);
|
||||
|
||||
Assert.Null(_repo.GetById(group.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Services;
|
||||
using SQLite;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class GroupServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _testDir;
|
||||
private readonly SQLiteConnection _db;
|
||||
private readonly IGroupService _service;
|
||||
|
||||
public GroupServiceTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"cd_test_{Guid.NewGuid():N}");
|
||||
var paths = new TestPathProvider(_testDir);
|
||||
var dbManager = new DatabaseManager(paths);
|
||||
_db = dbManager.OpenMainDb();
|
||||
_service = new GroupService(new Repository.GroupRepository(_db));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db?.Close();
|
||||
if (Directory.Exists(_testDir))
|
||||
Directory.Delete(_testDir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGroup_Valid_ReturnsGroup()
|
||||
{
|
||||
var group = _service.CreateGroup("Alpha", GroupType.DroneFleet, "Test fleet");
|
||||
|
||||
Assert.NotNull(group);
|
||||
Assert.Equal("Alpha", group.Name);
|
||||
Assert.NotEmpty(group.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGroup_EmptyName_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
_service.CreateGroup("", GroupType.DroneFleet, ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetGroups_WithTypeFilter()
|
||||
{
|
||||
_service.CreateGroup("Fleet A", GroupType.DroneFleet, "");
|
||||
_service.CreateGroup("Fleet B", GroupType.DroneFleet, "");
|
||||
_service.CreateGroup("Equip X", GroupType.EquipmentGroup, "");
|
||||
|
||||
var fleets = _service.GetGroups(GroupType.DroneFleet);
|
||||
Assert.Equal(2, fleets.Count);
|
||||
|
||||
var all = _service.GetGroups(null);
|
||||
Assert.Equal(3, all.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteGroup_RemovesGroup()
|
||||
{
|
||||
var group = _service.CreateGroup("DeleteMe", GroupType.DroneFleet, "");
|
||||
_service.DeleteGroup(group.Id);
|
||||
|
||||
Assert.Null(_service.GetGroup(group.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetGroup_ById_ReturnsCorrectGroup()
|
||||
{
|
||||
var created = _service.CreateGroup("Target", GroupType.EquipmentGroup, "desc");
|
||||
var fetched = _service.GetGroup(created.Id);
|
||||
|
||||
Assert.Equal("Target", fetched.Name);
|
||||
Assert.Equal("desc", fetched.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,8 @@ namespace CounterDrone.Core.Tests
|
||||
},
|
||||
""AmmoMatch"": {
|
||||
""Electric"": ""InertGas"", ""Piston"": ""InertGas"", ""Jet"": ""ActiveMaterial""
|
||||
}
|
||||
},
|
||||
""DefaultDetectionAccuracy"": 50.0
|
||||
}";
|
||||
|
||||
[Fact]
|
||||
|
||||
@ -103,7 +103,7 @@ namespace CounterDrone.Core.Tests
|
||||
|
||||
_service.SaveTarget(task.Id, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.FixedWing,
|
||||
Quantity = 3,
|
||||
});
|
||||
@ -201,7 +201,7 @@ namespace CounterDrone.Core.Tests
|
||||
var task = _service.CreateTask("Target Test", "");
|
||||
_service.SaveTarget(task.Id, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston,
|
||||
Quantity = 5,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
@ -236,7 +236,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1,
|
||||
DetectionRadius = 5000.0,
|
||||
RadarRange = 5000.0,
|
||||
},
|
||||
};
|
||||
|
||||
@ -394,7 +394,7 @@ namespace CounterDrone.Core.Tests
|
||||
// 4. 步骤2:目标配置
|
||||
_service.SaveTarget(task.Id, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.HighSpeed,
|
||||
Quantity = 2,
|
||||
PowerType = (int)PowerType.Jet,
|
||||
@ -409,7 +409,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1,
|
||||
DetectionRadius = 6000.0,
|
||||
RadarRange = 6000.0,
|
||||
},
|
||||
new EquipmentDeployment
|
||||
{
|
||||
@ -484,5 +484,81 @@ namespace CounterDrone.Core.Tests
|
||||
var searchResult = _service.SearchTasks("完整流程", null, null, 1, 10);
|
||||
Assert.Equal(1, searchResult.TotalCount);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 探测设备独立 CRUD
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AddDetection_PersistsAndQueryable()
|
||||
{
|
||||
var task = _service.CreateTask("探测设备测试", "");
|
||||
_service.AddDetection(task.Id, new EquipmentDeployment
|
||||
{
|
||||
Quantity = 1,
|
||||
PositionX = 5000, PositionY = 0, PositionZ = 0,
|
||||
RadarRange = 8000, EORange = 4000, IRRange = 3000,
|
||||
DetectionAccuracy = 50,
|
||||
});
|
||||
|
||||
var detections = _service.GetDetections(task.Id);
|
||||
Assert.Single(detections);
|
||||
Assert.Equal((int)EquipmentRole.Detection, detections[0].EquipmentRole);
|
||||
Assert.Equal(8000.0, detections[0].RadarRange);
|
||||
Assert.Equal(50.0, detections[0].DetectionAccuracy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDetections_FiltersOutLaunchPlatforms()
|
||||
{
|
||||
var task = _service.CreateTask("探测过滤测试", "");
|
||||
// 一个火力单元
|
||||
_service.SaveDeployment(task.Id, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
Quantity = 1, PositionX = 1000,
|
||||
},
|
||||
});
|
||||
// 两个探测设备(独立添加)
|
||||
_service.AddDetection(task.Id, new EquipmentDeployment { RadarRange = 5000 });
|
||||
_service.AddDetection(task.Id, new EquipmentDeployment { EORange = 3000 });
|
||||
|
||||
var detections = _service.GetDetections(task.Id);
|
||||
Assert.Equal(2, detections.Count); // 只有探测设备,不含火力单元
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteDetection_RemovesOnlyOne()
|
||||
{
|
||||
var task = _service.CreateTask("探测删除测试", "");
|
||||
_service.AddDetection(task.Id, new EquipmentDeployment { RadarRange = 5000 });
|
||||
var det2 = new EquipmentDeployment { EORange = 3000 };
|
||||
_service.AddDetection(task.Id, det2);
|
||||
|
||||
_service.DeleteDetection(det2.Id);
|
||||
|
||||
var detections = _service.GetDetections(task.Id);
|
||||
Assert.Single(detections);
|
||||
Assert.Equal(5000.0, detections[0].RadarRange);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTaskDetail_IncludesDetections()
|
||||
{
|
||||
var task = _service.CreateTask("想定含探测", "");
|
||||
_service.SaveDeployment(task.Id, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.LaunchPlatform, Quantity = 1 },
|
||||
});
|
||||
_service.AddDetection(task.Id, new EquipmentDeployment { RadarRange = 10000 });
|
||||
|
||||
var detail = _service.GetTaskDetail(task.Id);
|
||||
// Equipment 包含火力单元 + 探测设备
|
||||
Assert.Equal(2, detail.Equipment.Count);
|
||||
Assert.Contains(detail.Equipment, e => e.EquipmentRole == (int)EquipmentRole.Detection);
|
||||
Assert.Contains(detail.Equipment, e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
|
||||
_scenarioService.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
GroupId = "default",
|
||||
WaveId = "default",
|
||||
TargetType = (int)TargetType.Piston,
|
||||
Quantity = 1,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
@ -147,7 +147,7 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
var task = _scenarioService.CreateTask("t", "");
|
||||
_scenarioService.SaveScene(task.Id, new CombatScene());
|
||||
_scenarioService.SaveTarget(task.Id, new TargetConfig { GroupId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
|
||||
_scenarioService.SaveTarget(task.Id, new TargetConfig { WaveId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
|
||||
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
|
||||
{
|
||||
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user