diff --git a/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs b/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs index f53c4b2..1e6a9b8 100644 --- a/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs +++ b/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs @@ -67,10 +67,12 @@ namespace CounterDrone.Core.Algorithms return TimeToReach(r); } - /// 覆盖指定距离需要的云团数量(间距=2R) - public int RoundsNeeded(float requiredCoverage) + /// 覆盖指定距离需要的云团数量 + /// 需要覆盖的距离 (m) + /// 使用哪个半径值(null=默认TurbulentRadius) + 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)); } diff --git a/src/CounterDrone.Core/Algorithms/DamageAssessment.cs b/src/CounterDrone.Core/Algorithms/DamageAssessment.cs new file mode 100644 index 0000000..ab6d39c --- /dev/null +++ b/src/CounterDrone.Core/Algorithms/DamageAssessment.cs @@ -0,0 +1,41 @@ +using System; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Algorithms +{ + /// 毁伤评估——路径积分计算无人机穿过云团的暴露时间 + public static class DamageAssessment + { + /// 线段 (p1→p2) 在球体内的长度,换算为路径长度 (m) + /// 球体内的路径长度 (m),0 表示未进入 + 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; + } + } +} diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index 85c5653..d3bd6d1 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -396,7 +396,12 @@ namespace CounterDrone.Core.Algorithms var mid = ThreatMidpoint(threat); var cloudModel = new CloudExpansionModel(ammo, env); - float expansionTime = cloudModel.TimeToReach(cloudModel.TurbulentRadius); + // 云龄 = expansionTime,expansionTime = 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); diff --git a/src/CounterDrone.Core/Simulation/DroneEntity.cs b/src/CounterDrone.Core/Simulation/DroneEntity.cs index a448af4..b82620a 100644 --- a/src/CounterDrone.Core/Simulation/DroneEntity.cs +++ b/src/CounterDrone.Core/Simulation/DroneEntity.cs @@ -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; } + /// 上一帧位置(路径积分用) + 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; diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index 157b657..398bd80 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -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); diff --git a/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs b/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs index b5a6a24..3a24c7f 100644 --- a/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs @@ -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() { diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index a6509d8..5e24f79 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -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";