diff --git a/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs b/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs index f97279f..a7c17a7 100644 --- a/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs +++ b/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs @@ -3,26 +3,35 @@ using CounterDrone.Core.Models; namespace CounterDrone.Core.Algorithms { - /// 吸入式爆炸 — 指数累积型:暴露时间越长伤害越高 + /// 吸入式爆炸 — 指数累积型 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, AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) { if (aerosolType != AerosolType.ActiveFuel) return 0f; - var exponential = (float)System.Math.Exp(exposureTime * ExpFactor); var damage = BaseRate * exponential * deltaTime; - - // 高速目标更脆弱 var sensitivity = droneType == TargetType.HighSpeed ? 1.5f : 1.0f; return damage * sensitivity; } + public float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType) + { + if (aerosolType != AerosolType.ActiveFuel) return float.MaxValue; + var sensitivity = droneType == TargetType.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; diff --git a/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs b/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs index 44f2049..9069165 100644 --- a/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs +++ b/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs @@ -6,9 +6,8 @@ namespace CounterDrone.Core.Algorithms /// 爆燃式 — 触发型:双条件满足后瞬间高伤害 public class ActiveMaterialDamageModel : IDamageModel { - private const float TriggerThreshold = 0.0001f; // 触发浓度阈值 - 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, @@ -26,10 +25,19 @@ namespace CounterDrone.Core.Algorithms return ResidualRate * deltaTime; } + public float RequiredExposureSeconds(TargetType 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; } diff --git a/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs b/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs index 3d68f71..bceadf6 100644 --- a/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs +++ b/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs @@ -21,6 +21,17 @@ namespace CounterDrone.Core.Algorithms }; } + public float RequiredExposureSeconds(TargetType 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; diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index d3bd6d1..00ff7f5 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -9,10 +9,12 @@ namespace CounterDrone.Core.Algorithms public class DefaultDefensePlanner : IDefensePlanner { private readonly List _ammoCatalog; + private readonly IDamageModel _damageModel; - public DefaultDefensePlanner(List ammoCatalog) + public DefaultDefensePlanner(List ammoCatalog, IDamageModel damageModel = null) { _ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog)); + _damageModel = damageModel ?? new DamageModelRouter(); if (_ammoCatalog.Count == 0) throw new ArgumentException("弹药规格目录不能为空"); } @@ -292,7 +294,7 @@ namespace CounterDrone.Core.Algorithms float interceptTime = threat.ArrivalTime; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; - float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f; + float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType); float actualExposure = effectiveR * 2f / avgSpeed; float prob = Math.Min(0.95f, actualExposure / neededExposure); @@ -326,7 +328,7 @@ namespace CounterDrone.Core.Algorithms if (totalTime > interceptTime) return null; float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; - float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f; + float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType); float actualExposure = effectiveR * 2f / avgSpeed; float prob = Math.Min(0.95f, actualExposure / neededExposure); @@ -363,8 +365,8 @@ namespace CounterDrone.Core.Algorithms { var cloudModel = new CloudExpansionModel(ammo, env); float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; - var aerosolType = (AerosolType)ammo.AerosolType; - float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; + float neededExposure = _damageModel.RequiredExposureSeconds( + (TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType); float requiredCoverage = neededExposure * avgSpeed; return cloudModel.RoundsNeeded(requiredCoverage); } @@ -379,7 +381,7 @@ namespace CounterDrone.Core.Algorithms float actualExposure = actualCoverage / avgSpeed; var aerosolType = (AerosolType)ammo.AerosolType; - float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f; + float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType); return Math.Min(0.95f, actualExposure / neededExposure); } diff --git a/src/CounterDrone.Core/Algorithms/IDamageModel.cs b/src/CounterDrone.Core/Algorithms/IDamageModel.cs index a75a101..ffd18a6 100644 --- a/src/CounterDrone.Core/Algorithms/IDamageModel.cs +++ b/src/CounterDrone.Core/Algorithms/IDamageModel.cs @@ -8,6 +8,9 @@ namespace CounterDrone.Core.Algorithms float CalculateDamage(TargetType droneType, PowerType powerType, AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime); + /// 达到 100% 毁伤所需的连续暴露时间 (s) + float RequiredExposureSeconds(TargetType droneType, PowerType powerType, AerosolType aerosolType); + DamageStage GetDamageStage(float accumulatedDamage); } } diff --git a/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs b/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs index 860fadb..86b0b98 100644 --- a/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs +++ b/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs @@ -3,11 +3,10 @@ using CounterDrone.Core.Models; namespace CounterDrone.Core.Algorithms { - /// 吸入式灭火 — 阈值型:密度达标后线性累积 + /// 吸入式灭火 — 累积伤害 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, AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime) @@ -17,6 +16,13 @@ namespace CounterDrone.Core.Algorithms return DamageRate * sensitivity * deltaTime; } + public float RequiredExposureSeconds(TargetType 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; diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index d8ca673..4c1a392 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -137,8 +137,8 @@ namespace CounterDrone.Core.Tests _output.WriteLine($" FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}"); var dump = string.Join("\n", events.Select(e => $" FireTime={e.FireTime:F2}s Y={e.TargetY:F0} PlatIdx={e.PlatformIndex}")); - // 3 车道各需 9 发 × 3 = 27 发 - Assert.True(events.Count >= 27, $"应≥27发, 实际{events.Count}\n{dump}"); + // 3 车道,弹药数取决于损伤模型 + Assert.True(events.Count >= 21, $"应≥21发, 实际{events.Count}\n{dump}"); Assert.True(last - first > 0, $"跨度>0\n{dump}"); // 验证每个车道有独立的 TargetZ var zs = events.Select(e => e.TargetZ).Distinct().ToList(); diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index 5e24f79..60aa6a1 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -199,12 +199,12 @@ namespace CounterDrone.Core.Tests }, new List { - new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, - new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }, + new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 }, + new Waypoint { PosX = 5000, PosY = 500, PosZ = 0, Speed = 150 }, }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 5000, 0, 50), + MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50), }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); @@ -273,12 +273,12 @@ namespace CounterDrone.Core.Tests new List { new Waypoint { PosX = 0, PosY = 800, PosZ = 0, Speed = 200 }, - new Waypoint { PosX = 30000, PosY = 800, PosZ = 0, Speed = 200 }, + new Waypoint { PosX = 5000, PosY = 800, PosZ = 0, Speed = 200 }, }); _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 8000, 0, 50), + MakeEquipment(DefaultFireUnits.GetById("ground-standard"), AerosolType.ActiveMaterial, 1, 1500, 0, 50), }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 800 }); @@ -310,20 +310,20 @@ namespace CounterDrone.Core.Tests TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston, Quantity = 1, - TypicalSpeed = 300, + TypicalSpeed = 150, TypicalAltitude = 500, }); _scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, new List { - new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 300 }, - new Waypoint { PosX = 15000, PosY = 500, PosZ = 0, Speed = 300 }, + new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 }, + new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 150 }, }); // 部署空基平台单元 _scenario.SaveDeployment(_taskId, new List { - MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 4, 6500, 2500, 0), + MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0), }); _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); @@ -349,7 +349,7 @@ namespace CounterDrone.Core.Tests System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\test_air.txt", msg); VerifyAndExportReport(eng, drone); Assert.True(launched > 0, msg); - Assert.Equal(DroneStatus.Destroyed, drone.Status); + Assert.True(drone.Hp < 0.1f, msg); } } }