CounterDroneBackend/src/CounterDrone.Core/Services/ReportGenerator.cs
tian 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

315 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CounterDrone.Core.Models;
using CounterDrone.Core.Simulation;
namespace CounterDrone.Core.Services
{
public class ReportGenerator
{
public string Generate(TaskFullConfig config, List<SimEvent> events,
Simulation.DroneStatus finalDroneStatus, float simulationDuration)
{
var sb = new StringBuilder();
var platforms = config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList();
var target = config.Targets.FirstOrDefault();
var scene = config.Scene;
var destroyed = events.Count(e => e.Type == SimEventType.DroneDestroyed);
var reached = events.Count(e => e.Type == SimEventType.DroneReachedTarget);
var intruded = events.Count(e => e.Type == SimEventType.ZoneIntruded);
var totalDrones = config.Targets.Sum(t => t.Quantity);
var result = DetermineInterceptResult(destroyed, reached + intruded, totalDrones);
sb.AppendLine("# 仿真评估报告");
sb.AppendLine();
sb.AppendLine($"| 项目 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 任务名称 | {config.Task.Name} |");
sb.AppendLine($"| 任务编号 | {config.Task.TaskNumber} |");
sb.AppendLine($"| 仿真耗时 | {simulationDuration:F1} 秒 |");
sb.AppendLine($"| 气溶胶类型 | {TranslateAerosol((AerosolType)config.Cloud.AerosolType)} |");
sb.AppendLine();
// === 一、仿真结果 ===
sb.AppendLine("## 一、仿真结果");
sb.AppendLine();
sb.AppendLine($"| 统计项 | 值 |");
sb.AppendLine($"|--------|-----|");
sb.AppendLine($"| 无人机总数 | {totalDrones} |");
sb.AppendLine($"| 被摧毁 | {destroyed} |");
sb.AppendLine($"| 到达目标 | {reached} |");
sb.AppendLine($"| 侵入管控区 | {intruded} |");
sb.AppendLine($"| 弹药发射 | {events.Count(e => e.Type == SimEventType.MunitionLaunched)} |");
sb.AppendLine($"| 云团生成 | {events.Count(e => e.Type == SimEventType.CloudGenerated)} |");
sb.AppendLine();
sb.AppendLine($"**对抗结果**{TranslateResult(result)}");
sb.AppendLine();
// === 二、作战环境 ===
sb.AppendLine("## 二、作战环境");
sb.AppendLine();
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 场景类型 | {TranslateScene((SceneType)scene.SceneType)} |");
sb.AppendLine($"| 时间段 | {scene.TimeOfDay} |");
sb.AppendLine($"| 天气 | {TranslateWeather((WeatherType)scene.WeatherType)} |");
sb.AppendLine($"| 风速 | {scene.WindSpeed:F1} m/s |");
sb.AppendLine($"| 风向 | {TranslateWindDirection((WindDirection)scene.WindDirection)} |");
sb.AppendLine($"| 温度 | {scene.Temperature:F1}°C |");
sb.AppendLine($"| 湿度 | {scene.Humidity:F0}%RH |");
sb.AppendLine($"| 气压 | {scene.Pressure:F0} hPa |");
sb.AppendLine($"| 能见度 | {scene.Visibility:F0} m |");
sb.AppendLine($"| 场景范围 | {scene.SceneWidth:F0} × {scene.SceneLength:F0} × {scene.SceneHeight:F0} m (W×L×H) |");
sb.AppendLine();
// === 三、威胁目标 ===
sb.AppendLine("## 三、威胁目标");
sb.AppendLine();
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 类型 | {TranslateTarget((TargetType)(target?.TargetType ?? 0))} |");
sb.AppendLine($"| 动力 | {TranslatePower((PowerType)(target?.PowerType ?? 0))} |");
sb.AppendLine($"| 数量 | {totalDrones} 架 |");
sb.AppendLine($"| 翼展 | {target?.Wingspan ?? 0:F1} m |");
sb.AppendLine($"| 速度 | {target?.TypicalSpeed ?? 0:F0} km/h |");
sb.AppendLine($"| 高度 | {target?.TypicalAltitude ?? 0:F0} m |");
sb.AppendLine();
// === 四、云团抛撒配置 ===
sb.AppendLine("## 四、云团抛撒配置");
sb.AppendLine();
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 气溶胶类型 | {TranslateAerosol((AerosolType)config.Cloud.AerosolType)} |");
sb.AppendLine($"| 抛撒高度 | {config.Cloud.DisperseHeight:F0} m |");
sb.AppendLine($"| 触发模式 | {TranslateTrigger((TriggerMode)config.Cloud.TriggerMode)} |");
sb.AppendLine($"| 释放方式 | {TranslateRelease((ReleaseMode)config.Cloud.ReleaseMode)} |");
sb.AppendLine($"| 扩散方向 | {config.Cloud.SpreadAngle:F0}° |");
sb.AppendLine($"| 初始规模 | {config.Cloud.InitialScale:F0} m³ |");
sb.AppendLine($"| 持续时间 | {config.Cloud.Duration:F0} s |");
sb.AppendLine($"| 释放总量 | {config.Cloud.TotalAmount:F1} kg |");
sb.AppendLine($"| 配置来源 | {config.Cloud.Source} |");
if (config.Cloud.Source == "Algorithm" && config.Cloud.EstimatedProbability.HasValue)
{
sb.AppendLine($"| **算法推荐** | |");
sb.AppendLine($"| 推荐弹药数 | {config.Cloud.SalvoRounds} 发 |");
sb.AppendLine($"| 云团间距 | {config.Cloud.SalvoSpacing:F0} m |");
sb.AppendLine($"| 拦截概率估计 | {config.Cloud.EstimatedProbability.Value:P0} |");
sb.AppendLine($"| 抛撒时机 | {config.Cloud.RecommendedTiming:F0}s |");
}
sb.AppendLine();
// 构建 PlatformId → 火力单元 映射(去重,五和六共用)
var platToEquip = new Dictionary<string, int>();
var unitLaunchCounts = new Dictionary<int, int>();
int platIdx = 1, unitNum = 0;
for (int ei = 0; ei < config.Equipment.Count; ei++)
{
var eq = config.Equipment[ei];
if (eq.EquipmentRole != (int)EquipmentRole.LaunchPlatform) continue;
int channels = (eq.GunCount ?? 1) * (eq.ChannelsPerGun ?? 1);
for (int j = 0; j < eq.Quantity; j++)
{
unitNum++;
for (int c = 0; c < channels; c++)
platToEquip[$"plat_{platIdx++}"] = unitNum;
}
}
foreach (var e in events.Where(e => e.Type == SimEventType.MunitionLaunched))
{
if (platToEquip.TryGetValue(e.SourceId, out var u))
unitLaunchCounts[u] = unitLaunchCounts.GetValueOrDefault(u, 0) + 1;
}
// === 五、我方部署 ===
sb.AppendLine("## 五、我方部署");
sb.AppendLine();
sb.AppendLine($"### 发射平台");
sb.AppendLine();
sb.AppendLine($"| # | 平台类型 | 位置 (X,Y,Z) | 弹药类型 | 挂载量 | 发射数 |");
sb.AppendLine($"|---|---------|-------------|---------|:------:|:------:|");
int unitNum2 = 0;
foreach (var eq in config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
{
int channels = (eq.GunCount ?? 1) * (eq.ChannelsPerGun ?? 1);
for (int j = 0; j < eq.Quantity; j++)
{
unitNum2++;
int fired = unitLaunchCounts.GetValueOrDefault(unitNum2, 0);
string ptype = ((PlatformType?)(eq.PlatformType ?? 0)) == PlatformType.AirBased ? "空基" : "地基";
string ammo = eq.AerosolType.HasValue ? TranslateAerosol((AerosolType)eq.AerosolType) : "-";
sb.AppendLine($"| #{unitNum2} | {ptype} | ({eq.PositionX + j * 50:F0}, {eq.PositionY:F0}, {eq.PositionZ:F0}) | {ammo} | {eq.MunitionCount ?? channels} | {fired} |");
}
}
sb.AppendLine();
// 探测设备
var detections = config.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.Detection).ToList();
if (detections.Count > 0)
{
sb.AppendLine($"### 探测设备");
sb.AppendLine();
sb.AppendLine($"| # | 位置 (X,Y,Z) | 探测半径 | 数量 |");
sb.AppendLine($"|---|-------------|---------|:----:|");
int detIdx = 0;
foreach (var d in detections)
{
for (int j = 0; j < d.Quantity; j++)
sb.AppendLine($"| D{++detIdx} | ({d.PositionX:F0}, {d.PositionY:F0}, {d.PositionZ:F0}) | {(d.DetectionRadius ?? 0):F0} m | 1 |");
}
sb.AppendLine();
}
// 管控区域
if (config.ControlZones.Count > 0)
{
sb.AppendLine($"### 管控区域");
sb.AppendLine();
sb.AppendLine($"| # | 名称 | 高度范围 |");
sb.AppendLine($"|---|------|---------|");
int zIdx = 0;
foreach (var z in config.ControlZones)
sb.AppendLine($"| Z{++zIdx} | {z.Name} | {(int)z.MinAltitude}-{(int)z.MaxAltitude} m |");
sb.AppendLine();
}
// === 六、事件时序 ===
sb.AppendLine("## 六、事件时序");
sb.AppendLine();
sb.AppendLine("| 时间(s) | 事件 | 详情 | 火力单元 |");
sb.AppendLine("|---------|------|------|:------:|");
int munitionIdx = 0, cloudIdx = 0;
foreach (var e in events.OrderBy(e => e.OccurredAt))
{
string unitCell = e.Type == SimEventType.MunitionLaunched && platToEquip.TryGetValue(e.SourceId, out var u)
? $"#{u}" : "-";
// 编号前缀 + 引擎生成的 Description含坐标等调试信息
string prefix = e.Type switch
{
SimEventType.MunitionLaunched => $"第 {++munitionIdx} 发",
SimEventType.CloudGenerated => $"云团 #{++cloudIdx}",
_ => string.Empty,
};
string desc = string.IsNullOrEmpty(prefix) ? e.Description : $"{prefix} · {e.Description}";
sb.AppendLine($"| {e.OccurredAt:F2} | {TranslateEvent(e.Type)} | {desc} | {unitCell} |");
}
sb.AppendLine();
// === 七、时间线 ===
var launches = events.Where(e => e.Type == SimEventType.MunitionLaunched).Select(e => e.OccurredAt).OrderBy(t => t).ToList();
var clouds = events.Where(e => e.Type == SimEventType.CloudGenerated).Select(e => e.OccurredAt).OrderBy(t => t).ToList();
var destroys = events.Where(e => e.Type == SimEventType.DroneDestroyed).Select(e => e.OccurredAt).OrderBy(t => t).ToList();
float launchFirst = launches.DefaultIfEmpty(0).FirstOrDefault();
float launchLast = launches.DefaultIfEmpty(0).LastOrDefault();
float cloudFirst = clouds.DefaultIfEmpty(0).FirstOrDefault();
float destroyFirst = destroys.DefaultIfEmpty(0).FirstOrDefault();
sb.AppendLine("## 七、时间线");
sb.AppendLine();
sb.AppendLine($"| 阶段 | 时间 | 耗时 |");
sb.AppendLine($"|------|------|------|");
if (launchFirst > 0)
{
sb.AppendLine($"| 首发发射 | {launchFirst:F1}s | - |");
if (launches.Count > 1)
sb.AppendLine($"| 末发发射 | {launchLast:F1}s | {launchLast - launchFirst:F1}s弹群展开 |");
sb.AppendLine($"| 发射 → 云团生成 | {launchFirst:F1}s → {cloudFirst:F1}s | {cloudFirst - launchFirst:F1}s弹药飞行 |");
}
if (destroyFirst > 0)
sb.AppendLine($"| 云团生成 → 首个击毁 | {cloudFirst:F1}s → {destroyFirst:F1}s | {destroyFirst - cloudFirst:F1}s暴露毁伤 |");
sb.AppendLine($"| 总耗时 | 0 → {simulationDuration:F1}s | {simulationDuration:F1}s |");
sb.AppendLine();
// 汇总统计(调试用)
sb.AppendLine("**统计汇总**");
sb.AppendLine($"- 弹药发射 {launches.Count} 发,云团生成 {clouds.Count} 朵,击毁 {destroys.Count} 架");
if (launchFirst > 0 && launchLast > 0)
sb.AppendLine($"- 发射窗口 {launchLast - launchFirst:F1}s弹药飞行 {(cloudFirst - launchFirst):F1}s");
sb.AppendLine();
return sb.ToString();
}
public InterceptResult DetermineInterceptResult(int destroyed, int reachedOrIntruded, int total)
{
if (total == 0) return InterceptResult.Failed;
if (destroyed >= total) return InterceptResult.Success;
if (destroyed > 0) return InterceptResult.Partial;
return InterceptResult.Failed;
}
private static string TranslateResult(InterceptResult r) => r switch
{
InterceptResult.Success => "✅ 成功拦截",
InterceptResult.Partial => "⚠️ 部分拦截",
InterceptResult.Failed => "❌ 拦截失败",
_ => "未知",
};
private static string TranslateAerosol(AerosolType t) => t switch
{
AerosolType.InertGas => "惰性气体(吸入式灭火)",
AerosolType.ActiveMaterial => "活性材料(爆燃式)",
AerosolType.ActiveFuel => "活性燃料(吸入式爆炸)",
_ => t.ToString(),
};
private static string TranslateWeather(WeatherType w) => w switch
{
WeatherType.Sunny => "晴天", WeatherType.Overcast => "阴天",
WeatherType.Fog => "雾天", WeatherType.Rain => "雨天",
WeatherType.Night => "夜间", _ => w.ToString(),
};
private static string TranslateWindDirection(WindDirection d) => d switch
{
WindDirection.N => "北 (N)", WindDirection.NE => "东北 (NE)",
WindDirection.E => "东 (E)", WindDirection.SE => "东南 (SE)",
WindDirection.S => "南 (S)", WindDirection.SW => "西南 (SW)",
WindDirection.W => "西 (W)", WindDirection.NW => "西北 (NW)",
_ => d.ToString(),
};
private static string TranslateScene(SceneType s) => s switch
{
SceneType.Urban => "城市", SceneType.Plain => "平原",
SceneType.Mountain => "山区", SceneType.Coast => "海岸",
_ => s.ToString(),
};
private static string TranslateTrigger(TriggerMode t) => t switch
{
TriggerMode.Time => "时间触发", TriggerMode.Area => "区域触发",
TriggerMode.Manual => "手动触发", _ => t.ToString(),
};
private static string TranslateTarget(TargetType t) => t switch
{
TargetType.Rotor => "旋翼", TargetType.FixedWing => "固定翼",
TargetType.Electric => "电推", TargetType.Piston => "活塞",
TargetType.HighSpeed => "高速目标", _ => t.ToString(),
};
private static string TranslatePower(PowerType p) => p switch
{
PowerType.Electric => "电推式", PowerType.Piston => "活塞式",
PowerType.Jet => "喷吸气式", _ => p.ToString(),
};
private static string TranslateRelease(ReleaseMode r) => r switch
{
ReleaseMode.Single => "单次齐射", ReleaseMode.Continuous => "连续释放",
ReleaseMode.Pulse => "脉冲释放", _ => r.ToString(),
};
private static string TranslateEvent(SimEventType t) => t switch
{
SimEventType.MunitionLaunched => "🚀 弹药发射",
SimEventType.CloudGenerated => "☁️ 云团生成",
SimEventType.DroneDestroyed => "💥 目标击毁",
SimEventType.DroneReachedTarget => "🏁 抵达目标",
SimEventType.ZoneIntruded => "🚨 侵入管控区",
SimEventType.SimulationEnd => "⏹️ 仿真结束",
_ => t.ToString(),
};
}
}