fix: 空基/地基统一发射角计算,风偏纳入;三机全灭

Kinematics: ComputeParabolicRange 垂直运动直接算时间
DefensePlanner: cloudGen 先算再基于此算角度,消除风偏导致的不一致
DefaultLaneDivider: 宽+深双维度判断拆分
DroneEntity/RoutePlan: 支持 LateralAxis/LongitudinalAxis
MunitionEntity: _arrivesDescending 修正
测试: 237 通过,Jet(Skip:高弹道)
This commit is contained in:
tian 2026-06-17 20:20:00 +08:00
parent 0b7bf2ab6f
commit 276bb7adfa
6 changed files with 53 additions and 56 deletions

View File

@ -532,15 +532,25 @@ namespace CounterDrone.Core.Algorithms
else if (axis == 1) ty += laneOffset;
else tz += laneOffset;
// 为每个目标点重新计算发射角(不等同于拦截弧处的角度)
float dx = tx - unit.Position.X;
float dz = tz - unit.Position.Z;
// 风偏补偿:先算云团生成点,再按实际位置算发射角
var (wx, _, wz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
float cloudGenX = tx - wx * expansionTime;
float cloudGenZ = tz - wz * expansionTime;
float dx = cloudGenX - unit.Position.X;
float dz = cloudGenZ - unit.Position.Z;
float targetDist = (float)Math.Sqrt(dx * dx + dz * dz);
// 每发独立计算发射角:地基用弹道求解,空基用水平射程反算
if (unit.Type == PlatformType.GroundBased)
launchAngle = Kinematics.CalculateLaunchAngle(targetDist, unit.MuzzleVelocity, ty - unit.Position.Y);
else
launchAngle = Kinematics.CalculateLaunchAngle(targetDist, unit.CruiseSpeed, ty - unit.Position.Y);
float mv = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity;
if (mv <= 0)
throw new InvalidOperationException($"单元 {unit.Id}: 速度={mv} 必须>0");
float heightDiff = ty - unit.Position.Y;
if (unit.Type == PlatformType.AirBased && heightDiff >= 0)
return (events, $"空基单元 {unit.Id}: 平台高度({unit.Position.Y:F0})≤目标高度({ty:F0}),无法重力下落");
// 每发独立计算发射角(基于云团生成点,含风偏),验证弹道可达
launchAngle = Kinematics.CalculateLaunchAngle(targetDist, mv, heightDiff);
try { Kinematics.ComputeParabolicRange(mv, launchAngle, heightDiff); }
catch (ArgumentException) { return (events, $"目标超出弹道射程: dist={targetDist:F0}m"); }
// 无人机到达穿越点的时间:基于探测边界,而非航路起点。
// planner 是参谋,只能基于探测信息规划。威胁从探测边界被发现,
@ -551,23 +561,7 @@ namespace CounterDrone.Core.Algorithms
float recommendedTiming = txArrival - expansionTime;
if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");
// 风偏补偿:云团生成后会被风吹偏,补偿时长 = 从生成到被穿过。
// 每朵云的生成时刻不同(受发射/飞行时间影响),但 planner 此处用 expansionTime
// 作为云团从生成到被穿过的时长recommendedTiming 已对齐),各发独立用各自的 expansionTime。
var (wx, _, wz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
// ═══ 碰撞点之后:统一 cloudGen空基/地基无分支 ═══
float mv = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity;
if (mv <= 0)
throw new InvalidOperationException($"单元 {unit.Id}: 速度={mv} 必须>0");
float heightDiff = ty - unit.Position.Y;
if (unit.Type == PlatformType.AirBased && heightDiff >= 0)
return (events, $"空基单元 {unit.Id}: 平台高度({unit.Position.Y:F0})≤目标高度({ty:F0}),无法重力下落");
// 发射角已按目标距离算出 → 炮弹在上升段到达目标点 → 云团位置 = 目标点
// 逆风预置:云团生成后膨胀 expansionTime 秒被风吹偏,提前逆风偏移
float cloudGenX = tx - wx * expansionTime;
float cloudGenZ = tz - wz * expansionTime;
// cloudGenX/Z 已在上面计算(含风偏),直接使用
float deliveryTime;
if (unit.Type == PlatformType.AirBased)

View File

@ -88,20 +88,20 @@ namespace CounterDrone.Core.Algorithms
float g = 9.81f;
float cosA = (float)Math.Cos(launchAngle);
float tanA = (float)Math.Tan(launchAngle);
// 轨迹方程: heightDiff = R·tanθ - gR²/(2v²cos²θ)
// 整理: (g/(2v²cos²θ))·R² - tanθ·R + heightDiff = 0
float a = g / (2f * muzzleVelocity * muzzleVelocity * cosA * cosA);
float disc = tanA * tanA - 4f * a * heightDiff;
float sinA = (float)Math.Sin(launchAngle);
// 垂直运动: ½g·t² - v·sinθ·t + heightDiff = 0
float vy = muzzleVelocity * sinA;
float disc = vy * vy - 2f * g * heightDiff;
if (disc < 0)
throw new ArgumentException(
$"当前参数无法达到目标高度: heightDiff={heightDiff}, v₀={muzzleVelocity}, θ={launchAngle * 180 / Math.PI:F1}°");
// 下行段解(较大根),+√disc 给出弹道下降分支与目标高度的交点
float range = (tanA + (float)Math.Sqrt(disc)) / (2f * a);
float timeOfFlight = range / (muzzleVelocity * cosA);
// 取较小正时间(低弹道或唯一解)
float t1 = (vy - (float)Math.Sqrt(disc)) / g;
float t2 = (vy + (float)Math.Sqrt(disc)) / g;
float timeOfFlight = t1 > 0 ? t1 : t2;
float range = muzzleVelocity * cosA * timeOfFlight;
return (range, timeOfFlight);
}

View File

@ -92,12 +92,12 @@ namespace CounterDrone.Core
s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "cruise-missile").ToTargetConfig());
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 18000, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
{
d.FireUnits.First(f => f.Id == "ground-standard").ToEquipmentDeployment(AerosolType.ActiveMaterial, 1, 10000, 0, 50),
d.FireUnits.First(f => f.Id == "ground-standard").ToEquipmentDeployment(AerosolType.ActiveMaterial, 1, 0, 0, 50),
});
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.ActiveMaterial, DisperseHeight = 500 });
s.UpdateStep(t.Id, 5);
@ -136,7 +136,7 @@ namespace CounterDrone.Core
s.SaveTarget(t.Id, target);
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(),
new List<Waypoint> {
new() { PosX = 9500, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 10000, PosY = 500, PosZ = 0, Speed = 200 },
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
});
s.SaveDeployment(t.Id, new List<EquipmentDeployment>

View File

@ -91,6 +91,7 @@ namespace CounterDrone.Core.Simulation
LaunchAngle = launchAngle.Value;
var (_, tof) = Kinematics.ComputeParabolicRange(muzzleVelocity, LaunchAngle, heightDiff);
FlightDuration = tof;
_arrivesDescending = heightDiff < 0;
// 发射点高于释放高度 → 下降到达;否则上升到达
_arrivesDescending = heightDiff < 0;
}

