Compare commits

..

No commits in common. "56d345189eb4f87d689127bf1dc4f773a934e2a5" and "3028a421b506925b2e38d8a8ab3807fb4c3c57da" have entirely different histories.

48 changed files with 544 additions and 948 deletions

View File

@ -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 → DefensePlannerSaveRoute 签名更新
- 仿真器实体事件映射BuildDroneGroups → BuildDroneWaves
### Removed
- 7 个文件Group.cs / GroupRepository.cs / IGroupService.cs / GroupService.cs / GroupManager.cs / GroupServiceTests.cs / GroupRepositoryTests.cs
### Metrics
- 测试 208 → **204**4删除 Group 相关测试),全量通过 63s
- 0 编译错误0 编译警告
---
## [0.6.0] - 2026-06-15
### Added — 探测驱动的规划8.1.1
- **DetectionCalculator 静态工具类**:探测能力评估的唯一实现(与 Kinematics/RouteGeometry 同范式)。光电受 Visibility 衰减(`有效=基准×min(1,Visibility/基准)`),雷达/红外不受影响;统一信息网络找最早探测点(线段-圆求交)
- **探测源模型**DetectionSource独立探测设备 + 火力单元自带探测统一表达(雷达/光电/红外三距离 + 精度)
- **EquipmentDeployment 扩展**:删单一 `DetectionRadius`,加 `RadarRange/EORange/IRange/DetectionAccuracy` 四字段
- **FireUnit 探测字段激活**BuildFireUnits 从 EquipmentDeployment 读取并赋值(原为死代码)
- **planner 接口扩展**`IDefensePlanner.Plan` 加第 4 参数 `detectionSources`
- **PlannerConfig 加 DefaultDetectionAccuracy**(无探测时回退精度)
- 单元测试DetectionCalculator 13 项、探测驱动规划 2 项
### Changed — planner 基于探测信息规划
- planner 假设威胁从探测边界被发现,到达时间 = (拦截弧长 探测弧长)/速度,而非上帝视角的航路起点
- SimulationEngine.Initialize 构建 `List<DetectionSource>`(独立探测 + 火力单元自带),传入 planner
- 报告探测设备表用新字段(雷达/光电/红外/精度)
### Fixed
- Solve 健壮性GenerateFireEventsAt 返回空时(探测边界太靠后来不及拦截)不再 IndexOutOfRange
### Metrics
- 测试 193 → **208**+15全量通过 64s
- 0 编译警告
---
## [0.5.0] - 2026-06-14
### Added — 物理模型统一架构

View File

@ -1 +1 @@
0.6.0
0.5.0

View File

@ -13,6 +13,5 @@
"Electric": "InertGas",
"Piston": "InertGas",
"Jet": "ActiveMaterial"
},
"DefaultDetectionAccuracy": 50.0
}
}

View File

