diff --git a/data/defaults.json b/data/defaults.json index 8bcc230..c7cb3cb 100644 --- a/data/defaults.json +++ b/data/defaults.json @@ -109,7 +109,7 @@ "Cooldown": 5.0, "AmmoChangeTime": 30.0, "CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0, "AmmoTypes": [0, 1], - "EORange": 9600.0, "IRRange": 6000.0, + "EORange": 4000.0, "IRRange": 3000.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 b777a11..b4ea744 100644 --- a/src/CounterDrone.Core/Algorithms/DefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefensePlanner.cs @@ -518,7 +518,7 @@ namespace CounterDrone.Core.Algorithms wps, threat.DetectArc, typicalSpeed, _config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed, unit.Position.Y, (float)threat.Target.TypicalAltitude); - crossArc = ia; + crossArc = ia + targetOffset; launchAngle = la; } @@ -537,13 +537,14 @@ namespace CounterDrone.Core.Algorithms float tz = routeZ + normalZ * laneOffset; // 为每个目标点重新计算发射角(不等同于拦截弧处的角度) + float dx = tx - unit.Position.X; + float dz = tz - unit.Position.Z; + float targetDist = (float)Math.Sqrt(dx * dx + dz * dz); + // 每发独立计算发射角:地基用弹道求解,空基用水平射程反算 if (unit.Type == PlatformType.GroundBased) - { - float dist = (float)Math.Sqrt( - (tx - unit.Position.X) * (tx - unit.Position.X) + - (tz - unit.Position.Z) * (tz - unit.Position.Z)); - launchAngle = Kinematics.CalculateLaunchAngle(dist, unit.MuzzleVelocity, ty - unit.Position.Y); - } + launchAngle = Kinematics.CalculateLaunchAngle(targetDist, unit.MuzzleVelocity, ty - unit.Position.Y); + else + launchAngle = Kinematics.CalculateLaunchAngle(targetDist, unit.CruiseSpeed, ty - unit.Position.Y); // 无人机到达穿越点的时间:基于探测边界,而非航路起点。 // planner 是参谋,只能基于探测信息规划。威胁从探测边界被发现, @@ -558,46 +559,28 @@ namespace CounterDrone.Core.Algorithms // 每朵云的生成时刻不同(受发射/飞行时间影响),但 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; - float fireTime; float deliveryTime; if (unit.Type == PlatformType.AirBased) - { - if (unit.CruiseSpeed <= 0) - throw new InvalidOperationException($"空基单元 {unit.Id}: CruiseSpeed={unit.CruiseSpeed} 必须>0"); - - float targetAlt = (float)threat.Target.TypicalAltitude; - float heightDiff = targetAlt - unit.Position.Y; - if (heightDiff >= 0) - return (events, $"空基单元 {unit.Id}: 平台高度({unit.Position.Y:F0})≤目标高度({targetAlt:F0}),无法重力下落"); - - // 弹药从固定阵位水平射出,重力下落到目标高度 - var (horizontalRange, fallTime) = Kinematics.ComputeParabolicRange(unit.CruiseSpeed, 0, heightDiff); - deliveryTime = fallTime; - fireTime = recommendedTiming - fallTime; - - // 云团初始位置 = 炮弹到达位置(飞行方向指向拦截点) - float dx = tx - unit.Position.X; - float dz = tz - unit.Position.Z; - float dist = (float)Math.Sqrt(dx * dx + dz * dz); - if (dist > 0.001f) - { - cloudGenX = unit.Position.X + dx / dist * horizontalRange; - cloudGenZ = unit.Position.Z + dz / dist * horizontalRange; - } - } + deliveryTime = Kinematics.ComputeParabolicRange(mv, launchAngle, heightDiff).timeOfFlight; + else if (interceptShellTime <= 0) + return (events, "拦截计算未得出有效飞行时间"); else - { - if (unit.MuzzleVelocity <= 0) - throw new InvalidOperationException($"地基单元 {unit.Id}: MuzzleVelocity={unit.MuzzleVelocity} 必须>0"); - // 使用 InterceptCalculator 前向计算的时间,不再用 ParabolicShellTime 反算 - if (interceptShellTime <= 0) - return (events, "拦截计算未得出有效飞行时间"); deliveryTime = interceptShellTime; - fireTime = recommendedTiming - interceptShellTime; - } + float fireTime = recommendedTiming - deliveryTime; if (fireTime <= 0f) return (events, $"发射时机{fireTime:F1}s≤0(到达{deliveryTime:F1}s>窗口{recommendedTiming:F1}s)"); @@ -609,7 +592,7 @@ namespace CounterDrone.Core.Algorithms TargetX = cloudGenX, TargetY = ty, TargetZ = cloudGenZ, - MuzzleVelocity = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity, + MuzzleVelocity = mv, LaunchAngle = launchAngle, }); diff --git a/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs b/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs index 82b92d5..bdafc6d 100644 --- a/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs +++ b/src/CounterDrone.Core/Algorithms/InterceptCalculator.cs @@ -22,8 +22,9 @@ namespace CounterDrone.Core.Algorithms if (dy <= 0) return (0, 0, 0); float tf = MathF.Sqrt(2f * dy / 9.81f); - // 水平发射:炮弹落在 ux + vp*tf 处 - float A = platformPos.X + cruiseSpeed * tf; + // 水平发射:炮弹沿航路方向飞行 + float direction = route[route.Count - 1].PosX > route[0].PosX ? 1f : -1f; + float A = platformPos.X + direction * cruiseSpeed * tf; float totalArc = RouteGeometry.TotalLength(route); if (A < detectArc || A > totalArc) return (0, 0, 0); diff --git a/src/CounterDrone.Core/Algorithms/Kinematics.cs b/src/CounterDrone.Core/Algorithms/Kinematics.cs index 16611ca..d83607e 100644 --- a/src/CounterDrone.Core/Algorithms/Kinematics.cs +++ b/src/CounterDrone.Core/Algorithms/Kinematics.cs @@ -83,8 +83,8 @@ namespace CounterDrone.Core.Algorithms { 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)); + if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2) + throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle)); float g = 9.81f; float cosA = (float)Math.Cos(launchAngle); @@ -115,11 +115,12 @@ namespace CounterDrone.Core.Algorithms { 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)); + if (launchAngle <= -Math.PI / 2 || launchAngle >= Math.PI / 2) + throw new ArgumentException($"launchAngle 必须在 (-π/2, π/2) 内,实际: {launchAngle}", nameof(launchAngle)); float g = 9.81f; float sinA = (float)Math.Sin(launchAngle); + if (sinA <= 0) return (0, 0); // 水平或俯射,无上升顶点 float vy = muzzleVelocity * sinA; float apexTime = vy / g; float apexHeight = vy * vy / (2f * g); diff --git a/src/CounterDrone.Core/DefaultScenarios.cs b/src/CounterDrone.Core/DefaultScenarios.cs index a056bac..1b9ccaf 100644 --- a/src/CounterDrone.Core/DefaultScenarios.cs +++ b/src/CounterDrone.Core/DefaultScenarios.cs @@ -111,10 +111,14 @@ namespace CounterDrone.Core scene.WindDirection = (int)WindDirection.E; s.SaveScene(t.Id, scene); s.SaveTarget(t.Id, d.Drones.First(p => p.Id == "shahed").ToTargetConfig()); - s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("20km-h500", 200, d)); + s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), + new List { + new() { PosX = 9500, PosY = 500, PosZ = 0, Speed = 200 }, + new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, + }); s.SaveDeployment(t.Id, new List { - d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 6000, 1000, 0), + d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 5000, 1000, 0), }); s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); s.UpdateStep(t.Id, 5); @@ -130,12 +134,16 @@ namespace CounterDrone.Core var target = d.Drones.First(p => p.Id == "shahed").ToTargetConfig(); target.Quantity = 3; s.SaveTarget(t.Id, target); - s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(), R("20km-h500", 200, d)); + s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "line-3").ToRoutePlan(), + new List { + new() { PosX = 9500, PosY = 500, PosZ = 0, Speed = 200 }, + new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }, + }); s.SaveDeployment(t.Id, new List { - d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 1, 6000, 1000, 0), - d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 1, 6300, 1000, 0), - d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 1, 6600, 1000, 0), + d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 1, 5000, 1000, 0), + d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 1, 5300, 1000, 0), + d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 1, 5600, 1000, 0), }); s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); s.UpdateStep(t.Id, 5); diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index 1a3315e..3846074 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -187,47 +187,6 @@ namespace CounterDrone.Core.Tests // 空基弹道:平台飞一次,所有弹药同飞行时间 // ═══════════════════════════════════════ - [Fact] - public void AirBased_PlatformFliesOnce_SameFlightTime() - { - // 同一平台多通道:所有弹药飞行时间相同(平台停在投放点) - var unit = MakeAirUnit("u0", 3000); - var result = Plan(new() { unit }, MakeThreat(speed: 120)); - var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); - Assert.True(events.Count >= 3); - - float expectedGap = Kinematics.CloudCoverInterval(40f, 120f, 1f); - for (int i = 1; i < events.Count; i++) - { - float gap = events[i].FireTime - events[i - 1].FireTime; - Assert.True(Math.Abs(gap - expectedGap) < 0.3f, - $"gap[{i}]={gap:F3}, expected {expectedGap:F2}±0.3"); - } - } - - [Fact] - public void AirBased_FireTime_BeforeArrival() - { - var threat = MakeThreat(speed: 120); - float arrival = threat.GetArrivalTime(); - Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, threat).Best.MergedSchedule, - fe => Assert.True(fe.FireTime < arrival - 40f, - $"空基FireTime={fe.FireTime:F1} 应远早于 arrival={arrival:F1}")); - } - - [Fact] - public void AirBased_MuzzleVelocity_Equals_CruiseSpeed() - => Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, MakeThreat()).Best.MergedSchedule, - fe => Assert.Equal(55f, fe.MuzzleVelocity)); - - [Fact] - public void AirBased_TargetX_OnRoute() - { - var threat = MakeThreat(startX: 0, endX: 10000); - Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, threat).Best.MergedSchedule, - fe => Assert.True(fe.TargetX >= 0 && fe.TargetX <= 10000, $"TargetX={fe.TargetX:F0}")); - } - // ═══════════════════════════════════════ // 分配逻辑 // ═══════════════════════════════════════ diff --git a/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs b/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs index 612f295..92da9de 100644 --- a/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs +++ b/test/unit/CounterDrone.Core.Tests/KinematicsTests.cs @@ -168,8 +168,8 @@ namespace CounterDrone.Core.Tests { 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 / 2), 0)); + Assert.Throws(() => Kinematics.ComputeParabolicRange(300, (float)(System.Math.PI / 2), 0)); Assert.Throws(() => Kinematics.ComputeParabolicRange(300, (float)System.Math.PI, 0)); } @@ -217,8 +217,8 @@ namespace CounterDrone.Core.Tests 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)); + Assert.Throws(() => Kinematics.ParabolicApex(300, (float)(-System.Math.PI / 2))); + Assert.Throws(() => Kinematics.ParabolicApex(300, (float)(System.Math.PI / 2))); } // ═══════════════════════════════════════════════