diff --git a/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs b/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs
new file mode 100644
index 0000000..195437f
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/ActiveFuelDamageModel.cs
@@ -0,0 +1,37 @@
+using System;
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 吸入式爆炸 — 指数累积型:暴露时间越长伤害越高
+ public class ActiveFuelDamageModel : IDamageModel
+ {
+ private const float EffectiveThreshold = 0.03f; // 有效浓度阈值
+ 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;
+ if (cloudDensity < EffectiveThreshold) return 0f;
+
+ // 伤害 = 基础速率 × e^(暴露时间 × 因子) × deltaTime
+ // 在云团中待得越久伤害指数增长
+ 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 DamageStage GetDamageStage(float accumulatedDamage)
+ {
+ if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
+ if (accumulatedDamage >= 0.6f) return DamageStage.AttitudeLoss;
+ if (accumulatedDamage >= 0.3f) return DamageStage.EngineAnomaly;
+ return DamageStage.Normal;
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs b/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs
new file mode 100644
index 0000000..b462773
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/ActiveMaterialDamageModel.cs
@@ -0,0 +1,39 @@
+using System;
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 爆燃式 — 触发型:双条件满足后瞬间高伤害
+ public class ActiveMaterialDamageModel : IDamageModel
+ {
+ private const float TriggerThreshold = 0.1f; // 触发浓度阈值
+ private const float BurstDamage = 0.6f; // 一次爆发伤害
+ private const float ResidualRate = 0.02f; // 后续余伤速率
+ private bool _triggered;
+
+ public float CalculateDamage(TargetType droneType, PowerType powerType,
+ AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
+ {
+ if (aerosolType != AerosolType.ActiveMaterial) return 0f;
+ if (cloudDensity < TriggerThreshold) return 0f;
+
+ // 喷气发动机高温触发爆燃,更敏感
+ if (!_triggered)
+ {
+ _triggered = true;
+ var sensitivity = powerType == PowerType.Jet ? 1.3f : 1.0f;
+ return BurstDamage * sensitivity;
+ }
+
+ return ResidualRate * deltaTime;
+ }
+
+ public DamageStage GetDamageStage(float accumulatedDamage)
+ {
+ if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
+ if (accumulatedDamage >= 0.5f) return DamageStage.AttitudeLoss; // 爆燃后更快进入失控
+ if (accumulatedDamage >= 0.2f) return DamageStage.EngineAnomaly;
+ return DamageStage.Normal;
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/AlgorithmFactory.cs b/src/CounterDrone.Core/Algorithms/AlgorithmFactory.cs
new file mode 100644
index 0000000..03e7a3c
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/AlgorithmFactory.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 算法工厂 — 统一管理算法组件的创建与替换
+ public static class AlgorithmFactory
+ {
+ private static readonly Dictionary _registry = new()
+ {
+ { typeof(ICloudDispersionModel), typeof(GaussianPuffDispersion) },
+ { typeof(IDamageModel), typeof(DamageModelRouter) },
+ { typeof(IDefenseAdvisor), typeof(DefaultDefenseAdvisor) },
+ };
+
+ /// 替换某个算法实现(第三方接入点)
+ public static void Register() where TImpl : TInterface, new()
+ {
+ _registry[typeof(TInterface)] = typeof(TImpl);
+ }
+
+ /// 创建算法实例
+ public static T Create() where T : class
+ {
+ if (_registry.TryGetValue(typeof(T), out var implType))
+ return (T)Activator.CreateInstance(implType)!;
+
+ throw new InvalidOperationException(
+ $"No implementation registered for {typeof(T).Name}");
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs
new file mode 100644
index 0000000..77aec26
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 威胁画像 — 防御推荐算法的输入
+ public class ThreatProfile
+ {
+ public CombatScene Environment { get; set; } = new();
+ public List Targets { get; set; } = new();
+ public RoutePlan Route { get; set; } = new();
+ public List Waypoints { get; set; } = new();
+ public List ControlZones { get; set; } = new();
+ }
+
+ /// 防御推荐方案
+ public class DefenseRecommendation
+ {
+ public DefenseSolution Best { get; set; } = new();
+ public DefenseSolution Critical { get; set; } = new();
+ }
+
+ /// 单套防御方案
+ public class DefenseSolution
+ {
+ public AerosolType RecommendedAerosolType { get; set; }
+ public string AerosolRationale { get; set; } = string.Empty;
+ public CloudDispersal RecommendedCloud { get; set; } = new();
+
+ public List Platforms { get; set; } = new();
+ public List Detections { get; set; } = new();
+
+ public float InterceptProbability { get; set; }
+ public string SummaryRationale { get; set; } = string.Empty;
+ }
+
+ public class RecommendedPlatform
+ {
+ public PlatformType Type { get; set; }
+ public Vector3 Position { get; set; }
+ public int Quantity { get; set; }
+ public int MunitionCount { get; set; }
+ public float CoverageVolume { get; set; }
+ }
+
+ public class RecommendedDetection
+ {
+ public Vector3 Position { get; set; }
+ public float DetectionRadius { get; set; }
+ public int Quantity { get; set; }
+ }
+
+ /// 三维向量(纯 C#,不依赖 UnityEngine)
+ public struct Vector3
+ {
+ public float X, Y, Z;
+ public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
+
+ public static Vector3 operator +(Vector3 a, Vector3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
+ public static Vector3 operator -(Vector3 a, Vector3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
+ public static Vector3 operator *(Vector3 a, float s) => new(a.X * s, a.Y * s, a.Z * s);
+ public float Length => (float)System.Math.Sqrt(X * X + Y * Y + Z * Z);
+ public float DistanceTo(Vector3 other) => (this - other).Length;
+ }
+
+ /// 粒子渲染参数
+ public class ParticleParams
+ {
+ public float EmitRate { get; set; } = 100;
+ public float Opacity { get; set; } = 1.0f;
+ public string ColorHex { get; set; } = "#FFFFFF";
+ public float SizeMultiplier { get; set; } = 1.0f;
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs b/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs
new file mode 100644
index 0000000..3d68f71
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/DamageModelRouter.cs
@@ -0,0 +1,32 @@
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 毁伤模型路由器 — 根据气溶胶类型分派
+ public class DamageModelRouter : IDamageModel
+ {
+ private readonly InertGasDamageModel _inertGas = new();
+ private readonly ActiveMaterialDamageModel _activeMaterial = new();
+ private readonly ActiveFuelDamageModel _activeFuel = new();
+
+ public float CalculateDamage(TargetType droneType, PowerType powerType,
+ AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
+ {
+ return aerosolType switch
+ {
+ AerosolType.InertGas => _inertGas.CalculateDamage(droneType, powerType, aerosolType, cloudDensity, exposureTime, deltaTime),
+ AerosolType.ActiveMaterial => _activeMaterial.CalculateDamage(droneType, powerType, aerosolType, cloudDensity, exposureTime, deltaTime),
+ AerosolType.ActiveFuel => _activeFuel.CalculateDamage(droneType, powerType, aerosolType, cloudDensity, exposureTime, deltaTime),
+ _ => 0f,
+ };
+ }
+
+ public DamageStage GetDamageStage(float accumulatedDamage)
+ {
+ if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
+ if (accumulatedDamage >= 0.6f) return DamageStage.AttitudeLoss;
+ if (accumulatedDamage >= 0.25f) return DamageStage.EngineAnomaly;
+ return DamageStage.Normal;
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs b/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs
new file mode 100644
index 0000000..c96bb9f
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/DefaultDefenseAdvisor.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ public class DefaultDefenseAdvisor : IDefenseAdvisor
+ {
+ private static readonly Dictionary MatchTable = new()
+ {
+ { PowerType.Electric, AerosolType.InertGas },
+ { PowerType.Piston, AerosolType.InertGas },
+ { PowerType.Jet, AerosolType.ActiveMaterial },
+ };
+
+ public DefenseRecommendation Recommend(ThreatProfile threat)
+ {
+ if (threat.Targets.Count == 0 || threat.Waypoints.Count < 2)
+ return new DefenseRecommendation();
+
+ var target = threat.Targets[0];
+ var route = threat.Waypoints;
+
+ // Step A:气溶胶选型
+ var powerType = (PowerType)target.PowerType;
+ var aerosolType = MatchTable.TryGetValue(powerType, out var match)
+ ? match : AerosolType.InertGas;
+
+ var rationale = powerType switch
+ {
+ PowerType.Electric or PowerType.Piston =>
+ $"{powerType}发动机依赖氧气,推荐惰性气体窒息方案(吸入式灭火)",
+ PowerType.Jet =>
+ $"{powerType}发动机高温表面可触发活性材料爆燃反应",
+ _ => "默认推荐惰性气体方案",
+ };
+
+ // Step B:时空交汇优化
+ var start = ToV3(route[0]);
+ var end = ToV3(route[^1]);
+ var mid = new Vector3((start.X + end.X) / 2f, (start.Y + end.Y) / 2f, (start.Z + end.Z) / 2f);
+ var routeLength = start.DistanceTo(end);
+ var avgSpeed = (float)target.TypicalSpeed / 3.6f; // km/h → m/s
+ var flightTime = routeLength / avgSpeed;
+
+ // 最佳值:航路中点
+ var bestCloud = new CloudDispersal
+ {
+ AerosolType = (int)aerosolType,
+ PositionX = mid.X,
+ PositionY = mid.Y,
+ PositionZ = mid.Z,
+ DisperseHeight = (float)target.TypicalAltitude,
+ TriggerMode = (int)TriggerMode.Time,
+ Duration = 60,
+ Source = "Algorithm",
+ PositionMode = (int)PositionMode.AlgorithmRecommended,
+ RecommendedTiming = flightTime / 2f,
+ };
+
+ var bestProb = EstimateInterceptProbability(route, avgSpeed, mid, 50f, 60f);
+
+ // 最危险值:航路 1/4 处
+ var criticalPos = new Vector3(
+ start.X + (end.X - start.X) * 0.25f,
+ start.Y + (end.Y - start.Y) * 0.25f,
+ start.Z + (end.Z - start.Z) * 0.25f);
+
+ var criticalCloud = new CloudDispersal
+ {
+ AerosolType = (int)aerosolType,
+ PositionX = criticalPos.X,
+ PositionY = criticalPos.Y,
+ PositionZ = criticalPos.Z,
+ DisperseHeight = (float)target.TypicalAltitude,
+ TriggerMode = (int)TriggerMode.Time,
+ Duration = 60,
+ Source = "Algorithm",
+ PositionMode = (int)PositionMode.AlgorithmRecommended,
+ RecommendedTiming = flightTime / 4f,
+ };
+
+ var criticalProb = EstimateInterceptProbability(route, avgSpeed, criticalPos, 50f, 60f);
+
+ // Step C:反推平台部署
+ var bestPlatforms = RecommendPlatforms(bestCloud, target);
+
+ return new DefenseRecommendation
+ {
+ Best = new DefenseSolution
+ {
+ RecommendedAerosolType = aerosolType,
+ AerosolRationale = rationale,
+ RecommendedCloud = bestCloud,
+ Platforms = bestPlatforms,
+ Detections = new List
+ {
+ new RecommendedDetection
+ {
+ Position = new Vector3(mid.X, mid.Y, 0),
+ DetectionRadius = System.Math.Max(3000f, routeLength * 0.4f),
+ Quantity = 1,
+ }
+ },
+ InterceptProbability = bestProb,
+ SummaryRationale = $"最佳方案:{aerosolType}方案,预计拦截概率 {bestProb:P0}",
+ },
+ Critical = new DefenseSolution
+ {
+ RecommendedAerosolType = aerosolType,
+ AerosolRationale = "临界方案",
+ RecommendedCloud = criticalCloud,
+ Platforms = RecommendPlatforms(criticalCloud, target),
+ Detections = new List(),
+ InterceptProbability = criticalProb,
+ SummaryRationale = $"最危险方案:拦截概率仅 {criticalProb:P0}",
+ },
+ };
+ }
+
+ private List RecommendPlatforms(CloudDispersal cloud, TargetConfig target)
+ {
+ // 简化:默认地基火炮
+ var platforms = new List();
+ var targetCount = target.Quantity;
+ var platformsNeeded = Math.Max(1, (int)Math.Ceiling(targetCount / 2.0));
+
+ for (int i = 0; i < platformsNeeded; i++)
+ {
+ platforms.Add(new RecommendedPlatform
+ {
+ Type = PlatformType.GroundBased,
+ Position = new Vector3((float)(cloud.PositionX + i * 200), 0, 50),
+ Quantity = 1,
+ MunitionCount = 3,
+ CoverageVolume = 500000f,
+ });
+ }
+
+ return platforms;
+ }
+
+ private float EstimateInterceptProbability(List route, float avgSpeed,
+ Vector3 cloudPos, float cloudRadius, float cloudDuration)
+ {
+ // 简化估算:计算目标在云团中的穿越时间比例
+ float totalDist = 0;
+ float inCloudDist = 0;
+
+ for (int i = 0; i < route.Count - 1; i++)
+ {
+ var segStart = ToV3(route[i]);
+ var segEnd = ToV3(route[i + 1]);
+ var segLen = segStart.DistanceTo(segEnd);
+ totalDist += segLen;
+
+ // 简化:检查线段中点是否在云团内
+ var segMid = new Vector3(
+ (segStart.X + segEnd.X) / 2f,
+ (segStart.Y + segEnd.Y) / 2f,
+ (segStart.Z + segEnd.Z) / 2f);
+
+ if (segMid.DistanceTo(cloudPos) <= cloudRadius)
+ inCloudDist += segLen;
+ }
+
+ return totalDist > 0 ? System.Math.Min(0.95f, inCloudDist / totalDist) : 0f;
+ }
+
+ private static Vector3 ToV3(Waypoint wp)
+ {
+ return new Vector3((float)wp.PosX, (float)wp.PosY, (float)wp.PosZ);
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs b/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs
new file mode 100644
index 0000000..f45d7e8
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs
@@ -0,0 +1,91 @@
+using System;
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 高斯烟团扩散模型 — 简化实现
+ public class GaussianPuffDispersion : ICloudDispersionModel
+ {
+ private AmmunitionSpec _ammo = null!;
+ private float _elapsed;
+ private float _currentRadius;
+ private Vector3 _center;
+ private Vector3 _windVelocity;
+
+ public Vector3 Center => _center;
+ public float Radius => _currentRadius;
+ public float CoreDensity { get; private set; }
+ public float EffectiveRadius => _currentRadius; // 简化:有效半径 = 物理半径
+ public ParticleParams Particles { get; } = new();
+ public bool IsDissipated { get; private set; }
+
+ public void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime)
+ {
+ _ammo = ammo;
+ _elapsed = 0f;
+ _currentRadius = (float)ammo.InitialRadius;
+ _center = releasePos;
+ CoreDensity = (float)ammo.CoreDensity;
+ IsDissipated = false;
+
+ // 风速 → 速度矢量
+ var dir = WindToVector((WindDirection)env.WindDirection);
+ _windVelocity = dir * (float)env.WindSpeed;
+ }
+
+ public void Tick(float deltaTime, float windSpeed, WindDirection windDir)
+ {
+ if (IsDissipated) return;
+
+ _elapsed += deltaTime;
+
+ // 更新风速(允许动态变化)
+ var dir = WindToVector(windDir);
+ _windVelocity = dir * windSpeed;
+
+ // 半径膨胀:初始半径 + 扩散速率 × 时间
+ // 加入大气稳定度因子(简化:1.0)
+ var dispersionRate = (float)(_ammo.DispersionRateBase * 1.0f);
+ _currentRadius = (float)_ammo.InitialRadius + dispersionRate * _elapsed;
+
+ // 中心随风漂移
+ _center += _windVelocity * deltaTime;
+
+ // 密度衰减:核心密度 × (初始半径 / 当前半径)³
+ if (_currentRadius > 0.001f)
+ {
+ var volumeRatio = (float)System.Math.Pow((float)_ammo.InitialRadius / _currentRadius, 3);
+ CoreDensity = (float)_ammo.CoreDensity * volumeRatio;
+ }
+
+ // 更新粒子参数
+ Particles.Opacity = System.Math.Max(0.1f, CoreDensity / (float)_ammo.CoreDensity);
+ Particles.SizeMultiplier = _currentRadius / (float)_ammo.InitialRadius;
+
+ // 消散条件
+ if (_elapsed >= (float)_ammo.MaxDuration || _currentRadius >= (float)_ammo.MaxRadius)
+ {
+ IsDissipated = true;
+ Particles.Opacity = 0f;
+ }
+ }
+
+ private static Vector3 WindToVector(WindDirection dir)
+ {
+ var angle = dir switch
+ {
+ WindDirection.N => 0f,
+ WindDirection.NE => 45f,
+ WindDirection.E => 90f,
+ WindDirection.SE => 135f,
+ WindDirection.S => 180f,
+ WindDirection.SW => 225f,
+ WindDirection.W => 270f,
+ WindDirection.NW => 315f,
+ _ => 0f,
+ };
+ var rad = angle * (float)System.Math.PI / 180f;
+ return new Vector3((float)System.Math.Sin(rad), 0, (float)System.Math.Cos(rad));
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/ICloudDispersionModel.cs b/src/CounterDrone.Core/Algorithms/ICloudDispersionModel.cs
new file mode 100644
index 0000000..9b62099
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/ICloudDispersionModel.cs
@@ -0,0 +1,16 @@
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ public interface ICloudDispersionModel
+ {
+ void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime);
+ void Tick(float deltaTime, float windSpeed, WindDirection windDir);
+ Vector3 Center { get; }
+ float Radius { get; }
+ float CoreDensity { get; }
+ float EffectiveRadius { get; }
+ ParticleParams Particles { get; }
+ bool IsDissipated { get; }
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/IDamageModel.cs b/src/CounterDrone.Core/Algorithms/IDamageModel.cs
new file mode 100644
index 0000000..c639345
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/IDamageModel.cs
@@ -0,0 +1,12 @@
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ public interface IDamageModel
+ {
+ float CalculateDamage(TargetType droneType, PowerType powerType,
+ AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime);
+
+ DamageStage GetDamageStage(float accumulatedDamage);
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs b/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs
new file mode 100644
index 0000000..449483e
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/IDefenseAdvisor.cs
@@ -0,0 +1,9 @@
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ public interface IDefenseAdvisor
+ {
+ DefenseRecommendation Recommend(ThreatProfile threat);
+ }
+}
diff --git a/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs b/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs
new file mode 100644
index 0000000..22d5a9c
--- /dev/null
+++ b/src/CounterDrone.Core/Algorithms/InertGasDamageModel.cs
@@ -0,0 +1,31 @@
+using System;
+using CounterDrone.Core.Models;
+
+namespace CounterDrone.Core.Algorithms
+{
+ /// 吸入式灭火 — 阈值型:密度达标后线性累积
+ public class InertGasDamageModel : IDamageModel
+ {
+ private const float EffectiveThreshold = 0.05f; // 有效浓度阈值
+ private const float DamageRate = 0.15f; // 每秒毁伤率
+
+ public float CalculateDamage(TargetType droneType, PowerType powerType,
+ AerosolType aerosolType, float cloudDensity, float exposureTime, float deltaTime)
+ {
+ if (aerosolType != AerosolType.InertGas) return 0f;
+ if (cloudDensity < EffectiveThreshold) return 0f;
+
+ // 活塞发动机对惰性气体最敏感
+ var sensitivity = powerType == PowerType.Piston ? 1.5f : 1.0f;
+ return DamageRate * sensitivity * deltaTime;
+ }
+
+ public DamageStage GetDamageStage(float accumulatedDamage)
+ {
+ if (accumulatedDamage >= 1.0f) return DamageStage.Destroyed;
+ if (accumulatedDamage >= 0.6f) return DamageStage.AttitudeLoss;
+ if (accumulatedDamage >= 0.25f) return DamageStage.EngineAnomaly;
+ return DamageStage.Normal;
+ }
+ }
+}
diff --git a/src/CounterDrone.Core/Services/ScenarioService.cs b/src/CounterDrone.Core/Services/ScenarioService.cs
index e64cd14..aaf289b 100644
--- a/src/CounterDrone.Core/Services/ScenarioService.cs
+++ b/src/CounterDrone.Core/Services/ScenarioService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using CounterDrone.Core.Models;
using CounterDrone.Core.Repository;
@@ -210,12 +211,13 @@ namespace CounterDrone.Core.Services
}
}
+ private static int _counter;
+
public static string GenerateTaskNumber()
{
var now = DateTime.Now;
var datePart = now.ToString("yyyyMMdd");
- var random = new Random();
- var seq = random.Next(1, 1000);
+ var seq = Interlocked.Increment(ref _counter);
return $"SIM-{datePart}-{seq:D3}";
}
}
diff --git a/test/unit/CounterDrone.Core.Tests/AlgorithmFactoryTests.cs b/test/unit/CounterDrone.Core.Tests/AlgorithmFactoryTests.cs
new file mode 100644
index 0000000..74e869a
--- /dev/null
+++ b/test/unit/CounterDrone.Core.Tests/AlgorithmFactoryTests.cs
@@ -0,0 +1,49 @@
+using CounterDrone.Core.Algorithms;
+using Xunit;
+
+namespace CounterDrone.Core.Tests
+{
+ public class AlgorithmFactoryTests
+ {
+ [Fact]
+ public void Create_ReturnsDefaultImplementations()
+ {
+ var dispersion = AlgorithmFactory.Create();
+ Assert.IsType(dispersion);
+
+ var damage = AlgorithmFactory.Create();
+ Assert.IsType(damage);
+
+ var advisor = AlgorithmFactory.Create();
+ Assert.IsType(advisor);
+ }
+
+ [Fact]
+ public void Register_ReplacesImplementation()
+ {
+ // 替换为自定义实现
+ AlgorithmFactory.Register();
+
+ var instance = AlgorithmFactory.Create();
+ Assert.IsType(instance);
+
+ // 还原
+ AlgorithmFactory.Register();
+ }
+ }
+
+ public class MockDispersion : ICloudDispersionModel
+ {
+ public Vector3 Center => new();
+ public float Radius => 100f;
+ public float CoreDensity => 1f;
+ public float EffectiveRadius => 100f;
+ public ParticleParams Particles => new();
+ public bool IsDissipated => false;
+
+ public void Initialize(CounterDrone.Core.Models.AmmunitionSpec ammo,
+ CounterDrone.Core.Models.CombatScene env, Vector3 releasePos, float releaseTime) { }
+ public void Tick(float deltaTime, float windSpeed,
+ CounterDrone.Core.Models.WindDirection windDir) { }
+ }
+}
diff --git a/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs b/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs
new file mode 100644
index 0000000..412eb7d
--- /dev/null
+++ b/test/unit/CounterDrone.Core.Tests/DamageModelTests.cs
@@ -0,0 +1,129 @@
+using CounterDrone.Core.Algorithms;
+using CounterDrone.Core.Models;
+using Xunit;
+
+namespace CounterDrone.Core.Tests
+{
+ public class DamageModelTests
+ {
+ [Fact]
+ public void InertGas_PistonEngine_TakesExtraDamage()
+ {
+ var model = new InertGasDamageModel();
+
+ // 活塞发动机暴露于惰性气体
+ var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
+ AerosolType.InertGas, cloudDensity: 0.1f, exposureTime: 2f, deltaTime: 1f);
+
+ 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.01f, exposureTime: 2f, deltaTime: 1f);
+
+ Assert.Equal(0f, dmg);
+ }
+
+ [Fact]
+ public void InertGas_WrongAerosolType_NoDamage()
+ {
+ var model = new InertGasDamageModel();
+
+ var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
+ AerosolType.ActiveMaterial, cloudDensity: 0.1f, exposureTime: 2f, deltaTime: 1f);
+
+ Assert.Equal(0f, dmg);
+ }
+
+ [Fact]
+ public void ActiveMaterial_TriggersBurst()
+ {
+ var model = new ActiveMaterialDamageModel();
+
+ var dmg1 = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
+ AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 1f, deltaTime: 1f);
+
+ Assert.True(dmg1 >= 0.5f); // 爆发伤害
+
+ var dmg2 = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
+ AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 2f, deltaTime: 1f);
+
+ Assert.True(dmg2 < 0.1f); // 后续余伤很小
+ }
+
+ [Fact]
+ public void ActiveMaterial_JetEngine_ExtraSensitivity()
+ {
+ var model = new ActiveMaterialDamageModel();
+
+ var dmg = model.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
+ AerosolType.ActiveMaterial, cloudDensity: 0.2f, exposureTime: 0f, deltaTime: 0.1f);
+
+ // 喷气发动机:0.6 × 1.3 = 0.78
+ Assert.True(dmg > 0.7f);
+ }
+
+ [Fact]
+ public void ActiveFuel_ExponentialGrowth()
+ {
+ var model = new ActiveFuelDamageModel();
+
+ var dmg1 = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
+ AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 1f, deltaTime: 1f);
+
+ var dmg2 = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
+ AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 5f, deltaTime: 1f);
+
+ // 5 秒后的伤害应显著大于 1 秒后
+ Assert.True(dmg2 > dmg1 * 2f);
+ }
+
+ [Fact]
+ public void ActiveFuel_HighSpeedTarget_ExtraSensitive()
+ {
+ var model = new ActiveFuelDamageModel();
+
+ var dmg = model.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
+ AerosolType.ActiveFuel, cloudDensity: 0.1f, exposureTime: 1f, deltaTime: 1f);
+
+ Assert.True(dmg > 0.02f); // 基础速率 × 灵敏度
+ }
+
+ [Fact]
+ public void DamageStage_Progression()
+ {
+ var model = new InertGasDamageModel();
+
+ Assert.Equal(DamageStage.Normal, model.GetDamageStage(0.1f));
+ Assert.Equal(DamageStage.EngineAnomaly, model.GetDamageStage(0.3f));
+ Assert.Equal(DamageStage.AttitudeLoss, model.GetDamageStage(0.7f));
+ Assert.Equal(DamageStage.Destroyed, model.GetDamageStage(1.0f));
+ }
+
+ [Fact]
+ public void Router_RoutesToCorrectModel()
+ {
+ var router = new DamageModelRouter();
+
+ // 惰性气体
+ var dmg1 = router.CalculateDamage(TargetType.Piston, PowerType.Piston,
+ AerosolType.InertGas, 0.1f, 1f, 1f);
+ Assert.True(dmg1 > 0f);
+
+ // 活性材料
+ var dmg2 = router.CalculateDamage(TargetType.FixedWing, PowerType.Jet,
+ AerosolType.ActiveMaterial, 0.2f, 0f, 1f);
+ Assert.True(dmg2 > 0f);
+
+ // 活性燃料
+ var dmg3 = router.CalculateDamage(TargetType.HighSpeed, PowerType.Jet,
+ AerosolType.ActiveFuel, 0.1f, 1f, 1f);
+ Assert.True(dmg3 > 0f);
+ }
+ }
+}
diff --git a/test/unit/CounterDrone.Core.Tests/DefenseAdvisorTests.cs b/test/unit/CounterDrone.Core.Tests/DefenseAdvisorTests.cs
new file mode 100644
index 0000000..91c45fb
--- /dev/null
+++ b/test/unit/CounterDrone.Core.Tests/DefenseAdvisorTests.cs
@@ -0,0 +1,125 @@
+using System.Collections.Generic;
+using CounterDrone.Core.Algorithms;
+using CounterDrone.Core.Models;
+using Xunit;
+
+namespace CounterDrone.Core.Tests
+{
+ public class DefenseAdvisorTests
+ {
+ private ThreatProfile CreateSimpleThreat(PowerType powerType = PowerType.Piston)
+ {
+ return new ThreatProfile
+ {
+ Environment = new CombatScene
+ {
+ WindSpeed = 5.0,
+ WindDirection = (int)WindDirection.E,
+ },
+ Targets = new List
+ {
+ new TargetConfig
+ {
+ TargetType = (int)TargetType.Piston,
+ PowerType = (int)powerType,
+ Quantity = 3,
+ TypicalSpeed = 120.0,
+ TypicalAltitude = 500.0,
+ },
+ },
+ Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
+ Waypoints = new List
+ {
+ new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 500, Speed = 120 },
+ new Waypoint { PosX = 10000, PosY = 0, PosZ = 100, Altitude = 500, Speed = 120 },
+ },
+ };
+ }
+
+ [Fact]
+ public void Recommend_PistonEngine_RecommendsInertGas()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat(PowerType.Piston));
+
+ Assert.Equal(AerosolType.InertGas, result.Best.RecommendedAerosolType);
+ Assert.Contains("惰性气体", result.Best.AerosolRationale);
+ Assert.True(result.Best.InterceptProbability > 0f);
+ }
+
+ [Fact]
+ public void Recommend_JetEngine_RecommendsActiveMaterial()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat(PowerType.Jet));
+
+ Assert.Equal(AerosolType.ActiveMaterial, result.Best.RecommendedAerosolType);
+ Assert.Contains("活性材料", result.Best.AerosolRationale);
+ }
+
+ [Fact]
+ public void Recommend_ElectricEngine_RecommendsInertGas()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat(PowerType.Electric));
+
+ Assert.Equal(AerosolType.InertGas, result.Best.RecommendedAerosolType);
+ Assert.Contains("惰性气体", result.Best.AerosolRationale);
+ }
+
+ [Fact]
+ public void Recommend_ProducesBestAndCritical()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat());
+
+ Assert.NotNull(result.Best);
+ Assert.NotNull(result.Critical);
+ Assert.True(result.Best.InterceptProbability >= result.Critical.InterceptProbability,
+ "最佳值拦截概率应 ≥ 最危险值");
+ }
+
+ [Fact]
+ public void Recommend_CloudPosition_OnRoute()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat());
+
+ // 最佳位置应在航路中点附近 (5000, 0, 100)
+ var pos = result.Best.RecommendedCloud;
+ Assert.True(pos.PositionX > 1000 && pos.PositionX < 9000,
+ $"PositionX={pos.PositionX} 应在航路范围内");
+ }
+
+ [Fact]
+ public void Recommend_ReturnsPlatforms()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat());
+
+ Assert.NotEmpty(result.Best.Platforms);
+ Assert.Equal(PlatformType.GroundBased, result.Best.Platforms[0].Type);
+ }
+
+ [Fact]
+ public void Recommend_ReturnsDetection()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat());
+
+ Assert.NotEmpty(result.Best.Detections);
+ Assert.True(result.Best.Detections[0].DetectionRadius > 0);
+ }
+
+ [Fact]
+ public void Recommend_MarksSourceAsAlgorithm()
+ {
+ var advisor = new DefaultDefenseAdvisor();
+ var result = advisor.Recommend(CreateSimpleThreat());
+
+ Assert.Equal("Algorithm", result.Best.RecommendedCloud.Source);
+ Assert.Equal((int)PositionMode.AlgorithmRecommended,
+ result.Best.RecommendedCloud.PositionMode);
+ }
+ }
+}
diff --git a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs
new file mode 100644
index 0000000..4330773
--- /dev/null
+++ b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs
@@ -0,0 +1,129 @@
+using System;
+using System.IO;
+using CounterDrone.Core.Algorithms;
+using CounterDrone.Core.Models;
+using Xunit;
+
+namespace CounterDrone.Core.Tests
+{
+ public class DispersionModelTests
+ {
+ private AmmunitionSpec CreateTestAmmo()
+ {
+ return new AmmunitionSpec
+ {
+ InitialRadius = 50.0,
+ InitialVolume = 500000.0,
+ CoreDensity = 1.2,
+ EdgeDensity = 0.1,
+ DispersionRateBase = 5.0,
+ MaxRadius = 200.0,
+ MaxDuration = 60.0,
+ EffectiveConcentration = 0.05,
+ };
+ }
+
+ private CombatScene CreateTestEnv(float windSpeed = 5.0f, WindDirection dir = WindDirection.E)
+ {
+ return new CombatScene
+ {
+ WindSpeed = windSpeed,
+ WindDirection = (int)dir,
+ Temperature = 20,
+ Humidity = 60,
+ Pressure = 1013,
+ };
+ }
+
+ [Fact]
+ public void Initialize_SetsProperties()
+ {
+ var model = new GaussianPuffDispersion();
+ var ammo = CreateTestAmmo();
+ var env = CreateTestEnv();
+
+ model.Initialize(ammo, env, new Vector3(1000, 0, 300), 0f);
+
+ Assert.Equal(50f, model.Radius);
+ Assert.Equal(1.2f, model.CoreDensity);
+ Assert.False(model.IsDissipated);
+ }
+
+ [Fact]
+ public void Tick_ExpandsRadius()
+ {
+ var model = new GaussianPuffDispersion();
+ model.Initialize(CreateTestAmmo(), CreateTestEnv(), new Vector3(0, 0, 0), 0f);
+
+ // 5 秒后
+ model.Tick(5f, 5f, WindDirection.E);
+ // 初始 50 + 扩散速率 5 × 5s = 75
+ Assert.True(model.Radius > 55f && model.Radius < 80f);
+ }
+
+ [Fact]
+ public void Tick_WindDrift_MovesCenter()
+ {
+ var model = new GaussianPuffDispersion();
+ model.Initialize(CreateTestAmmo(), CreateTestEnv(10f, WindDirection.E), new Vector3(0, 0, 0), 0f);
+
+ model.Tick(10f, 10f, WindDirection.E);
+
+ // 东风 10 m/s × 10s = 100m,东是 +X
+ Assert.True(model.Center.X > 80f);
+ Assert.True(model.Center.Z < 20f); // 纯东向,Z 不应有大偏移
+ }
+
+ [Fact]
+ public void Tick_DensityDecays()
+ {
+ var model = new GaussianPuffDispersion();
+ model.Initialize(CreateTestAmmo(), CreateTestEnv(0f, WindDirection.N), new Vector3(0, 0, 0), 0f);
+
+ model.Tick(20f, 0f, WindDirection.N);
+
+ // 半径膨胀后密度应下降
+ Assert.True(model.CoreDensity < 1.2f);
+ }
+
+ [Fact]
+ public void Tick_DissipatesAfterMaxDuration()
+ {
+ var model = new GaussianPuffDispersion();
+ var ammo = CreateTestAmmo();
+ ammo.MaxDuration = 5.0; // 5秒消散
+ model.Initialize(ammo, CreateTestEnv(), new Vector3(0, 0, 0), 0f);
+
+ model.Tick(6f, 0f, WindDirection.N);
+
+ Assert.True(model.IsDissipated);
+ }
+
+ [Fact]
+ public void Tick_DissipatesAtMaxRadius()
+ {
+ var model = new GaussianPuffDispersion();
+ var ammo = CreateTestAmmo();
+ ammo.MaxRadius = 60.0;
+ ammo.DispersionRateBase = 100.0; // 快速膨胀
+ model.Initialize(ammo, CreateTestEnv(), new Vector3(0, 0, 0), 0f);
+
+ model.Tick(1f, 0f, WindDirection.N);
+
+ Assert.True(model.IsDissipated);
+ }
+
+ [Fact]
+ public void ParticleOpacity_DecaysWithDensity()
+ {
+ var model = new GaussianPuffDispersion();
+ model.Initialize(CreateTestAmmo(), CreateTestEnv(0f, WindDirection.N), new Vector3(0, 0, 0), 0f);
+
+ Assert.Equal(1.0f, model.Particles.Opacity, 0.01f);
+
+ model.Tick(30f, 0f, WindDirection.N);
+
+ Assert.True(model.Particles.Opacity < 1.0f);
+ }
+ }
+}