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); + } + } +}