Compare commits

...

202 Commits

Author SHA1 Message Date
36ab4fad7b 配置运行时重载 IConfigService + 防御推荐 + 文档更新
新增:
- IConfigService / ConfigService:读写 defaults.json / planner_config.json
- SavePlannerConfig / SaveDefaults / Reload,无需重启 Unity
- PlannerConfigReloaded 事件 → ScenarioManager 自动重建 planner
- SimulationEngine.UpdatePlanner / ScenarioService.UpdatePlanner
- SimulationRunner.ReloadPlanner 桥接
- DefenseRecommendation / RecommendOption 返回类型
- IScenarioService.GetDefenseRecommendation(配置阶段生成最佳抛撒参数)
- ConfigServiceTests 5 个测试 + DefenseRecommendationTests 3 个测试

修复:
- 云团间距半径基准不一致(间距用 R30、穿越用 R27 → 统一用 effectiveRadius)
- 消除硬编码 30f(DefensePlanner + GaussianPuffDispersion 读 Phase2Duration)
- TryGenerateFireEvents 复用 ComputeEffectiveRadius,消除重复计算
- 概率计算间距加重叠系数
- DatabaseManager 不再 DROP SimulationReport(保留历史报告)
- 清理迁移代码(MetaEntry / 旧表 DROP / 版本检查)

文档:
- 对接文档 V2.1:加 IConfigService API + 配置重载用法 + 修正 DLL 更新说明
- 实施跟踪 V1.8:8.2.1 更新 + 新增 8.2.3/8.2.4 + M14 里程碑
- 270 测试通过,Unity 编译通过
2026-06-25 22:10:22 +08:00
a3ceae5c14 清理数据库迁移代码 + 保留历史报告
- 删除 DROP TABLE IF EXISTS SimulationReport(不再每次启动清空报告)
- 删除 4 个旧表 DROP 语句(SimTask/TargetConfig/DroneProfile/EquipmentDeployment)
- 删除 MetaEntry 版本追踪表和模型文件(开发阶段不需要)
- SeedDefaultData 改为简单判断:有数据就跳过,无数据就插入
- SeedDefaultScenarios 改为简单判断:有想定就跳过,无就种子
- 265 测试通过,Unity 编译通过
2026-06-25 21:58:05 +08:00
dc62ba8c82 修复云团间距半径基准不一致 + 消除硬编码 30f
逻辑层修复(前端反馈的云团缝隙问题):
- ComputeEffectiveRadius 应用 ExpansionFactor,返回穿越时刻半径
- 间距/车道/弹药数统一用 effectiveRadius(原用 turbulentRadius=R30)
- TryGenerateFireEvents 复用 ComputeEffectiveRadius,消除重复计算
- 概率计算间距也加重叠系数
- GaussianPuffDispersion 消除硬编码 30f,读 _ammo.Phase2Duration
- DefensePlanner 消除硬编码 30f,读 model.Phase2Duration
- 265 测试通过,Unity 编译通过
2026-06-25 21:53:34 +08:00
9d81ba0b28 新增 GetDefenseRecommendation 防御推荐方案 + 去掉 Critical
- IScenarioService 新增 GetDefenseRecommendation(scenarioId)
- ScenarioService 构造函数加可选 IPathProvider + IDefensePlanner
- DefenseRecommendation / RecommendOption 返回类型
- 前端调用获取最佳抛撒参数 → 用户一键应用写入 CloudDispersal
- 去掉 Critical(省弹药到 50% 概率,无决策价值)
- Unity ScenarioManager 桥接 + 构造注入 planner
- 对接文档加推荐用法示例
- 265 测试通过(+3 推荐),Unity 编译通过
2026-06-22 08:05:09 +08:00
0002e7efbe SensorSpec +HasRadar/HasEO/HasIR 显式类型字段
- SensorSpec 新增三个 bool 字段,前端直接读取探测设备类型,无需推断
- DefaultData.Load 后由 Range 字段派生填充
- 支持复合传感器(如光电+红外 eo-station)
- 对接文档同步更新
- 262 测试通过,Unity 编译通过
2026-06-21 10:39:14 +08:00
a6ad8a348e GetEnums() +SensorType 枚举(雷达/光电/红外)
- 新增 SensorType 枚举(Radar=0, EO=1, IR=2)供前端探测设备 UI 分类
- EnumMetadata + GetEnums() 加入 SensorType(含中文翻译)
- 对接文档同步更新
- 262 测试通过,Unity 编译通过
2026-06-21 10:33:57 +08:00
fb3c6fd2eb EnumMetadata +EntityType + ScenarioUnit +AmmunitionSpecId
- GetEnums() 新增 EntityType 枚举(模型管理 UI 区分 Drone/Platform/DetectionEquip)
- ScenarioUnit 新增 AmmunitionSpecId 字段(FK → AmmunitionSpec,前端弹药配置用)
- 对接文档同步更新
- 262 测试通过,Unity 编译通过
2026-06-20 22:33:12 +08:00
6f83cfb105 对接文档:GetEnums +EntityType(12 个枚举) 2026-06-20 12:10:36 +08:00
e9c10bfe7d GetEnums +EntityType 枚举(Drone/Platform/DetectionEquip/Cloud/Munition) 2026-06-20 12:04:51 +08:00
8bcf4cf675 VERSION 0.12.0 — PDF 导出 + 报告模板架构 + 对接文档 V2.0
- PdfSharpCore 1.3.64 引入(纯托管,Unity IL2CPP 兼容,+9 DLL)
- ReportData 结构化模型 + MarkdownRenderer + StandardPdfTemplate
- IReportService.ExportReport(id, format) 按需导出 PDF/MD
- 仿真后自动生成 MD 到 reports 目录,PDF 按需调用
- CJK 字体嵌入(SimHei),IPathProvider + GetFontPath
- ReportGenerator 重构为构建 ReportData,去掉 emoji
- check_unity_build.ps1 修复 -quit 参数缺失导致超时
- 对接文档 V2.0:坐标系/3D 可视化/完整枚举/Manager 签名/模型字段
- 262 测试全部通过
2026-06-20 11:57:38 +08:00
f77fb88487 Unity 脚本适配:FireUnitSpecId→LaunchPlatformSpecId + DLL 更新 2026-06-19 21:56:41 +08:00
3dc56e4a6e 实施文档 +Phase 13:模型重构/LaunchPlatformSpec/代码审查 2026-06-19 21:55:28 +08:00
1db632676d 架构设计文档:核心作战概念更新(发射平台/探测设备分离) 2026-06-19 21:54:26 +08:00
bebc03ee43 对接文档更新:LaunchPlatformSpec 替换 FireUnitSpec 2026-06-19 21:53:02 +08:00
dabf367b21 VERSION 0.11.0 — LaunchPlatformSpec 分离 + 代码审查 + 250 测试
- LaunchPlatformSpec:纯发射参数,SensorSpec 统一探测
- 代码审查:拆分 DefaultData/AlgorithmTypes/SpecRepositories
- GetEnums 中英文对照 / ModelId 3D 引用
- DataServiceTests +12 / 死代码清除
- 250/250 全过
2026-06-19 21:50:10 +08:00
b23b19e4e9 LaunchPlatformSpec 适配完成:预设场景+测试加 SensorSpecId,250 全过
- DefaultScenarios 每个部署加 AddDetection
- TestData.CreateScenarioUnit 默认带 radar-sr
- FullPipelineTests 探测测试加 SensorSpec 引用
- 发射平台与探测设备完全分离
2026-06-19 21:46:54 +08:00
8a198a27a0 新增 LaunchPlatformSpec:发射平台与探测设备分离
- LaunchPlatformSpec(纯发射参数,无探测字段)
- ScenarioUnit.FireUnitSpecId → LaunchPlatformSpecId
- PlatformEntity/BuildFireUnits/BuildDetectionSources 适配
- ReportGenerator/ReportService 适配
- FireUnitSpec 保留但不使用
- 243/250 通过,7 个待修
2026-06-19 21:43:06 +08:00
8abaf0bf87 对接文档:GetEnums 返回 List<EnumItem> 中英文对照 2026-06-19 21:28:40 +08:00
4b2677e224 GetEnums 返回中英文对照:EnumItem{Name,ChineseName,Value} 替代 Dictionary<string,int>
- EnumMetadata 每个枚举从 Dictionary 改为 List<EnumItem>
- ScenarioService 内置中文翻译(与 ReportGenerator 统一)
- 250 测试全过
2026-06-19 21:24:51 +08:00
2a062c07bd +DataServiceTests (12 个) / 修复 FormationTemplate 缺 PrimaryKey / 删除死代码 2026-06-19 10:35:42 +08:00
d3248d80a3 删除死代码:MunitionEntity._hasExceededReleaseAltitude 2026-06-19 10:32:33 +08:00
2adacffe85 拆分 AlgorithmTypes: Vector3/DetectionSource 独立文件;SpecRepositories 拆分为独立文件;命名空间修正 2026-06-19 10:30:34 +08:00
62b718fa1e AGENTS.md: 提交前必须编译和复制 DLL 2026-06-19 10:28:17 +08:00
fc0a6b0397 Spec 类命名空间修正:CounterDrone.Core → CounterDrone.Core.Models
- DroneSpec/FireUnitSpec/SensorSpec/EnvironmentSpec/FormationTemplate/RouteTemplate
- EnumMetadata 移到 Services 目录
- 238 测试全过
2026-06-19 10:26:48 +08:00
f6d3f1bdb4 拆分 DefaultData.cs:Spec 类各自独立文件
- FireUnitSpec → Models/FireUnitSpec.cs
- DroneSpec → Models/DroneSpec.cs
- SensorSpec → Models/SensorSpec.cs
- EnvironmentSpec → Models/EnvironmentSpec.cs
- FormationTemplate → Models/FormationTemplate.cs
- RouteTemplate → Models/RouteTemplate.cs
- DefaultData.cs 仅保留 DefaultData 加载类
- 238 测试全过
2026-06-19 10:21:41 +08:00
dbe6808df7 更新 Unity DLL 2026-06-19 10:08:05 +08:00
7383b67fca 对接文档:ModelId/EntityType/FilePath 补充 + Model 注释说明 2026-06-19 10:07:01 +08:00
43e70a7482 对接文档 + DroneSpec.Model 注释:区分型号名称与 3D 模型引用 2026-06-19 10:06:37 +08:00
846d79a8d7 ModelInfo: ModelType→EntityType(枚举) + Description;ImportModel 支持传参 2026-06-19 10:02:09 +08:00
c8e066be64 仿真实体 +ModelId:DroneSpec/FireUnitSpec/SensorSpec → EntitySnapshot
- DroneSpec/FireUnitSpec/SensorSpec 加 ModelId (FK→ModelInfo)
- DroneEntity/PlatformEntity/DetectionEntity 内部带 ModelId
- DetectionSource +ModelId
- EntitySnapshot +ModelId,每帧推送给 Unity
- CollectSnapshots 传播所有实体 ModelId
2026-06-19 09:59:55 +08:00
7415d07d9f 更新 Unity DLL 2026-06-18 19:52:51 +08:00
d8f8ebd336 ModelManager.Update→UpdateModel(避免与 MonoBehaviour.Update 冲突) 2026-06-18 19:50:39 +08:00
ce11e0eb1f 修复 Unity 编译:EnumMetadata using + nullable 警告 2026-06-18 19:49:38 +08:00
efdd5ec94f 对接文档 V1.8:GetEnums API + Manager 完整方法列表 2026-06-18 19:46:53 +08:00
1fdffd2a41 GetEnums API:返回所有枚举定义供前端下拉框使用 2026-06-18 19:46:34 +08:00
178923c007 对接文档同步:完整 CRUD API 列表 2026-06-18 19:44:53 +08:00
5e7fd2fb16 IScenarioService +DeleteCloudDispersal 2026-06-18 19:44:29 +08:00
225590ac19 补全所有模型的完整 CRUD 接口
- ModelService: +AddModel/UpdateModel
- IScenarioService: +UpdateScenario +GetScene +Get/DeleteControlZone
  +Add/Update/Delete/GetScenarioDrones +Add/Update/Delete/GetDeploymentUnits
  +GetCloudDispersal +GetRoutes/DeleteRoute +GetWaypoints/GetWaypointsByWave/DeleteWaypoint
- ScenarioManager 全面暴露新增接口
- 238 测试全过
2026-06-18 19:42:48 +08:00
d73b586982 更新 Unity DLL(ScenarioDrone/ScenarioUnit 新表结构) 2026-06-18 17:50:42 +08:00
e30f173312 修复 Unity Manager 脚本:适配 ScenarioDrone/ScenarioUnit 新 schema 2026-06-18 17:50:15 +08:00
d2d0ae1414 air-standard 探测参数调整:雷达4km/光电3km/红外2km 2026-06-18 17:35:59 +08:00
bba7d2e505 air-standard 补 RadarRange=8000m 2026-06-18 17:34:33 +08:00
473890ed37 defaults.json: 补全 DroneSpec/FireUnitSpec 的 Model/Description 字段 2026-06-18 17:31:56 +08:00
184213b3c5 对接文档 V1.7:数据分层架构 + ScenarioDrone/ScenarioUnit 外键引用 2026-06-18 17:28:29 +08:00
1aa927e471 修复 ScenarioServiceTests 编译告警(CS8602/CS8625 空引用) 2026-06-18 17:26:58 +08:00
f0f5f76b8b SimulationReport: TargetCount→DroneCount, EquipmentCount→UnitCount 2026-06-18 17:25:02 +08:00
843141a7a8 修复测试断言:适配新 schema 的业务语义
- SaveDeployment: 显式 Guid 避免同纳秒冲突,验证 2 个部署正确保存
- DeleteDetection: 用 CreateDetectionUnit 创建传感器,verifying PositionX
- TargetDetected: 验证发射平台自带雷达可触发探测事件
2026-06-18 17:20:59 +08:00
4b08ffa659 238/238 tests pass: ScenarioDrone/ScenarioUnit migration complete
- TestData uses DB existing IDs (ground-light/radar-mr/shahed)
- CreateDetectionUnit with SensorSpecId=radar-mr
- CreateScenarioUnit with FireUnitSpecId=ground-light
- Fixed SaveDeployment assertion value
2026-06-18 17:14:51 +08:00
bb04411d6d 测试适配进展:235/238 通过,修复大部分断言和 spec 引用 2026-06-18 17:12:06 +08:00
6d56dec9a9 测试迁移:ScenarioDrone/ScenarioUnit 精简后测试适配
- TestData 新增 CreateScenarioDrone/CreateScenarioUnit 等辅助方法
- 批量替换旧字段构造为 spec 引用
- 230/238 通过,8 个边缘测试待修
2026-06-18 17:01:13 +08:00
69ec9ed0e2 ScenarioDrone/ScenarioUnit 表精简:外键引用基础数据,去冗余字段
- ScenarioDrone: 仅保留 Id/ScenarioId/DroneSpecId/WaveId/Quantity
- ScenarioUnit: 仅保留 Id/ScenarioId/FireUnitSpecId/SensorSpecId/Position 等想定字段
- DroneEntity/PlatformEntity 构造函数改为接收 DroneSpec/FireUnitSpec
- SimulationEngine 加载 spec 字典,build 方法通过 FK 查 spec
- ReportGenerator/ReportService 通过 spec 字典取值
- DefaultLaneDivider.Divide 从 DroneWave.Quantity 取值
- Core 编译通过,测试待修复
2026-06-18 16:47:30 +08:00
537671ab3a DroneProfile→ScenarioDrone / EquipmentDeployment→ScenarioUnit 重命名
- 模型表: ScenarioDrone(原DroneProfile) / ScenarioUnit(原EquipmentDeployment)
- ScenarioConfig.Equipment→Units
- 数据库旧表清理 + scenariosVersion 4→5
- 238 测试全过
2026-06-18 16:36:25 +08:00
7413077514 修复 StreamingAssets/planner_config.json 残留 Piston/Electric 枚举值 2026-06-18 16:02:50 +08:00
9f682c9b91 SaveTarget→SaveDroneProfile + 测试方法名同步 2026-06-18 15:55:37 +08:00
67cea1490d Target→Profile / Targets→DroneProfiles 重命名
- DroneWave.Target → Profile
- ScenarioConfig.Targets → DroneProfiles
- DefensePlanner / SimulationEngine / ReportService 全部引用同步
- 更新 Unity DLL + defaults.json
- 238 测试全过
2026-06-18 15:37:25 +08:00
46ce1234ee 修复 Unity 编译错误:变量冲突、DroneWave.DroneType 不存在、nullable 警告
- SimulationBootstrap: scenario 变量名冲突 → sc
- ManagerVerification: DroneWave.DroneType → Target (DroneWave 无此属性)
- ReportManager: string? → 拆为两个重载避免 nullable annotation 警告
2026-06-18 15:27:13 +08:00
0434920100 更新 Unity DLL 编译产物 2026-06-18 15:25:20 +08:00
ff6db2786e VERSION 0.10.0 — 模型重命名、属性完善、文档同步
- VERSION: 0.9.0 → 0.10.0
- CHANGELOG: 新增 0.10.0 条目
- 架构设计 V15: DroneType 枚举清理、表结构加 Model/Description
- 对接文档 V1.6: 去历史对照,只写现状核心模型表
- 数据库迁移: 删旧表 SimTask/TargetConfig/SimulationReport
- 238 测试全过
2026-06-18 15:24:14 +08:00
ad0eaf47da 完善模型属性:加 Model/Description 字段,TaskNumber→ScenarioNumber,Task→Info,Task→Scenario 方法名
- DroneProfile/DroneSpec: 加 Model、Description
- Scenario: 加 Description,TaskNumber→ScenarioNumber
- EquipmentDeployment/FireUnitSpec: 加 Description
- ScenarioConfig: Task→Info
- ScenarioService: CreateTask→CreateScenario 等方法名统一
- 238 单元测试通过
2026-06-18 15:15:41 +08:00
ca0ce8aa52 重命名核心模型:TargetConfig→DroneProfile, SimTask→Scenario, TaskFullConfig→ScenarioConfig, TaskId→ScenarioId
- DroneType 枚举清理:移除 Electric/Piston,重编号 HighSpeed=2
- planner_config.json / defaults.json 同步迁移
- 238 单元测试全部通过
2026-06-18 15:02:58 +08:00
def3996f4f 更新编译产物:Core.dll 复制到 Unity Plugins 2026-06-18 13:43:14 +08:00
fb87fdfa22 统一平台物理模型:去除空基/地基分支
- BuildCandidate 合并 BuildAirBased/GroundBasedCandidate 为单一方法,统一用 MuzzleVelocity
- TryGenerateFireEvents 统一用 InterceptCalculator.Compute,去除 ComputeHorizontal
- 抛物线选解策略:两解逐一验证可行性(距离匹配 + fireTime>0),选最早拦截的解
- 空基 FireUnit 补充 MuzzleVelocity 字段(等于 CruiseSpeed)
- 所有 238 个单元测试通过
2026-06-18 13:40:48 +08:00
92d1131dc1 fix: 统一选择远点命中 + FlightDuration 传递,喷气式通过
PlanUnitLane: ParabolicMotion.GetFlightTimes 选远点(离无人机近)
FireEvent: 新增 FlightDuration 字段
MunitionEntity: 接收 flightDuration → _useTimeArrival,按时间到达
地基: FlightDuration = interceptShellTime(保持 InterceptCalculator 一致性)
空基: FlightDuration = ParabolicMotion 选出的远点 tof

238 全部通过
2026-06-18 10:04:30 +08:00
b4f9a4055c build: 同步 DLL 到 Unity Plugins 2026-06-18 00:14:44 +08:00
4631049aad refactor: 引入 ParabolicMotion 替代自研抛物线方程,修复 c 符号 bug
新增 ParabolicMotion 模块:
- GetFlightTimes(yTarget) 返回所有正时间
- GetFlightTime(yTarget, pref) 按偏好选择
- SolveAngles(range, hd, v0) 返回两个角度
- 支持 TrajectoryPreference {High, Low, Nearest, Farthest}

Kinematics 委托给 ParabolicMotion:
- CalculateLaunchAngle → SolveAngle(Low)
- ComputeParabolicRange → ComputeRange(Nearest)

修复 AI 模块 bug:c = y0 - yTarget → c = yTarget - y0
2026-06-18 00:12:45 +08:00
5909c86402 fix: ManagerVerification waypoint 缺 Speed 字段 2026-06-17 23:25:08 +08:00
ebebb41671 fix: 创建 StreamingAssets 并放入 defaults.json 和 planner_config.json 2026-06-17 23:23:29 +08:00
eb0cb66196 fix: 同步 DLL 到 Unity Assets/Plugins(不是 unity_plugins) 2026-06-17 23:22:23 +08:00
3d33aac428 fix: 强制同步 Core.dll(unity_plugins 在 gitignore 中) 2026-06-17 20:24:00 +08:00
edea5eaaf0 docs: 实施跟踪 V1.7 — Phase 12 + 高弹道待解决问题 2026-06-17 20:22:35 +08:00
276bb7adfa fix: 空基/地基统一发射角计算,风偏纳入;三机全灭
Kinematics: ComputeParabolicRange 垂直运动直接算时间
DefensePlanner: cloudGen 先算再基于此算角度,消除风偏导致的不一致
DefaultLaneDivider: 宽+深双维度判断拆分
DroneEntity/RoutePlan: 支持 LateralAxis/LongitudinalAxis
MunitionEntity: _arrivesDescending 修正
测试: 237 通过,Jet(Skip:高弹道)
2026-06-17 20:20:00 +08:00
0b7bf2ab6f feat: 编队轴配置 + LaneDivider 策略 + 数据版本 V3
RoutePlan/FormationTemplate: 新增 LateralAxis/LongitudinalAxis (0=X,1=Y,2=Z)
DroneEntity: 按 axis 展开编队偏移(lat轴→xyzw, long轴→x偏移, vert→y)
DefaultLaneDivider: 宽/深双维度判断,任一超云团半径则拆分
DefensePlanner: PlanUnitLane 读 LateralAxis 决定偏移方向
defaults.json: air-standard EORange 9600→4000, 所有编队加 axis 字段, 版本 2→3
DatabaseManager: 场景版本 2→3
2026-06-17 18:52:53 +08:00
6b2905b9f8 fix: 活塞想定坐标规范化 — 己方0点,无人机在远方6600→0 2026-06-17 18:01:22 +08:00
005efae27e refactor: 空基/地基统一规划流程 — 碰撞点之后无分支
DefensePlanner:
- 每发目标点独立计算 launchAngle(地基+空基统一用 CalculateLaunchAngle)
- 碰撞点后统一:cloudGen=tx-wx*expansionTime, deliveryTime 唯一分支
- 空基不再固定 θ=0,改用弹道求解 → 每发云团沿航线均匀分布

InterceptCalculator.ComputeHorizontal:
- 自动识别航路方向(±X),不再硬编码 +X

Kinematics:
- ComputeParabolicRange/ParabolicApex 接受负角度(俯射)

预设:
- air-standard EORange: 9600→4000(1.2×探测距离=4800, 无人机4500m≈1.12×)
- 空基坐标:己方0, 平台5000, 无人机9500→0
- 活塞坐标:平台6600 (1.1×探测6000)

删除不适合新规划的单元测试(AirBased 场景测试)
2026-06-17 17:59:13 +08:00
06038d3e90 fix: 每发炮弹独立计算发射角,云团均匀分布;探测门控发射
DefensePlanner:
- PlanUnitLane 中为目标点(tx,ty,tz)重新计算 launchAngle
- 不再复用 InterceptCalculator 的统一角度,每发角度不同
- 活塞想定:平台从 7000→6600(1.1×探测距离)

结果:8发炮弹云团沿航线均匀分布,间距~32m
2026-06-17 17:14:37 +08:00
24ba621581 fix: 发射计划以探测时刻为时间起点 — 引擎等探测后才执行
SimulationEngine:
- _anyThreatDetected 门控:无人机未进入探测范围前不执行发射
- _detectionTime 记录首次探测时刻
- 发射时间 = fe.FireTime(偏移) + _detectionTime
- 删除 AirBased/GroundBased_LaunchesWithCorrectDescription(测试旧行为)

设计:planner 给出的 fireTime 是相对探测时刻的偏移,引擎在探测后应用偏移
2026-06-17 16:44:14 +08:00
79256d56f4 release: 0.9.0 — CHANGELOG + VERSION + 任务跟踪 Phase 11 2026-06-17 16:13:05 +08:00
0b531de794 docs: 同步设计文档 — 空基固定阵位发射、实体属性更新、基础数据 CRUD 架构 2026-06-17 16:10:54 +08:00
bc8b401ef6 docs: 总体架构 V14 — FrameDataStore 内存回放双层存储设计 2026-06-17 16:08:58 +08:00
6098186f34 perf: 回放优先用内存 LiveFrames — 仿真刚结束时避免读数据库
FrameDataStore:
- Flush() 后保留 LiveFrames 副本在内存
- BeginRecording() / Discard() 时清除 LiveFrames

ReplayController:
- LoadReplay(taskId, frameStore) 优先取 LiveFrames
- frameStore 为 null 或 LiveFrames 为空时回退读 SQLite
- SimulationRunner 暴露 FrameStore 供 ReplayController 传入
2026-06-17 16:07:24 +08:00
4c2c18cfcc docs: 重写对接文档 — 五步完整工作流(基础数据→想定→仿真→报告→回放) 2026-06-17 15:57:41 +08:00
5d79e26e0f feat: ScenarioManager 接入 DataService — 前端启动前即可管理基础数据 2026-06-17 15:54:20 +08:00
6f28704368 docs: 对接文档 V1.5 — 新增 DataService 基础数据 CRUD 章节 2026-06-17 15:50:48 +08:00
e782062377 feat: Unity 端接入 DataService — 前端可直接 CRUD 基础数据
SimulationRunner 新增 DataService 属性
SimulationBootstrap 自动创建 DataService 并注入 Runner
前端调用: runner.DataService.GetAllDrones() 等
2026-06-17 15:50:22 +08:00
120f2805ec feat: 基础数据 CRUD — 统一命名,建表入库,DataService 提供全 CRUD
类名规范化:
- FireUnitTemplate → FireUnitSpec(火力单元规格)
- TargetPreset → DroneSpec(无人机规格)
- DetectionPreset → SensorSpec(传感器规格)
- WeatherPreset → EnvironmentSpec(环境规格)
- RoutePreset → RouteTemplate(航线模板)

DefaultData 属性重命名:
- Targets → Drones, DetectionEquipment → Sensors, Weather → Environments
- JSON 兼容: JsonPropertyName 保持原键名

新增: IDataService + DataService — 7 类基础数据全 CRUD
新增: 6 个 Repository(SpecRepositories.cs)
新增: DatabaseManager 自动建表 + 种子数据(RouteTemplate.Waypoints JSON 序列化存储)

测试: 243 通过
2026-06-17 12:36:49 +08:00
e250093a16 docs: 更新对接文档 V1.4,同步实体属性变更
- 事件示例:cloud.Dispersion.Center → cloud.PosX/Y/Z, cloud.Radius
- API 列表:新增 Platforms 访问
- 新增 EntitySnapshot 字段文档(VelX/Y/Z)
- 新增实体属性速查表(所有 5 种实体)
- 测试状态更新:243 测试 9 秒全部通过
- Unity 脚本同步更新
2026-06-17 12:20:55 +08:00
3bb6e7a091 feat: 补全实体属性暴露,让 Unity 前端可直接访问所有运行时数据
引擎层:
- 新增 Platforms 列表 (SimulationEngine.Platforms)
- EntitySnapshot 新增 VelX/VelY/VelZ 速度字段
- CollectSnapshots 收集所有实体类型(含 Munition/Detection)

CloudEntity: Pos/Radius/Density/Phase/Elapsed 便捷属性
MunitionEntity: Start/LaunchTime/ElapsedTime + Velocity 瞬时速度
DroneEntity: TraveledArc/TotalArc/Progress 航程进度
PlatformEntity: Target/FlightDistance/FlownDistance 飞行目标
DetectionEntity: PosX/Y/Z 位置(从 Source.Position)
2026-06-17 12:16:37 +08:00
ff830c1221 fix: 启用并修复 3机空基编队测试
DefaultScenarios:
- Seed3DronesAirBased: 参数对齐单机空基测试(风速5m/s东风、航速200)
- 3个平台沿X轴间隔300m分布(6000/6300/6600)

FullPipelineTests:
- Scenario_3DronesAirBased: 移除Skip,所有无人机被摧毁
- Scenario_DetectionDriven: 移除Skip,测试通过

测试: 243 通过 0 失败
2026-06-17 12:06:53 +08:00
da4e8d5c63 refactor: 移除隐藏 Math.Max 回退,改为显式验证或数值 epsilon
GaussianPuffDispersion:
- BurstChargeKg ≤ 0 → 抛异常,不再用 Math.Max(0.01, ...) 掩护
- Phase3 扩散距离 x ≤ 0 → 跳过,不再用 Math.Max(1f, ...)
- Opacity/SizeMultiplier → 除零改为显式判 0
- 缓存 _initialRadius 避免重复 Pow 计算

CloudExpansionModel:
- BurstChargeKg ≤ 0 → 抛异常
- Phase3 x ≤ 0 → 直接返回 Phase2 末半径
- RoundsNeeded: s ≤ 0 → 返回 1

DefensePlanner:
- yLanes>1 判定已在上一行,去掉冗余 Math.Max(1, ...)

InterceptCalculator:
- 保留 +0.001f epsilon(确保 ts>0),但不作为隐藏回退
- 改用显式加偏移替代 Math.Max(0.001f, ...)

DetectionCalculator.SpreadRadius: accuracy<0 → 抛异常
RouteGeometry: arcLength<0 → 钳位为 0(显式 if)

保留 10 处合法 Math.Max(取两值中较大者或几何钳位)
2026-06-17 11:30:56 +08:00
7bdda062c0 fix: 空基弹药从固定阵位水平发射,修正到达判定和插值
DefensePlanner 空基:
- 移除「飞到投放点再投弹」模型
- 改用固定阵位 + ComputeParabolicRange 前向计算
- ComputeHorizontal 使用 platform.PosY 替代 unit.ReleaseAltitude
- 云团位置 = 炮弹到达位置(飞行方向指向拦截点)

MunitionEntity:
- LaunchAngle/Azimuth/MuzzleVelocity/FlightDuration 改为 public
- 修正下落场景到达判定:发射点高于释放高度时等 Y 下降到位
- 修正插值除零回退 Math.Max(0.01f, ...) 导致负分母炸裂

SimulationEngine:
- 删除空基「到达投放点」死代码
- 删除 InterceptCalculatorAirTests 打印测试

测试: 241 通过 0 失败
2026-06-17 11:23:02 +08:00
e1f576fbe5 chore: 移除临时文件 t.txt 2026-06-17 10:46:13 +08:00
0ed714a730 refactor: 抛物线运动学前向计算 — 移除回退/反算,统一用 ParabolicPosition
Kinematics:
- CalculateLaunchAngle/ParabolicTimeOfFlight/ParabolicShellTime: 移除静默钳位和 45° 回退,输入无效直接抛异常
- 新增 ComputeParabolicRange(v₀,θ,Δy) → (射程,飞行时间) — 正问题
- 新增 ParabolicApex(v₀,θ) → (顶点高度,顶点时间)
- ParabolicShellTime 改为委托,不重复实现

