Phase 4-5: DefenseAdvisor重构 — 弹药规格驱动、输入校验、多轮次CloudSalvo;引擎改为纯执行器;Kinematics工具类
This commit is contained in:
parent
2ef16d083c
commit
615c2bea3b
@ -6,27 +6,23 @@ namespace CounterDrone.Core.Algorithms
|
||||
/// <summary>算法工厂 — 统一管理算法组件的创建与替换</summary>
|
||||
public static class AlgorithmFactory
|
||||
{
|
||||
private static readonly Dictionary<Type, Type> _registry = new()
|
||||
private static readonly Dictionary<Type, Func<object>> _registry = new()
|
||||
{
|
||||
{ typeof(ICloudDispersionModel), typeof(GaussianPuffDispersion) },
|
||||
{ typeof(IDamageModel), typeof(DamageModelRouter) },
|
||||
{ typeof(IDefenseAdvisor), typeof(DefaultDefenseAdvisor) },
|
||||
[typeof(ICloudDispersionModel)] = () => new GaussianPuffDispersion(),
|
||||
[typeof(IDamageModel)] = () => new DamageModelRouter(),
|
||||
[typeof(IDefenseAdvisor)] = () => new DefaultDefenseAdvisor(null),
|
||||
};
|
||||
|
||||
/// <summary>替换某个算法实现(第三方接入点)</summary>
|
||||
public static void Register<TInterface, TImpl>() where TImpl : TInterface, new()
|
||||
public static void Register<TInterface>(Func<object> factory)
|
||||
{
|
||||
_registry[typeof(TInterface)] = typeof(TImpl);
|
||||
_registry[typeof(TInterface)] = factory;
|
||||
}
|
||||
|
||||
/// <summary>创建算法实例</summary>
|
||||
public static T Create<T>() 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}");
|
||||
if (_registry.TryGetValue(typeof(T), out var factory))
|
||||
return (T)factory();
|
||||
throw new InvalidOperationException($"No implementation for {typeof(T).Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
public AerosolType RecommendedAerosolType { get; set; }
|
||||
public string AerosolRationale { get; set; } = string.Empty;
|
||||
public CloudDispersal RecommendedCloud { get; set; } = new();
|
||||
/// <summary>多轮次云团列表(含主云团)</summary>
|
||||
public List<CloudDispersal> CloudSalvo { get; set; } = new();
|
||||
|
||||
public List<RecommendedPlatform> Platforms { get; set; } = new();
|
||||
public List<RecommendedDetection> Detections { get; set; } = new();
|
||||
|
||||
@ -7,6 +7,13 @@ namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
public class DefaultDefenseAdvisor : IDefenseAdvisor
|
||||
{
|
||||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||||
|
||||
public DefaultDefenseAdvisor(List<AmmunitionSpec> ammoCatalog)
|
||||
{
|
||||
_ammoCatalog = ammoCatalog ?? new List<AmmunitionSpec>();
|
||||
}
|
||||
|
||||
private static readonly Dictionary<PowerType, AerosolType> MatchTable = new()
|
||||
{
|
||||
{ PowerType.Electric, AerosolType.InertGas },
|
||||
@ -16,161 +23,155 @@ namespace CounterDrone.Core.Algorithms
|
||||
|
||||
public DefenseRecommendation Recommend(ThreatProfile threat)
|
||||
{
|
||||
if (threat.Targets.Count == 0 || threat.Waypoints.Count < 2)
|
||||
return new DefenseRecommendation();
|
||||
var result = new DefenseRecommendation
|
||||
{
|
||||
Best = new DefenseSolution(),
|
||||
Critical = new DefenseSolution(),
|
||||
};
|
||||
|
||||
// ===== 输入校验 =====
|
||||
if (threat.Targets.Count == 0)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:未配置威胁目标";
|
||||
return result;
|
||||
}
|
||||
if (threat.Waypoints.Count < 2)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:需要至少两个航路点";
|
||||
return result;
|
||||
}
|
||||
|
||||
var target = threat.Targets[0];
|
||||
var route = threat.Waypoints;
|
||||
if (target.TypicalSpeed <= 0)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:目标速度为 0";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Step A:气溶胶选型
|
||||
// ===== Step A:气溶胶选型 =====
|
||||
var powerType = (PowerType)target.PowerType;
|
||||
var aerosolType = MatchTable.TryGetValue(powerType, out var match)
|
||||
? match : AerosolType.InertGas;
|
||||
|
||||
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)aerosolType);
|
||||
if (ammo == null)
|
||||
{
|
||||
result.Best.SummaryRationale = $"失败:弹药库中未找到 {aerosolType} 类型的弹药规格";
|
||||
return result;
|
||||
}
|
||||
|
||||
var rationale = powerType switch
|
||||
{
|
||||
PowerType.Electric or PowerType.Piston =>
|
||||
$"{powerType}发动机依赖氧气,推荐惰性气体窒息方案(吸入式灭火)",
|
||||
$"{powerType}发动机依赖氧气,推荐惰性气体窒息方案",
|
||||
PowerType.Jet =>
|
||||
$"{powerType}发动机高温表面可触发活性材料爆燃反应",
|
||||
_ => "默认推荐惰性气体方案",
|
||||
};
|
||||
|
||||
// Step B:时空交汇优化
|
||||
var start = ToV3(route[0]);
|
||||
var end = ToV3(route[^1]);
|
||||
// ===== Step B:时空交汇优化 =====
|
||||
var start = ToV3(threat.Waypoints[0]);
|
||||
var end = ToV3(threat.Waypoints[^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 avgSpeed = (float)target.TypicalSpeed / 3.6f;
|
||||
var totalFlightTime = routeLength / avgSpeed;
|
||||
|
||||
// 最佳值:航路中点
|
||||
var bestCloud = new CloudDispersal
|
||||
if (routeLength < 100)
|
||||
{
|
||||
result.Best.SummaryRationale = "失败:航路太短(<100m)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 弹药参数
|
||||
var initialR = (float)ammo.InitialRadius;
|
||||
var maxDur = (float)ammo.MaxDuration;
|
||||
|
||||
// 无人机穿过单个云团的时间
|
||||
var crossTime = (2f * initialR) / avgSpeed;
|
||||
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
|
||||
var roundsNeeded = Math.Max(1, (int)Math.Ceiling(neededExposure / Math.Max(0.5f, crossTime)));
|
||||
var spacing = initialR * 1.5f; // 云团间隔 = 1.5×半径,部分重叠保证连续覆盖
|
||||
|
||||
// 多轮次云团:沿航路等距分布
|
||||
var cloudSalvo = new List<CloudDispersal>();
|
||||
for (int i = 0; i < roundsNeeded; i++)
|
||||
{
|
||||
var offset = (i - (roundsNeeded - 1) / 2f) * spacing;
|
||||
cloudSalvo.Add(new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)aerosolType,
|
||||
PositionX = mid.X,
|
||||
PositionX = mid.X + offset,
|
||||
PositionY = mid.Y,
|
||||
PositionZ = mid.Z,
|
||||
DisperseHeight = (float)target.TypicalAltitude,
|
||||
TriggerMode = (int)TriggerMode.Time,
|
||||
Duration = 60,
|
||||
Duration = (int)maxDur,
|
||||
InitialScale = ammo.InitialVolume,
|
||||
ReleaseMode = (int)ReleaseMode.Single,
|
||||
Source = "Algorithm",
|
||||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||||
RecommendedTiming = flightTime / 2f,
|
||||
};
|
||||
RecommendedTiming = totalFlightTime / 2f,
|
||||
});
|
||||
}
|
||||
|
||||
var bestProb = EstimateInterceptProbability(route, avgSpeed, mid, 50f, 60f);
|
||||
var prob = Math.Min(0.95f, (crossTime * roundsNeeded) / neededExposure);
|
||||
|
||||
// 最危险值:航路 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
|
||||
// 平台部署
|
||||
var platQty = Math.Max(1, (int)Math.Ceiling(roundsNeeded / 3f));
|
||||
var platforms = new List<RecommendedPlatform>();
|
||||
for (int i = 0; i < platQty; i++)
|
||||
platforms.Add(new RecommendedPlatform
|
||||
{
|
||||
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,
|
||||
};
|
||||
Type = PlatformType.GroundBased,
|
||||
Position = new Vector3(mid.X + i * 300, 0, 50),
|
||||
Quantity = 1,
|
||||
MunitionCount = (int)Math.Ceiling((float)roundsNeeded / platQty),
|
||||
CoverageVolume = (float)ammo.InitialVolume,
|
||||
});
|
||||
|
||||
var criticalProb = EstimateInterceptProbability(route, avgSpeed, criticalPos, 50f, 60f);
|
||||
|
||||
// Step C:反推平台部署
|
||||
var bestPlatforms = RecommendPlatforms(bestCloud, target);
|
||||
|
||||
return new DefenseRecommendation
|
||||
{
|
||||
Best = new DefenseSolution
|
||||
result.Best = new DefenseSolution
|
||||
{
|
||||
RecommendedAerosolType = aerosolType,
|
||||
AerosolRationale = rationale,
|
||||
RecommendedCloud = bestCloud,
|
||||
Platforms = bestPlatforms,
|
||||
RecommendedCloud = cloudSalvo[0],
|
||||
CloudSalvo = cloudSalvo,
|
||||
Platforms = platforms,
|
||||
Detections = new List<RecommendedDetection>
|
||||
{
|
||||
new RecommendedDetection
|
||||
{
|
||||
Position = new Vector3(mid.X, mid.Y, 0),
|
||||
DetectionRadius = System.Math.Max(3000f, routeLength * 0.4f),
|
||||
DetectionRadius = Math.Max(3000f, routeLength * 0.4f),
|
||||
Quantity = 1,
|
||||
}
|
||||
},
|
||||
InterceptProbability = bestProb,
|
||||
SummaryRationale = $"最佳方案:{aerosolType}方案,预计拦截概率 {bestProb:P0}",
|
||||
},
|
||||
Critical = new DefenseSolution
|
||||
InterceptProbability = prob,
|
||||
SummaryRationale = $"{aerosolType}方案,{roundsNeeded}发,预计拦截概率 {prob:P0}",
|
||||
};
|
||||
|
||||
result.Critical = new DefenseSolution
|
||||
{
|
||||
RecommendedAerosolType = aerosolType,
|
||||
AerosolRationale = "临界方案",
|
||||
RecommendedCloud = criticalCloud,
|
||||
Platforms = RecommendPlatforms(criticalCloud, target),
|
||||
Detections = new List<RecommendedDetection>(),
|
||||
InterceptProbability = criticalProb,
|
||||
SummaryRationale = $"最危险方案:拦截概率仅 {criticalProb:P0}",
|
||||
RecommendedCloud = new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)aerosolType,
|
||||
PositionX = start.X + (end.X - start.X) * 0.25f,
|
||||
PositionY = start.Y, PositionZ = start.Z,
|
||||
DisperseHeight = (float)target.TypicalAltitude,
|
||||
Duration = (int)maxDur,
|
||||
Source = "Algorithm",
|
||||
PositionMode = (int)PositionMode.AlgorithmRecommended,
|
||||
RecommendedTiming = totalFlightTime * 0.25f,
|
||||
},
|
||||
Platforms = platforms,
|
||||
InterceptProbability = prob * 0.3f,
|
||||
SummaryRationale = $"临界方案:拦截概率仅 {prob * 0.3f:P0}",
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<RecommendedPlatform> RecommendPlatforms(CloudDispersal cloud, TargetConfig target)
|
||||
{
|
||||
// 简化:默认地基火炮
|
||||
var platforms = new List<RecommendedPlatform>();
|
||||
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<Waypoint> 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);
|
||||
}
|
||||
private static Vector3 ToV3(Waypoint wp) => new((float)wp.PosX, (float)wp.PosY, (float)wp.PosZ);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,14 +21,10 @@ namespace CounterDrone.Core.Tests
|
||||
[Fact]
|
||||
public void Register_ReplacesImplementation()
|
||||
{
|
||||
// 替换为自定义实现
|
||||
AlgorithmFactory.Register<ICloudDispersionModel, MockDispersion>();
|
||||
|
||||
AlgorithmFactory.Register<ICloudDispersionModel>(() => new MockDispersion());
|
||||
var instance = AlgorithmFactory.Create<ICloudDispersionModel>();
|
||||
Assert.IsType<MockDispersion>(instance);
|
||||
|
||||
// 还原
|
||||
AlgorithmFactory.Register<ICloudDispersionModel, GaussianPuffDispersion>();
|
||||
AlgorithmFactory.Register<ICloudDispersionModel>(() => new GaussianPuffDispersion());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,12 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DefenseAdvisorRealityTests
|
||||
{
|
||||
private static readonly List<AmmunitionSpec> TestAmmo = new()
|
||||
{
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.InertGas, InitialRadius = 50, CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60 },
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveMaterial, InitialRadius = 30, CoreDensity = 2.0, DispersionRateBase = 3, MaxRadius = 150, MaxDuration = 45 },
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void PistonDrone_RecommendsReachableCloudPosition()
|
||||
{
|
||||
@ -34,7 +40,7 @@ namespace CounterDrone.Core.Tests
|
||||
},
|
||||
};
|
||||
|
||||
var advisor = new DefaultDefenseAdvisor();
|
||||
var advisor = new DefaultDefenseAdvisor(TestAmmo);
|
||||
var rec = advisor.Recommend(threat);
|
||||
|
||||
// 1. 选型正确
|
||||
|
||||
@ -7,119 +7,65 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class DefenseAdvisorTests
|
||||
{
|
||||
private static readonly List<AmmunitionSpec> TestAmmo = new()
|
||||
{
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.InertGas, InitialRadius = 50, CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60, EffectiveConcentration = 0.05 },
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveMaterial, InitialRadius = 30, CoreDensity = 2.0, DispersionRateBase = 3, MaxRadius = 150, MaxDuration = 45, EffectiveConcentration = 0.1 },
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveFuel, InitialRadius = 40, CoreDensity = 1.5, DispersionRateBase = 4, MaxRadius = 180, MaxDuration = 50, EffectiveConcentration = 0.03 },
|
||||
};
|
||||
|
||||
private ThreatProfile CreateSimpleThreat(PowerType powerType = PowerType.Piston)
|
||||
{
|
||||
return new ThreatProfile
|
||||
{
|
||||
Environment = new CombatScene
|
||||
{
|
||||
WindSpeed = 5.0,
|
||||
WindDirection = (int)WindDirection.E,
|
||||
},
|
||||
Environment = new CombatScene { WindSpeed = 5.0, WindDirection = (int)WindDirection.E },
|
||||
Targets = new List<TargetConfig>
|
||||
{
|
||||
new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston,
|
||||
PowerType = (int)powerType,
|
||||
Quantity = 3,
|
||||
TypicalSpeed = 120.0,
|
||||
TypicalAltitude = 500.0,
|
||||
},
|
||||
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<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 500, Speed = 120 },
|
||||
new Waypoint { PosX = 10000, PosY = 0, PosZ = 100, Altitude = 500, Speed = 120 },
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 100, Altitude = 500, Speed = 120 },
|
||||
new Waypoint { PosX = 10000, PosY = 500, PosZ = 100, Altitude = 500, Speed = 120 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recommend_PistonEngine_RecommendsInertGas()
|
||||
{
|
||||
var advisor = new DefaultDefenseAdvisor();
|
||||
var result = advisor.Recommend(CreateSimpleThreat(PowerType.Piston));
|
||||
[Fact] public void Piston_InertGas() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat(PowerType.Piston)); Assert.Equal(AerosolType.InertGas, r.Best.RecommendedAerosolType); Assert.Contains("惰性气体", r.Best.AerosolRationale); Assert.True(r.Best.InterceptProbability > 0); }
|
||||
[Fact] public void Jet_ActiveMaterial() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat(PowerType.Jet)); Assert.Equal(AerosolType.ActiveMaterial, r.Best.RecommendedAerosolType); Assert.Contains("活性材料", r.Best.AerosolRationale); }
|
||||
[Fact] public void Electric_InertGas() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat(PowerType.Electric)); Assert.Equal(AerosolType.InertGas, r.Best.RecommendedAerosolType); Assert.Contains("惰性气体", r.Best.AerosolRationale); }
|
||||
[Fact] public void Best_Critical_Ordered() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.True(r.Best.InterceptProbability >= r.Critical.InterceptProbability); }
|
||||
[Fact] public void Cloud_OnRoute() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.True(r.Best.RecommendedCloud.PositionX > 1000 && r.Best.RecommendedCloud.PositionX < 9000); }
|
||||
[Fact] public void Has_Platforms() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.NotEmpty(r.Best.Platforms); }
|
||||
[Fact] public void Has_Detection() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.NotEmpty(r.Best.Detections); }
|
||||
[Fact] public void Source_Algorithm() { var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(CreateSimpleThreat()); Assert.Equal("Algorithm", r.Best.RecommendedCloud.Source); }
|
||||
|
||||
Assert.Equal(AerosolType.InertGas, result.Best.RecommendedAerosolType);
|
||||
Assert.Contains("惰性气体", result.Best.AerosolRationale);
|
||||
Assert.True(result.Best.InterceptProbability > 0f);
|
||||
[Fact]
|
||||
public void NoTargets_ReturnsFailure()
|
||||
{
|
||||
var threat = CreateSimpleThreat();
|
||||
threat.Targets.Clear();
|
||||
var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(threat);
|
||||
Assert.Equal(0f, r.Best.InterceptProbability);
|
||||
Assert.Contains("失败", r.Best.SummaryRationale);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recommend_JetEngine_RecommendsActiveMaterial()
|
||||
public void NoWaypoints_ReturnsFailure()
|
||||
{
|
||||
var advisor = new DefaultDefenseAdvisor();
|
||||
var result = advisor.Recommend(CreateSimpleThreat(PowerType.Jet));
|
||||
|
||||
Assert.Equal(AerosolType.ActiveMaterial, result.Best.RecommendedAerosolType);
|
||||
Assert.Contains("活性材料", result.Best.AerosolRationale);
|
||||
var threat = CreateSimpleThreat();
|
||||
threat.Waypoints.Clear();
|
||||
var r = new DefaultDefenseAdvisor(TestAmmo).Recommend(threat);
|
||||
Assert.Equal(0f, r.Best.InterceptProbability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Recommend_ElectricEngine_RecommendsInertGas()
|
||||
public void NoAmmo_ReturnsFailure()
|
||||
{
|
||||
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);
|
||||
var r = new DefaultDefenseAdvisor(new List<AmmunitionSpec>()).Recommend(CreateSimpleThreat());
|
||||
Assert.Equal(0f, r.Best.InterceptProbability);
|
||||
Assert.Contains("未找到", r.Best.SummaryRationale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
92
test/unit/CounterDrone.Core.Tests/FireScheduleDiagnostics.cs
Normal file
92
test/unit/CounterDrone.Core.Tests/FireScheduleDiagnostics.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Repository;
|
||||
using CounterDrone.Core.Services;
|
||||
using CounterDrone.Core.Simulation;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
/// <summary>诊断 BuildFireSchedule 是否生成正确计划</summary>
|
||||
public class FireScheduleDiagnostics
|
||||
{
|
||||
private static readonly List<AmmunitionSpec> TestAmmo = new()
|
||||
{
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.InertGas, InitialRadius = 50, CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60 },
|
||||
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveMaterial, InitialRadius = 30, CoreDensity = 2.0, DispersionRateBase = 3, MaxRadius = 150, MaxDuration = 45 },
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void BuildFireSchedule_WithRecommendation_ProducesEvents()
|
||||
{
|
||||
// 模拟:活塞无人机威胁 → 算法推荐 → 发射计划
|
||||
var threat = new ThreatProfile
|
||||
{
|
||||
Environment = new CombatScene { WindSpeed = 3 },
|
||||
Targets = new List<TargetConfig>
|
||||
{
|
||||
new TargetConfig { TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston, Quantity = 1, TypicalSpeed = 200 },
|
||||
},
|
||||
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
|
||||
Waypoints = new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
},
|
||||
};
|
||||
|
||||
var rec = new DefaultDefenseAdvisor(TestAmmo).Recommend(threat);
|
||||
|
||||
// 检查推荐
|
||||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0,
|
||||
$"推荐时机={rec.Best.RecommendedCloud.RecommendedTiming}");
|
||||
Assert.NotEmpty(rec.Best.Platforms);
|
||||
|
||||
// 模拟持久化后读取
|
||||
var cloud = rec.Best.RecommendedCloud;
|
||||
var equips = new List<EquipmentDeployment>();
|
||||
foreach (var p in rec.Best.Platforms)
|
||||
equips.Add(new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
Quantity = p.Quantity,
|
||||
PositionX = p.Position.X, PositionY = p.Position.Y, PositionZ = p.Position.Z,
|
||||
AerosolType = (int)rec.Best.RecommendedAerosolType,
|
||||
MunitionCount = p.MunitionCount, Cooldown = 5,
|
||||
MuzzleVelocity = 800,
|
||||
});
|
||||
|
||||
// 手动调用 BuildFireSchedule 同样的逻辑
|
||||
var schedule = new List<FireEvent>();
|
||||
if (cloud.RecommendedTiming.HasValue && cloud.RecommendedTiming.Value > 0)
|
||||
{
|
||||
foreach (var p in equips)
|
||||
{
|
||||
var muzzleV = (float)(p.MuzzleVelocity ?? 800);
|
||||
var dx = (float)cloud.PositionX - (float)p.PositionX;
|
||||
var dz = (float)cloud.PositionZ - (float)p.PositionZ;
|
||||
var dist = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||||
var flightTime = dist / muzzleV;
|
||||
var fireTime = (float)cloud.RecommendedTiming.Value - flightTime;
|
||||
if (fireTime < 0) fireTime = 0.1f;
|
||||
|
||||
schedule.Add(new FireEvent
|
||||
{
|
||||
FireTime = fireTime,
|
||||
PlatformIndex = 0,
|
||||
TargetX = (float)cloud.PositionX,
|
||||
TargetY = (float)cloud.PositionY,
|
||||
TargetZ = (float)cloud.PositionZ,
|
||||
MuzzleVelocity = muzzleV,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Assert.NotEmpty(schedule);
|
||||
Assert.True(schedule[0].FireTime > 0);
|
||||
Assert.Equal((float)cloud.PositionX, schedule[0].TargetX);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ namespace CounterDrone.Core.Tests
|
||||
private readonly IScenarioService _scenario;
|
||||
private readonly FrameDataStore _frameStore;
|
||||
private readonly SQLiteConnection _mainDb;
|
||||
private List<AmmunitionSpec> _ammoCatalog;
|
||||
private string _taskId = string.Empty;
|
||||
|
||||
public FullPipelineTests()
|
||||
@ -32,16 +33,16 @@ namespace CounterDrone.Core.Tests
|
||||
_mainDb.Insert(new AmmunitionSpec
|
||||
{
|
||||
Id = "ammo-inert", AerosolType = (int)AerosolType.InertGas,
|
||||
Name = "惰性气体弹", InitialRadius = 50, CoreDensity = 1.0,
|
||||
DispersionRateBase = 5, MaxRadius = 500, MaxDuration = 120,
|
||||
EffectiveConcentration = 0.05,
|
||||
Name = "惰性气体弹", InitialRadius = 300, CoreDensity = 2.0,
|
||||
DispersionRateBase = 15, MaxRadius = 1000, MaxDuration = 180,
|
||||
EffectiveConcentration = 0.03,
|
||||
});
|
||||
_mainDb.Insert(new AmmunitionSpec
|
||||
{
|
||||
Id = "ammo-active", AerosolType = (int)AerosolType.ActiveMaterial,
|
||||
Name = "活性材料弹", InitialRadius = 30, CoreDensity = 2.0,
|
||||
DispersionRateBase = 3, MaxRadius = 400, MaxDuration = 90,
|
||||
EffectiveConcentration = 0.1,
|
||||
Name = "活性材料弹", InitialRadius = 250, CoreDensity = 3.0,
|
||||
DispersionRateBase = 20, MaxRadius = 800, MaxDuration = 120,
|
||||
EffectiveConcentration = 0.05,
|
||||
});
|
||||
_mainDb.Insert(new AmmunitionSpec
|
||||
{
|
||||
@ -51,6 +52,8 @@ namespace CounterDrone.Core.Tests
|
||||
EffectiveConcentration = 0.03,
|
||||
});
|
||||
|
||||
_ammoCatalog = _mainDb.Table<AmmunitionSpec>().ToList();
|
||||
|
||||
_scenario = new ScenarioService(
|
||||
new SimTaskRepository(_mainDb), new CombatSceneRepository(_mainDb),
|
||||
new ControlZoneRepository(_mainDb), new TargetConfigRepository(_mainDb),
|
||||
@ -75,7 +78,18 @@ namespace CounterDrone.Core.Tests
|
||||
if (detail.Equipment.Any(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform))
|
||||
{
|
||||
var schedule = BuildFireSchedule(detail);
|
||||
if (schedule.Count > 0) engine.SetFireSchedule(schedule);
|
||||
// 诊断:如果计划为空,检查原始数据
|
||||
if (schedule.Count == 0)
|
||||
{
|
||||
var c = detail.Cloud;
|
||||
throw new InvalidOperationException(
|
||||
$"FireSchedule empty. " +
|
||||
$"RecommendedTiming={c.RecommendedTiming}, " +
|
||||
$"CloudPos=({c.PositionX},{c.PositionY},{c.PositionZ}), " +
|
||||
$"PlatformCount={detail.Equipment.Count(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform)}, " +
|
||||
$"EquipCount={detail.Equipment.Count}");
|
||||
}
|
||||
engine.SetFireSchedule(schedule);
|
||||
}
|
||||
|
||||
engine.Initialize(_taskId);
|
||||
@ -141,7 +155,7 @@ namespace CounterDrone.Core.Tests
|
||||
Route = detail.Route,
|
||||
Waypoints = detail.Waypoints,
|
||||
};
|
||||
var rec = new DefaultDefenseAdvisor().Recommend(threat);
|
||||
var rec = new DefaultDefenseAdvisor(_ammoCatalog).Recommend(threat);
|
||||
|
||||
_scenario.SaveCloudDispersal(_taskId, rec.Best.RecommendedCloud);
|
||||
|
||||
@ -274,14 +288,25 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.True(saved.Cloud.RecommendedTiming.HasValue,
|
||||
$"Piston: RecommendedTiming={saved.Cloud.RecommendedTiming}");
|
||||
Assert.True(saved.Cloud.RecommendedTiming > 0);
|
||||
Assert.NotEmpty(saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform),
|
||||
"应有发射平台");
|
||||
var platforms = saved.Equipment.Where(e => e.EquipmentRole == (int)EquipmentRole.LaunchPlatform).ToList();
|
||||
Assert.True(platforms.Count > 0, "应有发射平台");
|
||||
|
||||
// 直接验证 BuildFireSchedule 逻辑
|
||||
var c = saved.Cloud;
|
||||
var p = platforms[0];
|
||||
var mv = (float)(p.MuzzleVelocity ?? 800);
|
||||
var dx = (float)c.PositionX - (float)p.PositionX;
|
||||
var dz = (float)c.PositionZ - (float)p.PositionZ;
|
||||
var dist = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||||
var ft = dist / mv;
|
||||
var fireTime = (float)c.RecommendedTiming!.Value - ft;
|
||||
Assert.True(fireTime > 0, $"发射时间={fireTime}, 距离={dist}, 飞行时间={ft}, 推荐时机={c.RecommendedTiming}");
|
||||
|
||||
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
|
||||
Assert.True(rec.Best.InterceptProbability > 0);
|
||||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0);
|
||||
|
||||
var eng = RunSimulation(3000);
|
||||
var eng = RunSimulation(8000); // 200 km/h, 20km, ~360s 航程,需充足时间
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
Assert.True(launched > 0, $"Piston: 应发射弹药,实际{launched}次");
|
||||
Assert.Equal(DroneStatus.Destroyed, eng.Drones[0].Status);
|
||||
@ -321,7 +346,7 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.True(rec.Best.InterceptProbability > 0);
|
||||
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0);
|
||||
|
||||
var eng = RunSimulation(3000);
|
||||
var eng = RunSimulation(8000); // 500 km/h, 30km, ~216s 航程
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
Assert.True(launched > 0, $"Jet: 应发射弹药,实际{launched}次");
|
||||
Assert.Equal(DroneStatus.Destroyed, eng.Drones[0].Status);
|
||||
|
||||
@ -147,41 +147,233 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.True(result.NewEvents.Count >= 0); // 至少不崩溃
|
||||
}
|
||||
|
||||
// 跳过:无人机到达终点受防御装备干扰,需重构测试场景
|
||||
/*
|
||||
// ========== 端到端集成测试 ==========
|
||||
|
||||
[Fact]
|
||||
public void Tick_DroneReachesEnd_Completes()
|
||||
public void FullScenario_NoDefense_DroneReachesTarget()
|
||||
{
|
||||
SetupScenario();
|
||||
var task = _scenarioService.CreateTask("无防御测试", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenarioService.SaveScene(_taskId, new CombatScene
|
||||
{
|
||||
SceneType = (int)SceneType.Plain, WindSpeed = 0,
|
||||
WindDirection = (int)WindDirection.N,
|
||||
SceneWidth = 5000, SceneLength = 5000,
|
||||
});
|
||||
_scenarioService.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston,
|
||||
Quantity = 1,
|
||||
TargetType = (int)TargetType.Piston, Quantity = 1,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
TypicalSpeed = 300, // 高速
|
||||
TypicalAltitude = 300,
|
||||
TypicalSpeed = 120, TypicalAltitude = 300,
|
||||
});
|
||||
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1, DetectionRadius = 10000,
|
||||
},
|
||||
});
|
||||
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)AerosolType.InertGas, Duration = 60,
|
||||
});
|
||||
_scenarioService.SaveRoute(_taskId, new RoutePlan
|
||||
{
|
||||
FormationMode = (int)FormationMode.Single,
|
||||
}, new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 300, Speed = 300 },
|
||||
new Waypoint { PosX = 2000, PosY = 0, PosZ = 100, Altitude = 300, Speed = 300 },
|
||||
new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 300, Speed = 120 },
|
||||
new Waypoint { PosX = 1000, PosY = 0, PosZ = 100, Altitude = 300, Speed = 120 },
|
||||
});
|
||||
|
||||
_engine.Initialize(_taskId);
|
||||
|
||||
// 无防御时无人机应能到达终点
|
||||
for (int i = 0; i < 800; i++)
|
||||
{
|
||||
_engine.Tick(1f / 20f);
|
||||
var r = _engine.Tick(1f / 20f);
|
||||
if (_engine.Drones[0].Status == DroneStatus.ReachedTarget) break;
|
||||
if (r.State == SimulationState.Completed) break;
|
||||
}
|
||||
|
||||
Assert.Equal(DroneStatus.ReachedTarget, _engine.Drones[0].Status);
|
||||
}
|
||||
*/
|
||||
|
||||
[Fact]
|
||||
public void FullScenario_InterceptSuccess_DroneDestroyed()
|
||||
{
|
||||
var task = _scenarioService.CreateTask("拦截测试", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenarioService.SaveScene(_taskId, new CombatScene
|
||||
{
|
||||
SceneType = (int)SceneType.Plain, WindSpeed = 0,
|
||||
WindDirection = (int)WindDirection.N,
|
||||
});
|
||||
_scenarioService.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Piston, Quantity = 1,
|
||||
PowerType = (int)PowerType.Piston,
|
||||
TypicalSpeed = 60, TypicalAltitude = 300,
|
||||
});
|
||||
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1, DetectionRadius = 20000,
|
||||
},
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.LaunchPlatform,
|
||||
PlatformType = (int)PlatformType.GroundBased,
|
||||
Quantity = 2,
|
||||
PositionX = 0, PositionY = 0, PositionZ = 0,
|
||||
AerosolType = (int)AerosolType.InertGas,
|
||||
MunitionCount = 10, Cooldown = 0.2f,
|
||||
},
|
||||
});
|
||||
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal
|
||||
{
|
||||
AerosolType = (int)AerosolType.InertGas,
|
||||
DisperseHeight = 300, Duration = 60,
|
||||
});
|
||||
_scenarioService.SaveRoute(_taskId, new RoutePlan
|
||||
{
|
||||
FormationMode = (int)FormationMode.Single,
|
||||
}, new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 300, Speed = 60 },
|
||||
new Waypoint { PosX = 5000, PosY = 0, PosZ = 100, Altitude = 300, Speed = 60 },
|
||||
});
|
||||
|
||||
_engine.Initialize(_taskId);
|
||||
|
||||
var destroyed = false;
|
||||
var cloudsGenerated = false;
|
||||
for (int i = 0; i < 2000 && !destroyed; i++)
|
||||
{
|
||||
var r = _engine.Tick(1f / 20f);
|
||||
if (r.NewEvents.Any(e => e.Type == SimEventType.CloudGenerated))
|
||||
cloudsGenerated = true;
|
||||
if (_engine.Drones[0].Status == DroneStatus.Destroyed)
|
||||
destroyed = true;
|
||||
if (r.State == SimulationState.Completed) break;
|
||||
}
|
||||
|
||||
Assert.True(cloudsGenerated, "应该生成了云团");
|
||||
Assert.True(destroyed, "无人机应被摧毁");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullScenario_ZoneIntrusion_DroneStopped()
|
||||
{
|
||||
var task = _scenarioService.CreateTask("管控区测试", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenarioService.SaveScene(_taskId, new CombatScene
|
||||
{
|
||||
SceneType = (int)SceneType.Urban, WindSpeed = 0,
|
||||
WindDirection = (int)WindDirection.N,
|
||||
});
|
||||
_scenarioService.SaveControlZones(_taskId, new List<Models.ControlZone>
|
||||
{
|
||||
new Models.ControlZone
|
||||
{
|
||||
Name = "核心禁飞区",
|
||||
VerticesJson = "[{\"X\":300,\"Y\":0,\"Z\":0},{\"X\":700,\"Y\":0,\"Z\":0},{\"X\":700,\"Y\":0,\"Z\":400},{\"X\":300,\"Y\":0,\"Z\":400}]",
|
||||
MinAltitude = 0, MaxAltitude = 1000,
|
||||
},
|
||||
});
|
||||
_scenarioService.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.FixedWing, Quantity = 1,
|
||||
PowerType = (int)PowerType.Jet,
|
||||
TypicalSpeed = 150, TypicalAltitude = 300,
|
||||
});
|
||||
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1, DetectionRadius = 10000,
|
||||
},
|
||||
});
|
||||
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal());
|
||||
_scenarioService.SaveRoute(_taskId, new RoutePlan
|
||||
{
|
||||
FormationMode = (int)FormationMode.Single,
|
||||
}, new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 300, Speed = 150 },
|
||||
new Waypoint { PosX = 2000, PosY = 0, PosZ = 100, Altitude = 300, Speed = 150 },
|
||||
});
|
||||
|
||||
_engine.Initialize(_taskId);
|
||||
|
||||
var intruded = false;
|
||||
for (int i = 0; i < 1000 && !intruded; i++)
|
||||
{
|
||||
var r = _engine.Tick(1f / 20f);
|
||||
if (r.NewEvents.Any(e => e.Type == SimEventType.ZoneIntruded))
|
||||
intruded = true;
|
||||
if (_engine.Drones[0].Status == DroneStatus.ZoneIntruded)
|
||||
intruded = true;
|
||||
if (r.State == SimulationState.Completed) break;
|
||||
}
|
||||
|
||||
Assert.True(intruded, "无人机应触发管控区域侵入");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullScenario_MultiDroneFormation()
|
||||
{
|
||||
var task = _scenarioService.CreateTask("编队测试", "");
|
||||
_taskId = task.Id;
|
||||
|
||||
_scenarioService.SaveScene(_taskId, new CombatScene
|
||||
{
|
||||
SceneType = (int)SceneType.Plain, WindSpeed = 3,
|
||||
WindDirection = (int)WindDirection.W,
|
||||
});
|
||||
_scenarioService.SaveTarget(_taskId, new TargetConfig
|
||||
{
|
||||
TargetType = (int)TargetType.Rotor, Quantity = 3,
|
||||
PowerType = (int)PowerType.Electric,
|
||||
TypicalSpeed = 80, TypicalAltitude = 200,
|
||||
});
|
||||
_scenarioService.SaveDeployment(_taskId, new List<EquipmentDeployment>
|
||||
{
|
||||
new EquipmentDeployment
|
||||
{
|
||||
EquipmentRole = (int)EquipmentRole.Detection,
|
||||
Quantity = 1, DetectionRadius = 5000,
|
||||
},
|
||||
});
|
||||
_scenarioService.SaveCloudDispersal(_taskId, new CloudDispersal());
|
||||
_scenarioService.SaveRoute(_taskId, new RoutePlan
|
||||
{
|
||||
FormationMode = (int)FormationMode.Formation,
|
||||
FormationSpacing = 50,
|
||||
}, new List<Waypoint>
|
||||
{
|
||||
new Waypoint { PosX = 0, PosY = 0, PosZ = 100, Altitude = 200, Speed = 80 },
|
||||
new Waypoint { PosX = 3000, PosY = 0, PosZ = 100, Altitude = 200, Speed = 80 },
|
||||
});
|
||||
|
||||
_engine.Initialize(_taskId);
|
||||
|
||||
Assert.Equal(3, _engine.Drones.Count);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
_engine.Tick(1f / 20f);
|
||||
|
||||
Assert.All(_engine.Drones, d => Assert.Equal(DroneStatus.Flying, d.Status));
|
||||
|
||||
var yPositions = _engine.Drones.Select(d => d.PosY).ToList();
|
||||
Assert.NotEqual(yPositions[0], yPositions[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PauseAndResume()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user