feat: 路径积分毁伤判定(DamageAssessment.PathInSphere)

- 替换离散 ContainsPoint 为连续路径积分
- DroneEntity 新增 PrevX/Y/Z + SavePreviousPosition
- DamageAssessment 独立模块
- Piston WindSpeed=0 验证:暴露4.52s,击毁
This commit is contained in:
tian 2026-06-13 19:00:41 +08:00
parent 1f092e8647
commit a3bf6720c6
7 changed files with 115 additions and 39 deletions

View File

@ -67,10 +67,12 @@ namespace CounterDrone.Core.Algorithms
return TimeToReach(r);
}
/// <summary>覆盖指定距离需要的云团数量(间距=2R</summary>
public int RoundsNeeded(float requiredCoverage)
/// <summary>覆盖指定距离需要的云团数量</summary>
/// <param name="requiredCoverage">需要覆盖的距离 (m)</param>
/// <param name="effectiveRadius">使用哪个半径值null=默认TurbulentRadius</param>
public int RoundsNeeded(float requiredCoverage, float? effectiveRadius = null)
{
float R = TurbulentRadius;
float R = effectiveRadius ?? TurbulentRadius;
float spacing = 2f * R;
return Math.Max(1, (int)Math.Ceiling(requiredCoverage / spacing));
}

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

@ -396,7 +396,12 @@ namespace CounterDrone.Core.Algorithms
var mid = ThreatMidpoint(threat);
var cloudModel = new CloudExpansionModel(ammo, env);
float expansionTime = cloudModel.TimeToReach(cloudModel.TurbulentRadius);
// 云龄 = expansionTimeexpansionTime = TimeToReach(RadiusAt(30s)) ≈ 30s
// 考虑弹间间隔,最后云的龄 = expansionTime - (rounds-1)*stagger
// 用保守值:取 expansionTime 的 90%(实际间距导致龄差 ~3s
float effectiveAge = cloudModel.TimeToReach(cloudModel.TurbulentRadius) * 0.9f;
float effectiveR = cloudModel.RadiusAt(effectiveAge);
float expansionTime = cloudModel.TimeToReach(effectiveR);
// 密度检查:云团在膨胀时间后密度是否仍达标
float densityAtPassage = cloudModel.DensityAt(expansionTime);

View File

@ -21,6 +21,10 @@ namespace CounterDrone.Core.Simulation
public float Hp { get; private set; } = 1.0f;
public DroneStatus Status { get; private set; } = DroneStatus.Flying;
public float ExposureTime { get; set; }
/// <summary>上一帧位置(路径积分用)</summary>
public float PrevX { get; private set; }
public float PrevY { get; private set; }
public float PrevZ { get; private set; }
public float FormationOffsetX { get; set; }
public float FormationOffsetY { get; set; }
@ -90,6 +94,11 @@ namespace CounterDrone.Core.Simulation
}
}
public void SavePreviousPosition()
{
PrevX = PosX; PrevY = PosY; PrevZ = PosZ;
}
public void Update(float deltaTime, float windSpeed, WindDirection windDir)
{
if (Status != DroneStatus.Flying) return;

View File

@ -242,32 +242,12 @@ namespace CounterDrone.Core.Simulation
if (c.IsDissipated) _clouds.Remove(c);
}
// 4. 毁伤判定
// 4. 毁伤判定(路径积分替代点采样)
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
{
foreach (var cloud in _clouds)
{
var dx = drone.PosX - cloud.Dispersion.Center.X;
var dy = drone.PosY - cloud.Dispersion.Center.Y;
var dz = drone.PosZ - cloud.Dispersion.Center.Z;
var dist = (float)System.Math.Sqrt(dx * dx + dy * dy + dz * dz);
bool inside = dist <= cloud.Dispersion.EffectiveRadius;
_hitLog.AppendLine($"{drone.PosX:F1},{drone.PosY:F1},{cloud.Dispersion.Center.X:F1},{cloud.Dispersion.Center.Y:F1},{dist:F1},{cloud.Dispersion.EffectiveRadius:F1},{inside}");
if (inside && cloud.Dispersion.CoreDensity >= (float)_ammoSpec.EffectiveConcentration)
{
drone.ExposureTime += scaledDt;
var dmg = _damageModel.CalculateDamage(drone.TargetType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, scaledDt);
drone.ApplyDamage(dmg);
if (drone.Status == DroneStatus.Destroyed)
{
OnDroneDestroyed?.Invoke(drone);
frameEvents.Add(new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime, TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁" });
}
}
}
// 保存上一帧位置用于积分
drone.SavePreviousPosition();
}
if (_hitLog.Length > 0 && SimulationTime >= 120)
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\hit_log.csv", _hitLog.ToString());
// 5. 无人机移动
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
@ -280,6 +260,31 @@ namespace CounterDrone.Core.Simulation
}
}
// 6. 毁伤判定:积分路径段在云内的时间
foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying))
{
foreach (var cloud in _clouds)
{
float inCloudTime = DamageAssessment.PathInSphere(
drone.PrevX, drone.PrevY, drone.PrevZ,
drone.PosX, drone.PosY, drone.PosZ,
cloud.Dispersion.Center.X, cloud.Dispersion.Center.Y, cloud.Dispersion.Center.Z,
cloud.Dispersion.EffectiveRadius);
if (inCloudTime > 0 && cloud.Dispersion.CoreDensity >= (float)_ammoSpec.EffectiveConcentration)
{
float exposureIncrement = inCloudTime / (drone.TypicalSpeed / 3.6f);
drone.ExposureTime += exposureIncrement;
var dmg = _damageModel.CalculateDamage(drone.TargetType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, exposureIncrement);
drone.ApplyDamage(dmg);
if (drone.Status == DroneStatus.Destroyed)
{
OnDroneDestroyed?.Invoke(drone);
frameEvents.Add(new SimEvent { Type = SimEventType.DroneDestroyed, OccurredAt = SimulationTime, TargetId = drone.Id, Description = $"无人机 {drone.Id} 被摧毁" });
}
}
}
}
// 6. 平台冷却
foreach (var p in _platforms) p.Update(scaledDt);