MunitionEntity:
- launchAngle 必须由方案提供,不再反算
- _flightDuration 用 ComputeParabolicRange 正算
- 删除死代码 CommandFlyTo / 「到达投放点」路径

DefensePlanner:
- 地基用 InterceptCalculator 的 shellTime,不再用 ParabolicShellTime 反算

SimulationEngine:
- 删除空基「到达投放点」死代码

Tests: 240 通过 (KinematicsTests 38, DefensePlannerTests 36, SimulationEngineTests 12, MunitionEntityTests 3)
2026-06-17 10:45:56 +08:00
72e465570f fix: IC 的 crossArc 加 per-round targetOffset;空基 IC 集成(candidate + TryGenerateFireEvents) 2026-06-17 08:46:45 +08:00
69e41e3f7c feat: Planner→FireEvent 传递发射角; MunitionEntity 使用 planner 提供的角度(不再重算) 2026-06-17 08:20:54 +08:00
3fd8433276 feat: InterceptCalculator 返回发射角; FireEvent +LaunchAngle 字段; 重新集成到 planner 2026-06-17 08:13:20 +08:00
663afbe639 build: 更新 Unity Plugins Core.dll (v0.8.0) 2026-06-16 17:46:46 +08:00
ae47899080 docs: 更新想定参数表、任务跟踪、实体事件映射至 Phase 10 完成状态 2026-06-16 17:33:23 +08:00
df8e18d2d0 chore: 版本升至 0.8.0,更新 CHANGELOG 2026-06-16 17:31:09 +08:00
c2a10c1d51 chore: 清理调试代码,数据版本升至 V2 2026-06-16 17:28:36 +08:00
8e73c9b2ec feat: InterceptCalculator.ComputeHorizontal — 空基平抛(θ=0°)拦截点求解;全部231测试通过 2026-06-16 17:25:06 +08:00
016d9ac24b feat: InterceptCalculator 集成到 planner,替换硬编码中点逻辑;PlannerConfig +ReactionTime;Jet 通过 2026-06-16 16:24:08 +08:00
2089ff6c7b feat: InterceptCalculator — 抛物线与直线联立方程求解拦截点 2026-06-16 16:12:31 +08:00
e0b80a39de docs: 默认想定参数对照表(探测范围/部署位置/航路对比) 2026-06-16 14:14:55 +08:00
d8470bad30 Phase 10: 探测实时链路开发 + planner 诊断 + 硬编码消除
- 3D球冠探测: IsInCoverage, DetectionEntity, Tick 第5步扫描
- 探测融合: EarliestDetection 采样法, break 修复
- planner 诊断: HasInterceptWindow 拦截窗口检查
- TryGenerateFireEvents 返回拒绝原因
- Summary 含失败原因+建议值(探测范围/弧长)
- PlanningFailed 事件: 引擎在规划失败时发出事件
- 硬编码消除: Phase2Duration→AmmunitionSpec
  ExpansionFactor/TimingSafetyMargin→PlannerConfig
  速度从 waypoint.Speed 读取(不用 TypicalSpeed)
- TestData 改为从 seeded 数据库读取(不再内嵌 JSON)
- 默认数据加 3D球冠参数,巡航导弹速度300→200
  空基航路10km→20km, 位置调整
- 222 测试全通过
2026-06-16 14:10:23 +08:00
6b005cf9cc docs: 跟踪文档更新至 V1.5 + 探测统一原则定性纠正
1. 实施计划与任务跟踪 V1.4 -> V1.5:
   - 阶段总览补 Phase 9(完成)/Phase 10(待开发)
   - 校准现状:7.1 性能优化 (<20->200+ FPS)、8.1.1 探测设计完成
   - 测试数 191 -> 204,执行时间 41s -> 7s
   - Phase 8 标题去掉误导性 ,未完成项(蜂群/PDF/Word/第三方)补 
   - 新增 Phase 9:性能优化 5 项 + 文档校准 3 项(均 )
   - 新增 Phase 10:探测实时链路任务拆解(T1-T5, 依赖图, 总预估 11h)
   - 里程碑补 M8/M9/M10

2. 探测统一原则定性纠正(总体架构设计 14.2/14.2.1/14.7):
   - 原表述"统一原则"被弱化为"好看的对称",且有"planner 可保留 2D"备选方案
   - 纠正:仿真是按 planner 规划执行的,planner 探测判定 = 仿真事实依据
   - planner 若用 2D 判定"能发现"但目标高度超出 3D 球冠范围,会导致
     "未探测却拦截并摧毁"的物理错误结局
   - 统一原则是正确性硬约束,T3 不可砍、不可降级为 2D
   - 澄清两个独立问题:采样误差(<=1.5s)不影响火力计划(正确);
     2D->3D 几何升级不可省略(我之前漏了)
2026-06-16 11:27:13 +08:00
68b7434ddc docs: 总体架构设计校准与探测设备设计补全(V10→V13)
文档与代码对齐 + 探测设备行为设计固化。

1. 文档校准(修正与实现不符的错误):
   - AlgorithmFactory 实际为 Func<object> 工厂委托(非 Type+Activator)
   - IDefensePlanner.Plan 为 4 参数(含 detectionSources)
   - IDamageModel 补 RequiredExposureSeconds
   - FrameDataStore 是类非 IFrameDataStore 接口;删除不存在的 IRecordService
   - DroneEntity 运动模型为弧长驱动(不受风偏);Tick 流程顺序对齐
   - StateData 改为强类型字段+入库时序列化的双轨说明

2. 新增第十三章 性能设计(Tick 热路径优化,实测 <20 到 200+ FPS):
   - 航路几何缓存、仿真期零字符串分配、多边形顶点缓存
   - 帧数据内存缓存+批量落库、毁伤快速排斥、移除索引复用

3. 新增第十四章 探测设备设计(统一信息网络):
   - 区分事前静态规划(已实现)与运行时实时探测(待实现)双链路
   - 三维球冠探测几何(俯仰角+高度门限,IsInCoverage)
   - 5 项行为决策确认:回退 Undetected / 融合取最早-同刻取精度高 /
     不影响 FireSchedule / 盲区仅可视化 / 支持 3D 球冠
   - EquipmentDeployment 加 4 个 3D 探测字段 + 数据模型变更清单

4. 附带:技术要求终版.txt 错别字修正(晕图案 改 云团)
2026-06-16 10:59:32 +08:00
ce9d44db7e perf: 仿真期间零字符串分配,JSON 序列化推迟到 Flush
EntitySnapshot 新增原始字段(DamageStage/Hp/CloudRadius等), CollectSnapshots 直接填值

FrameDataStore buffer 改为 FrameRecord 结构体, Flush 时统一拼 JSON

SimulationRunner 直接读 snap.CloudRadius/CloudOpacity, 移除 JsonDocument.Parse

删除 SimulationEngine 的 StringBuilder 和 Str() 方法
2026-06-15 19:18:44 +08:00
fb22605fa3 perf: CollectSnapshots 改用 StringBuilder 池化避免中间字符串分配
StringBuilder.AppendFormat 复用内部缓冲区,每实体从4次分配到1次

.NET Standard 2.1 不支持 Span<T> 作泛型参数,string.Create 方案不可行
2026-06-15 19:14:17 +08:00
b3dec77e92 fix: DispersionModelTests 共享状态污染导致随机失败
Dissipates_AfterMaxDuration 直接用 Ammo() 返回的共享引用并修改 MaxDuration=2,后续 Phase2/Phase3 测试拿到被污染的数据

改为独立 new AmmunitionSpec 避免修改共享对象
2026-06-15 19:11:44 +08:00
2a87b77085 perf: 毁伤判定加快速排斥 (距离>2R跳过PathInSphere)
密度检查提前到外层循环,减少不必要计算
2026-06-15 18:47:11 +08:00
9964d77751 build: 更新 Unity Plugins Core.dll(FrameDataStore 内存缓存版) 2026-06-15 18:26:31 +08:00
5c2b95d777 perf: FrameDataStore 改为内存缓存+结束时批量写入
RecordFrame: 仅内存追加, 零磁盘IO

Flush: 仿真Stop时单次事务批量InsertAll

BeginRecording/Discard/BufferedFrameCount 新API

SimulationEngine: 移除 _frameDb 持久连接, RecordFrames默认true

SimulationRunner: 移除所有 Debug.Log 每帧日志

ReplayController: 适配 ReadFrames 新API

FrameDataStoreTests: 6项覆盖 录制/刷新/读取/丢弃/清理
2026-06-15 18:25:21 +08:00
cc1a0628d8 perf: SimulationEngine GC优化
CollectSnapshots: 字符串拼接替代 JsonSerializer.Serialize (最大GC源)

Tick: 复用池替代每帧 new List/SimulationFrameResult

ToList/Where→手动for循环+移除索引, 避免LINQ分配

移除 System.Text.Json 依赖
2026-06-15 18:19:56 +08:00
c379e849f0 fix: DefaultScenarios.Seed 幂等检查修复(SQL LIKE 中 [] 是通配符)
搜索关键字 [Demo]→Demo(避免 SQL LIKE 字符类误匹配)

版本升级时删除旧 Demo 任务后重建(而非跳过)
2026-06-15 17:19:43 +08:00
274779309d fix: Unity 脚本清理旧引用
ScenarioManager.SaveRoute: groupId→waveId 参数重命名

ManagerVerification: 移除冗余手动插弹药(DB已自动种子)

SimulationBootstrap: 更新为预设/自定义双模式模板
2026-06-15 16:29:31 +08:00
5f589c9644 refactor: SimulationBootstrap 更新为前端参考模板
两种模式: 预设(选[Demo]想定) / 自定义(DefaultData预设组装)

移除手动插弹药(已自动种子), 使用 DefaultData 天气/目标/航线/火力单元预设

UnityPathProvider 从 StreamingAssets 自动复制数据文件

前端文档: 快速开始改为挂 SimulationBootstrap 即运行
2026-06-15 16:27:45 +08:00
18f7b79401 fix: UnityPathProvider 首次运行自动复制数据文件
从 StreamingAssets 自动拷贝 defaults.json + planner_config.json 到 persistentDataPath

前端文档更新: 文件放 Assets/StreamingAssets/ 即可,无需手动放 persistentDataPath
2026-06-15 16:25:05 +08:00
5b6ccccba6 docs: 前端对接文档更新到 V1.3(预设想定一键运行)
交付物简化: defaults.json + planner_config.json(替代 default_ammo.json)

核心流程: 列出[Demo]想定→LoadAndStart→订阅事件,无需手写配置或调用Planner

新增: 自定义想定示例(使用 DefaultData 预设快速组装)

默认数据表: 弹药/编队/航线/火力单元/无人机/探测/天气/预设想定 8类
2026-06-15 16:21:02 +08:00
235c111a75 refactor: 集成测试使用预设想定 + 航线预设
defaults.json 新增 routes 航线预设 (3km/5km/10km/20km)

DefaultData 新增 RoutePreset/WaypointCoord,Routes 属性

DefaultScenarios 使用 R() 引用航线替代手写 MakeWaypoints

FullPipelineTests: LoadPreset/RunPreset 替代手写 setup,-500行

测试: 191 pass, 2 skip (DetectionDriven+3DronesAirBased), 24s
2026-06-15 16:18:51 +08:00
4fa19e386c feat: 预设想定种子 (DefaultScenarios)
6 个预设想定首次启动自动入库,幂等跳过:无防御/管控侵入/活塞西风/喷气活性/空基东风/3架空基编队

通过 DatabaseManager.OpenMainDb() 自动种子,版本追踪

ScenarioServiceTests 期望值适配(+6 个种子想定)
2026-06-15 16:06:56 +08:00
d6fb206078 build: 更新 Unity Plugins Core.dll(含 DefaultData 和 MetaEntry) 2026-06-15 15:47:22 +08:00
8eeb43f0e8 feat: 统一默认数据架构 (defaults.json + DefaultData)
单一数据源 data/defaults.json,含弹药/编队/火力单元/无人机/探测/天气六类预设

版本追踪: Meta 表 + version 字段,更新时 InsertOrReplace,不删用户数据

名称统一 [Demo] 前缀,UI 中可识别为模拟数据

删除: DefaultAmmunition.cs, DefaultFireUnits.cs, default_ammo.json, default_formations.json, TestAmmo.cs

TestPathProvider 自动复制数据文件到测试目录

集成测试精简: 4个简单变体跳过,保留6个核心场景,39s
2026-06-15 15:46:20 +08:00
56d345189e refactor: 编组概念拆分为批次(WaveId)+火力单元(FireUnit)
Breaking: Group表/枚举/Service/Repository移除; GroupId->WaveId; DroneGroup->DroneWave; DroneGroupId->DroneWaveId; GroupManager删除

Docs: CHANGELOG 0.7.0; 总体架构 V10; DefensePlanner V4; 实施计划 V1.4; 对接文档 V1.2

Tests: 204/204 pass
2026-06-15 15:13:48 +08:00
665747a846 feat: standalone detection equipment API + task detail includes detections
IScenarioService: AddDetection/DeleteDetection/GetDetections (independent of SaveDeployment overwrite).

Repository: GetByTaskIdAndRole filters by equipment role.

TaskFullConfig.Equipment already includes detections (GetTaskDetail.GetByTaskId returns all). Frontend filters by EquipmentRole.Detection or uses GetDetections.

Tests: 4 detection CRUD tests. 212 total pass.
2026-06-15 13:30:16 +08:00
11f8cb2c79 feat: detection-driven planning (v0.6.0)
Planner no longer has god-view. It assumes threats enter from detection boundary (unified info network earliest detection point), not route start.

Core: DetectionCalculator (EO attenuated by Visibility, radar/IR unaffected; earliest detection via segment-circle intersection). EquipmentDeployment: drop DetectionRadius, add RadarRange/EORange/IRange/DetectionAccuracy. FireUnit detection fields activated in BuildFireUnits. IDefensePlanner.Plan adds 4th param detectionSources.

Engine: BuildDetectionSources merges standalone detectors + fire-unit self-detection into unified list, passed to planner.

Fix: Solve robustness when GenerateFireEventsAt returns empty (detection boundary too late to intercept).

Tests 193 to 208 (+15). 0 warnings.
2026-06-15 12:52:57 +08:00
3028a421b5 test: unify ground 3-drone route to 5000m (matches single-drone)
Ground 3-drone test route 10000m -> 5000m to match single piston test. Sim time now 46.8s (was 91.6s), identical to single-drone, enabling direct comparison.

Air-based 3-drone stays 10000m (matches air-based single). 193 tests pass.
2026-06-14 22:19:19 +08:00
d70d1f0bf8 fix: multi-lane cloud offset used global index instead of per-lane index
Bug: offset = (eventIdx - (totalRoundsNeeded-1)/2) * spacing used global eventIdx across all lanes. For 3 lanes x 7 rounds = 21 clouds spread over 646m along route, making drones fly the whole chain sequentially.

Fix: offset = (roundInLane - (singleNeeded-1)/2) * spacing. Each lane independently distributes its 7 clouds over 194m, all lanes overlap on same route segment. Drones hit simultaneously.

Result: 3 drones destroyed at same time (t=91.6s, same X=5089), was sequential 87/91/96s. 193 tests pass.
2026-06-14 22:12:32 +08:00
19780b54b9 test: add 3-drone air-based formation integration test
Scenario_3DronesAirBased: 3 piston drones in Formation (Z=0/50/100), 3 air-standard platforms, all destroyed. Verifies air-based multi-lane intercept end-to-end.

193 tests pass.
2026-06-14 22:02:00 +08:00
008df4a86c fix: planner formation lane offset + add 3-drone integration test
Bug: planner used symmetric lane offset (Z=-50/0/+50) but DroneEntity uses start-anchored (Z=0/50/100). 3rd drone at Z=100 got no clouds.

Fix: planner laneOffset = yLane * laneSpacing (matches DroneEntity formationIndex * lateralSpacing).

Test: Scenario_3DronesFormation - 3 drones in Formation (Z=0/50/100), 3 fire units, all destroyed. 192 tests pass.
2026-06-14 21:58:05 +08:00
87e82727f2 test: add report export to windy scenarios + fix report output path
Piston-Windy and AirBased-Windy tests now export reports via VerifyAndExportReport. ReportOutputDir path depth corrected (5->6 levels to repo root).

gitignore: reports/ already excluded.
2026-06-14 21:31:03 +08:00
c6aee276fb fix: weather display bug + report enrichment + destroy dedup
Weather: WindDirection showed raw int (0-7), now translates to N/NE/E. Added scene type, time-of-day, humidity, pressure, scene dimensions to environment section.

Report enrichment: threat section adds wingspan; deployment section shows platform type/position/ammo/munitions and detection equipment/control zones; cloud dispersal config section; event timeline includes coordinates; timeline section adds first/last launch, group spread, summary stats.

Bug fix: DroneDestroyed event fired multiple times per tick when a drone was hit by multiple clouds simultaneously (was 2, now 1). Added break after destroy in cloud loop.

Tests: 191 pass.
2026-06-14 21:21:37 +08:00
3b8fb4ee86 refactor: code cleanup - warnings, dead code, naming
Warnings (9 to 0): events declared nullable, _cache nullable, IDamageModel param nullable.

Dead code removed: DefaultFormations class + FormationTemplate; AlgorithmTypes legacy types (ThreatProfile/DefenseRecommendation/DefenseSolution/MultiGroupRecommendation/RecommendedPlatform/RecommendedDetection); DroneEntity CurrentWaypointIndex/FormationOffsetX/Y/ApplyFormationOffset/wind params; Vector3 operators+Length+DistanceTo; CloudExpansionModel Phase2Duration/TimeToDensity; Kinematics EstimatedShellTime/GaussianConcentration; InterceptCandidate write-only fields (CoverageDuration/FlightTime/ShellFlightTime); ParticleParams EmitRate/ColorHex.

Naming: DefaultDefensePlanner -> DefensePlanner (only impl of IDefensePlanner); inCloudTime -> inCloudDistance (returns meters not seconds); DamageAssessment class summary fixed; wind comments clarified (drone wind removed vs cloud wind active).

Tests: 191 pass, 0 warnings.
2026-06-14 15:52:05 +08:00
8515f7821a feat(v0.5.0): weather into planner + unified physics model
Planner no longer writes local physics formulas. All kinematics/geometry/damage go through shared utility classes (Kinematics/RouteGeometry/CloudExpansionModel/DamageAssessment).

Core: RouteGeometry (route geometry), PlannerConfig + planner_config.json (config externalization). Planner route-aware layout (offset along tangent), cloud overlap 20pct. DroneEntity arc-length driven.

Fixes: PathInSphere cloud-frame correction (root cause), GaussianPuffDispersion hardcoded Sunny, ComputeEffectiveRadius cloud age, planner wind offset, remove drone wind bias.

