PlanUnitLane: ParabolicMotion.GetFlightTimes 选远点(离无人机近) FireEvent: 新增 FlightDuration 字段 MunitionEntity: 接收 flightDuration → _useTimeArrival,按时间到达 地基: FlightDuration = interceptShellTime(保持 InterceptCalculator 一致性) 空基: FlightDuration = ParabolicMotion 选出的远点 tof 238 全部通过
659 lines
33 KiB
C#
659 lines
33 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using CounterDrone.Core.Models;
|
||
|
||
namespace CounterDrone.Core.Algorithms
|
||
{
|
||
/// <summary>防御规划器 — 五步流水线,全部调用与引擎共享的物理工具类</summary>
|
||
public class DefensePlanner : IDefensePlanner
|
||
{
|
||
private readonly List<AmmunitionSpec> _ammoCatalog;
|
||
private readonly IDamageModel _damageModel;
|
||
private readonly PlannerConfig _config;
|
||
private readonly ILaneDivider _laneDivider;
|
||
|
||
public DefensePlanner(List<AmmunitionSpec> ammoCatalog, PlannerConfig config,
|
||
IDamageModel? damageModel = null, ILaneDivider? laneDivider = null)
|
||
{
|
||
_ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog));
|
||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||
_damageModel = damageModel ?? new DamageModelRouter();
|
||
_laneDivider = laneDivider ?? new DefaultLaneDivider();
|
||
if (_ammoCatalog.Count == 0)
|
||
throw new ArgumentException("弹药规格目录不能为空");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 五步流水线
|
||
// ═══════════════════════════════════════════════
|
||
|
||
public PlannerResult Plan(List<FireUnit> fireUnits, List<DroneWave> threats,
|
||
CombatScene environment, List<DetectionSource> detectionSources)
|
||
{
|
||
var result = new PlannerResult();
|
||
|
||
if (threats.Count == 0)
|
||
{
|
||
result.Best.Summary = "无威胁目标";
|
||
return result;
|
||
}
|
||
if (fireUnits.Count == 0)
|
||
{
|
||
result.Best.Summary = "无可用火力单元";
|
||
result.Best.ThreatsUnengaged = threats.Count;
|
||
return result;
|
||
}
|
||
|
||
// 探测信息:每个威胁的最早发现弧长 + 精度(基于统一信息网络)
|
||
// 无探测设备时 detectArc=0(上帝视角,从航路起点算)、精度用配置默认值
|
||
float visibility = (float)environment.Visibility;
|
||
foreach (var t in threats)
|
||
{
|
||
var (detectArc, accuracy) = DetectionCalculator.EarliestDetection(
|
||
t.Waypoints, detectionSources, visibility);
|
||
t.DetectArc = detectArc;
|
||
t.DetectAccuracy = accuracy == float.MaxValue
|
||
? _config.DefaultDetectionAccuracy
|
||
: accuracy;
|
||
}
|
||
|
||
// Step 1: 威胁排序
|
||
foreach (var t in threats)
|
||
{
|
||
t.ArrivalTime = t.GetArrivalTime();
|
||
t.ThreatIndex = CalcThreatIndex(_config, t.Target);
|
||
}
|
||
var sorted = threats.OrderByDescending(t => t.Priority).ToList();
|
||
|
||
// Step 2-4: 贪心分配求解
|
||
// 预先检查:所有需要的弹药类型都在目录中
|
||
var neededTypes = sorted
|
||
.Select(t => MatchAmmo(_config, (PowerType)t.Target.PowerType))
|
||
.Distinct()
|
||
.ToList();
|
||
foreach (var t in neededTypes)
|
||
{
|
||
if (!_ammoCatalog.Any(a => a.AerosolType == (int)t))
|
||
throw new InvalidOperationException($"弹药规格目录中缺少类型: {t}");
|
||
}
|
||
|
||
result.Best = Solve(sorted, fireUnits, environment);
|
||
|
||
// Step 5: 临界方案
|
||
result.Critical = DeriveCritical(result.Best);
|
||
|
||
return result;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Step 1: 威胁指数
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private static float CalcThreatIndex(PlannerConfig config, TargetConfig target)
|
||
{
|
||
float typeCoef = config.TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f);
|
||
float speedCoef = (float)target.TypicalSpeed / 60f;
|
||
return typeCoef * speedCoef;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Step 2: 弹药匹配
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private static AerosolType MatchAmmo(PlannerConfig config, PowerType power)
|
||
{
|
||
return config.AmmoMatch.GetValueOrDefault(power, AerosolType.InertGas);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Step 3-4: 候选生成 → 贪心分配
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private DefensePlan Solve(List<DroneWave> sortedThreats,
|
||
List<FireUnit> fireUnits, CombatScene env)
|
||
{
|
||
var plan = new DefensePlan();
|
||
var remainingMunitions = new Dictionary<string, int>();
|
||
foreach (var u in fireUnits)
|
||
remainingMunitions[u.Id] = u.TotalMunitions;
|
||
|
||
foreach (var threat in sortedThreats)
|
||
{
|
||
var available = fireUnits
|
||
.Where(u => remainingMunitions.GetValueOrDefault(u.Id, 0) > 0)
|
||
.ToList();
|
||
|
||
var candidates = GenerateCandidates(threat, available, env);
|
||
if (candidates.Count == 0)
|
||
{
|
||
plan.ThreatsUnengaged++;
|
||
continue;
|
||
}
|
||
|
||
// 计算总弹药需求
|
||
var neededAmmo = MatchAmmo(_config, (PowerType)threat.Target.PowerType);
|
||
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
|
||
|
||
// 单机需求
|
||
var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env);
|
||
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius);
|
||
|
||
// 车道划分(可替换策略)
|
||
var (yLanes, laneSpacing) = _laneDivider.Divide(threat, turbulentRadius);
|
||
float formationWidth = yLanes > 1 ? (yLanes - 1) * laneSpacing : 0f;
|
||
int totalRoundsNeeded = singleNeeded * yLanes;
|
||
|
||
// 逐单元分配:每个单元锁定一个 Y 车道
|
||
// 云团间距 = 2R × (1 - 重叠比例),重叠由配置驱动,保证有效区互相覆盖
|
||
float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
|
||
int[] laneNeeded = new int[yLanes];
|
||
float[] laneBaseTime = new float[yLanes];
|
||
bool[] laneBaseSet = new bool[yLanes];
|
||
for (int l = 0; l < yLanes; l++) laneNeeded[l] = singleNeeded;
|
||
int currentLane = 0;
|
||
int roundsCollected = 0;
|
||
var assignedUnits = new List<(FireUnit unit, int rounds, List<FireEvent> events)>();
|
||
|
||
foreach (var c in candidates.OrderBy(c => c.EarliestInterceptTime)
|
||
.ThenByDescending(c => c.KillProbability))
|
||
{
|
||
if (roundsCollected >= totalRoundsNeeded) break;
|
||
int remaining = remainingMunitions.GetValueOrDefault(c.Unit.Id, 0);
|
||
if (remaining <= 0) continue;
|
||
|
||
while (currentLane < yLanes && laneNeeded[currentLane] <= 0) currentLane++;
|
||
if (currentLane >= yLanes) break;
|
||
|
||
int toTake = Math.Min(Math.Min(c.Unit.TotalChannels, remaining), laneNeeded[currentLane]);
|
||
if (toTake <= 0) continue;
|
||
|
||
int yLane = currentLane;
|
||
// 车道第一个单元设基准时间,后续单元以此为准保证间距均匀
|
||
if (!laneBaseSet[yLane])
|
||
{
|
||
var (refEvt, refRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
|
||
if (refEvt.Count == 0) { plan.RejectReason ??= refRej; continue; }
|
||
laneBaseTime[yLane] = refEvt[0].FireTime;
|
||
laneBaseSet[yLane] = true;
|
||
}
|
||
float stagger = Kinematics.CloudCoverInterval(turbulentRadius * 2f, GetDroneSpeedKph(threat), c.Unit.ChannelInterval);
|
||
|
||
var fevents = new List<FireEvent>();
|
||
int unitIdx = fireUnits.IndexOf(c.Unit);
|
||
int baseRoundInLane = singleNeeded - laneNeeded[currentLane];
|
||
for (int ch = 0; ch < toTake; ch++)
|
||
{
|
||
// offset 基于车道内发序(不是全局 eventIdx),使每车道独立分布在同一航路段,
|
||
// 多车道重叠覆盖而非沿航路连成长链
|
||
int roundInLane = baseRoundInLane + ch;
|
||
float offset = (roundInLane - (singleNeeded - 1) / 2f) * spacing;
|
||
var (fe, feRej) = TryGenerateFireEvents(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
|
||
if (fe.Count == 0) { plan.RejectReason ??= feRej; continue; }
|
||
foreach (var e in fe)
|
||
{
|
||
e.FireTime = laneBaseTime[yLane] + roundInLane * stagger;
|
||
// TargetX/Z 保留 GenerateFireEventsAt 算出的风偏补偿后抛撒点 cloudGenX/Z
|
||
e.PlatformIndex = unitIdx * c.Unit.TotalChannels + ch;
|
||
}
|
||
fevents.AddRange(fe);
|
||
}
|
||
|
||
assignedUnits.Add((c.Unit, toTake, fevents));
|
||
remainingMunitions[c.Unit.Id] -= toTake;
|
||
laneNeeded[currentLane] -= toTake;
|
||
roundsCollected += toTake;
|
||
}
|
||
|
||
if (roundsCollected <= 0) {
|
||
plan.RejectReason ??= $"候选{candidates.Count}个,弹药需求{totalRoundsNeeded}发,实际收集0发";
|
||
plan.ThreatsUnengaged++; continue;
|
||
}
|
||
|
||
foreach (var (unit, rounds, fireEvents) in assignedUnits)
|
||
{
|
||
plan.Assignments.Add(new UnitAssignment
|
||
{
|
||
FireUnitId = unit.Id,
|
||
DroneWaveId = threat.WaveId,
|
||
AmmoType = neededAmmo,
|
||
RoundsFired = rounds,
|
||
FirstFireTime = fireEvents.Count > 0 ? fireEvents[0].FireTime : 0,
|
||
FireEvents = fireEvents,
|
||
});
|
||
plan.MergedSchedule.AddRange(fireEvents);
|
||
}
|
||
|
||
plan.ThreatsEngaged++;
|
||
}
|
||
|
||
plan.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||
|
||
// 按威胁汇总概率
|
||
var threatProbs = new List<float>();
|
||
foreach (var threat in sortedThreats)
|
||
{
|
||
var a2 = _ammoCatalog.FirstOrDefault(s =>
|
||
s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Target.PowerType));
|
||
int totalRounds = plan.Assignments
|
||
.Where(a => a.DroneWaveId == threat.WaveId)
|
||
.Sum(a => a.RoundsFired);
|
||
if (totalRounds > 0)
|
||
threatProbs.Add(ComputeInterceptProbability(threat, a2, totalRounds, env));
|
||
}
|
||
plan.OverallProbability = threatProbs.Count > 0 ? threatProbs.Average() : 0f;
|
||
|
||
// 失败原因 + 建议值
|
||
var reasons = new List<string>();
|
||
foreach (var threat in sortedThreats)
|
||
{
|
||
bool engaged = plan.Assignments.Any(a => a.DroneWaveId == threat.WaveId);
|
||
if (engaged) continue;
|
||
var ammo = MatchAmmo(_config, (PowerType)threat.Target.PowerType);
|
||
var matching = fireUnits.Where(u => u.AmmoTypes.Contains(ammo)).ToList();
|
||
if (matching.Count == 0)
|
||
reasons.Add($"威胁 {threat.WaveId}: 无火力单元装载 {ammo} 弹药");
|
||
else
|
||
{
|
||
var (_, expTime, _) = ComputeEffectiveRadius(_ammoCatalog.First(a => a.AerosolType == (int)ammo), env);
|
||
float avgSpd = GetDroneSpeedMs(threat);
|
||
float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints,
|
||
(float)(threat.Waypoints[0].PosX + threat.Waypoints[^1].PosX) / 2f,
|
||
(float)(threat.Waypoints[0].PosZ + threat.Waypoints[^1].PosZ) / 2f);
|
||
float travel = midArc - threat.DetectArc;
|
||
if (travel <= 0)
|
||
{
|
||
var (mx, _, mz) = RouteGeometry.PositionAt(threat.Waypoints, midArc);
|
||
float r = matching.Min(u => MathF.Sqrt((mx - u.Position.X) * (mx - u.Position.X) + (mz - u.Position.Z) * (mz - u.Position.Z)));
|
||
reasons.Add($"威胁 {threat.WaveId}: 探测边界在拦截点之后,建议探测范围≥{r:F0}m");
|
||
}
|
||
else if (travel / avgSpd <= expTime)
|
||
{
|
||
float maxDA = Math.Max(0, midArc - avgSpd * (expTime + _config.TimingSafetyMargin));
|
||
var (nx, _, nz) = RouteGeometry.PositionAt(threat.Waypoints, maxDA);
|
||
float r = matching.Min(u => MathF.Sqrt((nx - u.Position.X) * (nx - u.Position.X) + (nz - u.Position.Z) * (nz - u.Position.Z)));
|
||
reasons.Add($"威胁 {threat.WaveId}: 拦截窗口不足({travel / avgSpd:F1}s<膨胀{expTime:F1}s),建议探测范围≥{r:F0}m(最晚弧长{maxDA:F0}m)");
|
||
}
|
||
else
|
||
{
|
||
string detail = plan.RejectReason != null ? $"({plan.RejectReason})" : "";
|
||
reasons.Add($"威胁 {threat.WaveId}: 候选存在但分配失败{detail}");
|
||
}
|
||
}
|
||
}
|
||
plan.Summary = plan.ThreatsEngaged > 0
|
||
? $"分配 {plan.ThreatsEngaged} 个威胁,{plan.ThreatsUnengaged} 个无方案"
|
||
+ (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "")
|
||
: "无威胁被分配拦截方案"
|
||
+ (reasons.Count > 0 ? "。原因: " + string.Join("; ", reasons) : "");
|
||
|
||
return plan;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 拦截窗口可行性检查
|
||
// ═══════════════════════════════════════════════
|
||
|
||
/// <summary>无人机速度(km/h),从航路第一个 waypoint 读取</summary>
|
||
private static float GetDroneSpeedKph(DroneWave threat)
|
||
{
|
||
float spd = (float)threat.Waypoints[0].Speed;
|
||
if (spd <= 0) throw new InvalidOperationException($"威胁 {threat.WaveId}: 航路点速度必须 > 0");
|
||
return spd;
|
||
}
|
||
|
||
/// <summary>无人机速度(m/s)</summary>
|
||
private static float GetDroneSpeedMs(DroneWave threat)
|
||
=> GetDroneSpeedKph(threat) / 3.6f;
|
||
|
||
internal static bool HasInterceptWindow(DroneWave threat, float midArc,
|
||
float expansionTime, float deliveryTime)
|
||
{
|
||
float travelArc = midArc - threat.DetectArc;
|
||
if (travelArc <= 0) return false;
|
||
float timeAvailable = travelArc / GetDroneSpeedMs(threat);
|
||
return timeAvailable > expansionTime + deliveryTime;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 候选生成(使用真实物理)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private List<InterceptCandidate> GenerateCandidates(DroneWave threat,
|
||
List<FireUnit> availableUnits, CombatScene env)
|
||
{
|
||
var candidates = new List<InterceptCandidate>();
|
||
var neededAmmo = MatchAmmo(_config, (PowerType)threat.Target.PowerType);
|
||
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
|
||
|
||
var (ammoEff, expansionTime, _) = ComputeEffectiveRadius(ammo, env);
|
||
var mid = ThreatMidpoint(threat);
|
||
float midArc = RouteGeometry.ArcLengthNearestTo(threat.Waypoints, mid.X, mid.Z);
|
||
|
||
foreach (var unit in availableUnits)
|
||
{
|
||
if (!unit.AmmoTypes.Contains(neededAmmo)) continue;
|
||
|
||
if (unit.Type == PlatformType.AirBased)
|
||
{
|
||
var c = BuildAirBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
|
||
if (c != null) candidates.Add(c);
|
||
}
|
||
else
|
||
{
|
||
var c = BuildGroundBasedCandidate(threat, unit, neededAmmo, ammo, ammoEff, expansionTime, mid, midArc, env);
|
||
if (c != null) candidates.Add(c);
|
||
}
|
||
}
|
||
|
||
return candidates;
|
||
}
|
||
|
||
private InterceptCandidate? BuildGroundBasedCandidate(DroneWave threat,
|
||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||
float effectiveR, float expansionTime,
|
||
Vector3 mid, float midArc, CombatScene env)
|
||
{
|
||
float dx = mid.X - unit.Position.X;
|
||
float dz = mid.Z - unit.Position.Z;
|
||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||
|
||
if (unit.MuzzleVelocity <= 0) return null;
|
||
float maxRange = unit.MuzzleVelocity * unit.MuzzleVelocity / 9.81f;
|
||
if (dist > maxRange) return null;
|
||
|
||
float shellTime = dist / unit.MuzzleVelocity;
|
||
if (!HasInterceptWindow(threat, midArc, expansionTime, shellTime)) return null;
|
||
|
||
float interceptTime = threat.ArrivalTime;
|
||
|
||
float avgSpeed = GetDroneSpeedMs(threat);
|
||
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
|
||
float actualExposure = effectiveR * 2f / avgSpeed;
|
||
float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
|
||
|
||
return new InterceptCandidate
|
||
{
|
||
Unit = unit,
|
||
AmmoType = ammoType,
|
||
EarliestInterceptTime = interceptTime,
|
||
KillProbability = prob,
|
||
};
|
||
}
|
||
|
||
private InterceptCandidate? BuildAirBasedCandidate(DroneWave threat,
|
||
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||
float effectiveR, float expansionTime,
|
||
Vector3 mid, float midArc, CombatScene env)
|
||
{
|
||
if (unit.ReleaseAltitude <= 0 || unit.CruiseSpeed <= 0) return null;
|
||
float releaseAlt = unit.ReleaseAltitude;
|
||
float flightDist = Kinematics.Distance3D(
|
||
unit.Position.X, unit.Position.Y, unit.Position.Z,
|
||
mid.X, releaseAlt, mid.Z);
|
||
float flightTime = flightDist / unit.CruiseSpeed;
|
||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
|
||
float deliveryTime = flightTime + fallTime;
|
||
|
||
float interceptTime = threat.ArrivalTime;
|
||
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,
|
||
unit.ReleaseAltitude, (float)threat.Target.TypicalAltitude);
|
||
if (icArc <= 0) return null;
|
||
|
||
float avgSpeed = GetDroneSpeedMs(threat);
|
||
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType);
|
||
float actualExposure = effectiveR * 2f / avgSpeed;
|
||
float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
|
||
|
||
return new InterceptCandidate
|
||
{
|
||
Unit = unit,
|
||
AmmoType = ammoType,
|
||
EarliestInterceptTime = interceptTime,
|
||
KillProbability = prob,
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 弹药计算(使用 AmmunitionSpec + 环境参数)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius(
|
||
AmmunitionSpec ammo, CombatScene env)
|
||
{
|
||
var model = new CloudExpansionModel(ammo, env);
|
||
float rPhase2 = model.RadiusAt(30f);
|
||
float expansionTime = model.TimeToReach(rPhase2);
|
||
// 云团被穿过时的真实年龄 = expansionTime(云团生成后膨胀到有效半径的时长)。
|
||
// 这是云团自身的物理量,与无人机飞行时间无关。
|
||
float effectiveR = model.RadiusAt(expansionTime);
|
||
return (effectiveR, expansionTime, rPhase2);
|
||
}
|
||
|
||
private int CalcRoundsNeeded(DroneWave threat, AmmunitionSpec ammo,
|
||
CombatScene env, bool isAirBased, float turbulentRadius)
|
||
{
|
||
var cloudModel = new CloudExpansionModel(ammo, env);
|
||
float avgSpeed = GetDroneSpeedMs(threat);
|
||
float neededExposure = _damageModel.RequiredExposureSeconds(
|
||
(TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType);
|
||
float requiredCoverage = neededExposure * avgSpeed;
|
||
// 间距由配置的重叠比例驱动,公式在 CloudExpansionModel(共享)
|
||
float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio);
|
||
return cloudModel.RoundsNeeded(requiredCoverage, spacing);
|
||
}
|
||
|
||
private float ComputeInterceptProbability(DroneWave threat,
|
||
AmmunitionSpec ammo, int rounds, CombatScene env)
|
||
{
|
||
float avgSpeed = GetDroneSpeedMs(threat);
|
||
var (effectiveR, _, _) = ComputeEffectiveRadius(ammo, env);
|
||
float spacing = 2f * effectiveR;
|
||
float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR;
|
||
float actualExposure = actualCoverage / avgSpeed;
|
||
|
||
var aerosolType = (AerosolType)ammo.AerosolType;
|
||
float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType);
|
||
|
||
return Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 发射事件生成(真实物理)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private (List<FireEvent> Events, string? RejectReason) TryGenerateFireEvents(
|
||
DroneWave threat, FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
|
||
CombatScene env, float targetOffset, int yLane, int yLanes, float formationWidth)
|
||
{
|
||
var events = new List<FireEvent>();
|
||
var wps = threat.Waypoints;
|
||
if (wps == null || wps.Count < 2) return (events, "航路无效");
|
||
|
||
var cloudModel = new CloudExpansionModel(ammo, env);
|
||
// 云龄 = expansionTime(云团生成后膨胀到有效半径的时长)
|
||
float effectiveAge = cloudModel.TimeToReach(cloudModel.TurbulentRadius) * _config.ExpansionFactor;
|
||
float effectiveR = cloudModel.RadiusAt(effectiveAge);
|
||
float expansionTime = cloudModel.TimeToReach(effectiveR);
|
||
|
||
float densityAtPassage = cloudModel.DensityAt(expansionTime);
|
||
if (densityAtPassage < (float)ammo.EffectiveConcentration)
|
||
return (events, $"云团密度{densityAtPassage:E2}<有效阈值{ammo.EffectiveConcentration:E2}");
|
||
|
||
float typicalSpeed = GetDroneSpeedKph(threat);
|
||
if (typicalSpeed <= 0) return (events, "目标速度无效");
|
||
|
||
// 用 InterceptCalculator 计算拦截弧长和发射角
|
||
var mid = ThreatMidpoint(threat);
|
||
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
|
||
float crossArc;
|
||
float launchAngle = 0;
|
||
float interceptShellTime = 0;
|
||
if (unit.Type == PlatformType.GroundBased)
|
||
{
|
||
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
|
||
{
|
||
var (ia, _, la) = InterceptCalculator.ComputeHorizontal(
|
||
wps, threat.DetectArc, typicalSpeed,
|
||
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
|
||
unit.Position.Y, (float)threat.Target.TypicalAltitude);
|
||
crossArc = ia + targetOffset;
|
||
launchAngle = la;
|
||
}
|
||
|
||
// 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。
|
||
// DroneEntity: latOffset = formationIndex * lateralSpacing(0/50/100...)
|
||
// 编队展开方向:读 RoutePlan.LateralAxis (0=X, 1=Y, 2=Z)
|
||
int axis = threat.Route?.LateralAxis ?? 2;
|
||
float laneSpacing = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;
|
||
float laneOffset = yLane * laneSpacing;
|
||
|
||
var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc);
|
||
float tx = routeX, ty = routeY, tz = routeZ;
|
||
if (axis == 0) tx += laneOffset;
|
||
else if (axis == 1) ty += laneOffset;
|
||
else tz += laneOffset;
|
||
|
||
// 风偏补偿:先算云团生成点,再按实际位置算发射角
|
||
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);
|
||
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);
|
||
var motion = new ParabolicMotion(mv, launchAngle);
|
||
var times = motion.GetFlightTimes(heightDiff);
|
||
if (times.Length == 0)
|
||
return (events, $"目标超出弹道射程: dist={targetDist:F0}m");
|
||
// 两个解时:远点更靠近无人机(较早拦截),优先选远点
|
||
float tof = times.Length == 2 ? times[1] : times[0];
|
||
float range = mv * (float)Math.Cos(launchAngle) * tof;
|
||
|
||
// 无人机到达穿越点的时间:基于探测边界,而非航路起点。
|
||
// planner 是参谋,只能基于探测信息规划。威胁从探测边界被发现,
|
||
// 飞行时间 = (拦截弧长 - 探测弧长) / 速度。无探测时 DetectArc=0(回退到起点)。
|
||
float travelArc = crossArc - threat.DetectArc;
|
||
if (travelArc < 0) return (events, $"探测边界在拦截点之后({threat.DetectArc:F0}m≥{crossArc:F0}m)");
|
||
float txArrival = RouteGeometry.TravelTimeTo(wps, travelArc, typicalSpeed);
|
||
float recommendedTiming = txArrival - expansionTime;
|
||
if (recommendedTiming <= 0f) return (events, $"膨胀{expansionTime:F1}s≥到达{txArrival:F1}s");
|
||
|
||
// cloudGenX/Z 已在上面计算(含风偏),直接使用
|
||
|
||
float deliveryTime;
|
||
if (unit.Type == PlatformType.AirBased)
|
||
deliveryTime = tof;
|
||
else if (interceptShellTime <= 0)
|
||
return (events, "拦截计算未得出有效飞行时间");
|
||
else
|
||
{
|
||
deliveryTime = interceptShellTime;
|
||
tof = interceptShellTime;
|
||
}
|
||
float fireTime = recommendedTiming - deliveryTime;
|
||
|
||
if (fireTime <= 0f)
|
||
return (events, $"发射时机{fireTime:F1}s≤0(到达{deliveryTime:F1}s>窗口{recommendedTiming:F1}s)");
|
||
|
||
events.Add(new FireEvent
|
||
{
|
||
FireTime = fireTime,
|
||
PlatformIndex = 0,
|
||
TargetX = cloudGenX,
|
||
TargetY = ty,
|
||
TargetZ = cloudGenZ,
|
||
MuzzleVelocity = mv,
|
||
LaunchAngle = launchAngle,
|
||
FlightDuration = tof,
|
||
});
|
||
|
||
return (events, null);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// Step 5: 临界方案(概率阈值 50%)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private DefensePlan DeriveCritical(DefensePlan best)
|
||
{
|
||
var critical = new DefensePlan
|
||
{
|
||
ThreatsEngaged = best.ThreatsEngaged,
|
||
ThreatsUnengaged = best.ThreatsUnengaged,
|
||
};
|
||
|
||
foreach (var assignment in best.Assignments)
|
||
{
|
||
int rounds = assignment.RoundsFired;
|
||
while (rounds > 1)
|
||
{
|
||
// 简化:弹药减半 → 概率减半
|
||
if ((float)rounds / assignment.RoundsFired < _config.CriticalProbabilityThreshold) break;
|
||
rounds--;
|
||
}
|
||
|
||
var reduced = new UnitAssignment
|
||
{
|
||
FireUnitId = assignment.FireUnitId,
|
||
DroneWaveId = assignment.DroneWaveId,
|
||
AmmoType = assignment.AmmoType,
|
||
RoundsFired = rounds,
|
||
FirstFireTime = assignment.FirstFireTime,
|
||
FireEvents = assignment.FireEvents.Take(rounds).ToList(),
|
||
};
|
||
critical.Assignments.Add(reduced);
|
||
critical.MergedSchedule.AddRange(reduced.FireEvents);
|
||
}
|
||
|
||
critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime));
|
||
critical.OverallProbability = _config.CriticalProbabilityThreshold;
|
||
critical.Summary = $"临界方案:刚好满足 50% 拦截概率";
|
||
return critical;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════
|
||
// 辅助
|
||
// ═══════════════════════════════════════════════
|
||
|
||
private static Vector3 ThreatMidpoint(DroneWave threat)
|
||
{
|
||
if (threat.Waypoints.Count < 2)
|
||
return new Vector3(0, (float)threat.Target.TypicalAltitude, 0);
|
||
var s = threat.Waypoints[0];
|
||
var e = threat.Waypoints[^1];
|
||
return new Vector3(
|
||
(float)(s.PosX + e.PosX) / 2f,
|
||
(float)(s.PosY + e.PosY) / 2f,
|
||
(float)(s.PosZ + e.PosZ) / 2f);
|
||
}
|
||
}
|
||
}
|