Compare commits

..

No commits in common. "develop" and "main" have entirely different histories.

203 changed files with 3365 additions and 13463 deletions

1
.gitignore vendored
View File

@ -60,4 +60,3 @@ src/Unity/Logs/
src/Unity/UserSettings/ src/Unity/UserSettings/
unity_plugins/ unity_plugins/
nul nul
reports/

View File

@ -76,65 +76,6 @@ 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. 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. **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.

View File

@ -1,388 +1,8 @@
# Changelog # Changelog
--- 本文件记录项目对外发布的变更历史(用户视角)。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
## [0.12.0] - 2026-06-20 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
### Added — PDF 导出 + 报告模板架构Phase 8.2.1
- **PdfSharpCore 1.3.64**:纯托管 PDF 库netstandard2.0Unity 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`SimHei9.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 — 基础数据 CRUDDataService
- **7 类规格入库**FireUnitSpec / DroneSpec / SensorSpec / EnvironmentSpec / FormationTemplate / RouteTemplate / AmmunitionSpec
- **类名规范化**TargetPreset→DroneSpec, DetectionPreset→SensorSpec, WeatherPreset→EnvironmentSpec, FireUnitTemplate→FireUnitSpec, RoutePreset→RouteTemplate
- **IDataService + DataService**:全 CRUDGetAll/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 ⇄ DetectedTick 第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 → DefensePlannerSaveRoute 签名更新
- 仿真器实体事件映射BuildDroneGroups → BuildDroneWaves
### Removed
- 7 个文件Group.cs / GroupRepository.cs / IGroupService.cs / GroupService.cs / GroupManager.cs / GroupServiceTests.cs / GroupRepositoryTests.cs
### Metrics
- 测试 208 → **204**4删除 Group 相关测试),全量通过 63s
- 0 编译错误0 编译警告
---
## [0.6.0] - 2026-06-15
### Added — 探测驱动的规划8.1.1
- **DetectionCalculator 静态工具类**:探测能力评估的唯一实现(与 Kinematics/RouteGeometry 同范式)。光电受 Visibility 衰减(`有效=基准×min(1,Visibility/基准)`),雷达/红外不受影响;统一信息网络找最早探测点(线段-圆求交)
- **探测源模型**DetectionSource独立探测设备 + 火力单元自带探测统一表达(雷达/光电/红外三距离 + 精度)
- **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 × ChannelsPerGunChannelInterval 物理间隔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
- 所有静默 fallbacknull return/empty events
- Models.SimEvent 死代码
--- ---

View File

@ -1 +1 @@
0.12.0 0.2.0

56
data/default_ammo.json Normal file
View File

@ -0,0 +1,56 @@
[
{
"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": "{}"
}
]

View File

@ -1,209 +0,0 @@
{
"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 }
]
}

Binary file not shown.

View File

@ -1,19 +0,0 @@
{
"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

View File

@ -1,369 +0,0 @@
# 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 |

View File

@ -4,83 +4,61 @@
| 实体 | 类 | 生命周期 | 主要属性 | | 实体 | 类 | 生命周期 | 主要属性 |
|------|------|------|------| |------|------|------|------|
| 无人机 | `DroneEntity` | 仿真全程 | Pos, Hp, Status, Route, ExposureTime, TraveledArc, Progress | | 无人机 | `DroneEntity` | 仿真全程 | Pos, Hp, Status, Route, ExposureTime |
| 发射平台 | `PlatformEntity` | 仿真全程 | Pos, PlatformType, StateIdle, AerosolType, MunitionCount, Cooldown, CruiseSpeed, MuzzleVelocity, CurrentVelocity, Target, FlightDistance | | 发射平台 | `PlatformEntity` | 仿真全程 | Pos, PlatformType, AerosolType, MunitionCount, Cooldown |
| 飞行弹药 | `MunitionEntity` | 发射→到达释放高度 | Pos, Velocity, LaunchAngle, Azimuth, MuzzleVelocity, FlightDuration, Start, Target, HasArrived, ElapsedTime | | 飞行弹药 | `MunitionEntity` | 发射→到达释放高度 | Pos, LaunchMode, Target, HasArrived |
| 气溶胶云团 | `CloudEntity` | 弹药到达→消散 | Pos, Radius, Density, Phase, Elapsed, IsDissipated | | 气溶胶云团 | `CloudEntity` | 弹药到达→消散 | Center, Radius, CoreDensity, IsDissipated |
| 管控区域 | `ControlZoneEntity` | 仿真全程 | Vertices, Min/MaxAltitude | | 管控区域 | `ControlZoneEntity` | 仿真全程 | Vertices, Min/MaxAltitude |
| 探测设备 | `DetectionEntity` | 仿真全程 | Source雷达/光电/红外距离+3D球冠参数, 每无人机探测状态机Undetected⇄Detected |
## 事件一览 ## 事件一览
| 事件 | 触发条件 | 参数 | Unity 典型响应 | | 事件 | 触发条件 | 参数 | Unity 典型响应 |
|------|------|------|------| |------|------|------|------|
| `OnTargetDetected` | 无人机首次进入任一探测设备 3D 球冠范围 | `DroneEntity`, `DetectionEntity` | 显示发现标记 / 3D 球冠 wireframe 高亮 |
| `OnMunitionLaunched` | 发射计划时间到达 + 平台就绪 | `MunitionEntity` | 生成炮弹模型,播放发射动画 | | `OnMunitionLaunched` | 发射计划时间到达 + 平台就绪 | `MunitionEntity` | 生成炮弹模型,播放发射动画 |
| `OnCloudGenerated` | 弹药到达释放高度 | `CloudEntity` | 生成粒子系统,初始半径+颜色 | | `OnCloudGenerated` | 弹药到达释放高度 | `CloudEntity` | 生成粒子系统,初始半径+颜色 |
| `OnDroneDestroyed` | 毁伤判定 HP ≤ 0 | `DroneEntity` | 播放爆炸/坠毁动画 | | `OnDroneDestroyed` | 毁伤判定 HP ≤ 0 | `DroneEntity` | 播放爆炸/坠毁动画 |
| `OnDroneReachedTarget` | 到达最后航路点 | `DroneEntity` | 显示"目标抵达"提示 | | `OnDroneReachedTarget` | 到达最后航路点 | `DroneEntity` | 显示"目标抵达"提示 |
| `OnZoneIntruded` | 无人机进入管控区 | `DroneEntity`, `ControlZoneEntity` | 红色警报 | | `OnZoneIntruded` | 无人机进入管控区 | `DroneEntity`, `ControlZoneEntity` | 红色警报 |
| `OnSimulationEnded` | 所有无人机状态 ≠ Flying | 无 | 显示结果面板,触发报告生成 | | `OnSimulationEnded` | 所有无人机状态 ≠ Flying | 无 | 显示结果面板,触发报告生成 |
| `PlanningFailed` | planner 无法生成拦截方案(引擎发出 SimEvent无回调 | 无(通过 `SimEventType.PlanningFailed` 事件流传递) | 前端显示规划失败原因 |
## 实体 × 事件 矩阵 ## 实体 × 事件 矩阵
| | TargetDetected | MunitionLaunched | CloudGenerated | DroneDestroyed | ReachedTarget | ZoneIntruded | SimEnded | PlanningFailed | | | MunitionLaunched | CloudGenerated | DroneDestroyed | ReachedTarget | ZoneIntruded | SimEnded |
|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |------|:---:|:---:|:---:|:---:|:---:|:---:|
| DroneEntity | ✅ | | | ✅ | ✅ | ✅ | | | | DroneEntity | | | ✅ | ✅ | ✅ | |
| PlatformEntity | | ✅ | | | | | | | | PlatformEntity | ✅ | | | | | |
| MunitionEntity | | ✅ | | | | | | | | MunitionEntity | ✅ | | | | | |
| CloudEntity | | | ✅ | | | | | | | CloudEntity | | ✅ | | | | |
| ControlZoneEntity | | | | | | ✅ | | | | ControlZoneEntity | | | | | ✅ | |
| DetectionEntity | ✅ | | | | | | | | | 全局 | | | | | | ✅ |
| 全局 | | | | | | | ✅ | ✅ |
## 数据流 ## 数据流
``` ```
想定配置ScenarioUnit + ScenarioDrone + RoutePlan + CombatScene FireSchedule (算法输出)
SimulationEngine.Initialize()
├─ BuildFireUnits() → List<FireUnit>
├─ BuildDroneWaves() → List<DroneWave>
├─ IDefensePlanner.Plan() → FireSchedule
│ └─ MergedSchedule.Count == 0 → PlanningFailed 事件
IDefensePlanner.Plan(fireUnits, threats, scene)
├─ 五步流水线 → FireSchedule发射计划
SimulationEngine.Tick() SimulationEngine.Tick()
├─ 【地基】时间到达 → PlatformEntity.Release() → MunitionEntity 创建 ├─ 时间到达 → PlatformEntity.Fire() → MunitionEntity 创建
│ └─ OnMunitionLaunched ──→ Unity: 炮弹 3D 模型 │ │
│ ├─ OnMunitionLaunched ──→ Unity: 炮弹 3D 模型
├─ 【空基】时间到达 → PlatformEntity.Release() → MunitionEntity 创建 │ │
│ └─ OnMunitionLaunched ──→ Unity: 空基发射 │ └─ 飞行 → 到达释放高度
├─ MunitionEntity 飞行 → 到达释放高度
│ ├─ 地基抛物线弹道Kinematics.ParabolicPosition
│ └─ 空基:水平初速 + 重力launchAngle=0, ParabolicPosition
│ │ │ │
│ ├─ CloudEntity 创建 │ ├─ CloudEntity 创建
│ │
│ ├─ OnCloudGenerated ──→ Unity: 粒子系统 │ ├─ OnCloudGenerated ──→ Unity: 粒子系统
│ │
│ └─ 扩散 → 毁伤判定 │ └─ 扩散 → 毁伤判定
│ │
│ ├─ DroneEntity.Hp -= dmg │ ├─ DroneEntity.Hp -= dmg
│ │
│ └─ HP≤0 → OnDroneDestroyed ──→ Unity: 爆炸动画 │ └─ HP≤0 → OnDroneDestroyed ──→ Unity: 爆炸动画
├─ DroneEntity.Update() → 到达终点 ├─ DroneEntity.Update() → 到达终点
│ └─ OnDroneReachedTarget ──→ Unity: 抵达提示 │ └─ OnDroneReachedTarget ──→ Unity: 抵达提示
├─ 【实时探测扫描】DetectionEntity × 飞行无人机
│ ├─ 3D 球冠判定IsInCoverage→ 首次进入置 Detected
│ ├─ 离开范围 → 回退 Undetected再次进入重新触发
│ └─ OnTargetDetected ──→ Unity: 发现标记 / 球冠高亮
├─ ControlZone.ContainsPoint() ├─ ControlZone.ContainsPoint()
│ └─ OnZoneIntruded ──→ Unity: 红色警报 │ └─ OnZoneIntruded ──→ Unity: 红色警报

View File

@ -1,58 +0,0 @@
# 默认想定参数对照表
> **目的**:跟踪 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 求解。 |

View File

@ -1,8 +1,8 @@
# 实施计划与任务跟踪 # 实施计划与任务跟踪
> **项目**:反无人机仿真系统后端 > **项目**:反无人机仿真系统后端
> **文档版本**V1.8 > **文档版本**V1.1
> **更新日期**2026-06-22 > **更新日期**2026-06-11
--- ---
@ -14,16 +14,10 @@ Phase 2 ✅ 想定管理
Phase 3 ✅ 算法层 Phase 3 ✅ 算法层
Phase 4 ✅ 仿真引擎 Phase 4 ✅ 仿真引擎
Phase 5 ✅ 报告生成 Phase 5 ✅ 报告生成
Phase 6 ✅ Unity 集成(桥接层 + 示例项目 Phase 6 ⬜ Unity 集成(需 Unity 22.3.62
Phase 7 ✅ 打磨收尾 Phase 7 ✅ 打磨收尾
Phase 8 ✅ 天气/物理模型统一
Phase 9 ✅ 性能优化 + 架构文档校准
Phase 10 ✅ 探测实时链路(已完成)
Phase 11 ✅ 运动学重构 + 实体暴露 + 基础数据 CRUD
Phase 12 ✅ 空基/地基统一规划 + 编队轴 + LaneDivider
Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
──────────────────────── ────────────────────────
已完成 P1-P13 已完成 P1-P5 + P7
``` ```
--- ---
@ -37,7 +31,7 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
| # | 任务 | 状态 | | # | 任务 | 状态 |
|---|------|------| |---|------|------|
| 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService、单元测试 | ✅ | | 1.1-1.12 | 项目骨架、枚举、数据模型、Repository、ModelService/GroupService、单元测试 | ✅ |
--- ---
@ -95,33 +89,27 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
|---|------|------| |---|------|------|
| 5.1-5.8 | ReportGenerator、ReportService、ExportToFile、搜索分页、单元测试 | ✅ | | 5.1-5.8 | ReportGenerator、ReportService、ExportToFile、搜索分页、单元测试 | ✅ |
**注**PDF 导出已实现(见 8.2.1),仿真后自动生成 MDPDF 按需调用 `ExportReport(id, "pdf")` **注**PDF/Word 导出暂以 Markdown 替代,后续可扩展
--- ---
### Phase 6Unity 集成 ### Phase 6Unity 集成
**目标**:将 Core.dll 集成到 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 | ✅ | `src/Unity/` 项目16 个 DLL → `Assets/Plugins/` | | 6.1 | 创建 Unity 项目,导入 Core.dll | 1h | ⬜ |
| 6.2 | 实现 `UnityPathProvider` | 1h | ✅ | `Application.persistentDataPath` 桥接 | | 6.2 | 实现 `UnityPathProvider` | 1h | ⬜ |
| 6.3 | 实现 `ModelManager` | 3h | ✅ | 导入/删除/查询,含 Verify | | 6.3 | 实现 `ModelManager` | 3h | ⬜ |
| 6.4 | 实现 `ScenarioManager`5 步配置) | 4h | ✅ | 完整 CRUD + 搜索分页 + 多批次 Route | | 6.4 | 实现 `ScenarioManager`5 步向导) | 4h | ⬜ |
| 6.5 | 实现 `SimulationRunner`Update 驱动) | 3h | ✅ | Tick 驱动 + 事件订阅 + 实体位置同步 + 炮弹轨迹可视化 | | 6.5 | 实现 `SimulationRunner`Update 驱动) | 3h | ⬜ |
| 6.6 | 实现 `ReplayController`(帧加载) | 3h | ✅ | 从分库加载帧数据TotalFrames/GetFrame | | 6.6 | 实现 `ReplayController`(协程回放) | 3h | ⬜ |
| 6.7 | 实现 `ReportManager` | 2h | ✅ | 生成 + Markdown 导出 | | 6.7 | 实现 `ReportManager` | 2h | ⬜ |
| 6.8 | 3D 实体可视化 | 4h | ✅ | Drone/Cube、Cloud/Sphere、Munition/Cylinder逐帧位置同步 | | 6.8 | 3D 实体可视化 | 4h | ⬜ |
| 6.9 | 粒子系统参数传递 | 3h | ✅ | 云团半径缩放 + 颜色/透明度动态更新 | | 6.9 | 粒子系统参数传递 | 3h | ⬜ |
| 6.10 | 端到端联调 + 一键验证 | 6h | ✅ | `ManagerVerification` 7 模块全过,`SimulationBootstrap` 一键 Demo | | 6.10 | 端到端联调 | 6h | ⬜ |
**额外交付物**
- `SimulationBootstrap.cs`:一键创建想定 → 推荐方案 → 启动仿真Scene 直接运行)
- `simple_simulation.unity`:预配置场景文件
- `ManagerVerification`Inspector 右键 `Run Full Verification` → Console 7 行 OK
--- ---
@ -129,7 +117,7 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
| # | 任务 | 状态 | | # | 任务 | 状态 |
|---|------|------| |---|------|------|
| 7.1 | 性能优化 | ✅ Tick 热路径优化Unity 满载帧率 <20200+ FPS详见总体架构设计第十三章 | | 7.1 | 性能优化 | ⬜(非关键,实体少时自然 < 5ms |
| 7.2 | TTL 清理FrameDataStore.CleanupExpired + 测试) | ✅ | | 7.2 | TTL 清理FrameDataStore.CleanupExpired + 测试) | ✅ |
| 7.3 | 边界测试(空部署/强风/超长航路/空基弹药) | ✅ | | 7.3 | 边界测试(空部署/强风/超长航路/空基弹药) | ✅ |
| 7.4 | 错误处理加固 | ⬜(基本校验已有) | | 7.4 | 错误处理加固 | ⬜(基本校验已有) |
@ -144,301 +132,16 @@ Phase 13 ✅ 模型重构:分层架构 + 代码审查 + 探测分离
| 指标 | 值 | | 指标 | 值 |
|------|------| |------|------|
| 测试总数 | **270**(全部通过) | | 测试总数 | **128** |
| 行覆盖率 | **95%+** | | 行覆盖率 | **95.4%** |
| 分支覆盖率 | **80%+** | | 分支覆盖率 | **80.5%** |
| 执行时间 | ~15 秒 | | 执行时间 | ~9 秒 |
| Core 程序集 | `CounterDrone.Core.dll` (.NET Standard 2.1) | | 项目文件 | `CounterDrone.Core.dll` (.NET Standard 2.1) |
| 共享物理工具类 | `Kinematics` / `RouteGeometry` / `CloudExpansionModel` / `DamageAssessment` | | 零 Unity 依赖 | ✅ 可脱离 Unity 独立运行和测试 |
| 全局配置 | `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 的 bugPhase3 用真实 `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.jsonSavePlannerConfig/SaveDefaults/Reload 无需重启 |
### 8.3 第三方对接
| # | 功能 | 说明 |
|---|------|------|
| 8.3.1 | 第三方 DLL 接口规范 | ⬜ 与供应商对齐 `ICloudDispersionModel` P/Invoke 签名和调用约定 |
| 8.3.2 | 扩散模型替换验证 | ⬜ 通过 `AlgorithmFactory.Register` 切换到第三方实现后的集成测试 |
---
## 五、Phase 9性能优化 + 架构文档校准 ✅
**目标**Tick 热路径性能优化Unity 满载 <20200+ FPS+ 总体架构设计文档与实现对齐
### 9.1 性能优化(✅ 已完成)
| # | 任务 | 状态 | 说明 |
|---|------|------|------|
| 9.1.1 | DroneEntity 航路几何缓存 | ✅ | 构造时预算 `_totalArc`/`_segLen[]`/`_cumArc[]`Update 内 O(1) 定位段,消除每帧重复几何运算 |
| 9.1.2 | 仿真期零字符串分配 | ✅ | EntitySnapshot 强类型字段流转JSON 推迟到 FlushUnity 端移除 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 球冠。
**前置设计**《总体架构设计》第十四章V135 项行为决策已确认(见 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.5s120km/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
T3planner 3D 适配 ⚠️耦合点)→ T4引擎实时扫描
T5前端/报告,可并行)
```
**总预估**:约 11hT1 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.PosYComputeParabolicRange 前向算时间;云团=到达位置 |
| 11.2.2 | 死代码清理 | CommandFlyTo/FlyingToTarget/ReadyToRelease + "到达投放点" 块删除 |
### 11.3 实体属性暴露
| # | 任务 | 说明 |
|---|------|------|
| 11.3.1 | EntitySnapshot + 速度 | 所有实体帧快照带 VelX/Y/ZCollectSnapshots 覆盖全部 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/*TemplateSQLite 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 等 |
---
## 七、里程碑
| 里程碑 | 状态 | | 里程碑 | 状态 |
|------|------| |------|------|
@ -447,27 +150,12 @@ T1数据层→ T2算法层 IsInCoverage
| M3 — 算法可计算 | ✅ | | M3 — 算法可计算 | ✅ |
| M4 — 仿真可运行 | ✅ | | M4 — 仿真可运行 | ✅ |
| M5 — 报告可生成 | ✅ | | M5 — 报告可生成 | ✅ |
| M6 — Unity 可演示 | | | M6 — Unity 可演示 | |
| M7 — 交付就绪 | ✅ | | M7 — 交付就绪 | ✅ |
| M8 — 天气/物理模型统一 | ✅ |
| M9 — 性能优化 + 文档校准 | ✅ |
| M10 — 探测实时链路 | ✅ |
| M11 — 运动学重构 + 实体暴露 + 基础数据 CRUD | ✅ |
| M12 — 空基/地基统一规划 + 编队轴 + LaneDivider | ✅ |
| M13 — PDF 导出 + 报告模板架构 | ✅ |
| M14 — 防御推荐 GetDefenseRecommendation + 配置运行时重载 | ✅ |
--- ---
## 九、待解决问题 ## 五、状态图例
| # | 问题 | 说明 | 状态 |
|---|------|------|------|
| 1 | **高弹道支持** | 当前只支持低弹道。喷气式ground-standard, 18000m 航线)需要高弹道才能命中。低弹道取上升段时间 t1弹在 6.5s/5199m 就触发了到达判定,实际应 15.5s/12318m 在下行段命中。需要修改 MunitionEntity 的到达判定逻辑 | 🔴 待解决 |
---
## 八、状态图例
| 符号 | 含义 | | 符号 | 含义 |
|------|------| |------|------|

View File

@ -1,35 +0,0 @@
# 天气纳入 Planner 规划与物理模型统一
- **日期**2026-06-14
- **提出人**tian
- **关联需求**:技术要求终版 2.2.3(云团扩散效果受气象条件影响)
- **优先级**:高
## 变更描述
将天气/风要素纳入 DefensePlanner 规划,并在此过程中发现并修复 planner 与仿真引擎物理模型分裂的根本问题。
### 起因
原 planner 未考虑天气对云团漂移的影响,有风场景下规划方案与仿真结果不一致。深入排查后发现根因不止于风偏补偿,而是 planner 大量本地重写了运动学/几何公式(直线距离、写死 X 轴 offset、`2R` 相切假设等与引擎的实际行为折线航路、云团移动、PathInSphere 几何)系统性偏离。
### 变更内容
1. **天气纳入扩散模型**:修复 `GaussianPuffDispersion` 写死 `WeatherType.Sunny` 的 bugPhase3 高斯扩散改用真实环境天气推导 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**+2441s 通过
- 活塞+西风 5m/s、空基+东风 5m/s 有风场景击毁成功(非边界)
- Z 向航路云团沿航路分布(不再写死 X 轴)
- L 形折线多 waypoint 运动正确

View File

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

View File

@ -31,7 +31,7 @@
类型、数量、挂载气溶胶的类型、数量(一发炮弹打出的云团大概覆盖体积) 类型、数量、挂载气溶胶的类型、数量(一发炮弹打出的云团大概覆盖体积)
根据算法自动推荐云团的抛洒位置、抛洒时机, 根据算法自动推荐晕图案的抛洒位置、抛洒时机,
最佳值和最危险值 最佳值和最危险值
]:支持自定义云团的抛撒位置、抛撒时机、风速风向参数。 ]:支持自定义云团的抛撒位置、抛撒时机、风速风向参数。
   2目标配置预置典型目标库包括多种无人机。    2目标配置预置典型目标库包括多种无人机。

File diff suppressed because it is too large Load Diff

View File

@ -14,27 +14,9 @@ Copy-Item "$projectRoot\unity_plugins\*.dll" "$unityProject\Assets\Plugins\Count
Remove-Item "$unityProject\Library\ScriptAssemblies" -Recurse -Force -ErrorAction SilentlyContinue Remove-Item "$unityProject\Library\ScriptAssemblies" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Compiling Unity scripts..." Write-Host "Compiling Unity scripts..."
# 3. 编译(-quit 确保编译完自动退出;超时 300s 兜底) # 3. 编译CompileErrorDetector 捕获错误并 Exit(-1)
Write-Host "Compiling Unity scripts (timeout 300s)..." & $unityEditor -batchmode -projectPath $unityProject -logFile - 2>&1 | Out-Null
$logFile = "$projectRoot\unity_build.log" if ($LASTEXITCODE -ne 0) { Write-Host "UNITY BUILD FAILED (exit code $LASTEXITCODE)"; exit 1 }
$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" Write-Host "ALL CHECKS PASSED"
exit 0 exit 0

View File

@ -3,33 +3,27 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms namespace CounterDrone.Core.Algorithms
{ {
/// <summary>吸入式爆炸 — 指数累积型</summary> /// <summary>吸入式爆炸 — 指数累积型:暴露时间越长伤害越高</summary>
public class ActiveFuelDamageModel : IDamageModel public class ActiveFuelDamageModel : IDamageModel
{ {
private const float BaseRate = 0.02f; private const float EffectiveThreshold = 0.001f; // 有效浓度阈值
private const float ExpFactor = 0.3f; private const float BaseRate = 0.02f; // 基础速率
private const float ExpFactor = 0.3f; // 指数增长因子
public float CalculateDamage(DroneType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{ {
if (aerosolType != AerosolType.ActiveFuel) return 0f; if (aerosolType != AerosolType.ActiveFuel) return 0f;
if (cloudDensity < EffectiveThreshold) return 0f;
// 伤害 = 基础速率 × e^(暴露时间 × 因子) × deltaTime
// 在云团中待得越久伤害指数增长
var exponential = (float)System.Math.Exp(exposureTime * ExpFactor); var exponential = (float)System.Math.Exp(exposureTime * ExpFactor);
var damage = BaseRate * exponential * deltaTime; var damage = BaseRate * exponential * deltaTime;
var sensitivity = droneType == DroneType.HighSpeed ? 1.5f : 1.0f;
return damage * sensitivity;
}
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType) // 高速目标更脆弱
{ var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f;
if (aerosolType != AerosolType.ActiveFuel) return float.MaxValue; return damage * sensitivity;
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) public DamageStage GetDamageStage(float accumulatedDamage)

View File

@ -6,15 +6,18 @@ namespace CounterDrone.Core.Algorithms
/// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary> /// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary>
public class ActiveMaterialDamageModel : IDamageModel public class ActiveMaterialDamageModel : IDamageModel
{ {
private const float BurstDamage = 0.85f; private const float TriggerThreshold = 0.0002f; // 触发浓度阈值
private const float ResidualRate = 0.05f; private const float BurstDamage = 0.85f; // 一次爆发伤害
private const float ResidualRate = 0.05f; // 后续余伤速率
private bool _triggered; private bool _triggered;
public float CalculateDamage(DroneType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{ {
if (aerosolType != AerosolType.ActiveMaterial) return 0f; if (aerosolType != AerosolType.ActiveMaterial) return 0f;
if (cloudDensity < TriggerThreshold) return 0f;
// 喷气发动机高温触发爆燃,更敏感
if (!_triggered) if (!_triggered)
{ {
_triggered = true; _triggered = true;
@ -25,19 +28,10 @@ namespace CounterDrone.Core.Algorithms
return ResidualRate * deltaTime; 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) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; 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; if (accumulatedDamage >= 0.2f) return DamageStage.EngineAnomaly;
return DamageStage.Normal; return DamageStage.Normal;
} }

View File

@ -10,6 +10,7 @@ namespace CounterDrone.Core.Algorithms
{ {
[typeof(ICloudDispersionModel)] = () => new GaussianPuffDispersion(), [typeof(ICloudDispersionModel)] = () => new GaussianPuffDispersion(),
[typeof(IDamageModel)] = () => new DamageModelRouter(), [typeof(IDamageModel)] = () => new DamageModelRouter(),
[typeof(IDefenseAdvisor)] = () => new DefaultDefenseAdvisor(null),
}; };
public static void Register<TInterface>(Func<object> factory) public static void Register<TInterface>(Func<object> factory)

View File

@ -1,98 +1,123 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using CounterDrone.Core;
using CounterDrone.Core.Models; using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms namespace CounterDrone.Core.Algorithms
{ {
/// <summary>火力单元(完整的武器系统)</summary> /// <summary>威胁画像</summary>
public class FireUnit public class ThreatProfile
{ {
public string Id { get; set; } = string.Empty; public CombatScene Environment { get; set; } = new();
public PlatformType Type { get; set; } public List<TargetConfig> Targets { get; set; } = new();
public Vector3 Position { get; set; } public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
// ── 发射装置 ── public List<ControlZone> ControlZones { get; set; } = new();
/// <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;
/// <summary>更换弹种时间(秒)</summary>
public float AmmoChangeTime { get; set; } = 30f;
// ── 搜索跟踪 ──
/// <summary>雷达探测距离m0 表示无此设备</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> /// <summary>防御推荐方案</summary>
public class DroneWave public class DefenseRecommendation
{ {
public string WaveId { get; set; } = string.Empty; public DefenseSolution Best { get; set; } = new();
public DroneSpec Profile { get; set; } = new(); public DefenseSolution Critical { get; set; } = new();
public int Quantity { get; set; } = 1; }
/// <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>
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 float Cooldown { get; set; } = 5f;
public float AmmoChangeTime { get; set; } = 300f;
// 弹药库:可以装填的弹药类型
public List<AerosolType> AvailableAmmo { get; set; } = new();
}
/// <summary>无人机编队(多编队推荐输入)</summary>
public class DroneGroup
{
public string GroupId { get; set; } = string.Empty;
public TargetConfig Target { get; set; } = new();
public RoutePlan Route { get; set; } = new(); public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new(); public List<Waypoint> Waypoints { get; set; } = new();
/// <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() public float GetArrivalTime()
{ {
if (Waypoints.Count < 2) return 0; if (Waypoints.Count < 2) return 0;
var s = Waypoints[0]; var start = new Vector3((float)Waypoints[0].PosX, (float)Waypoints[0].PosY, (float)Waypoints[0].PosZ);
var e = Waypoints[^1]; var end = new Vector3((float)Waypoints[^1].PosX, (float)Waypoints[^1].PosY, (float)Waypoints[^1].PosZ);
float midX = ((float)s.PosX + (float)e.PosX) / 2f; var speed = (float)Target.TypicalSpeed / 3.6f;
float midZ = ((float)s.PosZ + (float)e.PosZ) / 2f; return start.DistanceTo(end) / speed / 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> /// <summary>粒子渲染参数</summary>
public class ParticleParams public class ParticleParams
{ {
public float EmitRate { get; set; } = 100;
public float Opacity { get; set; } = 1.0f; public float Opacity { get; set; } = 1.0f;
public string ColorHex { get; set; } = "#FFFFFF";
public float SizeMultiplier { get; set; } = 1.0f; public float SizeMultiplier { get; set; } = 1.0f;
} }
@ -103,51 +128,5 @@ namespace CounterDrone.Core.Algorithms
public int PlatformIndex; public int PlatformIndex;
public float TargetX, TargetY, TargetZ; public float TargetX, TargetY, TargetZ;
public float MuzzleVelocity; 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();
} }
} }

View File

@ -1,85 +0,0 @@
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);
}
}
}

View File

@ -1,41 +0,0 @@
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;
}
}
}

View File

@ -9,7 +9,7 @@ namespace CounterDrone.Core.Algorithms
private readonly ActiveMaterialDamageModel _activeMaterial = new(); private readonly ActiveMaterialDamageModel _activeMaterial = new();
private readonly ActiveFuelDamageModel _activeFuel = new(); private readonly ActiveFuelDamageModel _activeFuel = new();
public float CalculateDamage(DroneType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{ {
return aerosolType switch return aerosolType switch
@ -21,17 +21,6 @@ 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) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -0,0 +1,59 @@
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];
}
}

View File

@ -0,0 +1,280 @@
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;
}
}
}

View File

@ -1,30 +0,0 @@
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);
}
}
}

View File

@ -1,579 +0,0 @@
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);
}
}
}

View File

@ -1,146 +0,0 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>探测计算工具——统一信息网络对威胁的探测能力评估。
/// 纯函数、无状态,与 Kinematics/RouteGeometry 同范式。
/// planner事前规划基于此估算威胁的最早发现点而非上帝视角的航路起点。</summary>
public static class DetectionCalculator
{
/// <summary>某探测源在给定能见度下的综合有效探测距离(水平面)。
/// 取雷达/光电/红外三者的最大有效距离(任一方式发现即算发现)。
/// 光电受 Visibility 衰减:有效 = 基准 × min(1, Visibility/基准)。
/// 雷达/红外不受能见度影响(当前范围)。</summary>
public static float EffectiveRange(float radarRange, float eoRange, float irRange, float visibility)
{
float effRadar = radarRange;
float effEO = eoRange > 0
? eoRange * Math.Min(1f, visibility / eoRange)
: 0f;
float effIR = irRange;
return Math.Max(effRadar, Math.Max(effEO, effIR));
}
/// <summary>统一信息网络对某威胁的最早探测点。
/// 沿航路以步长 ≤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;
}
}
}

View File

@ -1,17 +0,0 @@
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; }
}
}

View File

@ -12,19 +12,16 @@ namespace CounterDrone.Core.Algorithms
public class GaussianPuffDispersion : ICloudDispersionModel public class GaussianPuffDispersion : ICloudDispersionModel
{ {
private AmmunitionSpec _ammo = null!; private AmmunitionSpec _ammo = null!;
private CombatScene _env = null!;
private float _elapsed; private float _elapsed;
private float _currentRadius; private float _currentRadius;
private float _currentDensity; private float _currentDensity;
private Vector3 _center; private Vector3 _center;
private Vector3 _windVelocity; private Vector3 _windVelocity;
private bool _inPhase3; private bool _inPhase3;
private float _initialRadius;
public Vector3 Center => _center; public Vector3 Center => _center;
public float Radius => _currentRadius; public float Radius => _currentRadius;
public float CoreDensity => _currentDensity; public float CoreDensity => _currentDensity;
public float PeakDensity { get; private set; }
public float EffectiveRadius => _currentRadius; public float EffectiveRadius => _currentRadius;
public ParticleParams Particles { get; } = new(); public ParticleParams Particles { get; } = new();
public bool IsDissipated { get; private set; } public bool IsDissipated { get; private set; }
@ -34,17 +31,13 @@ namespace CounterDrone.Core.Algorithms
public void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime) public void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime)
{ {
_ammo = ammo; _ammo = ammo;
_env = env;
_elapsed = 0f; _elapsed = 0f;
_inPhase3 = false; _inPhase3 = false;
// Phase 1: 爆轰膨胀 → 初始半径
var w = (float)ammo.BurstChargeKg; var w = (float)ammo.BurstChargeKg;
if (w <= 0) _currentRadius = 3.3f * (float)Math.Pow(Math.Max(0.01, w), 0.32);
throw new ArgumentException($"BurstChargeKg 必须 > 0实际: {w}", nameof(ammo));
_initialRadius = 3.3f * (float)Math.Pow(w, 0.32);
_currentRadius = _initialRadius;
_currentDensity = (float)ammo.CoreDensity; _currentDensity = (float)ammo.CoreDensity;
PeakDensity = (float)ammo.CoreDensity;
_center = releasePos; _center = releasePos;
IsDissipated = false; IsDissipated = false;
@ -61,14 +54,16 @@ namespace CounterDrone.Core.Algorithms
var (vx, vy, vz) = Kinematics.WindToVector(windDir, windSpeed); var (vx, vy, vz) = Kinematics.WindToVector(windDir, windSpeed);
_windVelocity = new Vector3(vx, vy, vz); _windVelocity = new Vector3(vx, vy, vz);
if (!_inPhase3 && _elapsed >= (float)_ammo.Phase2Duration) // 判断阶段切换
if (!_inPhase3 && _elapsed >= 30f)
_inPhase3 = true; _inPhase3 = true;
if (!_inPhase3) if (!_inPhase3)
{ {
// Phase 2: 湍流扩散 R(t) = R₀ + k × √t // Phase 2: 湍流扩散 R(t) = R₀ + k × √t
var k = (float)_ammo.TurbulentExpansionK; var k = (float)_ammo.TurbulentExpansionK;
_currentRadius = _initialRadius + k * (float)Math.Sqrt(_elapsed); _currentRadius = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)_ammo.BurstChargeKg), 0.32)
+ k * (float)Math.Sqrt(_elapsed);
// 密度 = 源强 / 体积 // 密度 = 源强 / 体积
var volume = (4f / 3f) * (float)Math.PI * _currentRadius * _currentRadius * _currentRadius; var volume = (4f / 3f) * (float)Math.PI * _currentRadius * _currentRadius * _currentRadius;
_currentDensity = volume > 0.001f ? (float)_ammo.SourceStrength / volume : 0f; _currentDensity = volume > 0.001f ? (float)_ammo.SourceStrength / volume : 0f;
@ -76,12 +71,12 @@ namespace CounterDrone.Core.Algorithms
else else
{ {
// Phase 3: 高斯扩散 // Phase 3: 高斯扩散
float x = windSpeed * (_elapsed - (float)_ammo.Phase2Duration); var x = Math.Max(1f, windSpeed * (_elapsed - 30f));
if (x <= 0) return; // 尚未进入有效扩散距离 var cls = Kinematics.GetStabilityClass((WeatherType)0, windSpeed);
var cls = Kinematics.GetStabilityClass((WeatherType)_env.WeatherType, windSpeed);
var sY = Kinematics.SigmaY(cls, x); var sY = Kinematics.SigmaY(cls, x);
var sZ = Kinematics.SigmaZ(cls, x); var sZ = Kinematics.SigmaZ(cls, x);
_currentDensity = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ); _currentDensity = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ);
// 有效半径从浓度反推
var effConc = (float)_ammo.EffectiveConcentration; var effConc = (float)_ammo.EffectiveConcentration;
if (_currentDensity > effConc) if (_currentDensity > effConc)
{ {
@ -95,11 +90,11 @@ namespace CounterDrone.Core.Algorithms
_center.Y += _windVelocity.Y * deltaTime; _center.Y += _windVelocity.Y * deltaTime;
_center.Z += _windVelocity.Z * deltaTime; _center.Z += _windVelocity.Z * deltaTime;
// 粒子参数(仅用于可视化,不参与物理计算) // 粒子参数
float coreDensity = (float)_ammo.CoreDensity; Particles.Opacity = Math.Max(0.1f, _currentDensity / (float)_ammo.CoreDensity);
Particles.Opacity = coreDensity > 0 ? _currentDensity / coreDensity : 0f; Particles.SizeMultiplier = _currentRadius / Math.Max(0.5f, 3.3f * (float)Math.Pow(Math.Max(0.01, (float)_ammo.BurstChargeKg), 0.32));
Particles.SizeMultiplier = _initialRadius > 0 ? _currentRadius / _initialRadius : 0f;
// 消散
if (_elapsed >= (float)_ammo.MaxDuration || _currentRadius >= (float)_ammo.MaxRadius) if (_elapsed >= (float)_ammo.MaxDuration || _currentRadius >= (float)_ammo.MaxRadius)
{ {
IsDissipated = true; IsDissipated = true;

View File

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

View File

@ -0,0 +1,17 @@
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);
}
}

View File

@ -1,17 +0,0 @@
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);
}
}

View File

@ -1,14 +0,0 @@
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);
}
}

View File

@ -3,26 +3,23 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms namespace CounterDrone.Core.Algorithms
{ {
/// <summary>吸入式灭火 — 累积伤害</summary> /// <summary>吸入式灭火 — 阈值型:密度达标后线性累积</summary>
public class InertGasDamageModel : IDamageModel 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(DroneType droneType, PowerType powerType, public float CalculateDamage(TargetType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{ {
if (aerosolType != AerosolType.InertGas) return 0f; if (aerosolType != AerosolType.InertGas) return 0f;
if (cloudDensity < EffectiveThreshold) return 0f;
// 活塞发动机对惰性气体最敏感
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f; var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
return DamageRate * sensitivity * deltaTime; 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) public DamageStage GetDamageStage(float accumulatedDamage)
{ {
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed; if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -1,118 +0,0 @@
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));
}
}
}

View File

@ -27,88 +27,32 @@ namespace CounterDrone.Core.Algorithms
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed); return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
} }
/// <summary>给定水平距离和初速,求解抛物线发射角(逆问题)</summary> /// <summary>计算抛物线炮弹的发射角</summary>
/// <param name="range">水平距离 (m),必须 > 0</param> /// <param name="range">水平距离 (m)</param>
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param> /// <param name="muzzleVelocity">初速 (m/s)</param>
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param> /// <param name="releaseAltitude">释放高度 (m),弹道顶点必须 ≥ 此值</param>
/// <returns>发射角 (rad),取低弹道</returns> /// <returns>发射角 (rad),取低弹道</returns>
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float heightDiff) public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
{ {
if (range <= 0) var v2 = muzzleVelocity * muzzleVelocity;
throw new ArgumentException($"range 必须 > 0实际: {range}", nameof(range)); var g = 9.81f;
if (muzzleVelocity <= 0)
throw new ArgumentException($"muzzleVelocity 必须 > 0实际: {muzzleVelocity}", nameof(muzzleVelocity)); // 1. 射程所需角θr = arcsin(R*g/v²) / 2
var angle = ParabolicMotion.SolveAngle(range, heightDiff, muzzleVelocity, TrajectoryPreference.Low); var rangeRatio = range * g / v2;
if (!angle.HasValue) var angleRange = rangeRatio >= 1.0f
throw new ArgumentException( ? 45f * (float)Math.PI / 180f
$"当前参数无法命中目标: range={range}, heightDiff={heightDiff}, v₀={muzzleVelocity}"); : (float)Math.Asin(rangeRatio) / 2f;
return angle.Value;
// 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);
} }
/// <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( public static (float X, float Y, float Z) ParabolicPosition(
float startX, float startY, float startZ, float startX, float startY, float startZ,
float launchAngle, float azimuth, float launchAngle, float azimuth,
@ -126,60 +70,6 @@ namespace CounterDrone.Core.Algorithms
return (x, y, z); 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> /// <summary>点到矩形距离(简化判定)</summary>
public static float Distance2D(float x1, float z1, float x2, float z2) public static float Distance2D(float x1, float z1, float x2, float z2)
{ {
@ -188,18 +78,6 @@ namespace CounterDrone.Core.Algorithms
return (float)Math.Sqrt(dx * dx + dz * dz); 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> /// <summary>点到三维点距离</summary>
public static float Distance3D(float x1, float y1, float z1, float x2, float y2, float z2) public static float Distance3D(float x1, float y1, float z1, float x2, float y2, float z2)
{ {
@ -258,6 +136,16 @@ namespace CounterDrone.Core.Algorithms
var denom = (float)Math.Pow(2f * (float)Math.PI, 1.5f) * sigmaY * sigmaY * sigmaZ; var denom = (float)Math.Pow(2f * (float)Math.PI, 1.5f) * sigmaY * sigmaY * sigmaZ;
return denom > 0.001f ? sourceStrength / denom : 0f; 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) public static bool PointInPolygon(float px, float pz, ReadOnlySpan<(float X, float Z)> vertices)
{ {
if (vertices.Length < 3) return false; if (vertices.Length < 3) return false;

View File

@ -1,109 +0,0 @@
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
};
}
}
}

View File

@ -1,89 +0,0 @@
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,
};
}
}

View File

@ -1,132 +0,0 @@
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);
}
}
}

View File

@ -1,9 +0,0 @@
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; }
}
}

View File

@ -7,13 +7,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Json" Version="9.0.0" /> <PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -3,8 +3,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using CounterDrone.Core.Models; using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
using CounterDrone.Core.Services;
using SQLite; using SQLite;
namespace CounterDrone.Core namespace CounterDrone.Core
@ -26,91 +24,63 @@ namespace CounterDrone.Core
var db = new SQLiteConnection(dbPath); var db = new SQLiteConnection(dbPath);
CreateMainTables(db); CreateMainTables(db);
SeedDefaultData(db); SeedDefaultData(db);
SeedDefaultScenarios(db);
return db; return db;
} }
/// <summary>创建/打开任务帧数据库</summary> /// <summary>创建/打开任务帧数据库</summary>
public SQLiteConnection OpenFrameDb(string scenarioId) public SQLiteConnection OpenFrameDb(string taskId)
{ {
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db"); var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
var db = new SQLiteConnection(dbPath); var db = new SQLiteConnection(dbPath);
db.CreateTable<SimFrameRecord>(); db.CreateTable<SimFrameRecord>();
db.CreateIndex("SimFrameRecord", new[] { "ScenarioId", "FrameIndex" }); db.CreateIndex("SimFrameRecord", new[] { "TaskId", "FrameIndex" });
return db; return db;
} }
/// <summary>删除任务帧数据库文件</summary> /// <summary>删除任务帧数据库文件</summary>
public void DeleteFrameDb(string scenarioId) public void DeleteFrameDb(string taskId)
{ {
var dbPath = Path.Combine(_paths.GetFramesDir(), $"{scenarioId}.db"); var dbPath = Path.Combine(_paths.GetFramesDir(), $"{taskId}.db");
if (File.Exists(dbPath)) if (File.Exists(dbPath))
File.Delete(dbPath); File.Delete(dbPath);
} }
private void SeedDefaultData(SQLiteConnection db) private void SeedDefaultData(SQLiteConnection db)
{ {
if (db.Table<AmmunitionSpec>().Count() > 0) if (db.Table<AmmunitionSpec>().Count() > 0) return;
return;
var defaults = DefaultData.Load(_paths); var jsonPath = Path.Combine(_paths.GetDataRoot(), "default_ammo.json");
foreach (var a in defaults.Ammunition) db.InsertOrReplace(a); if (!File.Exists(jsonPath)) return;
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);
}
}
private void SeedDefaultScenarios(SQLiteConnection db) var specs = JsonSerializer.Deserialize<List<AmmunitionSpec>>(File.ReadAllText(jsonPath));
{ if (specs != null)
if (db.Table<Scenario>().Count() > 0) foreach (var s in specs) db.Insert(s);
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) private void CreateMainTables(SQLiteConnection db)
{ {
db.CreateTable<ModelInfo>(); db.CreateTable<ModelInfo>();
db.CreateTable<AmmunitionSpec>(); db.CreateTable<AmmunitionSpec>();
db.CreateTable<Scenario>(); db.CreateTable<SimTask>();
db.CreateTable<CombatScene>(); db.CreateTable<CombatScene>();
db.CreateTable<ControlZone>(); db.CreateTable<ControlZone>();
db.CreateTable<ScenarioDrone>(); db.CreateTable<TargetConfig>();
db.CreateTable<ScenarioUnit>(); db.CreateTable<EquipmentDeployment>();
db.CreateTable<CloudDispersal>(); db.CreateTable<CloudDispersal>();
db.CreateTable<RoutePlan>(); db.CreateTable<RoutePlan>();
db.CreateIndex("RoutePlan", new[] { "ScenarioId", "WaveId" }, true); db.CreateIndex("RoutePlan", new[] { "TaskId", "GroupId" }, true);
db.CreateTable<Waypoint>(); db.CreateTable<Waypoint>();
db.CreateTable<Group>();
db.CreateTable<SimulationReport>(); db.CreateTable<SimulationReport>();
db.CreateTable<FireUnitSpec>(); db.CreateTable<SimEvent>();
db.CreateTable<LaunchPlatformSpec>();
db.CreateTable<DroneSpec>();
db.CreateTable<SensorSpec>();
db.CreateTable<EnvironmentSpec>();
db.CreateTable<FormationTemplate>();
db.CreateTable<RouteTemplate>();
db.CreateIndex("Scenario", "ScenarioNumber"); db.CreateIndex("SimTask", "TaskNumber");
db.CreateIndex("ControlZone", "ScenarioId"); db.CreateIndex("ControlZone", "TaskId");
db.CreateIndex("ScenarioDrone", "ScenarioId"); db.CreateIndex("TargetConfig", "TaskId");
db.CreateIndex("ScenarioUnit", "ScenarioId"); db.CreateIndex("EquipmentDeployment", "TaskId");
db.CreateIndex("Waypoint", "ScenarioId"); db.CreateIndex("Waypoint", "TaskId");
db.CreateIndex("SimulationReport", "ScenarioId"); db.CreateIndex("SimEvent", "TaskId");
db.CreateIndex("SimulationReport", "TaskId");
} }
} }
} }

View File

@ -1,82 +0,0 @@
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/HasIRJSON 可能未显式设置)
foreach (var s in data.Sensors)
{
s.HasRadar = s.RadarRange > 0;
s.HasEO = s.EORange > 0;
s.HasIR = s.IRRange > 0;
}
}
}
}

View File

@ -1,156 +0,0 @@
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);
}
}
}

View File

@ -7,6 +7,5 @@ namespace CounterDrone.Core
string GetMainDbPath(); string GetMainDbPath();
string GetFramesDir(); string GetFramesDir();
string GetModelsDir(); string GetModelsDir();
string GetFontPath();
} }
} }

View File

@ -42,9 +42,6 @@ namespace CounterDrone.Core.Models
/// <summary>湍流扩散系数Phase 2 中 R=R₀+k√t 的 k 值</summary> /// <summary>湍流扩散系数Phase 2 中 R=R₀+k√t 的 k 值</summary>
public double TurbulentExpansionK { get; set; } = 5.0; 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 MaxRadius { get; set; }
public double MaxDuration { get; set; } public double MaxDuration { get; set; }

View File

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

View File

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

View File

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

View File

@ -1,35 +0,0 @@
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 模型 IDFK → ModelInfoUnity 可视化用</summary>
public string ModelId { get; set; } = "";
public ScenarioDrone ToScenarioDrone(string waveId = "default")
{
return new ScenarioDrone
{
DroneSpecId = Id,
WaveId = waveId,
Quantity = 1,
};
}
}
}

View File

@ -1,7 +1,7 @@
namespace CounterDrone.Core.Models namespace CounterDrone.Core.Models
{ {
// === 任务 === // === 任务 ===
public enum ScenarioStatus public enum TaskStatus
{ {
Draft = 0, Draft = 0,
Configuring = 1, Configuring = 1,
@ -33,11 +33,13 @@ namespace CounterDrone.Core.Models
} }
// === 目标 === // === 目标 ===
public enum DroneType public enum TargetType
{ {
Rotor = 0, Rotor = 0,
FixedWing = 1, FixedWing = 1,
HighSpeed = 2 Electric = 2,
Piston = 3,
HighSpeed = 4
} }
public enum PowerType public enum PowerType
@ -60,13 +62,6 @@ namespace CounterDrone.Core.Models
LaunchPlatform = 1 LaunchPlatform = 1
} }
public enum SensorType
{
Radar = 0,
EO = 1,
IR = 2
}
// === 气溶胶 & 毁伤 === // === 气溶胶 & 毁伤 ===
public enum AerosolType public enum AerosolType
{ {
@ -97,7 +92,7 @@ namespace CounterDrone.Core.Models
Destroyed = 3 Destroyed = 3
} }
// === 编队 & 批次 === // === 编队 & 编组 ===
public enum FormationMode public enum FormationMode
{ {
Single = 0, Single = 0,
@ -105,6 +100,12 @@ namespace CounterDrone.Core.Models
Swarm = 2 Swarm = 2
} }
public enum GroupType
{
DroneFleet = 0,
EquipmentGroup = 1
}
// === 运行时 === // === 运行时 ===
public enum EntityType public enum EntityType
{ {
@ -125,8 +126,7 @@ namespace CounterDrone.Core.Models
DroneReachedTarget = 5, DroneReachedTarget = 5,
ZoneIntruded = 6, ZoneIntruded = 6,
WaypointReached = 7, WaypointReached = 7,
SimulationEnd = 8, SimulationEnd = 8
PlanningFailed = 9
} }
// === 报告 === // === 报告 ===

View File

@ -1,35 +0,0 @@
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,
};
}
}
}

View File

@ -0,0 +1,44 @@
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; }
}
}

View File

@ -1,52 +0,0 @@
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 模型 IDFK → ModelInfoUnity 可视化用</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,
};
}
}
}

View File

@ -1,35 +0,0 @@
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,
};
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>编组</summary>
[Table("Group")]
public class Group
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[NotNull]
public string Name { get; set; } = string.Empty;
public int GroupType { get; set; } = (int)Models.GroupType.DroneFleet;
public string Description { get; set; } = string.Empty;
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("o");
}
}

View File

@ -1,44 +0,0 @@
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,
};
}
}
}

View File

@ -13,10 +13,7 @@ namespace CounterDrone.Core.Models
[NotNull] [NotNull]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
/// <summary>对应 EntityType 枚举0=Drone 1=Platform 2=DetectionEquip</summary> public string ModelType { get; set; } = string.Empty;
public int EntityType { get; set; }
public string Description { get; set; } = string.Empty;
public string FilePath { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty;

View File

@ -1,14 +0,0 @@
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;
}
}

View File

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

View File

@ -1,37 +0,0 @@
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; }
}
}

View File

@ -1,19 +0,0 @@
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();
}
}

View File

@ -1,24 +0,0 @@
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;
}
}

View File

@ -1,44 +0,0 @@
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;
}
}

View File

@ -1,46 +0,0 @@
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 模型 IDFK → ModelInfoUnity 可视化用</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,
};
}
}
}

View File

@ -0,0 +1,28 @@
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;
}
}

View File

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

View File

@ -4,8 +4,8 @@ using SQLite;
namespace CounterDrone.Core.Models namespace CounterDrone.Core.Models
{ {
/// <summary>仿真任务主表</summary> /// <summary>仿真任务主表</summary>
[Table("Scenario")] [Table("SimTask")]
public class Scenario public class SimTask
{ {
[PrimaryKey] [PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString(); public string Id { get; set; } = Guid.NewGuid().ToString();
@ -14,11 +14,9 @@ namespace CounterDrone.Core.Models
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
[Unique] [Unique]
public string ScenarioNumber { get; set; } = string.Empty; public string TaskNumber { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty; public int Status { get; set; } = (int)TaskStatus.Draft;
public int Status { get; set; } = (int)ScenarioStatus.Draft;
public int CurrentStep { get; set; } = 1; public int CurrentStep { get; set; } = 1;

View File

@ -11,24 +11,21 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString(); public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed] [Indexed]
public string ScenarioId { get; set; } = string.Empty; public string TaskId { get; set; } = string.Empty;
public string ScenarioName { get; set; } = string.Empty; public string TaskName { get; set; } = string.Empty;
public string ScenarioNumber { get; set; } = string.Empty; public string TaskNumber { get; set; } = string.Empty;
public string CompletedAt { get; set; } = string.Empty; public string CompletedAt { get; set; } = string.Empty;
public int DroneCount { get; set; } public int TargetCount { get; set; }
public int UnitCount { get; set; } public int EquipmentCount { get; set; }
public int InterceptResult { get; set; } public int InterceptResult { get; set; }
/// <summary>Markdown 文本(用于快速展示)</summary> /// <summary>报告富文本 / 结构化 JSON</summary>
public string Content { get; set; } = string.Empty; public string Content { get; set; } = string.Empty;
/// <summary>结构化报告数据 JSON用于 PDF 等格式重新渲染)</summary>
public string? ReportDataJson { get; set; }
} }
} }

View File

@ -0,0 +1,30 @@
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;
}
}

View File

@ -0,0 +1,29 @@
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;
}
}

View File

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

View File

@ -1,40 +0,0 @@
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;
}
}

View File

@ -1,8 +0,0 @@
namespace CounterDrone.Core.Reporting
{
/// <summary>报告模板接口 — 从 ReportData 渲染为目标格式</summary>
public interface IReportTemplate
{
byte[] Render(ReportData data);
}
}

View File

@ -1,79 +0,0 @@
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);
}
}
}

View File

@ -1,55 +0,0 @@
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();
}
}
}

View File

@ -1,50 +0,0 @@
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; }
}
}

View File

@ -1,377 +0,0 @@
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);
}
}
}

View File

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

View File

@ -1,11 +0,0 @@
using CounterDrone.Core;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Repository
{
public class DroneSpecRepository : BaseRepository<DroneSpec>
{
public DroneSpecRepository(SQLiteConnection db) : base(db) { }
}
}

View File

@ -1,11 +0,0 @@
using CounterDrone.Core;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Repository
{
public class EnvironmentSpecRepository : BaseRepository<EnvironmentSpec>
{
public EnvironmentSpecRepository(SQLiteConnection db) : base(db) { }
}
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class 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);
}
}
}

View File

@ -1,11 +0,0 @@
using CounterDrone.Core;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Repository
{
public class FireUnitSpecRepository : BaseRepository<FireUnitSpec>
{
public FireUnitSpecRepository(SQLiteConnection db) : base(db) { }
}
}

View File

@ -1,11 +0,0 @@
using CounterDrone.Core;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Repository
{
public class FormationTemplateRepository : BaseRepository<FormationTemplate>
{
public FormationTemplateRepository(SQLiteConnection db) : base(db) { }
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class GroupRepository : BaseRepository<Group>
{
public GroupRepository(SQLiteConnection db) : base(db) { }
public List<Group> GetByType(int groupType)
{
return Db.Table<Group>()
.Where(g => g.GroupType == groupType)
.ToList();
}
}
}

View File

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

View File

@ -1,11 +0,0 @@
using CounterDrone.Core;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Repository
{
public class RouteTemplateRepository : BaseRepository<RouteTemplate>
{
public RouteTemplateRepository(SQLiteConnection db) : base(db) { }
}
}

View File

@ -1,24 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class 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);
}
}
}

View File

@ -1,32 +0,0 @@
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);
}
}
}

View File

@ -1,11 +0,0 @@
using CounterDrone.Core;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Repository
{
public class SensorSpecRepository : BaseRepository<SensorSpec>
{
public SensorSpecRepository(SQLiteConnection db) : base(db) { }
}
}

View File

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

View File

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

View File

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

View File

@ -1,93 +0,0 @@
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);
}
}
}
}

View File

@ -1,74 +0,0 @@
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);
}
}

View File

@ -1,46 +0,0 @@
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; }
}
}

View File

@ -1,30 +0,0 @@
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