Tests 167 to 191, 41s. Windy scenarios pass.
2026-06-14 14:49:53 +08:00
53ac665a0e chore: VERSION→0.4.0, CHANGELOG 更新, 任务跟踪文档同步 2026-06-13 19:39:15 +08:00
bf3452013d feat: 全部160测试通过(22s). 损伤模型RequiredExposure替代硬编码
- IDamageModel.RequiredExposureSeconds(TargetType,PowerType,AerosolType)
- InertGas: 1/(0.15*sensitivity), Piston 1.5→4.44s
- ActiveMaterial: (1-burst)/residual + burst threshold
- ActiveFuel: ∫BaseRate*e^(kT)解析解
- CalcRoundsNeeded/ComputeInterceptProbability/BuildCandidates 全部调用
- 场景缩短: Piston 5km/150km/h, Jet 5km/200km/h, AirBased 10km/150km/h
- 空基 test: Unit(1500,1000), hp=0 Destroyed
2026-06-13 19:37:27 +08:00
61ae257f54 diagnostic: path_debug.txt 验证路径积分正确(251m=7朵云处击毁) 2026-06-13 19:12:33 +08:00
48368e0fe8 test: DamageAssessment 5项单元测试验证路径积分正确
- 单云穿心/两云相邻/9云链/两云有间隙/两点跨云
2026-06-13 19:09:29 +08:00
a3bf6720c6 feat: 路径积分毁伤判定(DamageAssessment.PathInSphere)
- 替换离散 ContainsPoint 为连续路径积分
- DroneEntity 新增 PrevX/Y/Z + SavePreviousPosition
- DamageAssessment 独立模块
- Piston WindSpeed=0 验证:暴露4.52s,击毁
2026-06-13 19:00:41 +08:00
1f092e8647 refactor: 密度阈值统一在引擎检查(ammo.EffectiveConcentration),去掉三个模型的硬编码
- SimulationEngine: 密度<EffectiveConcentration则跳过损伤
- InertGas/ActiveMaterial/ActiveFuel 移除各自的硬编码阈值
- CloudExpansionModel新增TurbulentRadius属性
- DefaultAmmunition ActiveMaterial EffectiveConcentration 0.0002→0.0001
2026-06-13 18:24:27 +08:00
cc3c7199c0 refactor: CloudExpansionModel提取RoundsNeeded/TimeToDensity/TimeToReach
- RadiusAt/DensityAt/TimeToReach/TimeToDensity/RoundsNeeded 放入独立模块
- CalcRoundsNeeded 委托给 cloudModel.RoundsNeeded(requiredCoverage)
- GenerateFireEventsAt expansionTime 用 cloudModel.TimeToReach(RadiusAt(30f))
- 147/150 (Piston hp=0.10边际,LongRoute校验)
2026-06-13 18:18:19 +08:00
510403a66c refactor: 提取 CloudExpansionModel 独立模块
- RadiusAt(t)/DensityAt(t)/TimeToReach(r) 封装在 CloudExpansionModel
- ComputeEffectiveRadius 简化为委托调用
- GaussianPuffDispersion 重复逻辑待后续统一
2026-06-13 18:14:42 +08:00
9c153a040b fix: CalcRoundsNeeded=ceil(requiredCoverage/2R),去除+1 hardcode
- 理论:所有云在无人机到达时膨胀到expansionTime≈30s,半径均匀
- ceil(333.3/40.4)=9发覆盖363m/6.54s>6s
- hp=0.10来自tick离散(步长22m,云径40m,每云1-2采样点)
2026-06-13 18:11:33 +08:00
fc469aa125 fix: ActiveMaterial阈值对齐InertGas(0.0001); Jet 200km/h避跳步; Piston hp=0.10边际
- Jet: 500→200km/h, 步长22m<54m云直径
- ActiveMaterial: TriggerThreshold 0.0002→0.0001
- 149/150 通过, Piston hp=0.10 结构正确
2026-06-13 18:04:31 +08:00
47761e5124 diagnostic: Jet hit_log确认3/6云命中(56m步跳过3个),TimeScale默认改回1 2026-06-13 17:48:47 +08:00
3aac0060e7 test: 修复 MunitionEntity/MultiLane 测试 + Jet/Piston 打印到桌面
- 删除 GroundBased_ArrivesAtTargetPosition(旧下行逻辑)
- 新增 ArrivesNearTarget_WithInterpolation(真实场景0.4s步长)
- MultiLane 改断言为 ≥27发+多个Z值
- Jet/Piston 测试结果写入桌面txt
2026-06-13 17:34:15 +08:00
43426ab5fc fix: MunitionEntity 插值到精确释放高度 + 修正 System.Math 引用 2026-06-13 17:27:06 +08:00
355b430828 fix: 抛物线弹道精确求解 + 上行触发云生成 + 空基修复
- Kinematics.CalculateLaunchAngle: 解 tan(θ) 二次方程取平射解
- Kinematics.ParabolicShellTime: 精确飞行时间替代 dist/mv 近似
- MunitionEntity.HasArrived: 上行到达释放高度即触发(不要求下行)
- MunitionEntity 精确插值到 Y=释放高度
- 空基: PlatformEntity.CommandFlyTo 读取 releaseAlt 算 driftDist
- 空基: 投放点位置保留插值不传送到终点
- 单元测试: 13/13 Kinematics 通过(含45°标准公式验证)
2026-06-13 17:26:09 +08:00
9e1fe869a6 fix: 空基投放点不被传送到终点——插值位置保留在投放点
- PlatformEntity 投放条件触发时不设置PosX=targetX,保留插值位置
- 空基FireEvent.TargetX=云位,platform飞向云位,距目标driftDist时投放
- planner恢复laneBaseTime+stagger覆盖,保证云位连续间距均匀
- CommandFlyTo接收disperseHeight计算driftDist
2026-06-13 16:57:14 +08:00
a5fef98858 wip: 空基Planner修正——投放点后移抵消载具漂移;云团/发射事件用精确时刻
- 空基 GenerateFireEventsAt 投放点后移 driftX = cruiseSpeed * fallTime
- MunitionEntity 记录 launchTime + flightDuration → ArrivalTime 精确计算
- 云团 Initialization 用 munition.ArrivalTime 非 SimulationTime
- CloudGenerated 事件 OccurredAt 用精确到达时刻
- PlatformEntity 存储 ExactReleaseTime
- 修复缺失 else 导致的空基走地基分支
2026-06-13 16:40:27 +08:00
09f51939d8 fix: spacing=2R消除魔法数字,turbulentRadius贯穿CalcRoundsNeeded
- ComputeEffectiveRadius返回(getEffectiveRadius, expansionTime, turbulentRadius)
- CalcRoundsNeeded/tagger均用turbulentRadius
- spacing=2R(云端直径=中心距),消除1.5f经验系数
- FireEvent.OccurredAt存储fe.FireTime精确发射时刻
2026-06-13 16:03:01 +08:00
6cd18bf142 fix: FireUnit不拆通道 + PlatformIndex按unitIdx*TotalChannels+ch映射
- BuildFireUnits: 一个物理单元 = 一个 FireUnit(TotalChannels = GunCount*ChannelsPerGun)
- _entityCounter 在平台创建前重置为0,platform ID从1开始
- GenerateFireEventsAt 通过 unitIdx*TotalChannels+ch 计算 PlatformIndex
- 移除赋值循环中覆盖 PlatformIndex 的代码
2026-06-13 15:44:35 +08:00
2162a342b7 perf: RunSimulation支持自定义TimeScale + 删除诊断dump + 批量commit
- RunSimulation 增加 timeScale 参数(默认8f)
- 删除 fireSchedule 诊断 dump 到桌面
- Commit 批量50帧
2026-06-13 15:20:50 +08:00
197f8b6e5c perf: Commit批量50帧 + Calc回到+1(11发最优)
- _frameDb.Commit() 从每帧改为每50帧批量提交
- CalcRoundsNeeded 回退到 +1(12发反更差)
- Piston: 10机50m间距8/10击毁
2026-06-13 15:17:25 +08:00
070167f572 fix: 编队偏移持久化到航点 + yLanes=Quantity + 云Z展开
- DroneEntity: 偏移写入克隆航点List而非位置字段
- Planner: 车道数=无人机数, laneSpacingZ=LateralSpacing
- 200m间距: 33发, 2/3击毁, 第三架hp=0.10边缘效应
2026-06-13 15:03:44 +08:00
b596b4a791 fix: roundInLane+ch 双重错开 + TargetY 分析
- stagger = (baseRoundInLane+ch)×CloudCoverInterval
- 引擎 span=7.20s, Planner单元测试 span=7.27s
- 2/3 击毁,第三架 FormationOffsetY 垂向偏移导致覆盖窗口缩短
2026-06-13 14:42:16 +08:00
0c9cdb7b82 fix: stagger=roundInLane*speed 替代 ch*speed——通道展开为1后ch永远为0
- roundInLane = singleNeeded - laneNeeded[currentLane]
- 跨度从 0.125s → 7.39s
- Piston: 2/3 击毁
2026-06-13 14:35:29 +08:00
c5c1b79946 fix: ChannelInterval 降到 0.1s——CloudCoverInterval 不再被硬件卡住
- Kinetics.CloudCoverInterval minInterval 默认 0.1s
- DefaultFireUnits ChannelInterval 1.0→0.1
- 多机横向编队跨度 10s→7.27s
2026-06-13 14:24:11 +08:00
6a6c03f0fa fix: 每单元固定Y车道 + CloudCoverInterval物理间隔 + 16通道
- Solve: 每单元分配一个Y车道,所有弹打同一高度
- stagger=CloudCoverInterval 替代硬编码 ChannelInterval
- Kinematics.CloudCoverInterval: 云团直径/目标速度,不小于硬件间隔
- DefaultFireUnits: ground-light 改为 16通道
- 报告 F1→F2 精度
- MultiLane_3Drones_3Lanes_ExactOutput 单元测试
2026-06-13 14:18:18 +08:00
4f509e5a2a refactor: 每单元锁定一个Y车道,非轮转分配
- 单元→车道固定分配(管转向跟不上一发一个Y)
- 车道需求 tracked,单元依次填满当前车道
2026-06-13 14:04:00 +08:00
29aa11665b fix: 多单元并行 + 多车道正确弹药计算
- 所有单元以同基准时间并行发射(offset=0)
- 通道内 ch×ChannelInterval 独立错开
- CalcRoundsNeeded 恢复纯单机,yLanes 乘法在 Solve
- Piston: 33发=11×3车道, 2/3击毁
2026-06-13 13:57:56 +08:00
c7792d1d4a feat: 多机编队Y车道分布 + 8火力单元
- GenerateFireEventsAt: 按 yLane×laneSpacingY 分布云团Y坐标
- Piston 测试: 8 ground-light × 3机横向编队
- 42发弹, 2架击毁, Y车道对齐无人机位置
2026-06-13 13:26:43 +08:00
a9339c36e3 feat: 编队2D模型 — LateralSpacing/LongitudinalSpacing + DroneEntity 纵向偏移
- RoutePlan: FormationSpacing→LateralSpacing, 新增LateralCount/LongitudinalCount/LongitudinalSpacing
- DroneEntity: 2D编队(latIdx/longIdx), 横向Y展开+纵向X落后
- DefaultFormations: 6种编队模板
- DroneEntityTests: 11个(含2D编队/2×2方队/蜂群)
- Planner: yLanes=ceil(formationWidth/cloudDiam), totalNeeded=single×lanes
2026-06-13 13:20:18 +08:00
4706c4b8c1 feat: 编队模型扩展 — LateralSpacing/LongitudinalSpacing/LateralCount/LongitudinalCount
- FormationSpacing → LateralSpacing(正面横向间距)
- 新增 LongitudinalCount/LongitudinalSpacing(纵深串列)
- DefaultFormations: 6种默认编队模板(单机/3机横队/5机横队/3机纵队/2×2方队/10机蜂群)
- Piston 测试设置 LateralCount=3, LongitudinalCount=1
2026-06-13 13:16:27 +08:00
67223cd392 docs: 仿真器实体与事件映射 — 补充空基流程/Planner/状态机 2026-06-13 12:41:51 +08:00
4e157834f7 docs: 总体架构V9 + DefensePlanner V3 同步当前实现
- 架构文档: IDefenseAdvisor→IDefensePlanner 全量替换,架构图更新
- DefensePlanner: FireUnit模型补全通道/检测字段,数据流改为引擎内部调用
- 旧接口迁移表标记完成,实现状态分已实现/已删除/不变
2026-06-13 12:40:54 +08:00
76d845f91b docs: 同步 0.3.0 版本 — VERSION/CHANGELOG/实施计划更新
- VERSION: 0.2.0 → 0.3.0
- CHANGELOG: 新增 0.3.0 条目(DefensePlanner/空基/通道模型/fallback清理)
- 实施计划: 更新 8.1.3/8.1.6 为完成,指标同步
2026-06-13 12:37:25 +08:00
5e29536b8b build: 更新 Unity DLL + SqliteConnectionTracker 最终修复 2026-06-13 12:35:04 +08:00
a19dced505 fix: SqliteConnectionTracker — AssemblyReloadEvents.beforeAssemblyReload 安全关闭
- 静态追踪所有 SQLiteConnection,domain reload 前 Dispose 全部
- 替换所有 GC.Collect+WaitForPendingFinalizers 为 Tracker 模式
- DatabaseManager、ScenarioManager、中、SimulationRunner 等全部 Track/Untrack
2026-06-13 12:30:48 +08:00
5c23e92587 fix: OnDisable 加 GC.Collect+WaitForPendingFinalizers 防 SQLite 析构崩溃
- sqlite-net PreparedSqlLiteInsertCommand 析构器在 GC 线程跑 sqlite3_finalize
- Domain reload 时 SQLite native 可能已卸载 → 崩溃
- GC.Collect+WaitForPendingFinalizers 确保析构器在 native 还活着时完成
2026-06-13 12:24:09 +08:00
a39439c023 build: 更新 Unity DLL 以匹配新 Engine 构造函数签名 2026-06-13 12:22:33 +08:00
3ba8e4f604 fix: 引擎使用传入的 _planner,不再内部 new DefaultDefensePlanner
- 构造函数传入的 planner 之前从未使用(死代码)
- Initialize 内删除了重复的 DB catalog 加载和 new DefaultDefensePlanner
- 现在统一由调用方注入 Planner
2026-06-13 12:20:51 +08:00
45387fee73 refactor: 删除所有逻辑fallback——缺失配置报错而非静默跳过
- DefaultDefensePlanner: 空catalog抛ArgumentEx, 缺ammo类型抛InvalidOp
- AmmunitionSpec? → AmmunitionSpec(非空), 删除 ==null 静默返回
- 删除 0.8f射程安全系数, 1.5f时间窗口乘数
- AlgorithmFactory 移除IDefensePlanner(null)注册
- SimulationEngine 构造必须传planner,6个调用点全部更新
2026-06-13 12:19:21 +08:00
b5b561039b refactor: 删除引擎 Tick 中 ReleaseAltitude → _disperseHeight fallback 2026-06-13 12:09:31 +08:00
fd723daa78 refactor: 删除 PlatformEntity/BuildFireUnits 中的速度/初速/高度默认值
- PlatformEntity: MuzzleVelocity/CruiseSpeed/ReleaseAltitude 默认 0,由 Planner 校验
- BuildFireUnits: 同上,不再硬写 800f/55f/1000f
- MunitionCount 默认从 3 改为 1
- 35 测试通过
2026-06-13 12:04:35 +08:00
162338ce83 refactor: 删除Planner所有配置默认值——参数必须从FireUnit取,取不到报错
- 删除 CruiseSpeed=55/MuzzleVelocity=800/ReleaseAltitude=+500 等8处默认值
- 空基参数<=0抛 InvalidOperationException,地基初速<=0跳过候选
- 无人机速度/航路长度为0跳过该弹
- 保留物理安全守卫(防NaN/除零)和概率帽0.95
2026-06-13 12:02:57 +08:00
182af5026d fix: recommendedTiming按目标点位置算而非中点——每个弹道到达时间不同
- GenerateFireEventsAt: txArrival = distToTx/droneSpeed 替代 threat.ArrivalTime
- 航路反方向截断保护
- 暴露毁伤时间 34.4s→38.4s
2026-06-13 11:54:39 +08:00
fe7eb723a1 refactor: 清除 SimEvent alias 遗留——统一使用 using CounterDrone.Core.Simulation 2026-06-13 11:50:49 +08:00
ced8d4014a refactor: 删除未使用的 Models.SimEvent,消除 SimEvent 重名
- 删除 Models/SimEvent.cs(DB表定义,无人读写)
- 删除 DatabaseManager 中 CreateTable<SimEvent> 和 CreateIndex
- DatabaseManagerTests 表数量 13→12
2026-06-13 11:46:31 +08:00
0f73bbf55e feat: 报告显示Per-unit发射数 + 事件标明火力单元
- 五、我方部署: 从实际发射事件统计每个火力单元发射数(非想定数据)
- 六、事件时序: 新增"火力单元"列,标注每发隶属哪个单元
- PlatformId→火力单元映射复用(去重)
2026-06-13 11:40:21 +08:00
26a372ffc5 config: 空基火力单元通道从4→8,增强火力覆盖 2026-06-13 11:32:22 +08:00
f4be469e72 fix: 空基巡逻点靠近目标 + 飞行时间优化,141测试全通过
- 空基巡逻点从5000→6000→6500,靠近中点7500
- Piston/Jet/AirBased 全部击毁
- 141测试全通过
2026-06-13 11:30:02 +08:00
c08d613d5b test: 引擎空基/地基发射类型单元测试 + Planner覆盖全面测试
- SimulationEngineTests: AirBased_LaunchesWithCorrectDescription (空基描述="空基投放")
- SimulationEngineTests: GroundBased_LaunchesWithCorrectDescription (地基描述="计划发射")
- DefensePlannerTests: 23个全覆盖(威胁排序/弹药匹配/地基弹道/空基弹道/分配/边界/多威胁/临界)
- FullPipeline AirBased: 加入事件描述诊断断言
2026-06-13 11:27:00 +08:00
7fa0268463 fix: 空基 FireEvent.MuzzleVelocity=0 + 空基弹道单元测试
- GenerateFireEventsAt: 空基时 MuzzleVelocity=0(而非800)
- 新增 AirBased_FireTime_EarlierThanArrival 验证提前量
- 新增 AirBased_FireTime_EarlierThanGroundBased 验证空基更早
- 新增 AirBased_NoMuzzleVelocityInFireEvent 验证 MuzzleVelocity=0
- 空基弹道计算本身正确(recommendedTiming - flightTime - fallTime)
2026-06-13 11:05:34 +08:00
c610573e34 fix: 物理间隔错开发射 — 云团直径/无人机速度,非硬件间隔
- 跨 FireUnit 按 physicsInterval = max(ChannelInterval, cloudDiameter/droneSpeed) 错开发射时间
- Piston 200km/h 测试通过(被摧毁=1)
- Jet 500km/h 测试通过(被摧毁=1)
2026-06-13 10:55:58 +08:00
08427e9e08 feat: 新增轻型地基火力单元模板(2炮×4通道); Piston测试使用 ground-light×2 2026-06-13 10:46:41 +08:00
c08edbd2f9 feat: 火力单元通道模型 + 默认配置 + 引擎通道展开
- FireUnit: 新增 GunCount/ChannelsPerGun/ChannelInterval/检测设备字段
- EquipmentDeployment: 新增 GunCount/ChannelsPerGun/ChannelInterval
- DefaultFireUnits: 3 种默认火力单元(标准地基/重地基/空基)
- Planner: 逐通道齐射分配,ChannelInterval 错开,Cooldown 轮次
- Engine: PlatformEntity 按 GunCount×ChannelsPerGun 展开
- BuildFireUnits: 与引擎同步展开通道
- FullPipelineTests: 使用 DefaultFireUnits 配置
2026-06-13 10:42:08 +08:00
9bd1d5a7ee test: 收紧集成测试断言 — 击毁才通过,暴露 Planner 真实场景不足
- 删除 ApplyDefensePlan helper(引擎自治,不再需要外部调Planner)
- Piston 200km/h: 断言 DroneStatus==Destroyed(当前 FAIL)
- Jet 500km/h: 断言 DroneStatus==Destroyed
- AirBased 300km/h: 断言 DroneStatus==Destroyed(当前 FAIL)
- 测试真实暴露 Planner 算法缺陷
2026-06-13 10:06:05 +08:00
50ec14bb24 refactor: 引擎自治 — 不再需要外部传 SetFireSchedule
- SimulationEngine.Initialize 内建 BuildFireUnits/BuildDroneGroups → 自动调 Planner
- 删除 FullPipelineTests 中的 BuildFireSchedule/_lastFireSchedule/手动 SetFireSchedule
- Unity Managers 删除手动 SetFireSchedule 调用
- 引擎是 Planner 的唯一调用者,测试和 UI 不再碰火控逻辑
2026-06-13 09:58:11 +08:00
77351d8198 fix: FireEvent 冷却错开,同平台多弹不再同 tick 被跳过
根因:同一平台多弹 FireTime 相同,引擎同 tick 处理时第 1 发后冷却触发 Ready=false,后续全被跳。
修正:每发 FireTime 增加 i * Cooldown,保证间隔 ≥ 冷却时间。
2026-06-13 09:56:11 +08:00
e3ea595204 refactor: SimulationEngine 内部调用 Planner,不再依赖外部传 SetFireSchedule
- Initialize 内建 BuildFireUnits/BuildDroneGroups
- 自动从 DB 取 AmmunitionSpec 创建 DefaultDefensePlanner
- SetFireSchedule 保留为手动覆盖(测试可用)
- fireSchedule 空时才自动生成
2026-06-13 09:53:08 +08:00
4411a452dc fix: Quantity expand + PlatformIndex 映射修正
- ApplyDefensePlan 展开 EquipmentDeployment.Quantity → 多个 FireUnit
- Planner 分配后用 fireUnits.IndexOf 修正 PlatformIndex
- 新增 FullPipeline_Piston_VerifiesRoundAllocation 验证 ≥8 发
- Piston 集成测试加入计划大小断言
2026-06-13 09:50:24 +08:00
5667aef495 fix: OnDisable + Dispose 替代 OnDestroy + Close,修复 domain reload SQLite 崩溃
OnDisable 在 domain reload 前触发,Dispose() 清理 prepared statements 并调用 GC.SuppressFinalize,阻止 GC finalizer 线程崩溃
2026-06-13 09:42:10 +08:00
8366bfcb93 fix: Planner 从多单元凑弹药,不再被单个 TotalMunitions 卡死
每个威胁可从多个火力单元汇集弹药,直到满足需求或耗尽。
单元测试 24/24 通过。
2026-06-13 09:40:37 +08:00
2bb8ba2e1e fix: Manager OnDestroy 关闭 SQLite 连接,修复 domain reload 崩溃
- 所有 Manager 在 Awake 保存 _db 引用,OnDestroy 调用 Close()
- ScenarioManager/ModelManager/ReportManager/GroupManager/SimulationRunner 全部修复
- 崩溃原因:domain reload 时 GC finalizer 线程清理 prepared statements,DB 未关闭导致 sqlite3_finalize 竞态
2026-06-13 09:35:53 +08:00
b957a5e2b4 feat: Planner 使用真实物理计算 + 完善单元测试 + 规则7
- ComputeEffectiveRadius: 基于 AmmunitionSpec 三阶段扩散模型
- CalcRoundsNeeded: 真实有效半径和弹药参数
- GenerateFireEvents: recommendedTiming = arrivalTime - expansionTime
- 候选概率基于真实覆盖计算
- 24 个单元测试覆盖威胁排序/弹药匹配/候选过滤/弹药数/时机/多威胁/空基
- AGENTS.md 规则7: 只跑相关测试,不跑全量
2026-06-13 09:30:04 +08:00
7d080bc929 fix: Unity 测试配置 - 火力单元改为 8×3 发(与集成测试一致) 2026-06-13 09:19:30 +08:00
66537d1621 fix: Unity 脚本编译错误 - 缺少 AVector3 别名和 detail 变量 2026-06-13 09:16:47 +08:00
3a2f68b02b refactor: 删除 SimulationRunner.BuildFireSchedule fallback
- Planner 输出直接传给 Engine.SetFireSchedule
- ManagerVerification/SimulationBootstrap 在 LoadAndStart 前设置 fire schedule
- 引擎不再需要从 CloudDispersal 反推发射计划
2026-06-13 09:15:21 +08:00
317f47280b refactor: DefensePlanner 替代 DefenseAdvisor
- 新增 IDefensePlanner 接口 + DefaultDefensePlanner 实现
- FireUnit 重构为统一平台模型(Type/Position/CruiseSpeed/ReleaseAltitude/MuzzleVelocity)
- DroneGroup 加入威胁指数(类型系数×速度系数)和优先级排序
- 五步流水线:威胁排序→弹药匹配→候选生成→贪心分配→时序生成
- 弹药精确匹配,不降级;临界方案基于50%概率阈值
- 删除 IDefenseAdvisor/DefaultDefenseAdvisor/RecommendMultiGroup
- AlgorithmFactory 注册 IDefensePlanner
- 测试更新:DefensePlannerTests 替代旧测试,FullPipelineTests 适配新 API
- Unity Managers 更新调用
- 125 测试全通过
2026-06-12 18:20:25 +08:00
1e07bcc535 docs: DefensePlanner 技术方案定稿
- 五步流水线:威胁排序→弹药匹配→候选生成→贪心分配→时序生成
- 统一输入:List<FireUnit> + List<DroneGroup> + CombatScene
- 输出:PlannerResult { Best, Critical }
- 威胁排序:类型系数×速度系数 / 到达时间
- 临界方案:刚好≥50%概率的最小配置
- 弹药精确匹配,不降级;贪心 v1,后续可升级
2026-06-12 18:12:41 +08:00
0a3c444a4b refactor: 推荐算法统一处理空基/地基平台类型
- ThreatProfile 新增 PreferredPlatformType 字段
- DefaultDefenseAdvisor 根据平台类型正确计算 FireTime/CruiseSpeed/ReleaseAltitude
- 空基:FireTime 提前扣除飞行+下落时间;地基:保持原逻辑
- 删除 SimulationEngine.AdjustFireTimesForAirBased(引擎不再做适配)
- 集成测试直接用 PreferredPlatformType 驱动空基方案
- 实施文档同步
2026-06-12 17:52:44 +08:00
c4d9ccc4fa feat: 空基平台飞行与投弹功能
- PlatformEntity: 新增状态机 Idle→FlyingToTarget→ReadyToRelease
- EquipmentDeployment: 新增 CruiseSpeed 字段(空基平台巡航速度)
- MunitionEntity: 空投弹药继承载机速度矢量,替代自由落体
- Kinematics: 新增 AirDropPosition/AirDropFallTime/DirectionVelocity 工具方法
- SimulationEngine: 空基平台先飞后投,AdjustFireTimesForAirBased 补偿飞行耗时
- SimulationRunner: 空基平台 3D 可视化(蓝色 Capsule)
- 集成测试 Scenario_AirBased_PlatformFliesAndDrops 通过(8x 加速)
- 实施文档同步进度
2026-06-12 17:38:21 +08:00
203 changed files with 13468 additions and 3370 deletions

1
.gitignore vendored
View File

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

View File

@ -76,6 +76,65 @@ pwsh scripts/check_unity_build.ps1
This automatically rebuilds Core.dll, copies it to Unity Plugins, and compiles Unity scripts. Exits 0 if all pass.
## 7. Run Targeted Tests First
**Always run the narrowest relevant test first.** If you just changed `DefensePlannerTests`, run `--filter 'FullyQualifiedName~DefensePlannerTests'`. If you changed `FullPipelineTests`, run that filter. Don't start with `dotnet test` on the whole project — it wastes time and buries the failures you're looking for.
Run full suite only after the targeted tests pass, to verify nothing else broke.
```bash
# Run a test class:
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/ --filter 'FullyQualifiedName~DefensePlannerTests'"
# Run a single test:
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/ --filter 'FullyQualifiedName~Scenario_AirBased'"
# Run full suite (only after targeted passes):
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/"
```
## 8. No Hardcoded Defaults or Fallbacks
**Every parameter must come from configuration. If a required value is missing, fail explicitly — never silently substitute a default.**
Good:
```csharp
if (unit.CruiseSpeed <= 0)
throw new InvalidOperationException($"单元 {unit.Id}: CruiseSpeed 必须 > 0");
```
Bad:
```csharp
float speed = unit.CruiseSpeed > 0 ? unit.CruiseSpeed : 55f; // 55f 是哪来的?
float mv = unit.MuzzleVelocity ?? 800f; // 为什么是 800
float alt = unit.ReleaseAltitude > 0 ? unit.ReleaseAltitude : threat.Altitude + 500f; // 500
```
This applies to:
- `??` operator with arbitrary numbers (55f, 800f, 1000f, 3, etc.)
- Ternary `> 0 ? x : default` patterns
- `Math.Max(0.1f, x)` to prevent division by zero — instead validate the input before the division
- `if (fireTime < 0.1f) fireTime = 0.1f` — instead skip the event and report the failure
Physics constants (9.81f, 3.6f, π) and documented model parameters (Phase 2 cutoff = 30s) are NOT arbitrary defaults — they are legitimate parts of the physical model.
## 9. Git Commit Must Include DLL
**Every commit must bundle the compiled CounterDrone.Core.dll.** Never leave DLL changes as a separate follow-up commit. The full commit flow:
```bash
# 1. Run full test suite (builds Core.dll)
pwsh -Command "dotnet test test/unit/CounterDrone.Core.Tests/"
# 2. Copy DLL to Unity plugin directories
cp src/CounterDrone.Core/bin/Debug/netstandard2.1/CounterDrone.Core.dll src/Unity/Assets/Plugins/CounterDrone.Core/
cp src/CounterDrone.Core/bin/Debug/netstandard2.1/CounterDrone.Core.dll unity_plugins/
# 3. Commit everything at once
git add -A
git commit -m "..."
```
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

View File

@ -1,8 +1,388 @@
# Changelog
本文件记录项目对外发布的变更历史(用户视角)。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
---
## [0.12.0] - 2026-06-20
### Added — PDF 导出 + 报告模板架构Phase 8.2.1
- **PdfSharpCore 1.3.64**:纯托管 PDF 库netstandard2.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.2.0
0.12.0

View File

@ -1,56 +0,0 @@
[
{
"Id": "default-inert",
"AerosolType": 0,
"Name": "惰性气体弹(发烟罐型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.5,
"EdgeDensity": 0.1,
"InitialTemperature": 1800.0,
"BuoyancyFactor": 0.3,
"EffectiveConcentration": 0.0001,
"MaxRadius": 100.0,
"MaxDuration": 120.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"ParticlesJson": "{}"
},
{
"Id": "default-active",
"AerosolType": 1,
"Name": "活性材料弹(爆炸分散型)",
"InitialRadius": 5.0,
"InitialVolume": 30000.0,
"CoreDensity": 2.0,
"EdgeDensity": 0.2,
"InitialTemperature": 2400.0,
"BuoyancyFactor": 0.6,
"EffectiveConcentration": 0.0002,
"MaxRadius": 80.0,
"MaxDuration": 90.0,
"SourceStrength": 12.0,
"BurstChargeKg": 4.0,
"TurbulentExpansionK": 4.0,
"ParticlesJson": "{}"
},
{
"Id": "default-fuel",
"AerosolType": 2,
"Name": "活性燃料弹(抛射分散型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.8,
"EdgeDensity": 0.15,
"InitialTemperature": 1900.0,
"BuoyancyFactor": 0.4,
"EffectiveConcentration": 0.0001,
"MaxRadius": 90.0,
"MaxDuration": 100.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"ParticlesJson": "{}"
}
]

209
data/defaults.json Normal file
View File

@ -0,0 +1,209 @@
{
"version": "3",
"ammunition": [
{
"Id": "inert",
"AerosolType": 0,
"Name": "[Demo] 惰性气体弹(发烟罐型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.5,
"EdgeDensity": 0.1,
"InitialTemperature": 1800.0,
"BuoyancyFactor": 0.3,
"EffectiveConcentration": 0.0001,
"MaxRadius": 100.0,
"MaxDuration": 120.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"Phase2Duration": 30.0,
"ParticlesJson": "{}"
},
{
"Id": "active",
"AerosolType": 1,
"Name": "[Demo] 活性材料弹(爆炸分散型)",
"InitialRadius": 5.0,
"InitialVolume": 30000.0,
"CoreDensity": 2.0,
"EdgeDensity": 0.2,
"InitialTemperature": 2400.0,
"BuoyancyFactor": 0.6,
"EffectiveConcentration": 0.0001,
"MaxRadius": 80.0,
"MaxDuration": 90.0,
"SourceStrength": 12.0,
"BurstChargeKg": 4.0,
"TurbulentExpansionK": 4.0,
"Phase2Duration": 30.0,
"ParticlesJson": "{}"
},
{
"Id": "fuel",
"AerosolType": 2,
"Name": "[Demo] 活性燃料弹(抛射分散型)",
"InitialRadius": 3.8,
"InitialVolume": 14000.0,
"CoreDensity": 1.8,
"EdgeDensity": 0.15,
"InitialTemperature": 1900.0,
"BuoyancyFactor": 0.4,
"EffectiveConcentration": 0.0001,
"MaxRadius": 90.0,
"MaxDuration": 100.0,
"SourceStrength": 10.0,
"BurstChargeKg": 1.5,
"TurbulentExpansionK": 3.0,
"Phase2Duration": 30.0,
"ParticlesJson": "{}"
}
],
"formations": [
{ "Id": "single", "Name": "[Demo] 单机", "FormationMode": 0, "LateralCount": 1, "LongitudinalCount": 1, "LateralSpacing": 0, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
{ "Id": "line-3", "Name": "[Demo] 3机横队", "FormationMode": 1, "LateralCount": 3, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
{ "Id": "line-5", "Name": "[Demo] 5机横队", "FormationMode": 1, "LateralCount": 5, "LongitudinalCount": 1, "LateralSpacing": 50, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 },
{ "Id": "column-3", "Name": "[Demo] 3机纵队", "FormationMode": 1, "LateralCount": 1, "LongitudinalCount": 3, "LateralSpacing": 0, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 },
{ "Id": "box-2x2", "Name": "[Demo] 2×2方队", "FormationMode": 1, "LateralCount": 2, "LongitudinalCount": 2, "LateralSpacing": 50, "LongitudinalSpacing": 100, "LateralAxis": 2, "LongitudinalAxis": 0 },
{ "Id": "swarm-10", "Name": "[Demo] 蜂群(10架)", "FormationMode": 2, "LateralCount": 10, "LongitudinalCount": 1, "LateralSpacing": 30, "LongitudinalSpacing": 0, "LateralAxis": 2, "LongitudinalAxis": 0 }
],
"routes": [
{ "Id": "3km-h400", "Name": "[Demo] 3km航线-400m高", "Waypoints": [{"X":0,"Y":400,"Z":0},{"X":3000,"Y":400,"Z":0}] },
{ "Id": "3km-h300", "Name": "[Demo] 3km航线-300m高", "Waypoints": [{"X":0,"Y":300,"Z":0},{"X":3000,"Y":300,"Z":0}] },
{ "Id": "5km-h500", "Name": "[Demo] 5km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":5000,"Y":500,"Z":0}] },
{ "Id": "5km-h800", "Name": "[Demo] 5km航线-800m高", "Waypoints": [{"X":0,"Y":800,"Z":0},{"X":5000,"Y":800,"Z":0}] },
{ "Id": "10km-h500", "Name": "[Demo] 10km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":10000,"Y":500,"Z":0}] },
{ "Id": "20km-h500", "Name": "[Demo] 20km航线-500m高", "Waypoints": [{"X":0,"Y":500,"Z":0},{"X":20000,"Y":500,"Z":0}] }
],
"fireUnits": [
{
"Id": "ground-light", "Name": "[Demo] 轻型地基火力单元",
"Description": "4通道轻型平台800m/s初速雷达6km",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1],
"RadarRange": 6000.0, "EORange": 4000.0, "IRRange": 2000.0,
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 20000.0
},
{
"Id": "ground-standard", "Name": "[Demo] 标准地基火力单元",
"Description": "4通道标准平台800m/s初速雷达15km",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1],
"RadarRange": 15000.0, "EORange": 8000.0, "IRRange": 5000.0,
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 20000.0
},
{
"Id": "ground-heavy", "Name": "[Demo] 重型地基火力单元",
"Description": "6通道重型平台600m/s初速雷达11km支持全弹种",
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
"AmmoTypes": [0, 1, 2],
"RadarRange": 11200.0, "EORange": 7000.0, "IRRange": 4000.0,
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 25000.0
},
{
"Id": "air-standard", "Name": "[Demo] 标准空基火力单元",
"Description": "8通道空基平台80m/s巡航雷达8km+光电/红外",
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1],
"RadarRange": 4000.0, "EORange": 3000.0, "IRRange": 2000.0,
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0
}
],
"launchPlatforms": [
{
"Id": "ground-light", "Name": "[Demo] 轻型地基发射平台",
"Description": "4通道轻型平台800m/s初速",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1]
},
{
"Id": "ground-standard", "Name": "[Demo] 标准地基发射平台",
"Description": "4通道标准平台800m/s初速",
"PlatformType": 1, "GunCount": 4, "ChannelsPerGun": 4, "ChannelInterval": 0.1,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 800.0,
"AmmoTypes": [0, 1]
},
{
"Id": "ground-heavy", "Name": "[Demo] 重型地基发射平台",
"Description": "6通道重型平台600m/s初速支持全弹种",
"PlatformType": 1, "GunCount": 6, "ChannelsPerGun": 4, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0, "MuzzleVelocity": 600.0,
"AmmoTypes": [0, 1, 2]
},
{
"Id": "air-standard", "Name": "[Demo] 标准空基发射平台",
"Description": "8通道空基平台80m/s巡航",
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
"MuzzleVelocity": 80.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1]
}
],
"drones": [
{ "Id": "quadcopter", "Name": "[Demo] 小型四旋翼DJI类",
"Model": "DJI Mavic 类", "Description": "小型四旋翼,低空侦察",
"DroneType": 0, "PowerType": 0, "Wingspan": 1.2, "TypicalSpeed": 60.0, "TypicalAltitude": 300.0 },
{ "Id": "electric-scout", "Name": "[Demo] 电推侦察无人机",
"Model": "电推侦察型", "Description": "电推固定翼,中低空侦察",
"DroneType": 1, "PowerType": 0, "Wingspan": 1.8, "TypicalSpeed": 100.0, "TypicalAltitude": 500.0 },
{ "Id": "fixed-piston", "Name": "[Demo] 固定翼活塞Orlan类",
"Model": "Orlan-10 类", "Description": "活塞固定翼,中空侦察",
"DroneType": 1, "PowerType": 1, "Wingspan": 3.5, "TypicalSpeed": 150.0, "TypicalAltitude": 1000.0 },
{ "Id": "shahed", "Name": "[Demo] 活塞巡飞弹Shahed类",
"Model": "Shahed-136", "Description": "活塞巡飞弹,低空攻击",
"DroneType": 1, "PowerType": 1, "Wingspan": 2.5, "TypicalSpeed": 200.0, "TypicalAltitude": 500.0 },
{ "Id": "tb2", "Name": "[Demo] 中空长航时TB2类",
"Model": "TB2 类", "Description": "活塞中空长航时",
"DroneType": 1, "PowerType": 1, "Wingspan": 12.0, "TypicalSpeed": 220.0, "TypicalAltitude": 5500.0 },
{ "Id": "cruise-missile", "Name": "[Demo] 巡航导弹(喷气式)",
"Model": "巡航导弹类", "Description": "喷气式高速目标",
"DroneType": 2, "PowerType": 2, "Wingspan": 1.5, "TypicalSpeed": 200.0, "TypicalAltitude": 2000.0 }
],
"detectionEquipment": [
{ "Id": "radar-mr", "Name": "[Demo] 中程防空雷达",
"RadarRange": 20000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 30.0,
"MinElevation": -2.0, "MaxElevation": 70.0, "MinDetectAlt": 50.0, "MaxDetectAlt": 30000.0 },
{ "Id": "radar-sr", "Name": "[Demo] 近程防空雷达",
"RadarRange": 10000.0, "EORange": 0.0, "IRRange": 0.0, "Accuracy": 50.0,
"MinElevation": -2.0, "MaxElevation": 70.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0 },
{ "Id": "eo-station", "Name": "[Demo] 光电跟踪站",
"RadarRange": 0.0, "EORange": 15000.0, "IRRange": 8000.0, "Accuracy": 20.0,
"MinElevation": -10.0, "MaxElevation": 90.0, "MinDetectAlt": 10.0, "MaxDetectAlt": 20000.0 },
{ "Id": "ir-sentry", "Name": "[Demo] 红外哨",
"RadarRange": 0.0, "EORange": 0.0, "IRRange": 10000.0, "Accuracy": 40.0,
"MinElevation": -5.0, "MaxElevation": 85.0, "MinDetectAlt": 10.0, "MaxDetectAlt": 15000.0 }
],
"weather": [
{ "Id": "sunny-calm", "Name": "[Demo] 晴天无风",
"WeatherType": 0, "WindSpeed": 3.0, "WindDirection": 0,
"Temperature": 25.0, "Humidity": 50.0, "Pressure": 1013.0, "Visibility": 8000.0 },
{ "Id": "sunny-windy", "Name": "[Demo] 晴天大风",
"WeatherType": 0, "WindSpeed": 10.0, "WindDirection": 4,
"Temperature": 28.0, "Humidity": 40.0, "Pressure": 1010.0, "Visibility": 10000.0 },
{ "Id": "overcast", "Name": "[Demo] 阴天",
"WeatherType": 1, "WindSpeed": 5.0, "WindDirection": 2,
"Temperature": 18.0, "Humidity": 70.0, "Pressure": 1015.0, "Visibility": 5000.0 },
{ "Id": "fog", "Name": "[Demo] 雾天",
"WeatherType": 2, "WindSpeed": 2.0, "WindDirection": 1,
"Temperature": 12.0, "Humidity": 95.0, "Pressure": 1020.0, "Visibility": 500.0 },
{ "Id": "rain", "Name": "[Demo] 雨天",
"WeatherType": 3, "WindSpeed": 8.0, "WindDirection": 3,
"Temperature": 15.0, "Humidity": 90.0, "Pressure": 1005.0, "Visibility": 3000.0 },
{ "Id": "night", "Name": "[Demo] 夜间",
"WeatherType": 4, "WindSpeed": 2.0, "WindDirection": 5,
"Temperature": 10.0, "Humidity": 65.0, "Pressure": 1018.0, "Visibility": 2000.0 }
]
}

BIN
data/fonts/CJK-Font.ttf Normal file

Binary file not shown.

19
data/planner_config.json Normal file
View File

