Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36ab4fad7b | |||
| a3ceae5c14 | |||
| dc62ba8c82 | |||
| 9d81ba0b28 | |||
| 0002e7efbe | |||
| a6ad8a348e | |||
| fb3c6fd2eb | |||
| 6f83cfb105 | |||
| e9c10bfe7d | |||
| 8bcf4cf675 | |||
| f77fb88487 | |||
| 3dc56e4a6e | |||
| 1db632676d | |||
| bebc03ee43 | |||
| dabf367b21 | |||
| b23b19e4e9 | |||
| 8a198a27a0 | |||
| 8abaf0bf87 | |||
| 4b2677e224 | |||
| 2a062c07bd | |||
| d3248d80a3 | |||
| 2adacffe85 | |||
| 62b718fa1e | |||
| fc0a6b0397 | |||
| f6d3f1bdb4 | |||
| dbe6808df7 | |||
| 7383b67fca | |||
| 43e70a7482 | |||
| 846d79a8d7 | |||
| c8e066be64 | |||
| 7415d07d9f | |||
| d8f8ebd336 | |||
| ce11e0eb1f | |||
| efdd5ec94f | |||
| 1fdffd2a41 | |||
| 178923c007 | |||
| 5e7fd2fb16 | |||
| 225590ac19 | |||
| d73b586982 | |||
| e30f173312 | |||
| d2d0ae1414 | |||
| bba7d2e505 | |||
| 473890ed37 | |||
| 184213b3c5 | |||
| 1aa927e471 | |||
| f0f5f76b8b | |||
| 843141a7a8 | |||
| 4b08ffa659 | |||
| bb04411d6d | |||
| 6d56dec9a9 | |||
| 69ec9ed0e2 | |||
| 537671ab3a | |||
| 7413077514 | |||
| 9f682c9b91 | |||
| 67cea1490d | |||
| 46ce1234ee | |||
| 0434920100 | |||
| ff6db2786e | |||
| ad0eaf47da | |||
| ca0ce8aa52 | |||
| def3996f4f | |||
| fb87fdfa22 | |||
| 92d1131dc1 | |||
| b4f9a4055c | |||
| 4631049aad | |||
| 5909c86402 | |||
| ebebb41671 | |||
| eb0cb66196 | |||
| 3d33aac428 | |||
| edea5eaaf0 | |||
| 276bb7adfa | |||
| 0b7bf2ab6f | |||
| 6b2905b9f8 | |||
| 005efae27e | |||
| 06038d3e90 | |||
| 24ba621581 | |||
| 79256d56f4 | |||
| 0b531de794 | |||
| bc8b401ef6 | |||
| 6098186f34 | |||
| 4c2c18cfcc | |||
| 5d79e26e0f | |||
| 6f28704368 | |||
| e782062377 | |||
| 120f2805ec | |||
| e250093a16 | |||
| 3bb6e7a091 | |||
| ff830c1221 | |||
| da4e8d5c63 | |||
| 7bdda062c0 | |||
| e1f576fbe5 | |||
| 0ed714a730 | |||
| 72e465570f | |||
| 69e41e3f7c | |||
| 3fd8433276 | |||
| 663afbe639 | |||
| ae47899080 | |||
| df8e18d2d0 | |||
| c2a10c1d51 | |||
| 8e73c9b2ec | |||
| 016d9ac24b | |||
| 2089ff6c7b | |||
| e0b80a39de | |||
| d8470bad30 | |||
| 6b005cf9cc | |||
| 68b7434ddc | |||
| ce9d44db7e | |||
| fb22605fa3 | |||
| b3dec77e92 | |||
| 2a87b77085 | |||
| 9964d77751 | |||
| 5c2b95d777 | |||
| cc1a0628d8 | |||
| c379e849f0 | |||
| 274779309d | |||
| 5f589c9644 | |||
| 18f7b79401 | |||
| 5b6ccccba6 | |||
| 235c111a75 | |||
| 4fa19e386c | |||
| d6fb206078 | |||
| 8eeb43f0e8 | |||
| 56d345189e | |||
| 665747a846 | |||
| 11f8cb2c79 | |||
| 3028a421b5 | |||
| d70d1f0bf8 | |||
| 19780b54b9 | |||
| 008df4a86c | |||
| 87e82727f2 | |||
| c6aee276fb | |||
| 3b8fb4ee86 | |||
| 8515f7821a | |||
| 53ac665a0e | |||
| bf3452013d | |||
| 61ae257f54 | |||
| 48368e0fe8 | |||
| a3bf6720c6 | |||
| 1f092e8647 | |||
| cc3c7199c0 | |||
| 510403a66c | |||
| 9c153a040b | |||
| fc469aa125 | |||
| 47761e5124 | |||
| 3aac0060e7 | |||
| 43426ab5fc | |||
| 355b430828 | |||
| 9e1fe869a6 | |||
| a5fef98858 | |||
| 09f51939d8 | |||
| 6cd18bf142 | |||
| 2162a342b7 | |||
| 197f8b6e5c | |||
| 070167f572 | |||
| b596b4a791 | |||
| 0c9cdb7b82 | |||
| c5c1b79946 | |||
| 6a6c03f0fa | |||
| 4f509e5a2a | |||
| 29aa11665b | |||
| c7792d1d4a | |||
| a9339c36e3 | |||
| 4706c4b8c1 | |||
| 67223cd392 | |||
| 4e157834f7 | |||
| 76d845f91b | |||
| 5e29536b8b | |||
| a19dced505 | |||
| 5c23e92587 | |||
| a39439c023 | |||
| 3ba8e4f604 | |||
| 45387fee73 | |||
| b5b561039b | |||
| fd723daa78 | |||
| 162338ce83 | |||
| 182af5026d | |||
| fe7eb723a1 | |||
| ced8d4014a | |||
| 0f73bbf55e | |||
| 26a372ffc5 | |||
| f4be469e72 | |||
| c08d613d5b | |||
| 7fa0268463 | |||
| c610573e34 | |||
| 08427e9e08 | |||
| c08edbd2f9 | |||
| 9bd1d5a7ee | |||
| 50ec14bb24 | |||
| 77351d8198 | |||
| e3ea595204 | |||
| 4411a452dc | |||
| 5667aef495 | |||
| 8366bfcb93 | |||
| 2bb8ba2e1e | |||
| b957a5e2b4 | |||
| 7d080bc929 | |||
| 66537d1621 | |||
| 3a2f68b02b | |||
| 317f47280b | |||
| 1e07bcc535 | |||
| 0a3c444a4b | |||
| c4d9ccc4fa |
1
.gitignore
vendored
1
.gitignore
vendored
@ -60,3 +60,4 @@ src/Unity/Logs/
|
||||
src/Unity/UserSettings/
|
||||
unity_plugins/
|
||||
nul
|
||||
reports/
|
||||
|
||||
59
AGENTS.md
59
AGENTS.md
@ -76,6 +76,65 @@ pwsh scripts/check_unity_build.ps1
|
||||
|
||||
This automatically rebuilds Core.dll, copies it to Unity Plugins, and compiles Unity scripts. Exits 0 if all pass.
|
||||
|
||||
## 7. Run Targeted Tests First
|
||||
|
||||
**Always run the narrowest relevant test first.** If you just changed `DefensePlannerTests`, run `--filter 'FullyQualifiedName~DefensePlannerTests'`. If you changed `FullPipelineTests`, run that filter. Don't start with `dotnet test` on the whole project — it wastes time and buries the failures you're looking for.
|
||||
|
||||
Run full suite only after the targeted tests pass, to verify nothing else broke.
|
||||
|
||||
```bash
|
||||
# Run a test class:
|
||||
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/ --filter 'FullyQualifiedName~DefensePlannerTests'"
|
||||
|
||||
# Run a single test:
|
||||
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/ --filter 'FullyQualifiedName~Scenario_AirBased'"
|
||||
|
||||
# Run full suite (only after targeted passes):
|
||||
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/"
|
||||
```
|
||||
|
||||
## 8. No Hardcoded Defaults or Fallbacks
|
||||
|
||||
**Every parameter must come from configuration. If a required value is missing, fail explicitly — never silently substitute a default.**
|
||||
|
||||
Good:
|
||||
```csharp
|
||||
if (unit.CruiseSpeed <= 0)
|
||||
throw new InvalidOperationException($"单元 {unit.Id}: CruiseSpeed 必须 > 0");
|
||||
```
|
||||
|
||||
Bad:
|
||||
```csharp
|
||||
float speed = unit.CruiseSpeed > 0 ? unit.CruiseSpeed : 55f; // 55f 是哪来的?
|
||||
float mv = unit.MuzzleVelocity ?? 800f; // 为什么是 800?
|
||||
float alt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : threat.Altitude + 500f; // 500?
|
||||
```
|
||||
|
||||
This applies to:
|
||||
- `??` operator with arbitrary numbers (55f, 800f, 1000f, 3, etc.)
|
||||
- Ternary `> 0 ? x : default` patterns
|
||||
- `Math.Max(0.1f, x)` to prevent division by zero — instead validate the input before the division
|
||||
- `if (fireTime < 0.1f) fireTime = 0.1f` — instead skip the event and report the failure
|
||||
|
||||
Physics constants (9.81f, 3.6f, π) and documented model parameters (Phase 2 cutoff = 30s) are NOT arbitrary defaults — they are legitimate parts of the physical model.
|
||||
|
||||
## 9. Git Commit Must Include DLL
|
||||
|
||||
**Every commit must bundle the compiled CounterDrone.Core.dll.** Never leave DLL changes as a separate follow-up commit. The full commit flow:
|
||||
|
||||
```bash
|
||||
# 1. Run full test suite (builds Core.dll)
|
||||
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/"
|
||||
|
||||
# 2. Copy DLL to Unity plugin directories
|
||||
cp src/CounterDrone.Core/bin/Debug/netstandard2.1/CounterDrone.Core.dll src/Unity/Assets/Plugins/CounterDrone.Core/
|
||||
cp src/CounterDrone.Core/bin/Debug/netstandard2.1/CounterDrone.Core.dll unity_plugins/
|
||||
|
||||
# 3. Commit everything at once
|
||||
git add -A
|
||||
git commit -m "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||
|
||||
386
CHANGELOG.md
386
CHANGELOG.md
@ -1,8 +1,388 @@
|
||||
# Changelog
|
||||
|
||||
本文件记录项目对外发布的变更历史(用户视角)。
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
---
|
||||
|
||||
## [0.12.0] - 2026-06-20
|
||||
|
||||
### Added — PDF 导出 + 报告模板架构(Phase 8.2.1)
|
||||
|
||||
- **PdfSharpCore 1.3.64**:纯托管 PDF 库(netstandard2.0,Unity IL2CPP 兼容),新增 9 个传递依赖 DLL
|
||||
- **ReportData 结构化模型**:`ReportData → Sections → Blocks`(KeyValueBlock / TableBlock / TextBlock),替代原 StringBuilder 拼接
|
||||
- **ReportBlockConverter**:ReportBlock 多态 JSON 序列化转换器(netstandard2.1 不支持 JsonPolymorphic 属性)
|
||||
- **MarkdownRenderer**:从 ReportData 渲染 Markdown,与 PDF 共用数据源
|
||||
- **StandardPdfTemplate**:内置标准 PDF 模板(A4、中文字体嵌入、表格边框、自动分页、页码)
|
||||
- **CjkFontResolver**:PdfSharpCore IFontResolver 实现,从文件路径加载 CJK TrueType 字体
|
||||
- **IPathProvider.GetFontPath()**:字体文件路径接口,`UnityPathProvider` / `TestPathProvider` 均已实现
|
||||
- **SimulationReport +ReportDataJson**:结构化报告数据 JSON 字段,供 PDF 重新渲染
|
||||
- **IReportService.ExportReport(id, format)**:返回 `byte[]`,支持 `"pdf"` / `"md"`
|
||||
- **IReportService.ExportToFile(id, dir, format)**:导出文件,返回路径
|
||||
- **ReportService.Generate 自动导出 MD**:仿真后自动生成 `.md` 文件到 `{DataRoot}/reports/`
|
||||
- **Unity ReportManager**:新增 `ExportReport(reportId, format)` 和 `Export(reportId, dir, format)` 桥接
|
||||
- **CJK 字体文件**:`data/fonts/CJK-Font.ttf`(SimHei,9.7MB)+ Unity StreamingAssets 同步
|
||||
- **check_unity_build.ps1 修复**:加 `-quit` 参数 + 300s 超时,解决 Unity batchmode 不退出导致挂起
|
||||
|
||||
### Changed — ReportGenerator 重构
|
||||
|
||||
- **ReportGenerator.Generate 返回 ReportData**(原返回 string),翻译函数去掉 emoji 前缀(SimHei 不支持 emoji 字形)
|
||||
- **ExportToFile 签名变更**:加 `format` 参数(`"pdf"` / `"md"`),不向后兼容旧的两参数签名
|
||||
- **DLL 数量 14→22**:新增 PdfSharpCore + SharpZipLib + SixLabors.Fonts/ImageSharp + 5 个 System.* 传递依赖
|
||||
|
||||
### Docs
|
||||
|
||||
- 对接文档 V2.0:新增坐标系与 3D 可视化章节、完整枚举值清单、所有 Manager 完整方法签名、核心模型完整字段
|
||||
- 架构设计:导出章节更新为 PdfSharpCore 选型,接口签名同步
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 **262**(+12),全部通过 15s
|
||||
|
||||
---
|
||||
|
||||
## [0.11.0] - 2026-06-18
|
||||
|
||||
### Breaking — 发射平台与探测设备分离
|
||||
|
||||
- **LaunchPlatformSpec**:新增纯发射平台规格(GunCount/ChannelsPerGun/MuzzleVelocity/CruiseSpeed 等,无探测字段)
|
||||
- **ScenarioUnit.FireUnitSpecId → LaunchPlatformSpecId**:发射平台部署通过新 FK 引用
|
||||
- **SensorSpec 统一探测来源**:发射平台自带探测 + 独立探测设备,统一通过 SensorSpecId 引用
|
||||
- **BuildDetectionSources 简化**:只查 SensorSpec,不分 Launch/Detection 两条路径
|
||||
- **FireUnitSpec 保留不用**:旧类型保留代码但不参与运行时
|
||||
- **defaults.json +launchPlatforms**:新数据区
|
||||
|
||||
### Added — 代码审查 + 测试覆盖
|
||||
|
||||
- **DataServiceTests**:12 个,覆盖 7 类基础数据 CRUD
|
||||
- **死代码清除**:MunitionEntity._hasExceededReleaseAltitude
|
||||
- **Spec 类独立文件**:DroneSpec/FireUnitSpec/SensorSpec 等从 DefaultData.cs 拆出,命名空间 → Models
|
||||
- **PagedResult/EnumMetadata 独立文件**
|
||||
- **GetEnums 返回中英文对照**:EnumItem{Name,ChineseName,Value}
|
||||
- **FormationTemplate 补 PrimaryKey**
|
||||
|
||||
### Added — 3D 模型引用
|
||||
|
||||
- **DroneSpec/FireUnitSpec/SensorSpec +ModelId**:FK → ModelInfo
|
||||
- **EntitySnapshot +ModelId**:每帧推送给 Unity
|
||||
- **ModelInfo.ModelType→EntityType(枚举) +Description**
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 **250**(+12),全部通过 12s
|
||||
|
||||
---
|
||||
|
||||
## [0.10.0] - 2026-06-18
|
||||
|
||||
### Breaking — 平台物理统一 + 模型重命名
|
||||
|
||||
- **空基/地基物理统一**:去除 DefensePlanner 所有平台类型分支,统一用 `InterceptCalculator.Compute`(删 `ComputeHorizontal`),统一用 `MuzzleVelocity`(删 `CruiseSpeed` 分支)
|
||||
- **抛物线选解策略**:两解逐一计算距离匹配 + `fireTime>0` 可行性,选最早拦截的解(非简单选高角/低角)
|
||||
- **`TargetType` → `DroneType`**:删 `Electric`/`Piston`,重编号 `HighSpeed=2`,`TargetConfig.DroneType` 属性同步改名
|
||||
- **`TargetConfig` → `ScenarioDrone`**:核心模型重命名
|
||||
- **`SimTask` → `Scenario`**:想定主表重命名,`ScenarioNumber` 取代 `TaskNumber`
|
||||
- **`TaskFullConfig` → `ScenarioConfig`**:聚合配置重命名,`Task` 属性 → `Info`
|
||||
- **`TaskId` → `ScenarioId`**:所有 FK 重命名
|
||||
- **`ScenarioService` 方法**:`CreateTask`→`CreateScenario`、`DeleteTask`→`DeleteScenario`、`SearchTasks`→`SearchScenarios`、`GetTaskDetail`→`GetScenarioDetail`
|
||||
|
||||
### Added — 模型业务属性
|
||||
|
||||
- **`ScenarioDrone` / `DroneSpec`**:+`Model`(型号)、+`Description`(描述/用途)
|
||||
- **`Scenario`**:+`Description`(想定描述)
|
||||
- **`ScenarioUnit` / `FireUnitSpec`**:+`Description`
|
||||
- **`ReportManager.Export(reportId, outputDir)`**:支持指定导出路径
|
||||
|
||||
### Changed — 数据库迁移
|
||||
|
||||
- `CreateMainTables` 删旧表 `SimTask`、`TargetConfig`、`SimulationReport`(自动重建)
|
||||
- `scenariosVersion` 3→4 触发 demo 想定重建
|
||||
|
||||
### Docs
|
||||
|
||||
- 对接文档 V1.6:去历史命名对照,只写现状
|
||||
- 架构设计 V15:同步模型表结构 + 枚举值
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 **238**,全量通过 11s
|
||||
|
||||
---
|
||||
|
||||
## [0.9.0] - 2026-06-17
|
||||
|
||||
### Breaking — 运动学前向计算 + 基础数据 CRUD
|
||||
|
||||
- **Kinematics 前向计算**:新增 `ComputeParabolicRange(v₀, θ, Δy)` 和 `ParabolicApex(v₀, θ)`,正问题直接由角度算射程和时间
|
||||
- **Math.Max 回退全部移除**:`CalculateLaunchAngle`/`ParabolicShellTime`/`ParabolicTimeOfFlight` 不再静默钳位或 45° 回退,非法输入直接抛异常
|
||||
- **GaussianPuffDispersion / CloudExpansionModel / DetectionCalculator**:BurstChargeKg≤0 抛异常,不再用 `Math.Max(0.01, ...)` 掩护;Phase3 `x≤0` 显式跳过
|
||||
|
||||
### Changed — 空基固定阵位发射
|
||||
|
||||
- **空基不再「飞向投放点」**:改为固定阵位水平发射(θ=0°),`ComputeParabolicRange` 前向计算飞行时间
|
||||
- **DefensePlanner 空基**:`ComputeHorizontal` 使用 `platform.PosY`(非 `ReleaseAltitude`);云团位置 = 炮弹实际到达位置
|
||||
- **MunitionEntity 下落修正**:`_arrivesDescending` 区分上升/下落到达;`Math.Max(0.01, ...)` 除零回退移除
|
||||
- **死代码删除**:`CommandFlyTo`/`FlyingToTarget`/`ReadyToRelease` 状态 + SimulationEngine "1b. 到达投放点" 块
|
||||
|
||||
### Added — 实体属性全面暴露
|
||||
|
||||
- **EntitySnapshot +VelX/Y/Z**:所有实体帧快照带瞬时速度
|
||||
- **引擎 Platforms 列表公开**:`SimulationEngine.Platforms`
|
||||
- **CloudEntity**:Pos/Radius/Density/Phase/Elapsed 便捷属性(不再穿透 Dispersion)
|
||||
- **MunitionEntity**:LaunchAngle/Azimuth/MuzzleVelocity/FlightDuration/Start/LaunchTime/ElapsedTime/Velocity 全部 public
|
||||
- **DroneEntity**:TraveledArc/TotalArc/Progress
|
||||
- **PlatformEntity**:Target/FlightDistance/FlownDistance
|
||||
- **DetectionEntity**:PosX/Y/Z
|
||||
|
||||
### Added — 基础数据 CRUD(DataService)
|
||||
|
||||
- **7 类规格入库**:FireUnitSpec / DroneSpec / SensorSpec / EnvironmentSpec / FormationTemplate / RouteTemplate / AmmunitionSpec
|
||||
- **类名规范化**:TargetPreset→DroneSpec, DetectionPreset→SensorSpec, WeatherPreset→EnvironmentSpec, FireUnitTemplate→FireUnitSpec, RoutePreset→RouteTemplate
|
||||
- **IDataService + DataService**:全 CRUD(GetAll/Save/Delete)
|
||||
- **Unity Manager 接入**:ScenarioManager.DataService + SimulationRunner.DataService
|
||||
|
||||
### Added — FrameDataStore LiveFrames 内存回放
|
||||
|
||||
- **Flush 后保留 LiveFrames 副本**:仿真刚结束可内存回放,零磁盘 IO
|
||||
- **ReplayController 双路径**:`LoadReplay(scenarioId, frameStore)` 优先内存,回退 SQLite
|
||||
- **LiveFrames 生命周期**:BeginRecording/Discard 清除
|
||||
|
||||
### Changed — 3机空基编队测试启用
|
||||
|
||||
- `Seed3DronesAirBased` 参数对齐单机(风速/航速),平台沿 X 轴间隔 300m
|
||||
- `Scenario_3DronesAirBased_AllDestroyed` 移除 Skip,全部通过
|
||||
- `Scenario_DetectionDriven_PlanningDelayedByDetectionBoundary` 移除 Skip
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 **243**(+12),跳过 0,全量通过 11s
|
||||
|
||||
---
|
||||
|
||||
## [0.8.0] - 2026-06-16
|
||||
|
||||
### Added — 探测实时链路(Phase 10)
|
||||
|
||||
- **3D 球冠探测**:`DetectionCalculator.IsInCoverage`(水平距离 + 俯仰角 + 高度门限),planner 与运行时共用
|
||||
- **EarliestDetection 采样法**:线段-圆求交改为沿航路采样(步长≤50m)调 `IsInCoverage`
|
||||
- **DetectionEntity**:运行时探测实体 + per-drone 状态机(Undetected ⇄ Detected),Tick 第5步扫描
|
||||
- **实时探测事件**:`OnTargetDetected` / `SimEventType.TargetDetected` / `PlanningFailed`
|
||||
- **ScenarioUnit + DetectionSource**:+4 个 3D 球冠字段(MinElevation/MaxElevation/MinDetectAlt/MaxDetectAlt)
|
||||
- **默认数据全部加入 3D 球冠参数**(地基 -5°~85°、空基 -80°~30°)
|
||||
|
||||
### Added — Planner 诊断 + 拦截点计算
|
||||
|
||||
- **InterceptCalculator**:抛物线与直线联立方程求解拦截点(地基可变角度 + 空基固定水平 θ=0°)
|
||||
- **HasInterceptWindow**:拦截窗口可行性检查(从探测边界算有效飞行时间)
|
||||
- **TryGenerateFireEvents**:每个 `return events` 带拒绝原因
|
||||
- **Planner Summary**:含失败原因 + 建议值(探测范围/弧长/反应时间)
|
||||
- **PlannerConfig**:+`ReactionTime`(5s)/ `ExpansionFactor`(0.9)/ `TimingSafetyMargin`(1s)
|
||||
|
||||
### Changed — 消除硬编码
|
||||
|
||||
- **速度从 waypoint.Speed 读取**:`DroneEntity.CruiseSpeed` / `GetDroneSpeedKph` 不再用 `ScenarioDrone.TypicalSpeed`
|
||||
- **`AmmunitionSpec` +`Phase2Duration`**(30s):`CloudExpansionModel` 不再写死 `30f`
|
||||
- **`GetArrivalTime` 基于 DetectArc**:从探测点到中点算到达时间(而非航路起点)
|
||||
- **TestData 改为从 seeded 数据库读取**(不再内嵌 JSON 重复 defaults.json)
|
||||
|
||||
### Changed — 默认想定参数
|
||||
|
||||
- 巡航导弹速度 300→200 km/h
|
||||
- 空基航路 10km→20km,平台位置 1500→6000,巡航速度 55→80 m/s
|
||||
- 地基火力单元放航路终点,探测范围按 80% 规则
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 204 → **231**(+27),全量通过 7s
|
||||
|
||||
---
|
||||
|
||||
## [0.7.0] - 2026-06-15
|
||||
|
||||
### Breaking — 概念升级:编组拆分 + Group 表移除
|
||||
|
||||
- **Group 表移除**:原 `Group` 表(DroneFleet / EquipmentGroup)不再创建。向后兼容保留旧库中的 Group 表,但不再主动读写
|
||||
- **GroupType 枚举删除**:不再区分 DroneFleet / EquipmentGroup
|
||||
- **GroupService / IGroupService / GroupRepository 删除**:不再需要编组管理服务
|
||||
- **Unity GroupManager 删除**:不再需要编组管理桥接
|
||||
|
||||
### Changed — 数据模型重命名
|
||||
|
||||
- **`GroupId` → `WaveId`**ScenarioDrone 四表的编组外键重命名为批次外键
|
||||
- **RoutePlan 索引重命名**:`(ScenarioId, GroupId)` → `(ScenarioId, WaveId)`
|
||||
- **RoutePlanRepository / WaypointRepository**:`GetByScenarioAndGroup` → `GetByScenarioAndWave`
|
||||
- **IScenarioService.SaveRoute**:`groupId` 参数 → `waveId`
|
||||
|
||||
### Changed — 算法类型重命名
|
||||
|
||||
- **`DroneGroup` → `DroneWave`**:AlgorithmTypes、IDefensePlanner、DefensePlanner、SimulationEngine 全部使用新类型名
|
||||
- **`DroneGroupId` → `DroneWaveId`**:UnitAssignment 中的编队引用 → 批次引用
|
||||
- **`BuildDroneGroups()` → `BuildDroneWaves()`**:SimulationEngine 构建方法重命名
|
||||
|
||||
### Changed — 文档同步
|
||||
|
||||
- 总体架构设计:编组概念升级说明
|
||||
- DefensePlanner 技术方案:DroneGroup → DroneWave
|
||||
- 实施计划:GroupService 移除、多编队 → 多批次
|
||||
- Unity 前端对接文档:DefaultDefenseAdvisor → DefensePlanner,SaveRoute 签名更新
|
||||
- 仿真器实体事件映射:BuildDroneGroups → BuildDroneWaves
|
||||
|
||||
### Removed
|
||||
|
||||
- 7 个文件:Group.cs / GroupRepository.cs / IGroupService.cs / GroupService.cs / GroupManager.cs / GroupServiceTests.cs / GroupRepositoryTests.cs
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 208 → **204**(−4,删除 Group 相关测试),全量通过 63s
|
||||
- 0 编译错误,0 编译警告
|
||||
|
||||
---
|
||||
|
||||
## [0.6.0] - 2026-06-15
|
||||
|
||||
### Added — 探测驱动的规划(8.1.1)
|
||||
|
||||
- **DetectionCalculator 静态工具类**:探测能力评估的唯一实现(与 Kinematics/RouteGeometry 同范式)。光电受 Visibility 衰减(`有效=基准×min(1,Visibility/基准)`),雷达/红外不受影响;统一信息网络找最早探测点(线段-圆求交)
|
||||
- **探测源模型**(DetectionSource):独立探测设备 + 火力单元自带探测统一表达(雷达/光电/红外三距离 + 精度)
|
||||
- **ScenarioUnit 扩展**:删单一 `DetectionRadius`,加 `RadarRange/EORange/IRange/DetectionAccuracy` 四字段
|
||||
- **FireUnit 探测字段激活**:BuildFireUnits 从 ScenarioUnit 读取并赋值(原为死代码)
|
||||
- **planner 接口扩展**:`IDefensePlanner.Plan` 加第 4 参数 `detectionSources`
|
||||
- **PlannerConfig 加 DefaultDetectionAccuracy**(无探测时回退精度)
|
||||
- 单元测试:DetectionCalculator 13 项、探测驱动规划 2 项
|
||||
|
||||
### Changed — planner 基于探测信息规划
|
||||
|
||||
- planner 假设威胁从探测边界被发现,到达时间 = (拦截弧长 − 探测弧长)/速度,而非上帝视角的航路起点
|
||||
- SimulationEngine.Initialize 构建 `List<DetectionSource>`(独立探测 + 火力单元自带),传入 planner
|
||||
- 报告探测设备表用新字段(雷达/光电/红外/精度)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Solve 健壮性:GenerateFireEventsAt 返回空时(探测边界太靠后来不及拦截)不再 IndexOutOfRange
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 193 → **208**(+15),全量通过 64s
|
||||
- 0 编译警告
|
||||
|
||||
---
|
||||
|
||||
## [0.5.0] - 2026-06-14
|
||||
|
||||
### Added — 物理模型统一架构
|
||||
|
||||
- **RouteGeometry 静态工具类**:航路几何唯一实现(总弧长/弧长→位置/点→最近弧长/切向量/到达时间)。planner(预测)和 DroneEntity(执行)共用,消除本地折线插值
|
||||
- **PlannerConfig + planner_config.json**:planner 策略参数全部外置(重叠系数、威胁类型系数、弹药匹配表、临界/上限概率阈值)。代码零默认值,文件缺失即抛异常
|
||||
- 单元测试:RouteGeometry 16 项、PlannerConfig 6 项、DroneEntity L 形多 waypoint 1 项、Z 向航路感知 1 项
|
||||
|
||||
### Changed — planner 不再写本地物理公式
|
||||
|
||||
- **DefaultDefensePlanner 航路感知布局**:云团 offset 沿航路切向(`RouteGeometry.TangentAt`),不再写死 X 轴;穿越点用 `RouteGeometry.PositionAt`,到达时间用 `RouteGeometry.TravelTimeTo`——支持任意方向/折线航路
|
||||
- **云团重叠**:间距从 `2R`(相切)改为 `2R×(1−重叠系数)`,重叠 20% 由配置驱动,消除相切处的密度空洞
|
||||
- **DroneEntity 弧长驱动**:运动改为 `_traveledArc += speed×dt` + `RouteGeometry.PositionAt`,删除 `CurrentWaypointIndex` 逐段插值、`dist<1.0` 阈值、`step>=dist` snap 丢位移等本地逻辑
|
||||
- **CloudExpansionModel.RoundsNeeded** 签名:`effectiveRadius` 参数改为 `spacing`,间距由调用方(planner)按重叠系数传入,公式仍在共享模块
|
||||
- **策略参数从配置读**:威胁类型系数、弹药匹配表、临界概率(0.5)、拦截概率上限(0.95)全部从 `PlannerConfig` 读,planner 内零硬编码
|
||||
|
||||
### Fixed — planner 与引擎物理一致性
|
||||
|
||||
- **PathInSphere 云团参考系修正**:毁伤判定改在云团参考系计算(`drone.Pos − cloud.Center`),修正云团在 tick 内移动导致的每 tick ~2m 系统误差
|
||||
- **ComputeEffectiveRadius 云龄 bug**:从 `ArrivalTime×2`(无人机飞行时间,概念错误)改为 `expansionTime`(云团自身膨胀时长)
|
||||
|
||||
### Removed
|
||||
|
||||
- SimulationEngine 所有硬编码桌面路径的诊断写入(`planner_targets.csv`/`cloud_actual.csv`/`path_in_cloud.txt`/`_hitLog`/`_totalPathInCloud`)——这些造成集成测试并行时文件竞争(flaky 失败根因)
|
||||
- DefaultDefensePlanner 的硬编码 `MatchTable`/`TypeCoefficient` 字典(移入配置)
|
||||
|
||||
### Metrics
|
||||
|
||||
- 测试 167 → **191**(+24),全量通过 41s
|
||||
- 关键验证:活塞+西风、空基+东风有风场景击毁成功;Z 向航路云团沿航路分布;L 形折线多 waypoint 运动正确
|
||||
|
||||
---
|
||||
|
||||
## [0.4.1] - 2026-06-14
|
||||
|
||||
### Added
|
||||
|
||||
- 天气纳入 Planner 规划:抛撒点风偏预补偿。云团生成后会在 `expansionTime` 内被风吹偏 `windVec × expansionTime`,Planner 逆风预置抛撒点 `cloudGen = 穿越点 − windVec × expansionTime`,使云团漂移后中心正好回到无人机航路上
|
||||
- 单元测试:`DispersionModelTests` Phase3 天气差异 2 项(雾 vs 晴、夜 vs 晴);`DefensePlannerTests` 风偏补偿 4 项(无风/东风/西风/北风方向性)
|
||||
|
||||
### Fixed
|
||||
|
||||
- `GaussianPuffDispersion.Tick` Phase3 写死 `WeatherType.Sunny` 的 bug:原代码 `GetStabilityClass((WeatherType)0, windSpeed)` 导致任何天气下扩散行为都相同,预估(`CloudExpansionModel` 已正确读 env)与运行时模型不一致。改为存储 `env` 字段,使用真实 `env.WeatherType`
|
||||
|
||||
### Changed
|
||||
|
||||
- `DefaultDefensePlanner.GenerateFireEventsAt` 区分无人机穿越点 `tx,tz`(用于 `txArrival` 计算)与云团生成点 `cloudGenX,Z`(弹药瞄准目标 + FireEvent 输出)
|
||||
- 移除 `Solve` 中 `e.TargetX = mid.X + offset` 的覆盖(会抹掉风偏补偿;多发散布偏移已由 `targetOffset` 参数处理)
|
||||
|
||||
---
|
||||
|
||||
## [0.4.0] - 2026-06-13
|
||||
|
||||
### Added
|
||||
|
||||
- 路径积分毁伤判定:`DamageAssessment.PathInSphere` 替代离散 `ContainsPoint`,无人机穿云暴露精确计算
|
||||
- 损伤模型暴露所需暴露时间:`IDamageModel.RequiredExposureSeconds`,Planner 从损伤模型读取所需覆盖时长
|
||||
- 云团膨胀独立模块:`CloudExpansionModel`,封装 `RadiusAt`/`DensityAt`/`TimeToReach`/`TimeToDensity`/`RoundsNeeded`
|
||||
- 单元测试:`DamageAssessment` 5 项 (穿心/相邻/间隙/9 链/跨云)、`Kinematics` 抛物线 4 项 (45°标准公式验证)
|
||||
- 密度阈值统一:引擎处 `cloudDensity >= ammo.EffectiveConcentration` 统一检查,三个损伤模型去掉各自硬编码阈值
|
||||
|
||||
### Changed
|
||||
|
||||
- Planner `CalcRoundsNeeded` 从硬编码 `2:6s` 改为 `_damageModel.RequiredExposureSeconds`
|
||||
- `CloudExpansionModel.RoundsNeeded` 间距 `= 2R`(消除 `1.5f` 魔法数字)
|
||||
- `FireUnit` = 物理单元(不拆通道),Planner 一辆车发一条线
|
||||
- `MunitionEntity.HasArrived` 上升段触发(不再要求下行),附带精确插值
|
||||
- 空基平台投放逻辑:飞向云位、距目标 `driftDist` 释放、位置插值保留
|
||||
- 仿真场景缩短:Piston 5km/150km/h、Jet 5km/200km/h、AirBased 10km/150km/h,全量 22s
|
||||
|
||||
### Fixed
|
||||
|
||||
- 抛物线弹道精确求解:`CalculateLaunchAngle`/`ParabolicShellTime` 解 tan(θ) 二次方程取平射解
|
||||
- 空基 `PlatformEntity` 投放时不再传送到终点
|
||||
- 缺失 `else` 导致空基走地基分支
|
||||
- `Event.OccurredAt` 使用精确 `FireEvent.FireTime`/`m.ArrivalTime`
|
||||
- `_frameDb.Commit` 批量 50 帧
|
||||
- 报告火力单元计数(`_entityCounter` 重置)
|
||||
|
||||
### Removed
|
||||
|
||||
- `ComputeEffectiveRadius` 公式转移到 `CloudExpansionModel`
|
||||
- 损伤模型内部浓度阈值(`TriggerThreshold`/`EffectiveThreshold`)
|
||||
- `CalcRoundsNeeded` `/2` 魔法加 1
|
||||
|
||||
### Added
|
||||
|
||||
- DefensePlanner 防御规划引擎:五步流水线(威胁排序 → 弹药匹配 → 候选生成 → 贪心分配 → 时序生成)
|
||||
- 空基平台飞行与投弹:状态机(Idle → FlyingToTarget → ReadyToRelease),载机速度继承,弹药抛物线下落
|
||||
- 火力单元通道模型:GunCount × ChannelsPerGun,ChannelInterval 物理间隔,Cooldown 冷却
|
||||
- 默认火力单元配置:4 种模板(轻型/标准/重地基 + 标准空基 8 通道)
|
||||
- 推荐算法统一处理空基/地基平台类型
|
||||
- 物理间隔错开发射:发射间隔 = 云团直径 / 无人机速度
|
||||
- 每发弹按目标点位置独立计算到达时间
|
||||
- 报告显示 per-unit 发射数 + 事件标注火力单元
|
||||
|
||||
### Changed
|
||||
|
||||
- IDefenseAdvisor → IDefensePlanner:统一输入 `List<FireUnit>` + `List<DroneGroup>`
|
||||
- FireUnit 重构:统一表达空基/地基平台(Type/Position/GunCount/ChannelsPerGun)
|
||||
- SimulationEngine 内部调用 Planner 自动生成发射计划
|
||||
- MunitionEntity 空投使用 Kinematics.AirDropPosition(继承载机速度 + 重力)
|
||||
- PlatformEntity 加入 CurrentVelocity 属性
|
||||
|
||||
### Fixed
|
||||
|
||||
- 同平台多弹 FireTime 相同导致冷却跳过:FireTime += i × Cooldown
|
||||
- 同平台弹药飞行时间按目标位置变化被吃掉:统一用中心点飞行时间
|
||||
- recommendedTiming 按每发目标点独立计算(非中点)
|
||||
- 空基 FireEvent.MuzzleVelocity 错误写 800
|
||||
- SQLite domain reload 崩溃:SqliteConnectionTracker + AssemblyReloadEvents.beforeAssemblyReload
|
||||
|
||||
### Removed
|
||||
|
||||
- IDefenseAdvisor / DefaultDefenseAdvisor / RecommendMultiGroup
|
||||
- SimulationRunner.BuildFireSchedule fallback
|
||||
- 所有硬编码默认值(55f/800f/1000f/+500f/0.1f clamp)
|
||||
- 所有静默 fallback(null return/empty events)
|
||||
- Models.SimEvent 死代码
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "default-inert",
|
||||
"AerosolType": 0,
|
||||
"Name": "惰性气体弹(发烟罐型)",
|
||||
"InitialRadius": 3.8,
|
||||
"InitialVolume": 14000.0,
|
||||
"CoreDensity": 1.5,
|
||||
"EdgeDensity": 0.1,
|
||||
"InitialTemperature": 1800.0,
|
||||
"BuoyancyFactor": 0.3,
|
||||
"EffectiveConcentration": 0.0001,
|
||||
"MaxRadius": 100.0,
|
||||
"MaxDuration": 120.0,
|
||||
"SourceStrength": 10.0,
|
||||
"BurstChargeKg": 1.5,
|
||||
"TurbulentExpansionK": 3.0,
|
||||
"ParticlesJson": "{}"
|
||||
},
|
||||
{
|
||||
"Id": "default-active",
|
||||
"AerosolType": 1,
|
||||
"Name": "活性材料弹(爆炸分散型)",
|
||||
"InitialRadius": 5.0,
|
||||
"InitialVolume": 30000.0,
|
||||
"CoreDensity": 2.0,
|
||||
"EdgeDensity": 0.2,
|
||||
"InitialTemperature": 2400.0,
|
||||
"BuoyancyFactor": 0.6,
|
||||
"EffectiveConcentration": 0.0002,
|
||||
"MaxRadius": 80.0,
|
||||
"MaxDuration": 90.0,
|
||||
"SourceStrength": 12.0,
|
||||
"BurstChargeKg": 4.0,
|
||||
"TurbulentExpansionK": 4.0,
|
||||
"ParticlesJson": "{}"
|
||||
},
|
||||
{
|
||||
"Id": "default-fuel",
|
||||
"AerosolType": 2,
|
||||
"Name": "活性燃料弹(抛射分散型)",
|
||||
"InitialRadius": 3.8,
|
||||
"InitialVolume": 14000.0,
|
||||
"CoreDensity": 1.8,
|
||||
"EdgeDensity": 0.15,
|
||||
"InitialTemperature": 1900.0,
|
||||
"BuoyancyFactor": 0.4,
|
||||
"EffectiveConcentration": 0.0001,
|
||||
"MaxRadius": 90.0,
|
||||
"MaxDuration": 100.0,
|
||||
"SourceStrength": 10.0,
|
||||
"BurstChargeKg": 1.5,
|
||||
"TurbulentExpansionK": 3.0,
|
||||
"ParticlesJson": "{}"
|
||||
}
|
||||
]
|
||||
209
data/defaults.json
Normal file
209
data/defaults.json
Normal file
@ -0,0 +1,209 @@
|
||||
{
|
||||
"version": "3",
|
||||
"ammunition": [
|
||||
{
|
||||
"Id": "inert",
|
||||
"AerosolType": 0,
|
||||
"Name": "[Demo] 惰性气体弹(发烟罐型)",
|
||||
"InitialRadius": 3.8,
|
||||
"InitialVolume": 14000.0,
|
||||
"CoreDensity": 1.5,
|
||||
"EdgeDensity": 0.1,
|
||||
"InitialTemperature": 1800.0,
|
||||
"BuoyancyFactor": 0.3,
|
||||
"EffectiveConcentration": 0.0001,
|
||||
"MaxRadius": 100.0,
|
||||
"MaxDuration": 120.0,
|
||||
"SourceStrength": 10.0,
|
||||
"BurstChargeKg": 1.5,
|
||||
"TurbulentExpansionK": 3.0,
|
||||
"Phase2Duration": 30.0,
|
||||
"ParticlesJson": "{}"
|
||||
},
|
||||
{
|
||||
"Id": "active",
|
||||
"AerosolType": 1,
|
||||
"Name": "[Demo] 活性材料弹(爆炸分散型)",
|
||||
"InitialRadius": 5.0,
|
||||
"InitialVolume": 30000.0,
|
||||
"CoreDensity": 2.0,
|
||||
"EdgeDensity": 0.2,
|
||||
"InitialTemperature": 2400.0,
|
||||
"BuoyancyFactor": 0.6,
|
||||
"EffectiveConcentration": 0.0001,
|
||||
"MaxRadius": 80.0,
|
||||
"MaxDuration": 90.0,
|
||||
"SourceStrength": 12.0,
|
||||
"BurstChargeKg": 4.0,
|
||||
"TurbulentExpansionK": 4.0,
|
||||
"Phase2Duration": 30.0,
|
||||
"ParticlesJson": "{}"
|
||||
},
|
||||
{
|
||||
"Id": "fuel",
|
||||
"AerosolType": 2,
|
||||
"Name": "[Demo] 活性燃料弹(抛射分散型)",
|
||||
"InitialRadius": 3.8,
|
||||
"InitialVolume": 14000.0,
|
||||
"CoreDensity": 1.8,
|
||||
"EdgeDensity": 0.15,
|
||||
"InitialTemperature": 1900.0,
|
||||
"BuoyancyFactor": 0.4,
|
||||
"EffectiveConcentration": 0.0001,
|
||||
"MaxRadius": 90.0,
|
||||
"MaxDuration": 100.0,
|
||||
"SourceStrength": 10.0,
|
||||
"BurstChargeKg": 1.5,
|
||||
"TurbulentExpansionK": 3.0,
|
||||
"Phase2Duration": 30.0,
|
||||
"ParticlesJson": "{}"
|
||||
}
|
||||
],
|
||||
|
||||
"formations": [
|
||||
{ "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 },
|
||||
{ "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 }
|
||||
],
|
||||
|
||||
"routes": [
|
||||
{ "Id": "3km-h400", "Name": "[Demo] 3km航线-400m高", "Waypoints": [{"X":0,"Y":400,"Z":0},{"X":3000,"Y":400,"Z":0}] },
|
||||
{ "Id": "3km-h300", "Name": "[Demo] 3km航线-300m高", "Waypoints": [{"X":0,"Y":300,"Z":0},{"X":3000,"Y":300,"Z":0}] },
|
||||
{ "Id": "5km-h500", "Name": "[Demo] 5km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":5000,"Y":500,"Z":0}] },
|
||||
{ "Id": "5km-h800", "Name": "[Demo] 5km航线-800m高", "Waypoints": [{"X":0,"Y":800,"Z":0},{"X":5000,"Y":800,"Z":0}] },
|
||||
{ "Id": "10km-h500", "Name": "[Demo] 10km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":10000,"Y":500,"Z":0}] },
|
||||
{ "Id": "20km-h500", "Name": "[Demo] 20km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":20000,"Y":500,"Z":0}] }
|
||||
],
|
||||
|
||||
"fireUnits": [
|
||||
{
|
||||
"Id": "ground-light", "Name": "[Demo] 轻型地基火力单元",
|
||||
"Description": "4通道轻型平台,800m/s初速,雷达6km",
|
||||
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"RadarRange": 6000.0, "EORange": 4000.0, "IRRange": 2000.0,
|
||||
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 20000.0
|
||||
},
|
||||
{
|
||||
"Id": "ground-standard", "Name": "[Demo] 标准地基火力单元",
|
||||
"Description": "4通道标准平台,800m/s初速,雷达15km",
|
||||
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"RadarRange": 15000.0, "EORange": 8000.0, "IRRange": 5000.0,
|
||||
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 20000.0
|
||||
},
|
||||
{
|
||||
"Id": "ground-heavy", "Name": "[Demo] 重型地基火力单元",
|
||||
"Description": "6通道重型平台,600m/s初速,雷达11km,支持全弹种",
|
||||
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
|
||||
"AmmoTypes": [0, 1, 2],
|
||||
"RadarRange": 11200.0, "EORange": 7000.0, "IRRange": 4000.0,
|
||||
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 25000.0
|
||||
},
|
||||
{
|
||||
"Id": "air-standard", "Name": "[Demo] 标准空基火力单元",
|
||||
"Description": "8通道空基平台,80m/s巡航,雷达8km+光电/红外",
|
||||
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
|
||||
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"RadarRange": 4000.0, "EORange": 3000.0, "IRRange": 2000.0,
|
||||
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0
|
||||
}
|
||||
],
|
||||
|
||||
"launchPlatforms": [
|
||||
{
|
||||
"Id": "ground-light", "Name": "[Demo] 轻型地基发射平台",
|
||||
"Description": "4通道轻型平台,800m/s初速",
|
||||
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
|
||||
"AmmoTypes": [0, 1]
|
||||
},
|
||||
{
|
||||
"Id": "ground-standard", "Name": "[Demo] 标准地基发射平台",
|
||||
"Description": "4通道标准平台,800m/s初速",
|
||||
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
|
||||
"AmmoTypes": [0, 1]
|
||||
},
|
||||
{
|
||||
"Id": "ground-heavy", "Name": "[Demo] 重型地基发射平台",
|
||||
"Description": "6通道重型平台,600m/s初速,支持全弹种",
|
||||
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
|
||||
"AmmoTypes": [0, 1, 2]
|
||||
},
|
||||
{
|
||||
"Id": "air-standard", "Name": "[Demo] 标准空基发射平台",
|
||||
"Description": "8通道空基平台,80m/s巡航",
|
||||
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
|
||||
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
|
||||
"AmmoTypes": [0, 1]
|
||||
}
|
||||
],
|
||||
|
||||
"drones": [
|
||||
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼(DJI类)",
|
||||
"Model": "DJI Mavic 类", "Description": "小型四旋翼,低空侦察",
|
||||
"DroneType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
|
||||
{ "Id": "electric-scout", "Name": "[Demo] 电推侦察无人机",
|
||||
"Model": "电推侦察型", "Description": "电推固定翼,中低空侦察",
|
||||
"DroneType": 1, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
|
||||
{ "Id": "fixed-piston", "Name": "[Demo] 固定翼活塞(Orlan类)",
|
||||
"Model": "Orlan-10 类", "Description": "活塞固定翼,中空侦察",
|
||||
"DroneType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
|
||||
{ "Id": "shahed", "Name": "[Demo] 活塞巡飞弹(Shahed类)",
|
||||
"Model": "Shahed-136", "Description": "活塞巡飞弹,低空攻击",
|
||||
"DroneType": 1, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
|
||||
{ "Id": "tb2", "Name": "[Demo] 中空长航时(TB2类)",
|
||||
"Model": "TB2 类", "Description": "活塞中空长航时",
|
||||
"DroneType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
|
||||
{ "Id": "cruise-missile", "Name": "[Demo] 巡航导弹(喷气式)",
|
||||
"Model": "巡航导弹类", "Description": "喷气式高速目标",
|
||||
"DroneType": 2, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 200.0, "TypicalAltitude": 2000.0 }
|
||||
],
|
||||
|
||||
"detectionEquipment": [
|
||||
{ "Id": "radar-mr", "Name": "[Demo] 中程防空雷达",
|
||||
"RadarRange": 20000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 30.0,
|
||||
"MinElevation": -2.0, "MaxElevation": 70.0, "MinDetectAlt": 50.0, "MaxDetectAlt": 30000.0 },
|
||||
{ "Id": "radar-sr", "Name": "[Demo] 近程防空雷达",
|
||||
"RadarRange": 10000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 50.0,
|
||||
"MinElevation": -2.0, "MaxElevation": 70.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0 },
|
||||
{ "Id": "eo-station", "Name": "[Demo] 光电跟踪站",
|
||||
"RadarRange": 0.0, "EORange": 15000.0, "IRRange": 8000.0, "Accuracy": 20.0,
|
||||
"MinElevation": -10.0, "MaxElevation": 90.0, "MinDetectAlt": 10.0, "MaxDetectAlt": 20000.0 },
|
||||
{ "Id": "ir-sentry", "Name": "[Demo] 红外哨",
|
||||
"RadarRange": 0.0, "EORange": 0.0, "IRRange": 10000.0, "Accuracy": 40.0,
|
||||
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 10.0, "MaxDetectAlt": 15000.0 }
|
||||
],
|
||||
|
||||
"weather": [
|
||||
{ "Id": "sunny-calm", "Name": "[Demo] 晴天无风",
|
||||
"WeatherType": 0, "WindSpeed": 3.0, "WindDirection": 0,
|
||||
"Temperature": 25.0, "Humidity": 50.0, "Pressure": 1013.0, "Visibility": 8000.0 },
|
||||
{ "Id": "sunny-windy", "Name": "[Demo] 晴天大风",
|
||||
"WeatherType": 0, "WindSpeed": 10.0, "WindDirection": 4,
|
||||
"Temperature": 28.0, "Humidity": 40.0, "Pressure": 1010.0, "Visibility": 10000.0 },
|
||||
{ "Id": "overcast", "Name": "[Demo] 阴天",
|
||||
"WeatherType": 1, "WindSpeed": 5.0, "WindDirection": 2,
|
||||
"Temperature": 18.0, "Humidity": 70.0, "Pressure": 1015.0, "Visibility": 5000.0 },
|
||||
{ "Id": "fog", "Name": "[Demo] 雾天",
|
||||
"WeatherType": 2, "WindSpeed": 2.0, "WindDirection": 1,
|
||||
"Temperature": 12.0, "Humidity": 95.0, "Pressure": 1020.0, "Visibility": 500.0 },
|
||||
{ "Id": "rain", "Name": "[Demo] 雨天",
|
||||
"WeatherType": 3, "WindSpeed": 8.0, "WindDirection": 3,
|
||||
"Temperature": 15.0, "Humidity": 90.0, "Pressure": 1005.0, "Visibility": 3000.0 },
|
||||
{ "Id": "night", "Name": "[Demo] 夜间",
|
||||
"WeatherType": 4, "WindSpeed": 2.0, "WindDirection": 5,
|
||||
"Temperature": 10.0, "Humidity": 65.0, "Pressure": 1018.0, "Visibility": 2000.0 }
|
||||
]
|
||||
}
|
||||
BIN
data/fonts/CJK-Font.ttf
Normal file
BIN
data/fonts/CJK-Font.ttf
Normal file
Binary file not shown.
19
data/planner_config.json
Normal file
19
data/planner_config.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"CloudOverlapRatio": 0.2,
|
||||
"CriticalProbabilityThreshold": 0.5,
|
||||
"MaxInterceptProbability": 0.95,
|
||||
"TypeCoefficient": {
|
||||
"HighSpeed": 4.0,
|
||||
"FixedWing": 2.0,
|
||||
"Rotor": 1.0
|
||||
},
|
||||
"AmmoMatch": {
|
||||
"Electric": "InertGas",
|
||||
"Piston": "InertGas",
|
||||
"Jet": "ActiveMaterial"
|
||||
},
|
||||
"DefaultDetectionAccuracy": 50.0,
|
||||
"ExpansionFactor": 0.9,
|
||||
"TimingSafetyMargin": 1.0,
|
||||
"ReactionTime": 5.0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
369
docs/design/technical/DefensePlanner-技术方案.md
Normal file
369
docs/design/technical/DefensePlanner-技术方案.md
Normal file
@ -0,0 +1,369 @@
|
||||
# DefensePlanner 防御规划引擎 — 技术方案
|
||||
|
||||
- **版本**:V4
|
||||
- **日期**:2026-06-15
|
||||
- **状态**:已实现
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁批次列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
|
||||
|
||||
**当前问题**:
|
||||
|
||||
- `DefaultDefenseAdvisor` 本质是"单威胁 → 单方案"的规则匹配器
|
||||
- `Recommend()` 接收 `ThreatProfile`(不含火力单元信息),无法做资源分配
|
||||
- `RecommendMultiGroup()` 接收 `List<FireUnit>`,但决策逻辑简单(先到先服务)
|
||||
- 空基/地基平台类型通过 `PreferredPlatformType` 开关绕过,而非算法自主选择
|
||||
|
||||
**目标**:统一的规划入口,给定火力单元池和威胁列表,输出分配方案。
|
||||
|
||||
---
|
||||
|
||||
## 2. 输入模型
|
||||
|
||||
### 2.1 FireUnit(火力单元)
|
||||
|
||||
统一表达空基和地基平台,作为规划器的基本资产单元:
|
||||
|
||||
```csharp
|
||||
class FireUnit {
|
||||
string Id; // 唯一标识
|
||||
PlatformType Type; // AirBased / GroundBased
|
||||
Vector3 Position; // 待命坐标(空基含巡航高度)
|
||||
// ── 发射装置 ──
|
||||
int GunCount; // 火炮数量(默认 1)
|
||||
int ChannelsPerGun; // 每炮火力通道数(默认 1)
|
||||
int TotalChannels => GunCount * ChannelsPerGun;
|
||||
float ChannelInterval; // 同通道连发最小间隔 s
|
||||
// ── 弹药 ──
|
||||
int TotalMunitions; // 总载弹量
|
||||
List<AerosolType> AmmoTypes; // 可装填的弹药类型
|
||||
float Cooldown; // 通道冷却时间 s
|
||||
float AmmoChangeTime; // 更换弹种时间 s
|
||||
// ── 搜索跟踪 ──
|
||||
float RadarRange; // 雷达探测距离 m
|
||||
float EORange; // 光电探测距离 m
|
||||
float IRRange; // 红外探测距离 m
|
||||
// ── 空基 ──
|
||||
float CruiseSpeed; // 巡航速度 m/s
|
||||
float ReleaseAltitude; // 投放高度 m
|
||||
// ── 地基 ──
|
||||
float MuzzleVelocity; // 初速 m/s
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 DroneWave(威胁批次)
|
||||
|
||||
```csharp
|
||||
class DroneWave {
|
||||
string WaveId; // 批次 ID
|
||||
ScenarioDrone Profile; // 类型、数量、动力、翼展、速度、高度
|
||||
List<Waypoint> Waypoints; // 航路点
|
||||
float ArrivalTime; // 预计算:到达防御区域中点的时间 s
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 CombatScene(作战环境)
|
||||
|
||||
沿用现有的 `CombatScene`,提供风速、风向、天气等影响扩散模型计算的参数。
|
||||
|
||||
---
|
||||
|
||||
## 3. 输出模型
|
||||
|
||||
### 3.1 UnitAssignment(单元分配)
|
||||
|
||||
```csharp
|
||||
class UnitAssignment {
|
||||
string FireUnitId; // 分配到的火力单元
|
||||
string DroneWaveId; // 对抗的威胁批次
|
||||
AerosolType AmmoType; // 装填的弹药类型
|
||||
int RoundsFired; // 本次发射几发
|
||||
float FirstFireTime; // 首发发射时机 s
|
||||
List<FireEvent> FireEvents; // 具体发射事件(含位置、时间)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 DefensePlan(规划结果)
|
||||
|
||||
```csharp
|
||||
class DefensePlan {
|
||||
List<UnitAssignment> Assignments; // 分配方案
|
||||
List<FireEvent> MergedSchedule; // 合并后的发射计划(排序)
|
||||
float OverallProbability; // 总体拦截概率估计
|
||||
int ThreatsEngaged; // 被分配方案的威胁数
|
||||
int ThreatsUnengaged; // 无法分配方案的威胁数
|
||||
string Summary; // 人类可读概览
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 DefensePlanner 输出
|
||||
|
||||
```csharp
|
||||
class PlannerResult {
|
||||
DefensePlan Best; // 最优方案
|
||||
DefensePlan Critical; // 临界方案(刚好有效)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 内部流程(五步法)
|
||||
|
||||
```
|
||||
输入:List<FireUnit> + List<DroneWave> + CombatScene
|
||||
│
|
||||
▼
|
||||
Step 1 — 威胁排序
|
||||
│ 威胁指数 = 速度系数 × 目标类型系数(可扩展重量、载弹量等)
|
||||
│ 综合优先级 = 威胁指数 / (到达时间 + 1)
|
||||
│ 同到达时间下高威胁优先
|
||||
│
|
||||
▼
|
||||
Step 2 — 弹药匹配
|
||||
│ PowerType → AerosolType(规则表)
|
||||
│ 输出:每个威胁需要的弹药类型
|
||||
│
|
||||
▼
|
||||
Step 3 — 候选生成
|
||||
│ 对每个(威胁, 火力单元)组合:
|
||||
│ ① 弹药兼容性检查
|
||||
│ ② 空间可达性(地基:弹道射程;空基:飞行距离)
|
||||
│ ③ 时间窗口计算(最早/最晚拦截时机)
|
||||
│ 输出:List<InterceptCandidate>
|
||||
│
|
||||
▼
|
||||
Step 4 — 分配求解
|
||||
│ 贪心策略(v1,后续可升级回溯/匈牙利):
|
||||
│ 按威胁优先级遍历
|
||||
│ → 选最早可拦截的兼容单元
|
||||
│ → 弹药必须精确匹配,不降级(ActiveMaterial 不能替代 InertGas)
|
||||
│ → 标记单元占用(冷却 + 换弹 + 飞行时间)
|
||||
│ → 弹药耗尽则移除
|
||||
│
|
||||
▼
|
||||
Step 5 — 时序生成
|
||||
│ 对每个分配:
|
||||
│ 地基:FireTime = 最佳交汇时刻 − 炮弹飞行时间
|
||||
│ 空基:FireTime = 最佳交汇时刻 − 弹药下落时间(固定阵位水平发射)
|
||||
│ 合并排序 → MergedFireSchedule
|
||||
│
|
||||
▼
|
||||
输出:PlannerResult { Best, Critical }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键算法详设
|
||||
|
||||
### 5.1 威胁排序
|
||||
|
||||
**威胁指数** = 速度系数 × 目标类型系数(后续可扩展重量、载弹量等要素)
|
||||
|
||||
| 目标类型 | 类型系数 | 理由 |
|
||||
|----------|:------:|------|
|
||||
| 高速目标(300km/h+) | 4 | 最快突防 |
|
||||
| 喷气式 | 3 | 高温发动机,毁伤窗口短 |
|
||||
| 固定翼 | 2 | 速度中等 |
|
||||
| 活塞式 | 2 | 速度中等 |
|
||||
| 旋翼 | 1 | 慢速 |
|
||||
| 电推 | 1 | 慢速,无热源 |
|
||||
|
||||
速度系数 = TypicalSpeed / 60(以 60 km/h 为基准归一化)
|
||||
|
||||
**综合优先级** = 威胁指数 / (ArrivalTime + 1),按降序排列。同到达时间下高威胁优先。
|
||||
|
||||
### 5.2 InterceptCandidate(拦截候选)
|
||||
|
||||
```csharp
|
||||
class InterceptCandidate {
|
||||
FireUnit Unit;
|
||||
AerosolType AmmoType;
|
||||
float EarliestTime; // 最早可拦截时刻(仿真秒)
|
||||
float LatestTime; // 最晚可拦截时刻
|
||||
float Coverage; // 有效覆盖时长
|
||||
float KillProbability; // 预计杀伤概率
|
||||
}
|
||||
```
|
||||
|
||||
**候选生成逻辑**:
|
||||
|
||||
对于给定的威胁批次和火力单元:
|
||||
|
||||
1. **弹药兼容性**:`Unit.AmmoTypes` 包含威胁需要的弹药类型
|
||||
2. **地基可达性**:计算目标与部署点的水平距离,校验 `MuzzleVelocity` 射程
|
||||
3. **空基可达性**:计算飞行距离,`飞行时间 = distance(巡逻点, 投放点) / CruiseSpeed`
|
||||
4. **时间窗口**:
|
||||
- 威胁到达防御区域的时间段 `[tEnter, tExit]`
|
||||
- 单元可拦截的时间段 = 威胁窗口 ∩(单元可用时间 + 弹药飞行时间)
|
||||
- 空基额外加平台飞行时间
|
||||
5. **覆盖率**:基于云团有效半径、持续时间、无人机速度的综合估计
|
||||
|
||||
### 5.3 贪心分配算法
|
||||
|
||||
```
|
||||
pending = threats.OrderBy(t => t.ArrivalTime)
|
||||
available = fireUnits.Clone()
|
||||
|
||||
timeNow = 0
|
||||
assignments = []
|
||||
|
||||
for each threat in pending:
|
||||
candidates = GenerateCandidates(threat, available, timeNow)
|
||||
if candidates.isEmpty:
|
||||
threats.Unengaged++
|
||||
continue
|
||||
|
||||
best = candidates.OrderBy(c => c.EarliestTime)
|
||||
.ThenByDescending(c => c.KillProbability)
|
||||
.First()
|
||||
|
||||
assignment = Commit(best, threat)
|
||||
assignments.Add(assignment)
|
||||
|
||||
// 更新单元状态
|
||||
unit = best.Unit
|
||||
unit.TotalMunitions -= assignment.RoundsFired
|
||||
timeNow = max(timeNow, assignment.LastFireTime + unit.Cooldown)
|
||||
```
|
||||
|
||||
### 5.4 临界方案(Critical Plan)
|
||||
|
||||
临界方案 = 刚好达到可接受下限概率(默认 50%)的最小配置:
|
||||
- 从最优方案的分配列表出发
|
||||
- 逐次减少每个单元分配的弹药数
|
||||
- 直到整体拦截概率刚好跌破 50%
|
||||
- 上一轮(≥50% 的最小配置)即为临界方案
|
||||
|
||||
> 作用:给操作员一个置信区间——最优 vs 临界,展示"再少就不够了"的底线。
|
||||
|
||||
### 5.5 多批次合并
|
||||
|
||||
```
|
||||
Step 4 的贪心算法天然支持多威胁:
|
||||
- 按优先级顺序处理
|
||||
- 前一威胁占用的单元在后续威胁中不可用(直到冷却/换弹完成)
|
||||
- 弹药消耗全局追踪
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 接口设计
|
||||
|
||||
### 6.1 规划器接口
|
||||
|
||||
```csharp
|
||||
public interface IDefensePlanner
|
||||
{
|
||||
/// <summary>为给定火力单元池和威胁列表生成规划方案</summary>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene environment);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 兼容现有接口
|
||||
|
||||
`DefaultDefenseAdvisor` 改造为 `DefaultDefensePlanner`:
|
||||
|
||||
```csharp
|
||||
public class DefaultDefensePlanner : IDefensePlanner
|
||||
{
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
|
||||
public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog) { ... }
|
||||
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene env)
|
||||
{
|
||||
// Step 1-5
|
||||
}
|
||||
|
||||
// 内部方法
|
||||
private List<DroneWave> Prioritize(List<DroneWave> threats) { ... }
|
||||
private AerosolType MatchAmmo(PowerType power) { ... }
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneWave threat, List<FireUnit> units, float now) { ... }
|
||||
private DefensePlan Solve(List<DroneWave> threats, List<FireUnit> units) { ... }
|
||||
private DefensePlan DeriveCritical(DefensePlan best) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 接口迁移(已完成)
|
||||
|
||||
| 旧接口 | 新接口 | 状态 |
|
||||
|--------|--------|------|
|
||||
| `IDefenseAdvisor.Recommend(ThreatProfile)` | `IDefensePlanner.Plan(fireUnits, threats, env)` | ✅ 已替换 |
|
||||
| `IDefenseAdvisor.GetDefenseRecommendation(scenarioId)` | 不再需要 | ✅ 已删除 |
|
||||
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多批次 | ✅ 已删除 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据流改造(已实现)
|
||||
|
||||
```
|
||||
SimulationEngine.Initialize(scenarioId)
|
||||
│
|
||||
│ // 引擎内部组装
|
||||
├── BuildFireUnits(config) → List<FireUnit>
|
||||
├── BuildDroneWaves(config) → List<DroneWave>
|
||||
└── _scene (CombatScene)
|
||||
│
|
||||
▼
|
||||
IDefensePlanner.Plan(fireUnits, threats, scene)
|
||||
│
|
||||
▼
|
||||
PlannerResult.Best.MergedSchedule
|
||||
│
|
||||
▼
|
||||
引擎 Tick() 按发射计划执行
|
||||
```
|
||||
|
||||
> 引擎是 Planner 的唯一调用者。测试和 UI 代码不再直接接触 Planner。
|
||||
|
||||
---
|
||||
|
||||
## 8. 实现状态
|
||||
|
||||
### ✅ 已实现
|
||||
|
||||
- `IDefensePlanner` + `DefaultDefensePlanner`(五步流水线)
|
||||
- `FireUnit` 通道模型(GunCount/ChannelsPerGun/ChannelInterval/搜索跟踪设备)
|
||||
- 贪心分配 + 物理间隔错发 + 按目标点独立计时
|
||||
- 空基/地基弹道统一处理
|
||||
- `SimulationEngine` 内部调用 Planner 自动生成发射计划
|
||||
- `DefaultFireUnits` 4 种模板
|
||||
- 报告显示 per-unit 发射数和火力单元列
|
||||
- 所有硬编码默认值和静默 fallback 已删除
|
||||
|
||||
### ❌ 已删除
|
||||
|
||||
- `IDefenseAdvisor` / `DefaultDefenseAdvisor` / `RecommendMultiGroup`
|
||||
- `ThreatProfile` / `DefenseSolution` / `DefenseRecommendation` / `MultiGroupRecommendation`(AlgorithmTypes 中保留但未使用)
|
||||
- `Models.SimEvent`(死代码)
|
||||
- 所有 `?.` + 任意默认值模式
|
||||
|
||||
### 🔒 不变
|
||||
|
||||
- `Kinematics` / `GaussianPuffDispersion` / `DamageModelRouter`
|
||||
- 所有 Repository 和数据模型(ScenarioUnit 新增 GunCount/ChannelsPerGun/ChannelInterval)
|
||||
- `ScenarioService` 接口
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对 |
|
||||
|------|------|------|
|
||||
| 贪心算法局部最优,整体非最优 | 部分威胁可能被跳过 | 后续可升级为匈牙利算法或回溯搜索;贪心结果对大多数场景足够 |
|
||||
| 覆盖率估算依赖简化模型 | 拦截概率不精确 | 使用现有的 `GaussianPuffDispersion` + `Kinematics`,与仿真引擎一致 |
|
||||
| 空基/地基混合分配复杂 | 边界情况遗漏 | Step 3 候选生成对每种平台类型独立计算,Step 4 统一排序;单元测试覆盖典型混合场景 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 已决策事项
|
||||
|
||||
| # | 议题 | 决策 |
|
||||
|---|------|------|
|
||||
| 1 | 威胁排序 | 综合威胁指数:类型系数(高速4/喷气3/固定翼2/活塞2/旋翼1/电推1)× 速度系数(km/h ÷ 60),除以到达时间。可扩展重量、载弹量等要素 |
|
||||
| 2 | 临界方案 | 概率阈值定义:刚好 ≥ 50% 的最小资源配置 |
|
||||
| 3 | 分配算法 | v1 贪心;后续按需升级回溯搜索或匈牙利算法 |
|
||||
| 4 | Failover | 不做降级。弹药必须精确匹配(ActiveMaterial 不能替代 InertGas) |
|
||||
@ -4,64 +4,86 @@
|
||||
|
||||
| 实体 | 类 | 生命周期 | 主要属性 |
|
||||
|------|------|------|------|
|
||||
| 无人机 | `DroneEntity` | 仿真全程 | Pos, Hp, Status, Route, ExposureTime |
|
||||
| 发射平台 | `PlatformEntity` | 仿真全程 | Pos, PlatformType, AerosolType, MunitionCount, Cooldown |
|
||||
| 飞行弹药 | `MunitionEntity` | 发射→到达释放高度 | Pos, LaunchMode, Target, HasArrived |
|
||||
| 气溶胶云团 | `CloudEntity` | 弹药到达→消散 | Center, Radius, CoreDensity, IsDissipated |
|
||||
| 无人机 | `DroneEntity` | 仿真全程 | Pos, Hp, Status, Route, ExposureTime, TraveledArc, Progress |
|
||||
| 发射平台 | `PlatformEntity` | 仿真全程 | Pos, PlatformType, State(Idle), AerosolType, MunitionCount, Cooldown, CruiseSpeed, MuzzleVelocity, CurrentVelocity, Target, FlightDistance |
|
||||
| 飞行弹药 | `MunitionEntity` | 发射→到达释放高度 | Pos, Velocity, LaunchAngle, Azimuth, MuzzleVelocity, FlightDuration, Start, Target, HasArrived, ElapsedTime |
|
||||
| 气溶胶云团 | `CloudEntity` | 弹药到达→消散 | Pos, Radius, Density, Phase, Elapsed, IsDissipated |
|
||||
| 管控区域 | `ControlZoneEntity` | 仿真全程 | Vertices, Min/MaxAltitude |
|
||||
| 探测设备 | `DetectionEntity` | 仿真全程 | Source(雷达/光电/红外距离+3D球冠参数), 每无人机探测状态机(Undetected⇄Detected) |
|
||||
|
||||
## 事件一览
|
||||
|
||||
| 事件 | 触发条件 | 参数 | Unity 典型响应 |
|
||||
|------|------|------|------|
|
||||
| `OnTargetDetected` | 无人机首次进入任一探测设备 3D 球冠范围 | `DroneEntity`, `DetectionEntity` | 显示发现标记 / 3D 球冠 wireframe 高亮 |
|
||||
| `OnMunitionLaunched` | 发射计划时间到达 + 平台就绪 | `MunitionEntity` | 生成炮弹模型,播放发射动画 |
|
||||
| `OnCloudGenerated` | 弹药到达释放高度 | `CloudEntity` | 生成粒子系统,初始半径+颜色 |
|
||||
| `OnDroneDestroyed` | 毁伤判定 HP ≤ 0 | `DroneEntity` | 播放爆炸/坠毁动画 |
|
||||
| `OnDroneReachedTarget` | 到达最后航路点 | `DroneEntity` | 显示"目标抵达"提示 |
|
||||
| `OnZoneIntruded` | 无人机进入管控区 | `DroneEntity`, `ControlZoneEntity` | 红色警报 |
|
||||
| `OnSimulationEnded` | 所有无人机状态 ≠ Flying | 无 | 显示结果面板,触发报告生成 |
|
||||
| `PlanningFailed` | planner 无法生成拦截方案(引擎发出 SimEvent,无回调) | 无(通过 `SimEventType.PlanningFailed` 事件流传递) | 前端显示规划失败原因 |
|
||||
|
||||
## 实体 × 事件 矩阵
|
||||
|
||||
| | MunitionLaunched | CloudGenerated | DroneDestroyed | ReachedTarget | ZoneIntruded | SimEnded |
|
||||
|------|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| DroneEntity | | | ✅ | ✅ | ✅ | |
|
||||
| PlatformEntity | ✅ | | | | | |
|
||||
| MunitionEntity | ✅ | | | | | |
|
||||
| CloudEntity | | ✅ | | | | |
|
||||
| ControlZoneEntity | | | | | ✅ | |
|
||||
| 全局 | | | | | | ✅ |
|
||||
| | TargetDetected | MunitionLaunched | CloudGenerated | DroneDestroyed | ReachedTarget | ZoneIntruded | SimEnded | PlanningFailed |
|
||||
|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| DroneEntity | ✅ | | | ✅ | ✅ | ✅ | | |
|
||||
| PlatformEntity | | ✅ | | | | | | |
|
||||
| MunitionEntity | | ✅ | | | | | | |
|
||||
| CloudEntity | | | ✅ | | | | | |
|
||||
| ControlZoneEntity | | | | | | ✅ | | |
|
||||
| DetectionEntity | ✅ | | | | | | | |
|
||||
| 全局 | | | | | | | ✅ | ✅ |
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
FireSchedule (算法输出)
|
||||
想定配置(ScenarioUnit + ScenarioDrone + RoutePlan + CombatScene)
|
||||
│
|
||||
▼
|
||||
SimulationEngine.Initialize()
|
||||
│
|
||||
├─ BuildFireUnits() → List<FireUnit>
|
||||
├─ BuildDroneWaves() → List<DroneWave>
|
||||
├─ IDefensePlanner.Plan() → FireSchedule
|
||||
│ └─ MergedSchedule.Count == 0 → PlanningFailed 事件
|
||||
│
|
||||
▼
|
||||
IDefensePlanner.Plan(fireUnits, threats, scene)
|
||||
│
|
||||
├─ 五步流水线 → FireSchedule(发射计划)
|
||||
│
|
||||
▼
|
||||
SimulationEngine.Tick()
|
||||
│
|
||||
├─ 时间到达 → PlatformEntity.Fire() → MunitionEntity 创建
|
||||
│ │
|
||||
│ ├─ OnMunitionLaunched ──→ Unity: 炮弹 3D 模型
|
||||
│ │
|
||||
│ └─ 飞行 → 到达释放高度
|
||||
│ │
|
||||
│ ├─ CloudEntity 创建
|
||||
│ │
|
||||
│ ├─ OnCloudGenerated ──→ Unity: 粒子系统
|
||||
│ │
|
||||
│ └─ 扩散 → 毁伤判定
|
||||
│ │
|
||||
│ ├─ DroneEntity.Hp -= dmg
|
||||
│ │
|
||||
│ └─ HP≤0 → OnDroneDestroyed ──→ Unity: 爆炸动画
|
||||
├─ 【地基】时间到达 → PlatformEntity.Release() → MunitionEntity 创建
|
||||
│ └─ OnMunitionLaunched ──→ Unity: 炮弹 3D 模型
|
||||
│
|
||||
├─ 【空基】时间到达 → PlatformEntity.Release() → MunitionEntity 创建
|
||||
│ └─ OnMunitionLaunched ──→ Unity: 空基发射
|
||||
│
|
||||
├─ MunitionEntity 飞行 → 到达释放高度
|
||||
│ ├─ 地基:抛物线弹道(Kinematics.ParabolicPosition)
|
||||
│ └─ 空基:水平初速 + 重力(launchAngle=0, ParabolicPosition)
|
||||
│ │
|
||||
│ ├─ CloudEntity 创建
|
||||
│ ├─ OnCloudGenerated ──→ Unity: 粒子系统
|
||||
│ └─ 扩散 → 毁伤判定
|
||||
│ ├─ DroneEntity.Hp -= dmg
|
||||
│ └─ HP≤0 → OnDroneDestroyed ──→ Unity: 爆炸动画
|
||||
│
|
||||
├─ DroneEntity.Update() → 到达终点
|
||||
│ └─ OnDroneReachedTarget ──→ Unity: 抵达提示
|
||||
│ └─ OnDroneReachedTarget ──→ Unity: 抵达提示
|
||||
│
|
||||
├─ 【实时探测扫描】DetectionEntity × 飞行无人机
|
||||
│ ├─ 3D 球冠判定(IsInCoverage)→ 首次进入置 Detected
|
||||
│ ├─ 离开范围 → 回退 Undetected(再次进入重新触发)
|
||||
│ └─ OnTargetDetected ──→ Unity: 发现标记 / 球冠高亮
|
||||
│
|
||||
├─ ControlZone.ContainsPoint()
|
||||
│ └─ OnZoneIntruded ──→ Unity: 红色警报
|
||||
│ └─ OnZoneIntruded ──→ Unity: 红色警报
|
||||
│
|
||||
└─ All drones done
|
||||
└─ OnSimulationEnded ──→ Unity: 结果面板 + 报告生成
|
||||
└─ OnSimulationEnded ──→ Unity: 结果面板 + 报告生成
|
||||
```
|
||||
|
||||
58
docs/design/想定参数对照表.md
Normal file
58
docs/design/想定参数对照表.md
Normal file
@ -0,0 +1,58 @@
|
||||
# 默认想定参数对照表
|
||||
|
||||
> **目的**:跟踪 6 个 Demo 想定的参数配置,确保探测范围、部署位置、航路长度等相互匹配。
|
||||
> **更新日期**:2026-06-16
|
||||
|
||||
---
|
||||
|
||||
## 一、无人机模板
|
||||
|
||||
| ID | 名称 | 动力 | 翼展 | 速度(km/h) | 典型高度 |
|
||||
|------|------|------|:---:|:---:|:---:|
|
||||
| electric-scout | 电推侦察无人机 | 电推 | 1.8m | 100 | 500m |
|
||||
| shahed | 活塞巡飞弹(Shahed类) | 活塞 | 2.5m | 200 | 500m |
|
||||
| cruise-missile | 巡航导弹(喷气式) | 喷气 | 1.5m | 200 | 2000m |
|
||||
|
||||
> **注**:速度以航路 waypoint.Speed 为准,TypicalSpeed 仅展示用。
|
||||
|
||||
---
|
||||
|
||||
## 二、火力单元模板
|
||||
|
||||
| ID | 类型 | 雷达 | 光电 | 红外 | 3D 球冠 | 初速/巡航 |
|
||||
|------|------|------:|------:|------:|------|:---:|
|
||||
| ground-light | 地基 | 6000m | 4000m | 2000m | -5°~85°, 30m~20km | 800m/s |
|
||||
| ground-standard | 地基 | 15000m | 8000m | 5000m | -5°~85°, 30m~20km | 800m/s |
|
||||
| ground-heavy | 地基 | 8000m | 5000m | 3000m | -5°~85°, 30m~25km | 600m/s |
|
||||
| air-standard | 空基 | — | 9600m | 6000m | -80°~30°, 30m~15km | 80m/s |
|
||||
|
||||
---
|
||||
|
||||
## 三、Demo 想定对照
|
||||
|
||||
| # | 想定 | 无人机 | 高度 | 速度 | 航路 | 火力单元 | 位置 | 探测 |
|
||||
|---|------|------|:---:|:---:|:---:|------|------|:---:|
|
||||
| 1 | 无防御 | shahed | 400m | 600 | 3km | — | — | — |
|
||||
| 2 | 管控区侵入 | electric-scout | 300m | 300 | 3km | — | — | — |
|
||||
| 3 | 活塞拦截-西风5ms | shahed | 500m | 200 | **7km** | ground-light | **(7000,0,50)** | 80%覆盖 |
|
||||
| 4 | 喷气拦截-活性材料 | cruise-missile | 500m | 200 | **10km** | ground-standard | **(10000,0,50)** | 全程覆盖 |
|
||||
| 5 | 空基拦截-东风5ms | shahed | 500m | 200 | 20km | air-standard | **(6000,1000,0)** | 80%覆盖 |
|
||||
| 6 | 3架空基编队 ⏸️ | shahed×3 | 500m | 200 | 20km | air-standard×3 | (12000,1000,0) | — |
|
||||
|
||||
---
|
||||
|
||||
## 四、拦截点计算
|
||||
|
||||
| 想定 | 算法 | 探测弧长 | R+exp+fall | 拦截弧长 | 炮弹/平台时间 |
|
||||
|------|------|------:|------:|------:|------:|
|
||||
| 活塞 | `InterceptCalculator.Compute`(可变角度抛物线) | ~1021m | 5+27s | 由算法求解 | 由算法求解 |
|
||||
| 喷气 | `InterceptCalculator.Compute`(可变角度抛物线) | 0 | 5+30s | 由算法求解 | 由算法求解 |
|
||||
| 空基 | `InterceptCalculator.ComputeHorizontal`(θ=0°平抛) | 0 | 5+27+10s | 由算法求解 | 算法 + 平台飞行 |
|
||||
|
||||
---
|
||||
|
||||
## 五、历史变更
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-06-16 | 初始创建。Phase 10 完成。地基放终点+80%探测;空基平抛算法;拦截点由 InterceptCalculator 求解。 |
|
||||
@ -1,8 +1,8 @@
|
||||
# 实施计划与任务跟踪
|
||||
|
||||
> **项目**:反无人机仿真系统后端
|
||||
> **文档版本**:V1.1
|
||||
> **更新日期**:2026-06-11
|
||||
> **文档版本**:V1.8
|
||||
> **更新日期**:2026-06-22
|
||||
|
||||
---
|
||||
|
||||
@ -14,10 +14,16 @@ Phase 2 ✅ 想定管理
|
||||
Phase 3 ✅ 算法层
|
||||
Phase 4 ✅ 仿真引擎
|
||||
Phase 5 ✅ 报告生成
|
||||
Phase 6 ⬜ Unity 集成(需 Unity 22.3.62)
|
||||
Phase 6 ✅ Unity 集成(桥接层 + 示例项目)
|
||||
Phase 7 ✅ 打磨收尾
|
||||
Phase 8 ✅ 天气/物理模型统一
|
||||
Phase 9 ✅ 性能优化 + 架构文档校准
|
||||
Phase 10 ✅ 探测实时链路(已完成)
|
||||
Phase 11 ✅ 运动学重构 + 实体暴露 + 基础数据 CRUD
|
||||
Phase 12 ✅ 空基/地基统一规划 + 编队轴 + LaneDivider
|
||||
Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
|
||||
────────────────────────
|
||||
已完成 P1-P5 + P7
|
||||
已完成 P1-P13
|
||||
```
|
||||
|
||||
---
|
||||
@ -31,7 +37,7 @@ Phase 7 ✅ 打磨收尾
|
||||
|
||||
| # | 任务 | 状态 |
|
||||
|---|------|------|
|
||||
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService/GroupService、单元测试 | ✅ |
|
||||
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService、单元测试 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
@ -89,27 +95,33 @@ Phase 7 ✅ 打磨收尾
|
||||
|---|------|------|
|
||||
| 5.1-5.8 | ReportGenerator、ReportService、ExportToFile、搜索分页、单元测试 | ✅ |
|
||||
|
||||
**注**:PDF/Word 导出暂以 Markdown 替代,后续可扩展。
|
||||
**注**:PDF 导出已实现(见 8.2.1),仿真后自动生成 MD,PDF 按需调用 `ExportReport(id, "pdf")`。
|
||||
|
||||
---
|
||||
|
||||
### Phase 6:Unity 集成 ⬜
|
||||
### Phase 6:Unity 集成 ✅
|
||||
|
||||
**目标**:将 Core.dll 集成到 Unity,实现完整运行链路。
|
||||
**前置条件**:安装 Unity Hub + Editor 22.3.62
|
||||
**前置条件**:安装 Unity Hub + Editor 22.3.62
|
||||
**位置**:`src/Unity/`(完整 Unity 项目)
|
||||
|
||||
| # | 任务 | 预估 | 状态 |
|
||||
|---|------|------|------|
|
||||
| 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ⬜ |
|
||||
| 6.2 | 实现 `UnityPathProvider` | 1h | ⬜ |
|
||||
| 6.3 | 实现 `ModelManager` | 3h | ⬜ |
|
||||
| 6.4 | 实现 `ScenarioManager`(5 步向导) | 4h | ⬜ |
|
||||
| 6.5 | 实现 `SimulationRunner`(Update 驱动) | 3h | ⬜ |
|
||||
| 6.6 | 实现 `ReplayController`(协程回放) | 3h | ⬜ |
|
||||
| 6.7 | 实现 `ReportManager` | 2h | ⬜ |
|
||||
| 6.8 | 3D 实体可视化 | 4h | ⬜ |
|
||||
| 6.9 | 粒子系统参数传递 | 3h | ⬜ |
|
||||
| 6.10 | 端到端联调 | 6h | ⬜ |
|
||||
| # | 任务 | 预估 | 状态 | 说明 |
|
||||
|---|------|------|------|------|
|
||||
| 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ✅ | `src/Unity/` 项目,16 个 DLL → `Assets/Plugins/` |
|
||||
| 6.2 | 实现 `UnityPathProvider` | 1h | ✅ | `Application.persistentDataPath` 桥接 |
|
||||
| 6.3 | 实现 `ModelManager` | 3h | ✅ | 导入/删除/查询,含 Verify |
|
||||
| 6.4 | 实现 `ScenarioManager`(5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多批次 Route |
|
||||
| 6.5 | 实现 `SimulationRunner`(Update 驱动) | 3h | ✅ | Tick 驱动 + 事件订阅 + 实体位置同步 + 炮弹轨迹可视化 |
|
||||
| 6.6 | 实现 `ReplayController`(帧加载) | 3h | ✅ | 从分库加载帧数据,TotalFrames/GetFrame |
|
||||
| 6.7 | 实现 `ReportManager` | 2h | ✅ | 生成 + Markdown 导出 |
|
||||
| 6.8 | 3D 实体可视化 | 4h | ✅ | Drone/Cube、Cloud/Sphere、Munition/Cylinder,逐帧位置同步 |
|
||||
| 6.9 | 粒子系统参数传递 | 3h | ✅ | 云团半径缩放 + 颜色/透明度动态更新 |
|
||||
| 6.10 | 端到端联调 + 一键验证 | 6h | ✅ | `ManagerVerification` 7 模块全过,`SimulationBootstrap` 一键 Demo |
|
||||
|
||||
**额外交付物**:
|
||||
- `SimulationBootstrap.cs`:一键创建想定 → 推荐方案 → 启动仿真(Scene 直接运行)
|
||||
- `simple_simulation.unity`:预配置场景文件
|
||||
- `ManagerVerification`:Inspector 右键 `Run Full Verification` → Console 7 行 OK
|
||||
|
||||
---
|
||||
|
||||
@ -117,7 +129,7 @@ Phase 7 ✅ 打磨收尾
|
||||
|
||||
| # | 任务 | 状态 |
|
||||
|---|------|------|
|
||||
| 7.1 | 性能优化 | ⬜(非关键,实体少时自然 < 5ms) |
|
||||
| 7.1 | 性能优化 | ✅ Tick 热路径优化,Unity 满载帧率 <20→200+ FPS(详见总体架构设计第十三章) |
|
||||
| 7.2 | TTL 清理(FrameDataStore.CleanupExpired + 测试) | ✅ |
|
||||
| 7.3 | 边界测试(空部署/强风/超长航路/空基弹药) | ✅ |
|
||||
| 7.4 | 错误处理加固 | ⬜(基本校验已有) |
|
||||
@ -132,16 +144,301 @@ Phase 7 ✅ 打磨收尾
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|------|
|
||||
| 测试总数 | **128** |
|
||||
| 行覆盖率 | **95.4%** |
|
||||
| 分支覆盖率 | **80.5%** |
|
||||
| 执行时间 | ~9 秒 |
|
||||
| 项目文件 | `CounterDrone.Core.dll` (.NET Standard 2.1) |
|
||||
| 零 Unity 依赖 | ✅ 可脱离 Unity 独立运行和测试 |
|
||||
| 测试总数 | **270**(全部通过) |
|
||||
| 行覆盖率 | **95%+** |
|
||||
| 分支覆盖率 | **80%+** |
|
||||
| 执行时间 | ~15 秒 |
|
||||
| Core 程序集 | `CounterDrone.Core.dll` (.NET Standard 2.1) |
|
||||
| 共享物理工具类 | `Kinematics` / `RouteGeometry` / `CloudExpansionModel` / `DamageAssessment` |
|
||||
| 全局配置 | `data/planner_config.json`(planner 策略参数,代码零默认值) |
|
||||
| Unity 项目 | `src/Unity/`(Unity 2022.3.62) |
|
||||
| Unity Manager | 8 个 MonoBehaviour 桥接 + Bootstrap + SqliteConnectionTracker |
|
||||
| 零 Unity 依赖 | ✅ Core 可脱离 Unity 独立运行和测试 |
|
||||
|
||||
---
|
||||
|
||||
## 四、里程碑
|
||||
## 四、Phase 8:天气/物理统一与功能增强
|
||||
|
||||
> 8.0(天气/物理统一)已完成;8.1/8.2/8.3 为后续增强功能,部分待开发。
|
||||
> UI/视觉/动画属于前端同事范畴,以下仅列后端 Core 的功能。
|
||||
|
||||
### 8.0 天气与物理模型统一(✅ 已完成)
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|------|------|
|
||||
| 8.0.1 | 天气纳入扩散模型 | ✅ 修复 `GaussianPuffDispersion` 写死 Sunny 的 bug,Phase3 用真实 `env.WeatherType` 推导 Pasquill 稳定度 |
|
||||
| 8.0.2 | Planner 风偏补偿 | ✅ 抛撒点逆风预置(`cloudGen = 穿越点 − windVec×expansionTime`),云团漂移后回到航路 |
|
||||
| 8.0.3 | 去除无人机风偏叠加 | ✅ DroneEntity 不再叠加风位移(真实无人机有飞控修正),planner 与引擎速度模型一致 |
|
||||
| 8.0.4 | PathInSphere 云团参考系修正 | ✅ 毁伤判定改在云团参考系(`drone.Pos − cloud.Center`),修正移动球导致的每 tick ~2m 系统误差 |
|
||||
| 8.0.5 | ComputeEffectiveRadius 云龄 bug | ✅ 从 `ArrivalTime×2`(无人机飞行时间,概念错误)改为 `expansionTime`(云团自身膨胀时长) |
|
||||
| 8.0.6 | 物理模型统一架构 | ✅ 新增 `RouteGeometry` 静态工具类,planner 与引擎共用航路几何;planner 删除所有本地物理公式 |
|
||||
| 8.0.7 | 配置外置 | ✅ 新增 `PlannerConfig` + `planner_config.json`,策略参数(重叠系数、类型系数、弹药匹配、概率阈值)全部从配置读,代码零默认值 |
|
||||
| 8.0.8 | 云团重叠布局 | ✅ 间距 `2R×(1−重叠比例)`,默认重叠 20%,消除相切处密度空洞;offset 沿航路切向,支持任意方向/折线航路 |
|
||||
|
||||
### 8.1 仿真增强
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|------|------|
|
||||
| 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 等),导入 `ScenarioDrone` 默认值 |
|
||||
| 8.1.5 | 毁伤曲线参数校准 | ✅ RequiredExposureSeconds 替代硬编码,密度阈值统一在引擎检查 |
|
||||
| 8.1.6 | Fallback/default 清理 | ✅ 删除所有硬编码默认值和静默 fallback,参数缺失即报错 |
|
||||
|
||||
### 8.2 报告与导出
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|------|------|
|
||||
| 8.2.1 | PDF 导出 | ✅ PdfSharpCore + ReportData 结构化模型 + StandardPdfTemplate + CJK 字体嵌入 + IConfigService 运行时重载 |
|
||||
| 8.2.2 | Word 导出 | ⬜ 调研可行方案 |
|
||||
| 8.2.3 | 防御推荐方案 | ✅ `IScenarioService.GetDefenseRecommendation`,配置阶段调 planner 生成最佳抛撒参数,前端"一键应用" |
|
||||
| 8.2.4 | 配置运行时重载 | ✅ `IConfigService` 读写 defaults.json / planner_config.json,SavePlannerConfig/SaveDefaults/Reload 无需重启 |
|
||||
|
||||
### 8.3 第三方对接
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|------|------|
|
||||
| 8.3.1 | 第三方 DLL 接口规范 | ⬜ 与供应商对齐 `ICloudDispersionModel` P/Invoke 签名和调用约定 |
|
||||
| 8.3.2 | 扩散模型替换验证 | ⬜ 通过 `AlgorithmFactory.Register` 切换到第三方实现后的集成测试 |
|
||||
|
||||
---
|
||||
|
||||
## 五、Phase 9:性能优化 + 架构文档校准 ✅
|
||||
|
||||
**目标**:Tick 热路径性能优化(Unity 满载 <20→200+ FPS)+ 总体架构设计文档与实现对齐。
|
||||
|
||||
### 9.1 性能优化(✅ 已完成)
|
||||
|
||||
| # | 任务 | 状态 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 9.1.1 | DroneEntity 航路几何缓存 | ✅ | 构造时预算 `_totalArc`/`_segLen[]`/`_cumArc[]`,Update 内 O(1) 定位段,消除每帧重复几何运算 |
|
||||
| 9.1.2 | 仿真期零字符串分配 | ✅ | EntitySnapshot 强类型字段流转,JSON 推迟到 Flush;Unity 端移除 JsonDocument.Parse(最大收益项) |
|
||||
| 9.1.3 | ControlZoneEntity 顶点 2D 缓存 | ✅ | 构造时缓存 `_vertices2D`,ContainsPoint 零分配 |
|
||||
| 9.1.4 | 帧数据内存缓存+批量落库 | ✅ | List\<FrameRecord\> struct 缓冲,Flush 单事务 InsertAll |
|
||||
| 9.1.5 | 毁伤判定快速排斥 | ✅ | 浓度阈值 + 2R 距离两层裁剪 |
|
||||
|
||||
> 详见《总体架构设计》第十三章。优化前后实测:Unity 满载渲染帧率 <20 FPS(卡顿)→ 200+ FPS。
|
||||
|
||||
### 9.2 架构文档校准(✅ 已完成)
|
||||
|
||||
| # | 任务 | 状态 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 9.2.1 | 修正文档与实现不符 | ✅ | AlgorithmFactory(Func 工厂)、IDefensePlanner.Plan(4参数)、IDamageModel(RequiredExposureSeconds)、FrameDataStore(类非接口)、删除 IRecordService、DroneEntity 弧长运动、Tick 流程顺序、StateData 双轨 |
|
||||
| 9.2.2 | 新增性能设计章节 | ✅ | 总体架构设计 第十三章 |
|
||||
| 9.2.3 | 探测设备行为设计 | ✅ | 总体架构设计 第十四章(双链路 + 3D 球冠 + 5 项决策 + 数据模型清单)|
|
||||
|
||||
---
|
||||
|
||||
## 六、Phase 10:探测实时链路开发 ✅(已完成)
|
||||
|
||||
**目标**:实现仿真运行时实时探测(链路 B),产生 `TargetDetected` 事件 + 可视化数据;并将 planner 探测判定升级到 3D 球冠。
|
||||
**前置设计**:《总体架构设计》第十四章(V13),5 项行为决策已确认(见 14.6)。
|
||||
**关键约束**:
|
||||
- 实时探测是**纯只读观测层**(决策 3/4),不影响 FireSchedule、不驱动拦截;
|
||||
- **planner 与实时探测必须共用 3D 球冠判定**(`IsInCoverage`)——正确性硬要求,否则会出现"未探测却拦截"的物理错误(详见 T3)。
|
||||
|
||||
> 任务依赖关系见 10.5。建议按 T1→T2→T3→T4 顺序,T5/T6 可并行。
|
||||
|
||||
### 10.1 数据层(T1)
|
||||
|
||||
| # | 任务 | 预估 | 状态 | 说明 |
|
||||
|---|------|------|------|------|
|
||||
| T1.1 | ScenarioUnit 加 4 列 | 0.5h | ✅ | `MinElevation`/`MaxElevation`/`MinDetectAlt`/`MaxDetectAlt`,均 NULLABLE |
|
||||
| T1.2 | DetectionSource 类扩展 | 0.5h | ✅ | 同步加 4 个三维几何属性(float.MaxValue=无限制,退化 2D) |
|
||||
| T1.3 | BuildDetectionSources 读取新字段 | 0.5h | ✅ | 缺失时退化球冠(float.MaxValue),保证存量数据平滑过渡 |
|
||||
|
||||
### 10.2 算法层(T2)
|
||||
|
||||
| # | 任务 | 预估 | 状态 | 说明 |
|
||||
|---|------|------|------|------|
|
||||
| T2.1 | DetectionCalculator.IsInCoverage | 1h | ✅ | 3D 球冠判定:水平距离 + 俯仰角 ∈ [Min,Max]Elevation + 高度 ∈ [Min,Max]DetectAlt。float.MaxValue=无限制(退化 2D) |
|
||||
| T2.2 | IsInCoverage 单元测试 | 0.5h | ✅ | 7 个边界用例:球冠内/外、正顶、俯仰越界、高度越界、无限制等价 2D |
|
||||
|
||||
### 10.3 planner 3D 适配(T3)🔒 正确性硬要求
|
||||
|
||||
| # | 任务 | 预估 | 状态 | 说明 |
|
||||
|---|------|------|------|------|
|
||||
| T3.1 | EarliestDetection 改采样法 | 2h | ✅ | 从"线段-圆解析求交"改为"沿航路采样点(步长≤50m)调 IsInCoverage"。删除旧 EarliestEntryArc/IsInside |
|
||||
| T3.2 | DefensePlannerTests 回归验证 | 1h | ✅ | 30 个测试全通过,无回归 |
|
||||
|
||||
> 🔒 **T3 不可砍、不可降级为 2D**。这是正确性硬约束,不是精度优化:
|
||||
> - 仿真是按 planner 规划执行的,**planner 的探测判定 = 仿真的事实依据**。
|
||||
> - 若 planner 保留 2D 圆判定,会出现"目标高度超出探测设备真实 3D 球冠范围(探测不到),但 planner 按 2D 判定能发现并规划拦截、仿真照此摧毁"的**物理错误结局**——防空系统不可能拦截它没发现的目标。
|
||||
> - 因此 planner 必须与实时探测共用 `IsInCoverage`(详见总体架构设计 14.2.1)。
|
||||
>
|
||||
> **关于采样误差(已澄清,可放心)**:采样法 vs 解析法的弧长误差 ≤ 一个步长(50m),换算到推荐抛撒时机 ≤ ~1.5s(120km/h),远小于云团膨胀窗口(~30s)和云团重叠冗余,**不影响火力计划结果**。所以 T3 的重点是用 3D 几何保证"该探测的能探测、不该探测的探测不到",而非追求采样精度。
|
||||
|
||||
### 10.4 引擎层(T4)
|
||||
|
||||
| # | 任务 | 预估 | 状态 | 说明 |
|
||||
|---|------|------|------|------|
|
||||
| T4.1 | DetectionEntity 运行时实体 | 1h | ✅ | 持有 DetectionSource + 每无人机探测状态机(Undetected ⇄ Detected)。`UpdateState` 返回首次进入/离开信号 |
|
||||
| T4.2 | SimulationEngine 加 _detectionEntities | 0.5h | ✅ | Initialize 时从 BuildDetectionSources 构建,Tick 第 5 步遍历 |
|
||||
| T4.3 | Tick 第 5 步实时扫描 | 2h | ✅ | 决策 1(离开回退)= Undetected;决策 2(融合取最早/同刻取精度高);触发 TargetDetected(含 OnTargetDetected 事件) |
|
||||
| T4.4 | SimulationEngineTests 验证 | 1h | ✅ | 4 个测试:进入探测范围触发事件、多设备融合取最早、无探测设备无事件、离开后再次进入重新触发 |
|
||||
|
||||
### 10.5 前端 / 报告(T5,可并行)
|
||||
|
||||
| # | 任务 | 预估 | 状态 | 说明 |
|
||||
|---|------|------|------|------|
|
||||
| T5.1 | Unity 探测范围可视化 | — | ⬜ | 前端范畴:3D 球冠 wireframe / 发现标记(决策 4 仅可视化) |
|
||||
| T5.2 | 时序图/报告纳入 TargetDetected | 0.5h | ✅ | ReportGenerator 时序表增加发现事件(👁️ 目标发现);统计增加目标发现计数 |
|
||||
|
||||
### 10.6 任务依赖与顺序
|
||||
|
||||
```
|
||||
T1(数据层)→ T2(算法层 IsInCoverage)
|
||||
↓
|
||||
T3(planner 3D 适配 ⚠️耦合点)→ T4(引擎实时扫描)
|
||||
↓
|
||||
T5(前端/报告,可并行)
|
||||
```
|
||||
|
||||
**总预估**:约 11h(T1 1.5h + T2 1.5h + T3 3h + T4 4.5h + T5.2 0.5h;前端 T5.1 不计入后端)。
|
||||
|
||||
---
|
||||
|
||||
## 六、Phase 11:运动学重构 + 实体暴露 + 基础数据 CRUD ✅
|
||||
|
||||
**目标**:抛物线前向计算、空基固定阵位发射、全部实体属性公开、DataService 全 CRUD、LiveFrames 内存回放。
|
||||
|
||||
### 11.1 运动学重构
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.1.1 | ComputeParabolicRange / ParabolicApex | 正问题:给定 v₀+θ → (射程, 飞行时间) + 顶点参数 |
|
||||
| 11.1.2 | Math.Max 回退全部移除 | CalculateLaunchAngle/ParabolicShellTime/ParabolicTimeOfFlight 非法输入抛异常;GaussianPuffDispersion/CloudExpansionModel Pow 输入验证 |
|
||||
| 11.1.3 | MunitionEntity 前向到达 | launchAngle 必须由方案提供;_arrivesDescending 区分上升/下落;Math.Max 除零移除 |
|
||||
|
||||
### 11.2 空基固定阵位
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.2.1 | DefensePlanner 空基 | ComputeHorizontal 用 platform.PosY;ComputeParabolicRange 前向算时间;云团=到达位置 |
|
||||
| 11.2.2 | 死代码清理 | CommandFlyTo/FlyingToTarget/ReadyToRelease + "到达投放点" 块删除 |
|
||||
|
||||
### 11.3 实体属性暴露
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.3.1 | EntitySnapshot + 速度 | 所有实体帧快照带 VelX/Y/Z;CollectSnapshots 覆盖全部 5 类实体 |
|
||||
| 11.3.2 | CloudEntity | Pos/Radius/Density/Phase/Elapsed 直接属性 |
|
||||
| 11.3.3 | MunitionEntity | LaunchAngle/Azimuth/MuzzleVelocity/FlightDuration/Start/LaunchTime/ElapsedTime/Velocity 全部 public |
|
||||
| 11.3.4 | DroneEntity/PlatformEntity/DetectionEntity | TraveledArc/Progress; Target/FlightDistance; PosX/Y/Z |
|
||||
|
||||
### 11.4 基础数据 CRUD
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.4.1 | 类名规范化 | 7 类模板统一命名(*Spec/*Template);SQLite Table+PrimaryKey |
|
||||
| 11.4.2 | 6 个 Repository | SpecRepositories.cs |
|
||||
| 11.4.3 | IDataService + DataService | 全 CRUD,构造注入 |
|
||||
| 11.4.4 | DatabaseManager 建表+种子 | 自动建表 + InsertOrReplace 种子 |
|
||||
| 11.4.5 | Unity 接入 | ScenarioManager.DataService + SimulationRunner.DataService |
|
||||
|
||||
### 11.5 回放(LiveFrames)
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.5.1 | FrameDataStore.LiveFrames | Flush 后保留副本;BeginRecording/Discard 清除 |
|
||||
| 11.5.2 | ReplayController 双路径 | LoadReplay(scenarioId, frameStore) 优先内存,回退 SQLite |
|
||||
|
||||
### 11.6 测试启用
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 11.6.1 | Scenario_3DronesAirBased | 参数对齐,平台间隔 300m,移除 Skip |
|
||||
| 11.6.2 | Scenario_DetectionDriven | 移除 Skip |
|
||||
|
||||
---
|
||||
|
||||
## 十二、Phase 12:空基/地基统一规划 + 编队轴 + LaneDivider ✅
|
||||
|
||||
**目标**:碰撞点后统一流程,编队支持 XYZ 任意轴展开,lane 划分策略可替换。
|
||||
|
||||
### 12.1 运动学修复
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 12.1.1 | ComputeParabolicRange 重写 | 从 tanθ 二次改为垂直运动直接算时间,消除与 CalculateLaunchAngle 的不互逆 |
|
||||
| 12.1.2 | 风偏纳入角度 | cloudGen 先于 launchAngle 计算,消除风偏导致的 range≠targetDist |
|
||||
| 12.1.3 | 弹道可达验证 | 每发 PlanUnitLane 验证 ComputeParabolicRange 不抛异常 |
|
||||
|
||||
### 12.2 空基/地基统一
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 12.2.1 | 碰撞点后无分支 | cloudGen/deliveryTime/fireTime 统一公式,mv 和 launchAngle 按类型取不同值 |
|
||||
| 12.2.2 | 每发独立角度 | launchAngle 对每个目标点重新计算,不共用 InterceptCalculator 的统一值 |
|
||||
| 12.2.3 | 3机空基全灭 | 修复后三机编队全部击毁 |
|
||||
|
||||
### 12.3 编队轴 + LaneDivider
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 12.3.1 | LateralAxis/LongitudinalAxis | RoutePlan + FormationTemplate + DroneEntity 支持 0=X/1=Y/2=Z |
|
||||
| 12.3.2 | ILaneDivider 接口 | 车道划分策略抽象 |
|
||||
| 12.3.3 | DefaultLaneDivider | 宽+深双维度判断,任一超云团半径则拆分 |
|
||||
| 12.3.4 | PlanUnitLane 读轴 | 按 LateralAxis 决定 X/Y/Z 偏移方向 |
|
||||
|
||||
### 12.4 探测门控
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 12.4.1 | _anyThreatDetected | 引擎等首次探测后才执行发射 |
|
||||
| 12.4.2 | _detectionTime | 发射时间 = fe.FireTime(偏移) + 探测时刻 |
|
||||
|
||||
### 12.5 测试
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 12.5.1 | Kinematics 俯射往返 | CalculateLaunchAngle→ComputeParabolicRange 往返一致 |
|
||||
| 12.5.2 | 3机空基 | 平台 X 轴分布、无人机 Z 轴分布,交叉维度全覆盖 |
|
||||
|
||||
---
|
||||
|
||||
## 十三、Phase 13:模型重构 ✅
|
||||
|
||||
### 13.1 数据分层架构
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 13.1.1 | ScenarioDrone/ScenarioUnit 精简 | 删冗余基础字段,改为 FK 引用 |
|
||||
| 13.1.2 | DroneSpec/FireUnitSpec 独立文件 | 从 DefaultData.cs 拆出,命名空间修正 |
|
||||
| 13.1.3 | Model/Description 字段 | DroneSpec/FireUnitSpec/SensorSpec 加业务属性 |
|
||||
| 13.1.4 | ModelId 3D 引用 | 仿真实体 + EntitySnapshot +ModelId |
|
||||
|
||||
### 13.2 发射平台与探测设备分离
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 13.2.1 | LaunchPlatformSpec | 纯发射参数,无探测字段 |
|
||||
| 13.2.2 | SensorSpec 统一探测 | 发射平台自带探测 + 独立探测设备,统一 SensorySpecId 引用 |
|
||||
| 13.2.3 | BuildDetectionSources 简化 | 只查 SensorSpec |
|
||||
| 13.2.4 | FireUnitSpec 保留不用 | 旧类型保留代码不删 |
|
||||
|
||||
### 13.3 代码审查
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 13.3.1 | Spec 类拆分 | SpecRepositories → 6 个独立文件 |
|
||||
| 13.3.2 | PagedResult/EnumMetadata 独立 | 从 ScenarioConfig/IScenarioService 拆出 |
|
||||
| 13.3.3 | Vector3/DetectionSource 独立 | 从 AlgorithmTypes 拆出 |
|
||||
| 13.3.4 | 死代码清除 | _hasExceededReleaseAltitude |
|
||||
| 13.3.5 | FormationTemplate PrimaryKey | 追加缺失的 SQLite 属性 |
|
||||
|
||||
### 13.4 API 增强
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 13.4.1 | GetEnums 中英文对照 | EnumItem{Name,ChineseName,Value} |
|
||||
| 13.4.2 | DataServiceTests | 12 个,覆盖 7 类基础数据 CRUD |
|
||||
| 13.4.3 | 全模型 CRUD 补齐 | ScenarioDrone/ScenarioUnit/Route/Waypoint 等 |
|
||||
|
||||
---
|
||||
|
||||
## 七、里程碑
|
||||
|
||||
| 里程碑 | 状态 |
|
||||
|------|------|
|
||||
@ -150,12 +447,27 @@ Phase 7 ✅ 打磨收尾
|
||||
| M3 — 算法可计算 | ✅ |
|
||||
| M4 — 仿真可运行 | ✅ |
|
||||
| M5 — 报告可生成 | ✅ |
|
||||
| M6 — Unity 可演示 | ⬜ |
|
||||
| M6 — Unity 可演示 | ✅ |
|
||||
| M7 — 交付就绪 | ✅ |
|
||||
| M8 — 天气/物理模型统一 | ✅ |
|
||||
| M9 — 性能优化 + 文档校准 | ✅ |
|
||||
| M10 — 探测实时链路 | ✅ |
|
||||
| M11 — 运动学重构 + 实体暴露 + 基础数据 CRUD | ✅ |
|
||||
| M12 — 空基/地基统一规划 + 编队轴 + LaneDivider | ✅ |
|
||||
| M13 — PDF 导出 + 报告模板架构 | ✅ |
|
||||
| M14 — 防御推荐 GetDefenseRecommendation + 配置运行时重载 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 五、状态图例
|
||||
## 九、待解决问题
|
||||
|
||||
| # | 问题 | 说明 | 状态 |
|
||||
|---|------|------|------|
|
||||
| 1 | **高弹道支持** | 当前只支持低弹道。喷气式(ground-standard, 18000m 航线)需要高弹道才能命中。低弹道取上升段时间 t1,弹在 6.5s/5199m 就触发了到达判定,实际应 15.5s/12318m 在下行段命中。需要修改 MunitionEntity 的到达判定逻辑 | 🔴 待解决 |
|
||||
|
||||
---
|
||||
|
||||
## 八、状态图例
|
||||
|
||||
| 符号 | 含义 |
|
||||
|------|------|
|
||||
|
||||
35
docs/requirements/changes/2026-06-14-天气纳入规划与物理模型统一.md
Normal file
35
docs/requirements/changes/2026-06-14-天气纳入规划与物理模型统一.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 天气纳入 Planner 规划与物理模型统一
|
||||
|
||||
- **日期**:2026-06-14
|
||||
- **提出人**:tian
|
||||
- **关联需求**:技术要求终版 2.2.3(云团扩散效果受气象条件影响)
|
||||
- **优先级**:高
|
||||
|
||||
## 变更描述
|
||||
|
||||
将天气/风要素纳入 DefensePlanner 规划,并在此过程中发现并修复 planner 与仿真引擎物理模型分裂的根本问题。
|
||||
|
||||
### 起因
|
||||
原 planner 未考虑天气对云团漂移的影响,有风场景下规划方案与仿真结果不一致。深入排查后发现根因不止于风偏补偿,而是 planner 大量本地重写了运动学/几何公式(直线距离、写死 X 轴 offset、`2R` 相切假设等),与引擎的实际行为(折线航路、云团移动、PathInSphere 几何)系统性偏离。
|
||||
|
||||
### 变更内容
|
||||
1. **天气纳入扩散模型**:修复 `GaussianPuffDispersion` 写死 `WeatherType.Sunny` 的 bug,Phase3 高斯扩散改用真实环境天气推导 Pasquill 稳定度
|
||||
2. **Planner 风偏补偿**:抛撒点逆风预置,云团生成后漂移 expansionTime 秒回到无人机穿越点
|
||||
3. **PathInSphere 云团参考系修正**:毁伤判定改在云团参考系计算,修正云团在 tick 内移动导致的每 tick ~2m 系统误差(关键根因)
|
||||
4. **物理模型统一架构**:新增 `RouteGeometry` 静态工具类,planner 与引擎共用航路几何;planner 删除所有本地物理公式
|
||||
5. **配置外置**:新增 `PlannerConfig` + `planner_config.json`,策略参数(重叠系数、类型系数、弹药匹配、概率阈值)全部从配置读,代码零默认值
|
||||
6. **云团重叠布局**:间距 `2R×(1−重叠比例)`,默认重叠 20%,offset 沿航路切向,支持任意方向/折线航路
|
||||
|
||||
## 影响范围
|
||||
|
||||
- [x] 接口变更:`DefaultDefensePlanner` 构造函数必传 `PlannerConfig`;`CloudExpansionModel.RoundsNeeded` 签名变更(`effectiveRadius` → `spacing`)
|
||||
- [ ] 数据库变更
|
||||
- [ ] UI 变更
|
||||
- [x] 文档变更:架构设计 6.5 节、CHANGELOG、VERSION、实施计划
|
||||
|
||||
## 验收
|
||||
|
||||
- 全量测试 167 → **191**(+24),41s 通过
|
||||
- 活塞+西风 5m/s、空基+东风 5m/s 有风场景击毁成功(非边界)
|
||||
- Z 向航路云团沿航路分布(不再写死 X 轴)
|
||||
- L 形折线多 waypoint 运动正确
|
||||
34
docs/requirements/changes/2026-06-15-探测驱动规划.md
Normal file
34
docs/requirements/changes/2026-06-15-探测驱动规划.md
Normal file
@ -0,0 +1,34 @@
|
||||
# 探测驱动的规划(8.1.1)
|
||||
|
||||
- **日期**:2026-06-15
|
||||
- **提出人**:tian
|
||||
- **关联需求**:V1.0 功能需求(探测设备)、技术要求终版
|
||||
- **优先级**:高
|
||||
|
||||
## 变更描述
|
||||
|
||||
planner 不再有上帝视角。仿真开始时,planner 假设威胁从"探测边界"(统一信息网络的最早发现点)进入,而非航路真实起点。探测精度差 → 抛撒散布范围扩大。
|
||||
|
||||
### 设计原则
|
||||
planner 是参谋,只能基于侦查信息制定方案。假设我方有统一信息网络,可同步威胁信息。独立探测设备与火力单元自带探测并存。
|
||||
|
||||
### 变更内容
|
||||
1. **新增 DetectionCalculator**:探测能力评估的唯一实现。光电受 Visibility 衰减,雷达/红外不受影响;统一信息网络找最早探测点
|
||||
2. **ScenarioUnit 扩展**:删单一 DetectionRadius,加 RadarRange/EORange/IRange/DetectionAccuracy
|
||||
3. **FireUnit 探测字段激活**:BuildFireUnits 赋值(原为死代码)
|
||||
4. **planner 接口扩展**:Plan 加第 4 参数 detectionSources;到达时间基于探测边界
|
||||
5. **SimulationEngine 构建探测源**:独立探测 + 火力单元自带探测统一传入 planner
|
||||
|
||||
## 影响范围
|
||||
|
||||
- [x] 接口变更:IDefensePlanner.Plan 加参数;ScenarioUnit 字段变更(删 DetectionRadius)
|
||||
- [ ] 数据库变更:新字段为 nullable,SQLite 自动处理
|
||||
- [ ] UI 变更
|
||||
- [x] 文档变更:实施计划 8.1.1、CHANGELOG、VERSION
|
||||
|
||||
## 验收
|
||||
|
||||
- 全量测试 193 → **208**(+15),64s 通过
|
||||
- DetectionCalculator 13 项单测(天气衰减/最早探测点/散布半径)
|
||||
- 探测驱动规划 2 项测试(planner 基于探测边界、集成场景不破坏)
|
||||
- 范围外:实时探测→火控链路(未来增强)
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
类型、数量、挂载气溶胶的类型、数量(一发炮弹打出的云团大概覆盖体积)
|
||||
|
||||
根据算法自动推荐晕图案的抛洒位置、抛洒时机,
|
||||
根据算法自动推荐云团的抛洒位置、抛洒时机,
|
||||
最佳值和最危险值
|
||||
]:支持自定义云团的抛撒位置、抛撒时机、风速风向参数。
|
||||
(2)目标配置:预置典型目标库,包括多种无人机。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -14,9 +14,27 @@ Copy-Item "$projectRoot\unity_plugins\*.dll" "$unityProject\Assets\Plugins\Count
|
||||
Remove-Item "$unityProject\Library\ScriptAssemblies" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Compiling Unity scripts..."
|
||||
|
||||
# 3. 编译(CompileErrorDetector 捕获错误并 Exit(-1))
|
||||
& $unityEditor -batchmode -projectPath $unityProject -logFile - 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "UNITY BUILD FAILED (exit code $LASTEXITCODE)"; exit 1 }
|
||||
# 3. 编译(-quit 确保编译完自动退出;超时 300s 兜底)
|
||||
Write-Host "Compiling Unity scripts (timeout 300s)..."
|
||||
$logFile = "$projectRoot\unity_build.log"
|
||||
$proc = Start-Process -FilePath $unityEditor `
|
||||
-ArgumentList "-batchmode","-quit","-projectPath","`"$unityProject`"","-logFile","`"$logFile`"" `
|
||||
-PassThru -NoNewWindow
|
||||
$proc | Wait-Process -Timeout 300 -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $proc.HasExited) {
|
||||
Write-Host "Unity build TIMEOUT (300s), killing process..."
|
||||
$proc | Stop-Process -Force
|
||||
exit 1
|
||||
}
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Host "UNITY BUILD FAILED (exit code $($proc.ExitCode))"
|
||||
if (Test-Path $logFile) {
|
||||
Write-Host "--- Last 30 lines of log ---"
|
||||
Get-Content $logFile -Tail 30
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "ALL CHECKS PASSED"
|
||||
exit 0
|
||||
|
||||
@ -3,29 +3,35 @@ using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>吸入式爆炸 — 指数累积型:暴露时间越长伤害越高</summary>
|
||||
/// <summary>吸入式爆炸 — 指数累积型</summary>
|
||||
public class ActiveFuelDamageModel : IDamageModel
|
||||
{
|
||||
private const float EffectiveThreshold = 0.001f; // 有效浓度阈值
|
||||
private const float BaseRate = 0.02f; // 基础速率
|
||||
private const float ExpFactor = 0.3f; // 指数增长因子
|
||||
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;
|
||||
if (cloudDensity < EffectiveThreshold) return 0f;
|
||||
|
||||
// 伤害 = 基础速率 × e^(暴露时间 × 因子) × deltaTime
|
||||
// 在云团中待得越久伤害指数增长
|
||||
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(DroneType droneType, PowerType powerType, AerosolType aerosolType)
|
||||
{
|
||||
if (aerosolType != AerosolType.ActiveFuel) return float.MaxValue;
|
||||
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)
|
||||
float k = ExpFactor;
|
||||
float bs = BaseRate * sensitivity;
|
||||
float target = 1f + k / bs;
|
||||
return (float)Math.Log(target) / k;
|
||||
}
|
||||
|
||||
public DamageStage GetDamageStage(float accumulatedDamage)
|
||||
{
|
||||
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
|
||||
|
||||
@ -6,18 +6,15 @@ namespace CounterDrone.Core.Algorithms
|
||||
/// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary>
|
||||
public class ActiveMaterialDamageModel : IDamageModel
|
||||
{
|
||||
private const float TriggerThreshold = 0.0002f; // 触发浓度阈值
|
||||
private const float BurstDamage = 0.85f; // 一次爆发伤害
|
||||
private const float ResidualRate = 0.05f; // 后续余伤速率
|
||||
private const float BurstDamage = 0.85f;
|
||||
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;
|
||||
if (cloudDensity < TriggerThreshold) return 0f;
|
||||
|
||||
// 喷气发动机高温触发爆燃,更敏感
|
||||
if (!_triggered)
|
||||
{
|
||||
_triggered = true;
|
||||
@ -28,10 +25,19 @@ namespace CounterDrone.Core.Algorithms
|
||||
return ResidualRate * deltaTime;
|
||||
}
|
||||
|
||||
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;
|
||||
float burst = BurstDamage * sensitivity;
|
||||
if (burst >= 1.0f) return 0f;
|
||||
return (1.0f - burst) / ResidualRate;
|
||||
}
|
||||
|
||||
public DamageStage GetDamageStage(float accumulatedDamage)
|
||||
{
|
||||
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
|
||||
if (accumulatedDamage >= 0.5f) return DamageStage.AttitudeLoss; // 爆燃后更快进入失控
|
||||
if (accumulatedDamage >= 0.5f) return DamageStage.AttitudeLoss;
|
||||
if (accumulatedDamage >= 0.2f) return DamageStage.EngineAnomaly;
|
||||
return DamageStage.Normal;
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
[typeof(ICloudDispersionModel)] = () => new GaussianPuffDispersion(),
|
||||
[typeof(IDamageModel)] = () => new DamageModelRouter(),
|
||||
[typeof(IDefenseAdvisor)] = () => new DefaultDefenseAdvisor(null),
|
||||
};
|
||||
|
||||
public static void Register<TInterface>(Func<object> factory)
|
||||
|
||||
@ -1,123 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>威胁画像</summary>
|
||||
public class ThreatProfile
|
||||
{
|
||||
public CombatScene Environment { get; set; } = new();
|
||||
public List<TargetConfig> Targets { get; set; } = new();
|
||||
public RoutePlan Route { get; set; } = new();
|
||||
public List<Waypoint> Waypoints { get; set; } = new();
|
||||
public List<ControlZone> ControlZones { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>防御推荐方案</summary>
|
||||
public class DefenseRecommendation
|
||||
{
|
||||
public DefenseSolution Best { get; set; } = new();
|
||||
public DefenseSolution Critical { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>单套防御方案</summary>
|
||||
public class DefenseSolution
|
||||
{
|
||||
public AerosolType RecommendedAerosolType { get; set; }
|
||||
public string AerosolRationale { get; set; } = string.Empty;
|
||||
public CloudDispersal RecommendedCloud { get; set; } = new();
|
||||
|
||||
public List<RecommendedPlatform> Platforms { get; set; } = new();
|
||||
public List<RecommendedDetection> Detections { get; set; } = new();
|
||||
|
||||
public float InterceptProbability { get; set; }
|
||||
public string SummaryRationale { get; set; } = string.Empty;
|
||||
|
||||
public List<FireEvent> FireSchedule { get; set; } = new();
|
||||
|
||||
/// <summary>此方案占用的平台起始索引(多编队合并用)</summary>
|
||||
public int PlatformOffset { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>多编队推荐结果</summary>
|
||||
public class MultiGroupRecommendation
|
||||
{
|
||||
/// <summary>每组的独立方案</summary>
|
||||
public List<DefenseSolution> GroupSolutions { get; set; } = new();
|
||||
/// <summary>合并后的总发射计划</summary>
|
||||
public List<FireEvent> MergedFireSchedule { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>火力单元(装备编组)</summary>
|
||||
/// <summary>火力单元(完整的武器系统)</summary>
|
||||
public class FireUnit
|
||||
{
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public AerosolType LoadedAmmo { get; set; }
|
||||
public int PlatformCount { get; set; }
|
||||
public float PositionX, PositionY, PositionZ;
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public PlatformType Type { get; set; }
|
||||
public Vector3 Position { get; set; }
|
||||
|
||||
// ── 发射装置 ──
|
||||
/// <summary>火炮/发射器数量</summary>
|
||||
public int GunCount { get; set; } = 1;
|
||||
/// <summary>每门炮的火力通道数(同时可装填的弹药数量)</summary>
|
||||
public int ChannelsPerGun { get; set; } = 1;
|
||||
/// <summary>总火力通道数 = GunCount × ChannelsPerGun</summary>
|
||||
public int TotalChannels => GunCount * ChannelsPerGun;
|
||||
/// <summary>同一通道连续发射的最小间隔(秒)</summary>
|
||||
public float ChannelInterval { get; set; } = 1f;
|
||||
|
||||
// ── 弹药 ──
|
||||
/// <summary>总载弹量</summary>
|
||||
public int TotalMunitions { get; set; }
|
||||
/// <summary>可装填的弹药类型</summary>
|
||||
public List<AerosolType> AmmoTypes { get; set; } = new();
|
||||
/// <summary>单通道射击后冷却时间(秒)</summary>
|
||||
public float Cooldown { get; set; } = 5f;
|
||||
public float AmmoChangeTime { get; set; } = 300f;
|
||||
// 弹药库:可以装填的弹药类型
|
||||
public List<AerosolType> AvailableAmmo { get; set; } = new();
|
||||
/// <summary>更换弹种时间(秒)</summary>
|
||||
public float AmmoChangeTime { get; set; } = 30f;
|
||||
|
||||
// ── 搜索跟踪 ──
|
||||
/// <summary>雷达探测距离(m),0 表示无此设备</summary>
|
||||
public float RadarRange { get; set; }
|
||||
/// <summary>光电探测距离(m)</summary>
|
||||
public float EORange { get; set; }
|
||||
/// <summary>红外探测距离(m)</summary>
|
||||
public float IRRange { get; set; }
|
||||
|
||||
// ── 空基 ──
|
||||
public float CruiseSpeed { get; set; }
|
||||
public float ReleaseAltitude { get; set; }
|
||||
|
||||
// ── 地基 ──
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
}
|
||||
|
||||
/// <summary>无人机编队(多编队推荐输入)</summary>
|
||||
public class DroneGroup
|
||||
/// <summary>无人机批次(规划器输入)</summary>
|
||||
public class DroneWave
|
||||
{
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
public TargetConfig Target { get; set; } = new();
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
public DroneSpec Profile { get; set; } = new();
|
||||
public int Quantity { get; set; } = 1;
|
||||
public RoutePlan Route { get; set; } = new();
|
||||
public List<Waypoint> Waypoints { get; set; } = new();
|
||||
|
||||
/// <summary>预计到达航路中点的时间(秒)</summary>
|
||||
/// <summary>到达防御区域中点的时间(秒),由规划器预计算</summary>
|
||||
public float ArrivalTime { get; set; }
|
||||
/// <summary>威胁指数(速度系数 × 类型系数)</summary>
|
||||
public float ThreatIndex { get; set; }
|
||||
/// <summary>综合优先级 = 威胁指数 / (到达时间 + 1)</summary>
|
||||
public float Priority => ArrivalTime > -1 ? ThreatIndex / (ArrivalTime + 1f) : ThreatIndex;
|
||||
|
||||
/// <summary>最早探测弧长(米)。威胁航路进入探测范围的弧长位置。
|
||||
/// 0 = 起点即可探测(或无探测设备,上帝视角)。float.MaxValue = 探测不到。</summary>
|
||||
public float DetectArc { get; set; }
|
||||
/// <summary>探测精度(位置误差 m)。影响 planner 抛撒散布范围。</summary>
|
||||
public float DetectAccuracy { get; set; }
|
||||
|
||||
/// <summary>预计到达航路中点的时间(秒)。基于探测边界,而非航路起点。</summary>
|
||||
public float GetArrivalTime()
|
||||
{
|
||||
if (Waypoints.Count < 2) return 0;
|
||||
var start = new Vector3((float)Waypoints[0].PosX, (float)Waypoints[0].PosY, (float)Waypoints[0].PosZ);
|
||||
var end = new Vector3((float)Waypoints[^1].PosX, (float)Waypoints[^1].PosY, (float)Waypoints[^1].PosZ);
|
||||
var speed = (float)Target.TypicalSpeed / 3.6f;
|
||||
return start.DistanceTo(end) / speed / 2f;
|
||||
var s = Waypoints[0];
|
||||
var e = Waypoints[^1];
|
||||
float midX = ((float)s.PosX + (float)e.PosX) / 2f;
|
||||
float midZ = ((float)s.PosZ + (float)e.PosZ) / 2f;
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(
|
||||
Waypoints, midX, midZ);
|
||||
float travelArc = midArc - DetectArc;
|
||||
if (travelArc <= 0) return 0;
|
||||
float speed = (float)Waypoints[0].Speed;
|
||||
if (speed <= 0) throw new InvalidOperationException($"威胁 {WaveId}: 航路点速度必须 > 0");
|
||||
return travelArc / (speed / 3.6f);
|
||||
}
|
||||
}
|
||||
|
||||
public class RecommendedPlatform
|
||||
{
|
||||
public PlatformType Type { get; set; }
|
||||
public Vector3 Position { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public int MunitionCount { get; set; }
|
||||
public float CoverageVolume { get; set; }
|
||||
public float Cooldown { get; set; } = 5f;
|
||||
public float MuzzleVelocity { get; set; } = 800f;
|
||||
}
|
||||
|
||||
public class RecommendedDetection
|
||||
{
|
||||
public Vector3 Position { get; set; }
|
||||
public float DetectionRadius { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>三维向量(纯 C#,不依赖 UnityEngine)</summary>
|
||||
public struct Vector3
|
||||
{
|
||||
public float X, Y, Z;
|
||||
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
|
||||
|
||||
public static Vector3 operator +(Vector3 a, Vector3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
|
||||
public static Vector3 operator -(Vector3 a, Vector3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
|
||||
public static Vector3 operator *(Vector3 a, float s) => new(a.X * s, a.Y * s, a.Z * s);
|
||||
public float Length => (float)System.Math.Sqrt(X * X + Y * Y + Z * Z);
|
||||
public float DistanceTo(Vector3 other) => (this - other).Length;
|
||||
}
|
||||
|
||||
/// <summary>粒子渲染参数</summary>
|
||||
public class ParticleParams
|
||||
{
|
||||
public float EmitRate { get; set; } = 100;
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
public string ColorHex { get; set; } = "#FFFFFF";
|
||||
public float SizeMultiplier { get; set; } = 1.0f;
|
||||
}
|
||||
|
||||
@ -128,5 +103,51 @@ namespace CounterDrone.Core.Algorithms
|
||||
public int PlatformIndex;
|
||||
public float TargetX, TargetY, TargetZ;
|
||||
public float MuzzleVelocity;
|
||||
public float LaunchAngle;
|
||||
public float FlightDuration;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// DefensePlanner 类型
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// <summary>拦截候选:一个火力单元对一个威胁的可行性评估</summary>
|
||||
public class InterceptCandidate
|
||||
{
|
||||
public FireUnit Unit { get; set; } = null!;
|
||||
public AerosolType AmmoType { get; set; }
|
||||
public float EarliestInterceptTime { get; set; }
|
||||
public float KillProbability { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>单元分配:一个火力单元被分配给一个威胁的配置</summary>
|
||||
public class UnitAssignment
|
||||
{
|
||||
public string FireUnitId { get; set; } = string.Empty;
|
||||
public string DroneWaveId { get; set; } = string.Empty;
|
||||
public AerosolType AmmoType { get; set; }
|
||||
public int RoundsFired { get; set; }
|
||||
public float FirstFireTime { get; set; }
|
||||
public List<FireEvent> FireEvents { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>规划方案</summary>
|
||||
public class DefensePlan
|
||||
{
|
||||
public List<UnitAssignment> Assignments { get; set; } = new();
|
||||
public List<FireEvent> MergedSchedule { get; set; } = new();
|
||||
public float OverallProbability { get; set; }
|
||||
public int ThreatsEngaged { get; set; }
|
||||
public int ThreatsUnengaged { get; set; }
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
internal string? RejectReason;
|
||||
}
|
||||
|
||||
/// <summary>规划器输出</summary>
|
||||
public class PlannerResult
|
||||
{
|
||||
public DefensePlan Best { get; set; } = new();
|
||||
public DefensePlan Critical { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
85
src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs
Normal file
85
src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>云团膨胀模型——根据弹药和环境参数计算各阶段半径、密度、膨胀时间</summary>
|
||||
public class CloudExpansionModel
|
||||
{
|
||||
private readonly AmmunitionSpec _ammo;
|
||||
private readonly CombatScene _env;
|
||||
private readonly float _initialRadius;
|
||||
|
||||
public CloudExpansionModel(AmmunitionSpec ammo, CombatScene env)
|
||||
{
|
||||
_ammo = ammo;
|
||||
_env = env;
|
||||
|
||||
var w = (float)ammo.BurstChargeKg;
|
||||
if (w <= 0)
|
||||
throw new ArgumentException($"BurstChargeKg 必须 > 0,实际: {w}", nameof(ammo));
|
||||
_initialRadius = 3.3f * (float)Math.Pow(w, 0.32);
|
||||
}
|
||||
|
||||
/// <summary>Phase 1 爆轰初始半径 (m)</summary>
|
||||
public float InitialRadius => _initialRadius;
|
||||
/// <summary>Phase 2 结束时的有效半径</summary>
|
||||
public float TurbulentRadius => RadiusAt(Phase2Duration);
|
||||
|
||||
/// <summary>湍流膨胀系数 k</summary>
|
||||
public float TurbulentExpansionK => (float)_ammo.TurbulentExpansionK;
|
||||
|
||||
/// <summary>Phase 2 持续时间(s),从 AmmunitionSpec 读取</summary>
|
||||
public float Phase2Duration => (float)_ammo.Phase2Duration;
|
||||
|
||||
/// <summary>指定时刻湍流膨胀半径 R(t) = R₀ + k√t</summary>
|
||||
public float RadiusAt(float elapsedSeconds)
|
||||
{
|
||||
if (elapsedSeconds <= 0) return _initialRadius;
|
||||
float p2 = Phase2Duration;
|
||||
bool inPhase3 = elapsedSeconds > p2;
|
||||
if (!inPhase3)
|
||||
return _initialRadius + TurbulentExpansionK * (float)Math.Sqrt(elapsedSeconds);
|
||||
|
||||
// Phase 3: 高斯扩散
|
||||
float windSpeed = (float)_env.WindSpeed;
|
||||
float x = windSpeed * (elapsedSeconds - p2);
|
||||
if (x <= 0) return _initialRadius + TurbulentExpansionK * (float)Math.Sqrt(p2);
|
||||
var cls = Kinematics.GetStabilityClass((WeatherType)_env.WeatherType, windSpeed);
|
||||
float sY = Kinematics.SigmaY(cls, x);
|
||||
float sZ = Kinematics.SigmaZ(cls, x);
|
||||
float peakC = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ);
|
||||
float effConc = (float)_ammo.EffectiveConcentration;
|
||||
float rPhase2 = _initialRadius + TurbulentExpansionK * (float)Math.Sqrt(p2);
|
||||
if (peakC <= effConc) return rPhase2;
|
||||
float sigma = (sY + sZ) / 2f;
|
||||
return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / effConc));
|
||||
}
|
||||
|
||||
/// <summary>指定时刻中心浓度</summary>
|
||||
public float DensityAt(float elapsedSeconds)
|
||||
{
|
||||
float r = RadiusAt(elapsedSeconds);
|
||||
float volume = (4f / 3f) * (float)Math.PI * r * r * r;
|
||||
if (volume <= 0.001f) return (float)_ammo.CoreDensity;
|
||||
return (float)_ammo.SourceStrength / volume;
|
||||
}
|
||||
|
||||
/// <summary>覆盖指定距离需要的云团数量</summary>
|
||||
public int RoundsNeeded(float requiredCoverage, float? spacing = null)
|
||||
{
|
||||
float s = spacing ?? 2f * TurbulentRadius;
|
||||
if (s <= 0) return 1;
|
||||
int n = (int)Math.Ceiling(requiredCoverage / s);
|
||||
return n < 1 ? 1 : n;
|
||||
}
|
||||
|
||||
/// <summary>达到指定半径所需时间 (s)</summary>
|
||||
public float TimeToReach(float radius)
|
||||
{
|
||||
if (radius <= _initialRadius) return 0;
|
||||
float k = TurbulentExpansionK;
|
||||
return (radius - _initialRadius) * (radius - _initialRadius) / (k * k);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/CounterDrone.Core/Algorithms/DamageAssessment.cs
Normal file
41
src/CounterDrone.Core/Algorithms/DamageAssessment.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>毁伤评估——路径积分计算无人机穿过云团的路径长度(米),用于换算暴露时间</summary>
|
||||
public static class DamageAssessment
|
||||
{
|
||||
/// <summary>线段 (p1→p2) 在球体内的长度,换算为路径长度 (m)</summary>
|
||||
/// <returns>球体内的路径长度 (m),0 表示未进入</returns>
|
||||
public static float PathInSphere(
|
||||
float p1x, float p1y, float p1z,
|
||||
float p2x, float p2y, float p2z,
|
||||
float cx, float cy, float cz, float radius)
|
||||
{
|
||||
float dx = p2x - p1x, dy = p2y - p1y, dz = p2z - p1z;
|
||||
float segLen = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (segLen < 0.0001f) return 0f;
|
||||
|
||||
float fx = p1x - cx, fy = p1y - cy, fz = p1z - cz;
|
||||
|
||||
// |p1 + t*d - c|² = r² → a*t² + 2b*t + c = 0
|
||||
float a = dx * dx + dy * dy + dz * dz;
|
||||
float b = dx * fx + dy * fy + dz * fz;
|
||||
float c = fx * fx + fy * fy + fz * fz - radius * radius;
|
||||
|
||||
float disc = b * b - a * c;
|
||||
if (disc <= 0) return 0f;
|
||||
|
||||
float sqrtDisc = (float)Math.Sqrt(disc);
|
||||
float t1 = (-b - sqrtDisc) / a;
|
||||
float t2 = (-b + sqrtDisc) / a;
|
||||
|
||||
t1 = Math.Max(0f, t1);
|
||||
t2 = Math.Min(1f, t2);
|
||||
if (t1 >= t2) return 0f;
|
||||
|
||||
return (t2 - t1) * segLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,6 +21,17 @@ namespace CounterDrone.Core.Algorithms
|
||||
};
|
||||
}
|
||||
|
||||
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
|
||||
{
|
||||
return aerosolType switch
|
||||
{
|
||||
AerosolType.InertGas => _inertGas.RequiredExposureSeconds(droneType, powerType, aerosolType),
|
||||
AerosolType.ActiveMaterial => _activeMaterial.RequiredExposureSeconds(droneType, powerType, aerosolType),
|
||||
AerosolType.ActiveFuel => _activeFuel.RequiredExposureSeconds(droneType, powerType, aerosolType),
|
||||
_ => float.MaxValue,
|
||||
};
|
||||
}
|
||||
|
||||
public DamageStage GetDamageStage(float accumulatedDamage)
|
||||
{
|
||||
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>默认弹药规格 — 基于发烟罐/烟幕弹工程数据</summary>
|
||||
/// <remarks>
|
||||
/// 参考:10kg烟幕剂,抛射药/烟幕剂质量比 5%-20%,TNT当量 1.5-2kg(典型值)
|
||||
/// 初始半径 R₀ = 3.3 × W^0.32 ≈ 3.8~4.1m
|
||||
/// </remarks>
|
||||
public static class DefaultAmmunition
|
||||
{
|
||||
public static List<AmmunitionSpec> GetAll()
|
||||
{
|
||||
return new List<AmmunitionSpec>
|
||||
{
|
||||
new AmmunitionSpec
|
||||
{
|
||||
Id = "default-inert", AerosolType = (int)AerosolType.InertGas,
|
||||
Name = "惰性气体弹(发烟罐型)",
|
||||
InitialRadius = 3.8,
|
||||
CoreDensity = 1.5, EdgeDensity = 0.1,
|
||||
InitialTemperature = 1800, BuoyancyFactor = 0.3,
|
||||
EffectiveConcentration = 0.0001,
|
||||
MaxRadius = 100.0, MaxDuration = 120.0,
|
||||
SourceStrength = 10.0,
|
||||
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
|
||||
},
|
||||
new AmmunitionSpec
|
||||
{
|
||||
Id = "default-active", AerosolType = (int)AerosolType.ActiveMaterial,
|
||||
Name = "活性材料弹(爆炸分散型)",
|
||||
InitialRadius = 5.0,
|
||||
CoreDensity = 2.0, EdgeDensity = 0.2,
|
||||
InitialTemperature = 2400, BuoyancyFactor = 0.6,
|
||||
EffectiveConcentration = 0.0002,
|
||||
MaxRadius = 80.0, MaxDuration = 90.0,
|
||||
SourceStrength = 12.0,
|
||||
BurstChargeKg = 4.0, TurbulentExpansionK = 4.0,
|
||||
},
|
||||
new AmmunitionSpec
|
||||
{
|
||||
Id = "default-fuel", AerosolType = (int)AerosolType.ActiveFuel,
|
||||
Name = "活性燃料弹(抛射分散型)",
|
||||
InitialRadius = 3.8,
|
||||
CoreDensity = 1.8, EdgeDensity = 0.15,
|
||||
InitialTemperature = 1900, BuoyancyFactor = 0.4,
|
||||
EffectiveConcentration = 0.0001,
|
||||
MaxRadius = 90.0, MaxDuration = 100.0,
|
||||
SourceStrength = 10.0,
|
||||
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static AmmunitionSpec GetByType(AerosolType type)
|
||||
=> GetAll().Find(a => a.AerosolType == (int)type) ?? GetAll()[0];
|
||||
}
|
||||
}
|
||||
@ -1,280 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
public class DefaultDefenseAdvisor : IDefenseAdvisor
|
||||
{
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
|
||||
public DefaultDefenseAdvisor(List<AmmunitionSpec> ammoCatalog)
|
||||
{
|
||||
_ammoCatalog = ammoCatalog ?? new List<AmmunitionSpec>();
|
||||
}
|
||||
|
||||
private static readonly Dictionary<PowerType, AerosolType> MatchTable = new()
|
||||
{
|
||||
{ PowerType.Electric, AerosolType.InertGas },
|
||||
{ PowerType.Piston, AerosolType.InertGas },
|
||||
{ PowerType.Jet, AerosolType.ActiveMaterial },
|
||||
};
|
||||
|
||||
public DefenseRecommendation Recommend(ThreatProfile threat)
|
||||
{
|
||||
var result = new DefenseRecommendation
|
||||
{
|
||||
Best = new DefenseSolution(),
|
||||
Critical = new DefenseSolution(),
|
||||
};
|
||||
|
||||
// ===== 输入校验 =====
|
||||
if (threat.Targets.Count == 0)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:未配置威胁目标";
|
||||
return result;
|
||||
}
|
||||
if (threat.Waypoints.Count < 2)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:需要至少两个航路点";
|
||||
return result;
|
||||
}
|
||||
|
||||
var target = threat.Targets[0];
|
||||
if (target.TypicalSpeed <= 0)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:目标速度为 0";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ===== Step A:气溶胶选型 =====
|
||||
var powerType = (PowerType)target.PowerType;
|
||||
var aerosolType = MatchTable.TryGetValue(powerType, out var match)
|
||||
? match : AerosolType.InertGas;
|
||||
|
||||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)aerosolType);
|
||||
if (ammo == null)
|
||||
{
|
||||
result.Best.SummaryRationale = $"失败:弹药库中未找到 {aerosolType} 类型的弹药规格";
|
||||
return result;
|
||||
}
|
||||
|
||||
var rationale = powerType switch
|
||||
{
|
||||
PowerType.Electric or PowerType.Piston =>
|
||||
$"{powerType}发动机依赖氧气,推荐惰性气体窒息方案",
|
||||
PowerType.Jet =>
|
||||
$"{powerType}发动机高温表面可触发活性材料爆燃反应",
|
||||
_ => "默认推荐惰性气体方案",
|
||||
};
|
||||
|
||||
// ===== Step B:时空交汇优化 =====
|
||||
var start = ToV3(threat.Waypoints[0]);
|
||||
var end = ToV3(threat.Waypoints[^1]);
|
||||
var mid = new Vector3((start.X + end.X) / 2f, (start.Y + end.Y) / 2f, (start.Z + end.Z) / 2f);
|
||||
var routeLength = start.DistanceTo(end);
|
||||
var avgSpeed = (float)target.TypicalSpeed / 3.6f;
|
||||
var totalFlightTime = routeLength / avgSpeed;
|
||||
|
||||
if (routeLength < 100)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:航路太短(<100m)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 弹药参数:Phase 1 爆轰 + Phase 2 膨胀后的有效半径
|
||||
var r0 = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)ammo.BurstChargeKg), 0.32);
|
||||
var k = (float)ammo.TurbulentExpansionK;
|
||||
var maxDur = (float)ammo.MaxDuration;
|
||||
var halfTime = totalFlightTime / 2f;
|
||||
var windSpeed = (float)threat.Environment.WindSpeed;
|
||||
var weather = (WeatherType)threat.Environment.WeatherType;
|
||||
var effectiveR = ComputeEffectiveRadius(r0, k, halfTime, windSpeed, weather, ammo);
|
||||
|
||||
var crossTime = (2f * effectiveR) / avgSpeed;
|
||||
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||||
var spacing = effectiveR * 1.5f;
|
||||
|
||||
var requiredCoverage = neededExposure * avgSpeed;
|
||||
var roundsNeeded = Math.Max(1,
|
||||
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
|
||||
var actualCoverage = spacing * (roundsNeeded - 1) + 2f * effectiveR;
|
||||
var actualExposure = actualCoverage / avgSpeed;
|
||||
var prob = Math.Min(0.95f, actualExposure / neededExposure);
|
||||
var expansionTime = (float)Math.Pow((effectiveR - r0) / k, 2);
|
||||
var recommendedTiming = halfTime - expansionTime;
|
||||
|
||||
// 生成发射计划
|
||||
var fireSchedule = new List<FireEvent>();
|
||||
for (int i = 0; i < roundsNeeded; i++)
|
||||
{
|
||||
var offset = (i - (roundsNeeded - 1) / 2f) * spacing;
|
||||
fireSchedule.Add(new FireEvent
|
||||
{
|
||||
FireTime = recommendedTiming,
|
||||
PlatformIndex = i % roundsNeeded,
|
||||
TargetX = mid.X + offset,
|
||||
TargetY = mid.Y,
|
||||
TargetZ = mid.Z,
|
||||
MuzzleVelocity = 800f,
|
||||
});
|
||||
}
|
||||
|
||||
var bestCloud = new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)aerosolType,
|
||||
PositionX = mid.X, PositionY = mid.Y, PositionZ = mid.Z,
|
||||
DisperseHeight = (float)target.TypicalAltitude,
|
||||
TriggerMode = (int)TriggerMode.Time,
|
||||
Duration = (int)maxDur,
|
||||
InitialScale = ammo.InitialVolume,
|
||||
ReleaseMode = (int)ReleaseMode.Single,
|
||||
Source = "Algorithm",
|
||||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||||
RecommendedTiming = recommendedTiming,
|
||||
SalvoRounds = roundsNeeded,
|
||||
SalvoSpacing = spacing,
|
||||
EstimatedProbability = prob,
|
||||
};
|
||||
|
||||
// 平台部署:假设齐射(同时发射),每门炮打一发后冷却 5s
|
||||
// 需要 roundsNeeded 门炮同时发射
|
||||
var gunCount = roundsNeeded;
|
||||
var platforms = new List<RecommendedPlatform>();
|
||||
for (int i = 0; i < gunCount; i++)
|
||||
platforms.Add(new RecommendedPlatform
|
||||
{
|
||||
Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(mid.X + i * 50, 0, 50),
|
||||
Quantity = 1,
|
||||
MunitionCount = 1,
|
||||
CoverageVolume = (float)ammo.InitialVolume,
|
||||
Cooldown = 5f,
|
||||
MuzzleVelocity = 800f,
|
||||
});
|
||||
|
||||
result.Best = new DefenseSolution
|
||||
{
|
||||
RecommendedAerosolType = aerosolType,
|
||||
AerosolRationale = rationale,
|
||||
RecommendedCloud = bestCloud,
|
||||
FireSchedule = fireSchedule,
|
||||
Platforms = platforms,
|
||||
Detections = new List<RecommendedDetection>
|
||||
{
|
||||
new RecommendedDetection
|
||||
{
|
||||
Position = new Vector3(mid.X, mid.Y, 0),
|
||||
DetectionRadius = Math.Max(3000f, routeLength * 0.4f),
|
||||
Quantity = 1,
|
||||
}
|
||||
},
|
||||
InterceptProbability = prob,
|
||||
SummaryRationale = $"{aerosolType}方案,{roundsNeeded}发,预计拦截概率 {prob:P0}",
|
||||
};
|
||||
|
||||
result.Critical = new DefenseSolution
|
||||
{
|
||||
RecommendedAerosolType = aerosolType,
|
||||
RecommendedCloud = new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)aerosolType,
|
||||
PositionX = start.X + (end.X - start.X) * 0.25f,
|
||||
PositionY = start.Y,
|
||||
PositionZ = start.Z,
|
||||
DisperseHeight = (float)target.TypicalAltitude,
|
||||
Duration = (int)maxDur,
|
||||
Source = "Algorithm",
|
||||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||||
RecommendedTiming = totalFlightTime * 0.25f - expansionTime,
|
||||
},
|
||||
Platforms = platforms,
|
||||
InterceptProbability = prob * 0.3f,
|
||||
SummaryRationale = $"临界方案:拦截概率仅 {prob * 0.3f:P0}",
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Vector3 ToV3(Waypoint wp) => new((float)wp.PosX, (float)wp.PosY, (float)wp.PosZ);
|
||||
|
||||
/// <summary>计算云团在目标时刻的有效半径</summary>
|
||||
private static float ComputeEffectiveRadius(float r0, float k, float halfTime,
|
||||
float windSpeed, WeatherType weather, AmmunitionSpec ammo)
|
||||
{
|
||||
// Phase 2 结束时的半径
|
||||
var rPhase2 = r0 + k * (float)Math.Sqrt(Math.Min(halfTime, 30f));
|
||||
if (halfTime <= 30f) return rPhase2;
|
||||
|
||||
// Phase 3:用高斯扩散公式计算浓度场下的有效半径
|
||||
var x = Math.Max(1f, windSpeed * (halfTime - 30f));
|
||||
var cls = Kinematics.GetStabilityClass(weather, windSpeed);
|
||||
var sY = Kinematics.SigmaY(cls, x);
|
||||
var sZ = Kinematics.SigmaZ(cls, x);
|
||||
var peakC = Kinematics.GaussianPeakConcentration((float)ammo.SourceStrength, sY, sZ);
|
||||
var threshold = (float)ammo.EffectiveConcentration;
|
||||
if (peakC <= threshold) return rPhase2;
|
||||
|
||||
var sigma = (sY + sZ) / 2f;
|
||||
return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
|
||||
}
|
||||
|
||||
/// <summary>多编队推荐 — 每组独立方案,按时间分配火力单元,合并发射计划</summary>
|
||||
public MultiGroupRecommendation RecommendMultiGroup(
|
||||
List<DroneGroup> droneGroups, List<FireUnit> fireUnits, CombatScene environment)
|
||||
{
|
||||
var result = new MultiGroupRecommendation();
|
||||
if (droneGroups.Count == 0 || fireUnits.Count == 0) return result;
|
||||
|
||||
var sorted = droneGroups.OrderBy(g => g.GetArrivalTime()).ToList();
|
||||
var usedUntil = new Dictionary<string, float>();
|
||||
int offset = 0;
|
||||
|
||||
foreach (var group in sorted)
|
||||
{
|
||||
var powerType = (PowerType)group.Target.PowerType;
|
||||
var neededAmmo = MatchTable.GetValueOrDefault(powerType, AerosolType.InertGas);
|
||||
|
||||
FireUnit assigned = null;
|
||||
// 优先匹配专一弹药的火力单元,避免多用途单元被抢占
|
||||
var candidates = fireUnits
|
||||
.Where(u => u.AvailableAmmo.Contains(neededAmmo))
|
||||
.OrderBy(u => u.AvailableAmmo.Count) // 少弹种的优先
|
||||
.ThenBy(u => usedUntil.ContainsKey(u.GroupId) ? 1 : 0); // 不忙的优先
|
||||
|
||||
foreach (var unit in candidates)
|
||||
{
|
||||
if (!unit.AvailableAmmo.Contains(neededAmmo)) continue;
|
||||
if (!usedUntil.TryGetValue(unit.GroupId, out var busyUntil))
|
||||
{ assigned = unit; break; }
|
||||
if (unit.LoadedAmmo == neededAmmo && busyUntil <= group.GetArrivalTime())
|
||||
{ assigned = unit; break; }
|
||||
if (busyUntil + unit.AmmoChangeTime <= group.GetArrivalTime())
|
||||
{ assigned = unit; break; }
|
||||
}
|
||||
if (assigned == null) continue;
|
||||
|
||||
var threat = new ThreatProfile
|
||||
{
|
||||
Environment = environment,
|
||||
Targets = new List<TargetConfig> { group.Target },
|
||||
Route = group.Route,
|
||||
Waypoints = group.Waypoints,
|
||||
};
|
||||
var sol = Recommend(threat).Best;
|
||||
foreach (var fe in sol.FireSchedule) fe.PlatformIndex += offset;
|
||||
|
||||
result.GroupSolutions.Add(sol);
|
||||
result.MergedFireSchedule.AddRange(sol.FireSchedule);
|
||||
|
||||
assigned.LoadedAmmo = sol.RecommendedAerosolType;
|
||||
usedUntil[assigned.GroupId] = (sol.FireSchedule.Count > 0
|
||||
? sol.FireSchedule.Max(f => f.FireTime) : group.GetArrivalTime()) + assigned.Cooldown;
|
||||
offset += sol.Platforms.Count;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs
Normal file
30
src/CounterDrone.Core/Algorithms/DefaultLaneDivider.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>默认车道划分:按云团覆盖能力合并或拆分</summary>
|
||||
public class DefaultLaneDivider : ILaneDivider
|
||||
{
|
||||
public (int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius)
|
||||
{
|
||||
if (wave.Route == null || wave.Quantity <= 1)
|
||||
return (1, 0);
|
||||
|
||||
var mode = (FormationMode)wave.Route.FormationMode;
|
||||
if (mode != FormationMode.Formation)
|
||||
return (1, 0);
|
||||
|
||||
int latCount = wave.Route.LateralCount ?? wave.Quantity;
|
||||
int longCount = wave.Route.LongitudinalCount ?? 1;
|
||||
float latWidth = (latCount - 1) * (float)wave.Route.LateralSpacing;
|
||||
float longDepth = (longCount - 1) * (float)wave.Route.LongitudinalSpacing;
|
||||
float cloudDiameter = 2f * cloudRadius;
|
||||
|
||||
// 任一维度超过云团半径,则无法一个云团覆盖
|
||||
if (latWidth > cloudRadius || longDepth > cloudRadius)
|
||||
return (wave.Quantity, (float)wave.Route.LateralSpacing);
|
||||
|
||||
return (1, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
579
src/CounterDrone.Core/Algorithms/DefensePlanner.cs
Normal file
579
src/CounterDrone.Core/Algorithms/DefensePlanner.cs
Normal file
@ -0,0 +1,579 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御规划器 — 五步流水线,全部调用与引擎共享的物理工具类</summary>
|
||||
public class DefensePlanner : IDefensePlanner
|
||||
{
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
private readonly IDamageModel _damageModel;
|
||||
private readonly PlannerConfig _config;
|
||||
private readonly ILaneDivider _laneDivider;
|
||||
|
||||
public DefensePlanner(List<AmmunitionSpec> ammoCatalog, PlannerConfig config,
|
||||
IDamageModel? damageModel = null, ILaneDivider? laneDivider = null)
|
||||
{
|
||||
_ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog));
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_damageModel = damageModel ?? new DamageModelRouter();
|
||||
_laneDivider = laneDivider ?? new DefaultLaneDivider();
|
||||
if (_ammoCatalog.Count == 0)
|
||||
throw new ArgumentException("弹药规格目录不能为空");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 五步流水线
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
|
||||
CombatScene environment, List<DetectionSource> detectionSources)
|
||||
{
|
||||
var result = new PlannerResult();
|
||||
|
||||
if (threats.Count == 0)
|
||||
{
|
||||
result.Best.Summary = "无威胁目标";
|
||||
return result;
|
||||
}
|
||||
if (fireUnits.Count == 0)
|
||||
{
|
||||
result.Best.Summary = "无可用火力单元";
|
||||
result.Best.ThreatsUnengaged = threats.Count;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 探测信息:每个威胁的最早发现弧长 + 精度(基于统一信息网络)
|
||||
// 无探测设备时 detectArc=0(上帝视角,从航路起点算)、精度用配置默认值
|
||||
float visibility = (float)environment.Visibility;
|
||||
foreach (var t in threats)
|
||||
{
|
||||
var (detectArc, accuracy) = DetectionCalculator.EarliestDetection(
|
||||
t.Waypoints, detectionSources, visibility);
|
||||
t.DetectArc = detectArc;
|
||||
t.DetectAccuracy = accuracy == float.MaxValue
|
||||
? _config.DefaultDetectionAccuracy
|
||||
: accuracy;
|
||||
}
|
||||
|
||||
// Step 1: 威胁排序
|
||||
foreach (var t in threats)
|
||||
{
|
||||
t.ArrivalTime = t.GetArrivalTime();
|
||||
t.ThreatIndex = CalcThreatIndex(_config, t.Profile);
|
||||
}
|
||||
var sorted = threats.OrderByDescending(t => t.Priority).ToList();
|
||||
|
||||
// Step 2-4: 贪心分配求解
|
||||
// 预先检查:所有需要的弹药类型都在目录中
|
||||
var neededTypes = sorted
|
||||
.Select(t => MatchAmmo(_config, (PowerType)t.Profile.PowerType))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
foreach (var t in neededTypes)
|
||||
{
|
||||
if (!_ammoCatalog.Any(a => a.AerosolType == (int)t))
|
||||
throw new InvalidOperationException($"弹药规格目录中缺少类型: {t}");
|
||||
}
|
||||
|
||||
result.Best = Solve(sorted, fireUnits, environment);
|
||||
|
||||
// Step 5: 临界方案
|
||||
result.Critical = DeriveCritical(result.Best);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 1: 威胁指数
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static float CalcThreatIndex(PlannerConfig config, DroneSpec spec)
|
||||
{
|
||||
float typeCoef = config.TypeCoefficient.GetValueOrDefault((DroneType)spec.DroneType, 1f);
|
||||
float speedCoef = (float)spec.TypicalSpeed / 60f;
|
||||
return typeCoef * speedCoef;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 2: 弹药匹配
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static AerosolType MatchAmmo(PlannerConfig config, PowerType power)
|
||||
{
|
||||
return config.AmmoMatch.GetValueOrDefault(power, AerosolType.InertGas);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 3-4: 候选生成 → 贪心分配
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private DefensePlan Solve(List<DroneWave> sortedThreats,
|
||||
List<FireUnit> fireUnits, CombatScene env)
|
||||
{
|
||||
var plan = new DefensePlan();
|
||||
var remainingMunitions = new Dictionary<string, int>();
|
||||
foreach (var u in fireUnits)
|
||||
remainingMunitions[u.Id] = u.TotalMunitions;
|
||||
|
||||
foreach (var threat in sortedThreats)
|
||||
{
|
||||
var available = fireUnits
|
||||
.Where(u => remainingMunitions.GetValueOrDefault(u.Id, 0) > 0)
|
||||
.ToList();
|
||||
|
||||
var candidates = GenerateCandidates(threat, available, env);
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
plan.ThreatsUnengaged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算总弹药需求
|
||||
var neededAmmo = MatchAmmo(_config, (PowerType)threat.Profile.PowerType);
|
||||
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
|
||||
|
||||
// 单机需求
|
||||
var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env);
|
||||
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, effectiveRadius);
|
||||
|
||||
// 车道划分(可替换策略)
|
||||
var (yLanes, laneSpacing) = _laneDivider.Divide(threat, effectiveRadius);
|
||||
float formationWidth = yLanes > 1 ? (yLanes - 1) * laneSpacing : 0f;
|
||||
int totalRoundsNeeded = singleNeeded * yLanes;
|
||||
|
||||
// 逐单元分配:每个单元锁定一个 Y 车道
|
||||
// 云团间距 = 2R × (1 - 重叠比例),R 为穿越时刻的有效半径,保证无缝覆盖
|
||||
float spacing = 2f * effectiveRadius * (1f - _config.CloudOverlapRatio);
|
||||
int[] laneNeeded = new int[yLanes];
|
||||
float[] laneBaseTime = new float[yLanes];
|
||||
bool[] laneBaseSet = new bool[yLanes];
|
||||
for (int l = 0; l < yLanes; l++) laneNeeded[l] = singleNeeded;
|
||||
int currentLane = 0;
|
||||
int roundsCollected = 0;
|
||||
var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();
|
||||
|
||||
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
||||
.ThenByDescending(c => c.KillProbability))
|
||||
{
|
||||
if (roundsCollected >= totalRoundsNeeded) break;
|
||||
int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
|
||||
if (remaining <= 0) continue;
|
||||
|
||||
while (currentLane < yLanes && laneNeeded[currentLane] <= 0) currentLane++;
|
||||
if (currentLane >= yLanes) break;
|
||||
|
||||
int toTake = Math.Min(Math.Min(c.Unit.TotalChannels, remaining), laneNeeded[currentLane]);
|
||||
if (toTake <= 0) continue;
|
||||
|
||||
int yLane = currentLane;
|
||||
// 车道第一个单元设基准时间,后续单元以此为准保证间距均匀
|
||||
if (!laneBaseSet[yLane])
|
||||
{
|
||||
var (refEvt, refRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
|
||||
if (refEvt.Count == 0) { plan.RejectReason ??= refRej; continue; }
|
||||
laneBaseTime[yLane] = refEvt[0].FireTime;
|
||||
laneBaseSet[yLane] = true;
|
||||
}
|
||||
float stagger = Kinematics.CloudCoverInterval(effectiveRadius * 2f, GetDroneSpeedKph(threat), c.Unit.ChannelInterval);
|
||||
|
||||
var fevents = new List<FireEvent>();
|
||||
int unitIdx = fireUnits.IndexOf(c.Unit);
|
||||
int baseRoundInLane = singleNeeded - laneNeeded[currentLane];
|
||||
for (int ch = 0; ch < toTake; ch++)
|
||||
{
|
||||
// offset 基于车道内发序(不是全局 eventIdx),使每车道独立分布在同一航路段,
|
||||
// 多车道重叠覆盖而非沿航路连成长链
|
||||
int roundInLane = baseRoundInLane + ch;
|
||||
float offset = (roundInLane - (singleNeeded - 1) / 2f) * spacing;
|
||||
var (fe, feRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
|
||||
if (fe.Count == 0) { plan.RejectReason ??= feRej; continue; }
|
||||
foreach (var e in fe)
|
||||
{
|
||||
e.FireTime = laneBaseTime[yLane] + roundInLane * stagger;
|
||||
// TargetX/Z 保留 GenerateFireEventsAt 算出的风偏补偿后抛撒点 cloudGenX/Z
|
||||
e.PlatformIndex = unitIdx * c.Unit.TotalChannels + ch;
|
||||
}
|
||||
fevents.AddRange(fe);
|
||||
}
|
||||
|
||||
assignedUnits.Add((c.Unit, toTake, fevents));
|
||||
remainingMunitions[c.Unit.Id] -= toTake;
|
||||
laneNeeded[currentLane] -= toTake;
|
||||
roundsCollected += toTake;
|
||||
}
|
||||
|
||||
if (roundsCollected <= 0) {
|
||||
plan.RejectReason ??= $"候选{candidates.Count}个,弹药需求{totalRoundsNeeded}发,实际收集0发";
|
||||
plan.ThreatsUnengaged++; continue;
|
||||
}
|
||||
|
||||
foreach (var (unit, rounds, fireEvents) in assignedUnits)
|
||||
{
|
||||
plan.Assignments.Add(new UnitAssignment
|
||||
{
|
||||
FireUnitId = unit.Id,
|
||||
DroneWaveId = threat.WaveId,
|
||||
AmmoType = neededAmmo,
|
||||
RoundsFired = rounds,
|
||||
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
|
||||
FireEvents = fireEvents,
|
||||
});
|
||||
plan.MergedSchedule.AddRange(fireEvents);
|
||||
}
|
||||
|
||||
plan.ThreatsEngaged++;
|
||||
}
|
||||
|
||||
plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||||
|
||||
// 按威胁汇总概率
|
||||
var threatProbs = new List<float>();
|
||||
foreach (var threat in sortedThreats)
|
||||
{
|
||||
var a2 = _ammoCatalog.FirstOrDefault(s =>
|
||||
s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Profile.PowerType));
|
||||
int totalRounds = plan.Assignments
|
||||
.Where(a => a.DroneWaveId == threat.WaveId)
|
||||
.Sum(a => a.RoundsFired);
|
||||
if (totalRounds > 0)
|
||||
threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
|
||||
}
|
||||
plan.OverallProbability = threatProbs.Count > 0 ? threatProbs.Average() : 0f;
|
||||
|
||||
// 失败原因 + 建议值
|
||||
var reasons = new List<string>();
|
||||
foreach (var threat in sortedThreats)
|
||||
{
|
||||
bool engaged = plan.Assignments.Any(a => a.DroneWaveId == threat.WaveId);
|
||||
if (engaged) continue;
|
||||
var ammo = MatchAmmo(_config, (PowerType)threat.Profile.PowerType);
|
||||
var matching = fireUnits.Where(u => u.AmmoTypes.Contains(ammo)).ToList();
|
||||
if (matching.Count == 0)
|
||||
reasons.Add($"威胁 {threat.WaveId}: 无火力单元装载 {ammo} 弹药");
|
||||
else
|
||||
{
|
||||
var (_, expTime, _) = ComputeEffectiveRadius(_ammoCatalog.First(a => a.AerosolType == (int)ammo), env);
|
||||
float avgSpd = GetDroneSpeedMs(threat);
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints,
|
||||
(float)(threat.Waypoints[0].PosX + threat.Waypoints[^1].PosX) / 2f,
|
||||
(float)(threat.Waypoints[0].PosZ + threat.Waypoints[^1].PosZ) / 2f);
|
||||
float travel = midArc - threat.DetectArc;
|
||||
if (travel <= 0)
|
||||
{
|
||||
var (mx, _, mz) = RouteGeometry.PositionAt(threat.Waypoints, midArc);
|
||||
float r = matching.Min(u => MathF.Sqrt((mx - u.Position.X) * (mx - u.Position.X) + (mz - u.Position.Z) * (mz - u.Position.Z)));
|
||||
reasons.Add($"威胁 {threat.WaveId}: 探测边界在拦截点之后,建议探测范围≥{r:F0}m");
|
||||
}
|
||||
else if (travel / avgSpd <= expTime)
|
||||
{
|
||||
float maxDA = Math.Max(0, midArc - avgSpd * (expTime + _config.TimingSafetyMargin));
|
||||
var (nx, _, nz) = RouteGeometry.PositionAt(threat.Waypoints, maxDA);
|
||||
float r = matching.Min(u => MathF.Sqrt((nx - u.Position.X) * (nx - u.Position.X) + (nz - u.Position.Z) * (nz - u.Position.Z)));
|
||||
reasons.Add($"威胁 {threat.WaveId}: 拦截窗口不足({travel / avgSpd:F1}s<膨胀{expTime:F1}s),建议探测范围≥{r:F0}m(最晚弧长{maxDA:F0}m)");
|
||||
}
|
||||
else
|
||||
{
|
||||
string detail = plan.RejectReason != null ? $"({plan.RejectReason})" : "";
|
||||
reasons.Add($"威胁 {threat.WaveId}: 候选存在但分配失败{detail}");
|
||||
}
|
||||
}
|
||||
}
|
||||
plan.Summary = plan.ThreatsEngaged > 0
|
||||
? $"分配 {plan.ThreatsEngaged} 个威胁,{plan.ThreatsUnengaged} 个无方案"
|
||||
+ (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "")
|
||||
: "无威胁被分配拦截方案"
|
||||
+ (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "");
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 拦截窗口可行性检查
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// <summary>无人机速度(km/h),从航路第一个 waypoint 读取</summary>
|
||||
private static float GetDroneSpeedKph(DroneWave threat)
|
||||
{
|
||||
float spd = (float)threat.Waypoints[0].Speed;
|
||||
if (spd <= 0) throw new InvalidOperationException($"威胁 {threat.WaveId}: 航路点速度必须 > 0");
|
||||
return spd;
|
||||
}
|
||||
|
||||
/// <summary>无人机速度(m/s)</summary>
|
||||
private static float GetDroneSpeedMs(DroneWave threat)
|
||||
=> GetDroneSpeedKph(threat) / 3.6f;
|
||||
|
||||
internal static bool HasInterceptWindow(DroneWave threat, float midArc,
|
||||
float expansionTime, float deliveryTime)
|
||||
{
|
||||
float travelArc = midArc - threat.DetectArc;
|
||||
if (travelArc <= 0) return false;
|
||||
float timeAvailable = travelArc / GetDroneSpeedMs(threat);
|
||||
return timeAvailable > expansionTime + deliveryTime;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 候选生成(使用真实物理)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private List<InterceptCandidate> GenerateCandidates(DroneWave threat,
|
||||
List<FireUnit> availableUnits, CombatScene env)
|
||||
{
|
||||
var candidates = new List<InterceptCandidate>();
|
||||
var neededAmmo = MatchAmmo(_config, (PowerType)threat.Profile.PowerType);
|
||||
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
|
||||
|
||||
var (ammoEff, expansionTime, _) = ComputeEffectiveRadius(ammo, env);
|
||||
var mid = ThreatMidpoint(threat);
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints, mid.X, mid.Z);
|
||||
|
||||
foreach (var unit in availableUnits)
|
||||
{
|
||||
if (!unit.AmmoTypes.Contains(neededAmmo)) continue;
|
||||
|
||||
var c = BuildCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
|
||||
if (c != null) candidates.Add(c);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private InterceptCandidate? BuildCandidate(DroneWave threat,
|
||||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||||
float effectiveR, float expansionTime,
|
||||
Vector3 mid, float midArc, CombatScene env)
|
||||
{
|
||||
float dx = mid.X - unit.Position.X;
|
||||
float dz = mid.Z - unit.Position.Z;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (unit.MuzzleVelocity <= 0) return null;
|
||||
float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f;
|
||||
if (dist > maxRange) return null;
|
||||
|
||||
float deliveryTime = dist / unit.MuzzleVelocity;
|
||||
if (deliveryTime > threat.ArrivalTime) return null;
|
||||
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
|
||||
|
||||
float avgSpeed = GetDroneSpeedMs(threat);
|
||||
float neededExposure = _damageModel.RequiredExposureSeconds((DroneType)threat.Profile.DroneType, (PowerType)threat.Profile.PowerType, ammoType);
|
||||
float actualExposure = effectiveR * 2f / avgSpeed;
|
||||
float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
|
||||
|
||||
return new InterceptCandidate
|
||||
{
|
||||
Unit = unit,
|
||||
AmmoType = ammoType,
|
||||
EarliestInterceptTime = threat.ArrivalTime,
|
||||
KillProbability = prob,
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 弹药计算(使用 AmmunitionSpec + 环境参数)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius(
|
||||
AmmunitionSpec ammo, CombatScene env)
|
||||
{
|
||||
var model = new CloudExpansionModel(ammo, env);
|
||||
float rPhase2 = model.RadiusAt(model.Phase2Duration);
|
||||
float expansionTime = model.TimeToReach(rPhase2) * _config.ExpansionFactor;
|
||||
float effectiveR = model.RadiusAt(expansionTime);
|
||||
return (effectiveR, expansionTime, rPhase2);
|
||||
}
|
||||
|
||||
private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
|
||||
CombatScene env, bool isAirBased, float turbulentRadius)
|
||||
{
|
||||
var cloudModel = new CloudExpansionModel(ammo, env);
|
||||
float avgSpeed = GetDroneSpeedMs(threat);
|
||||
float neededExposure = _damageModel.RequiredExposureSeconds(
|
||||
(DroneType)threat.Profile.DroneType, (PowerType)threat.Profile.PowerType, (AerosolType)ammo.AerosolType);
|
||||
float requiredCoverage = neededExposure * avgSpeed;
|
||||
// 间距由配置的重叠比例驱动,公式在 CloudExpansionModel(共享)
|
||||
float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
|
||||
return cloudModel.RoundsNeeded(requiredCoverage, spacing);
|
||||
}
|
||||
|
||||
private float ComputeInterceptProbability(DroneWave threat,
|
||||
AmmunitionSpec ammo, int rounds, CombatScene env)
|
||||
{
|
||||
float avgSpeed = GetDroneSpeedMs(threat);
|
||||
var (effectiveR, _, _) = ComputeEffectiveRadius(ammo, env);
|
||||
float spacing = 2f * effectiveR * (1f - _config.CloudOverlapRatio);
|
||||
float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR;
|
||||
float actualExposure = actualCoverage / avgSpeed;
|
||||
|
||||
var aerosolType = (AerosolType)ammo.AerosolType;
|
||||
float neededExposure = _damageModel.RequiredExposureSeconds((DroneType)threat.Profile.DroneType, (PowerType)threat.Profile.PowerType, aerosolType);
|
||||
|
||||
return Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 发射事件生成(真实物理)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private (List<FireEvent> Events, string? RejectReason) TryGenerateFireEvents(
|
||||
DroneWave threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||||
CombatScene env, float targetOffset, int yLane, int yLanes, float formationWidth)
|
||||
{
|
||||
var events = new List<FireEvent>();
|
||||
var wps = threat.Waypoints;
|
||||
if (wps == null || wps.Count < 2) return (events, "航路无效");
|
||||
|
||||
var cloudModel = new CloudExpansionModel(ammo, env);
|
||||
var (effectiveR, expansionTime, _) = ComputeEffectiveRadius(ammo, env);
|
||||
|
||||
float densityAtPassage = cloudModel.DensityAt(expansionTime);
|
||||
if (densityAtPassage < (float)ammo.EffectiveConcentration)
|
||||
return (events, $"云团密度{densityAtPassage:E2}<有效阈值{ammo.EffectiveConcentration:E2}");
|
||||
|
||||
float typicalSpeed = GetDroneSpeedKph(threat);
|
||||
if (typicalSpeed <= 0) return (events, "目标速度无效");
|
||||
|
||||
// ═══ 拦截弧长 ═══
|
||||
var mid = ThreatMidpoint(threat);
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
|
||||
var (ia, _, _) = InterceptCalculator.Compute(
|
||||
wps, threat.DetectArc, typicalSpeed,
|
||||
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
|
||||
float crossArc = (ia > 0 ? ia : midArc) + targetOffset;
|
||||
|
||||
// 无人机到达穿越点的时间:基于探测边界,而非航路起点
|
||||
float travelArc = crossArc - threat.DetectArc;
|
||||
if (travelArc < 0) return (events, $"探测边界在拦截点之后({threat.DetectArc:F0}m≥{crossArc:F0}m)");
|
||||
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
|
||||
float recommendedTiming = txArrival - expansionTime;
|
||||
if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");
|
||||
|
||||
// ═══ 编队横向偏移 ═══
|
||||
int axis = threat.Route?.LateralAxis ?? 2;
|
||||
float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
|
||||
float laneOffset = yLane * laneSpacing;
|
||||
|
||||
var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc);
|
||||
float tx = routeX, ty = routeY, tz = routeZ;
|
||||
if (axis == 0) tx += laneOffset;
|
||||
else if (axis == 1) ty += laneOffset;
|
||||
else tz += laneOffset;
|
||||
|
||||
// 风偏补偿:先算云团生成点,再按实际位置算发射角
|
||||
var (wx, _, wz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
|
||||
float cloudGenX = tx - wx * expansionTime;
|
||||
float cloudGenZ = tz - wz * expansionTime;
|
||||
|
||||
float dx = cloudGenX - unit.Position.X;
|
||||
float dz = cloudGenZ - unit.Position.Z;
|
||||
float targetDist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
float mv = unit.MuzzleVelocity;
|
||||
if (mv <= 0)
|
||||
throw new InvalidOperationException($"单元 {unit.Id}: MuzzleVelocity={mv} 必须>0");
|
||||
float heightDiff = ty - unit.Position.Y;
|
||||
|
||||
// ═══ 抛物线求解:两解逐一验证,选最早摧毁无人机的可行解 ═══
|
||||
var angles = ParabolicMotion.SolveAngles(targetDist, heightDiff, mv);
|
||||
if (!angles.HasValue)
|
||||
return (events, $"目标超出弹道射程: dist={targetDist:F0}m");
|
||||
float? bestLaunchAngle = null, bestTof = null, bestFireTime = null;
|
||||
foreach (var a in new[] { angles.Value.Item1, angles.Value.Item2 })
|
||||
{
|
||||
var motion = new ParabolicMotion(mv, a);
|
||||
var ts = motion.GetFlightTimes(heightDiff);
|
||||
foreach (var t in ts)
|
||||
{
|
||||
float r = mv * (float)Math.Cos(a) * t;
|
||||
if (Math.Abs(r - targetDist) > 0.5f) continue;
|
||||
float ft = recommendedTiming - t;
|
||||
if (ft <= 0f) continue;
|
||||
// 选最早摧毁(fireTime 最小 → 发射最早 → 拦截最早)
|
||||
if (!bestFireTime.HasValue || ft < bestFireTime.Value)
|
||||
{
|
||||
bestLaunchAngle = a; bestTof = t; bestFireTime = ft;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bestLaunchAngle.HasValue)
|
||||
return (events, $"所有弹道解均不可行: dist={targetDist:F0}m");
|
||||
float launchAngle = bestLaunchAngle.Value;
|
||||
float tof = bestTof!.Value;
|
||||
float fireTime = bestFireTime!.Value;
|
||||
|
||||
events.Add(new FireEvent
|
||||
{
|
||||
FireTime = fireTime,
|
||||
PlatformIndex = 0,
|
||||
TargetX = cloudGenX,
|
||||
TargetY = ty,
|
||||
TargetZ = cloudGenZ,
|
||||
MuzzleVelocity = mv,
|
||||
LaunchAngle = launchAngle,
|
||||
FlightDuration = tof,
|
||||
});
|
||||
|
||||
return (events, null);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Step 5: 临界方案(概率阈值 50%)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private DefensePlan DeriveCritical(DefensePlan best)
|
||||
{
|
||||
var critical = new DefensePlan
|
||||
{
|
||||
ThreatsEngaged = best.ThreatsEngaged,
|
||||
ThreatsUnengaged = best.ThreatsUnengaged,
|
||||
};
|
||||
|
||||
foreach (var assignment in best.Assignments)
|
||||
{
|
||||
int rounds = assignment.RoundsFired;
|
||||
while (rounds > 1)
|
||||
{
|
||||
// 简化:弹药减半 → 概率减半
|
||||
if ((float)rounds / assignment.RoundsFired < _config.CriticalProbabilityThreshold) break;
|
||||
rounds--;
|
||||
}
|
||||
|
||||
var reduced = new UnitAssignment
|
||||
{
|
||||
FireUnitId = assignment.FireUnitId,
|
||||
DroneWaveId = assignment.DroneWaveId,
|
||||
AmmoType = assignment.AmmoType,
|
||||
RoundsFired = rounds,
|
||||
FirstFireTime = assignment.FirstFireTime,
|
||||
FireEvents = assignment.FireEvents.Take(rounds).ToList(),
|
||||
};
|
||||
critical.Assignments.Add(reduced);
|
||||
critical.MergedSchedule.AddRange(reduced.FireEvents);
|
||||
}
|
||||
|
||||
critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||||
critical.OverallProbability = _config.CriticalProbabilityThreshold;
|
||||
critical.Summary = $"临界方案:刚好满足 50% 拦截概率";
|
||||
return critical;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 辅助
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
private static Vector3 ThreatMidpoint(DroneWave threat)
|
||||
{
|
||||
if (threat.Waypoints.Count < 2)
|
||||
return new Vector3(0, (float)threat.Profile.TypicalAltitude, 0);
|
||||
var s = threat.Waypoints[0];
|
||||
var e = threat.Waypoints[^1];
|
||||
return new Vector3(
|
||||
(float)(s.PosX + e.PosX) / 2f,
|
||||
(float)(s.PosY + e.PosY) / 2f,
|
||||
(float)(s.PosZ + e.PosZ) / 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
146
src/CounterDrone.Core/Algorithms/DetectionCalculator.cs
Normal file
146
src/CounterDrone.Core/Algorithms/DetectionCalculator.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>探测计算工具——统一信息网络对威胁的探测能力评估。
|
||||
/// 纯函数、无状态,与 Kinematics/RouteGeometry 同范式。
|
||||
/// planner(事前规划)基于此估算威胁的最早发现点,而非上帝视角的航路起点。</summary>
|
||||
public static class DetectionCalculator
|
||||
{
|
||||
/// <summary>某探测源在给定能见度下的综合有效探测距离(水平面)。
|
||||
/// 取雷达/光电/红外三者的最大有效距离(任一方式发现即算发现)。
|
||||
/// 光电受 Visibility 衰减:有效 = 基准 × min(1, Visibility/基准)。
|
||||
/// 雷达/红外不受能见度影响(当前范围)。</summary>
|
||||
public static float EffectiveRange(float radarRange, float eoRange, float irRange, float visibility)
|
||||
{
|
||||
float effRadar = radarRange;
|
||||
float effEO = eoRange > 0
|
||||
? eoRange * Math.Min(1f, visibility / eoRange)
|
||||
: 0f;
|
||||
float effIR = irRange;
|
||||
return Math.Max(effRadar, Math.Max(effEO, effIR));
|
||||
}
|
||||
|
||||
/// <summary>统一信息网络对某威胁的最早探测点。
|
||||
/// 沿航路以步长 ≤50m 采样,对每个采样点调 IsInCoverage 做 3D 球冠判定。
|
||||
/// 返回首次落入任一探测源球冠的弧长与精度。
|
||||
/// 无探测源时返回 (0, float.MaxValue)(回退到航路起点,上帝视角)。
|
||||
/// 航路完全不经过任何探测范围时返回 (float.MaxValue, float.MaxValue)。</summary>
|
||||
public static (float detectArc, float accuracy) EarliestDetection(
|
||||
IReadOnlyList<Waypoint> threatRoute,
|
||||
List<DetectionSource> sources,
|
||||
float visibility)
|
||||
{
|
||||
if (sources == null || sources.Count == 0 || threatRoute == null || threatRoute.Count < 2)
|
||||
return (0f, float.MaxValue);
|
||||
|
||||
// 计算总弧长,确定采样步长(≤50m)
|
||||
float totalArc = 0f;
|
||||
for (int i = 0; i < threatRoute.Count - 1; i++)
|
||||
{
|
||||
float dx = (float)(threatRoute[i + 1].PosX - threatRoute[i].PosX);
|
||||
float dy = (float)(threatRoute[i + 1].PosY - threatRoute[i].PosY);
|
||||
float dz = (float)(threatRoute[i + 1].PosZ - threatRoute[i].PosZ);
|
||||
totalArc += MathF.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
const float sampleStep = 50f;
|
||||
int sampleCount = Math.Max(1, (int)(totalArc / sampleStep) + 1);
|
||||
float stepArc = totalArc / sampleCount;
|
||||
|
||||
float bestArc = float.MaxValue;
|
||||
float bestAccuracy = float.MaxValue;
|
||||
|
||||
// 预计算每个源的 EffectiveRange(能见度衰减)
|
||||
var srcCache = new (float range, DetectionSource src)[sources.Count];
|
||||
for (int s = 0; s < sources.Count; s++)
|
||||
{
|
||||
var src = sources[s];
|
||||
float range = EffectiveRange(src.RadarRange, src.EORange, src.IRRange, visibility);
|
||||
srcCache[s] = (range, src);
|
||||
}
|
||||
|
||||
// 沿航路采样
|
||||
for (int i = 0; i <= sampleCount; i++)
|
||||
{
|
||||
float sampleArc = i * stepArc;
|
||||
var (sx, sy, sz) = RouteGeometry.PositionAt(threatRoute, sampleArc);
|
||||
var pos = new Algorithms.Vector3(sx, sy, sz);
|
||||
|
||||
for (int s = 0; s < srcCache.Length; s++)
|
||||
{
|
||||
var (range, src) = srcCache[s];
|
||||
if (range <= 0) continue;
|
||||
if (IsInCoverage(pos, src.Position, range,
|
||||
src.MinElevation, src.MaxElevation,
|
||||
src.MinDetectAlt, src.MaxDetectAlt))
|
||||
{
|
||||
// 最早发现优先;同一点取精度最高
|
||||
if (sampleArc < bestArc)
|
||||
{
|
||||
bestArc = sampleArc;
|
||||
bestAccuracy = src.Accuracy;
|
||||
}
|
||||
else if (MathF.Abs(sampleArc - bestArc) < 0.001f
|
||||
&& src.Accuracy < bestAccuracy)
|
||||
{
|
||||
bestAccuracy = src.Accuracy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 一旦发现第一个覆盖点即停止(后续采样点距离更远,不可能是"最早")
|
||||
if (bestArc < float.MaxValue)
|
||||
break;
|
||||
}
|
||||
|
||||
if (bestArc == float.MaxValue)
|
||||
return (float.MaxValue, float.MaxValue);
|
||||
return (bestArc, bestAccuracy);
|
||||
}
|
||||
|
||||
/// <summary>三维球冠探测判定:目标是否在探测设备的有效探测范围内。
|
||||
/// 三项判定:① 水平距离 ≤ effectiveRange;② 俯仰角 ∈ [MinElevation, MaxElevation];
|
||||
/// ③ 目标高度 ∈ [MinDetectAlt, MaxDetectAlt]。
|
||||
/// MinElevation/MaxElevation 为 float.MaxValue 时表示无限制(退化球冠→等价 2D 圆)。
|
||||
/// MinDetectAlt/MaxDetectAlt 为 float.MaxValue 时同理。
|
||||
/// planner 与运行时实时探测共用此判定。</summary>
|
||||
public static bool IsInCoverage(
|
||||
Algorithms.Vector3 target, Algorithms.Vector3 detector,
|
||||
float effectiveRange,
|
||||
float minElevation, float maxElevation,
|
||||
float minDetectAlt, float maxDetectAlt)
|
||||
{
|
||||
float dx = target.X - detector.X;
|
||||
float dy = target.Y - detector.Y; // 高度差
|
||||
float dz = target.Z - detector.Z;
|
||||
float horizDistSq = dx * dx + dz * dz;
|
||||
// ① 水平距离
|
||||
if (horizDistSq > effectiveRange * effectiveRange) return false;
|
||||
// ③ 高度门限
|
||||
if (minDetectAlt != float.MaxValue && target.Y < minDetectAlt) return false;
|
||||
if (maxDetectAlt != float.MaxValue && target.Y > maxDetectAlt) return false;
|
||||
// ② 俯仰角(正顶/正下按角度门限放行)
|
||||
if (minElevation == float.MaxValue && maxElevation == float.MaxValue) return true;
|
||||
float horizDist = MathF.Sqrt(horizDistSq);
|
||||
if (horizDist < 0.0001f) return true;
|
||||
float elevation = MathF.Atan2(dy, horizDist) * (180f / MathF.PI);
|
||||
if (minElevation != float.MaxValue && elevation < minElevation) return false;
|
||||
if (maxElevation != float.MaxValue && elevation > maxElevation) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>探测精度换算为抛撒散布半径(m)。
|
||||
/// 精度差 → 散布半径大,planner 增加横向覆盖。
|
||||
/// 当前模型:散布半径 = 精度值(如精度 100m → 散布 ±100m)。</summary>
|
||||
public static float SpreadRadius(float accuracy)
|
||||
{
|
||||
if (accuracy < 0)
|
||||
throw new ArgumentException($"accuracy 必须 ≥ 0,实际: {accuracy}", nameof(accuracy));
|
||||
return accuracy;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
17
src/CounterDrone.Core/Algorithms/DetectionSource.cs
Normal file
17
src/CounterDrone.Core/Algorithms/DetectionSource.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>探测源(独立探测设备或火力单元自带探测能力)</summary>
|
||||
public class DetectionSource
|
||||
{
|
||||
public Vector3 Position { get; set; }
|
||||
public float RadarRange { get; set; }
|
||||
public float EORange { get; set; }
|
||||
public float IRRange { get; set; }
|
||||
public float Accuracy { get; set; }
|
||||
public float MinElevation { get; set; } = float.MaxValue;
|
||||
public float MaxElevation { get; set; } = float.MaxValue;
|
||||
public float MinDetectAlt { get; set; } = float.MaxValue;
|
||||
public float MaxDetectAlt { get; set; } = float.MaxValue;
|
||||
public string? ModelId { get; set; }
|
||||
}
|
||||
}
|
||||
@ -12,16 +12,19 @@ namespace CounterDrone.Core.Algorithms
|
||||
public class GaussianPuffDispersion : ICloudDispersionModel
|
||||
{
|
||||
private AmmunitionSpec _ammo = null!;
|
||||
private CombatScene _env = null!;
|
||||
private float _elapsed;
|
||||
private float _currentRadius;
|
||||
private float _currentDensity;
|
||||
private Vector3 _center;
|
||||
private Vector3 _windVelocity;
|
||||
private bool _inPhase3;
|
||||
private float _initialRadius;
|
||||
|
||||
public Vector3 Center => _center;
|
||||
public float Radius => _currentRadius;
|
||||
public float CoreDensity => _currentDensity;
|
||||
public float PeakDensity { get; private set; }
|
||||
public float EffectiveRadius => _currentRadius;
|
||||
public ParticleParams Particles { get; } = new();
|
||||
public bool IsDissipated { get; private set; }
|
||||
@ -31,13 +34,17 @@ namespace CounterDrone.Core.Algorithms
|
||||
public void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime)
|
||||
{
|
||||
_ammo = ammo;
|
||||
_env = env;
|
||||
_elapsed = 0f;
|
||||
_inPhase3 = false;
|
||||
|
||||
// Phase 1: 爆轰膨胀 → 初始半径
|
||||
var w = (float)ammo.BurstChargeKg;
|
||||
_currentRadius = 3.3f * (float)Math.Pow(Math.Max(0.01, w), 0.32);
|
||||
if (w <= 0)
|
||||
throw new ArgumentException($"BurstChargeKg 必须 > 0,实际: {w}", nameof(ammo));
|
||||
_initialRadius = 3.3f * (float)Math.Pow(w, 0.32);
|
||||
_currentRadius = _initialRadius;
|
||||
_currentDensity = (float)ammo.CoreDensity;
|
||||
PeakDensity = (float)ammo.CoreDensity;
|
||||
_center = releasePos;
|
||||
IsDissipated = false;
|
||||
|
||||
@ -54,16 +61,14 @@ namespace CounterDrone.Core.Algorithms
|
||||
var (vx, vy, vz) = Kinematics.WindToVector(windDir, windSpeed);
|
||||
_windVelocity = new Vector3(vx, vy, vz);
|
||||
|
||||
// 判断阶段切换
|
||||
if (!_inPhase3 && _elapsed >= 30f)
|
||||
if (!_inPhase3 && _elapsed >= (float)_ammo.Phase2Duration)
|
||||
_inPhase3 = true;
|
||||
|
||||
if (!_inPhase3)
|
||||
{
|
||||
// Phase 2: 湍流扩散 R(t) = R₀ + k × √t
|
||||
var k = (float)_ammo.TurbulentExpansionK;
|
||||
_currentRadius = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)_ammo.BurstChargeKg), 0.32)
|
||||
+ k * (float)Math.Sqrt(_elapsed);
|
||||
_currentRadius = _initialRadius + k * (float)Math.Sqrt(_elapsed);
|
||||
// 密度 = 源强 / 体积
|
||||
var volume = (4f / 3f) * (float)Math.PI * _currentRadius * _currentRadius * _currentRadius;
|
||||
_currentDensity = volume > 0.001f ? (float)_ammo.SourceStrength / volume : 0f;
|
||||
@ -71,12 +76,12 @@ namespace CounterDrone.Core.Algorithms
|
||||
else
|
||||
{
|
||||
// Phase 3: 高斯扩散
|
||||
var x = Math.Max(1f, windSpeed * (_elapsed - 30f));
|
||||
var cls = Kinematics.GetStabilityClass((WeatherType)0, windSpeed);
|
||||
float x = windSpeed * (_elapsed - (float)_ammo.Phase2Duration);
|
||||
if (x <= 0) return; // 尚未进入有效扩散距离
|
||||
var cls = Kinematics.GetStabilityClass((WeatherType)_env.WeatherType, windSpeed);
|
||||
var sY = Kinematics.SigmaY(cls, x);
|
||||
var sZ = Kinematics.SigmaZ(cls, x);
|
||||
_currentDensity = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ);
|
||||
// 有效半径从浓度反推
|
||||
var effConc = (float)_ammo.EffectiveConcentration;
|
||||
if (_currentDensity > effConc)
|
||||
{
|
||||
@ -90,11 +95,11 @@ namespace CounterDrone.Core.Algorithms
|
||||
_center.Y += _windVelocity.Y * deltaTime;
|
||||
_center.Z += _windVelocity.Z * deltaTime;
|
||||
|
||||
// 粒子参数
|
||||
Particles.Opacity = Math.Max(0.1f, _currentDensity / (float)_ammo.CoreDensity);
|
||||
Particles.SizeMultiplier = _currentRadius / Math.Max(0.5f, 3.3f * (float)Math.Pow(Math.Max(0.01, (float)_ammo.BurstChargeKg), 0.32));
|
||||
// 粒子参数(仅用于可视化,不参与物理计算)
|
||||
float coreDensity = (float)_ammo.CoreDensity;
|
||||
Particles.Opacity = coreDensity > 0 ? _currentDensity / coreDensity : 0f;
|
||||
Particles.SizeMultiplier = _initialRadius > 0 ? _currentRadius / _initialRadius : 0f;
|
||||
|
||||
// 消散
|
||||
if (_elapsed >= (float)_ammo.MaxDuration || _currentRadius >= (float)_ammo.MaxRadius)
|
||||
{
|
||||
IsDissipated = true;
|
||||
|
||||
@ -5,9 +5,12 @@ 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(DroneType droneType, PowerType powerType, AerosolType aerosolType);
|
||||
|
||||
DamageStage GetDamageStage(float accumulatedDamage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御推荐接口 — 威胁驱动</summary>
|
||||
public interface IDefenseAdvisor
|
||||
{
|
||||
DefenseRecommendation Recommend(ThreatProfile threat);
|
||||
|
||||
/// <summary>多编队推荐 — 为每组分配火力单元,合并发射计划</summary>
|
||||
MultiGroupRecommendation RecommendMultiGroup(
|
||||
List<DroneGroup> droneGroups,
|
||||
List<FireUnit> fireUnits,
|
||||
CombatScene environment);
|
||||
}
|
||||
}
|
||||
17
src/CounterDrone.Core/Algorithms/IDefensePlanner.cs
Normal file
17
src/CounterDrone.Core/Algorithms/IDefensePlanner.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御规划引擎 — 给定火力单元池、威胁列表和探测能力,输出分配方案。
|
||||
/// planner 基于探测能力估算威胁的最早发现点(而非上帝视角的航路起点)。</summary>
|
||||
public interface IDefensePlanner
|
||||
{
|
||||
/// <param name="fireUnits">可用的火力单元(空基/地基统一表达)</param>
|
||||
/// <param name="threats">威胁批次列表</param>
|
||||
/// <param name="environment">作战环境(含天气,影响光电探测距离)</param>
|
||||
/// <param name="detectionSources">统一信息网络的探测源列表(独立探测设备 + 火力单元自带探测)。可为空列表。</param>
|
||||
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
|
||||
CombatScene environment, List<DetectionSource> detectionSources);
|
||||
}
|
||||
}
|
||||
14
src/CounterDrone.Core/Algorithms/ILaneDivider.cs
Normal file
14
src/CounterDrone.Core/Algorithms/ILaneDivider.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>车道划分策略:决定一个批次拆成几个车道</summary>
|
||||
public interface ILaneDivider
|
||||
{
|
||||
/// <summary>计算车道数和不重叠的车道间距</summary>
|
||||
/// <param name="wave">无人机批次</param>
|
||||
/// <param name="cloudRadius">云团有效半径 (m)</param>
|
||||
/// <returns>(车道数, 车道间距 m)</returns>
|
||||
(int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius);
|
||||
}
|
||||
}
|
||||
@ -3,23 +3,26 @@ using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>吸入式灭火 — 阈值型:密度达标后线性累积</summary>
|
||||
/// <summary>吸入式灭火 — 累积伤害</summary>
|
||||
public class InertGasDamageModel : IDamageModel
|
||||
{
|
||||
private const float EffectiveThreshold = 0.0001f; // 对齐弹药的有效浓度
|
||||
private const float DamageRate = 0.15f; // 每秒毁伤率
|
||||
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;
|
||||
if (cloudDensity < EffectiveThreshold) return 0f;
|
||||
|
||||
// 活塞发动机对惰性气体最敏感
|
||||
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
|
||||
return DamageRate * sensitivity * deltaTime;
|
||||
}
|
||||
|
||||
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;
|
||||
return 1.0f / (DamageRate * sensitivity);
|
||||
}
|
||||
|
||||
public DamageStage GetDamageStage(float accumulatedDamage)
|
||||
{
|
||||
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
|
||||
|
||||
118
src/CounterDrone.Core/Algorithms/InterceptCalculator.cs
Normal file
118
src/CounterDrone.Core/Algorithms/InterceptCalculator.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
public static class InterceptCalculator
|
||||
{
|
||||
/// <summary>空基拦截:水平发射(θ=0°),从当前位置直接抛出。
|
||||
/// 炮弹以 vp 水平飞出,下落 tf=√(2Δy/g) 秒后到达目标高度。
|
||||
/// 拦截点 = ux + vp·tf。方程: (A-d)/vd = R + expansion + tf。</summary>
|
||||
public static (float, float, float) ComputeHorizontal(
|
||||
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
|
||||
float reactionTime, Vector3 platformPos, float cruiseSpeed,
|
||||
float releaseAlt, float targetAlt)
|
||||
{
|
||||
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || cruiseSpeed <= 0)
|
||||
return (0, 0, 0);
|
||||
|
||||
float vd = droneSpeedKmh / 3.6f;
|
||||
float dy = releaseAlt - targetAlt;
|
||||
if (dy <= 0) return (0, 0, 0);
|
||||
float tf = MathF.Sqrt(2f * dy / 9.81f);
|
||||
|
||||
// 水平发射:炮弹沿航路方向飞行
|
||||
float direction = route[route.Count - 1].PosX > route[0].PosX ? 1f : -1f;
|
||||
float A = platformPos.X + direction * cruiseSpeed * tf;
|
||||
float totalArc = RouteGeometry.TotalLength(route);
|
||||
if (A < detectArc || A > totalArc) return (0, 0, 0);
|
||||
|
||||
// 验证时间匹配
|
||||
float droneTime = (A - detectArc) / vd;
|
||||
float defendTime = reactionTime + tf;
|
||||
if (droneTime < defendTime) return (0, 0, 0);
|
||||
|
||||
return (A, droneTime - reactionTime, 0);
|
||||
}
|
||||
|
||||
/// <summary>地基拦截:D² + (Δy + ½g·ts²)² = vs²·ts²</summary>
|
||||
public static (float InterceptArc, float ShellTime, float LaunchAngle) Compute(
|
||||
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
|
||||
float reactionTime, Vector3 fireUnitPos, float muzzleVelocity)
|
||||
{
|
||||
// ... existing code unchanged ...
|
||||
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || muzzleVelocity <= 0)
|
||||
return (0, 0, 0);
|
||||
|
||||
float totalArc = RouteGeometry.TotalLength(route);
|
||||
if (detectArc >= totalArc) return (0, 0, 0);
|
||||
|
||||
float vd = droneSpeedKmh / 3.6f;
|
||||
float vs2 = muzzleVelocity * muzzleVelocity;
|
||||
float g = 9.81f;
|
||||
float uy = fireUnitPos.Y;
|
||||
IReadOnlyList<Waypoint> r = route;
|
||||
|
||||
float F(float a)
|
||||
{
|
||||
float ts = (a - detectArc) / vd - reactionTime;
|
||||
if (ts <= 0) return float.MaxValue;
|
||||
var (px, py, pz) = RouteGeometry.PositionAt(r, a);
|
||||
float dx = px - fireUnitPos.X, dz = pz - fireUnitPos.Z;
|
||||
float D2 = dx * dx + dz * dz;
|
||||
float dy = py - uy;
|
||||
float term = dy + 0.5f * g * ts * ts;
|
||||
return D2 + term * term - vs2 * ts * ts;
|
||||
}
|
||||
|
||||
static float Angle(Vector3 unit, float arc, float ts, float vs, IReadOnlyList<Waypoint> wps)
|
||||
{
|
||||
var (px, py, pz) = RouteGeometry.PositionAt(wps, arc);
|
||||
float D = MathF.Sqrt((px - unit.X) * (px - unit.X) + (pz - unit.Z) * (pz - unit.Z));
|
||||
float dy = py - unit.Y;
|
||||
float sinA = (dy + 0.5f * 9.81f * ts * ts) / (vs * ts);
|
||||
float cosA = D / (vs * ts);
|
||||
return MathF.Atan2(sinA, cosA);
|
||||
}
|
||||
|
||||
// +0.001f 确保 ts > 0,否则 F() 返回 MaxValue 导致搜索失败
|
||||
float lo = detectArc + vd * reactionTime + 0.001f;
|
||||
if (lo >= totalArc) return (0, 0, 0);
|
||||
|
||||
float fLo = F(lo);
|
||||
if (fLo >= float.MaxValue - 1) return (0, 0, 0);
|
||||
|
||||
float step = (totalArc - lo) / 100f;
|
||||
if (step < 0.001f) step = 0.001f;
|
||||
float hi = lo + step;
|
||||
float fHi = 0;
|
||||
while (hi <= totalArc)
|
||||
{
|
||||
fHi = F(hi);
|
||||
if (fHi >= float.MaxValue - 1 || fHi <= 0) break;
|
||||
lo = hi; fLo = fHi;
|
||||
hi += step;
|
||||
}
|
||||
if (hi > totalArc || fHi >= float.MaxValue - 1) return (0, 0, 0);
|
||||
if (fHi > 0 && fLo > 0) return (0, 0, 0);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
float mid = (lo + hi) / 2f;
|
||||
float fMid = F(mid);
|
||||
if (MathF.Abs(fMid) < 0.001f || hi - lo < 0.001f)
|
||||
{
|
||||
float ts = (mid - detectArc) / vd - reactionTime;
|
||||
return (mid, ts, Angle(fireUnitPos, mid, ts, muzzleVelocity, r));
|
||||
}
|
||||
if (fLo <= 0 && fMid >= 0 || fLo >= 0 && fMid <= 0) { hi = mid; fHi = fMid; }
|
||||
else { lo = mid; fLo = fMid; }
|
||||
}
|
||||
|
||||
float final = (lo + hi) / 2f;
|
||||
float finalTs = (final - detectArc) / vd - reactionTime;
|
||||
return (final, finalTs, Angle(fireUnitPos, final, finalTs, muzzleVelocity, r));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,32 +27,88 @@ namespace CounterDrone.Core.Algorithms
|
||||
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
|
||||
}
|
||||
|
||||
/// <summary>计算抛物线炮弹的发射角</summary>
|
||||
/// <param name="range">水平距离 (m)</param>
|
||||
/// <param name="muzzleVelocity">初速 (m/s)</param>
|
||||
/// <param name="releaseAltitude">释放高度 (m),弹道顶点必须 ≥ 此值</param>
|
||||
/// <returns>发射角 (rad),取低弹道</returns>
|
||||
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
|
||||
/// <summary>给定水平距离和初速,求解抛物线发射角(逆问题)</summary>
|
||||
/// <param name="range">水平距离 (m),必须 > 0</param>
|
||||
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
|
||||
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param>
|
||||
/// <returns>发射角 (rad),取低弹道解</returns>
|
||||
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float heightDiff)
|
||||
{
|
||||
var v2 = muzzleVelocity * muzzleVelocity;
|
||||
var g = 9.81f;
|
||||
|
||||
// 1. 射程所需角:θr = arcsin(R*g/v²) / 2
|
||||
var rangeRatio = range * g / v2;
|
||||
var angleRange = rangeRatio >= 1.0f
|
||||
? 45f * (float)Math.PI / 180f
|
||||
: (float)Math.Asin(rangeRatio) / 2f;
|
||||
|
||||
// 2. 释放高度所需最小角:H = v²*sin²(θ)/(2g) → sin(θ) = √(2gH)/v
|
||||
var sinMinHeight = (float)Math.Sqrt(2f * g * releaseAltitude * 1.05f) / muzzleVelocity;
|
||||
var angleHeight = sinMinHeight >= 1.0f
|
||||
? 90f * (float)Math.PI / 180f
|
||||
: (float)Math.Asin(sinMinHeight);
|
||||
|
||||
return Math.Max(angleRange, angleHeight);
|
||||
if (range <= 0)
|
||||
throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
var angle = ParabolicMotion.SolveAngle(range, heightDiff, muzzleVelocity, TrajectoryPreference.Low);
|
||||
if (!angle.HasValue)
|
||||
throw new ArgumentException(
|
||||
$"当前参数无法命中目标: range={range}, heightDiff={heightDiff}, v₀={muzzleVelocity}");
|
||||
return angle.Value;
|
||||
}
|
||||
|
||||
/// <summary>根据发射参数计算抛物线位置</summary>
|
||||
/// <summary>弹道飞行时间(秒)。水平距离 / 水平分速</summary>
|
||||
public static float ParabolicTimeOfFlight(float range, float launchAngle, float muzzleVelocity)
|
||||
{
|
||||
if (range <= 0)
|
||||
throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
|
||||
float cosAngle = (float)Math.Cos(launchAngle);
|
||||
return range / (muzzleVelocity * cosAngle);
|
||||
}
|
||||
|
||||
/// <summary>抛物线飞行时间:给定水平距离和目标高度差,计算弹道时间</summary>
|
||||
/// <remarks>等价于先调用 CalculateLaunchAngle 再调用 ParabolicTimeOfFlight</remarks>
|
||||
public static float ParabolicShellTime(float horizontalDist, float heightDiff, float muzzleVelocity)
|
||||
{
|
||||
float angle = CalculateLaunchAngle(horizontalDist, muzzleVelocity, heightDiff);
|
||||
return ParabolicTimeOfFlight(horizontalDist, angle, muzzleVelocity);
|
||||
}
|
||||
|
||||
/// <summary>给定初速和发射角,计算到达指定相对高度的水平射程和飞行时间(正问题)</summary>
|
||||
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
|
||||
/// <param name="launchAngle">发射角 (rad),必须 ∈ (0, π/2)</param>
|
||||
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param>
|
||||
/// <returns>(水平射程 m, 飞行时间 s),取下行段解</returns>
|
||||
public static (float range, float timeOfFlight) ComputeParabolicRange(
|
||||
float muzzleVelocity, float launchAngle, float heightDiff)
|
||||
{
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2)
|
||||
throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
|
||||
var motion = new ParabolicMotion(muzzleVelocity, launchAngle);
|
||||
var result = motion.ComputeRange(heightDiff, TrajectoryPreference.Nearest);
|
||||
if (!result.HasValue)
|
||||
throw new ArgumentException(
|
||||
$"当前参数无法达到目标高度: heightDiff={heightDiff}, v₀={muzzleVelocity}, θ={launchAngle * 180 / Math.PI:F1}°");
|
||||
return result.Value;
|
||||
}
|
||||
|
||||
/// <summary>计算弹道顶点(最大高度和到达时间),相对发射点</summary>
|
||||
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
|
||||
/// <param name="launchAngle">发射角 (rad),必须 ∈ [0, π/2)</param>
|
||||
/// <returns>(顶点高度 m, 到达顶点时间 s),高度是相对发射点的增量</returns>
|
||||
public static (float apexHeight, float apexTime) ParabolicApex(
|
||||
float muzzleVelocity, float launchAngle)
|
||||
{
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2)
|
||||
throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
|
||||
|
||||
float g = 9.81f;
|
||||
float sinA = (float)Math.Sin(launchAngle);
|
||||
if (sinA <= 0) return (0, 0); // 水平或俯射,无上升顶点
|
||||
float vy = muzzleVelocity * sinA;
|
||||
float apexTime = vy / g;
|
||||
float apexHeight = vy * vy / (2f * g);
|
||||
return (apexHeight, apexTime);
|
||||
}
|
||||
|
||||
/// <summary>给定初速、发射角和方位角,计算任意时刻的抛物线位置(正问题核心)</summary>
|
||||
/// <param name="launchAngle">发射角 (rad),水平面以上为正</param>
|
||||
/// <param name="azimuth">方位角 (rad),0=N(+Z), π/2=E(+X)</param>
|
||||
public static (float X, float Y, float Z) ParabolicPosition(
|
||||
float startX, float startY, float startZ,
|
||||
float launchAngle, float azimuth,
|
||||
@ -70,6 +126,60 @@ namespace CounterDrone.Core.Algorithms
|
||||
return (x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>空投弹药位置:继承载机速度 + 重力下落</summary>
|
||||
/// <param name="startX,startY,startZ">投放点坐标</param>
|
||||
/// <param name="carrierVelX,carrierVelY,carrierVelZ">载机速度矢量 (m/s)</param>
|
||||
/// <param name="time">经过时间 (s)</param>
|
||||
public static (float X, float Y, float Z) AirDropPosition(
|
||||
float startX, float startY, float startZ,
|
||||
float carrierVelX, float carrierVelY, float carrierVelZ,
|
||||
float time)
|
||||
{
|
||||
const float g = 9.81f;
|
||||
var x = startX + carrierVelX * time;
|
||||
var y = startY + carrierVelY * time - 0.5f * g * time * time;
|
||||
var z = startZ + carrierVelZ * time;
|
||||
return (x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>空投降落时间:从释放高度下落到目标高度所需时间</summary>
|
||||
/// <param name="releaseAlt">释放高度 (m)</param>
|
||||
/// <param name="targetAlt">目标高度 (m)</param>
|
||||
/// <param name="carrierVelY">载机垂直速度 (m/s),正=向上,默认 0(平飞)</param>
|
||||
public static float AirDropFallTime(float releaseAlt, float targetAlt, float carrierVelY = 0f)
|
||||
{
|
||||
const float g = 9.81f;
|
||||
var dy = releaseAlt - targetAlt;
|
||||
if (dy <= 0) return 0f;
|
||||
return ((float)Math.Sqrt(carrierVelY * carrierVelY + 2f * g * dy) - carrierVelY) / g;
|
||||
}
|
||||
|
||||
/// <summary>两点间方向的速度矢量:normalize(to - from) × speed</summary>
|
||||
public static (float X, float Y, float Z) DirectionVelocity(
|
||||
float fromX, float fromY, float fromZ,
|
||||
float toX, float toY, float toZ,
|
||||
float speed)
|
||||
{
|
||||
float dx = toX - fromX;
|
||||
float dy = toY - fromY;
|
||||
float dz = toZ - fromZ;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist < 0.01f) return (0, 0, 0);
|
||||
float s = speed / dist;
|
||||
return (dx * s, dy * s, dz * s);
|
||||
}
|
||||
|
||||
/// <summary>云团覆盖间隔:无人机穿越单个云团的时间 = 云团直径 / 无人机速度</summary>
|
||||
/// <param name="cloudDiameter">云团有效直径 m</param>
|
||||
/// <param name="targetSpeedKmh">目标速度 km/h</param>
|
||||
/// <param name="minInterval">硬件最小间隔 s</param>
|
||||
public static float CloudCoverInterval(float cloudDiameter, float targetSpeedKmh, float minInterval = 0.1f)
|
||||
{
|
||||
float speedMs = targetSpeedKmh / 3.6f;
|
||||
if (speedMs <= 0.1f) return minInterval;
|
||||
return Math.Max(minInterval, cloudDiameter / speedMs);
|
||||
}
|
||||
|
||||
/// <summary>点到矩形距离(简化判定)</summary>
|
||||
public static float Distance2D(float x1, float z1, float x2, float z2)
|
||||
{
|
||||
@ -78,6 +188,18 @@ namespace CounterDrone.Core.Algorithms
|
||||
return (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
/// <summary>匀速直线运动从一点到另一点的飞行时间(秒)。
|
||||
/// 物理模型:无人机/平台沿直线匀速飞行,时间 = 距离 / 速度。
|
||||
/// speedKmh 为 0 时抛异常(速度必须 > 0,由调用方保证)。</summary>
|
||||
/// <param name="speedKmh">速度 km/h(与 TypicalSpeed 单位一致)</param>
|
||||
public static float TravelTime(float fromX, float fromZ, float toX, float toZ, float speedKmh)
|
||||
{
|
||||
if (speedKmh <= 0)
|
||||
throw new ArgumentException("速度必须 > 0", nameof(speedKmh));
|
||||
float dist = Distance2D(fromX, fromZ, toX, toZ);
|
||||
return dist / (speedKmh / 3.6f);
|
||||
}
|
||||
|
||||
/// <summary>点到三维点距离</summary>
|
||||
public static float Distance3D(float x1, float y1, float z1, float x2, float y2, float z2)
|
||||
{
|
||||
@ -136,16 +258,6 @@ namespace CounterDrone.Core.Algorithms
|
||||
var denom = (float)Math.Pow(2f * (float)Math.PI, 1.5f) * sigmaY * sigmaY * sigmaZ;
|
||||
return denom > 0.001f ? sourceStrength / denom : 0f;
|
||||
}
|
||||
|
||||
/// <summary>高斯烟团某点浓度 C(x,y,z) — 简化:相对中心的偏移</summary>
|
||||
public static float GaussianConcentration(float q, float sx, float sy, float sz, float offsetY, float offsetZ)
|
||||
{
|
||||
var norm = q / ((float)Math.Pow(2f * (float)Math.PI, 1.5f) * sx * sy * sz);
|
||||
var ey = (float)Math.Exp(-0.5f * offsetY * offsetY / (sy * sy));
|
||||
var ez = (float)Math.Exp(-0.5f * offsetZ * offsetZ / (sz * sz));
|
||||
var ezr = (float)Math.Exp(-0.5f * offsetZ * offsetZ / (sz * sz));
|
||||
return norm * ey * (ez + ezr);
|
||||
}
|
||||
public static bool PointInPolygon(float px, float pz, ReadOnlySpan<(float X, float Z)> vertices)
|
||||
{
|
||||
if (vertices.Length < 3) return false;
|
||||
|
||||
109
src/CounterDrone.Core/Algorithms/ParabolicMotion.cs
Normal file
109
src/CounterDrone.Core/Algorithms/ParabolicMotion.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>轨迹偏好(多解选择)</summary>
|
||||
public enum TrajectoryPreference
|
||||
{
|
||||
High, // 高抛(较大仰角 / 较晚到达目标高度)
|
||||
Low, // 低抛(较小仰角 / 较早到达目标高度)
|
||||
Nearest, // 最短飞行时间(最小正时间)
|
||||
Farthest // 最长飞行时间(最大正时间)
|
||||
}
|
||||
|
||||
/// <summary>抛物线运动学核心模块</summary>
|
||||
public class ParabolicMotion
|
||||
{
|
||||
public readonly float V0, Theta, Y0, X0;
|
||||
public readonly float Vx, Vy;
|
||||
public const float G = 9.81f;
|
||||
|
||||
public ParabolicMotion(float v0, float theta, float y0 = 0, float x0 = 0)
|
||||
{
|
||||
V0 = v0; Theta = theta; Y0 = y0; X0 = x0;
|
||||
Vx = v0 * (float)Math.Cos(theta);
|
||||
Vy = v0 * (float)Math.Sin(theta);
|
||||
}
|
||||
|
||||
/// <summary>任意时刻 t 的状态</summary>
|
||||
public (float x, float y, float vx, float vy) GetState(float t)
|
||||
{
|
||||
return (X0 + Vx * t, Y0 + Vy * t - 0.5f * G * t * t, Vx, Vy - G * t);
|
||||
}
|
||||
|
||||
/// <summary>到达目标高度 yTarget 的所有正时间解(0/1/2 个)</summary>
|
||||
public float[] GetFlightTimes(float yTarget)
|
||||
{
|
||||
float a = 0.5f * G;
|
||||
float b = -Vy;
|
||||
float c = yTarget - Y0;
|
||||
float d = b * b - 4f * a * c;
|
||||
if (d < 0) return Array.Empty<float>();
|
||||
|
||||
float sqrtD = (float)Math.Sqrt(d);
|
||||
float t1 = (-b - sqrtD) / (2f * a);
|
||||
float t2 = (-b + sqrtD) / (2f * a);
|
||||
|
||||
if (t1 > 0 && t2 > 0)
|
||||
return t1 < t2 ? new[] { t1, t2 } : new[] { t2, t1 };
|
||||
if (t1 > 0) return new[] { t1 };
|
||||
if (t2 > 0) return new[] { t2 };
|
||||
return Array.Empty<float>();
|
||||
}
|
||||
|
||||
/// <summary>根据偏好选择到达目标高度的飞行时间</summary>
|
||||
public float? GetFlightTime(float yTarget, TrajectoryPreference pref)
|
||||
{
|
||||
var ts = GetFlightTimes(yTarget);
|
||||
if (ts.Length == 0) return null;
|
||||
if (ts.Length == 1) return ts[0];
|
||||
return pref switch
|
||||
{
|
||||
TrajectoryPreference.Nearest or TrajectoryPreference.Low => ts[0],
|
||||
TrajectoryPreference.Farthest or TrajectoryPreference.High => ts[1],
|
||||
_ => ts[0]
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>根据偏好获取水平和飞行时间</summary>
|
||||
public (float range, float timeOfFlight)? ComputeRange(float yTarget, TrajectoryPreference pref)
|
||||
{
|
||||
float? t = GetFlightTime(yTarget, pref);
|
||||
if (!t.HasValue) return null;
|
||||
return (Vx * t.Value, t.Value);
|
||||
}
|
||||
|
||||
// ═══ 逆问题:求解瞄准角度 ═══
|
||||
|
||||
/// <summary>已知水平距离、高度差、初速,求解两个可能的发射角 (rad)</summary>
|
||||
public static (float theta1, float theta2)? SolveAngles(float range, float heightDiff, float v0)
|
||||
{
|
||||
if (range <= 0 || v0 <= 0) return null;
|
||||
float v2 = v0 * v0;
|
||||
float A = (G * range * range) / (2f * v2);
|
||||
float B = -range;
|
||||
float C = heightDiff + A;
|
||||
float d = B * B - 4f * A * C;
|
||||
if (d < 0 || A == 0) return null;
|
||||
float sqrtD = (float)Math.Sqrt(d);
|
||||
float p1 = (-B + sqrtD) / (2f * A);
|
||||
float p2 = (-B - sqrtD) / (2f * A);
|
||||
return ((float)Math.Atan(p1), (float)Math.Atan(p2));
|
||||
}
|
||||
|
||||
/// <summary>根据偏好选择一个瞄准角度</summary>
|
||||
public static float? SolveAngle(float range, float heightDiff, float v0, TrajectoryPreference pref)
|
||||
{
|
||||
var result = SolveAngles(range, heightDiff, v0);
|
||||
if (!result.HasValue) return null;
|
||||
var (a1, a2) = result.Value;
|
||||
float high = Math.Max(a1, a2);
|
||||
float low = Math.Min(a1, a2);
|
||||
return pref switch
|
||||
{
|
||||
TrajectoryPreference.High => high,
|
||||
TrajectoryPreference.Low or TrajectoryPreference.Nearest or _ => low
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/CounterDrone.Core/Algorithms/PlannerConfig.cs
Normal file
89
src/CounterDrone.Core/Algorithms/PlannerConfig.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>防御规划器配置 — 全局策略参数,从 planner_config.json 加载。
|
||||
/// 代码零默认值,所有字段必须从配置文件读取;文件缺失或字段缺失即抛异常。</summary>
|
||||
public class PlannerConfig
|
||||
{
|
||||
/// <summary>云团重叠比例(0=相切,0.2=重叠 20%)。间距 = 2R × (1 − 重叠比例)。</summary>
|
||||
public float CloudOverlapRatio { get; set; }
|
||||
|
||||
/// <summary>临界方案概率阈值(DeriveCritical 用)</summary>
|
||||
public float CriticalProbabilityThreshold { get; set; }
|
||||
|
||||
/// <summary>拦截概率上限(封顶值)</summary>
|
||||
public float MaxInterceptProbability { get; set; }
|
||||
|
||||
/// <summary>威胁类型系数表(DroneType → 系数)</summary>
|
||||
public Dictionary<DroneType, float> TypeCoefficient { get; set; } = new();
|
||||
|
||||
/// <summary>弹药匹配表(PowerType → AerosolType)</summary>
|
||||
public Dictionary<PowerType, AerosolType> AmmoMatch { get; set; } = new();
|
||||
|
||||
/// <summary>无探测设备时的默认探测精度 m(回退值,上帝视角但有标称误差)</summary>
|
||||
public float DefaultDetectionAccuracy { get; set; }
|
||||
|
||||
/// <summary>云团有效膨胀系数(0~1)。planner 取 Phase 2 膨胀时间的此比例作为有效云团年龄。默认 0.9</summary>
|
||||
public float ExpansionFactor { get; set; } = 0.9f;
|
||||
|
||||
/// <summary>拦截窗口安全余量(s)。计算所需探测弧长时在膨胀时间基础上额外预留。默认 1</summary>
|
||||
public float TimingSafetyMargin { get; set; } = 1f;
|
||||
|
||||
/// <summary>火力单元反应时间(s)。探测到目标后装填+瞄准的时间。默认 5</summary>
|
||||
public float ReactionTime { get; set; } = 5f;
|
||||
|
||||
private const string ConfigFileName = "planner_config.json";
|
||||
|
||||
/// <summary>从 dataRoot 加载配置。文件缺失或字段非法即抛异常。</summary>
|
||||
public static PlannerConfig Load(string dataRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dataRoot))
|
||||
throw new ArgumentException("dataRoot 不能为空", nameof(dataRoot));
|
||||
string path = Path.Combine(dataRoot, ConfigFileName);
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"planner 配置文件不存在: {path}");
|
||||
|
||||
string json = File.ReadAllText(path);
|
||||
var config = JsonSerializer.Deserialize<PlannerConfig>(json, JsonOptions)
|
||||
?? throw new InvalidDataException($"planner 配置解析失败: {path}");
|
||||
config.Validate();
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>从 IPathProvider 加载(便捷重载)</summary>
|
||||
public static PlannerConfig Load(IPathProvider paths)
|
||||
=> Load(paths?.GetDataRoot() ?? throw new ArgumentNullException(nameof(paths)));
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
if (CloudOverlapRatio < 0f || CloudOverlapRatio >= 1f)
|
||||
throw new InvalidDataException($"CloudOverlapRatio 必须在 [0, 1),实际 {CloudOverlapRatio}");
|
||||
if (CriticalProbabilityThreshold <= 0f || CriticalProbabilityThreshold >= 1f)
|
||||
throw new InvalidDataException($"CriticalProbabilityThreshold 必须在 (0, 1),实际 {CriticalProbabilityThreshold}");
|
||||
if (MaxInterceptProbability <= 0f || MaxInterceptProbability > 1f)
|
||||
throw new InvalidDataException($"MaxInterceptProbability 必须在 (0, 1],实际 {MaxInterceptProbability}");
|
||||
if (TypeCoefficient == null || TypeCoefficient.Count == 0)
|
||||
throw new InvalidDataException("TypeCoefficient 不能为空");
|
||||
if (AmmoMatch == null || AmmoMatch.Count == 0)
|
||||
throw new InvalidDataException("AmmoMatch 不能为空");
|
||||
if (DefaultDetectionAccuracy < 0f)
|
||||
throw new InvalidDataException($"DefaultDetectionAccuracy 必须 >= 0,实际 {DefaultDetectionAccuracy}");
|
||||
if (ExpansionFactor <= 0f || ExpansionFactor > 1f)
|
||||
throw new InvalidDataException($"ExpansionFactor 必须在 (0, 1],实际 {ExpansionFactor}");
|
||||
if (TimingSafetyMargin < 0f)
|
||||
throw new InvalidDataException($"TimingSafetyMargin 必须 >= 0,实际 {TimingSafetyMargin}");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
132
src/CounterDrone.Core/Algorithms/RouteGeometry.cs
Normal file
132
src/CounterDrone.Core/Algorithms/RouteGeometry.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>航路几何工具——对 waypoint 序列做纯几何运算。
|
||||
/// 无状态、纯函数,与 Kinematics/DamageAssessment 同范式。
|
||||
/// planner(预测)和 DroneEntity(执行)共用,确保两边航路模型一致。</summary>
|
||||
public static class RouteGeometry
|
||||
{
|
||||
/// <summary>航路总弧长(米):所有相邻 waypoint 三维距离之和。</summary>
|
||||
public static float TotalLength(IReadOnlyList<Waypoint> wps)
|
||||
{
|
||||
if (wps == null || wps.Count < 2) return 0f;
|
||||
float total = 0f;
|
||||
for (int i = 0; i < wps.Count - 1; i++)
|
||||
total += Kinematics.Distance3D(
|
||||
(float)wps[i].PosX, (float)wps[i].PosY, (float)wps[i].PosZ,
|
||||
(float)wps[i + 1].PosX, (float)wps[i + 1].PosY, (float)wps[i + 1].PosZ);
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>沿航路弧长 s 处的三维位置(分段线性插值)。
|
||||
/// s≤0 返回起点,s≥TotalLength 返回终点。</summary>
|
||||
public static (float X, float Y, float Z) PositionAt(IReadOnlyList<Waypoint> wps, float arcLength)
|
||||
{
|
||||
if (wps == null || wps.Count == 0) return (0f, 0f, 0f);
|
||||
if (wps.Count == 1 || arcLength <= 0f)
|
||||
return ((float)wps[0].PosX, (float)wps[0].PosY, (float)wps[0].PosZ);
|
||||
|
||||
float remaining = arcLength;
|
||||
for (int i = 0; i < wps.Count - 1; i++)
|
||||
{
|
||||
float segLen = Kinematics.Distance3D(
|
||||
(float)wps[i].PosX, (float)wps[i].PosY, (float)wps[i].PosZ,
|
||||
(float)wps[i + 1].PosX, (float)wps[i + 1].PosY, (float)wps[i + 1].PosZ);
|
||||
if (remaining <= segLen)
|
||||
{
|
||||
float t = segLen > 0.0001f ? remaining / segLen : 0f;
|
||||
return (
|
||||
(float)(wps[i].PosX + (wps[i + 1].PosX - wps[i].PosX) * t),
|
||||
(float)(wps[i].PosY + (wps[i + 1].PosY - wps[i].PosY) * t),
|
||||
(float)(wps[i].PosZ + (wps[i + 1].PosZ - wps[i].PosZ) * t));
|
||||
}
|
||||
remaining -= segLen;
|
||||
}
|
||||
// 超过总弧长,返回终点
|
||||
var last = wps[^1];
|
||||
return ((float)last.PosX, (float)last.PosY, (float)last.PosZ);
|
||||
}
|
||||
|
||||
/// <summary>航路上离目标点 (x,z) 最近的点对应的弧长(水平投影最近)。
|
||||
/// 用于 planner 定位"无人机穿越点在航路上的弧长位置"。
|
||||
/// 算法:逐段求点到线段的最近点,取全局最近者。</summary>
|
||||
public static float ArcLengthNearestTo(IReadOnlyList<Waypoint> wps, float x, float z)
|
||||
{
|
||||
if (wps == null || wps.Count < 2) return 0f;
|
||||
|
||||
float bestArc = 0f;
|
||||
float bestDist = float.MaxValue;
|
||||
float accumArc = 0f;
|
||||
|
||||
for (int i = 0; i < wps.Count - 1; i++)
|
||||
{
|
||||
float ax = (float)wps[i].PosX, az = (float)wps[i].PosZ;
|
||||
float bx = (float)wps[i + 1].PosX, bz = (float)wps[i + 1].PosZ;
|
||||
float segDx = bx - ax, segDz = bz - az;
|
||||
float segLenSq = segDx * segDx + segDz * segDz;
|
||||
|
||||
float t = 0f;
|
||||
if (segLenSq > 0.0001f)
|
||||
{
|
||||
t = ((x - ax) * segDx + (z - az) * segDz) / segLenSq;
|
||||
t = Math.Max(0f, Math.Min(1f, t));
|
||||
}
|
||||
float projX = ax + segDx * t;
|
||||
float projZ = az + segDz * t;
|
||||
float distSq = (projX - x) * (projX - x) + (projZ - z) * (projZ - z);
|
||||
|
||||
if (distSq < bestDist)
|
||||
{
|
||||
bestDist = distSq;
|
||||
float segLen = (float)Math.Sqrt(segLenSq);
|
||||
bestArc = accumArc + t * segLen;
|
||||
}
|
||||
accumArc += (float)Math.Sqrt(segLenSq);
|
||||
}
|
||||
return bestArc;
|
||||
}
|
||||
|
||||
/// <summary>从航路起点匀速运动到弧长 s 处的飞行时间(秒)。
|
||||
/// 匀速直线模型,时间 = 弧长 / 速度。内部调用 Kinematics.TravelTime。</summary>
|
||||
/// <param name="speedKmh">速度 km/h(与 TypicalSpeed 单位一致),必须 > 0</param>
|
||||
public static float TravelTimeTo(IReadOnlyList<Waypoint> wps, float arcLength, float speedKmh)
|
||||
{
|
||||
if (speedKmh <= 0)
|
||||
throw new ArgumentException("速度必须 > 0", nameof(speedKmh));
|
||||
// 弧长 = 距离,匀速直线时间 = 距离/速度
|
||||
// 用 Kinematics.TravelTime 保持单位换算唯一(km/h → m/s 在 Kinematics 内)
|
||||
return arcLength / (speedKmh / 3.6f);
|
||||
}
|
||||
|
||||
/// <summary>沿航路方向给定弧长 s 处的水平单位切向量 (DirX, DirZ)。
|
||||
/// 用于 planner 沿航路方向布云(offset 沿切向,不再写死 X 轴)。
|
||||
/// 弧长超出范围时取末段方向。</summary>
|
||||
public static (float DirX, float DirZ) TangentAt(IReadOnlyList<Waypoint> wps, float arcLength)
|
||||
{
|
||||
if (wps == null || wps.Count < 2) return (1f, 0f);
|
||||
|
||||
if (arcLength < 0) arcLength = 0;
|
||||
float remaining = arcLength;
|
||||
for (int i = 0; i < wps.Count - 1; i++)
|
||||
{
|
||||
float dx = (float)wps[i + 1].PosX - (float)wps[i].PosX;
|
||||
float dz = (float)wps[i + 1].PosZ - (float)wps[i].PosZ;
|
||||
float segLen = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
if (remaining <= segLen || i == wps.Count - 2)
|
||||
{
|
||||
if (segLen < 0.0001f) return (1f, 0f);
|
||||
return (dx / segLen, dz / segLen);
|
||||
}
|
||||
remaining -= segLen;
|
||||
}
|
||||
// 兜底:末段方向
|
||||
float ldx = (float)wps[^1].PosX - (float)wps[^2].PosX;
|
||||
float ldz = (float)wps[^1].PosZ - (float)wps[^2].PosZ;
|
||||
float llen = (float)Math.Sqrt(ldx * ldx + ldz * ldz);
|
||||
return llen < 0.0001f ? (1f, 0f) : (ldx / llen, ldz / llen);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/CounterDrone.Core/Algorithms/Vector3.cs
Normal file
9
src/CounterDrone.Core/Algorithms/Vector3.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>三维向量(纯 C#,不依赖 UnityEngine)</summary>
|
||||
public struct Vector3
|
||||
{
|
||||
public float X, Y, Z;
|
||||
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>CounterDrone.Core.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PdfSharpCore" Version="1.3.64" />
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -3,6 +3,8 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using CounterDrone.Core.Services;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core
|
||||
@ -24,63 +26,91 @@ namespace CounterDrone.Core
|
||||
var db = new SQLiteConnection(dbPath);
|
||||
CreateMainTables(db);
|
||||
SeedDefaultData(db);
|
||||
SeedDefaultScenarios(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
private void SeedDefaultData(SQLiteConnection db)
|
||||
{
|
||||
if (db.Table<AmmunitionSpec>().Count() > 0) return;
|
||||
if (db.Table<AmmunitionSpec>().Count() > 0)
|
||||
return;
|
||||
|
||||
var jsonPath = Path.Combine(_paths.GetDataRoot(), "default_ammo.json");
|
||||
if (!File.Exists(jsonPath)) return;
|
||||
var defaults = DefaultData.Load(_paths);
|
||||
foreach (var a in defaults.Ammunition) db.InsertOrReplace(a);
|
||||
foreach (var f in defaults.FireUnits) db.InsertOrReplace(f);
|
||||
foreach (var l in defaults.LaunchPlatforms) db.InsertOrReplace(l);
|
||||
foreach (var d in defaults.Drones) db.InsertOrReplace(d);
|
||||
foreach (var s in defaults.Sensors) db.InsertOrReplace(s);
|
||||
foreach (var e in defaults.Environments) db.InsertOrReplace(e);
|
||||
foreach (var f in defaults.Formations) db.InsertOrReplace(f);
|
||||
foreach (var r in defaults.Routes)
|
||||
{
|
||||
r.WaypointsJson = System.Text.Json.JsonSerializer.Serialize(r.Waypoints);
|
||||
db.InsertOrReplace(r);
|
||||
}
|
||||
}
|
||||
|
||||
var specs = JsonSerializer.Deserialize<List<AmmunitionSpec>>(File.ReadAllText(jsonPath));
|
||||
if (specs != null)
|
||||
foreach (var s in specs) db.Insert(s);
|
||||
private void SeedDefaultScenarios(SQLiteConnection db)
|
||||
{
|
||||
if (db.Table<Scenario>().Count() > 0)
|
||||
return;
|
||||
|
||||
var defaults = DefaultData.Load(_paths);
|
||||
var scenario = new ScenarioService(
|
||||
new ScenarioRepository(db), new CombatSceneRepository(db),
|
||||
new ControlZoneRepository(db), new ScenarioDroneRepository(db),
|
||||
new ScenarioUnitRepository(db), new CloudDispersalRepository(db),
|
||||
new RoutePlanRepository(db), new WaypointRepository(db));
|
||||
|
||||
DefaultScenarios.Seed(scenario, defaults);
|
||||
}
|
||||
|
||||
private void CreateMainTables(SQLiteConnection db)
|
||||
{
|
||||
db.CreateTable<ModelInfo>();
|
||||
db.CreateTable<AmmunitionSpec>();
|
||||
db.CreateTable<SimTask>();
|
||||
db.CreateTable<Scenario>();
|
||||
db.CreateTable<CombatScene>();
|
||||
db.CreateTable<ControlZone>();
|
||||
db.CreateTable<TargetConfig>();
|
||||
db.CreateTable<EquipmentDeployment>();
|
||||
db.CreateTable<ScenarioDrone>();
|
||||
db.CreateTable<ScenarioUnit>();
|
||||
db.CreateTable<CloudDispersal>();
|
||||
db.CreateTable<RoutePlan>();
|
||||
db.CreateIndex("RoutePlan", new[] { "TaskId", "GroupId" }, true);
|
||||
db.CreateIndex("RoutePlan", new[] { "ScenarioId", "WaveId" }, true);
|
||||
db.CreateTable<Waypoint>();
|
||||
db.CreateTable<Group>();
|
||||
db.CreateTable<SimulationReport>();
|
||||
db.CreateTable<SimEvent>();
|
||||
db.CreateTable<FireUnitSpec>();
|
||||
db.CreateTable<LaunchPlatformSpec>();
|
||||
db.CreateTable<DroneSpec>();
|
||||
db.CreateTable<SensorSpec>();
|
||||
db.CreateTable<EnvironmentSpec>();
|
||||
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("SimEvent", "TaskId");
|
||||
db.CreateIndex("SimulationReport", "TaskId");
|
||||
db.CreateIndex("Scenario", "ScenarioNumber");
|
||||
db.CreateIndex("ControlZone", "ScenarioId");
|
||||
db.CreateIndex("ScenarioDrone", "ScenarioId");
|
||||
db.CreateIndex("ScenarioUnit", "ScenarioId");
|
||||
db.CreateIndex("Waypoint", "ScenarioId");
|
||||
db.CreateIndex("SimulationReport", "ScenarioId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
src/CounterDrone.Core/DefaultData.cs
Normal file
82
src/CounterDrone.Core/DefaultData.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 统一默认数据 — 弹药、编队、火力单元、无人机、探测设备、天气的预设库。
|
||||
/// 所有默认数据集中在 data/defaults.json,通过本类统一加载。
|
||||
/// </summary>
|
||||
public class DefaultData
|
||||
{
|
||||
public string Version { get; set; } = "0";
|
||||
public List<AmmunitionSpec> Ammunition { get; set; } = new();
|
||||
public List<FormationTemplate> Formations { get; set; } = new();
|
||||
public List<RouteTemplate> Routes { get; set; } = new();
|
||||
[System.Text.Json.Serialization.JsonPropertyName("fireUnits")]
|
||||
public List<FireUnitSpec> FireUnits { get; set; } = new();
|
||||
[System.Text.Json.Serialization.JsonPropertyName("launchPlatforms")]
|
||||
public List<LaunchPlatformSpec> LaunchPlatforms { get; set; } = new();
|
||||
[System.Text.Json.Serialization.JsonPropertyName("drones")]
|
||||
public List<DroneSpec> Drones { get; set; } = new();
|
||||
[System.Text.Json.Serialization.JsonPropertyName("detectionEquipment")]
|
||||
public List<SensorSpec> Sensors { get; set; } = new();
|
||||
[System.Text.Json.Serialization.JsonPropertyName("weather")]
|
||||
public List<EnvironmentSpec> Environments { get; set; } = new();
|
||||
|
||||
public static DefaultData Load(IPathProvider paths)
|
||||
{
|
||||
var jsonPath = Path.Combine(paths.GetDataRoot(), "defaults.json");
|
||||
if (!File.Exists(jsonPath))
|
||||
throw new FileNotFoundException($"默认数据文件未找到: {jsonPath}");
|
||||
|
||||
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var data = JsonSerializer.Deserialize<DefaultData>(File.ReadAllText(jsonPath), opts)
|
||||
?? throw new InvalidOperationException($"默认数据文件解析失败: {jsonPath}");
|
||||
|
||||
Validate(data, jsonPath);
|
||||
return data;
|
||||
}
|
||||
|
||||
public static DefaultData FromJson(string json)
|
||||
{
|
||||
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var data = JsonSerializer.Deserialize<DefaultData>(json, opts)
|
||||
?? throw new InvalidOperationException("默认数据 JSON 解析失败");
|
||||
Validate(data, "<string>");
|
||||
return data;
|
||||
}
|
||||
|
||||
private static void Validate(DefaultData data, string source)
|
||||
{
|
||||
if (data.Ammunition == null || data.Ammunition.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 ammunition: {source}");
|
||||
if (data.Formations == null || data.Formations.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 formations: {source}");
|
||||
if (data.Routes == null || data.Routes.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 routes: {source}");
|
||||
if (data.FireUnits == null || data.FireUnits.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 fireUnits: {source}");
|
||||
if (data.LaunchPlatforms == null || data.LaunchPlatforms.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 launchPlatforms: {source}");
|
||||
if (data.Drones == null || data.Drones.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 drones: {source}");
|
||||
if (data.Environments == null || data.Environments.Count == 0)
|
||||
throw new InvalidOperationException($"默认数据缺少 environments: {source}");
|
||||
if (data.Sensors == null)
|
||||
data.Sensors = new();
|
||||
|
||||
// 由 Range 字段派生 HasRadar/HasEO/HasIR(JSON 可能未显式设置)
|
||||
foreach (var s in data.Sensors)
|
||||
{
|
||||
s.HasRadar = s.RadarRange > 0;
|
||||
s.HasEO = s.EORange > 0;
|
||||
s.HasIR = s.IRRange > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/CounterDrone.Core/DefaultScenarios.cs
Normal file
156
src/CounterDrone.Core/DefaultScenarios.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Services;
|
||||
|
||||
namespace CounterDrone.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 预设想定种子 — 从 DefaultData 预设组装完整想定,写入数据库供前端测试。
|
||||
/// 首次运行时自动种子,同名想定跳过(幂等)。
|
||||
/// </summary>
|
||||
public static class DefaultScenarios
|
||||
{
|
||||
public static void Seed(IScenarioService scenario, DefaultData d)
|
||||
{
|
||||
// 检测旧格式任务(名称含 [Demo],但 SQL LIKE 中 [] 是通配符,搜索 "Demo")
|
||||
var existing = scenario.SearchScenarios("Demo", null, null, 1, 100);
|
||||
if (existing.TotalCount > 0)
|
||||
{
|
||||
// 版本升级:删除旧 Demo 任务,重新创建
|
||||
foreach (var t in existing.Items)
|
||||
scenario.DeleteScenario(t.Id);
|
||||
}
|
||||
|
||||
SeedNoDefense(scenario, d);
|
||||
SeedZoneIntrusion(scenario, d);
|
||||
SeedPistonWindy(scenario, d);
|
||||
SeedJetActiveMaterial(scenario, d);
|
||||
SeedAirBasedWindy(scenario, d);
|
||||
Seed3DronesAirBased(scenario, d);
|
||||
}
|
||||
|
||||
private static List<Waypoint> R(string routeId, double speed, DefaultData d)
|
||||
=> d.Routes.First(r => r.Id == routeId).ToWaypoints(speed);
|
||||
|
||||
private static void SeedNoDefense(IScenarioService s, DefaultData d)
|
||||
{
|
||||
var t = s.CreateScenario("[Demo] 无防御-无人机抵达目标", "");
|
||||
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
|
||||
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "shahed").ToScenarioDrone());
|
||||
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<ScenarioUnit>());
|
||||
s.UpdateStep(t.Id, 5);
|
||||
}
|
||||
|
||||
private static void SeedZoneIntrusion(IScenarioService s, DefaultData d)
|
||||
{
|
||||
var t = s.CreateScenario("[Demo] 管控区域侵入", "");
|
||||
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
|
||||
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "electric-scout").ToScenarioDrone());
|
||||
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<ScenarioUnit>());
|
||||
s.SaveControlZones(t.Id, new List<ControlZone>
|
||||
{
|
||||
new ControlZone
|
||||
{
|
||||
Name = "禁飞区",
|
||||
VerticesJson = "[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]",
|
||||
MinAltitude = 0, MaxAltitude = 1000,
|
||||
},
|
||||
});
|
||||
s.UpdateStep(t.Id, 5);
|
||||
}
|
||||
|
||||
private static void SeedPistonWindy(IScenarioService s, DefaultData d)
|
||||
{
|
||||
var t = s.CreateScenario("[Demo] 活塞拦截-西风5ms", "");
|
||||
var scene = d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene();
|
||||
scene.WindSpeed = 5;
|
||||
scene.WindDirection = (int)WindDirection.W;
|
||||
s.SaveScene(t.Id, scene);
|
||||
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "shahed").ToScenarioDrone());
|
||||
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 },
|
||||
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
s.SaveDeployment(t.Id, new List<ScenarioUnit>
|
||||
{
|
||||
d.FireUnits.First(f => f.Id == "ground-light").ToScenarioUnit(AerosolType.InertGas, 1, 0, 0, 50),
|
||||
});
|
||||
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "radar-sr").ToScenarioUnit(0, 0, 50));
|
||||
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
s.UpdateStep(t.Id, 5);
|
||||
}
|
||||
|
||||
private static void SeedJetActiveMaterial(IScenarioService s, DefaultData d)
|
||||
{
|
||||
var t = s.CreateScenario("[Demo] 喷气式拦截-活性材料", "");
|
||||
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
|
||||
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "cruise-missile").ToScenarioDrone());
|
||||
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 },
|
||||
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
s.SaveDeployment(t.Id, new List<ScenarioUnit>
|
||||
{
|
||||
d.FireUnits.First(f => f.Id == "ground-standard").ToScenarioUnit(AerosolType.ActiveMaterial, 1, 0, 0, 50),
|
||||
});
|
||||
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "radar-mr").ToScenarioUnit(0, 0, 50));
|
||||
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 500 });
|
||||
s.UpdateStep(t.Id, 5);
|
||||
}
|
||||
|
||||
private static void SeedAirBasedWindy(IScenarioService s, DefaultData d)
|
||||
{
|
||||
var t = s.CreateScenario("[Demo] 空基拦截-东风5ms", "");
|
||||
var scene = d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene();
|
||||
scene.WindSpeed = 5;
|
||||
scene.WindDirection = (int)WindDirection.E;
|
||||
s.SaveScene(t.Id, scene);
|
||||
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "shahed").ToScenarioDrone());
|
||||
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 },
|
||||
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
s.SaveDeployment(t.Id, new List<ScenarioUnit>
|
||||
{
|
||||
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 3, 5000, 1000, 0),
|
||||
});
|
||||
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "eo-station").ToScenarioUnit(5000, 1000, 0));
|
||||
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
s.UpdateStep(t.Id, 5);
|
||||
}
|
||||
|
||||
private static void Seed3DronesAirBased(IScenarioService s, DefaultData d)
|
||||
{
|
||||
var t = s.CreateScenario("[Demo] 3架空基编队拦截", "");
|
||||
var scene = d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene();
|
||||
scene.WindSpeed = 5;
|
||||
scene.WindDirection = (int)WindDirection.E;
|
||||
s.SaveScene(t.Id, scene);
|
||||
var target = d.Drones.First(p => p.Id == "shahed").ToScenarioDrone();
|
||||
target.Quantity = 3;
|
||||
s.SaveScenarioDrone(t.Id, target);
|
||||
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(),
|
||||
new List<Waypoint> {
|
||||
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
});
|
||||
s.SaveDeployment(t.Id, new List<ScenarioUnit>
|
||||
{
|
||||
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 1, 5000, 1000, 0),
|
||||
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 1, 5300, 1000, 0),
|
||||
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 1, 5600, 1000, 0),
|
||||
});
|
||||
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "eo-station").ToScenarioUnit(5000, 1000, 0));
|
||||
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
s.UpdateStep(t.Id, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,5 +7,6 @@ namespace CounterDrone.Core
|
||||
string GetMainDbPath();
|
||||
string GetFramesDir();
|
||||
string GetModelsDir();
|
||||
string GetFontPath();
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,9 @@ namespace CounterDrone.Core.Models
|
||||
/// <summary>湍流扩散系数,Phase 2 中 R=R₀+k√t 的 k 值</summary>
|
||||
public double TurbulentExpansionK { get; set; } = 5.0;
|
||||
|
||||
/// <summary>Phase 2 湍流膨胀持续时间(s),之后进入 Phase 3 高斯扩散。默认 30s</summary>
|
||||
public double Phase2Duration { get; set; } = 30.0;
|
||||
|
||||
public double MaxRadius { get; set; }
|
||||
|
||||
public double MaxDuration { get; set; }
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
35
src/CounterDrone.Core/Models/DroneSpec.cs
Normal file
35
src/CounterDrone.Core/Models/DroneSpec.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>无人机规格(基础数据模板)</summary>
|
||||
[Table("DroneSpec")]
|
||||
public class DroneSpec
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int DroneType { get; set; }
|
||||
/// <summary>型号名称(非 3D 模型文件。3D 模型引用见 ModelId)</summary>
|
||||
public string Model { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public int PowerType { get; set; }
|
||||
public double Wingspan { get; set; }
|
||||
public double TypicalSpeed { get; set; }
|
||||
public double TypicalAltitude { get; set; }
|
||||
|
||||
/// <summary>3D 模型 ID(FK → ModelInfo),Unity 可视化用</summary>
|
||||
public string ModelId { get; set; } = "";
|
||||
|
||||
public ScenarioDrone ToScenarioDrone(string waveId = "default")
|
||||
{
|
||||
return new ScenarioDrone
|
||||
{
|
||||
DroneSpecId = Id,
|
||||
WaveId = waveId,
|
||||
Quantity = 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
// === 任务 ===
|
||||
public enum TaskStatus
|
||||
public enum ScenarioStatus
|
||||
{
|
||||
Draft = 0,
|
||||
Configuring = 1,
|
||||
@ -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
|
||||
@ -62,6 +60,13 @@ namespace CounterDrone.Core.Models
|
||||
LaunchPlatform = 1
|
||||
}
|
||||
|
||||
public enum SensorType
|
||||
{
|
||||
Radar = 0,
|
||||
EO = 1,
|
||||
IR = 2
|
||||
}
|
||||
|
||||
// === 气溶胶 & 毁伤 ===
|
||||
public enum AerosolType
|
||||
{
|
||||
@ -92,7 +97,7 @@ namespace CounterDrone.Core.Models
|
||||
Destroyed = 3
|
||||
}
|
||||
|
||||
// === 编队 & 编组 ===
|
||||
// === 编队 & 批次 ===
|
||||
public enum FormationMode
|
||||
{
|
||||
Single = 0,
|
||||
@ -100,12 +105,6 @@ namespace CounterDrone.Core.Models
|
||||
Swarm = 2
|
||||
}
|
||||
|
||||
public enum GroupType
|
||||
{
|
||||
DroneFleet = 0,
|
||||
EquipmentGroup = 1
|
||||
}
|
||||
|
||||
// === 运行时 ===
|
||||
public enum EntityType
|
||||
{
|
||||
@ -126,7 +125,8 @@ namespace CounterDrone.Core.Models
|
||||
DroneReachedTarget = 5,
|
||||
ZoneIntruded = 6,
|
||||
WaypointReached = 7,
|
||||
SimulationEnd = 8
|
||||
SimulationEnd = 8,
|
||||
PlanningFailed = 9
|
||||
}
|
||||
|
||||
// === 报告 ===
|
||||
|
||||
35
src/CounterDrone.Core/Models/EnvironmentSpec.cs
Normal file
35
src/CounterDrone.Core/Models/EnvironmentSpec.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>环境规格(基础数据模板)</summary>
|
||||
[Table("EnvironmentSpec")]
|
||||
public class EnvironmentSpec
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int WeatherType { get; set; }
|
||||
public double WindSpeed { get; set; }
|
||||
public int WindDirection { get; set; }
|
||||
public double Temperature { get; set; } = 20.0;
|
||||
public double Humidity { get; set; } = 60.0;
|
||||
public double Pressure { get; set; } = 1013.0;
|
||||
public double Visibility { get; set; } = 5000.0;
|
||||
|
||||
public CombatScene ToCombatScene()
|
||||
{
|
||||
return new CombatScene
|
||||
{
|
||||
WeatherType = WeatherType,
|
||||
WindSpeed = WindSpeed,
|
||||
WindDirection = WindDirection,
|
||||
Temperature = Temperature,
|
||||
Humidity = Humidity,
|
||||
Pressure = Pressure,
|
||||
Visibility = Visibility,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>步骤3:装备部署</summary>
|
||||
[Table("EquipmentDeployment")]
|
||||
public class EquipmentDeployment
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[Indexed]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform;
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
|
||||
// 发射平台专用
|
||||
public int? PlatformType { get; set; }
|
||||
|
||||
public double PositionX { get; set; }
|
||||
public double PositionY { get; set; }
|
||||
public double PositionZ { get; set; }
|
||||
|
||||
public int? AerosolType { get; set; }
|
||||
|
||||
public int? MunitionCount { get; set; }
|
||||
|
||||
public int Source { get; set; } = (int)ConfigSource.Manual;
|
||||
|
||||
public double? MuzzleVelocity { get; set; }
|
||||
|
||||
public double? ReleaseAltitude { get; set; }
|
||||
|
||||
public double Cooldown { get; set; } = 5.0;
|
||||
|
||||
// 探测设备专用
|
||||
public double? DetectionRadius { get; set; }
|
||||
}
|
||||
}
|
||||
52
src/CounterDrone.Core/Models/FireUnitSpec.cs
Normal file
52
src/CounterDrone.Core/Models/FireUnitSpec.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>火力单元规格(基础数据模板)</summary>
|
||||
[Table("FireUnitSpec")]
|
||||
public class FireUnitSpec
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int PlatformType { get; set; }
|
||||
public int GunCount { get; set; } = 1;
|
||||
public int ChannelsPerGun { get; set; } = 1;
|
||||
public double ChannelInterval { get; set; } = 1.0;
|
||||
public double Cooldown { get; set; } = 5.0;
|
||||
public double AmmoChangeTime { get; set; } = 30.0;
|
||||
public double MuzzleVelocity { get; set; }
|
||||
public double CruiseSpeed { get; set; }
|
||||
public double ReleaseAltitude { get; set; }
|
||||
|
||||
/// <summary>3D 模型 ID(FK → ModelInfo),Unity 可视化用</summary>
|
||||
public string ModelId { get; set; } = "";
|
||||
[Ignore]
|
||||
public List<int> AmmoTypes { get; set; } = new();
|
||||
public double RadarRange { get; set; }
|
||||
public double EORange { get; set; }
|
||||
public double IRRange { get; set; }
|
||||
public double? MinElevation { get; set; }
|
||||
public double? MaxElevation { get; set; }
|
||||
public double? MinDetectAlt { get; set; }
|
||||
public double? MaxDetectAlt { get; set; }
|
||||
|
||||
public ScenarioUnit ToScenarioUnit(AerosolType ammoType, int quantity,
|
||||
double posX, double posY, double posZ)
|
||||
{
|
||||
return new ScenarioUnit
|
||||
{
|
||||
LaunchPlatformSpecId = Id,
|
||||
EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform,
|
||||
Quantity = quantity,
|
||||
PositionX = posX,
|
||||
PositionY = posY,
|
||||
PositionZ = posZ,
|
||||
AerosolType = (int)ammoType,
|
||||
MunitionCount = GunCount * ChannelsPerGun * quantity,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/CounterDrone.Core/Models/FormationTemplate.cs
Normal file
35
src/CounterDrone.Core/Models/FormationTemplate.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>编队模板</summary>
|
||||
[Table("FormationTemplate")]
|
||||
public class FormationTemplate
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public int FormationMode { get; set; }
|
||||
public int LateralCount { get; set; } = 1;
|
||||
public int LongitudinalCount { get; set; } = 1;
|
||||
public double LateralSpacing { get; set; }
|
||||
public double LongitudinalSpacing { get; set; }
|
||||
public int LateralAxis { get; set; } = 2;
|
||||
public int LongitudinalAxis { get; set; } = 0;
|
||||
|
||||
public RoutePlan ToRoutePlan()
|
||||
{
|
||||
return new RoutePlan
|
||||
{
|
||||
FormationMode = FormationMode,
|
||||
LateralCount = LateralCount,
|
||||
LongitudinalCount = LongitudinalCount,
|
||||
LateralSpacing = LateralSpacing,
|
||||
LongitudinalSpacing = LongitudinalSpacing,
|
||||
LateralAxis = LateralAxis,
|
||||
LongitudinalAxis = LongitudinalAxis,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>编组</summary>
|
||||
[Table("Group")]
|
||||
public class Group
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[NotNull]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public int GroupType { get; set; } = (int)Models.GroupType.DroneFleet;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("o");
|
||||
}
|
||||
}
|
||||
44
src/CounterDrone.Core/Models/LaunchPlatformSpec.cs
Normal file
44
src/CounterDrone.Core/Models/LaunchPlatformSpec.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>搭载/发射平台规格(基础数据模板)</summary>
|
||||
[Table("LaunchPlatformSpec")]
|
||||
public class LaunchPlatformSpec
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public int PlatformType { get; set; }
|
||||
public int GunCount { get; set; } = 1;
|
||||
public int ChannelsPerGun { get; set; } = 1;
|
||||
public double ChannelInterval { get; set; } = 1.0;
|
||||
public double Cooldown { get; set; } = 5.0;
|
||||
public double AmmoChangeTime { get; set; } = 30.0;
|
||||
public double MuzzleVelocity { get; set; }
|
||||
public double CruiseSpeed { get; set; }
|
||||
public double ReleaseAltitude { get; set; }
|
||||
public string ModelId { get; set; } = "";
|
||||
[Ignore]
|
||||
public List<int> AmmoTypes { get; set; } = new();
|
||||
|
||||
public ScenarioUnit ToScenarioUnit(AerosolType ammoType, int quantity,
|
||||
double posX, double posY, double posZ)
|
||||
{
|
||||
return new ScenarioUnit
|
||||
{
|
||||
LaunchPlatformSpecId = Id,
|
||||
EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform,
|
||||
Quantity = quantity,
|
||||
PositionX = posX,
|
||||
PositionY = posY,
|
||||
PositionZ = posZ,
|
||||
AerosolType = (int)ammoType,
|
||||
MunitionCount = GunCount * ChannelsPerGun * quantity,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,10 @@ namespace CounterDrone.Core.Models
|
||||
[NotNull]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string ModelType { get; set; } = string.Empty;
|
||||
/// <summary>对应 EntityType 枚举:0=Drone 1=Platform 2=DetectionEquip</summary>
|
||||
public int EntityType { get; set; }
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
|
||||
|
||||
14
src/CounterDrone.Core/Models/PagedResult.cs
Normal file
14
src/CounterDrone.Core/Models/PagedResult.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>分页结果</summary>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>步骤5:航路规划 — 多编队支持:每个(任务,编队)一条航路</summary>
|
||||
/// <summary>步骤5:航路规划 — 多批次支持:每个(任务,批次)一条航路</summary>
|
||||
[Table("RoutePlan")]
|
||||
public class RoutePlan
|
||||
{
|
||||
@ -11,14 +11,28 @@ 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 GroupId { get; set; } = string.Empty;
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
public int FormationMode { get; set; } = (int)Models.FormationMode.Single;
|
||||
|
||||
public double FormationSpacing { get; set; } = 50.0;
|
||||
public double LateralSpacing { get; set; } = 50.0;
|
||||
|
||||
/// <summary>正面架数(横向展开),null=全部横向(=Quantity)</summary>
|
||||
public int? LateralCount { get; set; }
|
||||
|
||||
/// <summary>纵深架数(串列跟随),null=1(无纵深)</summary>
|
||||
public int? LongitudinalCount { get; set; }
|
||||
|
||||
/// <summary>纵向串列间距 m</summary>
|
||||
public double LongitudinalSpacing { get; set; } = 50.0;
|
||||
|
||||
/// <summary>横向展开轴:0=X, 1=Y(垂直), 2=Z(默认)</summary>
|
||||
public int LateralAxis { get; set; } = 2;
|
||||
/// <summary>纵向串列轴:0=X(默认), 1=Y, 2=Z</summary>
|
||||
public int LongitudinalAxis { get; set; } = 0;
|
||||
|
||||
public string ETA { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
37
src/CounterDrone.Core/Models/RouteTemplate.cs
Normal file
37
src/CounterDrone.Core/Models/RouteTemplate.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>航线模板(基础数据模板)</summary>
|
||||
[Table("RouteTemplate")]
|
||||
public class RouteTemplate
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string WaypointsJson { get; set; } = "[]";
|
||||
[Ignore]
|
||||
public List<WaypointCoord> Waypoints { get; set; } = new();
|
||||
|
||||
public List<Waypoint> ToWaypoints(double speed)
|
||||
{
|
||||
var list = new List<Waypoint>();
|
||||
foreach (var w in Waypoints)
|
||||
list.Add(new Waypoint
|
||||
{
|
||||
PosX = w.X, PosY = w.Y, PosZ = w.Z,
|
||||
Altitude = w.Y, Speed = speed,
|
||||
});
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public class WaypointCoord
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Z { get; set; }
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
@ -14,9 +14,11 @@ namespace CounterDrone.Core.Models
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Unique]
|
||||
public string TaskNumber { get; set; } = string.Empty;
|
||||
public string ScenarioNumber { get; set; } = string.Empty;
|
||||
|
||||
public int Status { get; set; } = (int)TaskStatus.Draft;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public int Status { get; set; } = (int)ScenarioStatus.Draft;
|
||||
|
||||
public int CurrentStep { get; set; } = 1;
|
||||
|
||||
19
src/CounterDrone.Core/Models/ScenarioConfig.cs
Normal file
19
src/CounterDrone.Core/Models/ScenarioConfig.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>想定完整配置(聚合 5 步配置)</summary>
|
||||
public class ScenarioConfig
|
||||
{
|
||||
public Scenario Info { get; set; } = new();
|
||||
public CombatScene Scene { get; set; } = new();
|
||||
public List<ControlZone> ControlZones { get; set; } = new();
|
||||
public List<ScenarioDrone> Drones { get; set; } = new();
|
||||
public List<ScenarioUnit> Units { get; set; } = new();
|
||||
public CloudDispersal Cloud { get; set; } = new();
|
||||
/// <summary>多批次航路(多批次支持)</summary>
|
||||
public List<RoutePlan> Routes { get; set; } = new();
|
||||
/// <summary>按 WaveId 分组的航路点</summary>
|
||||
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
|
||||
}
|
||||
}
|
||||
24
src/CounterDrone.Core/Models/ScenarioDrone.cs
Normal file
24
src/CounterDrone.Core/Models/ScenarioDrone.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>想定数据:无人机批次</summary>
|
||||
[Table("ScenarioDrone")]
|
||||
public class ScenarioDrone
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[Indexed]
|
||||
public string ScenarioId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>外键 → DroneSpec</summary>
|
||||
public string DroneSpecId { get; set; } = string.Empty;
|
||||
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
44
src/CounterDrone.Core/Models/ScenarioUnit.cs
Normal file
44
src/CounterDrone.Core/Models/ScenarioUnit.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>想定数据:装备部署</summary>
|
||||
[Table("ScenarioUnit")]
|
||||
public class ScenarioUnit
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[Indexed]
|
||||
public string ScenarioId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>外键 → LaunchPlatformSpec(发射平台)</summary>
|
||||
public string LaunchPlatformSpecId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>外键 → SensorSpec(探测设备)</summary>
|
||||
public string SensorSpecId { get; set; } = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform;
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
public double PositionX { get; set; }
|
||||
public double PositionY { get; set; }
|
||||
public double PositionZ { get; set; }
|
||||
|
||||
public int? AerosolType { get; set; }
|
||||
|
||||
/// <summary>外键 → AmmunitionSpec(挂载的弹药规格,前端配置用)</summary>
|
||||
public string AmmunitionSpecId { get; set; } = string.Empty;
|
||||
|
||||
public int? MunitionCount { get; set; }
|
||||
|
||||
public int Source { get; set; } = (int)ConfigSource.Manual;
|
||||
}
|
||||
}
|
||||
46
src/CounterDrone.Core/Models/SensorSpec.cs
Normal file
46
src/CounterDrone.Core/Models/SensorSpec.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>传感器规格(基础数据模板)</summary>
|
||||
[Table("SensorSpec")]
|
||||
public class SensorSpec
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public double RadarRange { get; set; }
|
||||
public double EORange { get; set; }
|
||||
public double IRRange { get; set; }
|
||||
|
||||
/// <summary>是否含雷达探测能力</summary>
|
||||
public bool HasRadar { get; set; }
|
||||
/// <summary>是否含光电探测能力</summary>
|
||||
public bool HasEO { get; set; }
|
||||
/// <summary>是否含红外探测能力</summary>
|
||||
public bool HasIR { get; set; }
|
||||
|
||||
public double Accuracy { get; set; } = 50.0;
|
||||
|
||||
/// <summary>3D 模型 ID(FK → ModelInfo),Unity 可视化用</summary>
|
||||
public string ModelId { get; set; } = "";
|
||||
public double? MinElevation { get; set; }
|
||||
public double? MaxElevation { get; set; }
|
||||
public double? MinDetectAlt { get; set; }
|
||||
public double? MaxDetectAlt { get; set; }
|
||||
|
||||
public ScenarioUnit ToScenarioUnit(double posX, double posY, double posZ)
|
||||
{
|
||||
return new ScenarioUnit
|
||||
{
|
||||
SensorSpecId = Id,
|
||||
EquipmentRole = (int)Models.EquipmentRole.Detection,
|
||||
Quantity = 1,
|
||||
PositionX = posX,
|
||||
PositionY = posY,
|
||||
PositionZ = posZ,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>仿真事件(数据库记录)</summary>
|
||||
[Table("SimEvent")]
|
||||
public class SimEvent
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[Indexed]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
public double OccurredAt { get; set; }
|
||||
|
||||
public int EventType { get; set; }
|
||||
|
||||
public string SourceId { get; set; } = string.Empty;
|
||||
|
||||
public string TargetId { get; set; } = string.Empty;
|
||||
|
||||
public string DataJson { get; set; } = "{}";
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
|
||||
@ -11,21 +11,24 @@ 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;
|
||||
public string ScenarioName { get; set; } = string.Empty;
|
||||
|
||||
public string TaskNumber { get; set; } = string.Empty;
|
||||
public string ScenarioNumber { get; set; } = string.Empty;
|
||||
|
||||
public string CompletedAt { get; set; } = string.Empty;
|
||||
|
||||
public int TargetCount { get; set; }
|
||||
public int DroneCount { get; set; }
|
||||
|
||||
public int EquipmentCount { get; set; }
|
||||
public int UnitCount { get; set; }
|
||||
|
||||
public int InterceptResult { get; set; }
|
||||
|
||||
/// <summary>报告富文本 / 结构化 JSON</summary>
|
||||
/// <summary>Markdown 文本(用于快速展示)</summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>结构化报告数据 JSON(用于 PDF 等格式重新渲染)</summary>
|
||||
public string? ReportDataJson { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>步骤2:目标配置</summary>
|
||||
[Table("TargetConfig")]
|
||||
public class TargetConfig
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[Indexed]
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
|
||||
public string GroupId { get; set; } = string.Empty;
|
||||
|
||||
public int TargetType { get; set; } = (int)Models.TargetType.Rotor;
|
||||
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
public int PowerType { get; set; } = (int)Models.PowerType.Electric;
|
||||
|
||||
public double Wingspan { get; set; } = 1.2;
|
||||
|
||||
public double TypicalSpeed { get; set; } = 60.0;
|
||||
|
||||
public double TypicalAltitude { get; set; } = 300.0;
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CounterDrone.Core.Models
|
||||
{
|
||||
/// <summary>想定完整配置(聚合 5 步配置)</summary>
|
||||
public class TaskFullConfig
|
||||
{
|
||||
public SimTask 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<EquipmentDeployment> Equipment { get; set; } = new();
|
||||
public CloudDispersal Cloud { get; set; } = new();
|
||||
/// <summary>多编队航路(多批次支持)</summary>
|
||||
public List<RoutePlan> Routes { get; set; } = new();
|
||||
/// <summary>按 GroupId 分组的航路点</summary>
|
||||
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>分页结果</summary>
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
public int Page { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
|
||||
}
|
||||
}
|
||||
@ -11,10 +11,10 @@ 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 GroupId { get; set; } = string.Empty;
|
||||
/// <summary>关联的批次 ID(多批次支持)</summary>
|
||||
public string WaveId { get; set; } = string.Empty;
|
||||
|
||||
[NotNull]
|
||||
public int OrderIndex { get; set; }
|
||||
|
||||
40
src/CounterDrone.Core/Reporting/FontResolver.cs
Normal file
40
src/CounterDrone.Core/Reporting/FontResolver.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using PdfSharpCore.Fonts;
|
||||
|
||||
namespace CounterDrone.Core.Reporting
|
||||
{
|
||||
/// <summary>CJK 字体解析器 — 从文件路径加载 TrueType 字体供 PdfSharpCore 使用</summary>
|
||||
public sealed class CjkFontResolver : IFontResolver
|
||||
{
|
||||
private readonly byte[] _fontData;
|
||||
private readonly string _familyName;
|
||||
|
||||
/// <summary>注册 CJK 字体解析器到全局设置(进程内只能注册一次)</summary>
|
||||
/// <param name="fontPath">CJK TrueType/OpenType 字体文件路径</param>
|
||||
/// <param name="familyName">字体族名(用于 XFont 构造)</param>
|
||||
public static void Register(string fontPath, string familyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fontPath))
|
||||
throw new ArgumentException("字体路径不能为空", nameof(fontPath));
|
||||
if (!File.Exists(fontPath))
|
||||
throw new FileNotFoundException($"字体文件不存在: {fontPath}", fontPath);
|
||||
|
||||
if (GlobalFontSettings.FontResolver == null)
|
||||
GlobalFontSettings.FontResolver = new CjkFontResolver(fontPath, familyName);
|
||||
}
|
||||
|
||||
private CjkFontResolver(string fontPath, string familyName)
|
||||
{
|
||||
_fontData = File.ReadAllBytes(fontPath);
|
||||
_familyName = familyName;
|
||||
}
|
||||
|
||||
public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
|
||||
=> new FontResolverInfo(_familyName);
|
||||
|
||||
public byte[] GetFont(string faceName) => _fontData;
|
||||
|
||||
public string DefaultFontName => _familyName;
|
||||
}
|
||||
}
|
||||
8
src/CounterDrone.Core/Reporting/IReportTemplate.cs
Normal file
8
src/CounterDrone.Core/Reporting/IReportTemplate.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace CounterDrone.Core.Reporting
|
||||
{
|
||||
/// <summary>报告模板接口 — 从 ReportData 渲染为目标格式</summary>
|
||||
public interface IReportTemplate
|
||||
{
|
||||
byte[] Render(ReportData data);
|
||||
}
|
||||
}
|
||||
79
src/CounterDrone.Core/Reporting/MarkdownRenderer.cs
Normal file
79
src/CounterDrone.Core/Reporting/MarkdownRenderer.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CounterDrone.Core.Reporting
|
||||
{
|
||||
/// <summary>从 ReportData 渲染 Markdown 文本</summary>
|
||||
public class MarkdownRenderer
|
||||
{
|
||||
public string Render(ReportData data)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"# {data.Title}");
|
||||
sb.AppendLine();
|
||||
AppendKeyValueTable(sb, "项目", "值", data.Header);
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var section in data.Sections)
|
||||
{
|
||||
sb.AppendLine($"## {section.Title}");
|
||||
sb.AppendLine();
|
||||
foreach (var block in section.Blocks)
|
||||
RenderBlock(sb, block);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void RenderBlock(StringBuilder sb, ReportBlock block)
|
||||
{
|
||||
switch (block)
|
||||
{
|
||||
case KeyValueBlock kv:
|
||||
AppendKeyValueTable(sb, kv.LabelHeader, kv.ValueHeader, kv.Items);
|
||||
sb.AppendLine();
|
||||
break;
|
||||
case TableBlock tb:
|
||||
if (!string.IsNullOrEmpty(tb.Caption))
|
||||
{
|
||||
sb.AppendLine($"### {tb.Caption}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
AppendTable(sb, tb.Headers, tb.Rows);
|
||||
sb.AppendLine();
|
||||
break;
|
||||
case TextBlock text:
|
||||
if (text.Bold)
|
||||
sb.AppendLine($"**{text.Text}**");
|
||||
else
|
||||
sb.AppendLine(text.Text);
|
||||
sb.AppendLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendKeyValueTable(StringBuilder sb, string labelHeader, string valueHeader, List<MetaItem> items)
|
||||
{
|
||||
sb.AppendLine($"| {labelHeader} | {valueHeader} |");
|
||||
sb.AppendLine($"|{new string('-', labelHeader.Length + 2)}|{new string('-', valueHeader.Length + 2)}|");
|
||||
foreach (var item in items)
|
||||
sb.AppendLine($"| {item.Label} | {item.Value} |");
|
||||
}
|
||||
|
||||
private static void AppendTable(StringBuilder sb, string[] headers, List<string[]> rows)
|
||||
{
|
||||
sb.AppendLine($"| {string.Join(" | ", headers)} |");
|
||||
sb.AppendLine($"|{string.Join("|", RepeatEach(headers, h => new string('-', h.Length + 2)))}|");
|
||||
foreach (var row in rows)
|
||||
sb.AppendLine($"| {string.Join(" | ", row)} |");
|
||||
}
|
||||
|
||||
private static IEnumerable<string> RepeatEach(string[] source, System.Func<string, string> fn)
|
||||
{
|
||||
foreach (var s in source)
|
||||
yield return fn(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/CounterDrone.Core/Reporting/ReportBlockConverter.cs
Normal file
55
src/CounterDrone.Core/Reporting/ReportBlockConverter.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CounterDrone.Core.Reporting
|
||||
{
|
||||
/// <summary>ReportBlock 多态序列化转换器</summary>
|
||||
public class ReportBlockConverter : JsonConverter<ReportBlock>
|
||||
{
|
||||
public override ReportBlock Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
if (!root.TryGetProperty("$blockType", out var typeProp))
|
||||
throw new JsonException("缺少 $blockType 字段");
|
||||
|
||||
var typeName = typeProp.GetString();
|
||||
var rawText = root.GetRawText();
|
||||
return typeName switch
|
||||
{
|
||||
"KeyValueBlock" => JsonSerializer.Deserialize<KeyValueBlock>(rawText, options)!,
|
||||
"TableBlock" => JsonSerializer.Deserialize<TableBlock>(rawText, options)!,
|
||||
"TextBlock" => JsonSerializer.Deserialize<TextBlock>(rawText, options)!,
|
||||
_ => throw new JsonException($"未知的 block 类型: {typeName}"),
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ReportBlock value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("$blockType", value.GetType().Name);
|
||||
switch (value)
|
||||
{
|
||||
case KeyValueBlock kv:
|
||||
writer.WriteString(nameof(kv.LabelHeader), kv.LabelHeader);
|
||||
writer.WriteString(nameof(kv.ValueHeader), kv.ValueHeader);
|
||||
writer.WritePropertyName(nameof(kv.Items));
|
||||
JsonSerializer.Serialize(writer, kv.Items, options);
|
||||
break;
|
||||
case TableBlock tb:
|
||||
writer.WriteString(nameof(tb.Caption), tb.Caption);
|
||||
writer.WritePropertyName(nameof(tb.Headers));
|
||||
JsonSerializer.Serialize(writer, tb.Headers, options);
|
||||
writer.WritePropertyName(nameof(tb.Rows));
|
||||
JsonSerializer.Serialize(writer, tb.Rows, options);
|
||||
break;
|
||||
case TextBlock text:
|
||||
writer.WriteString(nameof(text.Text), text.Text);
|
||||
writer.WriteBoolean(nameof(text.Bold), text.Bold);
|
||||
break;
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/CounterDrone.Core/Reporting/ReportData.cs
Normal file
50
src/CounterDrone.Core/Reporting/ReportData.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CounterDrone.Core.Reporting
|
||||
{
|
||||
/// <summary>结构化报告数据 — 模板渲染的统一数据源</summary>
|
||||
public class ReportData
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
public List<MetaItem> Header { get; set; } = new();
|
||||
public List<ReportSection> Sections { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MetaItem
|
||||
{
|
||||
public string Label { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
||||
|
||||
public class ReportSection
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
public List<ReportBlock> Blocks { get; set; } = new();
|
||||
}
|
||||
|
||||
public abstract class ReportBlock { }
|
||||
|
||||
/// <summary>键值对表(两列:参数 / 值)</summary>
|
||||
public class KeyValueBlock : ReportBlock
|
||||
{
|
||||
public string LabelHeader { get; set; } = "参数";
|
||||
public string ValueHeader { get; set; } = "值";
|
||||
public List<MetaItem> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>通用表格(可带子标题)</summary>
|
||||
public class TableBlock : ReportBlock
|
||||
{
|
||||
public string Caption { get; set; } = "";
|
||||
public string[] Headers { get; set; } = Array.Empty<string>();
|
||||
public List<string[]> Rows { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>文本段落(支持加粗、多行列表)</summary>
|
||||
public class TextBlock : ReportBlock
|
||||
{
|
||||
public string Text { get; set; } = "";
|
||||
public bool Bold { get; set; }
|
||||
}
|
||||
}
|
||||
377
src/CounterDrone.Core/Reporting/StandardPdfTemplate.cs
Normal file
377
src/CounterDrone.Core/Reporting/StandardPdfTemplate.cs
Normal file
@ -0,0 +1,377 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using PdfSharpCore;
|
||||
using PdfSharpCore.Drawing;
|
||||
using PdfSharpCore.Pdf;
|
||||
|
||||
namespace CounterDrone.Core.Reporting
|
||||
{
|
||||
/// <summary>标准 PDF 模板 — A4 排版,中文字体,表格边框,自动分页</summary>
|
||||
public class StandardPdfTemplate : IReportTemplate
|
||||
{
|
||||
private readonly string _fontFamily;
|
||||
private XFont _titleFont, _sectionFont, _tableHeaderFont, _tableCellFont, _textFont, _footerFont;
|
||||
|
||||
private const double MarginMm = 20;
|
||||
private const double FooterMm = 15;
|
||||
private static double MmToPt(double mm) => mm * 72.0 / 25.4;
|
||||
private static readonly double MarginPt = MmToPt(MarginMm);
|
||||
private static readonly XColor HeaderBgColor = XColor.FromArgb(240, 240, 240);
|
||||
private static readonly XColor BorderColor = XColor.FromArgb(180, 180, 180);
|
||||
private static readonly XColor TitleColor = XColor.FromArgb(30, 30, 30);
|
||||
private static readonly XColor SectionColor = XColor.FromArgb(50, 50, 50);
|
||||
|
||||
public StandardPdfTemplate(string fontPath, string fontFamily)
|
||||
{
|
||||
CjkFontResolver.Register(fontPath, fontFamily);
|
||||
_fontFamily = fontFamily;
|
||||
_titleFont = new XFont(_fontFamily, 22, XFontStyle.Bold);
|
||||
_sectionFont = new XFont(_fontFamily, 15, XFontStyle.Bold);
|
||||
_tableHeaderFont = new XFont(_fontFamily, 11, XFontStyle.Bold);
|
||||
_tableCellFont = new XFont(_fontFamily, 11, XFontStyle.Regular);
|
||||
_textFont = new XFont(_fontFamily, 12, XFontStyle.Regular);
|
||||
_footerFont = new XFont(_fontFamily, 9, XFontStyle.Regular);
|
||||
}
|
||||
|
||||
public byte[] Render(ReportData data)
|
||||
{
|
||||
var doc = new PdfDocument();
|
||||
doc.Info.Title = data.Title;
|
||||
|
||||
NewPage(doc);
|
||||
|
||||
DrawTitle(data.Title);
|
||||
|
||||
if (data.Header.Count > 0)
|
||||
{
|
||||
EnsureSpace(30);
|
||||
DrawKeyValueTable(data.Header, "项目", "值");
|
||||
_y += 16; // Header 后空行,与后续段落保持间距
|
||||
}
|
||||
|
||||
foreach (var section in data.Sections)
|
||||
{
|
||||
EnsureSpace(40);
|
||||
DrawSectionTitle(section.Title);
|
||||
|
||||
foreach (var block in section.Blocks)
|
||||
RenderBlock(doc, block);
|
||||
|
||||
_y += 16; // 段落后空行
|
||||
}
|
||||
|
||||
DrawFooter();
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
doc.Save(ms, false);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
// ═════════════════ 页面管理 ═════════════════
|
||||
|
||||
private PdfPage _page = null!;
|
||||
private XGraphics _gfx = null!;
|
||||
private double _y;
|
||||
private double _contentWidth;
|
||||
private double _bottomMargin;
|
||||
private int _pageNum;
|
||||
|
||||
private void NewPage(PdfDocument doc)
|
||||
{
|
||||
_page = doc.AddPage();
|
||||
_page.Size = PageSize.A4;
|
||||
_gfx = XGraphics.FromPdfPage(_page);
|
||||
_y = MarginPt;
|
||||
_contentWidth = _page.Width.Point - 2 * MarginPt;
|
||||
_bottomMargin = _page.Height.Point - MmToPt(FooterMm);
|
||||
_pageNum++;
|
||||
}
|
||||
|
||||
private void EnsureSpace(double neededPt)
|
||||
{
|
||||
if (_y + neededPt > _bottomMargin)
|
||||
{
|
||||
DrawFooter();
|
||||
NewPage(_page.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
var footerY = _page.Height.Point - MmToPt(FooterMm / 2);
|
||||
var fmt = new XStringFormat { Alignment = XStringAlignment.Center };
|
||||
_gfx.DrawString($"第 {_pageNum} 页", _footerFont, XBrushes.Gray,
|
||||
new XRect(0, footerY, _page.Width.Point, 20), fmt);
|
||||
}
|
||||
|
||||
// ═════════════════ 内容渲染 ═════════════════
|
||||
|
||||
private void DrawTitle(string title)
|
||||
{
|
||||
var fmt = new XStringFormat { Alignment = XStringAlignment.Center };
|
||||
var h = _titleFont.GetHeight() + 10;
|
||||
_gfx.DrawString(title, _titleFont, new XSolidBrush(TitleColor),
|
||||
new XRect(MarginPt, _y, _contentWidth, h), fmt);
|
||||
_y += h + 4;
|
||||
}
|
||||
|
||||
private void DrawSectionTitle(string title)
|
||||
{
|
||||
var h = _sectionFont.GetHeight() + 6;
|
||||
_gfx.DrawString(title, _sectionFont, new XSolidBrush(SectionColor),
|
||||
new XRect(MarginPt, _y, _contentWidth, h),
|
||||
new XStringFormat { Alignment = XStringAlignment.Near, LineAlignment = XLineAlignment.Near });
|
||||
_y += h;
|
||||
}
|
||||
|
||||
private void RenderBlock(PdfDocument doc, ReportBlock block)
|
||||
{
|
||||
switch (block)
|
||||
{
|
||||
case KeyValueBlock kv:
|
||||
DrawKeyValueTable(kv.Items, kv.LabelHeader, kv.ValueHeader);
|
||||
_y += 4;
|
||||
break;
|
||||
case TableBlock tb:
|
||||
if (!string.IsNullOrEmpty(tb.Caption))
|
||||
{
|
||||
EnsureSpace(20);
|
||||
_gfx.DrawString(tb.Caption, _textFont, XBrushes.Black,
|
||||
new XRect(MarginPt, _y, _contentWidth, 16),
|
||||
new XStringFormat { Alignment = XStringAlignment.Near, LineAlignment = XLineAlignment.Near });
|
||||
_y += 18;
|
||||
}
|
||||
DrawTable(tb.Headers, tb.Rows);
|
||||
_y += 4;
|
||||
break;
|
||||
case TextBlock text:
|
||||
RenderTextBlock(text);
|
||||
_y += 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawKeyValueTable(List<MetaItem> items, string labelHeader, string valueHeader)
|
||||
{
|
||||
double labelW = _contentWidth * 0.35;
|
||||
double valueW = _contentWidth - labelW;
|
||||
double rowH = 22;
|
||||
double x = MarginPt;
|
||||
|
||||
// header row
|
||||
EnsureSpace(rowH);
|
||||
_gfx.DrawRectangle(new XSolidBrush(HeaderBgColor), x, _y, _contentWidth, rowH);
|
||||
_gfx.DrawCellText(labelHeader, _tableHeaderFont, x + 6, _y, labelW, rowH);
|
||||
_gfx.DrawCellText(valueHeader, _tableHeaderFont, x + labelW + 6, _y, valueW, rowH);
|
||||
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, rowH);
|
||||
_gfx.DrawLine(new XPen(BorderColor), x + labelW, _y, x + labelW, _y + rowH);
|
||||
_y += rowH;
|
||||
|
||||
// data rows
|
||||
foreach (var item in items)
|
||||
{
|
||||
var wrapped = WrapText(item.Value, valueW - 12, _tableCellFont);
|
||||
double lineHeight = _tableCellFont.GetHeight();
|
||||
rowH = Math.Max(22, wrapped.Count * lineHeight + 8);
|
||||
|
||||
EnsureSpace(rowH);
|
||||
_gfx.DrawCellText(item.Label, _tableCellFont, x + 6, _y, labelW, rowH, true);
|
||||
int lines = wrapped.Count;
|
||||
if (lines == 1)
|
||||
{
|
||||
_gfx.DrawCellText(wrapped[0], _tableCellFont, x + labelW + 6, _y, valueW, rowH, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
double yOffset = (rowH - lines * lineHeight) / 2;
|
||||
for (int i = 0; i < lines; i++)
|
||||
_gfx.DrawCellText(wrapped[i], _tableCellFont, x + labelW + 6,
|
||||
_y + yOffset + i * lineHeight, valueW, lineHeight, false);
|
||||
}
|
||||
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, rowH);
|
||||
_gfx.DrawLine(new XPen(BorderColor), x + labelW, _y, x + labelW, _y + rowH);
|
||||
_y += rowH;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTable(string[] headers, List<string[]> rows)
|
||||
{
|
||||
double x = MarginPt;
|
||||
var colWidths = CalculateColumnWidths(headers, rows, _contentWidth);
|
||||
double headerH = 22;
|
||||
|
||||
EnsureSpace(headerH);
|
||||
_gfx.DrawRectangle(new XSolidBrush(HeaderBgColor), x, _y, _contentWidth, headerH);
|
||||
double cx = x;
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
_gfx.DrawCellText(headers[i], _tableHeaderFont, cx + 4, _y, colWidths[i], headerH);
|
||||
cx += colWidths[i];
|
||||
}
|
||||
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, headerH);
|
||||
cx = x;
|
||||
for (int i = 0; i < headers.Length - 1; i++)
|
||||
{
|
||||
cx += colWidths[i];
|
||||
_gfx.DrawLine(new XPen(BorderColor), cx, _y, cx, _y + headerH);
|
||||
}
|
||||
_y += headerH;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var wrappedCells = new List<string>[row.Length];
|
||||
int maxLines = 1;
|
||||
for (int i = 0; i < row.Length; i++)
|
||||
{
|
||||
wrappedCells[i] = WrapText(row[i], colWidths[i] - 8, _tableCellFont);
|
||||
maxLines = Math.Max(maxLines, wrappedCells[i].Count);
|
||||
}
|
||||
double lineHeight = _tableCellFont.GetHeight();
|
||||
double rowH = Math.Max(22, maxLines * lineHeight + 8);
|
||||
|
||||
EnsureSpace(rowH);
|
||||
cx = x;
|
||||
for (int i = 0; i < row.Length; i++)
|
||||
{
|
||||
int lines = wrappedCells[i].Count;
|
||||
if (lines == 1)
|
||||
{
|
||||
_gfx.DrawCellText(wrappedCells[i][0], _tableCellFont, cx + 4, _y, colWidths[i], rowH, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
double yOffset = (rowH - lines * lineHeight) / 2;
|
||||
for (int l = 0; l < lines; l++)
|
||||
_gfx.DrawCellText(wrappedCells[i][l], _tableCellFont, cx + 4,
|
||||
_y + yOffset + l * lineHeight, colWidths[i], lineHeight, false);
|
||||
}
|
||||
cx += colWidths[i];
|
||||
}
|
||||
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, rowH);
|
||||
cx = x;
|
||||
for (int i = 0; i < headers.Length - 1; i++)
|
||||
{
|
||||
cx += colWidths[i];
|
||||
_gfx.DrawLine(new XPen(BorderColor), cx, _y, cx, _y + rowH);
|
||||
}
|
||||
_y += rowH;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTextBlock(TextBlock block)
|
||||
{
|
||||
var font = block.Bold
|
||||
? new XFont(_fontFamily, 12, XFontStyle.Bold)
|
||||
: _textFont;
|
||||
|
||||
var lines = block.Text.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var wrapped = WrapText(line, _contentWidth, font);
|
||||
foreach (var wl in wrapped)
|
||||
{
|
||||
EnsureSpace(16);
|
||||
_gfx.DrawString(wl, font, XBrushes.Black,
|
||||
MarginPt, _y + 12);
|
||||
_y += 14;
|
||||
}
|
||||
}
|
||||
_y += 2;
|
||||
}
|
||||
|
||||
// ═════════════════ 辅助方法 ═════════════════
|
||||
|
||||
private double[] CalculateColumnWidths(string[] headers, List<string[]> rows, double totalWidth)
|
||||
{
|
||||
int n = headers.Length;
|
||||
var maxW = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
maxW[i] = _gfx.MeasureString(headers[i], _tableHeaderFont).Width;
|
||||
foreach (var row in rows)
|
||||
for (int i = 0; i < n && i < row.Length; i++)
|
||||
maxW[i] = Math.Max(maxW[i], _gfx.MeasureString(row[i], _tableCellFont).Width);
|
||||
|
||||
const double padding = 8;
|
||||
for (int i = 0; i < n; i++)
|
||||
maxW[i] += padding * 2;
|
||||
|
||||
double sum = maxW.Sum();
|
||||
if (sum > totalWidth)
|
||||
{
|
||||
double scale = totalWidth / sum;
|
||||
for (int i = 0; i < n; i++)
|
||||
maxW[i] *= scale;
|
||||
}
|
||||
return maxW;
|
||||
}
|
||||
|
||||
private List<string> WrapText(string text, double maxWidth, XFont font)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
result.Add("");
|
||||
return result;
|
||||
}
|
||||
|
||||
var paragraphs = text.Split('\n');
|
||||
foreach (var para in paragraphs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(para))
|
||||
{
|
||||
result.Add("");
|
||||
continue;
|
||||
}
|
||||
|
||||
var words = para.Split(' ');
|
||||
var current = "";
|
||||
foreach (var word in words)
|
||||
{
|
||||
var test = current == "" ? word : current + " " + word;
|
||||
if (_gfx.MeasureString(test, font).Width <= maxWidth)
|
||||
current = test;
|
||||
else
|
||||
{
|
||||
if (current != "") result.Add(current);
|
||||
if (_gfx.MeasureString(word, font).Width > maxWidth)
|
||||
{
|
||||
current = "";
|
||||
foreach (var ch in word)
|
||||
{
|
||||
var test2 = current + ch;
|
||||
if (_gfx.MeasureString(test2, font).Width <= maxWidth)
|
||||
current = test2;
|
||||
else
|
||||
{
|
||||
result.Add(current);
|
||||
current = ch.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
current = word;
|
||||
}
|
||||
}
|
||||
if (current != "") result.Add(current);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class GfxExtensions
|
||||
{
|
||||
public static void DrawCellText(this XGraphics gfx, string text, XFont font,
|
||||
double x, double y, double width, double height, bool centerVertical = true)
|
||||
{
|
||||
var rect = new XRect(x, y, width, height);
|
||||
var fmt = new XStringFormat
|
||||
{
|
||||
Alignment = XStringAlignment.Near,
|
||||
LineAlignment = centerVertical ? XLineAlignment.Center : XLineAlignment.Near,
|
||||
};
|
||||
gfx.DrawString(text, font, XBrushes.Black, rect, fmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
11
src/CounterDrone.Core/Repository/DroneSpecRepository.cs
Normal file
11
src/CounterDrone.Core/Repository/DroneSpecRepository.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using CounterDrone.Core;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class DroneSpecRepository : BaseRepository<DroneSpec>
|
||||
{
|
||||
public DroneSpecRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
using CounterDrone.Core;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class EnvironmentSpecRepository : BaseRepository<EnvironmentSpec>
|
||||
{
|
||||
public EnvironmentSpecRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class EquipmentDeploymentRepository : BaseRepository<EquipmentDeployment>
|
||||
{
|
||||
public EquipmentDeploymentRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<EquipmentDeployment> GetByTaskId(string taskId)
|
||||
{
|
||||
return Db.Table<EquipmentDeployment>().Where(e => e.TaskId == taskId).ToList();
|
||||
}
|
||||
|
||||
public void DeleteByTaskId(string taskId)
|
||||
{
|
||||
var equips = GetByTaskId(taskId);
|
||||
foreach (var e in equips)
|
||||
Db.Delete(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/CounterDrone.Core/Repository/FireUnitSpecRepository.cs
Normal file
11
src/CounterDrone.Core/Repository/FireUnitSpecRepository.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using CounterDrone.Core;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class FireUnitSpecRepository : BaseRepository<FireUnitSpec>
|
||||
{
|
||||
public FireUnitSpecRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
using CounterDrone.Core;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class FormationTemplateRepository : BaseRepository<FormationTemplate>
|
||||
{
|
||||
public FormationTemplateRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class GroupRepository : BaseRepository<Group>
|
||||
{
|
||||
public GroupRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<Group> GetByType(int groupType)
|
||||
{
|
||||
return Db.Table<Group>()
|
||||
.Where(g => g.GroupType == groupType)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 GetByTaskAndGroup(string taskId, string groupId)
|
||||
public RoutePlan GetByScenarioAndWave(string scenarioId, string waveId)
|
||||
{
|
||||
return Db.Table<RoutePlan>()
|
||||
.FirstOrDefault(r => r.TaskId == taskId && r.GroupId == groupId);
|
||||
.FirstOrDefault(r => r.ScenarioId == scenarioId && r.WaveId == waveId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/CounterDrone.Core/Repository/RouteTemplateRepository.cs
Normal file
11
src/CounterDrone.Core/Repository/RouteTemplateRepository.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using CounterDrone.Core;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class RouteTemplateRepository : BaseRepository<RouteTemplate>
|
||||
{
|
||||
public RouteTemplateRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
24
src/CounterDrone.Core/Repository/ScenarioDroneRepository.cs
Normal file
24
src/CounterDrone.Core/Repository/ScenarioDroneRepository.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class ScenarioDroneRepository : BaseRepository<ScenarioDrone>
|
||||
{
|
||||
public ScenarioDroneRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<ScenarioDrone> GetByScenarioId(string scenarioId)
|
||||
{
|
||||
return Db.Table<ScenarioDrone>().Where(t => t.ScenarioId == scenarioId).ToList();
|
||||
}
|
||||
|
||||
public void DeleteByScenarioId(string scenarioId)
|
||||
{
|
||||
var drones = GetByScenarioId(scenarioId);
|
||||
foreach (var t in drones)
|
||||
Db.Delete(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,21 +5,21 @@ 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 GetByScenarioNumber(string ScenarioNumber)
|
||||
{
|
||||
return Db.Table<SimTask>().FirstOrDefault(t => t.TaskNumber == taskNumber);
|
||||
return Db.Table<Scenario>().FirstOrDefault(t => t.ScenarioNumber == ScenarioNumber);
|
||||
}
|
||||
|
||||
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));
|
||||
query = query.Where(t => t.Name.Contains(keyword) || t.ScenarioNumber.Contains(keyword));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dateFrom))
|
||||
query = query.Where(t => t.CreatedAt.CompareTo(dateFrom) >= 0);
|
||||
32
src/CounterDrone.Core/Repository/ScenarioUnitRepository.cs
Normal file
32
src/CounterDrone.Core/Repository/ScenarioUnitRepository.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class ScenarioUnitRepository : BaseRepository<ScenarioUnit>
|
||||
{
|
||||
public ScenarioUnitRepository(SQLiteConnection db) : base(db) { }
|
||||
|
||||
public List<ScenarioUnit> GetByScenarioId(string scenarioId)
|
||||
{
|
||||
return Db.Table<ScenarioUnit>().Where(e => e.ScenarioId == scenarioId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>按任务和角色查询装备(EquipmentRole: 0=Detection, 1=LaunchPlatform)</summary>
|
||||
public List<ScenarioUnit> GetByScenarioIdAndRole(string scenarioId, int equipmentRole)
|
||||
{
|
||||
return Db.Table<ScenarioUnit>()
|
||||
.Where(e => e.ScenarioId == scenarioId && e.EquipmentRole == equipmentRole)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void DeleteByScenarioId(string scenarioId)
|
||||
{
|
||||
var equips = GetByScenarioId(scenarioId);
|
||||
foreach (var e in equips)
|
||||
Db.Delete(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/CounterDrone.Core/Repository/SensorSpecRepository.cs
Normal file
11
src/CounterDrone.Core/Repository/SensorSpecRepository.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using CounterDrone.Core;
|
||||
using SQLite;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Repository
|
||||
{
|
||||
public class SensorSpecRepository : BaseRepository<SensorSpec>
|
||||
{
|
||||
public SensorSpecRepository(SQLiteConnection db) : base(db) { }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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> GetByTaskAndGroup(string taskId, string groupId)
|
||||
public List<Waypoint> GetByScenarioAndWave(string scenarioId, string waveId)
|
||||
{
|
||||
return Db.Table<Waypoint>()
|
||||
.Where(w => w.TaskId == taskId && w.GroupId == groupId)
|
||||
.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);
|
||||
}
|
||||
|
||||
93
src/CounterDrone.Core/Services/ConfigService.cs
Normal file
93
src/CounterDrone.Core/Services/ConfigService.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using SQLite;
|
||||
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
/// <summary>配置服务实现 — 读写 JSON 配置文件,重载时触发事件通知消费方刷新</summary>
|
||||
public class ConfigService : IConfigService
|
||||
{
|
||||
private readonly IPathProvider _paths;
|
||||
private readonly SQLiteConnection _db;
|
||||
|
||||
/// <summary>planner 配置重载后触发(消费方应重建 DefensePlanner)</summary>
|
||||
public event Action<PlannerConfig>? PlannerConfigReloaded;
|
||||
|
||||
/// <summary>默认数据重载后触发(消费方应刷新缓存)</summary>
|
||||
public event Action<DefaultData>? DefaultsReloaded;
|
||||
|
||||
public ConfigService(IPathProvider paths, SQLiteConnection db)
|
||||
{
|
||||
_paths = paths;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public string GetPlannerConfigJson()
|
||||
{
|
||||
var path = Path.Combine(_paths.GetDataRoot(), "planner_config.json");
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"planner 配置文件不存在: {path}");
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public void SavePlannerConfig(string json)
|
||||
{
|
||||
var path = Path.Combine(_paths.GetDataRoot(), "planner_config.json");
|
||||
File.WriteAllText(path, json);
|
||||
ReloadPlannerConfig();
|
||||
}
|
||||
|
||||
public string GetDefaultsJson()
|
||||
{
|
||||
var path = Path.Combine(_paths.GetDataRoot(), "defaults.json");
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"默认数据文件不存在: {path}");
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public void SaveDefaults(string json)
|
||||
{
|
||||
var path = Path.Combine(_paths.GetDataRoot(), "defaults.json");
|
||||
File.WriteAllText(path, json);
|
||||
ReloadDefaults();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
ReloadPlannerConfig();
|
||||
ReloadDefaults();
|
||||
}
|
||||
|
||||
private void ReloadPlannerConfig()
|
||||
{
|
||||
var config = PlannerConfig.Load(_paths);
|
||||
PlannerConfigReloaded?.Invoke(config);
|
||||
}
|
||||
|
||||
private void ReloadDefaults()
|
||||
{
|
||||
var defaults = DefaultData.Load(_paths);
|
||||
ReseedDatabase(defaults);
|
||||
DefaultsReloaded?.Invoke(defaults);
|
||||
}
|
||||
|
||||
private void ReseedDatabase(DefaultData defaults)
|
||||
{
|
||||
foreach (var a in defaults.Ammunition) _db.InsertOrReplace(a);
|
||||
foreach (var f in defaults.FireUnits) _db.InsertOrReplace(f);
|
||||
foreach (var l in defaults.LaunchPlatforms) _db.InsertOrReplace(l);
|
||||
foreach (var d in defaults.Drones) _db.InsertOrReplace(d);
|
||||
foreach (var s in defaults.Sensors) _db.InsertOrReplace(s);
|
||||
foreach (var e in defaults.Environments) _db.InsertOrReplace(e);
|
||||
foreach (var f in defaults.Formations) _db.InsertOrReplace(f);
|
||||
foreach (var r in defaults.Routes)
|
||||
{
|
||||
r.WaypointsJson = JsonSerializer.Serialize(r.Waypoints);
|
||||
_db.InsertOrReplace(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/CounterDrone.Core/Services/DataService.cs
Normal file
74
src/CounterDrone.Core/Services/DataService.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
public class DataService : IDataService
|
||||
{
|
||||
private readonly AmmunitionSpecRepository _ammoRepo;
|
||||
private readonly FireUnitSpecRepository _fireUnitRepo;
|
||||
private readonly DroneSpecRepository _droneRepo;
|
||||
private readonly SensorSpecRepository _sensorRepo;
|
||||
private readonly EnvironmentSpecRepository _envRepo;
|
||||
private readonly FormationTemplateRepository _formationRepo;
|
||||
private readonly RouteTemplateRepository _routeRepo;
|
||||
|
||||
public DataService(AmmunitionSpecRepository ammoRepo, FireUnitSpecRepository fireUnitRepo,
|
||||
DroneSpecRepository droneRepo, SensorSpecRepository sensorRepo,
|
||||
EnvironmentSpecRepository envRepo, FormationTemplateRepository formationRepo,
|
||||
RouteTemplateRepository routeRepo)
|
||||
{
|
||||
_ammoRepo = ammoRepo;
|
||||
_fireUnitRepo = fireUnitRepo;
|
||||
_droneRepo = droneRepo;
|
||||
_sensorRepo = sensorRepo;
|
||||
_envRepo = envRepo;
|
||||
_formationRepo = formationRepo;
|
||||
_routeRepo = routeRepo;
|
||||
}
|
||||
|
||||
// ═══ AmmunitionSpec ═══
|
||||
public List<AmmunitionSpec> GetAllAmmo() => _ammoRepo.GetAll();
|
||||
public AmmunitionSpec GetAmmo(string id) => _ammoRepo.GetById(id);
|
||||
public void SaveAmmo(AmmunitionSpec spec) { if (_ammoRepo.GetById(spec.Id) != null) _ammoRepo.Update(spec); else _ammoRepo.Insert(spec); }
|
||||
public void DeleteAmmo(string id) => _ammoRepo.Delete(id);
|
||||
|
||||
// ═══ FireUnitSpec ═══
|
||||
public List<FireUnitSpec> GetAllFireUnits() => _fireUnitRepo.GetAll();
|
||||
public FireUnitSpec GetFireUnit(string id) => _fireUnitRepo.GetById(id);
|
||||
public void SaveFireUnit(FireUnitSpec spec) { if (_fireUnitRepo.GetById(spec.Id) != null) _fireUnitRepo.Update(spec); else _fireUnitRepo.Insert(spec); }
|
||||
public void DeleteFireUnit(string id) => _fireUnitRepo.Delete(id);
|
||||
|
||||
// ═══ DroneSpec ═══
|
||||
public List<DroneSpec> GetAllDrones() => _droneRepo.GetAll();
|
||||
public DroneSpec GetDrone(string id) => _droneRepo.GetById(id);
|
||||
public void SaveDrone(DroneSpec spec) { if (_droneRepo.GetById(spec.Id) != null) _droneRepo.Update(spec); else _droneRepo.Insert(spec); }
|
||||
public void DeleteDrone(string id) => _droneRepo.Delete(id);
|
||||
|
||||
// ═══ SensorSpec ═══
|
||||
public List<SensorSpec> GetAllSensors() => _sensorRepo.GetAll();
|
||||
public SensorSpec GetSensor(string id) => _sensorRepo.GetById(id);
|
||||
public void SaveSensor(SensorSpec spec) { if (_sensorRepo.GetById(spec.Id) != null) _sensorRepo.Update(spec); else _sensorRepo.Insert(spec); }
|
||||
public void DeleteSensor(string id) => _sensorRepo.Delete(id);
|
||||
|
||||
// ═══ EnvironmentSpec ═══
|
||||
public List<EnvironmentSpec> GetAllEnvironments() => _envRepo.GetAll();
|
||||
public EnvironmentSpec GetEnvironment(string id) => _envRepo.GetById(id);
|
||||
public void SaveEnvironment(EnvironmentSpec spec) { if (_envRepo.GetById(spec.Id) != null) _envRepo.Update(spec); else _envRepo.Insert(spec); }
|
||||
public void DeleteEnvironment(string id) => _envRepo.Delete(id);
|
||||
|
||||
// ═══ FormationTemplate ═══
|
||||
public List<FormationTemplate> GetAllFormations() => _formationRepo.GetAll();
|
||||
public FormationTemplate GetFormation(string id) => _formationRepo.GetById(id);
|
||||
public void SaveFormation(FormationTemplate spec) { if (_formationRepo.GetById(spec.Id) != null) _formationRepo.Update(spec); else _formationRepo.Insert(spec); }
|
||||
public void DeleteFormation(string id) => _formationRepo.Delete(id);
|
||||
|
||||
// ═══ RouteTemplate ═══
|
||||
public List<RouteTemplate> GetAllRoutes() => _routeRepo.GetAll();
|
||||
public RouteTemplate GetRoute(string id) => _routeRepo.GetById(id);
|
||||
public void SaveRoute(RouteTemplate spec) { if (_routeRepo.GetById(spec.Id) != null) _routeRepo.Update(spec); else _routeRepo.Insert(spec); }
|
||||
public void DeleteRoute(string id) => _routeRepo.Delete(id);
|
||||
}
|
||||
}
|
||||
46
src/CounterDrone.Core/Services/DefenseRecommendation.cs
Normal file
46
src/CounterDrone.Core/Services/DefenseRecommendation.cs
Normal file
@ -0,0 +1,46 @@
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
/// <summary>防御推荐方案 — 配置阶段调用 planner 生成,供前端"一键应用"</summary>
|
||||
public class DefenseRecommendation
|
||||
{
|
||||
/// <summary>最佳推荐参数(拦截概率最高的抛撒参数)</summary>
|
||||
public RecommendOption Best { get; set; } = new();
|
||||
|
||||
/// <summary>方案摘要(含失败原因,如有)</summary>
|
||||
public string Summary { get; set; } = "";
|
||||
|
||||
/// <summary>是否规划成功</summary>
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>推荐参数档位</summary>
|
||||
public class RecommendOption
|
||||
{
|
||||
/// <summary>推荐抛撒位置 X(米)</summary>
|
||||
public double PosX { get; set; }
|
||||
|
||||
/// <summary>推荐抛撒位置 Y(米,高度)</summary>
|
||||
public double PosY { get; set; }
|
||||
|
||||
/// <summary>推荐抛撒位置 Z(米)</summary>
|
||||
public double PosZ { get; set; }
|
||||
|
||||
/// <summary>推荐抛撒时机(仿真秒)</summary>
|
||||
public double Timing { get; set; }
|
||||
|
||||
/// <summary>推荐弹药数(齐射发数)</summary>
|
||||
public int SalvoRounds { get; set; }
|
||||
|
||||
/// <summary>云团展开间距(米)</summary>
|
||||
public double SalvoSpacing { get; set; }
|
||||
|
||||
/// <summary>估计拦截概率(0~1)</summary>
|
||||
public double EstimatedProbability { get; set; }
|
||||
|
||||
/// <summary>已拦截威胁数</summary>
|
||||
public int ThreatsEngaged { get; set; }
|
||||
|
||||
/// <summary>未拦截威胁数</summary>
|
||||
public int ThreatsUnengaged { get; set; }
|
||||
}
|
||||
}
|
||||
30
src/CounterDrone.Core/Services/EnumMetadata.cs
Normal file
30
src/CounterDrone.Core/Services/EnumMetadata.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CounterDrone.Core.Services
|
||||
{
|
||||
/// <summary>枚举条目(前端下拉框数据源)</summary>
|
||||
public class EnumItem
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string ChineseName { get; set; } = "";
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>枚举元数据(中英文对照)</summary>
|
||||
public class EnumMetadata
|
||||
{
|
||||
public List<EnumItem> DroneType { get; set; } = new();
|
||||
public List<EnumItem> PowerType { get; set; } = new();
|
||||
public List<EnumItem> PlatformType { get; set; } = new();
|
||||
public List<EnumItem> AerosolType { get; set; } = new();
|
||||
public List<EnumItem> WeatherType { get; set; } = new();
|
||||
public List<EnumItem> WindDirection { get; set; } = new();
|
||||
public List<EnumItem> SceneType { get; set; } = new();
|
||||
public List<EnumItem> FormationMode { get; set; } = new();
|
||||
public List<EnumItem> TriggerMode { get; set; } = new();
|
||||
public List<EnumItem> ReleaseMode { get; set; } = new();
|
||||
public List<EnumItem> EquipmentRole { get; set; } = new();
|
||||
public List<EnumItem> EntityType { get; set; } = new();
|
||||
public List<EnumItem> SensorType { get; set; } = new();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user