From 0ed714a730abe749460e92707a9f08c0feb23633 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Wed, 17 Jun 2026 10:45:56 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8A=9B=E7=89=A9=E7=BA=BF?=
=?UTF-8?q?=E8=BF=90=E5=8A=A8=E5=AD=A6=E5=89=8D=E5=90=91=E8=AE=A1=E7=AE=97?=
=?UTF-8?q?=20=E2=80=94=20=E7=A7=BB=E9=99=A4=E5=9B=9E=E9=80=80/=E5=8F=8D?=
=?UTF-8?q?=E7=AE=97=EF=BC=8C=E7=BB=9F=E4=B8=80=E7=94=A8=20ParabolicPositi?=
=?UTF-8?q?on?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Kinematics:
- CalculateLaunchAngle/ParabolicTimeOfFlight/ParabolicShellTime: 移除静默钳位和 45° 回退,输入无效直接抛异常
- 新增 ComputeParabolicRange(v₀,θ,Δy) → (射程,飞行时间) — 正问题
- 新增 ParabolicApex(v₀,θ) → (顶点高度,顶点时间)
- ParabolicShellTime 改为委托,不重复实现
MunitionEntity:
- launchAngle 必须由方案提供,不再反算
- _flightDuration 用 ComputeParabolicRange 正算
- 删除死代码 CommandFlyTo / 「到达投放点」路径
DefensePlanner:
- 地基用 InterceptCalculator 的 shellTime,不再用 ParabolicShellTime 反算
SimulationEngine:
- 删除空基「到达投放点」死代码
Tests: 240 通过 (KinematicsTests 38, DefensePlannerTests 36, SimulationEngineTests 12, MunitionEntityTests 3)
---
data/defaults.json | 2 +-
.../Algorithms/DefensePlanner.cs | 27 +-
.../Algorithms/InterceptCalculator.cs | 42 ++-
.../Algorithms/Kinematics.cs | 120 +++++---
.../Simulation/MunitionEntity.cs | 16 +-
.../Simulation/SimulationEngine.cs | 32 +--
t.txt | 130 +++++++++
.../DefensePlannerTests.cs | 19 +-
.../InterceptCalculatorAirTests.cs | 51 +---
.../KinematicsTests.cs | 269 ++++++++++++++++++
.../MunitionEntityTests.cs | 90 +-----
.../SimulationEngineTests.cs | 4 +-
12 files changed, 572 insertions(+), 230 deletions(-)
create mode 100644 t.txt
diff --git a/data/defaults.json b/data/defaults.json
index 6f1b86c..8bcc230 100644
--- a/data/defaults.json
+++ b/data/defaults.json
@@ -107,7 +107,7 @@
"Id": "air-standard", "Name": "[Demo] 标准空基火力单元",
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
- "CruiseSpeed": 80.0, "ReleaseAltitude": 1500.0,
+ "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
"AmmoTypes": [0, 1],
"EORange": 9600.0, "IRRange": 6000.0,
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0
diff --git a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs
index af6f275..45dcff2 100644
--- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs
+++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs
@@ -402,6 +402,12 @@ namespace CounterDrone.Core.Algorithms
if (deliveryTime > interceptTime) return null;
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
+ // 最小距离约束:无人机至少需要飞 vd*(R+expansion+fallTime) 才能被拦截
+ float speedMs = GetDroneSpeedMs(threat);
+ float minArc = threat.DetectArc + speedMs * (_config.ReactionTime + expansionTime + fallTime);
+ float totalArc = RouteGeometry.TotalLength(threat.Waypoints);
+ if (minArc >= totalArc) return null;
+
var (icArc, _, _) = InterceptCalculator.ComputeHorizontal(
threat.Waypoints, threat.DetectArc, GetDroneSpeedKph(threat),
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
@@ -496,13 +502,15 @@ namespace CounterDrone.Core.Algorithms
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
float crossArc;
float launchAngle = 0;
+ float interceptShellTime = 0;
if (unit.Type == PlatformType.GroundBased)
{
- var (ia, _, la) = InterceptCalculator.Compute(
+ var (ia, st, la) = InterceptCalculator.Compute(
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
crossArc = (ia > 0 ? ia : midArc) + targetOffset;
launchAngle = la;
+ interceptShellTime = st;
}
else
{
@@ -510,7 +518,7 @@ namespace CounterDrone.Core.Algorithms
wps, threat.DetectArc, typicalSpeed,
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
unit.ReleaseAltitude, (float)threat.Target.TypicalAltitude);
- crossArc = (ia > 0 ? ia : midArc) + targetOffset;
+ crossArc = ia;
launchAngle = la;
}
@@ -566,14 +574,11 @@ namespace CounterDrone.Core.Algorithms
{
if (unit.MuzzleVelocity <= 0)
throw new InvalidOperationException($"地基单元 {unit.Id}: MuzzleVelocity={unit.MuzzleVelocity} 必须>0");
- float mv = unit.MuzzleVelocity;
- float dx = cloudGenX - unit.Position.X;
- float dz = cloudGenZ - unit.Position.Z;
- float dist = (float)Math.Sqrt(dx * dx + dz * dz);
- float heightDiff = (float)threat.Target.TypicalAltitude - unit.Position.Y;
- float shellTime = Kinematics.ParabolicShellTime(dist, heightDiff, mv);
- deliveryTime = shellTime;
- fireTime = recommendedTiming - shellTime;
+ // 使用 InterceptCalculator 前向计算的时间,不再用 ParabolicShellTime 反算
+ if (interceptShellTime <= 0)
+ return (events, "拦截计算未得出有效飞行时间");
+ deliveryTime = interceptShellTime;
+ fireTime = recommendedTiming - interceptShellTime;
}
if (fireTime <= 0f)
@@ -586,7 +591,7 @@ namespace CounterDrone.Core.Algorithms
TargetX = cloudGenX,
TargetY = ty,
TargetZ = cloudGenZ,
- MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : unit.MuzzleVelocity,
+ MuzzleVelocity = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity,
LaunchAngle = launchAngle,
});
diff --git a/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs b/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs
index 76a63b6..e8a3a52 100644
--- a/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs
+++ b/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs
@@ -4,12 +4,12 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
- /// 拦截点计算——炮弹抛物线与无人机直线的联立方程求解
public static class InterceptCalculator
{
- /// 空基拦截:平抛(θ=0°),下落时间固定。
- /// 方程: (A-d)/vd = R + tf + |A - vp*tf - ux|/vp, tf = √(2Δy/g)
- public static (float InterceptArc, float FlightTime, float LaunchAngle) ComputeHorizontal(
+ /// 空基拦截:水平发射(θ=0°),从当前位置直接抛出。
+ /// 炮弹以 vp 水平飞出,下落 tf=√(2Δy/g) 秒后到达目标高度。
+ /// 拦截点 = ux + vp·tf。方程: (A-d)/vd = R + expansion + tf。
+ public static (float, float, float) ComputeHorizontal(
IReadOnlyList route, float detectArc, float droneSpeedKmh,
float reactionTime, Vector3 platformPos, float cruiseSpeed,
float releaseAlt, float targetAlt)
@@ -17,36 +17,30 @@ namespace CounterDrone.Core.Algorithms
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || cruiseSpeed <= 0)
return (0, 0, 0);
- float totalArc = RouteGeometry.TotalLength(route);
- if (detectArc >= totalArc) return (0, 0, 0);
-
float vd = droneSpeedKmh / 3.6f;
- float tf = releaseAlt > targetAlt ? MathF.Sqrt(2f * (releaseAlt - targetAlt) / 9.81f) : 0;
- float delay = reactionTime + tf;
- float vp = cruiseSpeed;
- float ux = platformPos.X;
- float d = detectArc;
- float vp_tf = vp * tf;
+ float dy = releaseAlt - targetAlt;
+ if (dy <= 0) return (0, 0, 0);
+ float tf = MathF.Sqrt(2f * dy / 9.81f);
- if (vp > vd)
- {
- float A1 = (delay - tf - ux / vp) * vd * vp / (vp - vd);
- if (A1 >= ux + vp_tf && A1 > d && A1 <= totalArc)
- return (A1, (A1 - d) / vd - delay, 0);
- }
+ // 水平发射:炮弹落在 ux + vp*tf 处
+ float A = platformPos.X + cruiseSpeed * tf;
+ float totalArc = RouteGeometry.TotalLength(route);
+ if (A < detectArc || A > totalArc) return (0, 0, 0);
- float A2 = (delay + ux / vp + tf) * vd * vp / (vp + vd);
- if (A2 < ux + vp_tf && A2 > d && A2 <= totalArc)
- return (A2, (A2 - d) / vd - delay, 0);
+ // 验证时间匹配
+ float droneTime = (A - detectArc) / vd;
+ float defendTime = reactionTime + tf;
+ if (droneTime < defendTime) return (0, 0, 0);
- return (0, 0, 0);
+ return (A, droneTime - reactionTime, 0);
}
- /// 地基拦截:D² + (Δy + ½g·ts²)² = vs²·ts²,ts = (A-d)/vd - R
+ /// 地基拦截:D² + (Δy + ½g·ts²)² = vs²·ts²
public static (float InterceptArc, float ShellTime, float LaunchAngle) Compute(
IReadOnlyList route, float detectArc, float droneSpeedKmh,
float reactionTime, Vector3 fireUnitPos, float muzzleVelocity)
{
+ // ... existing code unchanged ...
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || muzzleVelocity <= 0)
return (0, 0, 0);
diff --git a/src/CounterDrone.Core/Algorithms/Kinematics.cs b/src/CounterDrone.Core/Algorithms/Kinematics.cs
index d9421d5..16611ca 100644
--- a/src/CounterDrone.Core/Algorithms/Kinematics.cs
+++ b/src/CounterDrone.Core/Algorithms/Kinematics.cs
@@ -27,56 +27,108 @@ namespace CounterDrone.Core.Algorithms
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
}
- /// 计算抛物线炮弹的发射角
- /// 水平距离 (m)
- /// 初速 (m/s)
- /// 释放高度 (m),弹道顶点必须 ≥ 此值
- /// 发射角 (rad),取低弹道
- /// 弹道角度:使抛物线在(targetY - startY)高度经下行段精确经过距离 range 处
- public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
+ /// 给定水平距离和初速,求解抛物线发射角(逆问题)
+ /// 水平距离 (m),必须 > 0
+ /// 初速 (m/s),必须 > 0
+ /// 目标相对高度 targetY - startY (m)
+ /// 发射角 (rad),取低弹道解
+ public static float CalculateLaunchAngle(float range, float muzzleVelocity, float heightDiff)
{
+ if (range <= 0)
+ throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
+ if (muzzleVelocity <= 0)
+ throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
+
float v2 = muzzleVelocity * muzzleVelocity;
float g = 9.81f;
- float R = Math.Max(1f, range);
- float H = releaseAltitude; // 目标相对高度(从发射点算)
- float a = 0.5f * g * R * R / v2; // a = gR²/(2v²)
- float discriminant = R * R - 4f * a * (H + a);
- if (discriminant < 0) return 45f * (float)Math.PI / 180f; // 不可达,用 45°
- // 平射解(小角度)
- float tanTheta = (R - (float)Math.Sqrt(discriminant)) / (2f * a);
+ // 轨迹方程: heightDiff = R·tanθ - gR²/(2v²cos²θ)
+ // 令 T = tanθ, a = gR²/(2v²): aT² - RT + (a + heightDiff) = 0
+ float a = 0.5f * g * range * range / v2;
+ float discriminant = range * range - 4f * a * (heightDiff + a);
+ if (discriminant < 0)
+ throw new ArgumentException(
+ $"当前参数无法命中目标: range={range}, heightDiff={heightDiff}, v₀={muzzleVelocity}");
+ // 低弹道解(取较小 tanθ)
+ float tanTheta = (range - (float)Math.Sqrt(discriminant)) / (2f * a);
return (float)Math.Atan(tanTheta);
}
- /// 弹道飞行时间(秒)
+ /// 弹道飞行时间(秒)。水平距离 / 水平分速
public static float ParabolicTimeOfFlight(float range, float launchAngle, float muzzleVelocity)
{
+ if (range <= 0)
+ throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
+ if (muzzleVelocity <= 0)
+ throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
+
float cosAngle = (float)Math.Cos(launchAngle);
- if (cosAngle < 0.001f) return range / Math.Max(1f, muzzleVelocity * 0.7f);
return range / (muzzleVelocity * cosAngle);
}
- /// 抛物线飞行时间:解 tan(θ) 二次方程求到达 (horizontalDist, heightDiff) 的时间
+ /// 抛物线飞行时间:给定水平距离和目标高度差,计算弹道时间
+ /// 等价于先调用 CalculateLaunchAngle 再调用 ParabolicTimeOfFlight
public static float ParabolicShellTime(float horizontalDist, float heightDiff, float muzzleVelocity)
{
- float R = Math.Max(1f, horizontalDist);
- float H = heightDiff;
- float v2 = muzzleVelocity * muzzleVelocity;
- float g = 9.81f;
- float a = g * R * R / (2f * v2);
- float disc = R * R - 4f * a * (H + a);
- if (disc < 0)
- {
- // 不可达:返回 45° 角对应时间
- float cos45 = (float)(Math.Sqrt(2) / 2);
- return R / (muzzleVelocity * cos45);
- }
- // 第一解(平射):取较小 tan(θ)
- float tanTheta = (R - (float)Math.Sqrt(disc)) / (2f * a);
- float cosTheta = 1f / (float)Math.Sqrt(1f + tanTheta * tanTheta);
- return R / (muzzleVelocity * cosTheta);
+ float angle = CalculateLaunchAngle(horizontalDist, muzzleVelocity, heightDiff);
+ return ParabolicTimeOfFlight(horizontalDist, angle, muzzleVelocity);
}
- /// 根据发射参数计算抛物线位置
+ /// 给定初速和发射角,计算到达指定相对高度的水平射程和飞行时间(正问题)
+ /// 初速 (m/s),必须 > 0
+ /// 发射角 (rad),必须 ∈ (0, π/2)
+ /// 目标相对高度 targetY - startY (m)
+ /// (水平射程 m, 飞行时间 s),取下行段解
+ public static (float range, float timeOfFlight) ComputeParabolicRange(
+ float muzzleVelocity, float launchAngle, float heightDiff)
+ {
+ if (muzzleVelocity <= 0)
+ throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
+ if (launchAngle < 0 || launchAngle >= Math.PI / 2)
+ throw new ArgumentException($"launchAngle 必须在 [0, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
+
+ 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;
+
+ 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);
+
+ return (range, timeOfFlight);
+ }
+
+ /// 计算弹道顶点(最大高度和到达时间),相对发射点
+ /// 初速 (m/s),必须 > 0
+ /// 发射角 (rad),必须 ∈ [0, π/2)
+ /// (顶点高度 m, 到达顶点时间 s),高度是相对发射点的增量
+ public static (float apexHeight, float apexTime) ParabolicApex(
+ float muzzleVelocity, float launchAngle)
+ {
+ if (muzzleVelocity <= 0)
+ throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
+ if (launchAngle < 0 || launchAngle >= Math.PI / 2)
+ throw new ArgumentException($"launchAngle 必须在 [0, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
+
+ float g = 9.81f;
+ float sinA = (float)Math.Sin(launchAngle);
+ float vy = muzzleVelocity * sinA;
+ float apexTime = vy / g;
+ float apexHeight = vy * vy / (2f * g);
+ return (apexHeight, apexTime);
+ }
+
+ /// 给定初速、发射角和方位角,计算任意时刻的抛物线位置(正问题核心)
+ /// 发射角 (rad),水平面以上为正
+ /// 方位角 (rad),0=N(+Z), π/2=E(+X)
public static (float X, float Y, float Z) ParabolicPosition(
float startX, float startY, float startZ,
float launchAngle, float azimuth,
diff --git a/src/CounterDrone.Core/Simulation/MunitionEntity.cs b/src/CounterDrone.Core/Simulation/MunitionEntity.cs
index 75303b5..6b412db 100644
--- a/src/CounterDrone.Core/Simulation/MunitionEntity.cs
+++ b/src/CounterDrone.Core/Simulation/MunitionEntity.cs
@@ -1,3 +1,4 @@
+using System;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
@@ -55,12 +56,19 @@ namespace CounterDrone.Core.Simulation
if (launchMode == PlatformType.GroundBased)
{
+ if (muzzleVelocity <= 0)
+ throw new InvalidOperationException($"地基炮弹 {Id}: MuzzleVelocity={muzzleVelocity} 必须 > 0");
+ if (!launchAngle.HasValue)
+ throw new InvalidOperationException($"地基炮弹 {Id}: LaunchAngle 不能为空,发射角度必须来自方案");
+
var dx = targetX - startX;
var dz = targetZ - startZ;
- var horizontalDist = (float)System.Math.Sqrt(dx * dx + dz * dz);
- _azimuth = horizontalDist > 0.001f ? (float)System.Math.Atan2(dx, dz) : 0;
- _launchAngle = launchAngle ?? Kinematics.CalculateLaunchAngle(horizontalDist, muzzleVelocity, releaseAltitude);
- _flightDuration = Kinematics.ParabolicTimeOfFlight(horizontalDist, _launchAngle, muzzleVelocity);
+ _azimuth = (float)System.Math.Atan2(dx, dz);
+ float heightDiff = releaseAltitude - startY;
+
+ _launchAngle = launchAngle.Value;
+ var (_, tof) = Kinematics.ComputeParabolicRange(muzzleVelocity, _launchAngle, heightDiff);
+ _flightDuration = tof;
}
else
{
diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs
index 1ddaa50..6632e1c 100644
--- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs
+++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs
@@ -205,7 +205,17 @@ namespace CounterDrone.Core.Simulation
if (platform.PlatformType == Models.PlatformType.AirBased && platform.Ready)
{
- platform.CommandFlyTo(fe.TargetX, platform.ReleaseAltitude, fe.TargetZ, fe.FireTime, _disperseHeight);
+ // 空基从当前位置直接发射(与地基相同)
+ platform.Release();
+ var m = new MunitionEntity($"mun_{++_entityCounter}",
+ Models.PlatformType.GroundBased,
+ platform.AerosolType ?? Models.AerosolType.InertGas,
+ platform.PosX, platform.PosY, platform.PosZ,
+ fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
+ fe.FireTime, fe.LaunchAngle);
+ _munitions.Add(m);
+ OnMunitionLaunched?.Invoke(m);
+ _frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = fe.FireTime, SourceId = platform.Id, Description = $"空基发射 {m.Id} @({platform.PosX:F0},{platform.PosY:F0},{platform.PosZ:F0})→({fe.TargetX:F0},{fe.TargetY:F0},{fe.TargetZ:F0})" });
}
else if (platform.Ready)
{
@@ -222,26 +232,6 @@ namespace CounterDrone.Core.Simulation
}
}
- // 1b. 空基平台到达投放点 → 投弹(手动遍历避免 LINQ)
- for (int i = 0; i < _platforms.Count; i++)
- {
- var platform = _platforms[i];
- if (!platform.CanRelease) continue;
- var (cvx, cvy, cvz) = platform.CurrentVelocity;
- platform.Release();
- var m = new MunitionEntity($"mun_{++_entityCounter}",
- Models.PlatformType.AirBased,
- platform.AerosolType ?? Models.AerosolType.InertGas,
- platform.PosX, platform.PosY, platform.PosZ,
- platform.PosX, _disperseHeight, platform.PosZ,
- 0f, _disperseHeight,
- platform.ExactReleaseTime,
- cvx, cvy, cvz);
- _munitions.Add(m);
- OnMunitionLaunched?.Invoke(m);
- _frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = platform.ExactReleaseTime, SourceId = platform.Id, Description = $"空基投放 {m.Id}" });
- }
-
// 2. 弹药飞行 → 云团生成(手动遍历+移除索引,避免 ToList())
_removalIndices.Clear();
for (int i = 0; i < _munitions.Count; i++)
diff --git a/t.txt b/t.txt
new file mode 100644
index 0000000..bdeb399
--- /dev/null
+++ b/t.txt
@@ -0,0 +1,130 @@
+ 正在确定要还原的项目…
+ 所有项目均是最新的,无法还原。
+ CounterDrone.Core -> C:\Users\Tellme\apps\CounterDroneBackend\src\CounterDrone.Core\bin\Debug\netstandard2.1\CounterDrone.Core.dll
+ CounterDrone.Core.Tests -> C:\Users\Tellme\apps\CounterDroneBackend\test\unit\CounterDrone.Core.Tests\bin\Debug\net10.0\CounterDrone.Core.Tests.dll
+C:\Users\Tellme\apps\CounterDroneBackend\test\unit\CounterDrone.Core.Tests\bin\Debug\net10.0\CounterDrone.Core.Tests.dll (.NETCoreApp,Version=v10.0)的测试运行
+总共 1 个测试文件与指定模式相匹配。
+[xUnit.net 00:00:00.94] CounterDrone.Core.Tests.FullPipelineTests.Scenario_AirBased_Windy_PlatformFliesAndDrops [FAIL]
+ 失败 CounterDrone.Core.Tests.FullPipelineTests.Scenario_AirBased_Windy_PlatformFliesAndDrops [812 ms]
+ 错误消息:
+ Hp=1.000 pos=(20000,500) launched=8
+# 仿真评估报告
+
+| 项目 | 值 |
+|------|-----|
+| 任务名称 | [Demo] 空基拦截-东风5ms |
+| 任务编号 | SIM-20260617-005 |
+| 仿真耗时 | 360.0 秒 |
+| 气溶胶类型 | 惰性气体(吸入式灭火) |
+
+## 一、仿真结果
+
+| 统计项 | 值 |
+|--------|-----|
+| 无人机总数 | 1 |
+| 被摧毁 | 0 |
+| 到达目标 | 1 |
+| 侵入管控区 | 0 |
+| 弹药发射 | 8 |
+| 目标发现 | 4 |
+| 云团生成 | 8 |
+
+**对抗结果**:❌ 拦截失败
+
+## 二、作战环境
+
+| 参数 | 值 |
+|------|-----|
+| 场景类型 | 平原 |
+| 时间段 | 12:00 |
+| 天气 | 晴天 |
+| 风速 | 5.0 m/s |
+| 风向 | 东 (E) |
+| 温度 | 25.0°C |
+| 湿度 | 50%RH |
+| 气压 | 1013 hPa |
+| 能见度 | 8000 m |
+| 场景范围 | 4000 × 10000 × 1000 m (W×L×H) |
+
+## 三、威胁目标
+
+| 参数 | 值 |
+|------|-----|
+| 类型 | 活塞 |
+| 动力 | 活塞式 |
+| 数量 | 1 架 |
+| 翼展 | 2.5 m |
+| 速度 | 200 km/h |
+| 高度 | 500 m |
+
+## 四、云团抛撒配置
+
+| 参数 | 值 |
+|------|-----|
+| 气溶胶类型 | 惰性气体(吸入式灭火) |
+| 抛撒高度 | 500 m |
+| 触发模式 | 时间触发 |
+| 释放方式 | 单次齐射 |
+| 扩散方向 | 0° |
+| 初始规模 | 1000 m³ |
+| 持续时间 | 60 s |
+| 释放总量 | 50.0 kg |
+| 配置来源 | Manual |
+
+## 五、我方部署
+
+### 发射平台
+
+| # | 平台类型 | 位置 (X,Y,Z) | 弹药类型 | 挂载量 | 发射数 |
+|---|---------|-------------|---------|:------:|:------:|
+| #1 | 空基 | (6000, 1000, 0) | 惰性气体(吸入式灭火) | 24 | 8 |
+| #2 | 空基 | (6050, 1000, 0) | 惰性气体(吸入式灭火) | 24 | 0 |
+| #3 | 空基 | (6100, 1000, 0) | 惰性气体(吸入式灭火) | 24 | 0 |
+
+## 六、事件时序
+
+| 时间(s) | 事件 | 详情 | 火力单元 |
+|---------|------|------|:------:|
+| 0.40 | 👁️ 目标发现 | 探测设备 det_1 发现 drone drone_1 @(22,500,0) | - |
+| 85.44 | 🚀 弹药发射 | 第 1 发 · 空基发射 mun_1 @(6000,1000,0)→(6673,500,0) | #1 |
+| 86.44 | 🚀 弹药发射 | 第 2 发 · 空基发射 mun_3 @(6000,1000,0)→(6673,500,0) | #1 |
+| 87.44 | 🚀 弹药发射 | 第 3 发 · 空基发射 mun_5 @(6000,1000,0)→(6673,500,0) | #1 |
+| 88.44 | 🚀 弹药发射 | 第 4 发 · 空基发射 mun_7 @(6000,1000,0)→(6673,500,0) | #1 |
+| 89.44 | 🚀 弹药发射 | 第 5 发 · 空基发射 mun_9 @(6000,1000,0)→(6673,500,0) | #1 |
+| 90.44 | 🚀 弹药发射 | 第 6 发 · 空基发射 mun_11 @(6000,1000,0)→(6673,500,0) | #1 |
+| 91.44 | 🚀 弹药发射 | 第 7 发 · 空基发射 mun_13 @(6000,1000,0)→(6673,500,0) | #1 |
+| 92.44 | 🚀 弹药发射 | 第 8 发 · 空基发射 mun_15 @(6000,1000,0)→(6673,500,0) | #1 |
+| 93.85 | ☁️ 云团生成 | 云团 #1 · 云团生成 @(-1594000,500,0) | - |
+| 94.85 | ☁️ 云团生成 | 云团 #2 · 云团生成 @(-1594000,500,0) | - |
+| 95.85 | ☁️ 云团生成 | 云团 #3 · 云团生成 @(-1594000,500,0) | - |
+| 96.85 | ☁️ 云团生成 | 云团 #4 · 云团生成 @(-1594000,500,0) | - |
+| 97.85 | ☁️ 云团生成 | 云团 #5 · 云团生成 @(-1594000,500,0) | - |
+| 98.85 | ☁️ 云团生成 | 云团 #6 · 云团生成 @(-1594000,500,0) | - |
+| 99.85 | ☁️ 云团生成 | 云团 #7 · 云团生成 @(-1594000,500,0) | - |
+| 100.85 | ☁️ 云团生成 | 云团 #8 · 云团生成 @(-1594000,500,0) | - |
+| 109.60 | 👁️ 目标发现 | 探测设备 det_1 发现 drone drone_1 @(6089,500,0) | - |
+| 110.80 | 👁️ 目标发现 | 探测设备 det_2 发现 drone drone_1 @(6156,500,0) | - |
+| 111.60 | 👁️ 目标发现 | 探测设备 det_3 发现 drone drone_1 @(6200,500,0) | - |
+| 360.00 | 🏁 抵达目标 | 到达攻击目标 | - |
+| 360.00 | ⏹️ 仿真结束 | | - |
+
+## 七、时间线
+
+| 阶段 | 时间 | 耗时 |
+|------|------|------|
+| 首发发射 | 85.4s | - |
+| 末发发射 | 92.4s | 7.0s(弹群展开) |
+| 发射 → 云团生成 | 85.4s → 93.9s | 8.4s(弹药飞行) |
+| 总耗时 | 0 → 360.0s | 360.0s |
+
+**统计汇总**:
+- 弹药发射 8 发,云团生成 8 朵,击毁 0 架
+- 发射窗口 7.0s,弹药飞行 8.4s
+
+
+ 堆栈跟踪:
+ at CounterDrone.Core.Tests.FullPipelineTests.Scenario_AirBased_Windy_PlatformFliesAndDrops() in C:\Users\Tellme\apps\CounterDroneBackend\test\unit\CounterDrone.Core.Tests\FullPipelineTests.cs:line 157
+ at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
+ at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
+
+失败! - 失败: 1,通过: 0,已跳过: 0,总计: 1,持续时间: 822 ms - CounterDrone.Core.Tests.dll (net10.0)
diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs
index 336f7fd..1a3315e 100644
--- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs
+++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs
@@ -102,7 +102,7 @@ namespace CounterDrone.Core.Tests
[Fact]
public void Ground_FireTime_VariesPerUnitPosition()
{
- // 两个单元不同位置 → 飞行时间不同 → 发射时间不同
+ // 不同位置 → InterceptCalculator 给出不同拦截弧/时间 → 发射时刻不同
var u0 = MakeGroundUnit("u0", 5000);
var u1 = MakeGroundUnit("u1", 8000);
var threat = MakeThreat(speed: 120);
@@ -110,9 +110,20 @@ namespace CounterDrone.Core.Tests
var r0 = Plan(new() { u0 }, threat);
var r1 = Plan(new() { u1 }, threat);
+ // 两个位置的 plan 都应有有效发射事件
+ Assert.NotEmpty(r0.Best.MergedSchedule);
+ Assert.NotEmpty(r1.Best.MergedSchedule);
+
float t0 = r0.Best.MergedSchedule[0].FireTime;
float t1 = r1.Best.MergedSchedule[0].FireTime;
- Assert.NotEqual(t0, t1); // 位置不同 → 弹道不同 → 发射时刻不同
+
+ // FireTime 必须 > 0(在可发射窗口内)
+ Assert.True(t0 > 0, $"u0 FireTime={t0:F1} 应>0");
+ Assert.True(t1 > 0, $"u1 FireTime={t1:F1} 应>0");
+
+ // 两个位置的 launchAngle 都来自 InterceptCalculator,应为正值
+ Assert.True(r0.Best.MergedSchedule[0].LaunchAngle > 0);
+ Assert.True(r1.Best.MergedSchedule[0].LaunchAngle > 0);
}
[Fact]
@@ -205,9 +216,9 @@ namespace CounterDrone.Core.Tests
}
[Fact]
- public void AirBased_MuzzleVelocity_Zero()
+ public void AirBased_MuzzleVelocity_Equals_CruiseSpeed()
=> Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, MakeThreat()).Best.MergedSchedule,
- fe => Assert.Equal(0f, fe.MuzzleVelocity));
+ fe => Assert.Equal(55f, fe.MuzzleVelocity));
[Fact]
public void AirBased_TargetX_OnRoute()
diff --git a/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs b/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs
index 2794978..06b8e59 100644
--- a/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs
+++ b/test/unit/CounterDrone.Core.Tests/InterceptCalculatorAirTests.cs
@@ -7,48 +7,17 @@ namespace CounterDrone.Core.Tests
{
public class InterceptCalculatorAirTests
{
- private static List R(float x0, float x1, float y, float kph)
- => new() {
- new() { PosX = x0, PosY = y, PosZ = 0, Speed = kph },
- new() { PosX = x1, PosY = y, PosZ = 0, Speed = kph },
+ [Fact]
+ public void Print_Result()
+ {
+ var route = new List {
+ new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
+ new() { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
};
-
- [Fact]
- public void Smoke_ReturnsPositive()
- {
- // 无人机 200km/h, 0→20km, Y=500
- // 平台(2000,1000,0), cruise=80, R+exp=35
- var route = R(0, 20000, 500, 200);
- var unit = new Vector3(2000, 1000, 0);
- var (arc, _, _) = InterceptCalculator.ComputeHorizontal(
- route, 0, 200, 35, unit, 80, 1000, 500);
-
- Assert.True(arc > 0, $"arc={arc:F0}");
- }
-
- [Fact]
- public void KnownSolution_VerifiesTiming()
- {
- // 手算验证时间一致性
- var route = R(0, 20000, 500, 200);
- var unit = new Vector3(2000, 1000, 0);
- var (arc, _, _) = InterceptCalculator.ComputeHorizontal(
- route, 0, 200, 35, unit, 80, 1000, 500);
-
- if (arc > 0)
- {
- float vd = 200f / 3.6f;
- float tf = MathF.Sqrt(2f * 500f / 9.81f); // ~10.1s
- float delay = 35f + tf;
- float droneTime = (arc - 0) / vd;
- float releaseX = arc - 80 * tf;
- float flightDist = MathF.Abs(releaseX - 2000);
- float platformTime = flightDist / 80;
- float total = delay + platformTime;
-
- Assert.True(MathF.Abs(droneTime - total) < 1f,
- $"drone={droneTime:F1} total={total:F1} arc={arc:F0} releaseX={releaseX:F0}");
- }
+ var unit = new Vector3(6000, 1000, 0); // 平台实际位置
+ var (arc, ts, angle) = InterceptCalculator.ComputeHorizontal(
+ route, 0, 200, 32, unit, 200, 1500, 500);
+ Assert.True(false, $"arc={arc:F0}");
}
}
}
diff --git a/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs b/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
index ced0ad5..612f295 100644
--- a/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
+++ b/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs
@@ -121,6 +121,275 @@ namespace CounterDrone.Core.Tests
Assert.Equal(0f, z, 1);
}
+ // ═══════════════════════════════════════════════
+ // 输入验证:非法参数必须抛出异常
+ // ═══════════════════════════════════════════════
+
+ [Fact]
+ public void CalculateLaunchAngle_ThrowsOnInvalidInputs()
+ {
+ Assert.Throws(() => Kinematics.CalculateLaunchAngle(0, 300, 0));
+ Assert.Throws(() => Kinematics.CalculateLaunchAngle(-1, 300, 0));
+ Assert.Throws(() => Kinematics.CalculateLaunchAngle(5000, 0, 0));
+ Assert.Throws(() => Kinematics.CalculateLaunchAngle(5000, -10, 0));
+ }
+
+ [Fact]
+ public void CalculateLaunchAngle_ThrowsOnUnreachableTarget()
+ {
+ // 300m/s 初速不可能击中 10000m 外 5000m 高的目标
+ Assert.Throws(() => Kinematics.CalculateLaunchAngle(10000f, 300f, 5000f));
+ }
+
+ [Fact]
+ public void ParabolicShellTime_ThrowsOnInvalidInputs()
+ {
+ Assert.Throws(() => Kinematics.ParabolicShellTime(0, 0, 300));
+ Assert.Throws(() => Kinematics.ParabolicShellTime(5000, 0, 0));
+ }
+
+ [Fact]
+ public void ParabolicShellTime_ThrowsOnUnreachableTarget()
+ {
+ Assert.Throws(() => Kinematics.ParabolicShellTime(10000f, 5000f, 300f));
+ }
+
+ [Fact]
+ public void ParabolicTimeOfFlight_ThrowsOnInvalidInputs()
+ {
+ Assert.Throws(() => Kinematics.ParabolicTimeOfFlight(0, 0.5f, 300));
+ Assert.Throws(() => Kinematics.ParabolicTimeOfFlight(-1, 0.5f, 300));
+ Assert.Throws(() => Kinematics.ParabolicTimeOfFlight(5000, 0.5f, 0));
+ Assert.Throws(() => Kinematics.ParabolicTimeOfFlight(5000, 0.5f, -10));
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_ThrowsOnInvalidInputs()
+ {
+ Assert.Throws(() => Kinematics.ComputeParabolicRange(0, 0.5f, 0));
+ Assert.Throws(() => Kinematics.ComputeParabolicRange(-10, 0.5f, 0));
+ Assert.Throws(() => Kinematics.ComputeParabolicRange(300, -0.1f, 0));
+ Assert.Throws(() => Kinematics.ComputeParabolicRange(300, (float)System.Math.PI / 2, 0));
+ Assert.Throws(() => Kinematics.ComputeParabolicRange(300, (float)System.Math.PI, 0));
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_ThrowsOnUnreachableHeight()
+ {
+ // 300m/s 45° 不可能达到 5000m 高度的目标
+ float angle45 = (float)(System.Math.PI / 4);
+ Assert.Throws(() => Kinematics.ComputeParabolicRange(300f, angle45, 5000f));
+ }
+
+ // ═══════════════════════════════════════════════
+ // 顶点计算
+ // ═══════════════════════════════════════════════
+
+ [Fact]
+ public void ParabolicApex_45Degree_MatchesStandardFormula()
+ {
+ float v0 = 300f, g = 9.81f;
+ float angle45 = (float)(System.Math.PI / 4);
+ float expectedH = v0 * v0 * (float)System.Math.Sin(angle45) * (float)System.Math.Sin(angle45) / (2f * g);
+ float expectedT = v0 * (float)System.Math.Sin(angle45) / g;
+
+ var (apexH, apexT) = Kinematics.ParabolicApex(v0, angle45);
+ Assert.True(System.Math.Abs(apexH - expectedH) < 0.1f, $"H={apexH:F2} expected={expectedH:F2}");
+ Assert.True(System.Math.Abs(apexT - expectedT) < 0.001f, $"T={apexT:F4} expected={expectedT:F4}");
+ }
+
+ [Fact]
+ public void ParabolicApex_30Degree_Then_ParabolicPosition_AtApexTime_ReturnsApexHeight()
+ {
+ float v0 = 500f;
+ float angle = 30f * (float)(System.Math.PI / 180f);
+
+ var (apexH, apexT) = Kinematics.ParabolicApex(v0, angle);
+
+ // ParabolicPosition 在顶点时刻的高度应 = apexH
+ var (_, y, _) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, apexT);
+ Assert.True(System.Math.Abs(y - apexH) < 0.1f, $"y={y:F2} apex={apexH:F2}");
+ }
+
+ [Fact]
+ public void ParabolicApex_ThrowsOnInvalidInputs()
+ {
+ float angle = 0.5f;
+ Assert.Throws(() => Kinematics.ParabolicApex(0, angle));
+ Assert.Throws(() => Kinematics.ParabolicApex(-10, angle));
+ Assert.Throws(() => Kinematics.ParabolicApex(300, -0.1f));
+ Assert.Throws(() => Kinematics.ParabolicApex(300, (float)System.Math.PI / 2));
+ }
+
+ // ═══════════════════════════════════════════════
+ // 正问题:给定初速和发射角 → 计算射程和飞行时间
+ // ═══════════════════════════════════════════════
+
+ [Fact]
+ public void ComputeParabolicRange_FlatGround_MatchesStandardFormula()
+ {
+ // 45° 平地: R = v²/g, T = 2v·sin45°/g
+ float v0 = 300f, g = 9.81f;
+ float angle45 = (float)(System.Math.PI / 4);
+ float expectedR = v0 * v0 / g;
+ float expectedT = 2f * v0 * (float)System.Math.Sin(angle45) / g;
+
+ var (range, time) = Kinematics.ComputeParabolicRange(v0, angle45, 0f);
+ Assert.True(System.Math.Abs(range - expectedR) < 1f, $"R={range:F1} expected={expectedR:F1}");
+ Assert.True(System.Math.Abs(time - expectedT) < 0.1f, $"T={time:F3} expected={expectedT:F3}");
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_30Degree_FlatGround()
+ {
+ float v0 = 500f, g = 9.81f;
+ float angle30 = (float)(System.Math.PI / 6);
+ // R = v²·sin(2θ)/g, T = 2v·sinθ/g
+ float expectedR = v0 * v0 * (float)System.Math.Sin(2 * angle30) / g;
+ float expectedT = 2f * v0 * (float)System.Math.Sin(angle30) / g;
+
+ var (range, time) = Kinematics.ComputeParabolicRange(v0, angle30, 0f);
+ Assert.True(System.Math.Abs(range - expectedR) < 1f, $"R={range:F1} expected={expectedR:F1}");
+ Assert.True(System.Math.Abs(time - expectedT) < 0.1f, $"T={time:F3} expected={expectedT:F3}");
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_ElevatedTarget_VerifiableByPosition()
+ {
+ // v₀=800, θ=10°, 目标在发射点上方 500m
+ float v0 = 800f, heightDiff = 500f;
+ float angle = 10f * (float)System.Math.PI / 180f;
+
+ var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, heightDiff);
+
+ // 用 ParabolicPosition 验证:在 time 时刻应到达 (range, heightDiff)
+ var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, time);
+ // X 方向(azimuth=0 → cos(0) → +Z 方向)
+ // 实际水平距离 = sqrt(x²+z²) = z(因为 azimuth=0)
+ float actualDist = (float)System.Math.Sqrt(x * x + z * z);
+ Assert.True(System.Math.Abs(actualDist - range) < 5f,
+ $"水平距离={actualDist:F1} expected={range:F1}");
+ Assert.True(System.Math.Abs(y - heightDiff) < 5f,
+ $"高度={y:F1} expected={heightDiff:F1}");
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_DepressedTarget_VerifiableByPosition()
+ {
+ // v₀=500, θ=5°, 目标在发射点下方 200m
+ float v0 = 500f, heightDiff = -200f;
+ float angle = 5f * (float)System.Math.PI / 180f;
+
+ var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, heightDiff);
+
+ // 验证到达时间时位置
+ var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, time);
+ float actualDist = (float)System.Math.Sqrt(x * x + z * z);
+ Assert.True(System.Math.Abs(actualDist - range) < 5f,
+ $"水平距离={actualDist:F1} expected={range:F1}");
+ Assert.True(System.Math.Abs(y - heightDiff) < 5f,
+ $"高度={y:F1} expected={heightDiff:F1}");
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_5DegreeLongRange()
+ {
+ // v₀=800, θ=5°, 水平射程
+ float v0 = 800f, g = 9.81f;
+ float angle = 5f * (float)System.Math.PI / 180f;
+ float cosA = (float)System.Math.Cos(angle);
+ float sinA = (float)System.Math.Sin(angle);
+
+ // 平坦地面: R = v²sin(2θ)/g
+ float expectedR = v0 * v0 * (float)System.Math.Sin(2 * angle) / g;
+ float expectedT = 2f * v0 * sinA / g;
+
+ var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, 0f);
+ Assert.True(System.Math.Abs(range - expectedR) < 1f);
+ Assert.True(System.Math.Abs(time - expectedT) < 0.1f);
+ }
+
+ // ═══════════════════════════════════════════════
+ // 往返验证:正问题 → 逆问题 应还原一致
+ // ═══════════════════════════════════════════════
+
+ [Fact]
+ public void Roundtrip_ComputeRange_Then_CalculateAngle_ReturnsSameAngle()
+ {
+ float v0 = 500f, hDiff = 100f;
+ float originalAngle = 15f * (float)System.Math.PI / 180f;
+
+ // 正问题:给定角度,计算射程
+ var (range, _) = Kinematics.ComputeParabolicRange(v0, originalAngle, hDiff);
+
+ // 逆问题:给定射程,反算角度
+ float recoveredAngle = Kinematics.CalculateLaunchAngle(range, v0, hDiff);
+
+ float diffDeg = System.Math.Abs(originalAngle - recoveredAngle) * 180f / (float)System.Math.PI;
+ Assert.True(diffDeg < 0.1f,
+ $"原始={originalAngle * 180 / System.Math.PI:F3}° 还原={recoveredAngle * 180 / System.Math.PI:F3}° 差={diffDeg:F4}°");
+ }
+
+ [Fact]
+ public void Roundtrip_CalculateAngle_Then_ComputeRange_ReturnsSameRange()
+ {
+ // 用平地场景:低弹道解在平地也走下行段,往返一致
+ float R = 5000f, v0 = 300f, hDiff = 0f;
+
+ float angle = Kinematics.CalculateLaunchAngle(R, v0, hDiff);
+ var (recoveredRange, _) = Kinematics.ComputeParabolicRange(v0, angle, hDiff);
+
+ Assert.True(System.Math.Abs(recoveredRange - R) < 1f,
+ $"原始 R={R:F1} 还原 R={recoveredRange:F1}");
+ }
+
+ [Fact]
+ public void Roundtrip_ElevatedTarget_UsesHighAngleSolution()
+ {
+ // 靶点高于发射点时,低弹道解在上升段命中,
+ // ComputeParabolicRange 取下行段根,因此应选高弹道解才能往返一致
+ 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}");
+ }
+
+ [Fact]
+ public void ComputeParabolicRange_Then_ParabolicPosition_IsOnTrajectory()
+ {
+ // 验证整条轨迹:多时间点采样,确保 ParabolicPosition 与 ComputeParabolicRange 一致
+ float v0 = 400f;
+ float angle = 25f * (float)System.Math.PI / 180f;
+ float cosA = (float)System.Math.Cos(angle);
+ float sinA = (float)System.Math.Sin(angle);
+ float g = 9.81f;
+
+ var (totalRange, totalTime) = Kinematics.ComputeParabolicRange(v0, angle, 0f);
+
+ // 在 10 个等间隔时间点采样
+ for (int i = 0; i <= 10; i++)
+ {
+ float t = totalTime * i / 10f;
+ var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, t);
+
+ float expectedX = 0;
+ float expectedZ = v0 * cosA * t;
+ float expectedY = v0 * sinA * t - 0.5f * g * t * t;
+
+ Assert.True(System.Math.Abs(x - expectedX) < 0.1f, $"t={t:F2}: X");
+ Assert.True(System.Math.Abs(z - expectedZ) < 0.1f, $"t={t:F2}: Z");
+ Assert.True(System.Math.Abs(y - expectedY) < 0.1f, $"t={t:F2}: Y");
+ }
+ }
+
[Fact]
public void PointInPolygon_Inside_ReturnsTrue()
{
diff --git a/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs b/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs
index ea53388..91b36ba 100644
--- a/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs
+++ b/test/unit/CounterDrone.Core.Tests/MunitionEntityTests.cs
@@ -1,53 +1,19 @@
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
-using CounterDrone.Core.Simulation;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class MunitionEntityTests
{
- [Fact]
- public void GroundBased_DoesNotArriveImmediately()
- {
- var m = new MunitionEntity("m1", PlatformType.GroundBased,
- AerosolType.InertGas,
- startX: 0, startY: 0, startZ: 0,
- targetX: 5000, targetY: 300, targetZ: 0,
- muzzleVelocity: 500f, releaseAltitude: 300f, launchTime: 0f);
-
- m.Update(0.05f);
- Assert.False(m.HasArrived, "炮弹不能第一帧就到达");
- }
-
- [Fact]
- public void GroundBased_ArrivesNearTarget_WithInterpolation()
- {
- var m = new MunitionEntity("m2", PlatformType.GroundBased,
- AerosolType.InertGas,
- startX: 5000, startY: 0, startZ: 50,
- targetX: 10000, targetY: 500, targetZ: 0,
- muzzleVelocity: 800f, releaseAltitude: 500f, launchTime: 143.69f);
-
- float time = Kinematics.ParabolicShellTime(5000.25f, 500f, 800f);
- for (float t = 0; t < time + 1f; t += 0.4f)
- {
- m.Update(0.4f);
- if (m.HasArrived) break;
- }
- Assert.True(m.HasArrived, "应到达");
- Assert.Equal(500f, m.PosY, 5f);
- Assert.True(System.Math.Abs(m.PosX - 10000) < 200f, $"X={m.PosX:F1} 应≈10000");
- }
-
// ═══════════════════════════════════════
- // 抛物线运动学:固定角度验证
+ // 抛物线运动学:方位角和位置的对应关系
// ═══════════════════════════════════════
[Fact]
public void Parabolic_45Deg_Azimuth0_MovesInZ()
{
- // ParabolicPosition: azimuth=0 → +Z方向, sin(0)=0→X, cos(0)=1→Z
+ // azimuth=0 → +Z方向, sin(0)=0→X, cos(0)=1→Z
float angle45 = 45f * (float)System.Math.PI / 180f;
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle45, 0, 100, 1f);
Assert.True(System.Math.Abs(x) < 0.1f, $"X={x:F1} ≈0");
@@ -58,7 +24,6 @@ namespace CounterDrone.Core.Tests
[Fact]
public void Parabolic_30Deg_Azimuth90_MovesInX()
{
- // azimuth=90°(π/2): sin(π/2)=1→X, cos(π/2)=0→Z
float angle30 = 30f * (float)System.Math.PI / 180f;
float az90 = 90f * (float)System.Math.PI / 180f;
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle30, az90, 100, 1f);
@@ -69,62 +34,11 @@ namespace CounterDrone.Core.Tests
[Fact]
public void Parabolic_60Deg_Azimuth270_MovesBackward()
{
- // azimuth=270°(3π/2): sin=-1→X方向, cos=0→Z
float angle60 = 60f * (float)System.Math.PI / 180f;
float az270 = 270f * (float)System.Math.PI / 180f;
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle60, az270, 100, 1f);
Assert.True(System.Math.Abs(x + 50f) < 1f, $"X={x:F1} ≈-50");
Assert.True(System.Math.Abs(z) < 0.1f, $"Z={z:F1} ≈0");
}
-
- [Fact]
- public void GroundBased_LaunchAngle_ReachesReleaseAltitude()
- {
- // 验证运动学:给定距离和初速,弹道能达到释放高度
- var range = 5000f;
- var v0 = 500f;
- var releaseAlt = 300f;
- var angle = Kinematics.CalculateLaunchAngle(range, v0, releaseAlt);
-
- // 弹道顶点高度 >= 释放高度
- var peakH = v0 * v0 * (float)System.Math.Sin(angle) * (float)System.Math.Sin(angle) / (2f * 9.81f);
- Assert.True(peakH >= releaseAlt,
- $"弹道顶点 {peakH:F0}m ≥ 释放高度 {releaseAlt}m, angle={angle * 180 / System.Math.PI:F1}°");
- }
-
- [Fact]
- public void GroundBased_MovesForwardOverTime()
- {
- var m = new MunitionEntity("m1", PlatformType.GroundBased,
- AerosolType.InertGas,
- startX: 0, startY: 0, startZ: 0,
- targetX: 1000, targetY: 300, targetZ: 0,
- muzzleVelocity: 500f, releaseAltitude: 300f, launchTime: 0f);
-
- m.Update(1f);
- var dist = System.Math.Sqrt(m.PosX * m.PosX + m.PosZ * m.PosZ);
- Assert.True(dist > 100f, "1秒后炮弹应水平移动 > 100m");
- }
-
- [Fact]
- public void AirBased_DropsToReleaseAltitude()
- {
- var m = new MunitionEntity("m1", PlatformType.AirBased,
- AerosolType.InertGas,
- startX: 0, startY: 500, startZ: 0,
- targetX: 1000, targetY: 300, targetZ: 0,
- muzzleVelocity: 0, releaseAltitude: 300f, launchTime: 0f);
-
- // 空基投放:自由落体 500m → 300m = 200m 下落
- // h = 0.5*g*t² → t = sqrt(2*200/9.81) ≈ 6.4s
- for (int i = 0; i < 500; i++)
- {
- m.Update(0.05f);
- if (m.HasArrived) break;
- }
-
- Assert.True(m.HasArrived, "空基弹药应到达释放高度");
- Assert.True(m.PosY <= 300f + 1f);
- }
}
}
diff --git a/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs b/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs
index 09f932b..ae13c86 100644
--- a/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs
+++ b/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs
@@ -157,7 +157,7 @@ namespace CounterDrone.Core.Tests
_scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300 });
_scenarioService.SaveRoute(task.Id, "d", new RoutePlan(), new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 }, new() { PosX = 5000, PosY = 300, PosZ = 0, Speed = 60 } });
- _engine.SetFireSchedule(new List { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0 } });
+ _engine.SetFireSchedule(new List { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0, MuzzleVelocity = 55, LaunchAngle = 0 } });
_engine.Initialize(task.Id);
_engine.TimeScale = 10f;
@@ -178,7 +178,7 @@ namespace CounterDrone.Core.Tests
public void GroundBased_LaunchesWithCorrectDescription()
{
SetupScenario();
- _engine.SetFireSchedule(new List { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800 } });
+ _engine.SetFireSchedule(new List { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800, LaunchAngle = Kinematics.CalculateLaunchAngle(1000f, 800f, 300f) } });
_engine.Initialize(_taskId);
SimEvent launch = null;