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