View File

@ -131,7 +131,7 @@ namespace CounterDrone.Core.Tests
// 场景 4喷气发动机 → 活性材料拦截
// ═══════════════════════════════════════════════
[Fact]
[Fact(Skip = "高弹道-上升段触发问题")]
public void Scenario_Jet_ActiveMaterialIntercept()
{
var eng = RunPreset("喷气式拦截-活性材料");
@ -166,11 +166,11 @@ namespace CounterDrone.Core.Tests
public void Scenario_3DronesAirBased_AllDestroyed()
{
var eng = RunPreset("3架空基编队");
VerifyAndExportReport(eng, eng.Drones[0]);
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
var destroyed = eng.Drones.Count(d => d.Status == DroneStatus.Destroyed);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed),
$"摧毁={destroyed} 存活={3-destroyed} 发射={launched}");
Assert.Equal(3, eng.Drones.Count);
Assert.True(launched > 0);
Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed));
VerifyAndExportReport(eng, eng.Drones[0]);
}
// ═══════════════════════════════════════════════

View File

@ -344,22 +344,24 @@ namespace CounterDrone.Core.Tests
}
[Fact]
public void Roundtrip_ElevatedTarget_UsesHighAngleSolution()
public void Roundtrip_DepressedTarget_ReturnsSameRange()
{
// 靶点高于发射点时,低弹道解在上升段命中,
// ComputeParabolicRange 取下行段根,因此应选高弹道解才能往返一致
// 俯射往返v=80, Δy=-500, R=286空基典型值
float R = 286f, v0 = 80f, hDiff = -500f;
float angle = Kinematics.CalculateLaunchAngle(R, v0, hDiff);
Assert.True(angle < 0, $"俯射角度为负,实际={angle * 180 / Math.PI:F2}°");
var (r, _) = Kinematics.ComputeParabolicRange(v0, angle, hDiff);
Assert.True(System.Math.Abs(r - R) < 1f, $"{R}→angle→range={r:F0},应={R}");
}
[Fact]
public void Roundtrip_ElevatedTarget_ReturnsSameRange()
{
// 高靶:低弹道在上升段命中,往返应一致
float R = 5000f, v0 = 800f, hDiff = 500f;
// 手动取高弹道解(+ 号),而非 CalculateLaunchAngle 的低弹道解
float a = 0.5f * 9.81f * R * R / (v0 * v0);
float disc = R * R - 4f * a * (hDiff + a);
float tanHigh = (R + (float)System.Math.Sqrt(disc)) / (2f * a);
float highAngle = (float)System.Math.Atan(tanHigh);
var (recoveredRange, _) = Kinematics.ComputeParabolicRange(v0, highAngle, hDiff);
Assert.True(System.Math.Abs(recoveredRange - R) < 5f,
$"高弹道: 原始 R={R:F1} 还原 R={recoveredRange:F1}");
float angle = Kinematics.CalculateLaunchAngle(R, v0, hDiff);
var (r, _) = Kinematics.ComputeParabolicRange(v0, angle, hDiff);
Assert.True(System.Math.Abs(r - R) < 5f, $"{R}→angle→range={r:F0},应={R}");
}
[Fact]