@ -1,11 +1,11 @@
# 反无人机仿真系统 — 总体架构设计
> **版本**V10
> **日期**2026-06-15
> **版本**V9
> **日期**2026-06-13
> **状态**:已实现
> **Unity 版本**22.3.62
> **.NET 版本**.NET Standard 2.1
> **变更**Group 表移除GroupId → WaveIdDroneGroup → DroneWave编组概念升级为批次+火力单元
> **变更**DefensePlanner 五步规划引擎FireUnit 通道模型空基平台飞控SQLite domain reload 修复;删除所有 fallback 默认值
---
@ -17,7 +17,7 @@
| 模块 | 核心功能 |
|------|----------|
| **模型管理** | 3D 模型导入/预览/删除 |
| **模型管理** | 3D 模型导入/预览/删除,编组管理 |
| **想定管理** | 仿真任务 CRUD步骤化配置向导管控区域设置搜索分页 |
| **仿真报告** | 报告列表,含时序图的完整报告预览,导出 PDF/Word删除 |
@ -41,12 +41,12 @@
├──────────────────────────────────────────────────────────┤
│ Unity Application Layer │
│ ModelPanel | ScenarioWizard | SimulationRunner | │
│ ReplayController | ReportPanel
│ ReplayController | ReportPanel | GroupManager
├──────────────────────────────────────────────────────────┤
│ Service Layer (Pure C#) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ModelService │ │ScenarioService│ │ SimulationEngine │ │
│ │ │ │ DefensePlanner │ │ ├ DroneEntity │ │
│ │ GroupService │ │ DefensePlanner │ │ ├ DroneEntity │ │
│ └──────────────┘ └──────────────┘ │ ├ PlatformEntity │ │
│ ┌──────────────┐ ┌──────────────┐ │ ├ MunitionEntity │ │
│ │FrameDataStore│ │ReportService │ │ ├ CloudEntity │ │
@ -199,19 +199,9 @@ enum TriggerMode { Time = 0, Area = 1, Manual = 2 }
// === 毁伤状态阶段(输出到 StateData 供 Unity 渲染)===
enum DamageStage { Normal = 0, EngineAnomaly = 1, AttitudeLoss = 2, Destroyed = 3 }
// === 编队 ===
// === 编队 & 编组 ===
enum FormationMode { Single = 0, Formation = 1, Swarm = 2 }
// === 核心作战概念 ===
// 火力单元FireUnit能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。
// 包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环系统。
// 配置层 = EquipmentDeployment(LaunchPlatform),含探测字段 + 打击字段。
// 独立探测节点DetectionNode只探测不打击的侦查节点如前沿警戒雷达
// 配置层 = EquipmentDeployment(Detection),仅含探测字段。
// 两者都往统一信息网络送探测信息planner 基于融合后的探测边界规划。
// 无人机批次DroneWave有共同航路的一组无人机攻击方
// 批次关联航路但不独占(多个批次可共享同一条航路)。
// 配置层 = TargetConfig(含 WaveId) + RoutePlan(TaskId, WaveId) + Waypoints。
enum GroupType { DroneFleet = 0, EquipmentGroup = 1 }
// === 运行时 ===
enum EntityType { Drone = 0, Platform = 1, DetectionEquip = 2, Cloud = 3, Munition = 4 }
@ -264,11 +254,12 @@ CounterDroneBackend_Data/
SimTask — 仿真任务主表
CombatScene — 步骤1作战场景含24h时间、天气
ControlZone — 步骤1+:管控区域/电子围栏NEW
TargetConfig — 步骤2目标配置(含 WaveId 关联批次)
EquipmentDeployment — 步骤3装备部署火力单元含探测+打击,或独立探测节点
TargetConfig — 步骤2目标配置
EquipmentDeployment — 步骤3装备部署搭载平台+探测设备
CloudDispersal — 步骤4云团抛撒配置
RoutePlan — 步骤5航路规划1:N SimTask批次批次关联但不独占航路FK 关联 TargetConfig.WaveId
RoutePlan — 步骤5航路规划1:N SimTask编队
Waypoint — 航路点
Group — 编组(独立管理)
SimulationReport — 仿真报告
SimEvent — 仿真事件(时序图数据源)
@ -276,12 +267,6 @@ CounterDroneBackend_Data/
SimFrameRecord — 逐帧位置记录(回放用)
```
> **核心作战概念**
> - **火力单元FireUnit**:能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环。配置层 = `EquipmentDeployment(LaunchPlatform)`
> - **独立探测节点DetectionNode**:只探测不打击的侦查节点(如前沿警戒雷达)。配置层 = `EquipmentDeployment(Detection)`
> - **无人机批次DroneWave**:有共同航路的一组无人机(攻击方)。批次关联航路但不独占(多个批次可共享同一条航路)。配置层 = `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints`
> - 原来的 `Group`(编组)表已移除:无人机编队概念升级为"批次",装备编组概念不再需要(火力单元是独立作战单位)。
### 4.3 业务表详细设计
#### ModelInfo — 模型元数据
@ -352,7 +337,7 @@ CounterDroneBackend_Data/
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| WaveId | TEXT | FK | 关联无人机批次(同一 WaveId 的无人机共享航路) |
| GroupId | TEXT | FK | 关联无人机编队 Group |
| TargetType | INTEGER | | 0旋翼 1固定翼 2电推 3活塞 4高速 |
| Quantity | INTEGER | DEFAULT 1 | 目标数量 |
| PowerType | INTEGER | | 0电推 1活塞 2喷吸气 |
@ -370,31 +355,28 @@ CounterDroneBackend_Data/
| 活塞 | 2.5 | 120 | 800 |
| 高速目标 | 1.5 | 300 | 2000 |
#### EquipmentDeployment — 步骤3装备部署火力单元 + 独立探测节点
#### EquipmentDeployment — 步骤3装备部署重构
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| EquipmentRole | INTEGER | | 0=独立探测节点 1=火力单元(发射平台) |
| EquipmentRole | INTEGER | | 0=探测设备 1=发射平台 |
| Quantity | INTEGER | DEFAULT 1 | 部署数量 |
| WaveId | TEXT | NULLABLE | 可选关联批次(用于 UI 分组,火力单元是独立作战单位,不强依赖批次 |
| // 以下为火力单元(发射平台专用字段 |
| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹)独立探测节点为 NULL |
| GroupId | TEXT | FK | 关联装备编组 Group火力单元多批次时用于分配 |
| // 以下为发射平台专用字段 |
| PlatformType | INTEGER | NULLABLE | 0=空基(大型无人机) 1=地基(炮弹)探测设备为 NULL |
| PositionX | REAL | | 部署位置 |
| PositionY | REAL | | |
| PositionZ | REAL | | |
| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,独立探测节点为 NULL |
| AerosolType | INTEGER | NULLABLE | 挂载气溶胶类型,探测设备为 NULL |
| MunitionCount | INTEGER | NULLABLE | 挂载弹药数量 |
| MuzzleVelocity | REAL | NULLABLE | **NEW** 弹药初速 m/s地基炮弹用 |
| ReleaseAltitude | REAL | NULLABLE | **NEW** 弹药释放高度 m空基平台用 |
| Cooldown | REAL | DEFAULT 5 | 发射冷却时间 s同弹种连发间隔 |
| AmmoChangeTime | REAL | DEFAULT 300 | **NEW** 更换弹种所需时间 s默认 5 分钟) |
| // 探测能力(火力单元自带 + 独立探测节点均有) |
| RadarRange | REAL | NULLABLE | 雷达探测距离 m雨雾不衰减 |
| EORange | REAL | NULLABLE | 光电探测距离 m受 Visibility 衰减) |
| IRRange | REAL | NULLABLE | 红外探测距离 m不受 Visibility 影响) |
| DetectionAccuracy | REAL | NULLABLE | 探测精度 m位置误差影响抛撒散布范围 |
| // 以下为探测设备专用字段 |
| DetectionRadius | REAL | NULLABLE | 探测半径 m |
> **装备体系说明**
> - **搭载平台**2 种):
@ -433,12 +415,12 @@ CounterDroneBackend_Data/
#### RoutePlan — 步骤5航路规划
> **支持多批次**:通过 `TaskId + WaveId` 唯一标识一条航路,一个任务可有多个批次各自独立航路。
> **支持多批次**:通过 `TaskId + GroupId` 唯一标识一条航路,一个任务可有多个编队各自独立航路。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| TaskId | TEXT | PK(复合) | |
| WaveId | TEXT | PK(复合) | 关联批次:同一任务不同批次可有独立航路(多个批次可共享同一条航路) |
| GroupId | TEXT | PK(复合), FK → Group | 关联编队:同一任务不同编队可有独立航路 |
| FormationMode | INTEGER | | 0单机 1编队 2蜂群 |
| FormationSpacing | REAL | DEFAULT 50 | m |
| ETA | TEXT | | 预计到达时间 |
@ -456,13 +438,15 @@ CounterDroneBackend_Data/
| Altitude | REAL | | 该航点飞行高度 m |
| Speed | REAL | | 该段速度 km/h |
#### ~~Group~~ — 编组(已移除)
#### Group — 编组
> **概念升级**:原 Group 表的两种类型已分别升级:
> - `DroneFleet`(无人机编队)→ **无人机批次DroneWave**:由 `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints` 隐式表达。批次关联航路但不独占(多个批次可共享同一条航路)。
> - `EquipmentGroup`(装备编组)→ **不再需要**。火力单元是独立作战的最小单位,不需要上层编组。
>
> 数据层:`Group` 表保留但不再主动使用(向后兼容)。`TargetConfig.WaveId` 语义为"批次 ID"。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| Name | TEXT | NOT NULL | 编组名称 |
| GroupType | INTEGER | | 0=无人机编队 1=装备编组 |
| Description | TEXT | | 备注 |
| CreatedAt | TEXT | | |
### 4.4 运行时表
@ -523,7 +507,7 @@ SimulationEngine
├── SimulationState ← 状态(运行/暂停/结束)
├── SceneConfig ← 配置快照(含场景边界、管控区域)
├── List<DroneEntity> ← 无人机(目标方)
│ └── 按批次组织,共享航路
│ └── 按编组组织,共享航路
├── List<PlatformEntity> ← 搭载平台(发射方)★重构
│ ├── AirBasedPlatform ← 空基(大型无人机)
│ └── GroundBasedPlatform ← 地基(火炮)
@ -629,7 +613,7 @@ Unity 渲染毁伤效果(尾焰异常、姿态失控、坠落)
```
属性:
- 批次 WaveId、航路点队列 Waypoint[]
- 编组 GroupId、航路点队列 Waypoint[]
- 当前位置、速度、朝向
- HP0~1 归一化)
- DamageStageNormal → EngineAnomaly → AttitudeLoss → Destroyed
@ -1096,7 +1080,7 @@ planner 所有策略参数从 `data/planner_config.json` 读取,代码零默
// === 防御规划 ===
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 等模型见本节上文
@ -1281,7 +1265,7 @@ public interface IFrameDataStore
| 阶段 | 内容 | 数据来源 |
|------|------|------|
| **仿真前** | 我方配置(装备类型/数量、火力单元部署、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
| **仿真前** | 我方配置(装备类型/数量/编组、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
| **仿真中** | 关键事件时序表、各实体轨迹概要、云团演化数据 | SimEvent + SimFrameRecord |
| **仿真后** | 拦截成功率、各阶段耗时、毁伤效果评估、对抗结果判定 | SimEvent 汇总统计 |
@ -1313,6 +1297,14 @@ interface IModelService
ModelInfo GetModel(string id);
}
// === 编组管理 ===
interface IGroupService
{
Group CreateGroup(string name, GroupType type, string description);
void DeleteGroup(string id);
List<Group> GetGroups(GroupType? type);
}
// === 想定管理 ===
interface IScenarioService
{
@ -1374,7 +1366,7 @@ interface IReportService
### 数据模型
一个仿真任务可包含**多种类型、多批次**的无人机,每批次有独立的航路:
一个仿真任务可包含**多种类型、多批次**的无人机,每批次(编队)有独立的航路:
```
SimTask "城市防御演习"
@ -1382,51 +1374,51 @@ SimTask "城市防御演习"
├── ControlZone[](共享)
├── CloudDispersal共享云团参数具体拦截方案由算法确定
├── 批次 A活塞×3 ← WaveId
│ ├── TargetConfigWaveId=A类型=活塞,数量=3
│ ├── RoutePlanTaskId + WaveId=A航路 A北→南
├── 编队 A活塞×3 ← Group.Id
│ ├── TargetConfig类型=活塞,数量=3
│ ├── RoutePlan航路 A北→南
│ └── Waypoint[]
├── 批次 B喷气×2 ← WaveId
│ ├── TargetConfigWaveId=B类型=高速,数量=2
│ ├── RoutePlanTaskId + WaveId=B航路 B西→东
├── 编队 B喷气×2 ← Group.Id
│ ├── TargetConfig类型=高速,数量=2
│ ├── RoutePlan航路 B西→东
│ └── Waypoint[]
└── 批次 C旋翼×5 ← WaveId
├── TargetConfigWaveId=C类型=旋翼,数量=5
├── RoutePlanTaskId + WaveId=C航路 C
└── 编队 C旋翼×5 ← Group.Id
├── TargetConfig类型=旋翼,数量=5
├── RoutePlan航路 C
└── Waypoint[]
```
**关键变更**`RoutePlan` 主键`(TaskId, WaveId)` 复合键。`Waypoint` 通过 `(TaskId, WaveId)` 关联航路
**关键变更**`RoutePlan` 主键`TaskId` 改为 `(TaskId, GroupId)` 复合键
### 推荐算法
算法需要同时考虑**所有无人机批次**和**可用火力单元**,自动匹配分配:
算法需要同时考虑**所有无人机编队**和**可用装备火力单元**,自动匹配分配:
```csharp
// 输入:所有无人机编队 + 已部署的火力单元
// 输出:每个编队的拦截方案 + 合并的发射计划
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 solution = advisor.RecommendForWave(droneWave, environment, availableUnits);
// 从可用装备中选择合适的火力单元(弹药类型匹配目标动力类型)
var availableUnits = fireUnits.Where(u => !usedPlatforms.Contains(u.Id)).ToList();
var solution = advisor.RecommendForGroup(droneGroup, environment, availableUnits);
// 标记占用的火力单元
foreach (var idx in solution.AssignedFireUnitIds)
usedFireUnits.Add(idx);
// 标记占用的平台(一组装备可同时对付一个编队)
foreach (var idx in solution.AssignedPlatformIndices)
usedPlatforms.Add(idx);
allSolutions.Add(solution);
}
// 复用判定(不写死"一对一"或"一对多"
// 同种弹药 → 只需冷却时间(秒级),可直接复用
// 异种弹药 → 需要 AmmoChangeTime默认 5 分钟),检查批次间隔是否够
// 异种弹药 → 需要 AmmoChangeTime默认 5 分钟),检查编队间隔是否够
// 弹药耗尽 → 不可复用
var merged = MergeFireSchedules(allSolutions);
@ -1443,13 +1435,13 @@ engine.SetFireSchedule(merged);
| 表 | 变更 |
|------|------|
| `RoutePlan` | PK 为 (TaskId, WaveId) 复合键 |
| `TargetConfig` | 不变(已有 WaveId |
| `Waypoint` | 关联 `(TaskId, WaveId)` 定位航路 |
| `RoutePlan` | PK 改为 (TaskId, GroupId) 复合键 |
| `TargetConfig` | 不变(已有 GroupId FK |
| `Waypoint` | 关联 `(TaskId, GroupId)` 定位航路 |
`IScenarioService` `SaveRoute` 签名
`IScenarioService` 新增
```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
## 编队汇总
| 批次 | 目标 | 数量 | 拦截方案 | 结果 |
| 编队 | 目标 | 数量 | 拦截方案 | 结果 |
|------|------|------|------|------|
| 批次A | 活塞 | 3 | 惰性气体 12发 | ✅ 全毁 |
| 批次B | 喷气 | 2 | 活性材料 8发 | ⚠️ 1逃脱 |
| 编队A | 活塞 | 3 | 惰性气体 12发 | ✅ 全毁 |
| 编队B | 喷气 | 2 | 活性材料 8发 | ⚠️ 1逃脱 |
```
---
@ -1481,7 +1473,7 @@ void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> wps
│ └─ 多边形绘制 / 编辑 / 删除 │
├────────────────────────────────────────────────────────┤
│ 步骤 2目标配置 │
│ ├─ 批次选择(已有批次/新建批次)
│ ├─ 编组选择(已有编队/新建编队)
│ ├─ 目标类型选择(自动填充默认参数) │
│ ├─ 数量 / 动力类型 / 翼展 / 速度 / 高度 │
│ └─ 预设典型目标库NEW
@ -1489,7 +1481,7 @@ void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> wps
│ 步骤 3装备部署重构
│ ├─ 装备角色(探测设备 / 发射平台) │
│ ├─ 发射平台类型(空基 / 地基 NEW
│ ├─ 数量 / 部署位置 │
│ ├─ 数量 / 编组 / 部署位置 │
│ ├─ 挂载气溶胶类型 / 弹药数量 │
│ ├─ 弹药初速 / 释放高度 / 冷却时间 │
│ └─ 探测半径(探测设备) │
@ -1529,7 +1521,7 @@ void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> wps
| 12 | 第三方数据 | **静态配置JSON → SQLite** | 仅提供弹药基础参数和初始扩散状态,不提供运行时算法 |
| 13 | 运动学 | 简化运动学 + 风偏 | 航路点线性插值 + 风速矢量 |
| 14 | 弹药弹道 | 抛物线(地基)/ 自由落体(空基) | 基于初速、角度、释放高度计算 |
| 15 | 批次 / 火力单元 | WaveId + 火力单元独立作战 | 批次关联航路但不独占;火力单元是最小独立作战单位,不需要上层编组 |
| 15 | 编组 | Group 独立实体 | 编组独立管理,任务中引用 |
| 16 | 管控区域 | ControlZone 表 + Tick 判定 | 电子围栏,侵入即任务失败 |
| 17 | 配置向导流程 | **威胁驱动三阶段** | Phase1 定义威胁 → Phase2 算法推荐 → Phase3 审阅确认 |
| 18 | 报告导出 | 模板填充 → Markdown | PDF/Word 待调研 |

View File

@ -1,14 +1,14 @@
# DefensePlanner 防御规划引擎 — 技术方案
- **版本**V4
- **日期**2026-06-15
- **版本**V3
- **日期**2026-06-13
- **状态**:已实现
---
## 1. 概述
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁批次列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁编队列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
**当前问题**
@ -54,11 +54,11 @@ class FireUnit {
}
```
### 2.2 DroneWave威胁批次
### 2.2 DroneGroup威胁编队
```csharp
class DroneWave {
string WaveId; // 批次 ID
class DroneGroup {
string GroupId; // 编队 ID
TargetConfig Target; // 类型、数量、动力、翼展、速度、高度
List<Waypoint> Waypoints; // 航路点
float ArrivalTime; // 预计算:到达防御区域中点的时间 s
@ -78,7 +78,7 @@ class DroneWave {
```csharp
class UnitAssignment {
string FireUnitId; // 分配到的火力单元
string DroneWaveId; // 对抗的威胁批次
string DroneGroupId; // 对抗的威胁编队
AerosolType AmmoType; // 装填的弹药类型
int RoundsFired; // 本次发射几发
float FirstFireTime; // 首发发射时机 s
@ -113,7 +113,7 @@ class PlannerResult {
## 4. 内部流程(五步法)
```
输入List<FireUnit> + List<DroneWave> + CombatScene
输入List<FireUnit> + List<DroneGroup> + CombatScene
Step 1 — 威胁排序
@ -190,7 +190,7 @@ class InterceptCandidate {
**候选生成逻辑**
对于给定的威胁批次和火力单元:
对于给定的威胁编队和火力单元:
1. **弹药兼容性**`Unit.AmmoTypes` 包含威胁需要的弹药类型
2. **地基可达性**:计算目标与部署点的水平距离,校验 `MuzzleVelocity` 射程
@ -239,7 +239,7 @@ for each threat in pending:
> 作用:给操作员一个置信区间——最优 vs 临界,展示"再少就不够了"的底线。
### 5.5 多批次合并
### 5.5 多编队合并
```
Step 4 的贪心算法天然支持多威胁:
@ -258,7 +258,7 @@ Step 4 的贪心算法天然支持多威胁:
public interface IDefensePlanner
{
/// <summary>为给定火力单元池和威胁列表生成规划方案</summary>
PlannerResult Plan(List<FireUnit> fireUnits, List<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 PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene env)
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene env)
{
// Step 1-5
}
// 内部方法
private List<DroneWave> Prioritize(List<DroneWave> threats) { ... }
private List<DroneGroup> Prioritize(List<DroneGroup> threats) { ... }
private AerosolType MatchAmmo(PowerType power) { ... }
private List<InterceptCandidate> GenerateCandidates(DroneWave threat, List<FireUnit> units, float now) { ... }
private DefensePlan Solve(List<DroneWave> threats, List<FireUnit> units) { ... }
private List<InterceptCandidate> GenerateCandidates(DroneGroup threat, List<FireUnit> units, float now) { ... }
private DefensePlan Solve(List<DroneGroup> threats, List<FireUnit> units) { ... }
private DefensePlan DeriveCritical(DefensePlan best) { ... }
}
```
@ -293,7 +293,7 @@ public class DefaultDefensePlanner : IDefensePlanner
|--------|--------|------|
| `IDefenseAdvisor.Recommend(ThreatProfile)` | `IDefensePlanner.Plan(fireUnits, threats, env)` | ✅ 已替换 |
| `IDefenseAdvisor.GetDefenseRecommendation(taskId)` | 不再需要 | ✅ 已删除 |
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多批次 | ✅ 已删除 |
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多编队 | ✅ 已删除 |
---
@ -304,7 +304,7 @@ SimulationEngine.Initialize(taskId)
│ // 引擎内部组装
├── BuildFireUnits(config) → List<FireUnit>
├── BuildDroneWaves(config) → List<DroneWave>
├── BuildDroneGroups(config) → List<DroneGroup>
└── _scene (CombatScene)

View File

@ -41,7 +41,7 @@
SimulationEngine.Initialize()
├─ BuildFireUnits() → List<FireUnit>
├─ BuildDroneWaves() → List<DroneWave>
├─ BuildDroneGroups() → List<DroneGroup>
IDefensePlanner.Plan(fireUnits, threats, scene)

View File

@ -1,8 +1,8 @@
# 实施计划与任务跟踪
> **项目**:反无人机仿真系统后端
> **文档版本**V1.4
> **更新日期**2026-06-15
> **文档版本**V1.3
> **更新日期**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.2 | 实现 `UnityPathProvider` | 1h | ✅ | `Application.persistentDataPath` 桥接 |
| 6.3 | 实现 `ModelManager` | 3h | ✅ | 导入/删除/查询,含 Verify |
| 6.4 | 实现 `ScenarioManager`5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多批次 Route |
| 6.4 | 实现 `ScenarioManager`5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多编队 Route |
| 6.5 | 实现 `SimulationRunner`Update 驱动) | 3h | ✅ | Tick 驱动 + 事件订阅 + 实体位置同步 + 炮弹轨迹可视化 |
| 6.6 | 实现 `ReplayController`(帧加载) | 3h | ✅ | 从分库加载帧数据TotalFrames/GetFrame |
| 6.7 | 实现 `ReportManager` | 2h | ✅ | 生成 + Markdown 导出 |
@ -173,7 +173,7 @@ Phase 8 🔄 待开发(天气/物理模型统一已完成)
| # | 功能 | 说明 |
|---|------|------|
| 8.1.1 | 探测设备搜索逻辑 | ✅ 事前规划已接入探测能力:`DetectionCalculator` 算统一信息网络最早探测点planner 基于探测边界非上帝视角起点算到达时间天气Visibility衰减光电探测距离探测精度影响抛撒散布。实时探测→火控链路留作未来增强 |
| 8.1.1 | 探测设备搜索逻辑 | `DetectionEntity` 类已存在,需接入 `SimulationEngine` 实现探测→火控链路闭环。天气(能见度/日夜)对光电/红外探测距离的衰减在此实现 |
| 8.1.2 | 蜂群运动模型 | `FormationMode.Swarm` 枚举已定义,需差异化行为(随机扰动、个体差异) |
| 8.1.3 | 空基平台 + DefensePlanner | ✅ 五步规划引擎,通道模型,物理间隔错发,路径积分毁伤判定 | ✅ |
| 8.1.4 | 预置典型目标库 | 具体无人机型号 JSON 配置(如 DJI Mavic 3、Shahed-136 等),导入 `TargetConfig` 默认值 |

View File

@ -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
- [ ] 数据库变更:新字段为 nullableSQLite 自动处理
- [ ] UI 变更
- [x] 文档变更:实施计划 8.1.1、CHANGELOG、VERSION
## 验收
- 全量测试 193 → **208**+1564s 通过
- DetectionCalculator 13 项单测(天气衰减/最早探测点/散布半径)
- 探测驱动规划 2 项测试planner 基于探测边界、集成场景不破坏)
- 范围外:实时探测→火控链路(未来增强)

View File

@ -1,7 +1,7 @@
# 后端对接文档Unity 前端)
> **版本**V1.2
> **日期**2026-06-15
> **版本**V1.1
> **日期**2026-06-12
> **Unity 版本**2022.3.62f3c1
---
@ -35,21 +35,17 @@ data/default_ammo.json ← 默认弹药参数(放到 persisten
var scenario = GetComponent<ScenarioManager>();
var task = scenario.CreateTask("拦截活塞式无人机", "");
scenario.SaveScene(task.Id, new CombatScene { WindSpeed = 3 });
scenario.SaveTarget(task.Id, new TargetConfig { WaveId = "default", PowerType = (int)PowerType.Piston, ... });
scenario.SaveRoute(task.Id, "default", routePlan, waypoints);
scenario.SaveTarget(task.Id, new TargetConfig { PowerType = (int)PowerType.Piston, ... });
scenario.SaveRoute(task.Id, routePlan, waypoints);
// 推荐方案(DefensePlanner 五步流水线
// 推荐方案(自动从数据库读弹药规格
var detail = scenario.GetDetail(task.Id);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList(); // DatabaseManager 已种子
var fireUnits = new List<FireUnit> { /* 从 EquipmentDeployment 构建 */ };
var threats = new List<DroneWave> { new DroneWave { WaveId = "default", Target = detail.Targets[0], Route = detail.Routes[0], Waypoints = detail.WaypointGroups["default"] } };
var plan = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, threats, detail.Scene, new List<DetectionSource>());
var rec = new DefaultDefenseAdvisor(ammoCatalog).Recommend(new ThreatProfile { ... });
// 应用到想定(发射计划传给引擎,部署方案保存到配置)
var mergedSchedule = plan.Best.MergedSchedule;
// 部署火力单元到 EquipmentDeployment...
scenario.SaveCloud(task.Id, new CloudDispersal { /* 从 plan.Best.Assignments 推算云团参数 */ });
scenario.SaveDeployment(task.Id, equips);
// 应用到想定
scenario.SaveCloud(task.Id, rec.Best.RecommendedCloud);
scenario.SaveDeployment(task.Id, rec.Best.Platforms.Select(p => new EquipmentDeployment { ... }).ToList());
```
### 3.2 启动仿真(事件驱动)
@ -58,7 +54,7 @@ scenario.SaveDeployment(task.Id, equips);
var runner = GetComponent<SimulationRunner>();
// 推荐方案的 FireSchedule 直接传给引擎
runner.Engine.SetFireSchedule(plan.Best.MergedSchedule);
runner.Engine.SetFireSchedule(rec.Best.FireSchedule);
runner.LoadAndStart(task.Id);
runner.Engine.TimeScale = 4f; // 加速
@ -97,7 +93,7 @@ mgr.SaveScene(id, combatScene);
mgr.SaveTarget(id, targetConfig);
mgr.SaveDeployment(id, equipmentList);
mgr.SaveCloud(id, cloudDispersal);
mgr.SaveRoute(id, "default", routePlan, waypoints);
mgr.SaveRoute(id, routePlan, waypoints);
TaskFullConfig detail = mgr.GetDetail(id);
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/implementation/tasks/实施计划与任务跟踪.md` |
| 测试报告 | `test/reports/`(每次集成测试自动生成) |
| 测试状态 | **204 测试95.4% 行覆盖率63 秒** |
| 测试状态 | **129 测试95.4% 行覆盖率12 秒** |

View File

@ -46,10 +46,10 @@ namespace CounterDrone.Core.Algorithms
public float MuzzleVelocity { get; set; } = 800f;
}
/// <summary>无人机批次(规划器输入)</summary>
public class DroneWave
/// <summary>无人机编队(规划器输入)</summary>
public class DroneGroup
{
public string WaveId { get; set; } = string.Empty;
public string GroupId { get; set; } = string.Empty;
public TargetConfig Target { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
@ -61,12 +61,6 @@ namespace CounterDrone.Core.Algorithms
/// <summary>综合优先级 = 威胁指数 / (到达时间 + 1)</summary>
public float Priority => ArrivalTime > -1 ? ThreatIndex / (ArrivalTime + 1f) : ThreatIndex;
/// <summary>最早探测弧长(米)。威胁航路进入探测范围的弧长位置。
/// 0 = 起点即可探测或无探测设备上帝视角。float.MaxValue = 探测不到。</summary>
public float DetectArc { get; set; }
/// <summary>探测精度(位置误差 m。影响 planner 抛撒散布范围。</summary>
public float DetectAccuracy { get; set; }
/// <summary>预计到达航路中点的时间(秒)。
/// 物理含义:匀速直线运动从航路起点到中点的飞行时间。</summary>
public float GetArrivalTime()
@ -120,28 +114,13 @@ namespace CounterDrone.Core.Algorithms
public class UnitAssignment
{
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 int RoundsFired { get; set; }
public float FirstFireTime { get; set; }
public List<FireEvent> FireEvents { get; set; } = new();
}
/// <summary>探测源(独立探测设备或火力单元自带探测的统一表达)。
/// planner 基于探测源估算威胁的最早发现点,而非上帝视角的航路起点。</summary>
public class DetectionSource
{
public Vector3 Position { get; set; }
/// <summary>雷达探测距离 m不受能见度影响</summary>
public float RadarRange { get; set; }
/// <summary>光电探测距离 m受 Visibility 衰减)</summary>
public float EORange { get; set; }
/// <summary>红外探测距离 m不受能见度影响</summary>
public float IRRange { get; set; }
/// <summary>探测精度 m位置误差影响抛撒散布范围</summary>
public float Accuracy { get; set; }
}
/// <summary>规划方案</summary>
public class DefensePlan
{

View File

@ -26,8 +26,7 @@ namespace CounterDrone.Core.Algorithms
// 五步流水线
// ═══════════════════════════════════════════════
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
CombatScene environment, List<DetectionSource> detectionSources)
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment)
{
var result = new PlannerResult();
@ -43,19 +42,6 @@ namespace CounterDrone.Core.Algorithms
return result;
}
// 探测信息:每个威胁的最早发现弧长 + 精度(基于统一信息网络)
// 无探测设备时 detectArc=0上帝视角从航路起点算、精度用配置默认值
float visibility = (float)environment.Visibility;
foreach (var t in threats)
{
var (detectArc, accuracy) = DetectionCalculator.EarliestDetection(
t.Waypoints, detectionSources, visibility);
t.DetectArc = detectArc;
t.DetectAccuracy = accuracy == float.MaxValue
? _config.DefaultDetectionAccuracy
: accuracy;
}
// Step 1: 威胁排序
foreach (var t in threats)
{
@ -108,7 +94,7 @@ namespace CounterDrone.Core.Algorithms
// Step 3-4: 候选生成 → 贪心分配
// ═══════════════════════════════════════════════
private DefensePlan Solve(List<DroneWave> sortedThreats,
private DefensePlan Solve(List<DroneGroup> sortedThreats,
List<FireUnit> fireUnits, CombatScene env)
{
var plan = new DefensePlan();
@ -176,7 +162,6 @@ namespace CounterDrone.Core.Algorithms
if (!laneBaseSet[yLane])
{
var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
if (refEvt.Count == 0) continue; // 该车道无可行发射事件(探测边界太靠后等)
laneBaseTime[yLane] = refEvt[0].FireTime;
laneBaseSet[yLane] = true;
}
@ -192,7 +177,6 @@ namespace CounterDrone.Core.Algorithms
int roundInLane = baseRoundInLane + ch;
float offset = (roundInLane - (singleNeeded - 1) / 2f) * spacing;
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
if (fe.Count == 0) continue; // 该发不可行(探测边界太靠后等)
foreach (var e in fe)
{
e.FireTime = laneBaseTime[yLane] + roundInLane * stagger;
@ -215,7 +199,7 @@ namespace CounterDrone.Core.Algorithms
plan.Assignments.Add(new UnitAssignment
{
FireUnitId = unit.Id,
DroneWaveId = threat.WaveId,
DroneGroupId = threat.GroupId,
AmmoType = neededAmmo,
RoundsFired = rounds,
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
@ -236,7 +220,7 @@ namespace CounterDrone.Core.Algorithms
var a2 = _ammoCatalog.FirstOrDefault(s =>
s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Target.PowerType));
int totalRounds = plan.Assignments
.Where(a => a.DroneWaveId == threat.WaveId)
.Where(a => a.DroneGroupId == threat.GroupId)
.Sum(a => a.RoundsFired);
if (totalRounds > 0)
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)
{
var candidates = new List<InterceptCandidate>();
@ -281,7 +265,7 @@ namespace CounterDrone.Core.Algorithms
return candidates;
}
private InterceptCandidate? BuildGroundBasedCandidate(DroneWave threat,
private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
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,
float effectiveR, CombatScene env)
{
@ -358,7 +342,7 @@ namespace CounterDrone.Core.Algorithms
return (effectiveR, expansionTime, rPhase2);
}
private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec ammo,
CombatScene env, bool isAirBased, float turbulentRadius)
{
var cloudModel = new CloudExpansionModel(ammo, env);
@ -371,7 +355,7 @@ namespace CounterDrone.Core.Algorithms
return cloudModel.RoundsNeeded(requiredCoverage, spacing);
}
private float ComputeInterceptProbability(DroneWave threat,
private float ComputeInterceptProbability(DroneGroup threat,
AmmunitionSpec ammo, int rounds, CombatScene env)
{
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,
int yLane, int yLanes, float formationWidth)
{
@ -432,12 +416,8 @@ namespace CounterDrone.Core.Algorithms
float ty = routeY;
float tz = routeZ + normalZ * laneOffset;
// 无人机到达穿越点的时间:基于探测边界,而非航路起点。
// planner 是参谋,只能基于探测信息规划。威胁从探测边界被发现,
// 飞行时间 = (拦截弧长 - 探测弧长) / 速度。无探测时 DetectArc=0回退到起点
float travelArc = crossArc - threat.DetectArc;
if (travelArc < 0) return events; // 探测边界在拦截点之后,来不及拦截
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
// 无人机到达穿越点的时间:沿航路匀速飞行 crossArc 弧长
float txArrival = RouteGeometry.TravelTimeTo(wps, crossArc >= 0 ? crossArc : 0, typicalSpeed);
float recommendedTiming = txArrival - expansionTime;
if (recommendedTiming <= 0f) return events;
@ -517,7 +497,7 @@ namespace CounterDrone.Core.Algorithms
var reduced = new UnitAssignment
{
FireUnitId = assignment.FireUnitId,
DroneWaveId = assignment.DroneWaveId,
DroneGroupId = assignment.DroneGroupId,
AmmoType = assignment.AmmoType,
RoundsFired = rounds,
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)
return new Vector3(0, (float)threat.Target.TypicalAltitude, 0);

View File

@ -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;
}
}
}

View File

@ -3,15 +3,12 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>防御规划引擎 — 给定火力单元池、威胁列表和探测能力,输出分配方案。
/// planner 基于探测能力估算威胁的最早发现点(而非上帝视角的航路起点)。</summary>
/// <summary>防御规划引擎 — 给定火力单元池和威胁列表,输出分配方案</summary>
public interface IDefensePlanner
{
/// <param name="fireUnits">可用的火力单元(空基/地基统一表达)</param>
/// <param name="threats">威胁批次列表</param>
/// <param name="environment">作战环境(含天气,影响光电探测距离)</param>
/// <param name="detectionSources">统一信息网络的探测源列表(独立探测设备 + 火力单元自带探测)。可为空列表。</param>
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
CombatScene environment, List<DetectionSource> detectionSources);
/// <param name="threats">威胁编队列表</param>
/// <param name="environment">作战环境</param>
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneGroup> threats, CombatScene environment);
}
}

View File

@ -26,9 +26,6 @@ namespace CounterDrone.Core.Algorithms
/// <summary>弹药匹配表PowerType → AerosolType</summary>
public Dictionary<PowerType, AerosolType> AmmoMatch { get; set; } = new();
/// <summary>无探测设备时的默认探测精度 m回退值上帝视角但有标称误差</summary>
public float DefaultDetectionAccuracy { get; set; }
private const string ConfigFileName = "planner_config.json";
/// <summary>从 dataRoot 加载配置。文件缺失或字段非法即抛异常。</summary>
@ -63,8 +60,6 @@ namespace CounterDrone.Core.Algorithms
throw new InvalidDataException("TypeCoefficient 不能为空");
if (AmmoMatch == null || AmmoMatch.Count == 0)
throw new InvalidDataException("AmmoMatch 不能为空");
if (DefaultDetectionAccuracy < 0f)
throw new InvalidDataException($"DefaultDetectionAccuracy 必须 >= 0实际 {DefaultDetectionAccuracy}");
}
private static readonly JsonSerializerOptions JsonOptions = new()

View File

@ -68,8 +68,9 @@ namespace CounterDrone.Core
db.CreateTable<EquipmentDeployment>();
db.CreateTable<CloudDispersal>();
db.CreateTable<RoutePlan>();
db.CreateIndex("RoutePlan", new[] { "TaskId", "WaveId" }, true);
db.CreateIndex("RoutePlan", new[] { "TaskId", "GroupId" }, true);
db.CreateTable<Waypoint>();
db.CreateTable<Group>();
db.CreateTable<SimulationReport>();
db.CreateIndex("SimTask", "TaskNumber");

View File

@ -92,7 +92,7 @@ namespace CounterDrone.Core.Models
Destroyed = 3
}
// === 编队 & 批次 ===
// === 编队 & 编组 ===
public enum FormationMode
{
Single = 0,
@ -100,6 +100,12 @@ namespace CounterDrone.Core.Models
Swarm = 2
}
public enum GroupType
{
DroneFleet = 0,
EquipmentGroup = 1
}
// === 运行时 ===
public enum EntityType
{

View File

@ -17,7 +17,7 @@ namespace CounterDrone.Core.Models
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; }
@ -50,14 +50,7 @@ namespace CounterDrone.Core.Models
/// <summary>空基平台巡航速度 m/s地基为 NULL</summary>
public double? CruiseSpeed { get; set; }
// ── 探测能力(独立探测设备和火力单元自带探测统一表达)──
/// <summary>雷达探测距离 m雨雾不衰减发射平台也可有</summary>
public double? RadarRange { get; set; }
/// <summary>光电探测距离 m受 Visibility 衰减)</summary>
public double? EORange { get; set; }
/// <summary>红外探测距离 m不受 Visibility 影响)</summary>
public double? IRRange { get; set; }
/// <summary>探测精度 m位置误差影响抛撒散布范围</summary>
public double? DetectionAccuracy { get; set; }
// 探测设备专用
public double? DetectionRadius { get; set; }
}
}

View 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");
}
}

View File

@ -3,7 +3,7 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤5航路规划 — 多批次支持:每个(任务,批次)一条航路</summary>
/// <summary>步骤5航路规划 — 多编队支持:每个(任务,编队)一条航路</summary>
[Table("RoutePlan")]
public class RoutePlan
{
@ -14,7 +14,7 @@ namespace CounterDrone.Core.Models
public string TaskId { get; set; } = string.Empty;
[Indexed]
public string WaveId { get; set; } = string.Empty;
public string GroupId { get; set; } = string.Empty;
public int FormationMode { get; set; } = (int)Models.FormationMode.Single;

View File

@ -13,7 +13,7 @@ namespace CounterDrone.Core.Models
[Indexed]
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;

View File

@ -11,9 +11,9 @@ namespace CounterDrone.Core.Models
public List<TargetConfig> Targets { get; set; } = new();
public List<EquipmentDeployment> Equipment { get; set; } = new();
public CloudDispersal Cloud { get; set; } = new();
/// <summary>多批次航路(多批次支持)</summary>
/// <summary>多编队航路(多批次支持)</summary>
public List<RoutePlan> Routes { get; set; } = new();
/// <summary>按 WaveId 分组的航路点</summary>
/// <summary>按 GroupId 分组的航路点</summary>
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
}

View File

@ -13,8 +13,8 @@ namespace CounterDrone.Core.Models
[Indexed]
public string TaskId { get; set; } = string.Empty;
/// <summary>关联的批次 ID多批次支持)</summary>
public string WaveId { get; set; } = string.Empty;
/// <summary>关联的编队 ID多编队支持)</summary>
public string GroupId { get; set; } = string.Empty;
[NotNull]
public int OrderIndex { get; set; }

View File

@ -14,14 +14,6 @@ namespace CounterDrone.Core.Repository
return Db.Table<EquipmentDeployment>().Where(e => e.TaskId == taskId).ToList();
}
/// <summary>按任务和角色查询装备EquipmentRole: 0=Detection, 1=LaunchPlatform</summary>
public List<EquipmentDeployment> GetByTaskIdAndRole(string taskId, int equipmentRole)
{
return Db.Table<EquipmentDeployment>()
.Where(e => e.TaskId == taskId && e.EquipmentRole == equipmentRole)
.ToList();
}
public void DeleteByTaskId(string taskId)
{
var equips = GetByTaskId(taskId);

View 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();
}
}
}

View File

@ -14,10 +14,10 @@ namespace CounterDrone.Core.Repository
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>()
.FirstOrDefault(r => r.TaskId == taskId && r.WaveId == waveId);
.FirstOrDefault(r => r.TaskId == taskId && r.GroupId == groupId);
}
}
}

View File

@ -17,10 +17,10 @@ namespace CounterDrone.Core.Repository
.ToList();
}
public List<Waypoint> GetByTaskAndWave(string taskId, string waveId)
public List<Waypoint> GetByTaskAndGroup(string taskId, string groupId)
{
return Db.Table<Waypoint>()
.Where(w => w.TaskId == taskId && w.WaveId == waveId)
.Where(w => w.TaskId == taskId && w.GroupId == groupId)
.OrderBy(w => w.OrderIndex)
.ToList();
}

View 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);
}
}
}

View 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);
}
}

View File

@ -15,16 +15,8 @@ namespace CounterDrone.Core.Services
void SaveControlZones(string taskId, List<ControlZone> zones);
void SaveTarget(string taskId, TargetConfig target);
void SaveDeployment(string taskId, List<EquipmentDeployment> equips);
/// <summary>添加单个探测设备(独立于 SaveDeployment不覆盖火力单元</summary>
void AddDetection(string taskId, EquipmentDeployment detection);
/// <summary>删除探测设备</summary>
void DeleteDetection(string detectionId);
/// <summary>获取任务的所有探测设备EquipmentRole.Detection</summary>
List<EquipmentDeployment> GetDetections(string taskId);
void SaveCloudDispersal(string taskId, CloudDispersal cloud);
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints);
void SaveRoute(string taskId, string groupId, RoutePlan route, List<Waypoint> waypoints);
void UpdateStep(string taskId, int step);
}
}

View File

@ -150,13 +150,13 @@ namespace CounterDrone.Core.Services
{
sb.AppendLine($"### 探测设备");
sb.AppendLine();
sb.AppendLine($"| # | 位置 (X,Y,Z) | 雷达 | 光电 | 红外 | 精度 | 数量 |");
sb.AppendLine($"|---|-------------|:----:|:----:|:----:|:----:|:----:|");
sb.AppendLine($"| # | 位置 (X,Y,Z) | 探测半径 | 数量 |");
sb.AppendLine($"|---|-------------|---------|:----:|");
int detIdx = 0;
foreach (var d in detections)
{
for (int j = 0; j < d.Quantity; j++)
sb.AppendLine($"| D{++detIdx} | ({d.PositionX:F0}, {d.PositionY:F0}, {d.PositionZ:F0}) | {(d.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();
}

View File

@ -101,7 +101,7 @@ namespace CounterDrone.Core.Services
Cloud = _cloudRepo.GetById(id) ?? new CloudDispersal { TaskId = id },
Routes = _routeRepo.GetByTaskId(id),
WaypointGroups = _waypointRepo.GetByTaskId(id)
.GroupBy(w => w.WaveId)
.GroupBy(w => w.GroupId)
.ToDictionary(g => g.Key, g => g.OrderBy(w => w.OrderIndex).ToList()),
};
}
@ -158,26 +158,6 @@ namespace CounterDrone.Core.Services
TouchTask(taskId);
}
public void AddDetection(string taskId, EquipmentDeployment detection)
{
detection.TaskId = taskId;
detection.EquipmentRole = (int)EquipmentRole.Detection;
if (string.IsNullOrEmpty(detection.Id))
detection.Id = Guid.NewGuid().ToString();
_equipRepo.Insert(detection);
TouchTask(taskId);
}
public void DeleteDetection(string detectionId)
{
_equipRepo.Delete(detectionId);
}
public List<EquipmentDeployment> GetDetections(string taskId)
{
return _equipRepo.GetByTaskIdAndRole(taskId, (int)EquipmentRole.Detection);
}
public void SaveCloudDispersal(string taskId, CloudDispersal cloud)
{
cloud.TaskId = taskId;
@ -189,11 +169,11 @@ namespace CounterDrone.Core.Services
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.WaveId = waveId;
var existing = _routeRepo.GetByTaskAndWave(taskId, waveId);
route.GroupId = groupId;
var existing = _routeRepo.GetByTaskAndGroup(taskId, groupId);
if (existing != null)
{
route.Id = existing.Id;
@ -202,13 +182,13 @@ namespace CounterDrone.Core.Services
else
_routeRepo.Insert(route);
// 删除该批次的旧航路点,插入新的
var oldWps = _waypointRepo.GetByTaskAndWave(taskId, waveId);
// 删除该编队的旧航路点,插入新的
var oldWps = _waypointRepo.GetByTaskAndGroup(taskId, groupId);
foreach (var w in oldWps) _waypointRepo.Delete(w.Id);
for (int i = 0; i < waypoints.Count; i++)
{
waypoints[i].TaskId = taskId;
waypoints[i].WaveId = waveId;
waypoints[i].GroupId = groupId;
waypoints[i].OrderIndex = i;
if (string.IsNullOrEmpty(waypoints[i].Id))
waypoints[i].Id = Guid.NewGuid().ToString();

View File

@ -8,7 +8,7 @@ namespace CounterDrone.Core.Simulation
public class DroneEntity
{
public string Id { get; }
public string WaveId { get; }
public string GroupId { get; }
public TargetType TargetType { get; }
public PowerType PowerType { get; }
public float Wingspan { get; }
@ -27,11 +27,11 @@ namespace CounterDrone.Core.Simulation
/// <summary>沿航路已飞行弧长(米),由 RouteGeometry 驱动</summary>
private float _traveledArc;
public DroneEntity(string id, string 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)
{
Id = id;
WaveId = waveId;
GroupId = groupId;
TargetType = (TargetType)config.TargetType;
PowerType = (PowerType)config.PowerType;
Wingspan = (float)config.Wingspan;

View File

@ -88,8 +88,8 @@ namespace CounterDrone.Core.Simulation
_drones.Clear();
foreach (var target in config.Targets)
{
var route = config.Routes.FirstOrDefault(r => r.WaveId == target.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(target.WaveId, new List<Waypoint>());
var route = config.Routes.FirstOrDefault(r => r.GroupId == target.GroupId);
var wps = config.WaypointGroups.GetValueOrDefault(target.GroupId, new List<Waypoint>());
if (route == null || wps.Count == 0) continue;
var mode = (FormationMode)route.FormationMode;
var lateralSpacing = (float)route.LateralSpacing;
@ -100,7 +100,7 @@ namespace CounterDrone.Core.Simulation
{
int latIdx = i % latCount;
int longIdx = i / latCount;
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.WaveId,
_drones.Add(new DroneEntity($"drone_{++_entityCounter}", target.GroupId,
target, wps, latIdx, lateralSpacing, longIdx, longSpacing, mode));
}
}
@ -137,11 +137,10 @@ namespace CounterDrone.Core.Simulation
if (_fireSchedule.Count == 0)
{
var fireUnits = BuildFireUnits(config);
var threats = BuildDroneWaves(config);
var threats = BuildDroneGroups(config);
if (fireUnits.Count > 0 && threats.Count > 0)
{
var detectionSources = BuildDetectionSources(config);
var result = _planner.Plan(fireUnits, threats, _scene, detectionSources);
var result = _planner.Plan(fireUnits, threats, _scene);
SetFireSchedule(result.Best.MergedSchedule);
}
}
@ -342,52 +341,22 @@ namespace CounterDrone.Core.Simulation
TotalMunitions = eq.MunitionCount ?? channels,
AmmoTypes = new List<AerosolType> { (AerosolType)(eq.AerosolType ?? 0) },
Cooldown = (float)eq.Cooldown,
// 探测能力(火力单元自带探测,激活原死字段)
RadarRange = (float)(eq.RadarRange ?? 0f),
EORange = (float)(eq.EORange ?? 0f),
IRRange = (float)(eq.IRRange ?? 0f),
});
}
}
return units;
}
/// <summary>构建统一信息网络的探测源列表。
/// 独立探测设备EquipmentRole.Detection+ 火力单元自带探测能力。</summary>
private static List<DetectionSource> BuildDetectionSources(TaskFullConfig config)
private static List<DroneGroup> BuildDroneGroups(TaskFullConfig config)
{
var sources = new List<DetectionSource>();
foreach (var eq in config.Equipment)
{
// 只要有任何探测字段非 null 且 > 0就是有效探测源无论是否独立探测设备
bool hasDetection = (eq.RadarRange ?? 0) > 0 || (eq.EORange ?? 0) > 0 || (eq.IRRange ?? 0) > 0;
if (!hasDetection) continue;
int qty = Math.Max(1, eq.Quantity);
for (int i = 0; i < qty; i++)
{
sources.Add(new DetectionSource
{
Position = new Algorithms.Vector3((float)eq.PositionX + i * 50, (float)eq.PositionY, (float)eq.PositionZ),
RadarRange = (float)(eq.RadarRange ?? 0f),
EORange = (float)(eq.EORange ?? 0f),
IRRange = (float)(eq.IRRange ?? 0f),
Accuracy = (float)(eq.DetectionAccuracy ?? 0f),
});
}
}
return sources;
}
private static List<DroneWave> BuildDroneWaves(TaskFullConfig config)
{
var groups = new List<DroneWave>();
var groups = new List<DroneGroup>();
foreach (var t in config.Targets)
{
var route = config.Routes.FirstOrDefault(r => r.WaveId == t.WaveId);
var wps = config.WaypointGroups.GetValueOrDefault(t.WaveId, new List<Waypoint>());
groups.Add(new DroneWave
var route = config.Routes.FirstOrDefault(r => r.GroupId == t.GroupId);
var wps = config.WaypointGroups.GetValueOrDefault(t.GroupId, new List<Waypoint>());
groups.Add(new DroneGroup
{
WaveId = t.WaveId,
GroupId = t.GroupId,
Target = t,
Route = route ?? new RoutePlan(),
Waypoints = wps,

View 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})");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 24c080ea3d6cbcf45aa804fd10269e4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -35,7 +35,7 @@ namespace CounterDrone.Unity
scenarioMgr.SaveScene(taskId, new CombatScene { WindSpeed = 0 });
scenarioMgr.SaveTarget(taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
@ -54,9 +54,9 @@ namespace CounterDrone.Unity
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var detail = scenarioMgr.GetDetail(taskId);
var droneGroup = new DroneWave
var droneGroup = new DroneGroup
{
WaveId = "default",
GroupId = "default",
Target = detail.Targets[0],
Route = detail.Routes[0],
Waypoints = detail.WaypointGroups["default"],
@ -72,7 +72,7 @@ namespace CounterDrone.Unity
TotalMunitions = 3,
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
});
var result = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, new List<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
{
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,

View File

@ -53,7 +53,7 @@ namespace CounterDrone.Unity
});
scenario.SaveTarget(taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = _droneSpeed, TypicalAltitude = 500,
});
@ -68,9 +68,9 @@ namespace CounterDrone.Unity
var detail = scenario.GetTaskDetail(taskId);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var planner = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths));
var droneGroup = new DroneWave
var droneGroup = new DroneGroup
{
WaveId = "default",
GroupId = "default",
Target = detail.Targets[0],
Route = detail.Routes[0],
Waypoints = detail.WaypointGroups["default"],
@ -86,7 +86,7 @@ namespace CounterDrone.Unity
TotalMunitions = 3,
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
});
var result = planner.Plan(fireUnits, new List<DroneWave> { droneGroup }, detail.Scene, new List<DetectionSource>());
var result = planner.Plan(fireUnits, new List<DroneGroup> { droneGroup }, detail.Scene);
scenario.SaveCloudDispersal(taskId, new CloudDispersal
{
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,

View File

@ -31,12 +31,12 @@ namespace CounterDrone.Core.Tests
{
var db = _dbManager.OpenMainDb();
// 验证所有 11 张表存在
// 验证所有 12 张表存在
var tables = new[]
{
"ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene",
"ControlZone", "TargetConfig", "EquipmentDeployment",
"CloudDispersal", "RoutePlan", "Waypoint",
"CloudDispersal", "RoutePlan", "Waypoint", "Group",
"SimulationReport"
};

View File

@ -37,12 +37,12 @@ namespace CounterDrone.Core.Tests
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)
{
var t = new DroneWave
var t = new DroneGroup
{
WaveId = "default",
GroupId = "default",
Target = new TargetConfig
{
TargetType = (int)TargetType.Piston, PowerType = (int)power,
@ -57,9 +57,9 @@ namespace CounterDrone.Core.Tests
return t;
}
private PlannerResult Plan(List<FireUnit> units, DroneWave threat,
private PlannerResult Plan(List<FireUnit> units, DroneGroup threat,
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(
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.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_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_NoUnits_Unengaged() => Assert.Equal(1, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new(), new() { MakeThreat() }, new CombatScene()).Best.ThreatsUnengaged);
[Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene()).Best.ThreatsEngaged);
[Fact]
public void Edge_OutOfRange_Ground()
@ -291,11 +291,11 @@ namespace CounterDrone.Core.Tests
[Fact]
public void MultiThreat_BothEngaged()
{
var a = MakeThreat(PowerType.Piston, 120); a.WaveId = "g0";
var b = MakeThreat(PowerType.Jet, 300); b.WaveId = "g1";
var a = MakeThreat(PowerType.Piston, 120); a.GroupId = "g0";
var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1";
var result = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) },
new() { a, b }, new CombatScene(), new List<DetectionSource>());
new() { a, b }, new CombatScene());
Assert.Equal(2, result.Best.ThreatsEngaged);
}
@ -405,9 +405,9 @@ namespace CounterDrone.Core.Tests
// Z 向航路:从 (5000,0) 飞向 (5000,10000),即沿 +Z 方向
// 之前 offset 写死 X 轴时,多发云团会沿 X 发散到 5000±N×spacing偏离航路
// 改用 RouteGeometry 后offset 沿航路切向(+Z云团应集中在 X=5000
var threat = new DroneWave
var threat = new DroneGroup
{
WaveId = "default",
GroupId = "default",
Target = new TargetConfig
{
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
@ -429,55 +429,5 @@ namespace CounterDrone.Core.Tests
Assert.True(Math.Abs(fe.TargetX - 5000) < 50f,
$"Z 向航路下抛撒点 X={fe.TargetX:F1} 应在航路 X=5000 附近±50m而非沿 X 发散"));
}
// ═══════════════════════════════════════
// 探测驱动规划有探测设备时planner 基于探测边界算到达时间,发射时机改变
// ═══════════════════════════════════════
private static FireUnit MakeBlindGroundUnit(string id, float posX, int munitions = 16)
=> new()
{
Id = id, Type = PlatformType.GroundBased,
Position = new Vector3(posX, 0, 50),
GunCount = 1, ChannelsPerGun = 16,
ChannelInterval = 0.1f,
MuzzleVelocity = 800, TotalMunitions = munitions, Cooldown = 5f,
AmmoTypes = new() { AerosolType.InertGas, AerosolType.ActiveMaterial, AerosolType.ActiveFuel },
// 无探测能力RadarRange/EORange/IRange 默认 0
};
[Fact]
public void DetectionDriven_FireTime_UsesDetectionBoundary()
{
// 航路 X:0→10000无人机 200km/h。无探测时 planner 基于起点,到达中点 5000m = 90s
var threat = MakeThreat(speed: 200, startX: 0, endX: 10000);
var unit = MakeBlindGroundUnit("u0", 5000);
// 场景A无探测DetectArc=0上帝视角从起点算
var resultNoDet = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
new() { unit }, new() { threat }, new CombatScene { Visibility = 10000 },
new List<DetectionSource>());
float fireNoDet = resultNoDet.Best.MergedSchedule[0].FireTime;
// 场景B探测源雷达 3000m部署 X=8000边界 X=5000 和 X=11000
// 无人机从 X=0 飞,在 X=5000 进入探测DetectArc=5000
// planner 基于 X=5000 算到达时间(拦截点 5000m 中点travelArc = 5000-5000 = 0
// 但中点 X=5000 = 探测边界travelArc=0 意味着来不及拦截
// 所以实际拦截点会不同。简化验证:有探测时 fireTime 不同(推迟或无法拦截)
var detSrc = new DetectionSource
{
Position = new Vector3(8000, 0, 0),
RadarRange = 3000, Accuracy = 50,
};
var resultDet = new DefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(
new() { unit }, new() { threat }, new CombatScene { Visibility = 10000 },
new List<DetectionSource> { detSrc });
Assert.True(fireNoDet > 0, $"无探测 fireTime={fireNoDet}");
// 探测边界 X=5000 = 航路中点拦截点travelArc=0来不及拦截 → ThreatsUnengaged
// 这验证了 planner 确实基于探测边界算到达时间(而非上帝视角从起点算)
Assert.True(resultDet.Best.ThreatsUnengaged > 0,
$"探测边界=拦截点时应无法拦截(DetectArc=中点弧长)threatsUnengaged={resultDet.Best.ThreatsUnengaged}");
}
}
}

View File

@ -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 → 有效 = 10000min(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);
}
}
}

View File

@ -49,7 +49,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
TypicalSpeed = 100,
@ -92,7 +92,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
TypicalSpeed = 600, // 高速对抗强风
@ -131,7 +131,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.HighSpeed,
Quantity = 1,
TypicalSpeed = 500,

View File

@ -82,10 +82,6 @@ namespace CounterDrone.Core.Tests
MuzzleVelocity = template.MuzzleVelocity > 0 ? template.MuzzleVelocity : null,
CruiseSpeed = template.CruiseSpeed > 0 ? template.CruiseSpeed : null,
ReleaseAltitude = template.ReleaseAltitude > 0 ? template.ReleaseAltitude : null,
// 火力单元自带探测能力(激活)
RadarRange = template.RadarRange > 0 ? template.RadarRange : null,
EORange = template.EORange > 0 ? template.EORange : null,
IRRange = template.IRRange > 0 ? template.IRRange : null,
};
}
@ -114,7 +110,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.FixedWing,
PowerType = (int)PowerType.Jet,
Quantity = 1,
@ -147,7 +143,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Electric,
PowerType = (int)PowerType.Electric,
Quantity = 1,
@ -189,7 +185,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston,
GroupId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
TypicalSpeed = 200, TypicalAltitude = 500,
@ -273,7 +269,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston,
GroupId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
TypicalSpeed = 200, TypicalAltitude = 500,
@ -333,7 +329,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.HighSpeed,
PowerType = (int)PowerType.Jet,
Quantity = 1,
@ -377,7 +373,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
@ -443,7 +439,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 1,
@ -502,7 +498,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston,
GroupId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 3,
TypicalSpeed = 200, TypicalAltitude = 500,
@ -557,7 +553,7 @@ namespace CounterDrone.Core.Tests
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston,
GroupId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 3,
TypicalSpeed = 150, TypicalAltitude = 500,
@ -604,80 +600,5 @@ namespace CounterDrone.Core.Tests
Assert.True(launched > 0, msg);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg);
}
// ═══════════════════════════════════════════════
// 场景 8探测驱动规划 — 远航路 + 独立探测设备
// 验证有探测设备时planner 基于探测边界算到达时间,发射时机比无探测时晚
// ═══════════════════════════════════════════════
[Fact]
public void Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary()
{
// 场景A无探测设备上帝视角从航路起点算
var taskA = _scenario.CreateTask("无探测远航路", "");
_taskId = taskA.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
var engA = RunSimulation(8000);
float firstFireA = engA.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
// 场景B有独立探测设备雷达 6000m部署在 X=10000 航路中点附近)
// 探测圆边界 X=4000 和 X=16000无人机从 X=0 飞向 20000在 X=4000 进入探测
// planner 基于探测边界 X=4000 算到达时间(而非起点 X=0
var taskB = _scenario.CreateTask("有探测远航路", "");
_taskId = taskB.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 8000, 0, 50),
// 独立探测设备
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.Detection,
Quantity = 1,
PositionX = 10000, PositionY = 0, PositionZ = 0,
RadarRange = 6000,
DetectionAccuracy = 50,
},
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
var engB = RunSimulation(8000);
float firstFireB = engB.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Min(e => e.OccurredAt);
// 验证有探测时发射推迟planner 基于探测边界 X=4000 而非起点 X=0
var msg = $"无探测首发={firstFireA:F1}s, 有探测首发={firstFireB:F1}s";
Assert.True(firstFireA > 0 && firstFireB > 0, msg);
// 火力单元自带雷达 10000m 覆盖起点,两者 DetectArc 都=0发射时机相同。
// 这个测试验证探测链路不破坏仿真(都击毁),而非时机差异。
Assert.True(engA.Drones.All(d => d.Status == DroneStatus.Destroyed), "无探测应击毁");
Assert.True(engB.Drones.All(d => d.Status == DroneStatus.Destroyed), "有探测应击毁");
VerifyAndExportReport(engB, engB.Drones[0]);
}
}
}

View 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));
}
}
}

View 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);
}
}
}

View File

@ -35,8 +35,7 @@ namespace CounterDrone.Core.Tests
},
""AmmoMatch"": {
""Electric"": ""InertGas"", ""Piston"": ""InertGas"", ""Jet"": ""ActiveMaterial""
},
""DefaultDetectionAccuracy"": 50.0
}
}";
[Fact]

View File

@ -103,7 +103,7 @@ namespace CounterDrone.Core.Tests
_service.SaveTarget(task.Id, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.FixedWing,
Quantity = 3,
});
@ -201,7 +201,7 @@ namespace CounterDrone.Core.Tests
var task = _service.CreateTask("Target Test", "");
_service.SaveTarget(task.Id, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 5,
PowerType = (int)PowerType.Piston,
@ -236,7 +236,7 @@ namespace CounterDrone.Core.Tests
{
EquipmentRole = (int)EquipmentRole.Detection,
Quantity = 1,
RadarRange = 5000.0,
DetectionRadius = 5000.0,
},
};
@ -394,7 +394,7 @@ namespace CounterDrone.Core.Tests
// 4. 步骤2目标配置
_service.SaveTarget(task.Id, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.HighSpeed,
Quantity = 2,
PowerType = (int)PowerType.Jet,
@ -409,7 +409,7 @@ namespace CounterDrone.Core.Tests
{
EquipmentRole = (int)EquipmentRole.Detection,
Quantity = 1,
RadarRange = 6000.0,
DetectionRadius = 6000.0,
},
new EquipmentDeployment
{
@ -484,81 +484,5 @@ namespace CounterDrone.Core.Tests
var searchResult = _service.SearchTasks("完整流程", null, null, 1, 10);
Assert.Equal(1, searchResult.TotalCount);
}
// ═══════════════════════════════════════
// 探测设备独立 CRUD
// ═══════════════════════════════════════
[Fact]
public void AddDetection_PersistsAndQueryable()
{
var task = _service.CreateTask("探测设备测试", "");
_service.AddDetection(task.Id, new EquipmentDeployment
{
Quantity = 1,
PositionX = 5000, PositionY = 0, PositionZ = 0,
RadarRange = 8000, EORange = 4000, IRRange = 3000,
DetectionAccuracy = 50,
});
var detections = _service.GetDetections(task.Id);
Assert.Single(detections);
Assert.Equal((int)EquipmentRole.Detection, detections[0].EquipmentRole);
Assert.Equal(8000.0, detections[0].RadarRange);
Assert.Equal(50.0, detections[0].DetectionAccuracy);
}
[Fact]
public void GetDetections_FiltersOutLaunchPlatforms()
{
var task = _service.CreateTask("探测过滤测试", "");
// 一个火力单元
_service.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
Quantity = 1, PositionX = 1000,
},
});
// 两个探测设备(独立添加)
_service.AddDetection(task.Id, new EquipmentDeployment { RadarRange = 5000 });
_service.AddDetection(task.Id, new EquipmentDeployment { EORange = 3000 });
var detections = _service.GetDetections(task.Id);
Assert.Equal(2, detections.Count); // 只有探测设备,不含火力单元
}
[Fact]
public void DeleteDetection_RemovesOnlyOne()
{
var task = _service.CreateTask("探测删除测试", "");
_service.AddDetection(task.Id, new EquipmentDeployment { RadarRange = 5000 });
var det2 = new EquipmentDeployment { EORange = 3000 };
_service.AddDetection(task.Id, det2);
_service.DeleteDetection(det2.Id);
var detections = _service.GetDetections(task.Id);
Assert.Single(detections);
Assert.Equal(5000.0, detections[0].RadarRange);
}
[Fact]
public void GetTaskDetail_IncludesDetections()
{
var task = _service.CreateTask("想定含探测", "");
_service.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.LaunchPlatform, Quantity = 1 },
});
_service.AddDetection(task.Id, new EquipmentDeployment { RadarRange = 10000 });
var detail = _service.GetTaskDetail(task.Id);
// Equipment 包含火力单元 + 探测设备
Assert.Equal(2, detail.Equipment.Count);
Assert.Contains(detail.Equipment, e => e.EquipmentRole == (int)EquipmentRole.Detection);
Assert.Contains(detail.Equipment, e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform);
}
}
}

View File

@ -53,7 +53,7 @@ namespace CounterDrone.Core.Tests
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenarioService.SaveTarget(_taskId, new TargetConfig
{
WaveId = "default",
GroupId = "default",
TargetType = (int)TargetType.Piston,
Quantity = 1,
PowerType = (int)PowerType.Piston,
@ -147,7 +147,7 @@ namespace CounterDrone.Core.Tests
{
var task = _scenarioService.CreateTask("t", "");
_scenarioService.SaveScene(task.Id, new CombatScene());
_scenarioService.SaveTarget(task.Id, new TargetConfig { 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>
{
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased,