重命名核心模型:TargetConfig→DroneProfile, SimTask→Scenario, TaskFullConfig→ScenarioConfig, TaskId→ScenarioId

- DroneType 枚举清理:移除 Electric/Piston,重编号 HighSpeed=2
- planner_config.json / defaults.json 同步迁移
- 238 单元测试全部通过
This commit is contained in:
tian 2026-06-18 15:02:58 +08:00
parent def3996f4f
commit ca0ce8aa52
66 changed files with 508 additions and 512 deletions

View File

@ -37,7 +37,7 @@
### Added — FrameDataStore LiveFrames 内存回放
- **Flush 后保留 LiveFrames 副本**:仿真刚结束可内存回放,零磁盘 IO
- **ReplayController 双路径**`LoadReplay(taskId, frameStore)` 优先内存,回退 SQLite
- **ReplayController 双路径**`LoadReplay(scenarioId, frameStore)` 优先内存,回退 SQLite
- **LiveFrames 生命周期**BeginRecording/Discard 清除
### Changed — 3机空基编队测试启用
@ -73,7 +73,7 @@
### Changed — 消除硬编码
- **速度从 waypoint.Speed 读取**`DroneEntity.CruiseSpeed` / `GetDroneSpeedKph` 不再用 `TargetConfig.TypicalSpeed`
- **速度从 waypoint.Speed 读取**`DroneEntity.CruiseSpeed` / `GetDroneSpeedKph` 不再用 `DroneProfile.TypicalSpeed`
- **`AmmunitionSpec` +`Phase2Duration`**30s`CloudExpansionModel` 不再写死 `30f`
- **`GetArrivalTime` 基于 DetectArc**:从探测点到中点算到达时间(而非航路起点)
- **TestData 改为从 seeded 数据库读取**(不再内嵌 JSON 重复 defaults.json
@ -101,8 +101,8 @@
### Changed — 数据模型重命名
- **`GroupId``WaveId`**TargetConfig、EquipmentDeployment、RoutePlan、Waypoint 四表的编组外键重命名为批次外键
- **RoutePlan 索引重命名**`(TaskId, GroupId)` → `(TaskId, WaveId)`
- **`GroupId``WaveId`**DroneProfile 四表的编组外键重命名为批次外键
- **RoutePlan 索引重命名**`(ScenarioId, GroupId)` → `(ScenarioId, WaveId)`
- **RoutePlanRepository / WaypointRepository**`GetByTaskAndGroup` → `GetByTaskAndWave`
- **IScenarioService.SaveRoute**`groupId` 参数 → `waveId`

View File

@ -116,17 +116,17 @@
"targets": [
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼DJI类",
"TargetType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
"DroneType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
{ "Id": "electric-scout", "Name": "[Demo] 电推侦察无人机",
"TargetType": 2, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
"DroneType": 1, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
{ "Id": "fixed-piston", "Name": "[Demo] 固定翼活塞Orlan类",
"TargetType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
"DroneType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
{ "Id": "shahed", "Name": "[Demo] 活塞巡飞弹Shahed类",
"TargetType": 3, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
"DroneType": 1, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
{ "Id": "tb2", "Name": "[Demo] 中空长航时TB2类",
"TargetType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
"DroneType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
{ "Id": "cruise-missile", "Name": "[Demo] 巡航导弹(喷气式)",
"TargetType": 4, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 200.0, "TypicalAltitude": 2000.0 }
"DroneType": 2, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 200.0, "TypicalAltitude": 2000.0 }
],
"detectionEquipment": [

View File

@ -5,9 +5,7 @@
"TypeCoefficient": {
"HighSpeed": 4.0,
"FixedWing": 2.0,
"Piston": 2.0,
"Rotor": 1.0,
"Electric": 1.0
"Rotor": 1.0
},
"AmmoMatch": {
"Electric": "InertGas",

View File

@ -134,7 +134,7 @@ public class SimulationRunner : MonoBehaviour
void Start()
{
// 通过 DI 或工厂获取实例Core 创建)
_engine = SimulationEngineFactory.Create(taskId);
_engine = SimulationEngineFactory.Create(scenarioId);
_recorder = ServiceFactory.CreateRecordService();
}
@ -185,7 +185,7 @@ enum WeatherType { Sunny = 0, Overcast = 1, Fog = 2, Rain = 3, Night = 4 }
enum WindDirection { N = 0, NE = 1, E = 2, SE = 3, S = 4, SW = 5, W = 6, NW = 7 }
// === 目标 ===
enum TargetType { Rotor = 0, FixedWing = 1, Electric = 2, Piston = 3, HighSpeed = 4 }
enum DroneType { Rotor = 0, FixedWing = 1, Electric = 2, Piston = 3, HighSpeed = 4 }
enum PowerType { Electric = 0, Piston = 1, Jet = 2 }
// === 装备 & 搭载平台 ===
@ -213,7 +213,7 @@ enum FormationMode { Single = 0, Formation = 1, Swarm = 2 }
// 两者都往统一信息网络送探测信息planner 基于融合后的探测边界规划。
// 无人机批次DroneWave有共同航路的一组无人机攻击方
// 批次关联航路但不独占(多个批次可共享同一条航路)。
// 配置层 = TargetConfig(含 WaveId) + RoutePlan(TaskId, WaveId) + Waypoints。
// 配置层 = DroneProfile(含 WaveId) + RoutePlan(ScenarioId, WaveId) + Waypoints。
// === 运行时 ===
enum EntityType { Drone = 0, Platform = 1, DetectionEquip = 2, Cloud = 3, Munition = 4 }
@ -249,8 +249,8 @@ CounterDroneBackend_Data/
│ └── SimulationReport
├── frames/ ← 帧数据分库6 个月 TTL
│ ├── {taskId_001}.db ← 单任务 ~161 MB30min 仿真)
│ └── {taskId_002}.db
│ ├── {scenarioId_001}.db ← 单任务 ~161 MB30min 仿真)
│ └── {scenarioId_002}.db
└── models/ ← 模型文件 + 预览图
```
@ -263,25 +263,25 @@ CounterDroneBackend_Data/
主库main.db— 永久保留:
ModelInfo — 模型元数据
AmmunitionSpec — 弹药基础参数第三方数据NEW
SimTask — 仿真任务主表
Scenario — 仿真任务主表
CombatScene — 步骤1作战场景含24h时间、天气
ControlZone — 步骤1+:管控区域/电子围栏NEW
TargetConfig — 步骤2目标配置含 WaveId 关联批次)
DroneProfile — 步骤2目标配置含 WaveId 关联批次)
EquipmentDeployment — 步骤3装备部署火力单元含探测+打击,或独立探测节点)
CloudDispersal — 步骤4云团抛撒配置
RoutePlan — 步骤5航路规划1:N SimTask多批次批次关联但不独占航路FK 关联 TargetConfig.WaveId
RoutePlan — 步骤5航路规划1:N Scenario多批次批次关联但不独占航路FK 关联 DroneProfile.WaveId
Waypoint — 航路点
SimulationReport — 仿真报告
SimEvent — 仿真事件(时序图数据源)
任务分库frames/{taskId}.db— 6 个月 TTL
任务分库frames/{scenarioId}.db— 6 个月 TTL
SimFrameRecord — 逐帧位置记录(回放用)
```
> **核心作战概念**
> - **火力单元FireUnit**:能独立完成搜索、跟踪、瞄准并实施打击的最小作战实体。包含探测(雷达/光电/红外)+ 打击(发射架/火炮)的完整闭环。配置层 = `EquipmentDeployment(LaunchPlatform)`
> - **独立探测节点DetectionNode**:只探测不打击的侦查节点(如前沿警戒雷达)。配置层 = `EquipmentDeployment(Detection)`
> - **无人机批次DroneWave**:有共同航路的一组无人机(攻击方)。批次关联航路但不独占(多个批次可共享同一条航路)。配置层 = `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints`
> - **无人机批次DroneWave**:有共同航路的一组无人机(攻击方)。批次关联航路但不独占(多个批次可共享同一条航路)。配置层 = `DroneProfile(WaveId)` + `RoutePlan(ScenarioId, WaveId)` + `Waypoints`
> - 原来的 `Group`(编组)表已移除:无人机编队概念升级为"批次",装备编组概念不再需要(火力单元是独立作战单位)。
### 4.3 业务表详细设计
@ -301,7 +301,7 @@ CounterDroneBackend_Data/
> 支持格式:**.fbx / .obj / .stl / .glb / .gltf**,单文件 ≤ 500MB支持 2K 及以上分辨率贴图。
#### SimTask — 仿真任务主表
#### Scenario — 仿真任务主表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
@ -318,7 +318,7 @@ CounterDroneBackend_Data/
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| TaskId | TEXT | PK, FK | 1:1 SimTask |
| ScenarioId | TEXT | PK, FK | 1:1 Scenario |
| SceneType | INTEGER | | 0城市 1平原 2山区 3海岸 |
| TimeOfDay | TEXT | DEFAULT "12:00" | **NEW** 24h 制时间,如 "14:30" |
| WeatherType | INTEGER | | 0晴 1阴 2雾 3雨 4夜 |
@ -339,7 +339,7 @@ CounterDroneBackend_Data/
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| ScenarioId | TEXT | FK | |
| Name | TEXT | | 区域名称 |
| VerticesJson | TEXT | NOT NULL | 多边形顶点 JSON`[{"x":0,"y":0,"z":0},...]` |
| MinAltitude | REAL | | 区域底高 m |
@ -348,14 +348,14 @@ CounterDroneBackend_Data/
> 管控区域在场景设置(地形+天气)之后配置。无人机进入管控区域 → 触发 ZoneIntruded 事件 → 该无人机任务失败。
#### TargetConfig — 步骤2目标配置
#### DroneProfile — 步骤2目标配置
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| ScenarioId | TEXT | FK | |
| WaveId | TEXT | FK | 关联无人机批次(同一 WaveId 的无人机共享航路) |
| TargetType | INTEGER | | 0旋翼 1固定翼 2电推 3活塞 4高速 |
| DroneType | INTEGER | | 0旋翼 1固定翼 2电推 3活塞 4高速 |
| Quantity | INTEGER | DEFAULT 1 | 目标数量 |
| PowerType | INTEGER | | 0电推 1活塞 2喷吸气 |
| Wingspan | REAL | | m |
@ -377,7 +377,7 @@ CounterDroneBackend_Data/
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| ScenarioId | TEXT | FK | |
| EquipmentRole | INTEGER | | 0=独立探测节点 1=火力单元(发射平台) |
| Quantity | INTEGER | DEFAULT 1 | 部署数量 |
| WaveId | TEXT | NULLABLE | 可选关联批次(用于 UI 分组,火力单元是独立作战单位,不强依赖批次) |
@ -414,7 +414,7 @@ CounterDroneBackend_Data/
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| TaskId | TEXT | PK, FK | 1:1 SimTask |
| ScenarioId | TEXT | PK, FK | 1:1 Scenario |
| AerosolType | INTEGER | | 0吸入式灭火 1爆燃式 2吸入式爆炸 |
| DisperseHeight | REAL | DEFAULT 300 | m |
| TriggerMode | INTEGER | | 0时间 1区域 2手动 |
@ -440,11 +440,11 @@ CounterDroneBackend_Data/
#### RoutePlan — 步骤5航路规划
> **支持多批次**:通过 `TaskId + WaveId` 唯一标识一条航路,一个任务可有多个批次各自独立航路。
> **支持多批次**:通过 `ScenarioId + WaveId` 唯一标识一条航路,一个任务可有多个批次各自独立航路。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| TaskId | TEXT | PK(复合) | |
| ScenarioId | TEXT | PK(复合) | |
| WaveId | TEXT | PK(复合) | 关联批次:同一任务不同批次可有独立航路(多个批次可共享同一条航路) |
| FormationMode | INTEGER | | 0单机 1编队 2蜂群 |
| FormationSpacing | REAL | DEFAULT 50 | m |
@ -455,7 +455,7 @@ CounterDroneBackend_Data/
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| ScenarioId | TEXT | FK | |
| OrderIndex | INTEGER | NOT NULL | 航路顺序 |
| PosX | REAL | | 坐标(支持三维点选) |
| PosY | REAL | | |
@ -466,21 +466,21 @@ CounterDroneBackend_Data/
#### ~~Group~~ — 编组(已移除)
> **概念升级**:原 Group 表的两种类型已分别升级:
> - `DroneFleet`(无人机编队)→ **无人机批次DroneWave**:由 `TargetConfig(WaveId)` + `RoutePlan(TaskId, WaveId)` + `Waypoints` 隐式表达。批次关联航路但不独占(多个批次可共享同一条航路)。
> - `DroneFleet`(无人机编队)→ **无人机批次DroneWave**:由 `DroneProfile(WaveId)` + `RoutePlan(ScenarioId, WaveId)` + `Waypoints` 隐式表达。批次关联航路但不独占(多个批次可共享同一条航路)。
> - `EquipmentGroup`(装备编组)→ **不再需要**。火力单元是独立作战的最小单位,不需要上层编组。
>
> 数据层:`Group` 表保留但不再主动使用(向后兼容)。`TargetConfig.WaveId` 语义为"批次 ID"。
> 数据层:`Group` 表保留但不再主动使用(向后兼容)。`DroneProfile.WaveId` 语义为"批次 ID"。
### 4.4 运行时表
> **注意**SimEvent 和 SimulationReport 存储在主库 main.dbSimFrameRecord 存储在各任务的独立分库 frames/{taskId}.db 中。
> **注意**SimEvent 和 SimulationReport 存储在主库 main.dbSimFrameRecord 存储在各任务的独立分库 frames/{scenarioId}.db 中。
#### SimFrameRecord — 逐帧记录
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | INTEGER | PK AUTOINCREMENT | |
| TaskId | TEXT | FK | |
| ScenarioId | TEXT | FK | |
| FrameIndex | INTEGER | | 帧序号 |
| Timestamp | REAL | | 仿真时间(秒) |
| EntityId | TEXT | | |
@ -511,7 +511,7 @@ CounterDroneBackend_Data/
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| Id | TEXT | PK | GUID |
| TaskId | TEXT | FK | |
| ScenarioId | TEXT | FK | |
| OccurredAt | REAL | | 仿真时间(秒) |
| EventType | INTEGER | | 事件类型枚举 |
| SourceId | TEXT | | 源实体 ID |
@ -691,7 +691,7 @@ Unity 渲染毁伤效果(尾焰异常、姿态失控、坠落)
- 当前位置、速度、朝向
- HP0~1 归一化)
- DamageStageNormal → EngineAnomaly → AttitudeLoss → Destroyed
- 目标类型 TargetType、动力类型 PowerType
- 目标类型 DroneType、动力类型 PowerType
- 存活状态(飞行中/抵达目标/侵入管控区/被摧毁)
运动模型(弧长驱动,统一在 RouteGeometry
@ -964,12 +964,12 @@ public interface IDamageModel
/// <param name="exposureTime">累计暴露时间s</param>
/// <param name="deltaTime">本次增量暴露时长s</param>
/// <returns>本次毁伤值0~1 归一化)</returns>
float CalculateDamage(TargetType droneType, PowerType powerType,
float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity,
float exposureTime, float deltaTime);
/// <summary>达到 100% 毁伤所需的连续暴露时间 (s),供 planner 估算拦截可行性</summary>
float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType);
float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType);
DamageStage GetDamageStage(float accumulatedDamage);
}
@ -1257,7 +1257,7 @@ public class FrameDataStore
// 内存回放Flush 后保留副本,供回放直接读取(零磁盘 IO
public List<SimFrameRecord> LiveFrames { get; private set; }
void BeginRecording(string taskId); // 清空 buffer + LiveFrames
void BeginRecording(string scenarioId); // 清空 buffer + LiveFrames
void RecordFrame(...); // 仅写内存 List<FrameRecord>
void Flush(); // 序列化 → LiveFrames 副本 + SQLite 入库
void Discard(); // 清空 buffer + LiveFrames
@ -1286,9 +1286,9 @@ public class FrameDataStore
### 7.3 回放
- `ReplayController.LoadReplay(taskId, frameStore)` 双路径:
- `ReplayController.LoadReplay(scenarioId, frameStore)` 双路径:
- 传入 `frameStore``LiveFrames` 非空 → 直接使用内存数据(仿真刚结束,零延迟)
- 否则 → 从 `frames/{taskId}.db` 读取(历史仿真)
- 否则 → 从 `frames/{scenarioId}.db` 读取(历史仿真)
- WHERE FrameIndex BETWEEN a AND b 命中索引,毫秒级
- 回放帧率由前端控制(可与录制不同)
- 支持暂停 / 快进 / 后退 / 跳转
@ -1316,12 +1316,12 @@ public class FrameDataStore
### 7.5 TTL 清理策略
```
仿真完成 → 生成 frames/{taskId}.db
仿真完成 → 生成 frames/{scenarioId}.db
CleanupExpired() 每天执行一次:
├─ 扫描 frames/ 目录
├─ 检查 {taskId}.db 的创建时间
├─ 检查 {scenarioId}.db 的创建时间
└─ 超过 6 个月 → 删除文件
可选扩展:
@ -1350,7 +1350,7 @@ public class FrameDataStore
| 阶段 | 内容 | 数据来源 |
|------|------|------|
| **仿真前** | 我方配置(装备类型/数量、火力单元部署、弹药类型/数量、部署位置、管控区域) | SimTask + 5 步配置 |
| **仿真前** | 我方配置(装备类型/数量、火力单元部署、弹药类型/数量、部署位置、管控区域) | Scenario + 5 步配置 |
| **仿真中** | 关键事件时序表、各实体轨迹概要、云团演化数据 | SimEvent + SimFrameRecord |
| **仿真后** | 拦截成功率、各阶段耗时、毁伤效果评估、对抗结果判定 | SimEvent 汇总统计 |
@ -1385,28 +1385,28 @@ interface IModelService
// === 想定管理 ===
interface IScenarioService
{
SimTask CreateTask(string name, string taskNumber);
Scenario CreateTask(string name, string taskNumber);
void DeleteTask(string id);
PagedResult<SimTask> SearchTasks(string kw, DateTime? from, DateTime? to, int page, int size);
TaskFullConfig GetTaskDetail(string id);
PagedResult<Scenario> SearchTasks(string kw, DateTime? from, DateTime? to, int page, int size);
ScenarioConfig GetTaskDetail(string id);
// 步骤保存
void SaveScene(string taskId, CombatScene scene);
void SaveControlZones(string taskId, List<ControlZone> zones); // NEW
void SaveTarget(string taskId, TargetConfig target);
void SaveDeployment(string taskId, List<EquipmentDeployment> equips); // 重构
void SaveCloudDispersal(string taskId, CloudDispersal cloud);
void SaveRoute(string taskId, RoutePlan route, List<Waypoint> wps);
void UpdateStep(string taskId, int step);
void SaveScene(string scenarioId, CombatScene scene);
void SaveControlZones(string scenarioId, List<ControlZone> zones); // NEW
void SaveTarget(string scenarioId, DroneProfile target);
void SaveDeployment(string scenarioId, List<EquipmentDeployment> equips); // 重构
void SaveCloudDispersal(string scenarioId, CloudDispersal cloud);
void SaveRoute(string scenarioId, RoutePlan route, List<Waypoint> wps);
void UpdateStep(string scenarioId, int step);
// 综合防御推荐NEW — 多编队版)
DefenseRecommendation GetDefenseRecommendation(string taskId);
DefenseRecommendation GetDefenseRecommendation(string scenarioId);
}
// === 仿真引擎 ===
interface ISimulationEngine
{
void Initialize(string taskId);
void Initialize(string scenarioId);
SimulationFrameResult Tick(float deltaTime); // 返回本帧快照+新事件(复用对象池)
void Pause();
void Resume();
@ -1420,19 +1420,19 @@ interface ISimulationEngine
// Stop/完成时调 Flush 批量落库。事件 SimEvent 由引擎直接写入主库。
class FrameDataStore
{
void BeginRecording(string taskId);
void BeginRecording(string scenarioId);
void RecordFrame(int frameIdx, float timestamp, List<EntitySnapshot> entities);
void Flush(); // 批量落库
void Discard();
List<SimFrameRecord> ReadFrames(string taskId, int fromFrame, int toFrame);
void DeleteTaskDb(string taskId);
List<SimFrameRecord> ReadFrames(string scenarioId, int fromFrame, int toFrame);
void DeleteTaskDb(string scenarioId);
int CleanupExpired(); // TTL 清理RetentionDays默认 180
}
// === 仿真报告 ===
interface IReportService
{
SimulationReport Generate(string taskId);
SimulationReport Generate(string scenarioId);
SimulationReport GetReport(string id);
void DeleteReport(string id);
byte[] ExportReport(string id, string format); // "pdf" | "word"
@ -1449,28 +1449,28 @@ interface IReportService
一个仿真任务可包含**多种类型、多批次**的无人机,每批次有独立的航路:
```
SimTask "城市防御演习"
Scenario "城市防御演习"
├── CombatScene共享
├── ControlZone[](共享)
├── CloudDispersal共享云团参数具体拦截方案由算法确定
├── 批次 A活塞×3 ← WaveId
│ ├── TargetConfigWaveId=A类型=活塞,数量=3
│ ├── RoutePlanTaskId + WaveId=A航路 A北→南
│ ├── DroneProfileWaveId=A类型=活塞,数量=3
│ ├── RoutePlanScenarioId + WaveId=A航路 A北→南
│ └── Waypoint[]
├── 批次 B喷气×2 ← WaveId
│ ├── TargetConfigWaveId=B类型=高速,数量=2
│ ├── RoutePlanTaskId + WaveId=B航路 B西→东
│ ├── DroneProfileWaveId=B类型=高速,数量=2
│ ├── RoutePlanScenarioId + WaveId=B航路 B西→东
│ └── Waypoint[]
└── 批次 C旋翼×5 ← WaveId
├── TargetConfigWaveId=C类型=旋翼,数量=5
├── RoutePlanTaskId + WaveId=C航路 C
├── DroneProfileWaveId=C类型=旋翼,数量=5
├── RoutePlanScenarioId + WaveId=C航路 C
└── Waypoint[]
```
**关键变更**`RoutePlan` 主键为 `(TaskId, WaveId)` 复合键。`Waypoint` 通过 `(TaskId, WaveId)` 关联航路。
**关键变更**`RoutePlan` 主键为 `(ScenarioId, WaveId)` 复合键。`Waypoint` 通过 `(ScenarioId, WaveId)` 关联航路。
### 推荐算法
@ -1515,13 +1515,13 @@ engine.SetFireSchedule(merged);
| 表 | 变更 |
|------|------|
| `RoutePlan` | PK 为 (TaskId, WaveId) 复合键 |
| `TargetConfig` | 不变(已有 WaveId |
| `Waypoint` | 关联 `(TaskId, WaveId)` 定位航路 |
| `RoutePlan` | PK 为 (ScenarioId, WaveId) 复合键 |
| `DroneProfile` | 不变(已有 WaveId |
| `Waypoint` | 关联 `(ScenarioId, WaveId)` 定位航路 |
`IScenarioService``SaveRoute` 签名:
```csharp
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> wps); // waveId 参数
void SaveRoute(string scenarioId, string waveId, RoutePlan route, List<Waypoint> wps); // waveId 参数
```
### 报告

View File

@ -59,7 +59,7 @@ class FireUnit {
```csharp
class DroneWave {
string WaveId; // 批次 ID
TargetConfig Target; // 类型、数量、动力、翼展、速度、高度
DroneProfile Target; // 类型、数量、动力、翼展、速度、高度
List<Waypoint> Waypoints; // 航路点
float ArrivalTime; // 预计算:到达防御区域中点的时间 s
}
@ -292,7 +292,7 @@ public class DefaultDefensePlanner : IDefensePlanner
| 旧接口 | 新接口 | 状态 |
|--------|--------|------|
| `IDefenseAdvisor.Recommend(ThreatProfile)` | `IDefensePlanner.Plan(fireUnits, threats, env)` | ✅ 已替换 |
| `IDefenseAdvisor.GetDefenseRecommendation(taskId)` | 不再需要 | ✅ 已删除 |
| `IDefenseAdvisor.GetDefenseRecommendation(scenarioId)` | 不再需要 | ✅ 已删除 |
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多批次 | ✅ 已删除 |
---
@ -300,7 +300,7 @@ public class DefaultDefensePlanner : IDefensePlanner
## 7. 数据流改造(已实现)
```
SimulationEngine.Initialize(taskId)
SimulationEngine.Initialize(scenarioId)
│ // 引擎内部组装
├── BuildFireUnits(config) → List<FireUnit>

View File

@ -39,7 +39,7 @@
## 数据流
```
想定配置EquipmentDeployment + TargetConfig + RoutePlan + CombatScene
想定配置EquipmentDeployment + DroneProfile + RoutePlan + CombatScene
SimulationEngine.Initialize()

View File

@ -181,7 +181,7 @@ Phase 12 ✅ 空基/地基统一规划 + 编队轴 + LaneDivider
| 8.1.1 | 探测设备搜索逻辑 | ✅ 事前规划:`DetectionCalculator` 算统一信息网络最早探测点planner 基于探测边界算到达时间;天气衰减光电;精度影响散布。**实时探测设计完成**(总体架构设计第十四章),待 Phase 10 开发 |
| 8.1.2 | 蜂群运动模型 | ⬜ `FormationMode.Swarm` 枚举已定义,需差异化行为(随机扰动、个体差异) |
| 8.1.3 | 空基平台 + DefensePlanner | ✅ 五步规划引擎,通道模型,物理间隔错发,路径积分毁伤判定 |
| 8.1.4 | 预置典型目标库 | ⬜ 具体无人机型号 JSON 配置(如 DJI Mavic 3、Shahed-136 等),导入 `TargetConfig` 默认值 |
| 8.1.4 | 预置典型目标库 | ⬜ 具体无人机型号 JSON 配置(如 DJI Mavic 3、Shahed-136 等),导入 `DroneProfile` 默认值 |
| 8.1.5 | 毁伤曲线参数校准 | ✅ RequiredExposureSeconds 替代硬编码,密度阈值统一在引擎检查 |
| 8.1.6 | Fallback/default 清理 | ✅ 删除所有硬编码默认值和静默 fallback参数缺失即报错 |
@ -339,7 +339,7 @@ T1数据层→ T2算法层 IsInCoverage
| # | 任务 | 说明 |
|---|------|------|
| 11.5.1 | FrameDataStore.LiveFrames | Flush 后保留副本BeginRecording/Discard 清除 |
| 11.5.2 | ReplayController 双路径 | LoadReplay(taskId, frameStore) 优先内存,回退 SQLite |
| 11.5.2 | ReplayController 双路径 | LoadReplay(scenarioId, frameStore) 优先内存,回退 SQLite |
### 11.6 测试启用

View File

@ -67,12 +67,12 @@ var scenario = GetComponent<ScenarioManager>();
// 方式 A预设想定最快
var demos = scenario.Search("Demo", null, null, 1, 100);
var taskId = demos.Items[0].Id;
var scenarioId = demos.Items[0].Id;
// 方式 B自定义
var task = scenario.CreateTask("我的想定", "");
scenario.SaveScene(task.Id, new CombatScene { WindSpeed = 5, WindDirection = (int)WindDirection.E });
scenario.SaveTarget(task.Id, new TargetConfig { WaveId = "w1", Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500 });
scenario.SaveTarget(task.Id, new DroneProfile { WaveId = "w1", Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500 });
scenario.SaveRoute(task.Id, "w1", new RoutePlan(), new List<Waypoint> {
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 }
@ -88,7 +88,7 @@ scenario.UpdateStep(task.Id, 5);
```csharp
var runner = GetComponent<SimulationRunner>();
runner.LoadAndStart(taskId);
runner.LoadAndStart(scenarioId);
runner.Engine.TimeScale = 4f;
// 事件
@ -110,8 +110,8 @@ void Update() {
```csharp
var rm = GetComponent<ReportManager>();
var detail = scenario.GetDetail(taskId);
var report = rm.Generate(taskId, detail, runner.Engine.Events.ToList(),
var detail = scenario.GetDetail(scenarioId);
var report = rm.Generate(scenarioId, detail, runner.Engine.Events.ToList(),
runner.Engine.Drones[0].Status.ToString(), runner.Engine.SimulationTime);
string path = rm.Export(report.Id);
```
@ -120,7 +120,7 @@ string path = rm.Export(report.Id);
```csharp
var replay = GetComponent<ReplayController>();
replay.LoadReplay(taskId);
replay.LoadReplay(scenarioId);
for (int i = 0; i < replay.TotalFrames; i++) {
var frame = replay.GetFrame(i);
// frame.SimulationTime, frame.EntitySnapshots...
@ -144,14 +144,14 @@ mgr.DataService.GetAllEnvironments() / GetAllFormations() / GetAllRoutes()
// 想定 CRUD
mgr.CreateTask(name, "") / mgr.DeleteTask(id) / mgr.Search(kw, from, to, page, size)
mgr.SaveScene / SaveTarget / SaveDeployment / SaveCloud / SaveRoute / UpdateStep
mgr.GetDetail(id) → TaskFullConfig
mgr.GetDetail(id) → ScenarioConfig
```
### SimulationRunner — 仿真执行
```csharp
var runner = GetComponent<SimulationRunner>();
runner.LoadAndStart(taskId);
runner.LoadAndStart(scenarioId);
runner.Engine.TimeScale = 4f;
runner.Engine.State / Drones / Clouds / Munitions / Platforms / DetectionEntities / Events
runner.Engine.Tick(dt) → SimulationFrameResult { EntitySnapshots, NewEvents }
@ -163,7 +163,7 @@ runner.Stop();
```csharp
var rm = GetComponent<ReportManager>();
rm.Generate(taskId, detail, events, status, duration) → SimulationReport
rm.Generate(scenarioId, detail, events, status, duration) → SimulationReport
rm.Export(reportId) → 文件路径
```
@ -171,7 +171,7 @@ rm.Export(reportId) → 文件路径
```csharp
var replay = GetComponent<ReplayController>();
replay.LoadReplay(taskId);
replay.LoadReplay(scenarioId);
replay.TotalFrames / replay.GetFrame(index)
```

View File

@ -9,20 +9,20 @@ namespace CounterDrone.Core.Algorithms
private const float BaseRate = 0.02f;
private const float ExpFactor = 0.3f;
public float CalculateDamage(TargetType droneType, PowerType powerType,
public float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.ActiveFuel) return 0f;
var exponential = (float)System.Math.Exp(exposureTime * ExpFactor);
var damage = BaseRate * exponential * deltaTime;
var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f;
var sensitivity = droneType == DroneType.HighSpeed ? 1.5f : 1.0f;
return damage * sensitivity;
}
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.ActiveFuel) return float.MaxValue;
var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f;
var sensitivity = droneType == DroneType.HighSpeed ? 1.5f : 1.0f;
// 数值求解BaseRate * e^(t*ExpFactor) * sensitivity 的积分 = 1
// ∫[0,T] BaseRate * s * e^(k*t) dt = BaseRate * s * (e^(k*T) - 1) / k = 1
// e^(k*T) = 1 + k / (BaseRate * s)

View File

@ -10,7 +10,7 @@ namespace CounterDrone.Core.Algorithms
private const float ResidualRate = 0.05f;
private bool _triggered;
public float CalculateDamage(TargetType droneType, PowerType powerType,
public float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.ActiveMaterial) return 0f;
@ -25,7 +25,7 @@ namespace CounterDrone.Core.Algorithms
return ResidualRate * deltaTime;
}
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.ActiveMaterial) return float.MaxValue;
var sensitivity = powerType == PowerType.Jet ? 1.3f : 1.0f;

View File

@ -51,7 +51,7 @@ namespace CounterDrone.Core.Algorithms
public class DroneWave
{
public string WaveId { get; set; } = string.Empty;
public TargetConfig Target { get; set; } = new();
public DroneProfile Target { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();

View File

@ -9,7 +9,7 @@ namespace CounterDrone.Core.Algorithms
private readonly ActiveMaterialDamageModel _activeMaterial = new();
private readonly ActiveFuelDamageModel _activeFuel = new();
public float CalculateDamage(TargetType droneType, PowerType powerType,
public float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
return aerosolType switch
@ -21,7 +21,7 @@ namespace CounterDrone.Core.Algorithms
};
}
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
{
return aerosolType switch
{

View File

@ -90,9 +90,9 @@ namespace CounterDrone.Core.Algorithms
// Step 1: 威胁指数
// ═══════════════════════════════════════════════
private static float CalcThreatIndex(PlannerConfig config, TargetConfig target)
private static float CalcThreatIndex(PlannerConfig config, DroneProfile target)
{
float typeCoef = config.TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f);
float typeCoef = config.TypeCoefficient.GetValueOrDefault((DroneType)target.DroneType, 1f);
float speedCoef = (float)target.TypicalSpeed / 60f;
return typeCoef * speedCoef;
}
@ -359,7 +359,7 @@ namespace CounterDrone.Core.Algorithms
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
float avgSpeed = GetDroneSpeedMs(threat);
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
float neededExposure = _damageModel.RequiredExposureSeconds((DroneType)threat.Target.DroneType, (PowerType)threat.Target.PowerType, ammoType);
float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
@ -394,7 +394,7 @@ namespace CounterDrone.Core.Algorithms
var cloudModel = new CloudExpansionModel(ammo, env);
float avgSpeed = GetDroneSpeedMs(threat);
float neededExposure = _damageModel.RequiredExposureSeconds(
(TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType);
(DroneType)threat.Target.DroneType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType);
float requiredCoverage = neededExposure * avgSpeed;
// 间距由配置的重叠比例驱动,公式在 CloudExpansionModel共享
float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
@ -411,7 +411,7 @@ namespace CounterDrone.Core.Algorithms
float actualExposure = actualCoverage / avgSpeed;
var aerosolType = (AerosolType)ammo.AerosolType;
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType);
float neededExposure = _damageModel.RequiredExposureSeconds((DroneType)threat.Target.DroneType, (PowerType)threat.Target.PowerType, aerosolType);
return Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
}

View File

@ -5,11 +5,11 @@ namespace CounterDrone.Core.Algorithms
/// <summary>毁伤模型接口</summary>
public interface IDamageModel
{
float CalculateDamage(TargetType droneType, PowerType powerType,
float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime);
/// <summary>达到 100% 毁伤所需的连续暴露时间 (s)</summary>
float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType);
float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType);
DamageStage GetDamageStage(float accumulatedDamage);
}

View File

@ -8,7 +8,7 @@ namespace CounterDrone.Core.Algorithms
{
private const float DamageRate = 0.15f; // 每秒毁伤率
public float CalculateDamage(TargetType droneType, PowerType powerType,
public float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.InertGas) return 0f;
@ -16,7 +16,7 @@ namespace CounterDrone.Core.Algorithms
return DamageRate * sensitivity * deltaTime;
}
public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType)
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.InertGas) return float.MaxValue;
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;

View File

@ -20,8 +20,8 @@ namespace CounterDrone.Core.Algorithms
/// <summary>拦截概率上限(封顶值)</summary>
public float MaxInterceptProbability { get; set; }
/// <summary>威胁类型系数表(TargetType → 系数)</summary>
public Dictionary<TargetType, float> TypeCoefficient { get; set; } = new();
/// <summary>威胁类型系数表(DroneType → 系数)</summary>
public Dictionary<DroneType, float> TypeCoefficient { get; set; } = new();
/// <summary>弹药匹配表PowerType → AerosolType</summary>
public Dictionary<PowerType, AerosolType> AmmoMatch { get; set; } = new();

View File

@ -31,19 +31,19 @@ namespace CounterDrone.Core
}
/// <summary>创建/打开任务帧数据库</summary>
public SQLiteConnection OpenFrameDb(string taskId)
public SQLiteConnection OpenFrameDb(string scenarioId)
{
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db");
var db = new SQLiteConnection(dbPath);
db.CreateTable<SimFrameRecord>();
db.CreateIndex("SimFrameRecord", new[] { "TaskId", "FrameIndex" });
db.CreateIndex("SimFrameRecord", new[] { "ScenarioId", "FrameIndex" });
return db;
}
/// <summary>删除任务帧数据库文件</summary>
public void DeleteFrameDb(string taskId)
public void DeleteFrameDb(string scenarioId)
{
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db");
if (File.Exists(dbPath))
File.Delete(dbPath);
}
@ -86,8 +86,8 @@ namespace CounterDrone.Core
var defaults = DefaultData.Load(_paths);
var scenario = new ScenarioService(
new SimTaskRepository(db), new CombatSceneRepository(db),
new ControlZoneRepository(db), new TargetConfigRepository(db),
new ScenarioRepository(db), new CombatSceneRepository(db),
new ControlZoneRepository(db), new DroneProfileRepository(db),
new EquipmentDeploymentRepository(db), new CloudDispersalRepository(db),
new RoutePlanRepository(db), new WaypointRepository(db));
@ -100,14 +100,14 @@ namespace CounterDrone.Core
{
db.CreateTable<ModelInfo>();
db.CreateTable<AmmunitionSpec>();
db.CreateTable<SimTask>();
db.CreateTable<Scenario>();
db.CreateTable<CombatScene>();
db.CreateTable<ControlZone>();
db.CreateTable<TargetConfig>();
db.CreateTable<DroneProfile>();
db.CreateTable<EquipmentDeployment>();
db.CreateTable<CloudDispersal>();
db.CreateTable<RoutePlan>();
db.CreateIndex("RoutePlan", new[] { "TaskId", "WaveId" }, true);
db.CreateIndex("RoutePlan", new[] { "ScenarioId", "WaveId" }, true);
db.CreateTable<Waypoint>();
db.CreateTable<SimulationReport>();
db.CreateTable<FireUnitSpec>();
@ -117,12 +117,12 @@ namespace CounterDrone.Core
db.CreateTable<FormationTemplate>();
db.CreateTable<RouteTemplate>();
db.CreateIndex("SimTask", "TaskNumber");
db.CreateIndex("ControlZone", "TaskId");
db.CreateIndex("TargetConfig", "TaskId");
db.CreateIndex("EquipmentDeployment", "TaskId");
db.CreateIndex("Waypoint", "TaskId");
db.CreateIndex("SimulationReport", "TaskId");
db.CreateIndex("Scenario", "TaskNumber");
db.CreateIndex("ControlZone", "ScenarioId");
db.CreateIndex("DroneProfile", "ScenarioId");
db.CreateIndex("EquipmentDeployment", "ScenarioId");
db.CreateIndex("Waypoint", "ScenarioId");
db.CreateIndex("SimulationReport", "ScenarioId");
}
}
}

View File

@ -163,18 +163,18 @@ namespace CounterDrone.Core
[SQLite.PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int TargetType { get; set; }
public int DroneType { get; set; }
public int PowerType { get; set; }
public double Wingspan { get; set; }
public double TypicalSpeed { get; set; }
public double TypicalAltitude { get; set; }
public TargetConfig ToTargetConfig(string waveId = "default")
public DroneProfile ToDroneProfile(string waveId = "default")
{
return new TargetConfig
return new DroneProfile
{
WaveId = waveId,
TargetType = TargetType,
DroneType = DroneType,
PowerType = PowerType,
Quantity = 1,
Wingspan = Wingspan,

View File

@ -37,7 +37,7 @@ namespace CounterDrone.Core
{
var t = s.CreateTask("[Demo] 无防御-无人机抵达目标", "");
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToDroneProfile());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("3km-h400", 600, d));
s.SaveCloudDispersal(t.Id, new CloudDispersal());
s.SaveDeployment(t.Id, new List<EquipmentDeployment>());
@ -48,7 +48,7 @@ namespace CounterDrone.Core
{
var t = s.CreateTask("[Demo] 管控区域侵入", "");
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "electric-scout").ToTargetConfig());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "electric-scout").ToDroneProfile());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("3km-h300", 300, d));
s.SaveCloudDispersal(t.Id, new CloudDispersal());
s.SaveDeployment(t.Id, new List<EquipmentDeployment>());
@ -71,7 +71,7 @@ namespace CounterDrone.Core
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.W;
s.SaveScene(t.Id, scene);
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToDroneProfile());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 6600, PosY = 500, PosZ = 0, Speed = 200 },
@ -89,7 +89,7 @@ namespace CounterDrone.Core
{
var t = s.CreateTask("[Demo] 喷气式拦截-活性材料", "");
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "cruise-missile").ToTargetConfig());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "cruise-missile").ToDroneProfile());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 18000, PosY = 500, PosZ = 0, Speed = 200 },
@ -110,7 +110,7 @@ namespace CounterDrone.Core
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.E;
s.SaveScene(t.Id, scene);
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToTargetConfig());
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToDroneProfile());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 9500, PosY = 500, PosZ = 0, Speed = 200 },
@ -131,7 +131,7 @@ namespace CounterDrone.Core
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.E;
s.SaveScene(t.Id, scene);
var target = d.Drones.First(p => p.Id == "shahed").ToTargetConfig();
var target = d.Drones.First(p => p.Id == "shahed").ToDroneProfile();
target.Quantity = 3;
s.SaveTarget(t.Id, target);
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(),

View File

