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.
This commit is contained in:
tian 2026-06-14 21:21:37 +08:00
parent 3b8fb4ee86
commit c6aee276fb
2 changed files with 128 additions and 36 deletions

View File

@ -52,11 +52,16 @@ namespace CounterDrone.Core.Services
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} m/s |");
sb.AppendLine($"| 风向 | {scene.WindDirection} |");
sb.AppendLine($"| 温度 | {scene.Temperature}°C |");
sb.AppendLine($"| 能见度 | {scene.Visibility} m |");
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();
// === 三、威胁目标 ===
@ -65,26 +70,36 @@ namespace CounterDrone.Core.Services
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 类型 | {TranslateTarget((TargetType)(target?.TargetType ?? 0))} |");
sb.AppendLine($"| 数量 | {totalDrones} 架 |");
sb.AppendLine($"| 速度 | {target?.TypicalSpeed ?? 0} km/h |");
sb.AppendLine($"| 高度 | {target?.TypicalAltitude ?? 0} m |");
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();
sb.AppendLine($"| 参数 | 值 |");
sb.AppendLine($"|------|-----|");
sb.AppendLine($"| 发射方式 | {TranslateRelease((ReleaseMode)config.Cloud.ReleaseMode)} |");
sb.AppendLine($"| 弹药数 | {config.Cloud.SalvoRounds} 发 |");
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.EstimatedProbability.Value:P0} |");
sb.AppendLine($"| 抛撒时机 | {config.Cloud.RecommendedTiming:F0}s |");
sb.AppendLine();
}
sb.AppendLine();
// 构建 PlatformId → 火力单元 映射(去重,五和六共用)
var platToEquip = new Dictionary<string, int>();
@ -111,11 +126,52 @@ namespace CounterDrone.Core.Services
// === 五、我方部署 ===
sb.AppendLine("## 五、我方部署");
sb.AppendLine();
sb.AppendLine($"| 火力单元 | 发射数 |");
sb.AppendLine($"|------|------|");
foreach (var kv in unitLaunchCounts.OrderBy(kv => kv.Key))
sb.AppendLine($"| #{kv.Key} | {kv.Value} |");
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("## 六、事件时序");
@ -127,34 +183,47 @@ namespace CounterDrone.Core.Services
{
string unitCell = e.Type == SimEventType.MunitionLaunched && platToEquip.TryGetValue(e.SourceId, out var u)
? $"#{u}" : "-";
var desc = e.Type switch
// 编号前缀 + 引擎生成的 Description含坐标等调试信息
string prefix = e.Type switch
{
SimEventType.MunitionLaunched => $"第 {++munitionIdx} 发",
SimEventType.CloudGenerated => $"云团 #{++cloudIdx}",
SimEventType.DroneDestroyed => "目标被摧毁",
SimEventType.DroneReachedTarget => "目标抵达终点",
SimEventType.ZoneIntruded => "侵入管控区",
SimEventType.SimulationEnd => "仿真结束",
_ => e.Description,
_ => 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 launchTime = events.Where(e => e.Type == SimEventType.MunitionLaunched).Select(e => e.OccurredAt).DefaultIfEmpty(0).Min();
var cloudTime = events.Where(e => e.Type == SimEventType.CloudGenerated).Select(e => e.OccurredAt).DefaultIfEmpty(0).Min();
var destroyTime = events.Where(e => e.Type == SimEventType.DroneDestroyed).Select(e => e.OccurredAt).DefaultIfEmpty(0).Min();
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 (launchTime > 0)
sb.AppendLine($"| 发射 → 云团生成 | {launchTime:F1}s → {cloudTime:F1}s | {(cloudTime - launchTime):F1}s弹药飞行 |");
if (destroyTime > 0)
sb.AppendLine($"| 云团生成 → 击毁 | {cloudTime:F1}s → {destroyTime:F1}s | {(destroyTime - cloudTime):F1}s暴露毁伤 |");
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();
}
@ -190,6 +259,28 @@ namespace CounterDrone.Core.Services
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 => "固定翼",

View File

@ -182,7 +182,7 @@ namespace CounterDrone.Core.Simulation
fe.FireTime);
_munitions.Add(m);
OnMunitionLaunched?.Invoke(m);
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = fe.FireTime, SourceId = platform.Id, Description = $"计划发射 {m.Id}" });
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = fe.FireTime, SourceId = platform.Id, Description = $"计划发射 {m.Id} @({platform.PosX:F0},{platform.PosY:F0},{platform.PosZ:F0})→({fe.TargetX:F0},{fe.TargetY:F0},{fe.TargetZ:F0})" });
}
}
@ -216,7 +216,7 @@ namespace CounterDrone.Core.Simulation
_clouds.Add(cloud);
_munitions.Remove(m);
OnCloudGenerated?.Invoke(cloud);
frameEvents.Add(new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = m.ArrivalTime, Description = "云团生成" });
frameEvents.Add(new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = m.ArrivalTime, Description = $"云团生成 @({m.PosX:F0},{m.PosY:F0},{m.PosZ:F0})" });
}
}
@ -271,7 +271,8 @@ namespace CounterDrone.Core.Simulation
if (drone.Status == DroneStatus.Destroyed)
{
OnDroneDestroyed?.Invoke(drone);
frameEvents.Add(new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime, TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁" });
frameEvents.Add(new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime, TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁 @({drone.PosX:F0},{drone.PosY:F0},{drone.PosZ:F0})" });
break; // 已击毁,不再判定本 tick 其他云团,避免重复触发
}
}
}