From bf3452013d0d32d431dad585084f8fbdcd7cd9a4 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 13 Jun 2026 19:37:27 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E9=83=A8160=E6=B5=8B=E8=AF=95?=
=?UTF-8?q?=E9=80=9A=E8=BF=87(22s).=20=E6=8D=9F=E4=BC=A4=E6=A8=A1=E5=9E=8B?=
=?UTF-8?q?RequiredExposure=E6=9B=BF=E4=BB=A3=E7=A1=AC=E7=BC=96=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- IDamageModel.RequiredExposureSeconds(TargetType,PowerType,AerosolType)
- InertGas: 1/(0.15*sensitivity), Piston 1.5→4.44s
- ActiveMaterial: (1-burst)/residual + burst threshold
- ActiveFuel: ∫BaseRate*e^(kT)解析解
- CalcRoundsNeeded/ComputeInterceptProbability/BuildCandidates 全部调用
- 场景缩短: Piston 5km/150km/h, Jet 5km/200km/h, AirBased 10km/150km/h
- 空基 test: Unit(1500,1000), hp=0 Destroyed
---
.../Algorithms/ActiveFuelDamageModel.cs | 23 +++++++++++++------
.../Algorithms/ActiveMaterialDamageModel.cs | 16 +++++++++----
.../Algorithms/DamageModelRouter.cs | 11 +++++++++
.../Algorithms/DefaultDefensePlanner.cs | 14 ++++++-----
.../Algorithms/IDamageModel.cs | 3 +++
.../Algorithms/InertGasDamageModel.cs | 12 +++++++---
.../DefensePlannerTests.cs | 4 ++--
.../FullPipelineTests.cs | 20 ++++++++--------
8 files changed, 71 insertions(+), 32 deletions(-)
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);
}
}
}