@ -7,7 +7,7 @@ namespace CounterDrone.Core.Models
public class CloudDispersal
{
[PrimaryKey]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public int AerosolType { get; set; } = (int)Models.AerosolType.InertGas;

View File

@ -7,7 +7,7 @@ namespace CounterDrone.Core.Models
public class CombatScene
{
[PrimaryKey]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public int SceneType { get; set; } = (int)Models.SceneType.Plain;

View File

@ -11,7 +11,7 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;

View File

@ -4,18 +4,18 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤2目标配置</summary>
[Table("TargetConfig")]
public class TargetConfig
[Table("DroneProfile")]
public class DroneProfile
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public int TargetType { get; set; } = (int)Models.TargetType.Rotor;
public int DroneType { get; set; } = (int)Models.DroneType.Rotor;
public int Quantity { get; set; } = 1;

View File

@ -33,13 +33,11 @@ namespace CounterDrone.Core.Models
}
// === 目标 ===
public enum TargetType
public enum DroneType
{
Rotor = 0,
FixedWing = 1,
Electric = 2,
Piston = 3,
HighSpeed = 4
HighSpeed = 2
}
public enum PowerType

View File

@ -11,7 +11,7 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform;

View File

@ -11,7 +11,7 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
[Indexed]
public string WaveId { get; set; } = string.Empty;

View File

@ -4,8 +4,8 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>仿真任务主表</summary>
[Table("SimTask")]
public class SimTask
[Table("Scenario")]
public class Scenario
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();

View File

@ -3,12 +3,12 @@ using System.Collections.Generic;
namespace CounterDrone.Core.Models
{
/// <summary>想定完整配置(聚合 5 步配置)</summary>
public class TaskFullConfig
public class ScenarioConfig
{
public SimTask Task { get; set; } = new();
public Scenario Task { get; set; } = new();
public CombatScene Scene { get; set; } = new();
public List<ControlZone> ControlZones { get; set; } = new();
public List<TargetConfig> Targets { get; set; } = new();
public List<DroneProfile> Targets { get; set; } = new();
public List<EquipmentDeployment> Equipment { get; set; } = new();
public CloudDispersal Cloud { get; set; } = new();
/// <summary>多批次航路(多批次支持)</summary>

View File

@ -10,7 +10,7 @@ namespace CounterDrone.Core.Models
public int Id { get; set; }
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public int FrameIndex { get; set; }

View File

@ -11,7 +11,7 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
public string TaskName { get; set; } = string.Empty;

View File

@ -11,7 +11,7 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
/// <summary>关联的批次 ID多批次支持</summary>
public string WaveId { get; set; } = string.Empty;

View File

@ -9,17 +9,17 @@ namespace CounterDrone.Core.Repository
{
public ControlZoneRepository(SQLiteConnection db) : base(db) { }
public List<ControlZone> GetByTaskId(string taskId)
public List<ControlZone> GetByScenarioId(string scenarioId)
{
return Db.Table<ControlZone>()
.Where(z => z.TaskId == taskId)
.Where(z => z.ScenarioId == scenarioId)
.OrderBy(z => z.OrderIndex)
.ToList();
}
public void DeleteByTaskId(string taskId)
public void DeleteByScenarioId(string scenarioId)
{
var zones = GetByTaskId(taskId);
var zones = GetByScenarioId(scenarioId);
foreach (var z in zones)
Db.Delete(z);
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class DroneProfileRepository : BaseRepository<DroneProfile>
{
public DroneProfileRepository(SQLiteConnection db) : base(db) { }
public List<DroneProfile> GetByScenarioId(string scenarioId)
{
return Db.Table<DroneProfile>().Where(t => t.ScenarioId == scenarioId).ToList();
}
public void DeleteByScenarioId(string scenarioId)
{
var targets = GetByScenarioId(scenarioId);
foreach (var t in targets)
Db.Delete(t);
}
}
}

View File

@ -9,22 +9,22 @@ namespace CounterDrone.Core.Repository
{
public EquipmentDeploymentRepository(SQLiteConnection db) : base(db) { }
public List<EquipmentDeployment> GetByTaskId(string taskId)
public List<EquipmentDeployment> GetByScenarioId(string scenarioId)
{
return Db.Table<EquipmentDeployment>().Where(e => e.TaskId == taskId).ToList();
return Db.Table<EquipmentDeployment>().Where(e => e.ScenarioId == scenarioId).ToList();
}
/// <summary>按任务和角色查询装备EquipmentRole: 0=Detection, 1=LaunchPlatform</summary>
public List<EquipmentDeployment> GetByTaskIdAndRole(string taskId, int equipmentRole)
public List<EquipmentDeployment> GetByScenarioIdAndRole(string scenarioId, int equipmentRole)
{
return Db.Table<EquipmentDeployment>()
.Where(e => e.TaskId == taskId && e.EquipmentRole == equipmentRole)
.Where(e => e.ScenarioId == scenarioId && e.EquipmentRole == equipmentRole)
.ToList();
}
public void DeleteByTaskId(string taskId)
public void DeleteByScenarioId(string scenarioId)
{
var equips = GetByTaskId(taskId);
var equips = GetByScenarioId(scenarioId);
foreach (var e in equips)
Db.Delete(e);
}

View File

@ -9,15 +9,15 @@ namespace CounterDrone.Core.Repository
{
public RoutePlanRepository(SQLiteConnection db) : base(db) { }
public List<RoutePlan> GetByTaskId(string taskId)
public List<RoutePlan> GetByScenarioId(string scenarioId)
{
return Db.Table<RoutePlan>().Where(r => r.TaskId == taskId).ToList();
return Db.Table<RoutePlan>().Where(r => r.ScenarioId == scenarioId).ToList();
}
public RoutePlan GetByTaskAndWave(string taskId, string waveId)
public RoutePlan GetByTaskAndWave(string scenarioId, string waveId)
{
return Db.Table<RoutePlan>()
.FirstOrDefault(r => r.TaskId == taskId && r.WaveId == waveId);
.FirstOrDefault(r => r.ScenarioId == scenarioId && r.WaveId == waveId);
}
}
}

View File

@ -5,18 +5,18 @@ using SQLite;
namespace CounterDrone.Core.Repository
{
public class SimTaskRepository : BaseRepository<SimTask>
public class ScenarioRepository : BaseRepository<Scenario>
{
public SimTaskRepository(SQLiteConnection db) : base(db) { }
public ScenarioRepository(SQLiteConnection db) : base(db) { }
public SimTask GetByTaskNumber(string taskNumber)
public Scenario GetByTaskNumber(string taskNumber)
{
return Db.Table<SimTask>().FirstOrDefault(t => t.TaskNumber == taskNumber);
return Db.Table<Scenario>().FirstOrDefault(t => t.TaskNumber == taskNumber);
}
public List<SimTask> Search(string keyword, string? dateFrom, string? dateTo, int offset, int limit, out int totalCount)
public List<Scenario> Search(string keyword, string? dateFrom, string? dateTo, int offset, int limit, out int totalCount)
{
var query = Db.Table<SimTask>().AsQueryable();
var query = Db.Table<Scenario>().AsQueryable();
if (!string.IsNullOrWhiteSpace(keyword))
query = query.Where(t => t.Name.Contains(keyword) || t.TaskNumber.Contains(keyword));

View File

@ -1,24 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class TargetConfigRepository : BaseRepository<TargetConfig>
{
public TargetConfigRepository(SQLiteConnection db) : base(db) { }
public List<TargetConfig> GetByTaskId(string taskId)
{
return Db.Table<TargetConfig>().Where(t => t.TaskId == taskId).ToList();
}
public void DeleteByTaskId(string taskId)
{
var targets = GetByTaskId(taskId);
foreach (var t in targets)
Db.Delete(t);
}
}
}

View File

@ -9,25 +9,25 @@ namespace CounterDrone.Core.Repository
{
public WaypointRepository(SQLiteConnection db) : base(db) { }
public List<Waypoint> GetByTaskId(string taskId)
public List<Waypoint> GetByScenarioId(string scenarioId)
{
return Db.Table<Waypoint>()
.Where(w => w.TaskId == taskId)
.Where(w => w.ScenarioId == scenarioId)
.OrderBy(w => w.OrderIndex)
.ToList();
}
public List<Waypoint> GetByTaskAndWave(string taskId, string waveId)
public List<Waypoint> GetByTaskAndWave(string scenarioId, string waveId)
{
return Db.Table<Waypoint>()
.Where(w => w.TaskId == taskId && w.WaveId == waveId)
.Where(w => w.ScenarioId == scenarioId && w.WaveId == waveId)
.OrderBy(w => w.OrderIndex)
.ToList();
}
public void DeleteByTaskId(string taskId)
public void DeleteByScenarioId(string scenarioId)
{
var wps = GetByTaskId(taskId);
var wps = GetByScenarioId(scenarioId);
foreach (var w in wps)
Db.Delete(w);
}

View File

@ -7,7 +7,7 @@ namespace CounterDrone.Core.Services
/// <summary>报告服务</summary>
public interface IReportService
{
SimulationReport Generate(string taskId, TaskFullConfig config,
SimulationReport Generate(string scenarioId, ScenarioConfig config,
List<SimEvent> events, string finalDroneStatus, float duration);
SimulationReport GetReport(string id);
void DeleteReport(string id);

View File

@ -6,25 +6,25 @@ namespace CounterDrone.Core.Services
/// <summary>想定管理服务</summary>
public interface IScenarioService
{
SimTask CreateTask(string name, string taskNumber);
Scenario CreateTask(string name, string taskNumber);
void DeleteTask(string id);
PagedResult<SimTask> SearchTasks(string keyword, string? dateFrom, string? dateTo, int page, int pageSize);
TaskFullConfig? GetTaskDetail(string id);
PagedResult<Scenario> SearchTasks(string keyword, string? dateFrom, string? dateTo, int page, int pageSize);
ScenarioConfig? GetTaskDetail(string id);
void SaveScene(string taskId, CombatScene scene);
void SaveControlZones(string taskId, List<ControlZone> zones);
void SaveTarget(string taskId, TargetConfig target);
void SaveDeployment(string taskId, List<EquipmentDeployment> equips);
void SaveScene(string scenarioId, CombatScene scene);
void SaveControlZones(string scenarioId, List<ControlZone> zones);
void SaveTarget(string scenarioId, DroneProfile target);
void SaveDeployment(string scenarioId, List<EquipmentDeployment> equips);
/// <summary>添加单个探测设备(独立于 SaveDeployment不覆盖火力单元</summary>
void AddDetection(string taskId, EquipmentDeployment detection);
void AddDetection(string scenarioId, EquipmentDeployment detection);
/// <summary>删除探测设备</summary>
void DeleteDetection(string detectionId);
/// <summary>获取任务的所有探测设备EquipmentRole.Detection</summary>
List<EquipmentDeployment> GetDetections(string taskId);
List<EquipmentDeployment> GetDetections(string scenarioId);
void SaveCloudDispersal(string taskId, CloudDispersal cloud);
void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints);
void UpdateStep(string taskId, int step);
void SaveCloudDispersal(string scenarioId, CloudDispersal cloud);
void SaveRoute(string scenarioId, string waveId, RoutePlan route, List<Waypoint> waypoints);
void UpdateStep(string scenarioId, int step);
}
}

View File

@ -9,7 +9,7 @@ namespace CounterDrone.Core.Services
{
public class ReportGenerator
{
public string Generate(TaskFullConfig config, List<SimEvent> events,
public string Generate(ScenarioConfig config, List<SimEvent> events,
Simulation.DroneStatus finalDroneStatus, float simulationDuration)
{
var sb = new StringBuilder();
@ -70,7 +70,7 @@ namespace CounterDrone.Core.Services
sb.AppendLine();
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 类型 | {TranslateTarget((TargetType)(target?.TargetType ?? 0))} |");
sb.AppendLine($"| 类型 | {TranslateTarget((DroneType)(target?.DroneType ?? 0))} |");
sb.AppendLine($"| 动力 | {TranslatePower((PowerType)(target?.PowerType ?? 0))} |");
sb.AppendLine($"| 数量 | {totalDrones} 架 |");
sb.AppendLine($"| 翼展 | {target?.Wingspan ?? 0:F1} m |");
@ -282,11 +282,10 @@ namespace CounterDrone.Core.Services
TriggerMode.Manual => "手动触发", _ => t.ToString(),
};
private static string TranslateTarget(TargetType t) => t switch
private static string TranslateTarget(DroneType t) => t switch
{
TargetType.Rotor => "旋翼", TargetType.FixedWing => "固定翼",
TargetType.Electric => "电推", TargetType.Piston => "活塞",
TargetType.HighSpeed => "高速目标", _ => t.ToString(),
DroneType.Rotor => "旋翼", DroneType.FixedWing => "固定翼",
DroneType.HighSpeed => "高速目标", _ => t.ToString(),
};
private static string TranslatePower(PowerType p) => p switch

View File

@ -20,7 +20,7 @@ namespace CounterDrone.Core.Services
_paths = paths;
}
public SimulationReport Generate(string taskId, TaskFullConfig config,
public SimulationReport Generate(string scenarioId, ScenarioConfig config,
List<SimEvent> events, string finalDroneStatus, float duration)
{
var destroyed = events.Count(e => e.Type == SimEventType.DroneDestroyed);
@ -31,7 +31,7 @@ namespace CounterDrone.Core.Services
var report = new SimulationReport
{
TaskId = taskId,
ScenarioId = scenarioId,
TaskName = config.Task.Name,
TaskNumber = config.Task.TaskNumber,
CompletedAt = DateTime.UtcNow.ToString("o"),

View File

@ -9,20 +9,20 @@ namespace CounterDrone.Core.Services
{
public class ScenarioService : IScenarioService
{
private readonly SimTaskRepository _taskRepo;
private readonly ScenarioRepository _taskRepo;
private readonly CombatSceneRepository _sceneRepo;
private readonly ControlZoneRepository _zoneRepo;
private readonly TargetConfigRepository _targetRepo;
private readonly DroneProfileRepository _targetRepo;
private readonly EquipmentDeploymentRepository _equipRepo;
private readonly CloudDispersalRepository _cloudRepo;
private readonly RoutePlanRepository _routeRepo;
private readonly WaypointRepository _waypointRepo;
public ScenarioService(
SimTaskRepository taskRepo,
ScenarioRepository taskRepo,
CombatSceneRepository sceneRepo,
ControlZoneRepository zoneRepo,
TargetConfigRepository targetRepo,
DroneProfileRepository targetRepo,
EquipmentDeploymentRepository equipRepo,
CloudDispersalRepository cloudRepo,
RoutePlanRepository routeRepo,
@ -40,7 +40,7 @@ namespace CounterDrone.Core.Services
// ========== Task CRUD ==========
public SimTask CreateTask(string name, string taskNumber)
public Scenario CreateTask(string name, string taskNumber)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("任务名称不能为空");
@ -52,7 +52,7 @@ namespace CounterDrone.Core.Services
if (existing != null)
throw new ArgumentException($"任务编号 {taskNumber} 已存在");
var task = new SimTask
var task = new Scenario
{
Name = name,
TaskNumber = taskNumber,
@ -69,7 +69,7 @@ namespace CounterDrone.Core.Services
_taskRepo.Delete(id);
}
public PagedResult<SimTask> SearchTasks(string keyword, string? dateFrom, string? dateTo, int page, int pageSize)
public PagedResult<Scenario> SearchTasks(string keyword, string? dateFrom, string? dateTo, int page, int pageSize)
{
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10;
@ -77,7 +77,7 @@ namespace CounterDrone.Core.Services
var items = _taskRepo.Search(keyword, dateFrom, dateTo, offset, pageSize, out var total);
return new PagedResult<SimTask>
return new PagedResult<Scenario>
{
Items = items,
TotalCount = total,
@ -86,21 +86,21 @@ namespace CounterDrone.Core.Services
};
}
public TaskFullConfig? GetTaskDetail(string id)
public ScenarioConfig? GetTaskDetail(string id)
{
var task = _taskRepo.GetById(id);
if (task == null) return null;
return new TaskFullConfig
return new ScenarioConfig
{
Task = task,
Scene = _sceneRepo.GetById(id) ?? new CombatScene { TaskId = id },
ControlZones = _zoneRepo.GetByTaskId(id),
Targets = _targetRepo.GetByTaskId(id),
Equipment = _equipRepo.GetByTaskId(id),
Cloud = _cloudRepo.GetById(id) ?? new CloudDispersal { TaskId = id },
Routes = _routeRepo.GetByTaskId(id),
WaypointGroups = _waypointRepo.GetByTaskId(id)
Scene = _sceneRepo.GetById(id) ?? new CombatScene { ScenarioId = id },
ControlZones = _zoneRepo.GetByScenarioId(id),
Targets = _targetRepo.GetByScenarioId(id),
Equipment = _equipRepo.GetByScenarioId(id),
Cloud = _cloudRepo.GetById(id) ?? new CloudDispersal { ScenarioId = id },
Routes = _routeRepo.GetByScenarioId(id),
WaypointGroups = _waypointRepo.GetByScenarioId(id)
.GroupBy(w => w.WaveId)
.ToDictionary(g => g.Key, g => g.OrderBy(w => w.OrderIndex).ToList()),
};
@ -108,32 +108,32 @@ namespace CounterDrone.Core.Services
// ========== Step Save ==========
public void SaveScene(string taskId, CombatScene scene)
public void SaveScene(string scenarioId, CombatScene scene)
{
scene.TaskId = taskId;
var existing = _sceneRepo.GetById(taskId);
scene.ScenarioId = scenarioId;
var existing = _sceneRepo.GetById(scenarioId);
if (existing != null)
_sceneRepo.Update(scene);
else
_sceneRepo.Insert(scene);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void SaveControlZones(string taskId, List<ControlZone> zones)
public void SaveControlZones(string scenarioId, List<ControlZone> zones)
{
_zoneRepo.DeleteByTaskId(taskId);
_zoneRepo.DeleteByScenarioId(scenarioId);
for (int i = 0; i < zones.Count; i++)
{
zones[i].TaskId = taskId;
zones[i].ScenarioId = scenarioId;
zones[i].OrderIndex = i;
}
_zoneRepo.InsertAll(zones);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void SaveTarget(string taskId, TargetConfig target)
public void SaveTarget(string scenarioId, DroneProfile target)
{
target.TaskId = taskId;
target.ScenarioId = scenarioId;
if (string.IsNullOrEmpty(target.Id))
target.Id = Guid.NewGuid().ToString();
@ -142,30 +142,30 @@ namespace CounterDrone.Core.Services
_targetRepo.Update(target);
else
_targetRepo.Insert(target);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void SaveDeployment(string taskId, List<EquipmentDeployment> equips)
public void SaveDeployment(string scenarioId, List<EquipmentDeployment> equips)
{
_equipRepo.DeleteByTaskId(taskId);
_equipRepo.DeleteByScenarioId(scenarioId);
foreach (var e in equips)
{
e.TaskId = taskId;
e.ScenarioId = scenarioId;
if (string.IsNullOrEmpty(e.Id))
e.Id = Guid.NewGuid().ToString();
}
_equipRepo.InsertAll(equips);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void AddDetection(string taskId, EquipmentDeployment detection)
public void AddDetection(string scenarioId, EquipmentDeployment detection)
{
detection.TaskId = taskId;
detection.ScenarioId = scenarioId;
detection.EquipmentRole = (int)EquipmentRole.Detection;
if (string.IsNullOrEmpty(detection.Id))
detection.Id = Guid.NewGuid().ToString();
_equipRepo.Insert(detection);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void DeleteDetection(string detectionId)
@ -173,27 +173,27 @@ namespace CounterDrone.Core.Services
_equipRepo.Delete(detectionId);
}
public List<EquipmentDeployment> GetDetections(string taskId)
public List<EquipmentDeployment> GetDetections(string scenarioId)
{
return _equipRepo.GetByTaskIdAndRole(taskId, (int)EquipmentRole.Detection);
return _equipRepo.GetByScenarioIdAndRole(scenarioId, (int)EquipmentRole.Detection);
}
public void SaveCloudDispersal(string taskId, CloudDispersal cloud)
public void SaveCloudDispersal(string scenarioId, CloudDispersal cloud)
{
cloud.TaskId = taskId;
var existing = _cloudRepo.GetById(taskId);
cloud.ScenarioId = scenarioId;
var existing = _cloudRepo.GetById(scenarioId);
if (existing != null)
_cloudRepo.Update(cloud);
else
_cloudRepo.Insert(cloud);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void SaveRoute(string taskId, string waveId, RoutePlan route, List<Waypoint> waypoints)
public void SaveRoute(string scenarioId, string waveId, RoutePlan route, List<Waypoint> waypoints)
{
route.TaskId = taskId;
route.ScenarioId = scenarioId;
route.WaveId = waveId;
var existing = _routeRepo.GetByTaskAndWave(taskId, waveId);
var existing = _routeRepo.GetByTaskAndWave(scenarioId, waveId);
if (existing != null)
{
route.Id = existing.Id;
@ -203,23 +203,23 @@ namespace CounterDrone.Core.Services
_routeRepo.Insert(route);
// 删除该批次的旧航路点,插入新的
var oldWps = _waypointRepo.GetByTaskAndWave(taskId, waveId);
var oldWps = _waypointRepo.GetByTaskAndWave(scenarioId, waveId);
foreach (var w in oldWps) _waypointRepo.Delete(w.Id);
for (int i = 0; i < waypoints.Count; i++)
{
waypoints[i].TaskId = taskId;
waypoints[i].ScenarioId = scenarioId;
waypoints[i].WaveId = waveId;
waypoints[i].OrderIndex = i;
if (string.IsNullOrEmpty(waypoints[i].Id))
waypoints[i].Id = Guid.NewGuid().ToString();
}
_waypointRepo.InsertAll(waypoints);
TouchTask(taskId);
TouchTask(scenarioId);
}
public void UpdateStep(string taskId, int step)
public void UpdateStep(string scenarioId, int step)
{
var task = _taskRepo.GetById(taskId);
var task = _taskRepo.GetById(scenarioId);
if (task == null) return;
if (step < 1) step = 1;
@ -231,9 +231,9 @@ namespace CounterDrone.Core.Services
// ========== Helpers ==========
private void TouchTask(string taskId)
private void TouchTask(string scenarioId)
{
var task = _taskRepo.GetById(taskId);
var task = _taskRepo.GetById(scenarioId);
if (task != null)
{
task.UpdatedAt = DateTime.UtcNow.ToString("o");

View File

@ -9,7 +9,7 @@ namespace CounterDrone.Core.Simulation
{
public string Id { get; }
public string WaveId { get; }
public TargetType TargetType { get; }
public DroneType DroneType { get; }
public PowerType PowerType { get; }
public float Wingspan { get; }
public float CruiseSpeed { get; }
@ -38,13 +38,13 @@ namespace CounterDrone.Core.Simulation
/// 查找弧长 s 所在段:最大的 i 使 _cumArc[i] >= s。</summary>
private readonly float[] _cumArc;
public DroneEntity(string id, string waveId, TargetConfig config, List<Waypoint> route,
public DroneEntity(string id, string waveId, DroneProfile config, List<Waypoint> route,
int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing,
FormationMode mode, int lateralAxis = 2, int longitudinalAxis = 0)
{
Id = id;
WaveId = waveId;
TargetType = (TargetType)config.TargetType;
DroneType = (DroneType)config.DroneType;
PowerType = (PowerType)config.PowerType;
Wingspan = (float)config.Wingspan;
float spd = (float)route[0].Speed;

View File

@ -11,7 +11,7 @@ namespace CounterDrone.Core.Simulation
{
private readonly IPathProvider _paths;
private readonly List<FrameRecord> _buffer = new();
private string _taskId = string.Empty;
private string _scenarioId = string.Empty;
public int RetentionDays { get; set; } = 180;
public int BufferedFrameCount { get; private set; }
/// <summary>仿真刚结束时保留内存快照支持快速回放。Flush 后自动清空。</summary>
@ -23,9 +23,9 @@ namespace CounterDrone.Core.Simulation
}
/// <summary>开始录制。清空缓冲区,准备接收帧数据。</summary>
public void BeginRecording(string taskId)
public void BeginRecording(string scenarioId)
{
_taskId = taskId;
_scenarioId = scenarioId;
_buffer.Clear();
LiveFrames.Clear();
BufferedFrameCount = 0;
@ -65,7 +65,7 @@ namespace CounterDrone.Core.Simulation
var s = fr.Snapshot;
var record = new SimFrameRecord
{
TaskId = _taskId, FrameIndex = fr.FrameIndex, Timestamp = fr.Timestamp,
ScenarioId = _scenarioId, FrameIndex = fr.FrameIndex, Timestamp = fr.Timestamp,
EntityId = s.EntityId, EntityType = (int)s.EntityType,
PosX = s.PosX, PosY = s.PosY, PosZ = s.PosZ,
StateData = s.EntityType switch
@ -84,30 +84,30 @@ namespace CounterDrone.Core.Simulation
_buffer.Clear();
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{_taskId}.db");
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{_scenarioId}.db");
Directory.CreateDirectory(_paths.GetFramesDir());
using var db = new SQLiteConnection(dbPath);
db.CreateTable<SimFrameRecord>();
db.CreateIndex("SimFrameRecord", new[] { "TaskId", "FrameIndex" });
db.CreateIndex("SimFrameRecord", new[] { "ScenarioId", "FrameIndex" });
db.InsertAll(records);
}
public void Discard() { _buffer.Clear(); LiveFrames.Clear(); BufferedFrameCount = 0; }
public List<SimFrameRecord> ReadFrames(string taskId, int fromFrame, int toFrame)
public List<SimFrameRecord> ReadFrames(string scenarioId, int fromFrame, int toFrame)
{
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db");
if (!File.Exists(dbPath)) return new List<SimFrameRecord>();
using var db = new SQLiteConnection(dbPath);
return db.Table<SimFrameRecord>()
.Where(f => f.TaskId == taskId && f.FrameIndex >= fromFrame && f.FrameIndex <= toFrame)
.Where(f => f.ScenarioId == scenarioId && f.FrameIndex >= fromFrame && f.FrameIndex <= toFrame)
.OrderBy(f => f.FrameIndex)
.ToList();
}
public void DeleteFrameDb(string taskId)
public void DeleteFrameDb(string scenarioId)
{
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db");
if (File.Exists(dbPath)) File.Delete(dbPath);
}

View File

@ -54,7 +54,7 @@ namespace CounterDrone.Core.Simulation
private bool _anyThreatDetected;
private float _detectionTime;
private readonly List<SimEvent> _allEvents = new();
private string _taskId = string.Empty;
private string _scenarioId = string.Empty;
private readonly IDefensePlanner _planner;
private int _entityCounter;
@ -82,10 +82,10 @@ namespace CounterDrone.Core.Simulation
_nextFireIndex = 0;
}
public void Initialize(string taskId)
public void Initialize(string scenarioId)
{
_taskId = taskId;
var config = _scenarioService.GetTaskDetail(taskId);
_scenarioId = scenarioId;
var config = _scenarioService.GetTaskDetail(scenarioId);
if (config == null) throw new InvalidOperationException("Task not found");
_scene = config.Scene;
@ -179,7 +179,7 @@ namespace CounterDrone.Core.Simulation
}
if (RecordFrames)
_frameStore.BeginRecording(_taskId);
_frameStore.BeginRecording(_scenarioId);
_anyThreatDetected = false;
State = SimulationState.Running;
}
@ -363,7 +363,7 @@ namespace CounterDrone.Core.Simulation
{
float exposureIncrement = inCloudDistance / (drone.CruiseSpeed / 3.6f);
drone.ExposureTime += exposureIncrement;
var dmg = _damageModel.CalculateDamage(drone.TargetType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, exposureIncrement);
var dmg = _damageModel.CalculateDamage(drone.DroneType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, exposureIncrement);
drone.ApplyDamage(dmg);
if (drone.Status == DroneStatus.Destroyed)
{
@ -427,7 +427,7 @@ namespace CounterDrone.Core.Simulation
public void Resume() { if (State == SimulationState.Paused) State = SimulationState.Running; }
public void Stop() { State = SimulationState.Stopped; _frameStore.Flush(); }
private static List<FireUnit> BuildFireUnits(TaskFullConfig config)
private static List<FireUnit> BuildFireUnits(ScenarioConfig config)
{
var units = new List<FireUnit>();
foreach (var eq in config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
@ -464,7 +464,7 @@ namespace CounterDrone.Core.Simulation
/// <summary>构建统一信息网络的探测源列表。
/// 独立探测设备EquipmentRole.Detection+ 火力单元自带探测能力。</summary>
private static List<DetectionSource> BuildDetectionSources(TaskFullConfig config)
private static List<DetectionSource> BuildDetectionSources(ScenarioConfig config)
{
var sources = new List<DetectionSource>();
foreach (var eq in config.Equipment)
@ -493,7 +493,7 @@ namespace CounterDrone.Core.Simulation
return sources;
}
private static List<DroneWave> BuildDroneWaves(TaskFullConfig config)
private static List<DroneWave> BuildDroneWaves(ScenarioConfig config)
{
var groups = new List<DroneWave>();
foreach (var t in config.Targets)

View File

@ -29,34 +29,34 @@ namespace CounterDrone.Unity
var scenarioMgr = gameObject.AddComponent<ScenarioManager>();
scenarioMgr.Awake();
var task = scenarioMgr.CreateTask("Verification", "");
var taskId = task.Id;
Debug.Log($"2. ScenarioManager OK. Task: {taskId}");
var scenarioId = task.Id;
Debug.Log($"2. ScenarioManager OK. Task: {scenarioId}");
scenarioMgr.SaveScene(taskId, new CombatScene { WindSpeed = 0 });
scenarioMgr.SaveTarget(taskId, new TargetConfig
scenarioMgr.SaveScene(scenarioId, new CombatScene { WindSpeed = 0 });
scenarioMgr.SaveTarget(scenarioId, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
DroneType = (int)DroneType.FixedWing, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
scenarioMgr.SaveRoute(taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
scenarioMgr.SaveRoute(scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
});
scenarioMgr.UpdateStep(taskId, 3);
scenarioMgr.UpdateStep(scenarioId, 3);
Debug.Log("3. Scenario configured");
var db = new DatabaseManager(paths).OpenMainDb();
SqliteConnectionTracker.Track(db);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var detail = scenarioMgr.GetDetail(taskId);
var detail = scenarioMgr.GetDetail(scenarioId);
var droneGroup = new DroneWave
{
WaveId = "default",
Target = detail.Targets[0],
DroneType = detail.Targets[0].DroneType,
Route = detail.Routes[0],
Waypoints = detail.WaypointGroups["default"],
};
@ -72,7 +72,7 @@ namespace CounterDrone.Unity
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>());
scenarioMgr.SaveCloud(taskId, new CloudDispersal
scenarioMgr.SaveCloud(scenarioId, new CloudDispersal
{
PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2,
PositionY = (droneGroup.Waypoints[0].PosY + droneGroup.Waypoints[^1].PosY) / 2,
@ -92,7 +92,7 @@ namespace CounterDrone.Unity
CruiseSpeed = u.CruiseSpeed > 0 ? u.CruiseSpeed : null,
ReleaseAltitude = u.ReleaseAltitude > 0 ? u.ReleaseAltitude : null,
});
scenarioMgr.SaveDeployment(taskId, equips);
scenarioMgr.SaveDeployment(scenarioId, equips);
SqliteConnectionTracker.Untrack(db);
db.Dispose();
var plan = result.Best;
@ -100,7 +100,7 @@ namespace CounterDrone.Unity
var runner = gameObject.AddComponent<SimulationRunner>();
runner.Awake();
runner.LoadAndStart(taskId);
runner.LoadAndStart(scenarioId);
runner.Engine.TimeScale = 10f;
float endTime = Time.realtimeSinceStartup + 60f;
@ -116,16 +116,16 @@ namespace CounterDrone.Unity
var reportMgr = gameObject.AddComponent<ReportManager>();
reportMgr.Awake();
detail = scenarioMgr.GetDetail(taskId);
detail = scenarioMgr.GetDetail(scenarioId);
var simEvents = new List<SimEvent>(runner.Engine.Events);
var status = runner.Engine.Drones.Count > 0 ? runner.Engine.Drones[0].Status.ToString() : "Unknown";
var report = reportMgr.Generate(taskId, detail, simEvents, status, runner.Engine.SimulationTime);
var report = reportMgr.Generate(scenarioId, detail, simEvents, status, runner.Engine.SimulationTime);
var path = reportMgr.Export(report.Id);
Debug.Log($"6. Report OK. Exported: {path}");
var replay = gameObject.AddComponent<ReplayController>();
replay.Awake();
replay.LoadReplay(taskId);
replay.LoadReplay(scenarioId);
Debug.Log($"7. Replay OK. {replay.TotalFrames} frames, {replay.CurrentFrames.Count} records");
Debug.Log("===== ALL MANAGERS VERIFIED =====");

View File

@ -24,7 +24,7 @@ namespace CounterDrone.Unity
}
/// <summary>加载回放数据:优先使用传入的 frameStore 内存数据(仿真刚结束),否则读数据库</summary>
public void LoadReplay(string taskId, FrameDataStore frameStore = null)
public void LoadReplay(string scenarioId, FrameDataStore frameStore = null)
{
if (frameStore != null && frameStore.LiveFrames.Count > 0)
{
@ -34,7 +34,7 @@ namespace CounterDrone.Unity
else
{
_frameStore ??= new FrameDataStore(_paths);
CurrentFrames = _frameStore.ReadFrames(taskId, 0, int.MaxValue);
CurrentFrames = _frameStore.ReadFrames(scenarioId, 0, int.MaxValue);
}
TotalFrames = CurrentFrames.Count > 0

View File

@ -25,17 +25,17 @@ namespace CounterDrone.Unity
void OnDisable() { SqliteConnectionTracker.Untrack(_db); _db?.Dispose(); _db = null; }
public SimulationReport Generate(string taskId, TaskFullConfig cfg, List<SimEvent> events, string status, float duration)
=> _service.Generate(taskId, cfg, events, status, duration);
public SimulationReport Generate(string scenarioId, ScenarioConfig cfg, List<SimEvent> events, string status, float duration)
=> _service.Generate(scenarioId, cfg, events, status, duration);
public SimulationReport Get(string id) => _service.GetReport(id);
public void Delete(string id) => _service.DeleteReport(id);
public PagedResult<SimulationReport> Search(string kw, string from, string to, int page, int size)
=> _service.SearchReports(kw, from, to, page, size);
public string Export(string reportId)
public string Export(string reportId, string? outputDir = null)
{
var dir = System.IO.Path.Combine(_paths.GetDataRoot(), "reports");
var dir = outputDir ?? System.IO.Path.Combine(_paths.GetDataRoot(), "reports");
return _service.ExportToFile(reportId, dir);
}

View File

@ -26,8 +26,8 @@ namespace CounterDrone.Unity
_db = new DatabaseManager(paths).OpenMainDb();
SqliteConnectionTracker.Track(_db);
_service = new ScenarioService(
new SimTaskRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new TargetConfigRepository(_db),
new ScenarioRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new DroneProfileRepository(_db),
new EquipmentDeploymentRepository(_db), new CloudDispersalRepository(_db),
new RoutePlanRepository(_db), new WaypointRepository(_db));
_dataService = new DataService(
@ -45,18 +45,18 @@ namespace CounterDrone.Unity
void OnDisable() { SqliteConnectionTracker.Untrack(_db); _db?.Dispose(); _db = null; }
public SimTask CreateTask(string name, string number) => _service.CreateTask(name, number);
public Scenario CreateTask(string name, string number) => _service.CreateTask(name, number);
public void DeleteTask(string id) => _service.DeleteTask(id);
public TaskFullConfig GetDetail(string id) => _service.GetTaskDetail(id);
public PagedResult<SimTask> Search(string kw, string from, string to, int page, int size)
public ScenarioConfig GetDetail(string id) => _service.GetTaskDetail(id);
public PagedResult<Scenario> Search(string kw, string from, string to, int page, int size)
=> _service.SearchTasks(kw, from, to, page, size);
public void SaveScene(string taskId, CombatScene s) => _service.SaveScene(taskId, s);
public void SaveTarget(string taskId, TargetConfig t) => _service.SaveTarget(taskId, t);
public void SaveDeployment(string taskId, List<EquipmentDeployment> e) => _service.SaveDeployment(taskId, e);
public void SaveCloud(string taskId, CloudDispersal c) => _service.SaveCloudDispersal(taskId, c);
public void SaveRoute(string taskId, string waveId, RoutePlan r, List<Waypoint> w) => _service.SaveRoute(taskId, waveId, r, w);
public void UpdateStep(string taskId, int step) => _service.UpdateStep(taskId, step);
public void SaveScene(string scenarioId, CombatScene s) => _service.SaveScene(scenarioId, s);
public void SaveTarget(string scenarioId, DroneProfile t) => _service.SaveTarget(scenarioId, t);
public void SaveDeployment(string scenarioId, List<EquipmentDeployment> e) => _service.SaveDeployment(scenarioId, e);
public void SaveCloud(string scenarioId, CloudDispersal c) => _service.SaveCloudDispersal(scenarioId, c);
public void SaveRoute(string scenarioId, string waveId, RoutePlan r, List<Waypoint> w) => _service.SaveRoute(scenarioId, waveId, r, w);
public void UpdateStep(string scenarioId, int step) => _service.UpdateStep(scenarioId, step);
[ContextMenu("Verify")]
void Verify()

View File

@ -42,12 +42,12 @@ namespace CounterDrone.Unity
new RouteTemplateRepository(db));
var scenario = new ScenarioService(
new SimTaskRepository(db), new CombatSceneRepository(db),
new ControlZoneRepository(db), new TargetConfigRepository(db),
new ScenarioRepository(db), new CombatSceneRepository(db),
new ControlZoneRepository(db), new DroneProfileRepository(db),
new EquipmentDeploymentRepository(db), new CloudDispersalRepository(db),
new RoutePlanRepository(db), new WaypointRepository(db));
string taskId;
string scenarioId;
if (_usePreset)
{
@ -58,7 +58,7 @@ namespace CounterDrone.Unity
Debug.LogError("无预设想定,请确认 defaults.json 已放入 StreamingAssets");
return;
}
taskId = demos.Items[0].Id;
scenarioId = demos.Items[0].Id;
Debug.Log($"使用预设想定: {demos.Items[0].Name}");
}
else
@ -67,17 +67,17 @@ namespace CounterDrone.Unity
var defaults = DefaultData.Load(paths);
var task = scenario.CreateTask("Unity Demo", "");
taskId = task.Id;
scenarioId = task.Id;
scenario.SaveScene(taskId, defaults.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
scenario.SaveTarget(taskId, defaults.Drones.First(p => p.Id == "shahed").ToTargetConfig());
scenario.SaveRoute(taskId, "default",
scenario.SaveScene(scenarioId, defaults.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
scenario.SaveTarget(scenarioId, defaults.Drones.First(p => p.Id == "shahed").ToDroneProfile());
scenario.SaveRoute(scenarioId, "default",
defaults.Formations.First(f => f.Id == "single").ToRoutePlan(),
defaults.Routes.First(r => _routeLength > 6000 ? r.Id == "10km-h500" : r.Id == "5km-h500")
.ToWaypoints(_droneSpeed));
// 从火力单元预设构建 FireUnit → Planner 自动生成方案 → 引擎执行
var detail = scenario.GetTaskDetail(taskId);
var detail = scenario.GetTaskDetail(scenarioId);
var ammoCatalog = db.Table<AmmunitionSpec>().ToList();
var planner = new DefensePlanner(ammoCatalog, PlannerConfig.Load(paths));
var threats = new List<DroneWave>
@ -96,18 +96,18 @@ namespace CounterDrone.Unity
});
var plan = planner.Plan(fireUnits, threats, detail.Scene, new List<DetectionSource>());
scenario.SaveCloudDispersal(taskId, new CloudDispersal
scenario.SaveCloudDispersal(scenarioId, new CloudDispersal
{
AerosolType = (int)AerosolType.InertGas,
DisperseHeight = detail.Targets[0].TypicalAltitude,
Source = "Algorithm",
});
scenario.SaveDeployment(taskId, new List<EquipmentDeployment>
scenario.SaveDeployment(scenarioId, new List<EquipmentDeployment>
{
defaults.FireUnits.First(f => f.Id == "ground-light")
.ToEquipmentDeployment(AerosolType.InertGas, 1, _routeLength / 2f, 0, 50),
});
scenario.UpdateStep(taskId, 5);
scenario.UpdateStep(scenarioId, 5);
Debug.Log($"自定义想定: {plan.Best.ThreatsEngaged} 威胁已分配, {plan.Best.MergedSchedule.Count} 发, 概率 {plan.Best.OverallProbability:P0}");
}
@ -120,7 +120,7 @@ namespace CounterDrone.Unity
if (Runner == null) Runner = gameObject.AddComponent<SimulationRunner>();
else Runner.enabled = true;
Runner.DataService = dataService;
Runner.LoadAndStart(taskId);
Runner.LoadAndStart(scenarioId);
Runner.Engine.TimeScale = _timeScale;
// 定位相机

View File

@ -23,7 +23,7 @@ namespace CounterDrone.Unity
public FrameDataStore FrameStore => _frameStore;
private IScenarioService _scenario;
private IPathProvider _paths;
private string _taskId;
private string _scenarioId;
private SQLite.SQLiteConnection _db;
private readonly Dictionary<string, GameObject> _entityVisuals = new();
@ -43,8 +43,8 @@ namespace CounterDrone.Unity
SqliteConnectionTracker.Track(_db);
_scenario = new ScenarioService(
new SimTaskRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new TargetConfigRepository(_db),
new ScenarioRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new DroneProfileRepository(_db),
new EquipmentDeploymentRepository(_db), new CloudDispersalRepository(_db),
new RoutePlanRepository(_db), new WaypointRepository(_db));
@ -52,13 +52,13 @@ namespace CounterDrone.Unity
_engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(DefaultData.Load(_paths).Ammunition, PlannerConfig.Load(_paths)));
}
public void LoadAndStart(string taskId)
public void LoadAndStart(string scenarioId)
{
_taskId = taskId;
var detail = _scenario.GetTaskDetail(taskId);
_scenarioId = scenarioId;
var detail = _scenario.GetTaskDetail(scenarioId);
if (detail == null) { Debug.LogError("Task not found"); return; }
_engine.Initialize(taskId);
_engine.Initialize(scenarioId);
foreach (var drone in _engine.Drones)
SpawnDroneVisual(drone);

View File

@ -116,17 +116,17 @@
"targets": [
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼DJI类",
"TargetType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
"DroneType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
{ "Id": "electric-scout", "Name": "[Demo] 电推侦察无人机",
"TargetType": 2, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
"DroneType": 1, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
{ "Id": "fixed-piston", "Name": "[Demo] 固定翼活塞Orlan类",
"TargetType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
"DroneType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
{ "Id": "shahed", "Name": "[Demo] 活塞巡飞弹Shahed类",
"TargetType": 3, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
"DroneType": 1, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
{ "Id": "tb2", "Name": "[Demo] 中空长航时TB2类",
"TargetType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
"DroneType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
{ "Id": "cruise-missile", "Name": "[Demo] 巡航导弹(喷气式)",
"TargetType": 4, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 200.0, "TypicalAltitude": 2000.0 }
"DroneType": 2, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 200.0, "TypicalAltitude": 2000.0 }
],
"detectionEquipment": [

View File

@ -12,7 +12,7 @@ namespace CounterDrone.Core.Tests
var model = new InertGasDamageModel();
// 活塞发动机暴露于惰性气体
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
var dmg = model.CalculateDamage(DroneType.FixedWing, PowerType.Piston,
AerosolType.InertGas, cloudDensity: 0.1f, exposureTime: 2f, deltaTime: 1f);
Assert.True(dmg > 0.1f); // 高于基础速率0.15 × 1.5 = 0.225
@ -23,7 +23,7 @@ namespace CounterDrone.Core.Tests
{
var model = new InertGasDamageModel();
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
var dmg = model.CalculateDamage(DroneType.FixedWing, PowerType.Piston,
AerosolType.ActiveMaterial, cloudDensity: 0.1f, exposureTime: 2f, deltaTime: 1f);
Assert.Equal(0f, dmg);
@ -34,12 +34,12 @@ namespace CounterDrone.Core.Tests
{
var model = new ActiveMaterialDamageModel();
var dmg1 = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
var dmg1 = model.CalculateDamage(DroneType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 1f, deltaTime: 1f);
Assert.True(dmg1 >= 0.5f); // 爆发伤害
var dmg2 = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
var dmg2 = model.CalculateDamage(DroneType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 2f, deltaTime: 1f);
Assert.True(dmg2 < 0.1f); // 后续余伤很小
@ -50,7 +50,7 @@ namespace CounterDrone.Core.Tests
{
var model = new ActiveMaterialDamageModel();
var dmg = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
var dmg = model.CalculateDamage(DroneType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 0f, deltaTime: 0.1f);
// 喷气发动机0.6 × 1.3 = 0.78
@ -62,10 +62,10 @@ namespace CounterDrone.Core.Tests
{
var model = new ActiveFuelDamageModel();
var dmg1 = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
var dmg1 = model.CalculateDamage(DroneType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 1f, deltaTime: 1f);
var dmg2 = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
var dmg2 = model.CalculateDamage(DroneType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 5f, deltaTime: 1f);
// 5 秒后的伤害应显著大于 1 秒后
@ -77,7 +77,7 @@ namespace CounterDrone.Core.Tests
{
var model = new ActiveFuelDamageModel();
var dmg = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
var dmg = model.CalculateDamage(DroneType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 1f, deltaTime: 1f);
Assert.True(dmg > 0.02f); // 基础速率 × 灵敏度
@ -100,17 +100,17 @@ namespace CounterDrone.Core.Tests
var router = new DamageModelRouter();
// 惰性气体
var dmg1 = router.CalculateDamage(TargetType.Piston, PowerType.Piston,
var dmg1 = router.CalculateDamage(DroneType.FixedWing, PowerType.Piston,
AerosolType.InertGas, 0.1f, 1f, 1f);
Assert.True(dmg1 > 0f);
// 活性材料
var dmg2 = router.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
var dmg2 = router.CalculateDamage(DroneType.FixedWing, PowerType.Jet,
AerosolType.ActiveMaterial, 0.2f, 0f, 1f);
Assert.True(dmg2 > 0f);
// 活性燃料
var dmg3 = router.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
var dmg3 = router.CalculateDamage(DroneType.HighSpeed, PowerType.Jet,
AerosolType.ActiveFuel, 0.1f, 1f, 1f);
Assert.True(dmg3 > 0f);
}

View File

@ -34,8 +34,8 @@ namespace CounterDrone.Core.Tests
// 验证所有 12 张表存在(含 Meta
var tables = new[]
{
"ModelInfo", "AmmunitionSpec", "SimTask", "CombatScene",
"ControlZone", "TargetConfig", "EquipmentDeployment",
"ModelInfo", "AmmunitionSpec", "Scenario", "CombatScene",
"ControlZone", "DroneProfile", "EquipmentDeployment",
"CloudDispersal", "RoutePlan", "Waypoint",
"SimulationReport"
};
@ -65,13 +65,13 @@ namespace CounterDrone.Core.Tests
[Fact]
public void DeleteFrameDb_RemovesFile()
{
var taskId = "task_del";
var db = _dbManager.OpenFrameDb(taskId);
var scenarioId = "task_del";
var db = _dbManager.OpenFrameDb(scenarioId);
db.Close();
_dbManager.DeleteFrameDb(taskId);
_dbManager.DeleteFrameDb(scenarioId);
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db");
Assert.False(File.Exists(dbPath));
}

View File

@ -43,9 +43,9 @@ namespace CounterDrone.Core.Tests
var t = new DroneWave
{
WaveId = "default",
Target = new TargetConfig
Target = new DroneProfile
{
TargetType = (int)TargetType.Piston, PowerType = (int)power,
DroneType = (int)DroneType.FixedWing, PowerType = (int)power,
Quantity = 1, TypicalSpeed = speed, TypicalAltitude = alt,
},
Waypoints = new List<Waypoint>
@ -378,9 +378,9 @@ namespace CounterDrone.Core.Tests
var threat = new DroneWave
{
WaveId = "default",
Target = new TargetConfig
Target = new DroneProfile
{
TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
DroneType = (int)DroneType.FixedWing, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
},
Waypoints = new List<Waypoint>

View File

@ -8,11 +8,11 @@ namespace CounterDrone.Core.Tests
{
public class DroneEntityTests
{
private TargetConfig CreateConfig(float speed = 120, float altitude = 300)
private DroneProfile CreateConfig(float speed = 120, float altitude = 300)
{
return new TargetConfig
return new DroneProfile
{
TargetType = (int)TargetType.Piston,
DroneType = (int)DroneType.FixedWing,
PowerType = (int)PowerType.Piston,
TypicalSpeed = speed,
TypicalAltitude = altitude,

View File

@ -17,7 +17,7 @@ namespace CounterDrone.Core.Tests
private readonly string _testDir;
private readonly IScenarioService _scenario;
private readonly SQLite.SQLiteConnection _db;
private string _taskId = string.Empty;
private string _scenarioId = string.Empty;
public EdgeCaseTests()
{
@ -27,8 +27,8 @@ namespace CounterDrone.Core.Tests
_db = dbm.OpenMainDb();
_scenario = new ScenarioService(
new SimTaskRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new TargetConfigRepository(_db),
new ScenarioRepository(_db), new CombatSceneRepository(_db),
new ControlZoneRepository(_db), new DroneProfileRepository(_db),
new EquipmentDeploymentRepository(_db), new CloudDispersalRepository(_db),
new RoutePlanRepository(_db), new WaypointRepository(_db));
}
@ -43,20 +43,20 @@ namespace CounterDrone.Core.Tests
public void EmptyDeployment_EngineRunsWithoutCrash()
{
var task = _scenario.CreateTask("空部署", "");
_taskId = task.Id;
_scenarioId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
_scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.Piston,
DroneType = (int)DroneType.FixedWing,
Quantity = 1,
TypicalSpeed = 100,
TypicalAltitude = 300,
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenario.SaveDeployment(_scenarioId, new List<EquipmentDeployment>());
_scenario.SaveCloudDispersal(_scenarioId, new CloudDispersal());
_scenario.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 100 },
@ -65,7 +65,7 @@ namespace CounterDrone.Core.Tests
var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Ammo, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.Initialize(_scenarioId);
engine.TimeScale = 4f;
for (int i = 0; i < 500; i++)
@ -82,23 +82,23 @@ namespace CounterDrone.Core.Tests
public void ExtremeWind_DroneStillFlies()
{
var task = _scenario.CreateTask("飓风", "");
_taskId = task.Id;
_scenarioId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene
_scenario.SaveScene(_scenarioId, new CombatScene
{
WindSpeed = 30, // 最大风速
WindDirection = (int)WindDirection.E,
});
_scenario.SaveTarget(_taskId, new TargetConfig
_scenario.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.Piston,
DroneType = (int)DroneType.FixedWing,
Quantity = 1,
TypicalSpeed = 600, // 高速对抗强风
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenario.SaveDeployment(_scenarioId, new List<EquipmentDeployment>());
_scenario.SaveCloudDispersal(_scenarioId, new CloudDispersal());
_scenario.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 600 },
@ -107,7 +107,7 @@ namespace CounterDrone.Core.Tests
var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Ammo, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.Initialize(_scenarioId);
engine.TimeScale = 4f;
for (int i = 0; i < 500; i++)
@ -125,21 +125,21 @@ namespace CounterDrone.Core.Tests
public void LongRoute_CompletesWithoutError()
{
var task = _scenario.CreateTask("超长航路", "");
_taskId = task.Id;
_scenarioId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
_scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.HighSpeed,
DroneType = (int)DroneType.HighSpeed,
Quantity = 1,
TypicalSpeed = 500,
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>());
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal());
_scenario.SaveDeployment(_scenarioId, new List<EquipmentDeployment>());
_scenario.SaveCloudDispersal(_scenarioId, new CloudDispersal());
// 500 km/h, 100km 航程
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenario.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 500 },
@ -148,7 +148,7 @@ namespace CounterDrone.Core.Tests
var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)),
new DamageModelRouter(), new TestPathProvider(_testDir), new DefensePlanner(TestData.Ammo, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.Initialize(_scenarioId);
engine.TimeScale = 4f;
for (int i = 0; i < 5000; i++)

View File

@ -25,9 +25,9 @@ namespace CounterDrone.Core.Tests
if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true);
}
private void CreateFakeDb(string taskId)
private void CreateFakeDb(string scenarioId)
{
_store.BeginRecording(taskId);
_store.BeginRecording(scenarioId);
_store.RecordFrame(0, 0f, new List<EntitySnapshot> { new EntitySnapshot { EntityId = "d1" } });
_store.Flush();
}

View File

@ -21,7 +21,7 @@ namespace CounterDrone.Core.Tests
private readonly FrameDataStore _frameStore;
private readonly SQLiteConnection _mainDb;
private List<AmmunitionSpec> _ammoCatalog;
private string _taskId = string.Empty;
private string _scenarioId = string.Empty;
public FullPipelineTests()
{
@ -32,8 +32,8 @@ namespace CounterDrone.Core.Tests
_ammoCatalog = new List<AmmunitionSpec>(TestData.Ammo);
_scenario = new ScenarioService(
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
new ScenarioRepository(_mainDb), new CombatSceneRepository(_mainDb),
new ControlZoneRepository(_mainDb), new DroneProfileRepository(_mainDb),
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
@ -50,7 +50,7 @@ namespace CounterDrone.Core.Tests
{
var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefensePlanner(_ammoCatalog, TestPlannerConfig.Instance));
engine.Initialize(_taskId);
engine.Initialize(_scenarioId);
engine.TimeScale = timeScale; // 8倍速加速
for (int i = 0; i < maxTicks; i++)
{
@ -66,22 +66,22 @@ namespace CounterDrone.Core.Tests
private void VerifyAndExportReport(SimulationEngine eng, DroneEntity drone)
{
var detail = _scenario.GetTaskDetail(_taskId)!;
var detail = _scenario.GetTaskDetail(_scenarioId)!;
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
Assert.Contains("对抗结果", report);
Assert.Contains(detail.Task.Name, report);
var svc = new ReportService(_mainDb, _paths);
var r = svc.Generate(_taskId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime);
var r = svc.Generate(_scenarioId, detail, eng.Events.ToList(), drone.Status.ToString(), eng.SimulationTime);
svc.ExportToFile(r.Id, ReportOutputDir); // 持久化目录
}
private TaskFullConfig LoadPreset(string name)
private ScenarioConfig LoadPreset(string name)
{
var results = _scenario.SearchTasks(name, null, null, 1, 1);
Assert.True(results.TotalCount > 0, $"预设想定未找到: {name}");
_taskId = results.Items[0].Id;
return _scenario.GetTaskDetail(_taskId)!;
_scenarioId = results.Items[0].Id;
return _scenario.GetTaskDetail(_scenarioId)!;
}
private SimulationEngine RunPreset(string name, int maxFrames = 8000)
@ -152,7 +152,7 @@ namespace CounterDrone.Core.Tests
Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched),
e => Assert.Contains("空基", e.Description));
var drone = eng.Drones[0];
var detail = _scenario.GetTaskDetail(_taskId)!;
var detail = _scenario.GetTaskDetail(_scenarioId)!;
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
Assert.True(drone.Hp < 0.1f, $"Hp={drone.Hp:F3} pos=({drone.PosX:F0},{drone.PosY:F0}) launched={eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched)}\n{report}");
VerifyAndExportReport(eng, drone);
@ -183,24 +183,24 @@ namespace CounterDrone.Core.Tests
{
// 场景A无探测设备上帝视角从航路起点算
var taskA = _scenario.CreateTask("无探测远航路", "");
_taskId = taskA.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_taskId, new TargetConfig
_scenarioId = taskA.Id;
_scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
WaveId = "default", DroneType = (int)DroneType.FixedWing, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenario.SaveRoute(_scenarioId, "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>
_scenario.SaveDeployment(_scenarioId, new List<EquipmentDeployment>
{
TestData.All.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 8000, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
_scenario.SaveCloudDispersal(_scenarioId, 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);
@ -208,20 +208,20 @@ namespace CounterDrone.Core.Tests
// 探测圆边界 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
_scenarioId = taskB.Id;
_scenario.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenario.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "default", TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston,
WaveId = "default", DroneType = (int)DroneType.FixedWing, PowerType = (int)PowerType.Piston,
Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenario.SaveRoute(_scenarioId, "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>
_scenario.SaveDeployment(_scenarioId, new List<EquipmentDeployment>
{
TestData.All.FireUnits.First(f => f.Id == "ground-light").ToEquipmentDeployment(AerosolType.InertGas, 1, 8000, 0, 50),
// 独立探测设备
@ -234,7 +234,7 @@ namespace CounterDrone.Core.Tests
DetectionAccuracy = 50,
},
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
_scenario.SaveCloudDispersal(_scenarioId, 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);

View File

@ -30,8 +30,8 @@ namespace CounterDrone.Core.Tests
""CriticalProbabilityThreshold"": 0.5,
""MaxInterceptProbability"": 0.95,
""TypeCoefficient"": {
""HighSpeed"": 4.0, ""FixedWing"": 2.0, ""Piston"": 2.0,
""Rotor"": 1.0, ""Electric"": 1.0
""HighSpeed"": 4.0, ""FixedWing"": 2.0,
""Rotor"": 1.0
},
""AmmoMatch"": {
""Electric"": ""InertGas"", ""Piston"": ""InertGas"", ""Jet"": ""ActiveMaterial""
@ -47,7 +47,8 @@ namespace CounterDrone.Core.Tests
Assert.Equal(0.2f, cfg.CloudOverlapRatio);
Assert.Equal(0.5f, cfg.CriticalProbabilityThreshold);
Assert.Equal(0.95f, cfg.MaxInterceptProbability);
Assert.Equal(4.0f, cfg.TypeCoefficient[TargetType.HighSpeed]);
Assert.Equal(4.0f, cfg.TypeCoefficient[DroneType.HighSpeed]);
Assert.Equal(2.0f, cfg.TypeCoefficient[DroneType.FixedWing]);
Assert.Equal(AerosolType.InertGas, cfg.AmmoMatch[PowerType.Piston]);
Assert.Equal(AerosolType.ActiveMaterial, cfg.AmmoMatch[PowerType.Jet]);
}
@ -64,7 +65,7 @@ namespace CounterDrone.Core.Tests
{
// 缺 CriticalProbabilityThreshold
WriteConfig(@"{ ""CloudOverlapRatio"": 0.2, ""MaxInterceptProbability"": 0.95,
""TypeCoefficient"": {""Piston"": 2.0}, ""AmmoMatch"": {""Piston"": ""InertGas""} }");
""TypeCoefficient"": {""FixedWing"": 2.0}, ""AmmoMatch"": {""Piston"": ""InertGas""} }");
Assert.Throws<InvalidDataException>(() => PlannerConfig.Load(_testDir));
}
@ -89,7 +90,7 @@ namespace CounterDrone.Core.Tests
// 验证枚举用字符串(非数字)也能正确解析
WriteConfig(ValidJson);
var cfg = PlannerConfig.Load(_testDir);
Assert.Equal(1.0f, cfg.TypeCoefficient[TargetType.Rotor]);
Assert.Equal(1.0f, cfg.TypeCoefficient[DroneType.Rotor]);
Assert.Equal(AerosolType.InertGas, cfg.AmmoMatch[PowerType.Electric]);
}
}

View File

@ -33,10 +33,10 @@ namespace CounterDrone.Core.Tests
public void Generate_ProducesReportContent()
{
var gen = new ReportGenerator();
var config = new TaskFullConfig
var config = new ScenarioConfig
{
Task = new SimTask { Name = "测试任务", TaskNumber = "SIM-001" },
Targets = new List<TargetConfig> { new TargetConfig { Quantity = 1 } },
Task = new Scenario { Name = "测试任务", TaskNumber = "SIM-001" },
Targets = new List<DroneProfile> { new DroneProfile { Quantity = 1 } },
Equipment = new List<EquipmentDeployment>
{
new EquipmentDeployment { EquipmentRole = (int)EquipmentRole.LaunchPlatform, Quantity = 3 },

View File

@ -34,10 +34,10 @@ namespace CounterDrone.Core.Tests
[Fact]
public void Generate_And_Get_ReturnsReport()
{
var config = new TaskFullConfig
var config = new ScenarioConfig
{
Task = new SimTask { Name = "Test", TaskNumber = "SIM-001" },
Targets = new List<TargetConfig> { new TargetConfig { Quantity = 1 } },
Task = new Scenario { Name = "Test", TaskNumber = "SIM-001" },
Targets = new List<DroneProfile> { new DroneProfile { Quantity = 1 } },
Equipment = new List<EquipmentDeployment>(),
Cloud = new CloudDispersal(),
};
@ -56,10 +56,10 @@ namespace CounterDrone.Core.Tests
[Fact]
public void Delete_RemovesReport()
{
var config = new TaskFullConfig
var config = new ScenarioConfig
{
Task = new SimTask { Name = "Del", TaskNumber = "SIM-DEL" },
Targets = new List<TargetConfig> { new TargetConfig { Quantity = 1 } },
Task = new Scenario { Name = "Del", TaskNumber = "SIM-DEL" },
Targets = new List<DroneProfile> { new DroneProfile { Quantity = 1 } },
Equipment = new List<EquipmentDeployment>(),
Cloud = new CloudDispersal(),
};
@ -72,10 +72,10 @@ namespace CounterDrone.Core.Tests
[Fact]
public void SearchReports_Keyword()
{
var cfg = new TaskFullConfig
var cfg = new ScenarioConfig
{
Task = new SimTask { Name = "城市防御", TaskNumber = "SIM-A" },
Targets = new List<TargetConfig> { new TargetConfig { Quantity = 1 } },
Task = new Scenario { Name = "城市防御", TaskNumber = "SIM-A" },
Targets = new List<DroneProfile> { new DroneProfile { Quantity = 1 } },
Equipment = new List<EquipmentDeployment>(),
Cloud = new CloudDispersal(),
};

View File

@ -22,42 +22,42 @@ namespace CounterDrone.Core.Tests
public void Dispose() { _db?.Close(); if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); }
[Fact]
public void TargetConfig_DeleteByTaskId_RemovesAll()
public void DroneProfile_DeleteByScenarioId_RemovesAll()
{
var repo = new TargetConfigRepository(_db);
repo.Insert(new TargetConfig { Id = "t1", TaskId = "taskA", Quantity = 1 });
repo.Insert(new TargetConfig { Id = "t2", TaskId = "taskA", Quantity = 2 });
repo.Insert(new TargetConfig { Id = "t3", TaskId = "taskB", Quantity = 1 });
var repo = new DroneProfileRepository(_db);
repo.Insert(new DroneProfile { Id = "t1", ScenarioId = "taskA", Quantity = 1 });
repo.Insert(new DroneProfile { Id = "t2", ScenarioId = "taskA", Quantity = 2 });
repo.Insert(new DroneProfile { Id = "t3", ScenarioId = "taskB", Quantity = 1 });
repo.DeleteByTaskId("taskA");
repo.DeleteByScenarioId("taskA");
Assert.Empty(repo.GetByTaskId("taskA"));
Assert.Single(repo.GetByTaskId("taskB"));
Assert.Empty(repo.GetByScenarioId("taskA"));
Assert.Single(repo.GetByScenarioId("taskB"));
}
[Fact]
public void Waypoint_DeleteByTaskId_RemovesAll()
public void Waypoint_DeleteByScenarioId_RemovesAll()
{
var repo = new WaypointRepository(_db);
repo.Insert(new Waypoint { TaskId = "w1", OrderIndex = 0 });
repo.Insert(new Waypoint { TaskId = "w1", OrderIndex = 1 });
repo.Insert(new Waypoint { TaskId = "w2", OrderIndex = 0 });
repo.Insert(new Waypoint { ScenarioId = "w1", OrderIndex = 0 });
repo.Insert(new Waypoint { ScenarioId = "w1", OrderIndex = 1 });
repo.Insert(new Waypoint { ScenarioId = "w2", OrderIndex = 0 });
repo.DeleteByTaskId("w1");
repo.DeleteByScenarioId("w1");
Assert.Empty(repo.GetByTaskId("w1"));
Assert.Single(repo.GetByTaskId("w2"));
Assert.Empty(repo.GetByScenarioId("w1"));
Assert.Single(repo.GetByScenarioId("w2"));
}
[Fact]
public void EquipmentDeployment_DeleteByTaskId_Works()
public void EquipmentDeployment_DeleteByScenarioId_Works()
{
var repo = new EquipmentDeploymentRepository(_db);
repo.Insert(new EquipmentDeployment { Id = "e1", TaskId = "eqA" });
repo.Insert(new EquipmentDeployment { Id = "e2", TaskId = "eqA" });
repo.Insert(new EquipmentDeployment { Id = "e1", ScenarioId = "eqA" });
repo.Insert(new EquipmentDeployment { Id = "e2", ScenarioId = "eqA" });
repo.DeleteByTaskId("eqA");
Assert.Empty(repo.GetByTaskId("eqA"));
repo.DeleteByScenarioId("eqA");
Assert.Empty(repo.GetByScenarioId("eqA"));
}
}
}

View File

@ -24,10 +24,10 @@ namespace CounterDrone.Core.Tests
var dbManager = new DatabaseManager(paths);
_db = dbManager.OpenMainDb();
_service = new ScenarioService(
new SimTaskRepository(_db),
new ScenarioRepository(_db),
new CombatSceneRepository(_db),
new ControlZoneRepository(_db),
new TargetConfigRepository(_db),
new DroneProfileRepository(_db),
new EquipmentDeploymentRepository(_db),
new CloudDispersalRepository(_db),
new RoutePlanRepository(_db),
@ -101,10 +101,10 @@ namespace CounterDrone.Core.Tests
Temperature = 25.0,
});
_service.SaveTarget(task.Id, new TargetConfig
_service.SaveTarget(task.Id, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.FixedWing,
DroneType = (int)DroneType.FixedWing,
Quantity = 3,
});
@ -199,10 +199,10 @@ namespace CounterDrone.Core.Tests
public void SaveTarget_SavesCorrectly()
{
var task = _service.CreateTask("Target Test", "");
_service.SaveTarget(task.Id, new TargetConfig
_service.SaveTarget(task.Id, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.Piston,
DroneType = (int)DroneType.FixedWing,
Quantity = 5,
PowerType = (int)PowerType.Piston,
Wingspan = 2.5,
@ -212,7 +212,7 @@ namespace CounterDrone.Core.Tests
var detail = _service.GetTaskDetail(task.Id);
var target = detail.Targets[0];
Assert.Equal((int)TargetType.Piston, target.TargetType);
Assert.Equal((int)DroneType.FixedWing, target.DroneType);
Assert.Equal(5, target.Quantity);
Assert.Equal(120.0, target.TypicalSpeed);
}
@ -392,10 +392,10 @@ namespace CounterDrone.Core.Tests
});
// 4. 步骤2目标配置
_service.SaveTarget(task.Id, new TargetConfig
_service.SaveTarget(task.Id, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.HighSpeed,
DroneType = (int)DroneType.HighSpeed,
Quantity = 2,
PowerType = (int)PowerType.Jet,
TypicalSpeed = 300.0,

View File

@ -19,7 +19,7 @@ namespace CounterDrone.Core.Tests
private readonly SimulationEngine _engine;
private readonly IScenarioService _scenarioService;
private readonly SQLite.SQLiteConnection _mainDb;
private string _taskId = string.Empty;
private string _scenarioId = string.Empty;
public SimulationEngineTests()
{
@ -29,8 +29,8 @@ namespace CounterDrone.Core.Tests
_mainDb = dbManager.OpenMainDb();
_scenarioService = new ScenarioService(
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
new ScenarioRepository(_mainDb), new CombatSceneRepository(_mainDb),
new ControlZoneRepository(_mainDb), new DroneProfileRepository(_mainDb),
new EquipmentDeploymentRepository(_mainDb), new CloudDispersalRepository(_mainDb),
new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb));
@ -48,17 +48,17 @@ namespace CounterDrone.Core.Tests
private void SetupScenario()
{
var task = _scenarioService.CreateTask("Test", "");
_taskId = task.Id;
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenarioService.SaveTarget(_taskId, new TargetConfig
_scenarioId = task.Id;
_scenarioService.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0 });
_scenarioService.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "default",
TargetType = (int)TargetType.Piston,
DroneType = (int)DroneType.FixedWing,
Quantity = 1,
PowerType = (int)PowerType.Piston,
TypicalSpeed = 60,
});
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
_scenarioService.SaveDeployment(_scenarioId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
@ -68,7 +68,7 @@ namespace CounterDrone.Core.Tests
AerosolType = (int)AerosolType.InertGas, MunitionCount = 1, Cooldown = 5,
},
});
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal
_scenarioService.SaveCloudDispersal(_scenarioId, new CloudDispersal
{
AerosolType = (int)AerosolType.InertGas,
DisperseHeight = 300,
@ -79,7 +79,7 @@ namespace CounterDrone.Core.Tests
PositionZ = 0,
PositionMode = (int)PositionMode.AlgorithmRecommended,
});
_scenarioService.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenarioService.SaveRoute(_scenarioId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 },
@ -91,7 +91,7 @@ namespace CounterDrone.Core.Tests
public void Initialize_LoadsEntities()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
Assert.Single(_engine.Drones);
Assert.Equal(SimulationState.Running, _engine.State);
}
@ -100,7 +100,7 @@ namespace CounterDrone.Core.Tests
public void Tick_DroneMoves()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
_engine.Tick(1f / 20f);
Assert.True(_engine.Drones[0].PosX > 0);
}
@ -109,7 +109,7 @@ namespace CounterDrone.Core.Tests
public void PauseAndResume()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
_engine.Pause();
Assert.Equal(SimulationState.Paused, _engine.State);
@ -124,7 +124,7 @@ namespace CounterDrone.Core.Tests
public void Stop_ClosesGracefully()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
_engine.Tick(1f / 20f);
_engine.Stop();
Assert.Equal(SimulationState.Stopped, _engine.State);
@ -134,11 +134,11 @@ namespace CounterDrone.Core.Tests
public void FrameData_IsWritten()
{
SetupScenario();
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
for (int i = 0; i < 10; i++) _engine.Tick(1f / 20f);
_engine.Stop(); // 触发 Flush
var framePath = Path.Combine(_testDir, "frames", $"{_taskId}.db");
var framePath = Path.Combine(_testDir, "frames", $"{_scenarioId}.db");
Assert.True(File.Exists(framePath));
}
@ -149,19 +149,19 @@ namespace CounterDrone.Core.Tests
private void SetupScenarioWithDetection()
{
var task = _scenarioService.CreateTask("Detect", "");
_taskId = task.Id;
_scenarioService.SaveScene(_taskId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(_taskId, new TargetConfig
_scenarioId = task.Id;
_scenarioService.SaveScene(_scenarioId, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(_scenarioId, new DroneProfile
{
WaveId = "w1",
TargetType = (int)TargetType.Piston,
DroneType = (int)DroneType.FixedWing,
Quantity = 1,
PowerType = (int)PowerType.Piston,
TypicalSpeed = 60,
TypicalAltitude = 300,
});
// 探测设备在 (5000, 0, 0),雷达 3000m无角度/高度限制
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
_scenarioService.SaveDeployment(_scenarioId, new List<EquipmentDeployment>
{
new EquipmentDeployment
{
@ -177,7 +177,7 @@ namespace CounterDrone.Core.Tests
AerosolType = (int)AerosolType.InertGas, MunitionCount = 1, Cooldown = 5,
},
});
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal
_scenarioService.SaveCloudDispersal(_scenarioId, new CloudDispersal
{
AerosolType = (int)AerosolType.InertGas,
DisperseHeight = 300,
@ -186,7 +186,7 @@ namespace CounterDrone.Core.Tests
PositionX = 1000, PositionY = 300, PositionZ = 0,
PositionMode = (int)PositionMode.AlgorithmRecommended,
});
_scenarioService.SaveRoute(_taskId, "w1", new RoutePlan { FormationMode = (int)FormationMode.Single },
_scenarioService.SaveRoute(_scenarioId, "w1", new RoutePlan { FormationMode = (int)FormationMode.Single },
new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 },
@ -199,7 +199,7 @@ namespace CounterDrone.Core.Tests
{
SetupScenarioWithDetection();
_engine.SetFireSchedule(new List<FireEvent>()); // 不发射弹药
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
// 无人机起点 X=0探测设备在 X=5000 半径 3000 → 探测边界在 X=2000
// 速度 60 km/h = 16.67 m/s到达 X=2000 约需 120 秒
@ -224,7 +224,7 @@ namespace CounterDrone.Core.Tests
// 探测设备在 X=5000 半径 1000 → 入口 X=4000出口 X=6000
var task = _scenarioService.CreateTask("Reenter", "");
_scenarioService.SaveScene(task.Id, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(task.Id, new TargetConfig
_scenarioService.SaveTarget(task.Id, new DroneProfile
{
WaveId = "w1", Quantity = 1, TypicalSpeed = 120, PowerType = (int)PowerType.Piston, TypicalAltitude = 300,
});
@ -259,7 +259,7 @@ namespace CounterDrone.Core.Tests
{
// 使用普通的 SetupScenario无探测设备
SetupScenario();
_engine.Initialize(_taskId);
_engine.Initialize(_scenarioId);
Assert.Empty(_engine.DetectionEntities);
@ -275,7 +275,7 @@ namespace CounterDrone.Core.Tests
// D1 边界 X=1000D2 边界 X=3000 → D1 更早发现
var task = _scenarioService.CreateTask("Fuse", "");
_scenarioService.SaveScene(task.Id, new CombatScene { WindSpeed = 0, Visibility = 10000 });
_scenarioService.SaveTarget(task.Id, new TargetConfig
_scenarioService.SaveTarget(task.Id, new DroneProfile
{
WaveId = "w1", Quantity = 1, TypicalSpeed = 60, PowerType = (int)PowerType.Piston, TypicalAltitude = 300,
});