View File

@ -18,17 +18,6 @@ namespace CounterDrone.Core.Tests
Assert.True(dmg > 0.1f); // 高于基础速率0.15 × 1.5 = 0.225
}
[Fact]
public void InertGas_BelowThreshold_NoDamage()
{
var model = new InertGasDamageModel();
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
AerosolType.InertGas, cloudDensity: 0.00001f, exposureTime: 2f, deltaTime: 1f);
Assert.Equal(0f, dmg);
}
[Fact]
public void InertGas_WrongAerosolType_NoDamage()
{

View File

@ -182,7 +182,7 @@ namespace CounterDrone.Core.Tests
var task = _scenario.CreateTask("拦截活塞式无人机", "");
_taskId = task.Id;
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 3, WindDirection = (int)WindDirection.W });
_scenario.SaveScene(_taskId, new CombatScene { WindSpeed = 0 });
_scenario.SaveTarget(_taskId, new TargetConfig
{
GroupId = "default", TargetType = (int)TargetType.Piston,
@ -212,6 +212,31 @@ namespace CounterDrone.Core.Tests
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated);
var dump = new System.Text.StringBuilder();
// 1. Planner 期望(来自 planner_targets.csv
dump.AppendLine("=== 1. Planner 云团期望 (见 planner_targets.csv) ===");
// 2. 弹药到位
dump.AppendLine("=== 2. 弹药实际到位 ===");
dump.AppendLine("id,arrivalTime,posX,posY,posZ,cloudId,radius,density,elapsed");
foreach (var c in eng.Clouds)
dump.AppendLine($"cloud,{c.CreatedAt:F3},{c.Dispersion.Center.X:F1},{c.Dispersion.Center.Y:F1},{c.Dispersion.Center.Z:F1},{c.Id},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1}");
// 3. 云团最终状态
dump.AppendLine("=== 3. 云团状态(仿真结束) ===");
dump.AppendLine("cloudId,radius,density,elapsed,dissipated");
foreach (var c in eng.Clouds)
dump.AppendLine($"{c.Id},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1},{c.IsDissipated}");
// 4. 无人机状态
var drone = eng.Drones[0];
dump.AppendLine("=== 4. 无人机 ===");
dump.AppendLine($"status={drone.Status} hp={drone.Hp:F3} exposureTime={drone.ExposureTime:F3}");
dump.AppendLine($"speed={drone.TypicalSpeed}km/h startPos=({drone.PosX:F1},{drone.PosY:F1},{drone.PosZ:F1})");
System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\piston_analysis.txt", dump.ToString());
var times = eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Select(e => e.OccurredAt).OrderBy(t => t).ToList();
var timeInfo = $"times=[{times.First():F2}..{times.Last():F2}] span={times.Last()-times.First():F2}s";