@ -0,0 +1,19 @@
{
"CloudOverlapRatio": 0.2,
"CriticalProbabilityThreshold": 0.5,
"MaxInterceptProbability": 0.95,
"TypeCoefficient": {
"HighSpeed": 4.0,
"FixedWing": 2.0,
"Rotor": 1.0
},
"AmmoMatch": {
"Electric": "InertGas",
"Piston": "InertGas",
"Jet": "ActiveMaterial"
},
"DefaultDetectionAccuracy": 50.0,
"ExpansionFactor": 0.9,
"TimingSafetyMargin": 1.0,
"ReactionTime": 5.0
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,369 @@
# DefensePlanner 防御规划引擎 — 技术方案
- **版本**V4
- **日期**2026-06-15
- **状态**:已实现
---
## 1. 概述
DefensePlanner 是防御推荐模块的核心引擎。它接收**可用火力单元池**和**威胁批次列表**,综合考虑弹药匹配、空间可达性、时间约束、资源竞争,输出**最优分配方案**和**临界边际方案**。
**当前问题**
- `DefaultDefenseAdvisor` 本质是"单威胁 → 单方案"的规则匹配器
- `Recommend()` 接收 `ThreatProfile`(不含火力单元信息),无法做资源分配
- `RecommendMultiGroup()` 接收 `List<FireUnit>`,但决策逻辑简单(先到先服务)
- 空基/地基平台类型通过 `PreferredPlatformType` 开关绕过,而非算法自主选择
**目标**:统一的规划入口,给定火力单元池和威胁列表,输出分配方案。
---
## 2. 输入模型
### 2.1 FireUnit火力单元
统一表达空基和地基平台,作为规划器的基本资产单元:
```csharp
class FireUnit {
string Id; // 唯一标识
PlatformType Type; // AirBased / GroundBased
Vector3 Position; // 待命坐标(空基含巡航高度)
// ── 发射装置 ──
int GunCount; // 火炮数量(默认 1
int ChannelsPerGun; // 每炮火力通道数(默认 1
int TotalChannels => GunCount * ChannelsPerGun;
float ChannelInterval; // 同通道连发最小间隔 s
// ── 弹药 ──
int TotalMunitions; // 总载弹量
List<AerosolType> AmmoTypes; // 可装填的弹药类型
float Cooldown; // 通道冷却时间 s
float AmmoChangeTime; // 更换弹种时间 s
// ── 搜索跟踪 ──
float RadarRange; // 雷达探测距离 m
float EORange; // 光电探测距离 m
float IRRange; // 红外探测距离 m
// ── 空基 ──
float CruiseSpeed; // 巡航速度 m/s
float ReleaseAltitude; // 投放高度 m
// ── 地基 ──
float MuzzleVelocity; // 初速 m/s
}
```
### 2.2 DroneWave威胁批次
```csharp
class DroneWave {
string WaveId; // 批次 ID
ScenarioDrone Profile; // 类型、数量、动力、翼展、速度、高度
List<Waypoint> Waypoints; // 航路点
float ArrivalTime; // 预计算:到达防御区域中点的时间 s
}
```
### 2.3 CombatScene作战环境
沿用现有的 `CombatScene`,提供风速、风向、天气等影响扩散模型计算的参数。
---
## 3. 输出模型
### 3.1 UnitAssignment单元分配
```csharp
class UnitAssignment {
string FireUnitId; // 分配到的火力单元
string DroneWaveId; // 对抗的威胁批次
AerosolType AmmoType; // 装填的弹药类型
int RoundsFired; // 本次发射几发
float FirstFireTime; // 首发发射时机 s
List<FireEvent> FireEvents; // 具体发射事件(含位置、时间)
}
```
### 3.2 DefensePlan规划结果
```csharp
class DefensePlan {
List<UnitAssignment> Assignments; // 分配方案
List<FireEvent> MergedSchedule; // 合并后的发射计划(排序)
float OverallProbability; // 总体拦截概率估计
int ThreatsEngaged; // 被分配方案的威胁数
int ThreatsUnengaged; // 无法分配方案的威胁数
string Summary; // 人类可读概览
}
```
### 3.3 DefensePlanner 输出
```csharp
class PlannerResult {
DefensePlan Best; // 最优方案
DefensePlan Critical; // 临界方案(刚好有效)
}
```
---
## 4. 内部流程(五步法)
```
输入List<FireUnit> + List<DroneWave> + CombatScene
Step 1 — 威胁排序
│ 威胁指数 = 速度系数 × 目标类型系数(可扩展重量、载弹量等)
│ 综合优先级 = 威胁指数 / (到达时间 + 1)
│ 同到达时间下高威胁优先
Step 2 — 弹药匹配
│ PowerType → AerosolType规则表
│ 输出:每个威胁需要的弹药类型
Step 3 — 候选生成
│ 对每个(威胁, 火力单元)组合:
│ ① 弹药兼容性检查
│ ② 空间可达性(地基:弹道射程;空基:飞行距离)
│ ③ 时间窗口计算(最早/最晚拦截时机)
│ 输出List<InterceptCandidate>
Step 4 — 分配求解
│ 贪心策略v1后续可升级回溯/匈牙利):
│ 按威胁优先级遍历
│ → 选最早可拦截的兼容单元
│ → 弹药必须精确匹配不降级ActiveMaterial 不能替代 InertGas
│ → 标记单元占用(冷却 + 换弹 + 飞行时间)
│ → 弹药耗尽则移除
Step 5 — 时序生成
│ 对每个分配:
│ 地基FireTime = 最佳交汇时刻 炮弹飞行时间
│ 空基FireTime = 最佳交汇时刻 弹药下落时间(固定阵位水平发射)
│ 合并排序 → MergedFireSchedule
输出PlannerResult { Best, Critical }
```
---
## 5. 关键算法详设
### 5.1 威胁排序
**威胁指数** = 速度系数 × 目标类型系数(后续可扩展重量、载弹量等要素)
| 目标类型 | 类型系数 | 理由 |
|----------|:------:|------|
| 高速目标300km/h+ | 4 | 最快突防 |
| 喷气式 | 3 | 高温发动机,毁伤窗口短 |
| 固定翼 | 2 | 速度中等 |
| 活塞式 | 2 | 速度中等 |
| 旋翼 | 1 | 慢速 |
| 电推 | 1 | 慢速,无热源 |
速度系数 = TypicalSpeed / 60以 60 km/h 为基准归一化)
**综合优先级** = 威胁指数 / (ArrivalTime + 1),按降序排列。同到达时间下高威胁优先。
### 5.2 InterceptCandidate拦截候选
```csharp
class InterceptCandidate {
FireUnit Unit;
AerosolType AmmoType;
float EarliestTime; // 最早可拦截时刻(仿真秒)
float LatestTime; // 最晚可拦截时刻
float Coverage; // 有效覆盖时长
float KillProbability; // 预计杀伤概率
}
```
**候选生成逻辑**
对于给定的威胁批次和火力单元:
1. **弹药兼容性**`Unit.AmmoTypes` 包含威胁需要的弹药类型
2. **地基可达性**:计算目标与部署点的水平距离,校验 `MuzzleVelocity` 射程
3. **空基可达性**:计算飞行距离,`飞行时间 = distance(巡逻点, 投放点) / CruiseSpeed`
4. **时间窗口**
- 威胁到达防御区域的时间段 `[tEnter, tExit]`
- 单元可拦截的时间段 = 威胁窗口 ∩(单元可用时间 + 弹药飞行时间)
- 空基额外加平台飞行时间
5. **覆盖率**:基于云团有效半径、持续时间、无人机速度的综合估计
### 5.3 贪心分配算法
```
pending = threats.OrderBy(t => t.ArrivalTime)
available = fireUnits.Clone()
timeNow = 0
assignments = []
for each threat in pending:
candidates = GenerateCandidates(threat, available, timeNow)
if candidates.isEmpty:
threats.Unengaged++
continue
best = candidates.OrderBy(c => c.EarliestTime)
.ThenByDescending(c => c.KillProbability)
.First()
assignment = Commit(best, threat)
assignments.Add(assignment)
// 更新单元状态
unit = best.Unit
unit.TotalMunitions -= assignment.RoundsFired
timeNow = max(timeNow, assignment.LastFireTime + unit.Cooldown)
```
### 5.4 临界方案Critical Plan
临界方案 = 刚好达到可接受下限概率(默认 50%)的最小配置:
- 从最优方案的分配列表出发
- 逐次减少每个单元分配的弹药数
- 直到整体拦截概率刚好跌破 50%
- 上一轮≥50% 的最小配置)即为临界方案
> 作用:给操作员一个置信区间——最优 vs 临界,展示"再少就不够了"的底线。
### 5.5 多批次合并
```
Step 4 的贪心算法天然支持多威胁:
- 按优先级顺序处理
- 前一威胁占用的单元在后续威胁中不可用(直到冷却/换弹完成)
- 弹药消耗全局追踪
```
---
## 6. 接口设计
### 6.1 规划器接口
```csharp
public interface IDefensePlanner
{
/// <summary>为给定火力单元池和威胁列表生成规划方案</summary>
PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene environment);
}
```
### 6.2 兼容现有接口
`DefaultDefenseAdvisor` 改造为 `DefaultDefensePlanner`
```csharp
public class DefaultDefensePlanner : IDefensePlanner
{
private readonly List<AmmunitionSpec> _ammoCatalog;
public DefaultDefensePlanner(List<AmmunitionSpec> ammoCatalog) { ... }
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats, CombatScene env)
{
// Step 1-5
}
// 内部方法
private List<DroneWave> Prioritize(List<DroneWave> threats) { ... }
private AerosolType MatchAmmo(PowerType power) { ... }
private List<InterceptCandidate> GenerateCandidates(DroneWave threat, List<FireUnit> units, float now) { ... }
private DefensePlan Solve(List<DroneWave> threats, List<FireUnit> units) { ... }
private DefensePlan DeriveCritical(DefensePlan best) { ... }
}
```
### 6.3 接口迁移(已完成)
| 旧接口 | 新接口 | 状态 |
|--------|--------|------|
| `IDefenseAdvisor.Recommend(ThreatProfile)` | `IDefensePlanner.Plan(fireUnits, threats, env)` | ✅ 已替换 |
| `IDefenseAdvisor.GetDefenseRecommendation(scenarioId)` | 不再需要 | ✅ 已删除 |
| `DefaultDefenseAdvisor.RecommendMultiGroup()` | Planner 原生支持多批次 | ✅ 已删除 |
---
## 7. 数据流改造(已实现)
```
SimulationEngine.Initialize(scenarioId)
│ // 引擎内部组装
├── BuildFireUnits(config) → List<FireUnit>
├── BuildDroneWaves(config) → List<DroneWave>
└── _scene (CombatScene)
IDefensePlanner.Plan(fireUnits, threats, scene)
PlannerResult.Best.MergedSchedule
引擎 Tick() 按发射计划执行
```
> 引擎是 Planner 的唯一调用者。测试和 UI 代码不再直接接触 Planner。
---
## 8. 实现状态
### ✅ 已实现
- `IDefensePlanner` + `DefaultDefensePlanner`(五步流水线)
- `FireUnit` 通道模型GunCount/ChannelsPerGun/ChannelInterval/搜索跟踪设备)
- 贪心分配 + 物理间隔错发 + 按目标点独立计时
- 空基/地基弹道统一处理
- `SimulationEngine` 内部调用 Planner 自动生成发射计划
- `DefaultFireUnits` 4 种模板
- 报告显示 per-unit 发射数和火力单元列
- 所有硬编码默认值和静默 fallback 已删除
### ❌ 已删除
- `IDefenseAdvisor` / `DefaultDefenseAdvisor` / `RecommendMultiGroup`
- `ThreatProfile` / `DefenseSolution` / `DefenseRecommendation` / `MultiGroupRecommendation`AlgorithmTypes 中保留但未使用)
- `Models.SimEvent`(死代码)
- 所有 `?.` + 任意默认值模式
### 🔒 不变
- `Kinematics` / `GaussianPuffDispersion` / `DamageModelRouter`
- 所有 Repository 和数据模型ScenarioUnit 新增 GunCount/ChannelsPerGun/ChannelInterval
- `ScenarioService` 接口
---
## 9. 风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| 贪心算法局部最优,整体非最优 | 部分威胁可能被跳过 | 后续可升级为匈牙利算法或回溯搜索;贪心结果对大多数场景足够 |
| 覆盖率估算依赖简化模型 | 拦截概率不精确 | 使用现有的 `GaussianPuffDispersion` + `Kinematics`,与仿真引擎一致 |
| 空基/地基混合分配复杂 | 边界情况遗漏 | Step 3 候选生成对每种平台类型独立计算Step 4 统一排序;单元测试覆盖典型混合场景 |
---
## 10. 已决策事项
| # | 议题 | 决策 |
|---|------|------|
| 1 | 威胁排序 | 综合威胁指数类型系数高速4/喷气3/固定翼2/活塞2/旋翼1/电推1× 速度系数km/h ÷ 60除以到达时间。可扩展重量、载弹量等要素 |
| 2 | 临界方案 | 概率阈值定义:刚好 ≥ 50% 的最小资源配置 |
| 3 | 分配算法 | v1 贪心;后续按需升级回溯搜索或匈牙利算法 |
| 4 | Failover | 不做降级。弹药必须精确匹配ActiveMaterial 不能替代 InertGas |

View File

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

View File

@ -0,0 +1,58 @@
# 默认想定参数对照表
> **目的**:跟踪 6 个 Demo 想定的参数配置,确保探测范围、部署位置、航路长度等相互匹配。
> **更新日期**2026-06-16
---
## 一、无人机模板
| ID | 名称 | 动力 | 翼展 | 速度(km/h) | 典型高度 |
|------|------|------|:---:|:---:|:---:|
| electric-scout | 电推侦察无人机 | 电推 | 1.8m | 100 | 500m |
| shahed | 活塞巡飞弹Shahed类 | 活塞 | 2.5m | 200 | 500m |
| cruise-missile | 巡航导弹(喷气式) | 喷气 | 1.5m | 200 | 2000m |
> **注**:速度以航路 waypoint.Speed 为准TypicalSpeed 仅展示用。
---
## 二、火力单元模板
| ID | 类型 | 雷达 | 光电 | 红外 | 3D 球冠 | 初速/巡航 |
|------|------|------:|------:|------:|------|:---:|
| ground-light | 地基 | 6000m | 4000m | 2000m | -5°~85°, 30m~20km | 800m/s |
| ground-standard | 地基 | 15000m | 8000m | 5000m | -5°~85°, 30m~20km | 800m/s |
| ground-heavy | 地基 | 8000m | 5000m | 3000m | -5°~85°, 30m~25km | 600m/s |
| air-standard | 空基 | — | 9600m | 6000m | -80°~30°, 30m~15km | 80m/s |
---
## 三、Demo 想定对照
| # | 想定 | 无人机 | 高度 | 速度 | 航路 | 火力单元 | 位置 | 探测 |
|---|------|------|:---:|:---:|:---:|------|------|:---:|
| 1 | 无防御 | shahed | 400m | 600 | 3km | — | — | — |
| 2 | 管控区侵入 | electric-scout | 300m | 300 | 3km | — | — | — |
| 3 | 活塞拦截-西风5ms | shahed | 500m | 200 | **7km** | ground-light | **(7000,0,50)** | 80%覆盖 |
| 4 | 喷气拦截-活性材料 | cruise-missile | 500m | 200 | **10km** | ground-standard | **(10000,0,50)** | 全程覆盖 |
| 5 | 空基拦截-东风5ms | shahed | 500m | 200 | 20km | air-standard | **(6000,1000,0)** | 80%覆盖 |
| 6 | 3架空基编队 ⏸️ | shahed×3 | 500m | 200 | 20km | air-standard×3 | (12000,1000,0) | — |
---
## 四、拦截点计算
| 想定 | 算法 | 探测弧长 | R+exp+fall | 拦截弧长 | 炮弹/平台时间 |
|------|------|------:|------:|------:|------:|
| 活塞 | `InterceptCalculator.Compute`(可变角度抛物线) | ~1021m | 5+27s | 由算法求解 | 由算法求解 |
| 喷气 | `InterceptCalculator.Compute`(可变角度抛物线) | 0 | 5+30s | 由算法求解 | 由算法求解 |
| 空基 | `InterceptCalculator.ComputeHorizontal`(θ=0°平抛 | 0 | 5+27+10s | 由算法求解 | 算法 + 平台飞行 |
---
## 五、历史变更
| 日期 | 变更 |
|------|------|
| 2026-06-16 | 初始创建。Phase 10 完成。地基放终点+80%探测;空基平抛算法;拦截点由 InterceptCalculator 求解。 |

View File

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

View File

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

@ -0,0 +1,34 @@
# 探测驱动的规划8.1.1
- **日期**2026-06-15
- **提出人**tian
- **关联需求**V1.0 功能需求(探测设备)、技术要求终版
- **优先级**:高
## 变更描述
planner 不再有上帝视角。仿真开始时planner 假设威胁从"探测边界"(统一信息网络的最早发现点)进入,而非航路真实起点。探测精度差 → 抛撒散布范围扩大。
### 设计原则
planner 是参谋,只能基于侦查信息制定方案。假设我方有统一信息网络,可同步威胁信息。独立探测设备与火力单元自带探测并存。
### 变更内容
1. **新增 DetectionCalculator**:探测能力评估的唯一实现。光电受 Visibility 衰减,雷达/红外不受影响;统一信息网络找最早探测点
2. **ScenarioUnit 扩展**:删单一 DetectionRadius加 RadarRange/EORange/IRange/DetectionAccuracy
3. **FireUnit 探测字段激活**BuildFireUnits 赋值(原为死代码)
4. **planner 接口扩展**Plan 加第 4 参数 detectionSources到达时间基于探测边界
5. **SimulationEngine 构建探测源**:独立探测 + 火力单元自带探测统一传入 planner
## 影响范围
- [x] 接口变更IDefensePlanner.Plan 加参数ScenarioUnit 字段变更(删 DetectionRadius
- [ ] 数据库变更:新字段为 nullableSQLite 自动处理
- [ ] UI 变更
- [x] 文档变更:实施计划 8.1.1、CHANGELOG、VERSION
## 验收
- 全量测试 193 → **208**+1564s 通过
- DetectionCalculator 13 项单测(天气衰减/最早探测点/散布半径)
- 探测驱动规划 2 项测试planner 基于探测边界、集成场景不破坏)
- 范围外:实时探测→火控链路(未来增强)

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,27 @@ Copy-Item "$projectRoot\unity_plugins\*.dll" "$unityProject\Assets\Plugins\Count
Remove-Item "$unityProject\Library\ScriptAssemblies" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Compiling Unity scripts..."
# 3. 编译CompileErrorDetector 捕获错误并 Exit(-1)
& $unityEditor -batchmode -projectPath $unityProject -logFile - 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { Write-Host "UNITY BUILD FAILED (exit code $LASTEXITCODE)"; exit 1 }
# 3. 编译(-quit 确保编译完自动退出;超时 300s 兜底)
Write-Host "Compiling Unity scripts (timeout 300s)..."
$logFile = "$projectRoot\unity_build.log"
$proc = Start-Process -FilePath $unityEditor `
-ArgumentList "-batchmode","-quit","-projectPath","`"$unityProject`"","-logFile","`"$logFile`"" `
-PassThru -NoNewWindow
$proc | Wait-Process -Timeout 300 -ErrorAction SilentlyContinue
if (-not $proc.HasExited) {
Write-Host "Unity build TIMEOUT (300s), killing process..."
$proc | Stop-Process -Force
exit 1
}
if ($proc.ExitCode -ne 0) {
Write-Host "UNITY BUILD FAILED (exit code $($proc.ExitCode))"
if (Test-Path $logFile) {
Write-Host "--- Last 30 lines of log ---"
Get-Content $logFile -Tail 30
}
exit 1
}
Write-Host "ALL CHECKS PASSED"
exit 0

View File

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

View File

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

View File

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

View File

@ -1,123 +1,98 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>威胁画像</summary>
public class ThreatProfile
{
public CombatScene Environment { get; set; } = new();
public List<TargetConfig> Targets { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
public List<ControlZone> ControlZones { get; set; } = new();
}
/// <summary>防御推荐方案</summary>
public class DefenseRecommendation
{
public DefenseSolution Best { get; set; } = new();
public DefenseSolution Critical { get; set; } = new();
}
/// <summary>单套防御方案</summary>
public class DefenseSolution
{
public AerosolType RecommendedAerosolType { get; set; }
public string AerosolRationale { get; set; } = string.Empty;
public CloudDispersal RecommendedCloud { get; set; } = new();
public List<RecommendedPlatform> Platforms { get; set; } = new();
public List<RecommendedDetection> Detections { get; set; } = new();
public float InterceptProbability { get; set; }
public string SummaryRationale { get; set; } = string.Empty;
public List<FireEvent> FireSchedule { get; set; } = new();
/// <summary>此方案占用的平台起始索引(多编队合并用)</summary>
public int PlatformOffset { get; set; }
}
/// <summary>多编队推荐结果</summary>
public class MultiGroupRecommendation
{
/// <summary>每组的独立方案</summary>
public List<DefenseSolution> GroupSolutions { get; set; } = new();
/// <summary>合并后的总发射计划</summary>
public List<FireEvent> MergedFireSchedule { get; set; } = new();
}
/// <summary>火力单元(装备编组)</summary>
/// <summary>火力单元(完整的武器系统)</summary>
public class FireUnit
{
public string GroupId { get; set; } = string.Empty;
public AerosolType LoadedAmmo { get; set; }
public int PlatformCount { get; set; }
public float PositionX, PositionY, PositionZ;
public float MuzzleVelocity { get; set; } = 800f;
public string Id { get; set; } = string.Empty;
public PlatformType Type { get; set; }
public Vector3 Position { get; set; }
// ── 发射装置 ──
/// <summary>火炮/发射器数量</summary>
public int GunCount { get; set; } = 1;
/// <summary>每门炮的火力通道数(同时可装填的弹药数量)</summary>
public int ChannelsPerGun { get; set; } = 1;
/// <summary>总火力通道数 = GunCount × ChannelsPerGun</summary>
public int TotalChannels => GunCount * ChannelsPerGun;
/// <summary>同一通道连续发射的最小间隔(秒)</summary>
public float ChannelInterval { get; set; } = 1f;
// ── 弹药 ──
/// <summary>总载弹量</summary>
public int TotalMunitions { get; set; }
/// <summary>可装填的弹药类型</summary>
public List<AerosolType> AmmoTypes { get; set; } = new();
/// <summary>单通道射击后冷却时间(秒)</summary>
public float Cooldown { get; set; } = 5f;
public float AmmoChangeTime { get; set; } = 300f;
// 弹药库:可以装填的弹药类型
public List<AerosolType> AvailableAmmo { get; set; } = new();
/// <summary>更换弹种时间(秒)</summary>
public float AmmoChangeTime { get; set; } = 30f;
// ── 搜索跟踪 ──
/// <summary>雷达探测距离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>
public class DroneGroup
/// <summary>无人机批次(规划器输入)</summary>
public class DroneWave
{
public string GroupId { get; set; } = string.Empty;
public TargetConfig Target { get; set; } = new();
public string WaveId { get; set; } = string.Empty;
public DroneSpec Profile { get; set; } = new();
public int Quantity { get; set; } = 1;
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
/// <summary>预计到达航路中点的时间(秒)</summary>
/// <summary>到达防御区域中点的时间(秒),由规划器预计算</summary>
public float ArrivalTime { get; set; }
/// <summary>威胁指数(速度系数 × 类型系数)</summary>
public float ThreatIndex { get; set; }
/// <summary>综合优先级 = 威胁指数 / (到达时间 + 1)</summary>
public float Priority => ArrivalTime > -1 ? ThreatIndex / (ArrivalTime + 1f) : ThreatIndex;
/// <summary>最早探测弧长(米)。威胁航路进入探测范围的弧长位置。
/// 0 = 起点即可探测或无探测设备上帝视角。float.MaxValue = 探测不到。</summary>
public float DetectArc { get; set; }
/// <summary>探测精度(位置误差 m。影响 planner 抛撒散布范围。</summary>
public float DetectAccuracy { get; set; }
/// <summary>预计到达航路中点的时间(秒)。基于探测边界,而非航路起点。</summary>
public float GetArrivalTime()
{
if (Waypoints.Count < 2) return 0;
var start = new Vector3((float)Waypoints[0].PosX, (float)Waypoints[0].PosY, (float)Waypoints[0].PosZ);
var end = new Vector3((float)Waypoints[^1].PosX, (float)Waypoints[^1].PosY, (float)Waypoints[^1].PosZ);
var speed = (float)Target.TypicalSpeed / 3.6f;
return start.DistanceTo(end) / speed / 2f;
var s = Waypoints[0];
var e = Waypoints[^1];
float midX = ((float)s.PosX + (float)e.PosX) / 2f;
float midZ = ((float)s.PosZ + (float)e.PosZ) / 2f;
float midArc = RouteGeometry.ArcLengthNearestTo(
Waypoints, midX, midZ);
float travelArc = midArc - DetectArc;
if (travelArc <= 0) return 0;
float speed = (float)Waypoints[0].Speed;
if (speed <= 0) throw new InvalidOperationException($"威胁 {WaveId}: 航路点速度必须 > 0");
return travelArc / (speed / 3.6f);
}
}
public class RecommendedPlatform
{
public PlatformType Type { get; set; }
public Vector3 Position { get; set; }
public int Quantity { get; set; }
public int MunitionCount { get; set; }
public float CoverageVolume { get; set; }
public float Cooldown { get; set; } = 5f;
public float MuzzleVelocity { get; set; } = 800f;
}
public class RecommendedDetection
{
public Vector3 Position { get; set; }
public float DetectionRadius { get; set; }
public int Quantity { get; set; }
}
/// <summary>三维向量(纯 C#,不依赖 UnityEngine</summary>
public struct Vector3
{
public float X, Y, Z;
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
public static Vector3 operator +(Vector3 a, Vector3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
public static Vector3 operator -(Vector3 a, Vector3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
public static Vector3 operator *(Vector3 a, float s) => new(a.X * s, a.Y * s, a.Z * s);
public float Length => (float)System.Math.Sqrt(X * X + Y * Y + Z * Z);
public float DistanceTo(Vector3 other) => (this - other).Length;
}
/// <summary>粒子渲染参数</summary>
public class ParticleParams
{
public float EmitRate { get; set; } = 100;
public float Opacity { get; set; } = 1.0f;
public string ColorHex { get; set; } = "#FFFFFF";
public float SizeMultiplier { get; set; } = 1.0f;
}
@ -128,5 +103,51 @@ namespace CounterDrone.Core.Algorithms
public int PlatformIndex;
public float TargetX, TargetY, TargetZ;
public float MuzzleVelocity;
public float LaunchAngle;
public float FlightDuration;
}
// ═══════════════════════════════════════════════
// DefensePlanner 类型
// ═══════════════════════════════════════════════
/// <summary>拦截候选:一个火力单元对一个威胁的可行性评估</summary>
public class InterceptCandidate
{
public FireUnit Unit { get; set; } = null!;
public AerosolType AmmoType { get; set; }
public float EarliestInterceptTime { get; set; }
public float KillProbability { get; set; }
}
/// <summary>单元分配:一个火力单元被分配给一个威胁的配置</summary>
public class UnitAssignment
{
public string FireUnitId { get; set; } = string.Empty;
public string DroneWaveId { get; set; } = string.Empty;
public AerosolType AmmoType { get; set; }
public int RoundsFired { get; set; }
public float FirstFireTime { get; set; }
public List<FireEvent> FireEvents { get; set; } = new();
}
/// <summary>规划方案</summary>
public class DefensePlan
{
public List<UnitAssignment> Assignments { get; set; } = new();
public List<FireEvent> MergedSchedule { get; set; } = new();
public float OverallProbability { get; set; }
public int ThreatsEngaged { get; set; }
public int ThreatsUnengaged { get; set; }
public string Summary { get; set; } = string.Empty;
internal string? RejectReason;
}
/// <summary>规划器输出</summary>
public class PlannerResult
{
public DefensePlan Best { get; set; } = new();
public DefensePlan Critical { get; set; } = new();
}
}

View File

@ -0,0 +1,85 @@
using System;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>云团膨胀模型——根据弹药和环境参数计算各阶段半径、密度、膨胀时间</summary>
public class CloudExpansionModel
{
private readonly AmmunitionSpec _ammo;
private readonly CombatScene _env;
private readonly float _initialRadius;
public CloudExpansionModel(AmmunitionSpec ammo, CombatScene env)
{
_ammo = ammo;
_env = env;
var w = (float)ammo.BurstChargeKg;
if (w <= 0)
throw new ArgumentException($"BurstChargeKg 必须 > 0实际: {w}", nameof(ammo));
_initialRadius = 3.3f * (float)Math.Pow(w, 0.32);
}
/// <summary>Phase 1 爆轰初始半径 (m)</summary>
public float InitialRadius => _initialRadius;
/// <summary>Phase 2 结束时的有效半径</summary>
public float TurbulentRadius => RadiusAt(Phase2Duration);
/// <summary>湍流膨胀系数 k</summary>
public float TurbulentExpansionK => (float)_ammo.TurbulentExpansionK;
/// <summary>Phase 2 持续时间s从 AmmunitionSpec 读取</summary>
public float Phase2Duration => (float)_ammo.Phase2Duration;
/// <summary>指定时刻湍流膨胀半径 R(t) = R₀ + k√t</summary>
public float RadiusAt(float elapsedSeconds)
{
if (elapsedSeconds <= 0) return _initialRadius;
float p2 = Phase2Duration;
bool inPhase3 = elapsedSeconds > p2;
if (!inPhase3)
return _initialRadius + TurbulentExpansionK * (float)Math.Sqrt(elapsedSeconds);
// Phase 3: 高斯扩散
float windSpeed = (float)_env.WindSpeed;
float x = windSpeed * (elapsedSeconds - p2);
if (x <= 0) return _initialRadius + TurbulentExpansionK * (float)Math.Sqrt(p2);
var cls = Kinematics.GetStabilityClass((WeatherType)_env.WeatherType, windSpeed);
float sY = Kinematics.SigmaY(cls, x);
float sZ = Kinematics.SigmaZ(cls, x);
float peakC = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ);
float effConc = (float)_ammo.EffectiveConcentration;
float rPhase2 = _initialRadius + TurbulentExpansionK * (float)Math.Sqrt(p2);
if (peakC <= effConc) return rPhase2;
float sigma = (sY + sZ) / 2f;
return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / effConc));
}
/// <summary>指定时刻中心浓度</summary>
public float DensityAt(float elapsedSeconds)
{
float r = RadiusAt(elapsedSeconds);
float volume = (4f / 3f) * (float)Math.PI * r * r * r;
if (volume <= 0.001f) return (float)_ammo.CoreDensity;
return (float)_ammo.SourceStrength / volume;
}
/// <summary>覆盖指定距离需要的云团数量</summary>
public int RoundsNeeded(float requiredCoverage, float? spacing = null)
{
float s = spacing ?? 2f * TurbulentRadius;
if (s <= 0) return 1;
int n = (int)Math.Ceiling(requiredCoverage / s);
return n < 1 ? 1 : n;
}
/// <summary>达到指定半径所需时间 (s)</summary>
public float TimeToReach(float radius)
{
if (radius <= _initialRadius) return 0;
float k = TurbulentExpansionK;
return (radius - _initialRadius) * (radius - _initialRadius) / (k * k);
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>毁伤评估——路径积分计算无人机穿过云团的路径长度(米),用于换算暴露时间</summary>
public static class DamageAssessment
{
/// <summary>线段 (p1→p2) 在球体内的长度,换算为路径长度 (m)</summary>
/// <returns>球体内的路径长度 (m)0 表示未进入</returns>
public static float PathInSphere(
float p1x, float p1y, float p1z,
float p2x, float p2y, float p2z,
float cx, float cy, float cz, float radius)
{
float dx = p2x - p1x, dy = p2y - p1y, dz = p2z - p1z;
float segLen = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
if (segLen < 0.0001f) return 0f;
float fx = p1x - cx, fy = p1y - cy, fz = p1z - cz;
// |p1 + t*d - c|² = r² → a*t² + 2b*t + c = 0
float a = dx * dx + dy * dy + dz * dz;
float b = dx * fx + dy * fy + dz * fz;
float c = fx * fx + fy * fy + fz * fz - radius * radius;
float disc = b * b - a * c;
if (disc <= 0) return 0f;
float sqrtDisc = (float)Math.Sqrt(disc);
float t1 = (-b - sqrtDisc) / a;
float t2 = (-b + sqrtDisc) / a;
t1 = Math.Max(0f, t1);
t2 = Math.Min(1f, t2);
if (t1 >= t2) return 0f;
return (t2 - t1) * segLen;
}
}
}

View File

@ -9,7 +9,7 @@ namespace CounterDrone.Core.Algorithms
private readonly ActiveMaterialDamageModel _activeMaterial = new();
private readonly ActiveFuelDamageModel _activeFuel = new();
public float CalculateDamage(TargetType droneType, PowerType powerType,
public float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
return aerosolType switch
@ -21,6 +21,17 @@ namespace CounterDrone.Core.Algorithms
};
}
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
{
return aerosolType switch
{
AerosolType.InertGas => _inertGas.RequiredExposureSeconds(droneType, powerType, aerosolType),
AerosolType.ActiveMaterial => _activeMaterial.RequiredExposureSeconds(droneType, powerType, aerosolType),
AerosolType.ActiveFuel => _activeFuel.RequiredExposureSeconds(droneType, powerType, aerosolType),
_ => float.MaxValue,
};
}
public DamageStage GetDamageStage(float accumulatedDamage)
{
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -1,59 +0,0 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>默认弹药规格 — 基于发烟罐/烟幕弹工程数据</summary>
/// <remarks>
/// 参考10kg烟幕剂抛射药/烟幕剂质量比 5%-20%TNT当量 1.5-2kg典型值
/// 初始半径 R₀ = 3.3 × W^0.32 ≈ 3.8~4.1m
/// </remarks>
public static class DefaultAmmunition
{
public static List<AmmunitionSpec> GetAll()
{
return new List<AmmunitionSpec>
{
new AmmunitionSpec
{
Id = "default-inert", AerosolType = (int)AerosolType.InertGas,
Name = "惰性气体弹(发烟罐型)",
InitialRadius = 3.8,
CoreDensity = 1.5, EdgeDensity = 0.1,
InitialTemperature = 1800, BuoyancyFactor = 0.3,
EffectiveConcentration = 0.0001,
MaxRadius = 100.0, MaxDuration = 120.0,
SourceStrength = 10.0,
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
},
new AmmunitionSpec
{
Id = "default-active", AerosolType = (int)AerosolType.ActiveMaterial,
Name = "活性材料弹(爆炸分散型)",
InitialRadius = 5.0,
CoreDensity = 2.0, EdgeDensity = 0.2,
InitialTemperature = 2400, BuoyancyFactor = 0.6,
EffectiveConcentration = 0.0002,
MaxRadius = 80.0, MaxDuration = 90.0,
SourceStrength = 12.0,
BurstChargeKg = 4.0, TurbulentExpansionK = 4.0,
},
new AmmunitionSpec
{
Id = "default-fuel", AerosolType = (int)AerosolType.ActiveFuel,
Name = "活性燃料弹(抛射分散型)",
InitialRadius = 3.8,
CoreDensity = 1.8, EdgeDensity = 0.15,
InitialTemperature = 1900, BuoyancyFactor = 0.4,
EffectiveConcentration = 0.0001,
MaxRadius = 90.0, MaxDuration = 100.0,
SourceStrength = 10.0,
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
},
};
}
public static AmmunitionSpec GetByType(AerosolType type)
=> GetAll().Find(a => a.AerosolType == (int)type) ?? GetAll()[0];
}
}

View File

@ -1,280 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
public class DefaultDefenseAdvisor : IDefenseAdvisor
{
private readonly List<AmmunitionSpec> _ammoCatalog;
public DefaultDefenseAdvisor(List<AmmunitionSpec> ammoCatalog)
{
_ammoCatalog = ammoCatalog ?? new List<AmmunitionSpec>();
}
private static readonly Dictionary<PowerType, AerosolType> MatchTable = new()
{
{ PowerType.Electric, AerosolType.InertGas },
{ PowerType.Piston, AerosolType.InertGas },
{ PowerType.Jet, AerosolType.ActiveMaterial },
};
public DefenseRecommendation Recommend(ThreatProfile threat)
{
var result = new DefenseRecommendation
{
Best = new DefenseSolution(),
Critical = new DefenseSolution(),
};
// ===== 输入校验 =====
if (threat.Targets.Count == 0)
{
result.Best.SummaryRationale = "失败:未配置威胁目标";
return result;
}
if (threat.Waypoints.Count < 2)
{
result.Best.SummaryRationale = "失败:需要至少两个航路点";
return result;
}
var target = threat.Targets[0];
if (target.TypicalSpeed <= 0)
{
result.Best.SummaryRationale = "失败:目标速度为 0";
return result;
}
// ===== Step A气溶胶选型 =====
var powerType = (PowerType)target.PowerType;
var aerosolType = MatchTable.TryGetValue(powerType, out var match)
? match : AerosolType.InertGas;
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)aerosolType);
if (ammo == null)
{
result.Best.SummaryRationale = $"失败:弹药库中未找到 {aerosolType} 类型的弹药规格";
return result;
}
var rationale = powerType switch
{
PowerType.Electric or PowerType.Piston =>
$"{powerType}发动机依赖氧气,推荐惰性气体窒息方案",
PowerType.Jet =>
$"{powerType}发动机高温表面可触发活性材料爆燃反应",
_ => "默认推荐惰性气体方案",
};
// ===== Step B时空交汇优化 =====
var start = ToV3(threat.Waypoints[0]);
var end = ToV3(threat.Waypoints[^1]);
var mid = new Vector3((start.X + end.X) / 2f, (start.Y + end.Y) / 2f, (start.Z + end.Z) / 2f);
var routeLength = start.DistanceTo(end);
var avgSpeed = (float)target.TypicalSpeed / 3.6f;
var totalFlightTime = routeLength / avgSpeed;
if (routeLength < 100)
{
result.Best.SummaryRationale = "失败:航路太短(<100m";
return result;
}
// 弹药参数Phase 1 爆轰 + Phase 2 膨胀后的有效半径
var r0 = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)ammo.BurstChargeKg), 0.32);
var k = (float)ammo.TurbulentExpansionK;
var maxDur = (float)ammo.MaxDuration;
var halfTime = totalFlightTime / 2f;
var windSpeed = (float)threat.Environment.WindSpeed;
var weather = (WeatherType)threat.Environment.WeatherType;
var effectiveR = ComputeEffectiveRadius(r0, k, halfTime, windSpeed, weather, ammo);
var crossTime = (2f * effectiveR) / avgSpeed;
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
var spacing = effectiveR * 1.5f;
var requiredCoverage = neededExposure * avgSpeed;
var roundsNeeded = Math.Max(1,
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
var actualCoverage = spacing * (roundsNeeded - 1) + 2f * effectiveR;
var actualExposure = actualCoverage / avgSpeed;
var prob = Math.Min(0.95f, actualExposure / neededExposure);
var expansionTime = (float)Math.Pow((effectiveR - r0) / k, 2);
var recommendedTiming = halfTime - expansionTime;
// 生成发射计划
var fireSchedule = new List<FireEvent>();
for (int i = 0; i < roundsNeeded; i++)
{
var offset = (i - (roundsNeeded - 1) / 2f) * spacing;
fireSchedule.Add(new FireEvent
{
FireTime = recommendedTiming,
PlatformIndex = i % roundsNeeded,
TargetX = mid.X + offset,
TargetY = mid.Y,
TargetZ = mid.Z,
MuzzleVelocity = 800f,
});
}
var bestCloud = new CloudDispersal
{
AerosolType = (int)aerosolType,
PositionX = mid.X, PositionY = mid.Y, PositionZ = mid.Z,
DisperseHeight = (float)target.TypicalAltitude,
TriggerMode = (int)TriggerMode.Time,
Duration = (int)maxDur,
InitialScale = ammo.InitialVolume,
ReleaseMode = (int)ReleaseMode.Single,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
RecommendedTiming = recommendedTiming,
SalvoRounds = roundsNeeded,
SalvoSpacing = spacing,
EstimatedProbability = prob,
};
// 平台部署:假设齐射(同时发射),每门炮打一发后冷却 5s
// 需要 roundsNeeded 门炮同时发射
var gunCount = roundsNeeded;
var platforms = new List<RecommendedPlatform>();
for (int i = 0; i < gunCount; i++)
platforms.Add(new RecommendedPlatform
{
Type = PlatformType.GroundBased,
Position = new Vector3(mid.X + i * 50, 0, 50),
Quantity = 1,
MunitionCount = 1,
CoverageVolume = (float)ammo.InitialVolume,
Cooldown = 5f,
MuzzleVelocity = 800f,
});
result.Best = new DefenseSolution
{
RecommendedAerosolType = aerosolType,
AerosolRationale = rationale,
RecommendedCloud = bestCloud,
FireSchedule = fireSchedule,
Platforms = platforms,
Detections = new List<RecommendedDetection>
{
new RecommendedDetection
{
Position = new Vector3(mid.X, mid.Y, 0),
DetectionRadius = Math.Max(3000f, routeLength * 0.4f),
Quantity = 1,
}
},
InterceptProbability = prob,
SummaryRationale = $"{aerosolType}方案,{roundsNeeded}发,预计拦截概率 {prob:P0}",
};
result.Critical = new DefenseSolution
{
RecommendedAerosolType = aerosolType,
RecommendedCloud = new CloudDispersal
{
AerosolType = (int)aerosolType,
PositionX = start.X + (end.X - start.X) * 0.25f,
PositionY = start.Y,
PositionZ = start.Z,
DisperseHeight = (float)target.TypicalAltitude,
Duration = (int)maxDur,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
RecommendedTiming = totalFlightTime * 0.25f - expansionTime,
},
Platforms = platforms,
InterceptProbability = prob * 0.3f,
SummaryRationale = $"临界方案:拦截概率仅 {prob * 0.3f:P0}",
};
return result;
}
private static Vector3 ToV3(Waypoint wp) => new((float)wp.PosX, (float)wp.PosY, (float)wp.PosZ);
/// <summary>计算云团在目标时刻的有效半径</summary>
private static float ComputeEffectiveRadius(float r0, float k, float halfTime,
float windSpeed, WeatherType weather, AmmunitionSpec ammo)
{
// Phase 2 结束时的半径
var rPhase2 = r0 + k * (float)Math.Sqrt(Math.Min(halfTime, 30f));
if (halfTime <= 30f) return rPhase2;
// Phase 3用高斯扩散公式计算浓度场下的有效半径
var x = Math.Max(1f, windSpeed * (halfTime - 30f));
var cls = Kinematics.GetStabilityClass(weather, windSpeed);
var sY = Kinematics.SigmaY(cls, x);
var sZ = Kinematics.SigmaZ(cls, x);
var peakC = Kinematics.GaussianPeakConcentration((float)ammo.SourceStrength, sY, sZ);
var threshold = (float)ammo.EffectiveConcentration;
if (peakC <= threshold) return rPhase2;
var sigma = (sY + sZ) / 2f;
return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
}
/// <summary>多编队推荐 — 每组独立方案,按时间分配火力单元,合并发射计划</summary>
public MultiGroupRecommendation RecommendMultiGroup(
List<DroneGroup> droneGroups, List<FireUnit> fireUnits, CombatScene environment)
{
var result = new MultiGroupRecommendation();
if (droneGroups.Count == 0 || fireUnits.Count == 0) return result;
var sorted = droneGroups.OrderBy(g => g.GetArrivalTime()).ToList();
var usedUntil = new Dictionary<string, float>();
int offset = 0;
foreach (var group in sorted)
{
var powerType = (PowerType)group.Target.PowerType;
var neededAmmo = MatchTable.GetValueOrDefault(powerType, AerosolType.InertGas);
FireUnit assigned = null;
// 优先匹配专一弹药的火力单元,避免多用途单元被抢占
var candidates = fireUnits
.Where(u => u.AvailableAmmo.Contains(neededAmmo))
.OrderBy(u => u.AvailableAmmo.Count) // 少弹种的优先
.ThenBy(u => usedUntil.ContainsKey(u.GroupId) ? 1 : 0); // 不忙的优先
foreach (var unit in candidates)
{
if (!unit.AvailableAmmo.Contains(neededAmmo)) continue;
if (!usedUntil.TryGetValue(unit.GroupId, out var busyUntil))
{ assigned = unit; break; }
if (unit.LoadedAmmo == neededAmmo && busyUntil <= group.GetArrivalTime())
{ assigned = unit; break; }
if (busyUntil + unit.AmmoChangeTime <= group.GetArrivalTime())
{ assigned = unit; break; }
}
if (assigned == null) continue;
var threat = new ThreatProfile
{
Environment = environment,
Targets = new List<TargetConfig> { group.Target },
Route = group.Route,
Waypoints = group.Waypoints,
};
var sol = Recommend(threat).Best;
foreach (var fe in sol.FireSchedule) fe.PlatformIndex += offset;
result.GroupSolutions.Add(sol);
result.MergedFireSchedule.AddRange(sol.FireSchedule);
assigned.LoadedAmmo = sol.RecommendedAerosolType;
usedUntil[assigned.GroupId] = (sol.FireSchedule.Count > 0
? sol.FireSchedule.Max(f => f.FireTime) : group.GetArrivalTime()) + assigned.Cooldown;
offset += sol.Platforms.Count;
}
return result;
}
}
}

View File

@ -0,0 +1,30 @@
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>默认车道划分:按云团覆盖能力合并或拆分</summary>
public class DefaultLaneDivider : ILaneDivider
{
public (int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius)
{
if (wave.Route == null || wave.Quantity <= 1)
return (1, 0);
var mode = (FormationMode)wave.Route.FormationMode;
if (mode != FormationMode.Formation)
return (1, 0);
int latCount = wave.Route.LateralCount ?? wave.Quantity;
int longCount = wave.Route.LongitudinalCount ?? 1;
float latWidth = (latCount - 1) * (float)wave.Route.LateralSpacing;
float longDepth = (longCount - 1) * (float)wave.Route.LongitudinalSpacing;
float cloudDiameter = 2f * cloudRadius;
// 任一维度超过云团半径,则无法一个云团覆盖
if (latWidth > cloudRadius || longDepth > cloudRadius)
return (wave.Quantity, (float)wave.Route.LateralSpacing);
return (1, 0);
}
}
}

View File

@ -0,0 +1,579 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>防御规划器 — 五步流水线,全部调用与引擎共享的物理工具类</summary>
public class DefensePlanner : IDefensePlanner
{
private readonly List<AmmunitionSpec> _ammoCatalog;
private readonly IDamageModel _damageModel;
private readonly PlannerConfig _config;
private readonly ILaneDivider _laneDivider;
public DefensePlanner(List<AmmunitionSpec> ammoCatalog, PlannerConfig config,
IDamageModel? damageModel = null, ILaneDivider? laneDivider = null)
{
_ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog));
_config = config ?? throw new ArgumentNullException(nameof(config));
_damageModel = damageModel ?? new DamageModelRouter();
_laneDivider = laneDivider ?? new DefaultLaneDivider();
if (_ammoCatalog.Count == 0)
throw new ArgumentException("弹药规格目录不能为空");
}
// ═══════════════════════════════════════════════
// 五步流水线
// ═══════════════════════════════════════════════
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
CombatScene environment, List<DetectionSource> detectionSources)
{
var result = new PlannerResult();
if (threats.Count == 0)
{
result.Best.Summary = "无威胁目标";
return result;
}
if (fireUnits.Count == 0)
{
result.Best.Summary = "无可用火力单元";
result.Best.ThreatsUnengaged = threats.Count;
return result;
}
// 探测信息:每个威胁的最早发现弧长 + 精度(基于统一信息网络)
// 无探测设备时 detectArc=0上帝视角从航路起点算、精度用配置默认值
float visibility = (float)environment.Visibility;
foreach (var t in threats)
{
var (detectArc, accuracy) = DetectionCalculator.EarliestDetection(
t.Waypoints, detectionSources, visibility);
t.DetectArc = detectArc;
t.DetectAccuracy = accuracy == float.MaxValue
? _config.DefaultDetectionAccuracy
: accuracy;
}
// Step 1: 威胁排序
foreach (var t in threats)
{
t.ArrivalTime = t.GetArrivalTime();
t.ThreatIndex = CalcThreatIndex(_config, t.Profile);
}
var sorted = threats.OrderByDescending(t => t.Priority).ToList();
// Step 2-4: 贪心分配求解
// 预先检查:所有需要的弹药类型都在目录中
var neededTypes = sorted
.Select(t => MatchAmmo(_config, (PowerType)t.Profile.PowerType))
.Distinct()
.ToList();
foreach (var t in neededTypes)
{
if (!_ammoCatalog.Any(a => a.AerosolType == (int)t))
throw new InvalidOperationException($"弹药规格目录中缺少类型: {t}");
}
result.Best = Solve(sorted, fireUnits, environment);
// Step 5: 临界方案
result.Critical = DeriveCritical(result.Best);
return result;
}
// ═══════════════════════════════════════════════
// Step 1: 威胁指数
// ═══════════════════════════════════════════════
private static float CalcThreatIndex(PlannerConfig config, DroneSpec spec)
{
float typeCoef = config.TypeCoefficient.GetValueOrDefault((DroneType)spec.DroneType, 1f);
float speedCoef = (float)spec.TypicalSpeed / 60f;
return typeCoef * speedCoef;
}
// ═══════════════════════════════════════════════
// Step 2: 弹药匹配
// ═══════════════════════════════════════════════
private static AerosolType MatchAmmo(PlannerConfig config, PowerType power)
{
return config.AmmoMatch.GetValueOrDefault(power, AerosolType.InertGas);
}
// ═══════════════════════════════════════════════
// Step 3-4: 候选生成 → 贪心分配
// ═══════════════════════════════════════════════
private DefensePlan Solve(List<DroneWave> sortedThreats,
List<FireUnit> fireUnits, CombatScene env)
{
var plan = new DefensePlan();
var remainingMunitions = new Dictionary<string, int>();
foreach (var u in fireUnits)
remainingMunitions[u.Id] = u.TotalMunitions;
foreach (var threat in sortedThreats)
{
var available = fireUnits
.Where(u => remainingMunitions.GetValueOrDefault(u.Id, 0) > 0)
.ToList();
var candidates = GenerateCandidates(threat, available, env);
if (candidates.Count == 0)
{
plan.ThreatsUnengaged++;
continue;
}
// 计算总弹药需求
var neededAmmo = MatchAmmo(_config, (PowerType)threat.Profile.PowerType);
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
// 单机需求
var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env);
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, effectiveRadius);
// 车道划分(可替换策略)
var (yLanes, laneSpacing) = _laneDivider.Divide(threat, effectiveRadius);
float formationWidth = yLanes > 1 ? (yLanes - 1) * laneSpacing : 0f;
int totalRoundsNeeded = singleNeeded * yLanes;
// 逐单元分配:每个单元锁定一个 Y 车道
// 云团间距 = 2R × (1 - 重叠比例R 为穿越时刻的有效半径,保证无缝覆盖
float spacing = 2f * effectiveRadius * (1f - _config.CloudOverlapRatio);
int[] laneNeeded = new int[yLanes];
float[] laneBaseTime = new float[yLanes];
bool[] laneBaseSet = new bool[yLanes];
for (int l = 0; l < yLanes; l++) laneNeeded[l] = singleNeeded;
int currentLane = 0;
int roundsCollected = 0;
var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
.ThenByDescending(c => c.KillProbability))
{
if (roundsCollected >= totalRoundsNeeded) break;
int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
if (remaining <= 0) continue;
while (currentLane < yLanes && laneNeeded[currentLane] <= 0) currentLane++;
if (currentLane >= yLanes) break;
int toTake = Math.Min(Math.Min(c.Unit.TotalChannels, remaining), laneNeeded[currentLane]);
if (toTake <= 0) continue;
int yLane = currentLane;
// 车道第一个单元设基准时间,后续单元以此为准保证间距均匀
if (!laneBaseSet[yLane])
{
var (refEvt, refRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
if (refEvt.Count == 0) { plan.RejectReason ??= refRej; continue; }
laneBaseTime[yLane] = refEvt[0].FireTime;
laneBaseSet[yLane] = true;
}
float stagger = Kinematics.CloudCoverInterval(effectiveRadius * 2f, GetDroneSpeedKph(threat), c.Unit.ChannelInterval);
var fevents = new List<FireEvent>();
int unitIdx = fireUnits.IndexOf(c.Unit);
int baseRoundInLane = singleNeeded - laneNeeded[currentLane];
for (int ch = 0; ch < toTake; ch++)
{
// offset 基于车道内发序(不是全局 eventIdx使每车道独立分布在同一航路段
// 多车道重叠覆盖而非沿航路连成长链
int roundInLane = baseRoundInLane + ch;
float offset = (roundInLane - (singleNeeded - 1) / 2f) * spacing;
var (fe, feRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
if (fe.Count == 0) { plan.RejectReason ??= feRej; continue; }
foreach (var e in fe)
{
e.FireTime = laneBaseTime[yLane] + roundInLane * stagger;
// TargetX/Z 保留 GenerateFireEventsAt 算出的风偏补偿后抛撒点 cloudGenX/Z
e.PlatformIndex = unitIdx * c.Unit.TotalChannels + ch;
}
fevents.AddRange(fe);
}
assignedUnits.Add((c.Unit, toTake, fevents));
remainingMunitions[c.Unit.Id] -= toTake;
laneNeeded[currentLane] -= toTake;
roundsCollected += toTake;
}
if (roundsCollected <= 0) {
plan.RejectReason ??= $"候选{candidates.Count}个,弹药需求{totalRoundsNeeded}发实际收集0发";
plan.ThreatsUnengaged++; continue;
}
foreach (var (unit, rounds, fireEvents) in assignedUnits)
{
plan.Assignments.Add(new UnitAssignment
{
FireUnitId = unit.Id,
DroneWaveId = threat.WaveId,
AmmoType = neededAmmo,
RoundsFired = rounds,
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
FireEvents = fireEvents,
});
plan.MergedSchedule.AddRange(fireEvents);
}
plan.ThreatsEngaged++;
}
plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
// 按威胁汇总概率
var threatProbs = new List<float>();
foreach (var threat in sortedThreats)
{
var a2 = _ammoCatalog.FirstOrDefault(s =>
s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Profile.PowerType));
int totalRounds = plan.Assignments
.Where(a => a.DroneWaveId == threat.WaveId)
.Sum(a => a.RoundsFired);
if (totalRounds > 0)
threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
}
plan.OverallProbability = threatProbs.Count > 0 ? threatProbs.Average() : 0f;
// 失败原因 + 建议值
var reasons = new List<string>();
foreach (var threat in sortedThreats)
{
bool engaged = plan.Assignments.Any(a => a.DroneWaveId == threat.WaveId);
if (engaged) continue;
var ammo = MatchAmmo(_config, (PowerType)threat.Profile.PowerType);
var matching = fireUnits.Where(u => u.AmmoTypes.Contains(ammo)).ToList();
if (matching.Count == 0)
reasons.Add($"威胁 {threat.WaveId}: 无火力单元装载 {ammo} 弹药");
else
{
var (_, expTime, _) = ComputeEffectiveRadius(_ammoCatalog.First(a => a.AerosolType == (int)ammo), env);
float avgSpd = GetDroneSpeedMs(threat);
float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints,
(float)(threat.Waypoints[0].PosX + threat.Waypoints[^1].PosX) / 2f,
(float)(threat.Waypoints[0].PosZ + threat.Waypoints[^1].PosZ) / 2f);
float travel = midArc - threat.DetectArc;
if (travel <= 0)
{
var (mx, _, mz) = RouteGeometry.PositionAt(threat.Waypoints, midArc);
float r = matching.Min(u => MathF.Sqrt((mx - u.Position.X) * (mx - u.Position.X) + (mz - u.Position.Z) * (mz - u.Position.Z)));
reasons.Add($"威胁 {threat.WaveId}: 探测边界在拦截点之后,建议探测范围≥{r:F0}m");
}
else if (travel / avgSpd <= expTime)
{
float maxDA = Math.Max(0, midArc - avgSpd * (expTime + _config.TimingSafetyMargin));
var (nx, _, nz) = RouteGeometry.PositionAt(threat.Waypoints, maxDA);
float r = matching.Min(u => MathF.Sqrt((nx - u.Position.X) * (nx - u.Position.X) + (nz - u.Position.Z) * (nz - u.Position.Z)));
reasons.Add($"威胁 {threat.WaveId}: 拦截窗口不足({travel / avgSpd:F1}s<膨胀{expTime:F1}s建议探测范围≥{r:F0}m最晚弧长{maxDA:F0}m");
}
else
{
string detail = plan.RejectReason != null ? $"{plan.RejectReason}" : "";
reasons.Add($"威胁 {threat.WaveId}: 候选存在但分配失败{detail}");
}
}
}
plan.Summary = plan.ThreatsEngaged > 0
? $"分配 {plan.ThreatsEngaged} 个威胁,{plan.ThreatsUnengaged} 个无方案"
+ (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "")
: "无威胁被分配拦截方案"
+ (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "");
return plan;
}
// ═══════════════════════════════════════════════
// 拦截窗口可行性检查
// ═══════════════════════════════════════════════
/// <summary>无人机速度km/h从航路第一个 waypoint 读取</summary>
private static float GetDroneSpeedKph(DroneWave threat)
{
float spd = (float)threat.Waypoints[0].Speed;
if (spd <= 0) throw new InvalidOperationException($"威胁 {threat.WaveId}: 航路点速度必须 > 0");
return spd;
}
/// <summary>无人机速度m/s</summary>
private static float GetDroneSpeedMs(DroneWave threat)
=> GetDroneSpeedKph(threat) / 3.6f;
internal static bool HasInterceptWindow(DroneWave threat, float midArc,
float expansionTime, float deliveryTime)
{
float travelArc = midArc - threat.DetectArc;
if (travelArc <= 0) return false;
float timeAvailable = travelArc / GetDroneSpeedMs(threat);
return timeAvailable > expansionTime + deliveryTime;
}
// ═══════════════════════════════════════════════
// 候选生成(使用真实物理)
// ═══════════════════════════════════════════════
private List<InterceptCandidate> GenerateCandidates(DroneWave threat,
List<FireUnit> availableUnits, CombatScene env)
{
var candidates = new List<InterceptCandidate>();
var neededAmmo = MatchAmmo(_config, (PowerType)threat.Profile.PowerType);
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
var (ammoEff, expansionTime, _) = ComputeEffectiveRadius(ammo, env);
var mid = ThreatMidpoint(threat);
float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints, mid.X, mid.Z);
foreach (var unit in availableUnits)
{
if (!unit.AmmoTypes.Contains(neededAmmo)) continue;
var c = BuildCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
if (c != null) candidates.Add(c);
}
return candidates;
}
private InterceptCandidate? BuildCandidate(DroneWave threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
float effectiveR, float expansionTime,
Vector3 mid, float midArc, CombatScene env)
{
float dx = mid.X - unit.Position.X;
float dz = mid.Z - unit.Position.Z;
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
if (unit.MuzzleVelocity <= 0) return null;
float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f;
if (dist > maxRange) return null;
float deliveryTime = dist / unit.MuzzleVelocity;
if (deliveryTime > threat.ArrivalTime) return null;
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
float avgSpeed = GetDroneSpeedMs(threat);
float neededExposure = _damageModel.RequiredExposureSeconds((DroneType)threat.Profile.DroneType, (PowerType)threat.Profile.PowerType, ammoType);
float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
return new InterceptCandidate
{
Unit = unit,
AmmoType = ammoType,
EarliestInterceptTime = threat.ArrivalTime,
KillProbability = prob,
};
}
// ═══════════════════════════════════════════════
// 弹药计算(使用 AmmunitionSpec + 环境参数)
// ═══════════════════════════════════════════════
private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius(
AmmunitionSpec ammo, CombatScene env)
{
var model = new CloudExpansionModel(ammo, env);
float rPhase2 = model.RadiusAt(model.Phase2Duration);
float expansionTime = model.TimeToReach(rPhase2) * _config.ExpansionFactor;
float effectiveR = model.RadiusAt(expansionTime);
return (effectiveR, expansionTime, rPhase2);
}
private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
CombatScene env, bool isAirBased, float turbulentRadius)
{
var cloudModel = new CloudExpansionModel(ammo, env);
float avgSpeed = GetDroneSpeedMs(threat);
float neededExposure = _damageModel.RequiredExposureSeconds(
(DroneType)threat.Profile.DroneType, (PowerType)threat.Profile.PowerType, (AerosolType)ammo.AerosolType);
float requiredCoverage = neededExposure * avgSpeed;
// 间距由配置的重叠比例驱动,公式在 CloudExpansionModel共享
float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
return cloudModel.RoundsNeeded(requiredCoverage, spacing);
}
private float ComputeInterceptProbability(DroneWave threat,
AmmunitionSpec ammo, int rounds, CombatScene env)
{
float avgSpeed = GetDroneSpeedMs(threat);
var (effectiveR, _, _) = ComputeEffectiveRadius(ammo, env);
float spacing = 2f * effectiveR * (1f - _config.CloudOverlapRatio);
float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR;
float actualExposure = actualCoverage / avgSpeed;
var aerosolType = (AerosolType)ammo.AerosolType;
float neededExposure = _damageModel.RequiredExposureSeconds((DroneType)threat.Profile.DroneType, (PowerType)threat.Profile.PowerType, aerosolType);
return Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
}
// ═══════════════════════════════════════════════
// 发射事件生成(真实物理)
// ═══════════════════════════════════════════════
private (List<FireEvent> Events, string? RejectReason) TryGenerateFireEvents(
DroneWave threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
CombatScene env, float targetOffset, int yLane, int yLanes, float formationWidth)
{
var events = new List<FireEvent>();
var wps = threat.Waypoints;
if (wps == null || wps.Count < 2) return (events, "航路无效");
var cloudModel = new CloudExpansionModel(ammo, env);
var (effectiveR, expansionTime, _) = ComputeEffectiveRadius(ammo, env);
float densityAtPassage = cloudModel.DensityAt(expansionTime);
if (densityAtPassage < (float)ammo.EffectiveConcentration)
return (events, $"云团密度{densityAtPassage:E2}<有效阈值{ammo.EffectiveConcentration:E2}");
float typicalSpeed = GetDroneSpeedKph(threat);
if (typicalSpeed <= 0) return (events, "目标速度无效");
// ═══ 拦截弧长 ═══
var mid = ThreatMidpoint(threat);
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
var (ia, _, _) = InterceptCalculator.Compute(
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
float crossArc = (ia > 0 ? ia : midArc) + targetOffset;
// 无人机到达穿越点的时间:基于探测边界,而非航路起点
float travelArc = crossArc - threat.DetectArc;
if (travelArc < 0) return (events, $"探测边界在拦截点之后({threat.DetectArc:F0}m≥{crossArc:F0}m");
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
float recommendedTiming = txArrival - expansionTime;
if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");
// ═══ 编队横向偏移 ═══
int axis = threat.Route?.LateralAxis ?? 2;
float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
float laneOffset = yLane * laneSpacing;
var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc);
float tx = routeX, ty = routeY, tz = routeZ;
if (axis == 0) tx += laneOffset;
else if (axis == 1) ty += laneOffset;
else tz += laneOffset;
// 风偏补偿:先算云团生成点,再按实际位置算发射角
var (wx, _, wz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
float cloudGenX = tx - wx * expansionTime;
float cloudGenZ = tz - wz * expansionTime;
float dx = cloudGenX - unit.Position.X;
float dz = cloudGenZ - unit.Position.Z;
float targetDist = (float)Math.Sqrt(dx * dx + dz * dz);
float mv = unit.MuzzleVelocity;
if (mv <= 0)
throw new InvalidOperationException($"单元 {unit.Id}: MuzzleVelocity={mv} 必须>0");
float heightDiff = ty - unit.Position.Y;
// ═══ 抛物线求解:两解逐一验证,选最早摧毁无人机的可行解 ═══
var angles = ParabolicMotion.SolveAngles(targetDist, heightDiff, mv);
if (!angles.HasValue)
return (events, $"目标超出弹道射程: dist={targetDist:F0}m");
float? bestLaunchAngle = null, bestTof = null, bestFireTime = null;
foreach (var a in new[] { angles.Value.Item1, angles.Value.Item2 })
{
var motion = new ParabolicMotion(mv, a);
var ts = motion.GetFlightTimes(heightDiff);
foreach (var t in ts)
{
float r = mv * (float)Math.Cos(a) * t;
if (Math.Abs(r - targetDist) > 0.5f) continue;
float ft = recommendedTiming - t;
if (ft <= 0f) continue;
// 选最早摧毁fireTime 最小 → 发射最早 → 拦截最早)
if (!bestFireTime.HasValue || ft < bestFireTime.Value)
{
bestLaunchAngle = a; bestTof = t; bestFireTime = ft;
}
}
}
if (!bestLaunchAngle.HasValue)
return (events, $"所有弹道解均不可行: dist={targetDist:F0}m");
float launchAngle = bestLaunchAngle.Value;
float tof = bestTof!.Value;
float fireTime = bestFireTime!.Value;
events.Add(new FireEvent
{
FireTime = fireTime,
PlatformIndex = 0,
TargetX = cloudGenX,
TargetY = ty,
TargetZ = cloudGenZ,
MuzzleVelocity = mv,
LaunchAngle = launchAngle,
FlightDuration = tof,
});
return (events, null);
}
// ═══════════════════════════════════════════════
// Step 5: 临界方案(概率阈值 50%
// ═══════════════════════════════════════════════
private DefensePlan DeriveCritical(DefensePlan best)
{
var critical = new DefensePlan
{
ThreatsEngaged = best.ThreatsEngaged,
ThreatsUnengaged = best.ThreatsUnengaged,
};
foreach (var assignment in best.Assignments)
{
int rounds = assignment.RoundsFired;
while (rounds > 1)
{
// 简化:弹药减半 → 概率减半
if ((float)rounds / assignment.RoundsFired < _config.CriticalProbabilityThreshold) break;
rounds--;
}
var reduced = new UnitAssignment
{
FireUnitId = assignment.FireUnitId,
DroneWaveId = assignment.DroneWaveId,
AmmoType = assignment.AmmoType,
RoundsFired = rounds,
FirstFireTime = assignment.FirstFireTime,
FireEvents = assignment.FireEvents.Take(rounds).ToList(),
};
critical.Assignments.Add(reduced);
critical.MergedSchedule.AddRange(reduced.FireEvents);
}
critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
critical.OverallProbability = _config.CriticalProbabilityThreshold;
critical.Summary = $"临界方案:刚好满足 50% 拦截概率";
return critical;
}
// ═══════════════════════════════════════════════
// 辅助
// ═══════════════════════════════════════════════
private static Vector3 ThreatMidpoint(DroneWave threat)
{
if (threat.Waypoints.Count < 2)
return new Vector3(0, (float)threat.Profile.TypicalAltitude, 0);
var s = threat.Waypoints[0];
var e = threat.Waypoints[^1];
return new Vector3(
(float)(s.PosX + e.PosX) / 2f,
(float)(s.PosY + e.PosY) / 2f,
(float)(s.PosZ + e.PosZ) / 2f);
}
}
}

View File

@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>探测计算工具——统一信息网络对威胁的探测能力评估。
/// 纯函数、无状态,与 Kinematics/RouteGeometry 同范式。
/// planner事前规划基于此估算威胁的最早发现点而非上帝视角的航路起点。</summary>
public static class DetectionCalculator
{
/// <summary>某探测源在给定能见度下的综合有效探测距离(水平面)。
/// 取雷达/光电/红外三者的最大有效距离(任一方式发现即算发现)。
/// 光电受 Visibility 衰减:有效 = 基准 × min(1, Visibility/基准)。
/// 雷达/红外不受能见度影响(当前范围)。</summary>
public static float EffectiveRange(float radarRange, float eoRange, float irRange, float visibility)
{
float effRadar = radarRange;
float effEO = eoRange > 0
? eoRange * Math.Min(1f, visibility / eoRange)
: 0f;
float effIR = irRange;
return Math.Max(effRadar, Math.Max(effEO, effIR));
}
/// <summary>统一信息网络对某威胁的最早探测点。
/// 沿航路以步长 ≤50m 采样,对每个采样点调 IsInCoverage 做 3D 球冠判定。
/// 返回首次落入任一探测源球冠的弧长与精度。
/// 无探测源时返回 (0, float.MaxValue)(回退到航路起点,上帝视角)。
/// 航路完全不经过任何探测范围时返回 (float.MaxValue, float.MaxValue)。</summary>
public static (float detectArc, float accuracy) EarliestDetection(
IReadOnlyList<Waypoint> threatRoute,
List<DetectionSource> sources,
float visibility)
{
if (sources == null || sources.Count == 0 || threatRoute == null || threatRoute.Count < 2)
return (0f, float.MaxValue);
// 计算总弧长确定采样步长≤50m
float totalArc = 0f;
for (int i = 0; i < threatRoute.Count - 1; i++)
{
float dx = (float)(threatRoute[i + 1].PosX - threatRoute[i].PosX);
float dy = (float)(threatRoute[i + 1].PosY - threatRoute[i].PosY);
float dz = (float)(threatRoute[i + 1].PosZ - threatRoute[i].PosZ);
totalArc += MathF.Sqrt(dx * dx + dy * dy + dz * dz);
}
const float sampleStep = 50f;
int sampleCount = Math.Max(1, (int)(totalArc / sampleStep) + 1);
float stepArc = totalArc / sampleCount;
float bestArc = float.MaxValue;
float bestAccuracy = float.MaxValue;
// 预计算每个源的 EffectiveRange能见度衰减
var srcCache = new (float range, DetectionSource src)[sources.Count];
for (int s = 0; s < sources.Count; s++)
{
var src = sources[s];
float range = EffectiveRange(src.RadarRange, src.EORange, src.IRRange, visibility);
srcCache[s] = (range, src);
}
// 沿航路采样
for (int i = 0; i <= sampleCount; i++)
{
float sampleArc = i * stepArc;
var (sx, sy, sz) = RouteGeometry.PositionAt(threatRoute, sampleArc);
var pos = new Algorithms.Vector3(sx, sy, sz);
for (int s = 0; s < srcCache.Length; s++)
{
var (range, src) = srcCache[s];
if (range <= 0) continue;
if (IsInCoverage(pos, src.Position, range,
src.MinElevation, src.MaxElevation,
src.MinDetectAlt, src.MaxDetectAlt))
{
// 最早发现优先;同一点取精度最高
if (sampleArc < bestArc)
{
bestArc = sampleArc;
bestAccuracy = src.Accuracy;
}
else if (MathF.Abs(sampleArc - bestArc) < 0.001f
&& src.Accuracy < bestAccuracy)
{
bestAccuracy = src.Accuracy;
}
}
}
// 一旦发现第一个覆盖点即停止(后续采样点距离更远,不可能是"最早"
if (bestArc < float.MaxValue)
break;
}
if (bestArc == float.MaxValue)
return (float.MaxValue, float.MaxValue);
return (bestArc, bestAccuracy);
}
/// <summary>三维球冠探测判定:目标是否在探测设备的有效探测范围内。
/// 三项判定:① 水平距离 ≤ effectiveRange② 俯仰角 ∈ [MinElevation, MaxElevation]
/// ③ 目标高度 ∈ [MinDetectAlt, MaxDetectAlt]。
/// MinElevation/MaxElevation 为 float.MaxValue 时表示无限制(退化球冠→等价 2D 圆)。
/// MinDetectAlt/MaxDetectAlt 为 float.MaxValue 时同理。
/// planner 与运行时实时探测共用此判定。</summary>
public static bool IsInCoverage(
Algorithms.Vector3 target, Algorithms.Vector3 detector,
float effectiveRange,
float minElevation, float maxElevation,
float minDetectAlt, float maxDetectAlt)
{
float dx = target.X - detector.X;
float dy = target.Y - detector.Y; // 高度差
float dz = target.Z - detector.Z;
float horizDistSq = dx * dx + dz * dz;
// ① 水平距离
if (horizDistSq > effectiveRange * effectiveRange) return false;
// ③ 高度门限
if (minDetectAlt != float.MaxValue && target.Y < minDetectAlt) return false;
if (maxDetectAlt != float.MaxValue && target.Y > maxDetectAlt) return false;
// ② 俯仰角(正顶/正下按角度门限放行)
if (minElevation == float.MaxValue && maxElevation == float.MaxValue) return true;
float horizDist = MathF.Sqrt(horizDistSq);
if (horizDist < 0.0001f) return true;
float elevation = MathF.Atan2(dy, horizDist) * (180f / MathF.PI);
if (minElevation != float.MaxValue && elevation < minElevation) return false;
if (maxElevation != float.MaxValue && elevation > maxElevation) return false;
return true;
}
/// <summary>探测精度换算为抛撒散布半径m
/// 精度差 → 散布半径大planner 增加横向覆盖。
/// 当前模型:散布半径 = 精度值(如精度 100m → 散布 ±100m。</summary>
public static float SpreadRadius(float accuracy)
{
if (accuracy < 0)
throw new ArgumentException($"accuracy 必须 ≥ 0实际: {accuracy}", nameof(accuracy));
return accuracy;
}
}
}

View File

@ -0,0 +1,17 @@
namespace CounterDrone.Core.Algorithms
{
/// <summary>探测源(独立探测设备或火力单元自带探测能力)</summary>
public class DetectionSource
{
public Vector3 Position { get; set; }
public float RadarRange { get; set; }
public float EORange { get; set; }
public float IRRange { get; set; }
public float Accuracy { get; set; }
public float MinElevation { get; set; } = float.MaxValue;
public float MaxElevation { get; set; } = float.MaxValue;
public float MinDetectAlt { get; set; } = float.MaxValue;
public float MaxDetectAlt { get; set; } = float.MaxValue;
public string? ModelId { get; set; }
}
}

View File

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

View File

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

View File

@ -1,17 +0,0 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>防御推荐接口 — 威胁驱动</summary>
public interface IDefenseAdvisor
{
DefenseRecommendation Recommend(ThreatProfile threat);
/// <summary>多编队推荐 — 为每组分配火力单元,合并发射计划</summary>
MultiGroupRecommendation RecommendMultiGroup(
List<DroneGroup> droneGroups,
List<FireUnit> fireUnits,
CombatScene environment);
}
}

View File

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

View File

@ -0,0 +1,14 @@
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>车道划分策略:决定一个批次拆成几个车道</summary>
public interface ILaneDivider
{
/// <summary>计算车道数和不重叠的车道间距</summary>
/// <param name="wave">无人机批次</param>
/// <param name="cloudRadius">云团有效半径 (m)</param>
/// <returns>(车道数, 车道间距 m)</returns>
(int laneCount, float laneSpacing) Divide(DroneWave wave, float cloudRadius);
}
}

View File

@ -3,23 +3,26 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>吸入式灭火 — 阈值型:密度达标后线性累积</summary>
/// <summary>吸入式灭火 — 累积伤害</summary>
public class InertGasDamageModel : IDamageModel
{
private const float EffectiveThreshold = 0.0001f; // 对齐弹药的有效浓度
private const float DamageRate = 0.15f; // 每秒毁伤率
private const float DamageRate = 0.15f; // 每秒毁伤率
public float CalculateDamage(TargetType droneType, PowerType powerType,
public float CalculateDamage(DroneType droneType, PowerType powerType,
AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
{
if (aerosolType != AerosolType.InertGas) return 0f;
if (cloudDensity < EffectiveThreshold) return 0f;
// 活塞发动机对惰性气体最敏感
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
return DamageRate * sensitivity * deltaTime;
}
public float RequiredExposureSeconds(DroneType droneType, PowerType powerType, AerosolType aerosolType)
{
if (aerosolType != AerosolType.InertGas) return float.MaxValue;
var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
return 1.0f / (DamageRate * sensitivity);
}
public DamageStage GetDamageStage(float accumulatedDamage)
{
if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;

View File

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
public static class InterceptCalculator
{
/// <summary>空基拦截:水平发射(θ=0°),从当前位置直接抛出。
/// 炮弹以 vp 水平飞出,下落 tf=√(2Δy/g) 秒后到达目标高度。
/// 拦截点 = ux + vp·tf。方程: (A-d)/vd = R + expansion + tf。</summary>
public static (float, float, float) ComputeHorizontal(
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
float reactionTime, Vector3 platformPos, float cruiseSpeed,
float releaseAlt, float targetAlt)
{
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || cruiseSpeed <= 0)
return (0, 0, 0);
float vd = droneSpeedKmh / 3.6f;
float dy = releaseAlt - targetAlt;
if (dy <= 0) return (0, 0, 0);
float tf = MathF.Sqrt(2f * dy / 9.81f);
// 水平发射:炮弹沿航路方向飞行
float direction = route[route.Count - 1].PosX > route[0].PosX ? 1f : -1f;
float A = platformPos.X + direction * cruiseSpeed * tf;
float totalArc = RouteGeometry.TotalLength(route);
if (A < detectArc || A > totalArc) return (0, 0, 0);
// 验证时间匹配
float droneTime = (A - detectArc) / vd;
float defendTime = reactionTime + tf;
if (droneTime < defendTime) return (0, 0, 0);
return (A, droneTime - reactionTime, 0);
}
/// <summary>地基拦截D² + (Δy + ½g·ts²)² = vs²·ts²</summary>
public static (float InterceptArc, float ShellTime, float LaunchAngle) Compute(
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
float reactionTime, Vector3 fireUnitPos, float muzzleVelocity)
{
// ... existing code unchanged ...
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || muzzleVelocity <= 0)
return (0, 0, 0);
float totalArc = RouteGeometry.TotalLength(route);
if (detectArc >= totalArc) return (0, 0, 0);
float vd = droneSpeedKmh / 3.6f;
float vs2 = muzzleVelocity * muzzleVelocity;
float g = 9.81f;
float uy = fireUnitPos.Y;
IReadOnlyList<Waypoint> r = route;
float F(float a)
{
float ts = (a - detectArc) / vd - reactionTime;
if (ts <= 0) return float.MaxValue;
var (px, py, pz) = RouteGeometry.PositionAt(r, a);
float dx = px - fireUnitPos.X, dz = pz - fireUnitPos.Z;
float D2 = dx * dx + dz * dz;
float dy = py - uy;
float term = dy + 0.5f * g * ts * ts;
return D2 + term * term - vs2 * ts * ts;
}
static float Angle(Vector3 unit, float arc, float ts, float vs, IReadOnlyList<Waypoint> wps)
{
var (px, py, pz) = RouteGeometry.PositionAt(wps, arc);
float D = MathF.Sqrt((px - unit.X) * (px - unit.X) + (pz - unit.Z) * (pz - unit.Z));
float dy = py - unit.Y;
float sinA = (dy + 0.5f * 9.81f * ts * ts) / (vs * ts);
float cosA = D / (vs * ts);
return MathF.Atan2(sinA, cosA);
}
// +0.001f 确保 ts > 0否则 F() 返回 MaxValue 导致搜索失败
float lo = detectArc + vd * reactionTime + 0.001f;
if (lo >= totalArc) return (0, 0, 0);
float fLo = F(lo);
if (fLo >= float.MaxValue - 1) return (0, 0, 0);
float step = (totalArc - lo) / 100f;
if (step < 0.001f) step = 0.001f;
float hi = lo + step;
float fHi = 0;
while (hi <= totalArc)
{
fHi = F(hi);
if (fHi >= float.MaxValue - 1 || fHi <= 0) break;
lo = hi; fLo = fHi;
hi += step;
}
if (hi > totalArc || fHi >= float.MaxValue - 1) return (0, 0, 0);
if (fHi > 0 && fLo > 0) return (0, 0, 0);
for (int i = 0; i < 30; i++)
{
float mid = (lo + hi) / 2f;
float fMid = F(mid);
if (MathF.Abs(fMid) < 0.001f || hi - lo < 0.001f)
{
float ts = (mid - detectArc) / vd - reactionTime;
return (mid, ts, Angle(fireUnitPos, mid, ts, muzzleVelocity, r));
}
if (fLo <= 0 && fMid >= 0 || fLo >= 0 && fMid <= 0) { hi = mid; fHi = fMid; }
else { lo = mid; fLo = fMid; }
}
float final = (lo + hi) / 2f;
float finalTs = (final - detectArc) / vd - reactionTime;
return (final, finalTs, Angle(fireUnitPos, final, finalTs, muzzleVelocity, r));
}
}
}

View File

@ -27,32 +27,88 @@ namespace CounterDrone.Core.Algorithms
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
}
/// <summary>计算抛物线炮弹的发射角</summary>
/// <param name="range">水平距离 (m)</param>
/// <param name="muzzleVelocity">初速 (m/s)</param>
/// <param name="releaseAltitude">释放高度 (m),弹道顶点必须 ≥ 此值</param>
/// <returns>发射角 (rad),取低弹道</returns>
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
/// <summary>给定水平距离和初速,求解抛物线发射角(逆问题)</summary>
/// <param name="range">水平距离 (m),必须 > 0</param>
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param>
/// <returns>发射角 (rad),取低弹道</returns>
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float heightDiff)
{
var v2 = muzzleVelocity * muzzleVelocity;
var g = 9.81f;
// 1. 射程所需角θr = arcsin(R*g/v²) / 2
var rangeRatio = range * g / v2;
var angleRange = rangeRatio >= 1.0f
? 45f * (float)Math.PI / 180f
: (float)Math.Asin(rangeRatio) / 2f;
// 2. 释放高度所需最小角H = v²*sin²(θ)/(2g) → sin(θ) = √(2gH)/v
var sinMinHeight = (float)Math.Sqrt(2f * g * releaseAltitude * 1.05f) / muzzleVelocity;
var angleHeight = sinMinHeight >= 1.0f
? 90f * (float)Math.PI / 180f
: (float)Math.Asin(sinMinHeight);
return Math.Max(angleRange, angleHeight);
if (range <= 0)
throw new ArgumentException($"range 必须 > 0实际: {range}", nameof(range));
if (muzzleVelocity <= 0)
throw new ArgumentException($"muzzleVelocity 必须 > 0实际: {muzzleVelocity}", nameof(muzzleVelocity));
var angle = ParabolicMotion.SolveAngle(range, heightDiff, muzzleVelocity, TrajectoryPreference.Low);
if (!angle.HasValue)
throw new ArgumentException(
$"当前参数无法命中目标: range={range}, heightDiff={heightDiff}, v₀={muzzleVelocity}");
return angle.Value;
}
/// <summary>根据发射参数计算抛物线位置</summary>
/// <summary>弹道飞行时间(秒)。水平距离 / 水平分速</summary>
public static float ParabolicTimeOfFlight(float range, float launchAngle, float muzzleVelocity)
{
if (range <= 0)
throw new ArgumentException($"range 必须 > 0实际: {range}", nameof(range));
if (muzzleVelocity <= 0)
throw new ArgumentException($"muzzleVelocity 必须 > 0实际: {muzzleVelocity}", nameof(muzzleVelocity));
float cosAngle = (float)Math.Cos(launchAngle);
return range / (muzzleVelocity * cosAngle);
}
/// <summary>抛物线飞行时间:给定水平距离和目标高度差,计算弹道时间</summary>
/// <remarks>等价于先调用 CalculateLaunchAngle 再调用 ParabolicTimeOfFlight</remarks>
public static float ParabolicShellTime(float horizontalDist, float heightDiff, float muzzleVelocity)
{
float angle = CalculateLaunchAngle(horizontalDist, muzzleVelocity, heightDiff);
return ParabolicTimeOfFlight(horizontalDist, angle, muzzleVelocity);
}
/// <summary>给定初速和发射角,计算到达指定相对高度的水平射程和飞行时间(正问题)</summary>
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
/// <param name="launchAngle">发射角 (rad),必须 ∈ (0, π/2)</param>
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param>
/// <returns>(水平射程 m, 飞行时间 s),取下行段解</returns>
public static (float range, float timeOfFlight) ComputeParabolicRange(
float muzzleVelocity, float launchAngle, float heightDiff)
{
if (muzzleVelocity <= 0)
throw new ArgumentException($"muzzleVelocity 必须 > 0实际: {muzzleVelocity}", nameof(muzzleVelocity));
if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2)
throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
var motion = new ParabolicMotion(muzzleVelocity, launchAngle);
var result = motion.ComputeRange(heightDiff, TrajectoryPreference.Nearest);
if (!result.HasValue)
throw new ArgumentException(
$"当前参数无法达到目标高度: heightDiff={heightDiff}, v₀={muzzleVelocity}, θ={launchAngle * 180 / Math.PI:F1}°");
return result.Value;
}
/// <summary>计算弹道顶点(最大高度和到达时间),相对发射点</summary>
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
/// <param name="launchAngle">发射角 (rad),必须 ∈ [0, π/2)</param>
/// <returns>(顶点高度 m, 到达顶点时间 s),高度是相对发射点的增量</returns>
public static (float apexHeight, float apexTime) ParabolicApex(
float muzzleVelocity, float launchAngle)
{
if (muzzleVelocity <= 0)
throw new ArgumentException($"muzzleVelocity 必须 > 0实际: {muzzleVelocity}", nameof(muzzleVelocity));
if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2)
throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
float g = 9.81f;
float sinA = (float)Math.Sin(launchAngle);
if (sinA <= 0) return (0, 0); // 水平或俯射,无上升顶点
float vy = muzzleVelocity * sinA;
float apexTime = vy / g;
float apexHeight = vy * vy / (2f * g);
return (apexHeight, apexTime);
}
/// <summary>给定初速、发射角和方位角,计算任意时刻的抛物线位置(正问题核心)</summary>
/// <param name="launchAngle">发射角 (rad),水平面以上为正</param>
/// <param name="azimuth">方位角 (rad)0=N(+Z), π/2=E(+X)</param>
public static (float X, float Y, float Z) ParabolicPosition(
float startX, float startY, float startZ,
float launchAngle, float azimuth,
@ -70,6 +126,60 @@ namespace CounterDrone.Core.Algorithms
return (x, y, z);
}
/// <summary>空投弹药位置:继承载机速度 + 重力下落</summary>
/// <param name="startX,startY,startZ">投放点坐标</param>
/// <param name="carrierVelX,carrierVelY,carrierVelZ">载机速度矢量 (m/s)</param>
/// <param name="time">经过时间 (s)</param>
public static (float X, float Y, float Z) AirDropPosition(
float startX, float startY, float startZ,
float carrierVelX, float carrierVelY, float carrierVelZ,
float time)
{
const float g = 9.81f;
var x = startX + carrierVelX * time;
var y = startY + carrierVelY * time - 0.5f * g * time * time;
var z = startZ + carrierVelZ * time;
return (x, y, z);
}
/// <summary>空投降落时间:从释放高度下落到目标高度所需时间</summary>
/// <param name="releaseAlt">释放高度 (m)</param>
/// <param name="targetAlt">目标高度 (m)</param>
/// <param name="carrierVelY">载机垂直速度 (m/s),正=向上,默认 0平飞</param>
public static float AirDropFallTime(float releaseAlt, float targetAlt, float carrierVelY = 0f)
{
const float g = 9.81f;
var dy = releaseAlt - targetAlt;
if (dy <= 0) return 0f;
return ((float)Math.Sqrt(carrierVelY * carrierVelY + 2f * g * dy) - carrierVelY) / g;
}
/// <summary>两点间方向的速度矢量normalize(to - from) × speed</summary>
public static (float X, float Y, float Z) DirectionVelocity(
float fromX, float fromY, float fromZ,
float toX, float toY, float toZ,
float speed)
{
float dx = toX - fromX;
float dy = toY - fromY;
float dz = toZ - fromZ;
float dist = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 0.01f) return (0, 0, 0);
float s = speed / dist;
return (dx * s, dy * s, dz * s);
}
/// <summary>云团覆盖间隔:无人机穿越单个云团的时间 = 云团直径 / 无人机速度</summary>
/// <param name="cloudDiameter">云团有效直径 m</param>
/// <param name="targetSpeedKmh">目标速度 km/h</param>
/// <param name="minInterval">硬件最小间隔 s</param>
public static float CloudCoverInterval(float cloudDiameter, float targetSpeedKmh, float minInterval = 0.1f)
{
float speedMs = targetSpeedKmh / 3.6f;
if (speedMs <= 0.1f) return minInterval;
return Math.Max(minInterval, cloudDiameter / speedMs);
}
/// <summary>点到矩形距离(简化判定)</summary>
public static float Distance2D(float x1, float z1, float x2, float z2)
{
@ -78,6 +188,18 @@ namespace CounterDrone.Core.Algorithms
return (float)Math.Sqrt(dx * dx + dz * dz);
}
/// <summary>匀速直线运动从一点到另一点的飞行时间(秒)。
/// 物理模型:无人机/平台沿直线匀速飞行,时间 = 距离 / 速度。
/// speedKmh 为 0 时抛异常(速度必须 > 0由调用方保证。</summary>
/// <param name="speedKmh">速度 km/h与 TypicalSpeed 单位一致)</param>
public static float TravelTime(float fromX, float fromZ, float toX, float toZ, float speedKmh)
{
if (speedKmh <= 0)
throw new ArgumentException("速度必须 > 0", nameof(speedKmh));
float dist = Distance2D(fromX, fromZ, toX, toZ);
return dist / (speedKmh / 3.6f);
}
/// <summary>点到三维点距离</summary>
public static float Distance3D(float x1, float y1, float z1, float x2, float y2, float z2)
{
@ -136,16 +258,6 @@ namespace CounterDrone.Core.Algorithms
var denom = (float)Math.Pow(2f * (float)Math.PI, 1.5f) * sigmaY * sigmaY * sigmaZ;
return denom > 0.001f ? sourceStrength / denom : 0f;
}
/// <summary>高斯烟团某点浓度 C(x,y,z) — 简化:相对中心的偏移</summary>
public static float GaussianConcentration(float q, float sx, float sy, float sz, float offsetY, float offsetZ)
{
var norm = q / ((float)Math.Pow(2f * (float)Math.PI, 1.5f) * sx * sy * sz);
var ey = (float)Math.Exp(-0.5f * offsetY * offsetY / (sy * sy));
var ez = (float)Math.Exp(-0.5f * offsetZ * offsetZ / (sz * sz));
var ezr = (float)Math.Exp(-0.5f * offsetZ * offsetZ / (sz * sz));
return norm * ey * (ez + ezr);
}
public static bool PointInPolygon(float px, float pz, ReadOnlySpan<(float X, float Z)> vertices)
{
if (vertices.Length < 3) return false;

View File

@ -0,0 +1,109 @@
using System;
namespace CounterDrone.Core.Algorithms
{
/// <summary>轨迹偏好(多解选择)</summary>
public enum TrajectoryPreference
{
High, // 高抛(较大仰角 / 较晚到达目标高度)
Low, // 低抛(较小仰角 / 较早到达目标高度)
Nearest, // 最短飞行时间(最小正时间)
Farthest // 最长飞行时间(最大正时间)
}
/// <summary>抛物线运动学核心模块</summary>
public class ParabolicMotion
{
public readonly float V0, Theta, Y0, X0;
public readonly float Vx, Vy;
public const float G = 9.81f;
public ParabolicMotion(float v0, float theta, float y0 = 0, float x0 = 0)
{
V0 = v0; Theta = theta; Y0 = y0; X0 = x0;
Vx = v0 * (float)Math.Cos(theta);
Vy = v0 * (float)Math.Sin(theta);
}
/// <summary>任意时刻 t 的状态</summary>
public (float x, float y, float vx, float vy) GetState(float t)
{
return (X0 + Vx * t, Y0 + Vy * t - 0.5f * G * t * t, Vx, Vy - G * t);
}
/// <summary>到达目标高度 yTarget 的所有正时间解0/1/2 个)</summary>
public float[] GetFlightTimes(float yTarget)
{
float a = 0.5f * G;
float b = -Vy;
float c = yTarget - Y0;
float d = b * b - 4f * a * c;
if (d < 0) return Array.Empty<float>();
float sqrtD = (float)Math.Sqrt(d);
float t1 = (-b - sqrtD) / (2f * a);
float t2 = (-b + sqrtD) / (2f * a);
if (t1 > 0 && t2 > 0)
return t1 < t2 ? new[] { t1, t2 } : new[] { t2, t1 };
if (t1 > 0) return new[] { t1 };
if (t2 > 0) return new[] { t2 };
return Array.Empty<float>();
}
/// <summary>根据偏好选择到达目标高度的飞行时间</summary>
public float? GetFlightTime(float yTarget, TrajectoryPreference pref)
{
var ts = GetFlightTimes(yTarget);
if (ts.Length == 0) return null;
if (ts.Length == 1) return ts[0];
return pref switch
{
TrajectoryPreference.Nearest or TrajectoryPreference.Low => ts[0],
TrajectoryPreference.Farthest or TrajectoryPreference.High => ts[1],
_ => ts[0]
};
}
/// <summary>根据偏好获取水平和飞行时间</summary>
public (float range, float timeOfFlight)? ComputeRange(float yTarget, TrajectoryPreference pref)
{
float? t = GetFlightTime(yTarget, pref);
if (!t.HasValue) return null;
return (Vx * t.Value, t.Value);
}
// ═══ 逆问题:求解瞄准角度 ═══
/// <summary>已知水平距离、高度差、初速,求解两个可能的发射角 (rad)</summary>
public static (float theta1, float theta2)? SolveAngles(float range, float heightDiff, float v0)
{
if (range <= 0 || v0 <= 0) return null;
float v2 = v0 * v0;
float A = (G * range * range) / (2f * v2);
float B = -range;
float C = heightDiff + A;
float d = B * B - 4f * A * C;
if (d < 0 || A == 0) return null;
float sqrtD = (float)Math.Sqrt(d);
float p1 = (-B + sqrtD) / (2f * A);
float p2 = (-B - sqrtD) / (2f * A);
return ((float)Math.Atan(p1), (float)Math.Atan(p2));
}
/// <summary>根据偏好选择一个瞄准角度</summary>
public static float? SolveAngle(float range, float heightDiff, float v0, TrajectoryPreference pref)
{
var result = SolveAngles(range, heightDiff, v0);
if (!result.HasValue) return null;
var (a1, a2) = result.Value;
float high = Math.Max(a1, a2);
float low = Math.Min(a1, a2);
return pref switch
{
TrajectoryPreference.High => high,
TrajectoryPreference.Low or TrajectoryPreference.Nearest or _ => low
};
}
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>防御规划器配置 — 全局策略参数,从 planner_config.json 加载。
/// 代码零默认值,所有字段必须从配置文件读取;文件缺失或字段缺失即抛异常。</summary>
public class PlannerConfig
{
/// <summary>云团重叠比例0=相切0.2=重叠 20%)。间距 = 2R × (1 重叠比例)。</summary>
public float CloudOverlapRatio { get; set; }
/// <summary>临界方案概率阈值DeriveCritical 用)</summary>
public float CriticalProbabilityThreshold { get; set; }
/// <summary>拦截概率上限(封顶值)</summary>
public float MaxInterceptProbability { get; set; }
/// <summary>威胁类型系数表DroneType → 系数)</summary>
public Dictionary<DroneType, float> TypeCoefficient { get; set; } = new();
/// <summary>弹药匹配表PowerType → AerosolType</summary>
public Dictionary<PowerType, AerosolType> AmmoMatch { get; set; } = new();
/// <summary>无探测设备时的默认探测精度 m回退值上帝视角但有标称误差</summary>
public float DefaultDetectionAccuracy { get; set; }
/// <summary>云团有效膨胀系数0~1。planner 取 Phase 2 膨胀时间的此比例作为有效云团年龄。默认 0.9</summary>
public float ExpansionFactor { get; set; } = 0.9f;
/// <summary>拦截窗口安全余量s。计算所需探测弧长时在膨胀时间基础上额外预留。默认 1</summary>
public float TimingSafetyMargin { get; set; } = 1f;
/// <summary>火力单元反应时间s。探测到目标后装填+瞄准的时间。默认 5</summary>
public float ReactionTime { get; set; } = 5f;
private const string ConfigFileName = "planner_config.json";
/// <summary>从 dataRoot 加载配置。文件缺失或字段非法即抛异常。</summary>
public static PlannerConfig Load(string dataRoot)
{
if (string.IsNullOrEmpty(dataRoot))
throw new ArgumentException("dataRoot 不能为空", nameof(dataRoot));
string path = Path.Combine(dataRoot, ConfigFileName);
if (!File.Exists(path))
throw new FileNotFoundException($"planner 配置文件不存在: {path}");
string json = File.ReadAllText(path);
var config = JsonSerializer.Deserialize<PlannerConfig>(json, JsonOptions)
?? throw new InvalidDataException($"planner 配置解析失败: {path}");
config.Validate();
return config;
}
/// <summary>从 IPathProvider 加载(便捷重载)</summary>
public static PlannerConfig Load(IPathProvider paths)
=> Load(paths?.GetDataRoot() ?? throw new ArgumentNullException(nameof(paths)));
private void Validate()
{
if (CloudOverlapRatio < 0f || CloudOverlapRatio >= 1f)
throw new InvalidDataException($"CloudOverlapRatio 必须在 [0, 1),实际 {CloudOverlapRatio}");
if (CriticalProbabilityThreshold <= 0f || CriticalProbabilityThreshold >= 1f)
throw new InvalidDataException($"CriticalProbabilityThreshold 必须在 (0, 1),实际 {CriticalProbabilityThreshold}");
if (MaxInterceptProbability <= 0f || MaxInterceptProbability > 1f)
throw new InvalidDataException($"MaxInterceptProbability 必须在 (0, 1],实际 {MaxInterceptProbability}");
if (TypeCoefficient == null || TypeCoefficient.Count == 0)
throw new InvalidDataException("TypeCoefficient 不能为空");
if (AmmoMatch == null || AmmoMatch.Count == 0)
throw new InvalidDataException("AmmoMatch 不能为空");
if (DefaultDetectionAccuracy < 0f)
throw new InvalidDataException($"DefaultDetectionAccuracy 必须 >= 0实际 {DefaultDetectionAccuracy}");
if (ExpansionFactor <= 0f || ExpansionFactor > 1f)
throw new InvalidDataException($"ExpansionFactor 必须在 (0, 1],实际 {ExpansionFactor}");
if (TimingSafetyMargin < 0f)
throw new InvalidDataException($"TimingSafetyMargin 必须 >= 0实际 {TimingSafetyMargin}");
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
Converters = { new JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true,
};
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>航路几何工具——对 waypoint 序列做纯几何运算。
/// 无状态、纯函数,与 Kinematics/DamageAssessment 同范式。
/// planner预测和 DroneEntity执行共用确保两边航路模型一致。</summary>
public static class RouteGeometry
{
/// <summary>航路总弧长(米):所有相邻 waypoint 三维距离之和。</summary>
public static float TotalLength(IReadOnlyList<Waypoint> wps)
{
if (wps == null || wps.Count < 2) return 0f;
float total = 0f;
for (int i = 0; i < wps.Count - 1; i++)
total += Kinematics.Distance3D(
(float)wps[i].PosX, (float)wps[i].PosY, (float)wps[i].PosZ,
(float)wps[i + 1].PosX, (float)wps[i + 1].PosY, (float)wps[i + 1].PosZ);
return total;
}
/// <summary>沿航路弧长 s 处的三维位置(分段线性插值)。
/// s≤0 返回起点s≥TotalLength 返回终点。</summary>
public static (float X, float Y, float Z) PositionAt(IReadOnlyList<Waypoint> wps, float arcLength)
{
if (wps == null || wps.Count == 0) return (0f, 0f, 0f);
if (wps.Count == 1 || arcLength <= 0f)
return ((float)wps[0].PosX, (float)wps[0].PosY, (float)wps[0].PosZ);
float remaining = arcLength;
for (int i = 0; i < wps.Count - 1; i++)
{
float segLen = Kinematics.Distance3D(
(float)wps[i].PosX, (float)wps[i].PosY, (float)wps[i].PosZ,
(float)wps[i + 1].PosX, (float)wps[i + 1].PosY, (float)wps[i + 1].PosZ);
if (remaining <= segLen)
{
float t = segLen > 0.0001f ? remaining / segLen : 0f;
return (
(float)(wps[i].PosX + (wps[i + 1].PosX - wps[i].PosX) * t),
(float)(wps[i].PosY + (wps[i + 1].PosY - wps[i].PosY) * t),
(float)(wps[i].PosZ + (wps[i + 1].PosZ - wps[i].PosZ) * t));
}
remaining -= segLen;
}
// 超过总弧长,返回终点
var last = wps[^1];
return ((float)last.PosX, (float)last.PosY, (float)last.PosZ);
}
/// <summary>航路上离目标点 (x,z) 最近的点对应的弧长(水平投影最近)。
/// 用于 planner 定位"无人机穿越点在航路上的弧长位置"。
/// 算法:逐段求点到线段的最近点,取全局最近者。</summary>
public static float ArcLengthNearestTo(IReadOnlyList<Waypoint> wps, float x, float z)
{
if (wps == null || wps.Count < 2) return 0f;
float bestArc = 0f;
float bestDist = float.MaxValue;
float accumArc = 0f;
for (int i = 0; i < wps.Count - 1; i++)
{
float ax = (float)wps[i].PosX, az = (float)wps[i].PosZ;
float bx = (float)wps[i + 1].PosX, bz = (float)wps[i + 1].PosZ;
float segDx = bx - ax, segDz = bz - az;
float segLenSq = segDx * segDx + segDz * segDz;
float t = 0f;
if (segLenSq > 0.0001f)
{
t = ((x - ax) * segDx + (z - az) * segDz) / segLenSq;
t = Math.Max(0f, Math.Min(1f, t));
}
float projX = ax + segDx * t;
float projZ = az + segDz * t;
float distSq = (projX - x) * (projX - x) + (projZ - z) * (projZ - z);
if (distSq < bestDist)
{
bestDist = distSq;
float segLen = (float)Math.Sqrt(segLenSq);
bestArc = accumArc + t * segLen;
}
accumArc += (float)Math.Sqrt(segLenSq);
}
return bestArc;
}
/// <summary>从航路起点匀速运动到弧长 s 处的飞行时间(秒)。
/// 匀速直线模型,时间 = 弧长 / 速度。内部调用 Kinematics.TravelTime。</summary>
/// <param name="speedKmh">速度 km/h与 TypicalSpeed 单位一致),必须 > 0</param>
public static float TravelTimeTo(IReadOnlyList<Waypoint> wps, float arcLength, float speedKmh)
{
if (speedKmh <= 0)
throw new ArgumentException("速度必须 > 0", nameof(speedKmh));
// 弧长 = 距离,匀速直线时间 = 距离/速度
// 用 Kinematics.TravelTime 保持单位换算唯一km/h → m/s 在 Kinematics 内)
return arcLength / (speedKmh / 3.6f);
}
/// <summary>沿航路方向给定弧长 s 处的水平单位切向量 (DirX, DirZ)。
/// 用于 planner 沿航路方向布云offset 沿切向,不再写死 X 轴)。
/// 弧长超出范围时取末段方向。</summary>
public static (float DirX, float DirZ) TangentAt(IReadOnlyList<Waypoint> wps, float arcLength)
{
if (wps == null || wps.Count < 2) return (1f, 0f);
if (arcLength < 0) arcLength = 0;
float remaining = arcLength;
for (int i = 0; i < wps.Count - 1; i++)
{
float dx = (float)wps[i + 1].PosX - (float)wps[i].PosX;
float dz = (float)wps[i + 1].PosZ - (float)wps[i].PosZ;
float segLen = (float)Math.Sqrt(dx * dx + dz * dz);
if (remaining <= segLen || i == wps.Count - 2)
{
if (segLen < 0.0001f) return (1f, 0f);
return (dx / segLen, dz / segLen);
}
remaining -= segLen;
}
// 兜底:末段方向
float ldx = (float)wps[^1].PosX - (float)wps[^2].PosX;
float ldz = (float)wps[^1].PosZ - (float)wps[^2].PosZ;
float llen = (float)Math.Sqrt(ldx * ldx + ldz * ldz);
return llen < 0.0001f ? (1f, 0f) : (ldx / llen, ldz / llen);
}
}
}

View File

@ -0,0 +1,9 @@
namespace CounterDrone.Core.Algorithms
{
/// <summary>三维向量(纯 C#,不依赖 UnityEngine</summary>
public struct Vector3
{
public float X, Y, Z;
public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
}
}

View File

@ -7,6 +7,13 @@
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CounterDrone.Core.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="PdfSharpCore" Version="1.3.64" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>

View File

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

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using CounterDrone.Core.Models;
namespace CounterDrone.Core
{
/// <summary>
/// 统一默认数据 — 弹药、编队、火力单元、无人机、探测设备、天气的预设库。
/// 所有默认数据集中在 data/defaults.json通过本类统一加载。
/// </summary>
public class DefaultData
{
public string Version { get; set; } = "0";
public List<AmmunitionSpec> Ammunition { get; set; } = new();
public List<FormationTemplate> Formations { get; set; } = new();
public List<RouteTemplate> Routes { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("fireUnits")]
public List<FireUnitSpec> FireUnits { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("launchPlatforms")]
public List<LaunchPlatformSpec> LaunchPlatforms { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("drones")]
public List<DroneSpec> Drones { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("detectionEquipment")]
public List<SensorSpec> Sensors { get; set; } = new();
[System.Text.Json.Serialization.JsonPropertyName("weather")]
public List<EnvironmentSpec> Environments { get; set; } = new();
public static DefaultData Load(IPathProvider paths)
{
var jsonPath = Path.Combine(paths.GetDataRoot(), "defaults.json");
if (!File.Exists(jsonPath))
throw new FileNotFoundException($"默认数据文件未找到: {jsonPath}");
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var data = JsonSerializer.Deserialize<DefaultData>(File.ReadAllText(jsonPath), opts)
?? throw new InvalidOperationException($"默认数据文件解析失败: {jsonPath}");
Validate(data, jsonPath);
return data;
}
public static DefaultData FromJson(string json)
{
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var data = JsonSerializer.Deserialize<DefaultData>(json, opts)
?? throw new InvalidOperationException("默认数据 JSON 解析失败");
Validate(data, "<string>");
return data;
}
private static void Validate(DefaultData data, string source)
{
if (data.Ammunition == null || data.Ammunition.Count == 0)
throw new InvalidOperationException($"默认数据缺少 ammunition: {source}");
if (data.Formations == null || data.Formations.Count == 0)
throw new InvalidOperationException($"默认数据缺少 formations: {source}");
if (data.Routes == null || data.Routes.Count == 0)
throw new InvalidOperationException($"默认数据缺少 routes: {source}");
if (data.FireUnits == null || data.FireUnits.Count == 0)
throw new InvalidOperationException($"默认数据缺少 fireUnits: {source}");
if (data.LaunchPlatforms == null || data.LaunchPlatforms.Count == 0)
throw new InvalidOperationException($"默认数据缺少 launchPlatforms: {source}");
if (data.Drones == null || data.Drones.Count == 0)
throw new InvalidOperationException($"默认数据缺少 drones: {source}");
if (data.Environments == null || data.Environments.Count == 0)
throw new InvalidOperationException($"默认数据缺少 environments: {source}");
if (data.Sensors == null)
data.Sensors = new();
// 由 Range 字段派生 HasRadar/HasEO/HasIRJSON 可能未显式设置)
foreach (var s in data.Sensors)
{
s.HasRadar = s.RadarRange > 0;
s.HasEO = s.EORange > 0;
s.HasIR = s.IRRange > 0;
}
}
}
}

View File

@ -0,0 +1,156 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using CounterDrone.Core.Services;
namespace CounterDrone.Core
{
/// <summary>
/// 预设想定种子 — 从 DefaultData 预设组装完整想定,写入数据库供前端测试。
/// 首次运行时自动种子,同名想定跳过(幂等)。
/// </summary>
public static class DefaultScenarios
{
public static void Seed(IScenarioService scenario, DefaultData d)
{
// 检测旧格式任务(名称含 [Demo],但 SQL LIKE 中 [] 是通配符,搜索 "Demo"
var existing = scenario.SearchScenarios("Demo", null, null, 1, 100);
if (existing.TotalCount > 0)
{
// 版本升级:删除旧 Demo 任务,重新创建
foreach (var t in existing.Items)
scenario.DeleteScenario(t.Id);
}
SeedNoDefense(scenario, d);
SeedZoneIntrusion(scenario, d);
SeedPistonWindy(scenario, d);
SeedJetActiveMaterial(scenario, d);
SeedAirBasedWindy(scenario, d);
Seed3DronesAirBased(scenario, d);
}
private static List<Waypoint> R(string routeId, double speed, DefaultData d)
=> d.Routes.First(r => r.Id == routeId).ToWaypoints(speed);
private static void SeedNoDefense(IScenarioService s, DefaultData d)
{
var t = s.CreateScenario("[Demo] 无防御-无人机抵达目标", "");
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "shahed").ToScenarioDrone());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("3km-h400", 600, d));
s.SaveCloudDispersal(t.Id, new CloudDispersal());
s.SaveDeployment(t.Id, new List<ScenarioUnit>());
s.UpdateStep(t.Id, 5);
}
private static void SeedZoneIntrusion(IScenarioService s, DefaultData d)
{
var t = s.CreateScenario("[Demo] 管控区域侵入", "");
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "electric-scout").ToScenarioDrone());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("3km-h300", 300, d));
s.SaveCloudDispersal(t.Id, new CloudDispersal());
s.SaveDeployment(t.Id, new List<ScenarioUnit>());
s.SaveControlZones(t.Id, new List<ControlZone>
{
new ControlZone
{
Name = "禁飞区",
VerticesJson = "[{\"X\":1200,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":-200},{\"X\":1800,\"Y\":0,\"Z\":200},{\"X\":1200,\"Y\":0,\"Z\":200}]",
MinAltitude = 0, MaxAltitude = 1000,
},
});
s.UpdateStep(t.Id, 5);
}
private static void SeedPistonWindy(IScenarioService s, DefaultData d)
{
var t = s.CreateScenario("[Demo] 活塞拦截-西风5ms", "");
var scene = d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene();
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.W;
s.SaveScene(t.Id, scene);
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "shahed").ToScenarioDrone());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 6600, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<ScenarioUnit>
{
d.FireUnits.First(f => f.Id == "ground-light").ToScenarioUnit(AerosolType.InertGas, 1, 0, 0, 50),
});
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "radar-sr").ToScenarioUnit(0, 0, 50));
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
}
private static void SeedJetActiveMaterial(IScenarioService s, DefaultData d)
{
var t = s.CreateScenario("[Demo] 喷气式拦截-活性材料", "");
s.SaveScene(t.Id, d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene());
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "cruise-missile").ToScenarioDrone());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 18000, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<ScenarioUnit>
{
d.FireUnits.First(f => f.Id == "ground-standard").ToScenarioUnit(AerosolType.ActiveMaterial, 1, 0, 0, 50),
});
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "radar-mr").ToScenarioUnit(0, 0, 50));
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
}
private static void SeedAirBasedWindy(IScenarioService s, DefaultData d)
{
var t = s.CreateScenario("[Demo] 空基拦截-东风5ms", "");
var scene = d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene();
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.E;
s.SaveScene(t.Id, scene);
s.SaveScenarioDrone(t.Id, d.Drones.First(p => p.Id == "shahed").ToScenarioDrone());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 9500, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<ScenarioUnit>
{
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 3, 5000, 1000, 0),
});
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "eo-station").ToScenarioUnit(5000, 1000, 0));
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
}
private static void Seed3DronesAirBased(IScenarioService s, DefaultData d)
{
var t = s.CreateScenario("[Demo] 3架空基编队拦截", "");
var scene = d.Environments.First(w => w.Id == "sunny-calm").ToCombatScene();
scene.WindSpeed = 5;
scene.WindDirection = (int)WindDirection.E;
s.SaveScene(t.Id, scene);
var target = d.Drones.First(p => p.Id == "shahed").ToScenarioDrone();
target.Quantity = 3;
s.SaveScenarioDrone(t.Id, target);
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<ScenarioUnit>
{
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 1, 5000, 1000, 0),
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 1, 5300, 1000, 0),
d.FireUnits.First(f => f.Id == "air-standard").ToScenarioUnit(AerosolType.InertGas, 1, 5600, 1000, 0),
});
s.AddDetection(t.Id, d.Sensors.First(sn => sn.Id == "eo-station").ToScenarioUnit(5000, 1000, 0));
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
}
}
}

View File

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

View File

@ -42,6 +42,9 @@ namespace CounterDrone.Core.Models
/// <summary>湍流扩散系数Phase 2 中 R=R₀+k√t 的 k 值</summary>
public double TurbulentExpansionK { get; set; } = 5.0;
/// <summary>Phase 2 湍流膨胀持续时间s之后进入 Phase 3 高斯扩散。默认 30s</summary>
public double Phase2Duration { get; set; } = 30.0;
public double MaxRadius { get; set; }
public double MaxDuration { get; set; }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>无人机规格(基础数据模板)</summary>
[Table("DroneSpec")]
public class DroneSpec
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int DroneType { get; set; }
/// <summary>型号名称(非 3D 模型文件。3D 模型引用见 ModelId</summary>
public string Model { get; set; } = "";
public string Description { get; set; } = "";
public int PowerType { get; set; }
public double Wingspan { get; set; }
public double TypicalSpeed { get; set; }
public double TypicalAltitude { get; set; }
/// <summary>3D 模型 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
{
// === 任务 ===
public enum TaskStatus
public enum ScenarioStatus
{
Draft = 0,
Configuring = 1,
@ -33,13 +33,11 @@ namespace CounterDrone.Core.Models
}
// === 目标 ===
public enum TargetType
public enum DroneType
{
Rotor = 0,
FixedWing = 1,
Electric = 2,
Piston = 3,
HighSpeed = 4
HighSpeed = 2
}
public enum PowerType
@ -62,6 +60,13 @@ namespace CounterDrone.Core.Models
LaunchPlatform = 1
}
public enum SensorType
{
Radar = 0,
EO = 1,
IR = 2
}
// === 气溶胶 & 毁伤 ===
public enum AerosolType
{
@ -92,7 +97,7 @@ namespace CounterDrone.Core.Models
Destroyed = 3
}
// === 编队 & 编组 ===
// === 编队 & 批次 ===
public enum FormationMode
{
Single = 0,
@ -100,12 +105,6 @@ namespace CounterDrone.Core.Models
Swarm = 2
}
public enum GroupType
{
DroneFleet = 0,
EquipmentGroup = 1
}
// === 运行时 ===
public enum EntityType
{
@ -126,7 +125,8 @@ namespace CounterDrone.Core.Models
DroneReachedTarget = 5,
ZoneIntruded = 6,
WaypointReached = 7,
SimulationEnd = 8
SimulationEnd = 8,
PlanningFailed = 9
}
// === 报告 ===

View File

@ -0,0 +1,35 @@
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>环境规格(基础数据模板)</summary>
[Table("EnvironmentSpec")]
public class EnvironmentSpec
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int WeatherType { get; set; }
public double WindSpeed { get; set; }
public int WindDirection { get; set; }
public double Temperature { get; set; } = 20.0;
public double Humidity { get; set; } = 60.0;
public double Pressure { get; set; } = 1013.0;
public double Visibility { get; set; } = 5000.0;
public CombatScene ToCombatScene()
{
return new CombatScene
{
WeatherType = WeatherType,
WindSpeed = WindSpeed,
WindDirection = WindDirection,
Temperature = Temperature,
Humidity = Humidity,
Pressure = Pressure,
Visibility = Visibility,
};
}
}
}

View File

@ -1,44 +0,0 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤3装备部署</summary>
[Table("EquipmentDeployment")]
public class EquipmentDeployment
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform;
public int Quantity { get; set; } = 1;
public string GroupId { get; set; } = string.Empty;
// 发射平台专用
public int? PlatformType { get; set; }
public double PositionX { get; set; }
public double PositionY { get; set; }
public double PositionZ { get; set; }
public int? AerosolType { get; set; }
public int? MunitionCount { get; set; }
public int Source { get; set; } = (int)ConfigSource.Manual;
public double? MuzzleVelocity { get; set; }
public double? ReleaseAltitude { get; set; }
public double Cooldown { get; set; } = 5.0;
// 探测设备专用
public double? DetectionRadius { get; set; }
}
}

View File

@ -0,0 +1,52 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>火力单元规格(基础数据模板)</summary>
[Table("FireUnitSpec")]
public class FireUnitSpec
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int PlatformType { get; set; }
public int GunCount { get; set; } = 1;
public int ChannelsPerGun { get; set; } = 1;
public double ChannelInterval { get; set; } = 1.0;
public double Cooldown { get; set; } = 5.0;
public double AmmoChangeTime { get; set; } = 30.0;
public double MuzzleVelocity { get; set; }
public double CruiseSpeed { get; set; }
public double ReleaseAltitude { get; set; }
/// <summary>3D 模型 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

@ -0,0 +1,35 @@
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>编队模板</summary>
[Table("FormationTemplate")]
public class FormationTemplate
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public int FormationMode { get; set; }
public int LateralCount { get; set; } = 1;
public int LongitudinalCount { get; set; } = 1;
public double LateralSpacing { get; set; }
public double LongitudinalSpacing { get; set; }
public int LateralAxis { get; set; } = 2;
public int LongitudinalAxis { get; set; } = 0;
public RoutePlan ToRoutePlan()
{
return new RoutePlan
{
FormationMode = FormationMode,
LateralCount = LateralCount,
LongitudinalCount = LongitudinalCount,
LateralSpacing = LateralSpacing,
LongitudinalSpacing = LongitudinalSpacing,
LateralAxis = LateralAxis,
LongitudinalAxis = LongitudinalAxis,
};
}
}
}

View File

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

View File

@ -0,0 +1,44 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>搭载/发射平台规格(基础数据模板)</summary>
[Table("LaunchPlatformSpec")]
public class LaunchPlatformSpec
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public int PlatformType { get; set; }
public int GunCount { get; set; } = 1;
public int ChannelsPerGun { get; set; } = 1;
public double ChannelInterval { get; set; } = 1.0;
public double Cooldown { get; set; } = 5.0;
public double AmmoChangeTime { get; set; } = 30.0;
public double MuzzleVelocity { get; set; }
public double CruiseSpeed { get; set; }
public double ReleaseAltitude { get; set; }
public string ModelId { get; set; } = "";
[Ignore]
public List<int> AmmoTypes { get; set; } = new();
public ScenarioUnit ToScenarioUnit(AerosolType ammoType, int quantity,
double posX, double posY, double posZ)
{
return new ScenarioUnit
{
LaunchPlatformSpecId = Id,
EquipmentRole = (int)Models.EquipmentRole.LaunchPlatform,
Quantity = quantity,
PositionX = posX,
PositionY = posY,
PositionZ = posZ,
AerosolType = (int)ammoType,
MunitionCount = GunCount * ChannelsPerGun * quantity,
};
}
}
}

View File

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

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace CounterDrone.Core.Models
{
/// <summary>分页结果</summary>
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
}
}

View File

@ -3,7 +3,7 @@ using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤5航路规划 — 多编队支持:每个(任务,编队)一条航路</summary>
/// <summary>步骤5航路规划 — 多批次支持:每个(任务,批次)一条航路</summary>
[Table("RoutePlan")]
public class RoutePlan
{
@ -11,14 +11,28 @@ namespace CounterDrone.Core.Models
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string ScenarioId { get; set; } = string.Empty;
[Indexed]
public string GroupId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public int FormationMode { get; set; } = (int)Models.FormationMode.Single;
public double FormationSpacing { get; set; } = 50.0;
public double LateralSpacing { get; set; } = 50.0;
/// <summary>正面架数横向展开null=全部横向(=Quantity</summary>
public int? LateralCount { get; set; }
/// <summary>纵深架数串列跟随null=1无纵深</summary>
public int? LongitudinalCount { get; set; }
/// <summary>纵向串列间距 m</summary>
public double LongitudinalSpacing { get; set; } = 50.0;
/// <summary>横向展开轴0=X, 1=Y(垂直), 2=Z(默认)</summary>
public int LateralAxis { get; set; } = 2;
/// <summary>纵向串列轴0=X(默认), 1=Y, 2=Z</summary>
public int LongitudinalAxis { get; set; } = 0;
public string ETA { get; set; } = string.Empty;
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>航线模板(基础数据模板)</summary>
[Table("RouteTemplate")]
public class RouteTemplate
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string WaypointsJson { get; set; } = "[]";
[Ignore]
public List<WaypointCoord> Waypoints { get; set; } = new();
public List<Waypoint> ToWaypoints(double speed)
{
var list = new List<Waypoint>();
foreach (var w in Waypoints)
list.Add(new Waypoint
{
PosX = w.X, PosY = w.Y, PosZ = w.Z,
Altitude = w.Y, Speed = speed,
});
return list;
}
}
public class WaypointCoord
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
}

View File

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

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace CounterDrone.Core.Models
{
/// <summary>想定完整配置(聚合 5 步配置)</summary>
public class ScenarioConfig
{
public Scenario Info { get; set; } = new();
public CombatScene Scene { get; set; } = new();
public List<ControlZone> ControlZones { get; set; } = new();
public List<ScenarioDrone> Drones { get; set; } = new();
public List<ScenarioUnit> Units { get; set; } = new();
public CloudDispersal Cloud { get; set; } = new();
/// <summary>多批次航路(多批次支持)</summary>
public List<RoutePlan> Routes { get; set; } = new();
/// <summary>按 WaveId 分组的航路点</summary>
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
}
}

View File

@ -0,0 +1,24 @@
using System;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Models
{
/// <summary>想定数据:无人机批次</summary>
[Table("ScenarioDrone")]
public class ScenarioDrone
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string ScenarioId { get; set; } = string.Empty;
/// <summary>外键 → DroneSpec</summary>
public string DroneSpecId { get; set; } = string.Empty;
public string WaveId { get; set; } = string.Empty;
public int Quantity { get; set; } = 1;
}
}

View File

@ -0,0 +1,44 @@
using System;
using SQLite;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Models
{
/// <summary>想定数据:装备部署</summary>
[Table("ScenarioUnit")]
public class ScenarioUnit
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string ScenarioId { get; set; } = string.Empty;
/// <summary>外键 → LaunchPlatformSpec发射平台</summary>
public string LaunchPlatformSpecId { get; set; } = string.Empty;
/// <summary>外键 → SensorSpec探测设备</summary>
public string SensorSpecId { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int EquipmentRole { get; set; } = (int)Models.EquipmentRole.LaunchPlatform;
public int Quantity { get; set; } = 1;
public string WaveId { get; set; } = string.Empty;
public double PositionX { get; set; }
public double PositionY { get; set; }
public double PositionZ { get; set; }
public int? AerosolType { get; set; }
/// <summary>外键 → AmmunitionSpec挂载的弹药规格前端配置用</summary>
public string AmmunitionSpecId { get; set; } = string.Empty;
public int? MunitionCount { get; set; }
public int Source { get; set; } = (int)ConfigSource.Manual;
}
}

View File

@ -0,0 +1,46 @@
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>传感器规格(基础数据模板)</summary>
[Table("SensorSpec")]
public class SensorSpec
{
[PrimaryKey]
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public double RadarRange { get; set; }
public double EORange { get; set; }
public double IRRange { get; set; }
/// <summary>是否含雷达探测能力</summary>
public bool HasRadar { get; set; }
/// <summary>是否含光电探测能力</summary>
public bool HasEO { get; set; }
/// <summary>是否含红外探测能力</summary>
public bool HasIR { get; set; }
public double Accuracy { get; set; } = 50.0;
/// <summary>3D 模型 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

@ -1,28 +0,0 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>仿真事件(数据库记录)</summary>
[Table("SimEvent")]
public class SimEvent
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public double OccurredAt { get; set; }
public int EventType { get; set; }
public string SourceId { get; set; } = string.Empty;
public string TargetId { get; set; } = string.Empty;
public string DataJson { get; set; } = "{}";
public string Description { get; set; } = string.Empty;
}
}

View File

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

View File

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

View File

@ -1,30 +0,0 @@
using System;
using SQLite;
namespace CounterDrone.Core.Models
{
/// <summary>步骤2目标配置</summary>
[Table("TargetConfig")]
public class TargetConfig
{
[PrimaryKey]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Indexed]
public string TaskId { get; set; } = string.Empty;
public string GroupId { get; set; } = string.Empty;
public int TargetType { get; set; } = (int)Models.TargetType.Rotor;
public int Quantity { get; set; } = 1;
public int PowerType { get; set; } = (int)Models.PowerType.Electric;
public double Wingspan { get; set; } = 1.2;
public double TypicalSpeed { get; set; } = 60.0;
public double TypicalAltitude { get; set; } = 300.0;
}
}

View File

@ -1,29 +0,0 @@
using System.Collections.Generic;
namespace CounterDrone.Core.Models
{
/// <summary>想定完整配置(聚合 5 步配置)</summary>
public class TaskFullConfig
{
public SimTask Task { get; set; } = new();
public CombatScene Scene { get; set; } = new();
public List<ControlZone> ControlZones { get; set; } = new();
public List<TargetConfig> Targets { get; set; } = new();
public List<EquipmentDeployment> Equipment { get; set; } = new();
public CloudDispersal Cloud { get; set; } = new();
/// <summary>多编队航路(多批次支持)</summary>
public List<RoutePlan> Routes { get; set; } = new();
/// <summary>按 GroupId 分组的航路点</summary>
public Dictionary<string, List<Waypoint>> WaypointGroups { get; set; } = new();
}
/// <summary>分页结果</summary>
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
}
}

View File

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

View File

@ -0,0 +1,40 @@
using System;
using System.IO;
using PdfSharpCore.Fonts;
namespace CounterDrone.Core.Reporting
{
/// <summary>CJK 字体解析器 — 从文件路径加载 TrueType 字体供 PdfSharpCore 使用</summary>
public sealed class CjkFontResolver : IFontResolver
{
private readonly byte[] _fontData;
private readonly string _familyName;
/// <summary>注册 CJK 字体解析器到全局设置(进程内只能注册一次)</summary>
/// <param name="fontPath">CJK TrueType/OpenType 字体文件路径</param>
/// <param name="familyName">字体族名(用于 XFont 构造)</param>
public static void Register(string fontPath, string familyName)
{
if (string.IsNullOrEmpty(fontPath))
throw new ArgumentException("字体路径不能为空", nameof(fontPath));
if (!File.Exists(fontPath))
throw new FileNotFoundException($"字体文件不存在: {fontPath}", fontPath);
if (GlobalFontSettings.FontResolver == null)
GlobalFontSettings.FontResolver = new CjkFontResolver(fontPath, familyName);
}
private CjkFontResolver(string fontPath, string familyName)
{
_fontData = File.ReadAllBytes(fontPath);
_familyName = familyName;
}
public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
=> new FontResolverInfo(_familyName);
public byte[] GetFont(string faceName) => _fontData;
public string DefaultFontName => _familyName;
}
}

View File

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

View File

@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Text;
namespace CounterDrone.Core.Reporting
{
/// <summary>从 ReportData 渲染 Markdown 文本</summary>
public class MarkdownRenderer
{
public string Render(ReportData data)
{
var sb = new StringBuilder();
sb.AppendLine($"# {data.Title}");
sb.AppendLine();
AppendKeyValueTable(sb, "项目", "值", data.Header);
sb.AppendLine();
foreach (var section in data.Sections)
{
sb.AppendLine($"## {section.Title}");
sb.AppendLine();
foreach (var block in section.Blocks)
RenderBlock(sb, block);
sb.AppendLine();
}
return sb.ToString();
}
private void RenderBlock(StringBuilder sb, ReportBlock block)
{
switch (block)
{
case KeyValueBlock kv:
AppendKeyValueTable(sb, kv.LabelHeader, kv.ValueHeader, kv.Items);
sb.AppendLine();
break;
case TableBlock tb:
if (!string.IsNullOrEmpty(tb.Caption))
{
sb.AppendLine($"### {tb.Caption}");
sb.AppendLine();
}
AppendTable(sb, tb.Headers, tb.Rows);
sb.AppendLine();
break;
case TextBlock text:
if (text.Bold)
sb.AppendLine($"**{text.Text}**");
else
sb.AppendLine(text.Text);
sb.AppendLine();
break;
}
}
private static void AppendKeyValueTable(StringBuilder sb, string labelHeader, string valueHeader, List<MetaItem> items)
{
sb.AppendLine($"| {labelHeader} | {valueHeader} |");
sb.AppendLine($"|{new string('-', labelHeader.Length + 2)}|{new string('-', valueHeader.Length + 2)}|");
foreach (var item in items)
sb.AppendLine($"| {item.Label} | {item.Value} |");
}
private static void AppendTable(StringBuilder sb, string[] headers, List<string[]> rows)
{
sb.AppendLine($"| {string.Join(" | ", headers)} |");
sb.AppendLine($"|{string.Join("|", RepeatEach(headers, h => new string('-', h.Length + 2)))}|");
foreach (var row in rows)
sb.AppendLine($"| {string.Join(" | ", row)} |");
}
private static IEnumerable<string> RepeatEach(string[] source, System.Func<string, string> fn)
{
foreach (var s in source)
yield return fn(s);
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CounterDrone.Core.Reporting
{
/// <summary>ReportBlock 多态序列化转换器</summary>
public class ReportBlockConverter : JsonConverter<ReportBlock>
{
public override ReportBlock Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
if (!root.TryGetProperty("$blockType", out var typeProp))
throw new JsonException("缺少 $blockType 字段");
var typeName = typeProp.GetString();
var rawText = root.GetRawText();
return typeName switch
{
"KeyValueBlock" => JsonSerializer.Deserialize<KeyValueBlock>(rawText, options)!,
"TableBlock" => JsonSerializer.Deserialize<TableBlock>(rawText, options)!,
"TextBlock" => JsonSerializer.Deserialize<TextBlock>(rawText, options)!,
_ => throw new JsonException($"未知的 block 类型: {typeName}"),
};
}
public override void Write(Utf8JsonWriter writer, ReportBlock value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("$blockType", value.GetType().Name);
switch (value)
{
case KeyValueBlock kv:
writer.WriteString(nameof(kv.LabelHeader), kv.LabelHeader);
writer.WriteString(nameof(kv.ValueHeader), kv.ValueHeader);
writer.WritePropertyName(nameof(kv.Items));
JsonSerializer.Serialize(writer, kv.Items, options);
break;
case TableBlock tb:
writer.WriteString(nameof(tb.Caption), tb.Caption);
writer.WritePropertyName(nameof(tb.Headers));
JsonSerializer.Serialize(writer, tb.Headers, options);
writer.WritePropertyName(nameof(tb.Rows));
JsonSerializer.Serialize(writer, tb.Rows, options);
break;
case TextBlock text:
writer.WriteString(nameof(text.Text), text.Text);
writer.WriteBoolean(nameof(text.Bold), text.Bold);
break;
}
writer.WriteEndObject();
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace CounterDrone.Core.Reporting
{
/// <summary>结构化报告数据 — 模板渲染的统一数据源</summary>
public class ReportData
{
public string Title { get; set; } = "";
public List<MetaItem> Header { get; set; } = new();
public List<ReportSection> Sections { get; set; } = new();
}
public class MetaItem
{
public string Label { get; set; } = "";
public string Value { get; set; } = "";
}
public class ReportSection
{
public string Title { get; set; } = "";
public List<ReportBlock> Blocks { get; set; } = new();
}
public abstract class ReportBlock { }
/// <summary>键值对表(两列:参数 / 值)</summary>
public class KeyValueBlock : ReportBlock
{
public string LabelHeader { get; set; } = "参数";
public string ValueHeader { get; set; } = "值";
public List<MetaItem> Items { get; set; } = new();
}
/// <summary>通用表格(可带子标题)</summary>
public class TableBlock : ReportBlock
{
public string Caption { get; set; } = "";
public string[] Headers { get; set; } = Array.Empty<string>();
public List<string[]> Rows { get; set; } = new();
}
/// <summary>文本段落(支持加粗、多行列表)</summary>
public class TextBlock : ReportBlock
{
public string Text { get; set; } = "";
public bool Bold { get; set; }
}
}

View File

@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using PdfSharpCore;
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
namespace CounterDrone.Core.Reporting
{
/// <summary>标准 PDF 模板 — A4 排版,中文字体,表格边框,自动分页</summary>
public class StandardPdfTemplate : IReportTemplate
{
private readonly string _fontFamily;
private XFont _titleFont, _sectionFont, _tableHeaderFont, _tableCellFont, _textFont, _footerFont;
private const double MarginMm = 20;
private const double FooterMm = 15;
private static double MmToPt(double mm) => mm * 72.0 / 25.4;
private static readonly double MarginPt = MmToPt(MarginMm);
private static readonly XColor HeaderBgColor = XColor.FromArgb(240, 240, 240);
private static readonly XColor BorderColor = XColor.FromArgb(180, 180, 180);
private static readonly XColor TitleColor = XColor.FromArgb(30, 30, 30);
private static readonly XColor SectionColor = XColor.FromArgb(50, 50, 50);
public StandardPdfTemplate(string fontPath, string fontFamily)
{
CjkFontResolver.Register(fontPath, fontFamily);
_fontFamily = fontFamily;
_titleFont = new XFont(_fontFamily, 22, XFontStyle.Bold);
_sectionFont = new XFont(_fontFamily, 15, XFontStyle.Bold);
_tableHeaderFont = new XFont(_fontFamily, 11, XFontStyle.Bold);
_tableCellFont = new XFont(_fontFamily, 11, XFontStyle.Regular);
_textFont = new XFont(_fontFamily, 12, XFontStyle.Regular);
_footerFont = new XFont(_fontFamily, 9, XFontStyle.Regular);
}
public byte[] Render(ReportData data)
{
var doc = new PdfDocument();
doc.Info.Title = data.Title;
NewPage(doc);
DrawTitle(data.Title);
if (data.Header.Count > 0)
{
EnsureSpace(30);
DrawKeyValueTable(data.Header, "项目", "值");
_y += 16; // Header 后空行,与后续段落保持间距
}
foreach (var section in data.Sections)
{
EnsureSpace(40);
DrawSectionTitle(section.Title);
foreach (var block in section.Blocks)
RenderBlock(doc, block);
_y += 16; // 段落后空行
}
DrawFooter();
using var ms = new MemoryStream();
doc.Save(ms, false);
return ms.ToArray();
}
// ═════════════════ 页面管理 ═════════════════
private PdfPage _page = null!;
private XGraphics _gfx = null!;
private double _y;
private double _contentWidth;
private double _bottomMargin;
private int _pageNum;
private void NewPage(PdfDocument doc)
{
_page = doc.AddPage();
_page.Size = PageSize.A4;
_gfx = XGraphics.FromPdfPage(_page);
_y = MarginPt;
_contentWidth = _page.Width.Point - 2 * MarginPt;
_bottomMargin = _page.Height.Point - MmToPt(FooterMm);
_pageNum++;
}
private void EnsureSpace(double neededPt)
{
if (_y + neededPt > _bottomMargin)
{
DrawFooter();
NewPage(_page.Owner);
}
}
private void DrawFooter()
{
var footerY = _page.Height.Point - MmToPt(FooterMm / 2);
var fmt = new XStringFormat { Alignment = XStringAlignment.Center };
_gfx.DrawString($"第 {_pageNum} 页", _footerFont, XBrushes.Gray,
new XRect(0, footerY, _page.Width.Point, 20), fmt);
}
// ═════════════════ 内容渲染 ═════════════════
private void DrawTitle(string title)
{
var fmt = new XStringFormat { Alignment = XStringAlignment.Center };
var h = _titleFont.GetHeight() + 10;
_gfx.DrawString(title, _titleFont, new XSolidBrush(TitleColor),
new XRect(MarginPt, _y, _contentWidth, h), fmt);
_y += h + 4;
}
private void DrawSectionTitle(string title)
{
var h = _sectionFont.GetHeight() + 6;
_gfx.DrawString(title, _sectionFont, new XSolidBrush(SectionColor),
new XRect(MarginPt, _y, _contentWidth, h),
new XStringFormat { Alignment = XStringAlignment.Near, LineAlignment = XLineAlignment.Near });
_y += h;
}
private void RenderBlock(PdfDocument doc, ReportBlock block)
{
switch (block)
{
case KeyValueBlock kv:
DrawKeyValueTable(kv.Items, kv.LabelHeader, kv.ValueHeader);
_y += 4;
break;
case TableBlock tb:
if (!string.IsNullOrEmpty(tb.Caption))
{
EnsureSpace(20);
_gfx.DrawString(tb.Caption, _textFont, XBrushes.Black,
new XRect(MarginPt, _y, _contentWidth, 16),
new XStringFormat { Alignment = XStringAlignment.Near, LineAlignment = XLineAlignment.Near });
_y += 18;
}
DrawTable(tb.Headers, tb.Rows);
_y += 4;
break;
case TextBlock text:
RenderTextBlock(text);
_y += 4;
break;
}
}
private void DrawKeyValueTable(List<MetaItem> items, string labelHeader, string valueHeader)
{
double labelW = _contentWidth * 0.35;
double valueW = _contentWidth - labelW;
double rowH = 22;
double x = MarginPt;
// header row
EnsureSpace(rowH);
_gfx.DrawRectangle(new XSolidBrush(HeaderBgColor), x, _y, _contentWidth, rowH);
_gfx.DrawCellText(labelHeader, _tableHeaderFont, x + 6, _y, labelW, rowH);
_gfx.DrawCellText(valueHeader, _tableHeaderFont, x + labelW + 6, _y, valueW, rowH);
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, rowH);
_gfx.DrawLine(new XPen(BorderColor), x + labelW, _y, x + labelW, _y + rowH);
_y += rowH;
// data rows
foreach (var item in items)
{
var wrapped = WrapText(item.Value, valueW - 12, _tableCellFont);
double lineHeight = _tableCellFont.GetHeight();
rowH = Math.Max(22, wrapped.Count * lineHeight + 8);
EnsureSpace(rowH);
_gfx.DrawCellText(item.Label, _tableCellFont, x + 6, _y, labelW, rowH, true);
int lines = wrapped.Count;
if (lines == 1)
{
_gfx.DrawCellText(wrapped[0], _tableCellFont, x + labelW + 6, _y, valueW, rowH, true);
}
else
{
double yOffset = (rowH - lines * lineHeight) / 2;
for (int i = 0; i < lines; i++)
_gfx.DrawCellText(wrapped[i], _tableCellFont, x + labelW + 6,
_y + yOffset + i * lineHeight, valueW, lineHeight, false);
}
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, rowH);
_gfx.DrawLine(new XPen(BorderColor), x + labelW, _y, x + labelW, _y + rowH);
_y += rowH;
}
}
private void DrawTable(string[] headers, List<string[]> rows)
{
double x = MarginPt;
var colWidths = CalculateColumnWidths(headers, rows, _contentWidth);
double headerH = 22;
EnsureSpace(headerH);
_gfx.DrawRectangle(new XSolidBrush(HeaderBgColor), x, _y, _contentWidth, headerH);
double cx = x;
for (int i = 0; i < headers.Length; i++)
{
_gfx.DrawCellText(headers[i], _tableHeaderFont, cx + 4, _y, colWidths[i], headerH);
cx += colWidths[i];
}
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, headerH);
cx = x;
for (int i = 0; i < headers.Length - 1; i++)
{
cx += colWidths[i];
_gfx.DrawLine(new XPen(BorderColor), cx, _y, cx, _y + headerH);
}
_y += headerH;
foreach (var row in rows)
{
var wrappedCells = new List<string>[row.Length];
int maxLines = 1;
for (int i = 0; i < row.Length; i++)
{
wrappedCells[i] = WrapText(row[i], colWidths[i] - 8, _tableCellFont);
maxLines = Math.Max(maxLines, wrappedCells[i].Count);
}
double lineHeight = _tableCellFont.GetHeight();
double rowH = Math.Max(22, maxLines * lineHeight + 8);
EnsureSpace(rowH);
cx = x;
for (int i = 0; i < row.Length; i++)
{
int lines = wrappedCells[i].Count;
if (lines == 1)
{
_gfx.DrawCellText(wrappedCells[i][0], _tableCellFont, cx + 4, _y, colWidths[i], rowH, true);
}
else
{
double yOffset = (rowH - lines * lineHeight) / 2;
for (int l = 0; l < lines; l++)
_gfx.DrawCellText(wrappedCells[i][l], _tableCellFont, cx + 4,
_y + yOffset + l * lineHeight, colWidths[i], lineHeight, false);
}
cx += colWidths[i];
}
_gfx.DrawRectangle(new XPen(BorderColor), x, _y, _contentWidth, rowH);
cx = x;
for (int i = 0; i < headers.Length - 1; i++)
{
cx += colWidths[i];
_gfx.DrawLine(new XPen(BorderColor), cx, _y, cx, _y + rowH);
}
_y += rowH;
}
}
private void RenderTextBlock(TextBlock block)
{
var font = block.Bold
? new XFont(_fontFamily, 12, XFontStyle.Bold)
: _textFont;
var lines = block.Text.Split('\n');
foreach (var line in lines)
{
var wrapped = WrapText(line, _contentWidth, font);
foreach (var wl in wrapped)
{
EnsureSpace(16);
_gfx.DrawString(wl, font, XBrushes.Black,
MarginPt, _y + 12);
_y += 14;
}
}
_y += 2;
}
// ═════════════════ 辅助方法 ═════════════════
private double[] CalculateColumnWidths(string[] headers, List<string[]> rows, double totalWidth)
{
int n = headers.Length;
var maxW = new double[n];
for (int i = 0; i < n; i++)
maxW[i] = _gfx.MeasureString(headers[i], _tableHeaderFont).Width;
foreach (var row in rows)
for (int i = 0; i < n && i < row.Length; i++)
maxW[i] = Math.Max(maxW[i], _gfx.MeasureString(row[i], _tableCellFont).Width);
const double padding = 8;
for (int i = 0; i < n; i++)
maxW[i] += padding * 2;
double sum = maxW.Sum();
if (sum > totalWidth)
{
double scale = totalWidth / sum;
for (int i = 0; i < n; i++)
maxW[i] *= scale;
}
return maxW;
}
private List<string> WrapText(string text, double maxWidth, XFont font)
{
var result = new List<string>();
if (string.IsNullOrEmpty(text))
{
result.Add("");
return result;
}
var paragraphs = text.Split('\n');
foreach (var para in paragraphs)
{
if (string.IsNullOrEmpty(para))
{
result.Add("");
continue;
}
var words = para.Split(' ');
var current = "";
foreach (var word in words)
{
var test = current == "" ? word : current + " " + word;
if (_gfx.MeasureString(test, font).Width <= maxWidth)
current = test;
else
{
if (current != "") result.Add(current);
if (_gfx.MeasureString(word, font).Width > maxWidth)
{
current = "";
foreach (var ch in word)
{
var test2 = current + ch;
if (_gfx.MeasureString(test2, font).Width <= maxWidth)
current = test2;
else
{
result.Add(current);
current = ch.ToString();
}
}
}
else
current = word;
}
}
if (current != "") result.Add(current);
}
return result;
}
}
internal static class GfxExtensions
{
public static void DrawCellText(this XGraphics gfx, string text, XFont font,
double x, double y, double width, double height, bool centerVertical = true)
{
var rect = new XRect(x, y, width, height);
var fmt = new XStringFormat
{
Alignment = XStringAlignment.Near,
LineAlignment = centerVertical ? XLineAlignment.Center : XLineAlignment.Near,
};
gfx.DrawString(text, font, XBrushes.Black, rect, fmt);
}
}
}

View File

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

View File

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

View File

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

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 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

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

View File

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

View File

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

View File

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

View File

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

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 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

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

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Repository
{
public class ScenarioUnitRepository : BaseRepository<ScenarioUnit>
{
public ScenarioUnitRepository(SQLiteConnection db) : base(db) { }
public List<ScenarioUnit> GetByScenarioId(string scenarioId)
{
return Db.Table<ScenarioUnit>().Where(e => e.ScenarioId == scenarioId).ToList();
}
/// <summary>按任务和角色查询装备EquipmentRole: 0=Detection, 1=LaunchPlatform</summary>
public List<ScenarioUnit> GetByScenarioIdAndRole(string scenarioId, int equipmentRole)
{
return Db.Table<ScenarioUnit>()
.Where(e => e.ScenarioId == scenarioId && e.EquipmentRole == equipmentRole)
.ToList();
}
public void DeleteByScenarioId(string scenarioId)
{
var equips = GetByScenarioId(scenarioId);
foreach (var e in equips)
Db.Delete(e);
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Text.Json;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using SQLite;
namespace CounterDrone.Core.Services
{
/// <summary>配置服务实现 — 读写 JSON 配置文件,重载时触发事件通知消费方刷新</summary>
public class ConfigService : IConfigService
{
private readonly IPathProvider _paths;
private readonly SQLiteConnection _db;
/// <summary>planner 配置重载后触发(消费方应重建 DefensePlanner</summary>
public event Action<PlannerConfig>? PlannerConfigReloaded;
/// <summary>默认数据重载后触发(消费方应刷新缓存)</summary>
public event Action<DefaultData>? DefaultsReloaded;
public ConfigService(IPathProvider paths, SQLiteConnection db)
{
_paths = paths;
_db = db;
}
public string GetPlannerConfigJson()
{
var path = Path.Combine(_paths.GetDataRoot(), "planner_config.json");
if (!File.Exists(path))
throw new FileNotFoundException($"planner 配置文件不存在: {path}");
return File.ReadAllText(path);
}
public void SavePlannerConfig(string json)
{
var path = Path.Combine(_paths.GetDataRoot(), "planner_config.json");
File.WriteAllText(path, json);
ReloadPlannerConfig();
}
public string GetDefaultsJson()
{
var path = Path.Combine(_paths.GetDataRoot(), "defaults.json");
if (!File.Exists(path))
throw new FileNotFoundException($"默认数据文件不存在: {path}");
return File.ReadAllText(path);
}
public void SaveDefaults(string json)
{
var path = Path.Combine(_paths.GetDataRoot(), "defaults.json");
File.WriteAllText(path, json);
ReloadDefaults();
}
public void Reload()
{
ReloadPlannerConfig();
ReloadDefaults();
}
private void ReloadPlannerConfig()
{
var config = PlannerConfig.Load(_paths);
PlannerConfigReloaded?.Invoke(config);
}
private void ReloadDefaults()
{
var defaults = DefaultData.Load(_paths);
ReseedDatabase(defaults);
DefaultsReloaded?.Invoke(defaults);
}
private void ReseedDatabase(DefaultData defaults)
{
foreach (var a in defaults.Ammunition) _db.InsertOrReplace(a);
foreach (var f in defaults.FireUnits) _db.InsertOrReplace(f);
foreach (var l in defaults.LaunchPlatforms) _db.InsertOrReplace(l);
foreach (var d in defaults.Drones) _db.InsertOrReplace(d);
foreach (var s in defaults.Sensors) _db.InsertOrReplace(s);
foreach (var e in defaults.Environments) _db.InsertOrReplace(e);
foreach (var f in defaults.Formations) _db.InsertOrReplace(f);
foreach (var r in defaults.Routes)
{
r.WaypointsJson = JsonSerializer.Serialize(r.Waypoints);
_db.InsertOrReplace(r);
}
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
namespace CounterDrone.Core.Services
{
public class DataService : IDataService
{
private readonly AmmunitionSpecRepository _ammoRepo;
private readonly FireUnitSpecRepository _fireUnitRepo;
private readonly DroneSpecRepository _droneRepo;
private readonly SensorSpecRepository _sensorRepo;
private readonly EnvironmentSpecRepository _envRepo;
private readonly FormationTemplateRepository _formationRepo;
private readonly RouteTemplateRepository _routeRepo;
public DataService(AmmunitionSpecRepository ammoRepo, FireUnitSpecRepository fireUnitRepo,
DroneSpecRepository droneRepo, SensorSpecRepository sensorRepo,
EnvironmentSpecRepository envRepo, FormationTemplateRepository formationRepo,
RouteTemplateRepository routeRepo)
{
_ammoRepo = ammoRepo;
_fireUnitRepo = fireUnitRepo;
_droneRepo = droneRepo;
_sensorRepo = sensorRepo;
_envRepo = envRepo;
_formationRepo = formationRepo;
_routeRepo = routeRepo;
}
// ═══ AmmunitionSpec ═══
public List<AmmunitionSpec> GetAllAmmo() => _ammoRepo.GetAll();
public AmmunitionSpec GetAmmo(string id) => _ammoRepo.GetById(id);
public void SaveAmmo(AmmunitionSpec spec) { if (_ammoRepo.GetById(spec.Id) != null) _ammoRepo.Update(spec); else _ammoRepo.Insert(spec); }
public void DeleteAmmo(string id) => _ammoRepo.Delete(id);
// ═══ FireUnitSpec ═══
public List<FireUnitSpec> GetAllFireUnits() => _fireUnitRepo.GetAll();
public FireUnitSpec GetFireUnit(string id) => _fireUnitRepo.GetById(id);
public void SaveFireUnit(FireUnitSpec spec) { if (_fireUnitRepo.GetById(spec.Id) != null) _fireUnitRepo.Update(spec); else _fireUnitRepo.Insert(spec); }
public void DeleteFireUnit(string id) => _fireUnitRepo.Delete(id);
// ═══ DroneSpec ═══
public List<DroneSpec> GetAllDrones() => _droneRepo.GetAll();
public DroneSpec GetDrone(string id) => _droneRepo.GetById(id);
public void SaveDrone(DroneSpec spec) { if (_droneRepo.GetById(spec.Id) != null) _droneRepo.Update(spec); else _droneRepo.Insert(spec); }
public void DeleteDrone(string id) => _droneRepo.Delete(id);
// ═══ SensorSpec ═══
public List<SensorSpec> GetAllSensors() => _sensorRepo.GetAll();
public SensorSpec GetSensor(string id) => _sensorRepo.GetById(id);
public void SaveSensor(SensorSpec spec) { if (_sensorRepo.GetById(spec.Id) != null) _sensorRepo.Update(spec); else _sensorRepo.Insert(spec); }
public void DeleteSensor(string id) => _sensorRepo.Delete(id);
// ═══ EnvironmentSpec ═══
public List<EnvironmentSpec> GetAllEnvironments() => _envRepo.GetAll();
public EnvironmentSpec GetEnvironment(string id) => _envRepo.GetById(id);
public void SaveEnvironment(EnvironmentSpec spec) { if (_envRepo.GetById(spec.Id) != null) _envRepo.Update(spec); else _envRepo.Insert(spec); }
public void DeleteEnvironment(string id) => _envRepo.Delete(id);
// ═══ FormationTemplate ═══
public List<FormationTemplate> GetAllFormations() => _formationRepo.GetAll();
public FormationTemplate GetFormation(string id) => _formationRepo.GetById(id);
public void SaveFormation(FormationTemplate spec) { if (_formationRepo.GetById(spec.Id) != null) _formationRepo.Update(spec); else _formationRepo.Insert(spec); }
public void DeleteFormation(string id) => _formationRepo.Delete(id);
// ═══ RouteTemplate ═══
public List<RouteTemplate> GetAllRoutes() => _routeRepo.GetAll();
public RouteTemplate GetRoute(string id) => _routeRepo.GetById(id);
public void SaveRoute(RouteTemplate spec) { if (_routeRepo.GetById(spec.Id) != null) _routeRepo.Update(spec); else _routeRepo.Insert(spec); }
public void DeleteRoute(string id) => _routeRepo.Delete(id);
}
}

View File

@ -0,0 +1,46 @@
namespace CounterDrone.Core.Services
{
/// <summary>防御推荐方案 — 配置阶段调用 planner 生成,供前端"一键应用"</summary>
public class DefenseRecommendation
{
/// <summary>最佳推荐参数(拦截概率最高的抛撒参数)</summary>
public RecommendOption Best { get; set; } = new();
/// <summary>方案摘要(含失败原因,如有)</summary>
public string Summary { get; set; } = "";
/// <summary>是否规划成功</summary>
public bool Success { get; set; }
}
/// <summary>推荐参数档位</summary>
public class RecommendOption
{
/// <summary>推荐抛撒位置 X</summary>
public double PosX { get; set; }
/// <summary>推荐抛撒位置 Y高度</summary>
public double PosY { get; set; }
/// <summary>推荐抛撒位置 Z</summary>
public double PosZ { get; set; }
/// <summary>推荐抛撒时机(仿真秒)</summary>
public double Timing { get; set; }
/// <summary>推荐弹药数(齐射发数)</summary>
public int SalvoRounds { get; set; }
/// <summary>云团展开间距(米)</summary>
public double SalvoSpacing { get; set; }
/// <summary>估计拦截概率0~1</summary>
public double EstimatedProbability { get; set; }
/// <summary>已拦截威胁数</summary>
public int ThreatsEngaged { get; set; }
/// <summary>未拦截威胁数</summary>
public int ThreatsUnengaged { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace CounterDrone.Core.Services
{
/// <summary>枚举条目(前端下拉框数据源)</summary>
public class EnumItem
{
public string Name { get; set; } = "";
public string ChineseName { get; set; } = "";
public int Value { get; set; }
}
/// <summary>枚举元数据(中英文对照)</summary>
public class EnumMetadata
{
public List<EnumItem> DroneType { get; set; } = new();
public List<EnumItem> PowerType { get; set; } = new();
public List<EnumItem> PlatformType { get; set; } = new();
public List<EnumItem> AerosolType { get; set; } = new();
public List<EnumItem> WeatherType { get; set; } = new();
public List<EnumItem> WindDirection { get; set; } = new();
public List<EnumItem> SceneType { get; set; } = new();
public List<EnumItem> FormationMode { get; set; } = new();
public List<EnumItem> TriggerMode { get; set; } = new();
public List<EnumItem> ReleaseMode { get; set; } = new();
public List<EnumItem> EquipmentRole { get; set; } = new();
public List<EnumItem> EntityType { get; set; } = new();
public List<EnumItem> SensorType { get; set; } = new();
}
}

Some files were not shown because too many files have changed in this diff Show More