fix: spacing=2R消除魔法数字,turbulentRadius贯穿CalcRoundsNeeded

- ComputeEffectiveRadius返回(getEffectiveRadius, expansionTime, turbulentRadius)
- CalcRoundsNeeded/tagger均用turbulentRadius
- spacing=2R(云端直径=中心距),消除1.5f经验系数
- FireEvent.OccurredAt存储fe.FireTime精确发射时刻
This commit is contained in:
tian 2026-06-13 16:03:01 +08:00
parent 6cd18bf142
commit 09f51939d8
3 changed files with 26 additions and 27 deletions

View File

@ -131,7 +131,8 @@ namespace CounterDrone.Core.Algorithms
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
// 单机需求
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false);
var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(threat, ammo, env);
int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius);
// 横向编队:每种 Width = Quantity 个独立车道
int yLanes = 1;
@ -144,8 +145,7 @@ namespace CounterDrone.Core.Algorithms
int totalRoundsNeeded = singleNeeded * yLanes;
// 逐单元分配:每个单元锁定一个 Y 车道
var (effR2, _) = ComputeEffectiveRadius(threat, ammo, env);
float spacing = effR2 * 1.5f;
float spacing = 2f * turbulentRadius;
int[] laneNeeded = new int[yLanes];
for (int l = 0; l < yLanes; l++) laneNeeded[l] = singleNeeded;
int currentLane = 0;
@ -169,7 +169,7 @@ namespace CounterDrone.Core.Algorithms
int yLane = currentLane;
var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
float unitBaseTime = refEvt[0].FireTime;
float stagger = Kinematics.CloudCoverInterval(effR2 * 2f, (float)threat.Target.TypicalSpeed, c.Unit.ChannelInterval);
float stagger = Kinematics.CloudCoverInterval(turbulentRadius * 2f, (float)threat.Target.TypicalSpeed, c.Unit.ChannelInterval);
var fevents = new List<FireEvent>();
int unitIdx = fireUnits.IndexOf(c.Unit);
@ -245,7 +245,7 @@ namespace CounterDrone.Core.Algorithms
var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType);
var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo);
var ammoEff = ComputeEffectiveRadius(threat, ammo, env);
var (ammoEff, _, _) = ComputeEffectiveRadius(threat, ammo, env);
foreach (var unit in availableUnits)
{
@ -268,7 +268,7 @@ namespace CounterDrone.Core.Algorithms
private InterceptCandidate? BuildGroundBasedCandidate(DroneGroup threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
(float radius, float expTime) ammoEff, CombatScene env)
float effectiveR, CombatScene env)
{
var mid = ThreatMidpoint(threat);
float dx = mid.X - unit.Position.X;
@ -284,7 +284,7 @@ namespace CounterDrone.Core.Algorithms
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f;
float actualExposure = ammoEff.radius * 2f / avgSpeed;
float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(0.95f, actualExposure / neededExposure);
return new InterceptCandidate
@ -292,7 +292,7 @@ namespace CounterDrone.Core.Algorithms
Unit = unit,
AmmoType = ammoType,
EarliestInterceptTime = interceptTime,
CoverageDuration = ammoEff.radius * 2f,
CoverageDuration = effectiveR * 2f,
KillProbability = prob,
FlightTime = shellTime,
ShellFlightTime = shellTime,
@ -301,7 +301,7 @@ namespace CounterDrone.Core.Algorithms
private InterceptCandidate? BuildAirBasedCandidate(DroneGroup threat,
FireUnit unit, AerosolType ammoType, AmmunitionSpec ammo,
(float radius, float expTime) ammoEff, CombatScene env)
float effectiveR, CombatScene env)
{
if (unit.ReleaseAltitude <= 0 || unit.CruiseSpeed <= 0) return null;
var mid = ThreatMidpoint(threat);
@ -318,7 +318,7 @@ namespace CounterDrone.Core.Algorithms
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
float neededExposure = ammoType == AerosolType.ActiveMaterial ? 2f : 6f;
float actualExposure = ammoEff.radius * 2f / avgSpeed;
float actualExposure = effectiveR * 2f / avgSpeed;
float prob = Math.Min(0.95f, actualExposure / neededExposure);
return new InterceptCandidate
@ -326,7 +326,7 @@ namespace CounterDrone.Core.Algorithms
Unit = unit,
AmmoType = ammoType,
EarliestInterceptTime = interceptTime,
CoverageDuration = ammoEff.radius * 2f,
CoverageDuration = effectiveR * 2f,
KillProbability = prob,
FlightTime = totalTime,
ShellFlightTime = fallTime,
@ -337,7 +337,7 @@ namespace CounterDrone.Core.Algorithms
// 弹药计算(使用 AmmunitionSpec + 环境参数)
// ═══════════════════════════════════════════════
private (float effectiveRadius, float expansionTime) ComputeEffectiveRadius(
private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius(
DroneGroup threat, AmmunitionSpec ammo, CombatScene env)
{
float totalFlightTime = threat.ArrivalTime * 2f;
@ -353,10 +353,10 @@ namespace CounterDrone.Core.Algorithms
// Phase 2 湍流膨胀后的半径
float phase2Time = Math.Min(halfTime, 30f);
float rPhase2 = r0 + k * (float)Math.Sqrt(Math.Max(0, phase2Time));
float turbulentRadius = r0 + k * (float)Math.Sqrt(Math.Max(0, phase2Time));
float expansionTime = (float)Math.Pow((rPhase2 - r0) / Math.Max(k, 0.01f), 2);
float effectiveR = rPhase2;
float expansionTime = (float)Math.Pow((turbulentRadius - r0) / Math.Max(k, 0.01f), 2);
float effectiveR = turbulentRadius;
// Phase 3 高斯扩散
if (halfTime > 30f)
@ -370,19 +370,18 @@ namespace CounterDrone.Core.Algorithms
if (peakC > threshold)
{
float sigma = (sY + sZ) / 2f;
effectiveR = rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
effectiveR = turbulentRadius + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
}
}
return (effectiveR, expansionTime);
return (effectiveR, expansionTime, turbulentRadius);
}
private int CalcRoundsNeeded(DroneGroup threat, AmmunitionSpec ammo,
CombatScene env, bool isAirBased)
CombatScene env, bool isAirBased, float turbulentRadius)
{
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env);
float spacing = effectiveR * 1.5f;
float spacing = 2f * turbulentRadius;
var aerosolType = (AerosolType)ammo.AerosolType;
float neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
@ -390,14 +389,14 @@ namespace CounterDrone.Core.Algorithms
float requiredCoverage = singleCoverage;
return Math.Max(1,
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
(int)Math.Ceiling((requiredCoverage - 2f * turbulentRadius) / spacing) + 1);
}
private float ComputeInterceptProbability(DroneGroup threat,
AmmunitionSpec ammo, int rounds, CombatScene env)
{
float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f;
var (effectiveR, _) = ComputeEffectiveRadius(threat, ammo, env);
var (effectiveR, _, _) = ComputeEffectiveRadius(threat, ammo, env);
float spacing = effectiveR * 1.5f;
float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR;
float actualExposure = actualCoverage / avgSpeed;
@ -419,7 +418,7 @@ namespace CounterDrone.Core.Algorithms
var events = new List<FireEvent>();
var mid = ThreatMidpoint(threat);
var (effectiveR, expansionTime) = ComputeEffectiveRadius(threat, ammo, env);
var (effectiveR, expansionTime, _) = ComputeEffectiveRadius(threat, ammo, env);
// 编队 Y 偏移:按车道分布
float laneSpacingZ = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f;

View File

@ -181,7 +181,7 @@ namespace CounterDrone.Core.Simulation
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight);
_munitions.Add(m);
OnMunitionLaunched?.Invoke(m);
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = SimulationTime, SourceId = platform.Id, Description = $"计划发射 {m.Id}" });
frameEvents.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = fe.FireTime, SourceId = platform.Id, Description = $"计划发射 {m.Id}" });
}
}

View File

@ -187,14 +187,14 @@ namespace CounterDrone.Core.Tests
{
GroupId = "default", TargetType = (int)TargetType.Piston,
PowerType = (int)PowerType.Piston,
Quantity = 10, // 10
Quantity = 5, // 5
TypicalSpeed = 200, TypicalAltitude = 500,
});
_scenario.SaveRoute(_taskId, "default", new RoutePlan
{
FormationMode = (int)FormationMode.Formation,
LateralSpacing = 50,
LateralCount = 10,
LateralCount = 5,
LongitudinalCount = 1,
},
new List<Waypoint>
@ -204,7 +204,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, new List<EquipmentDeployment>
{
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 10, 5000, 0, 50),
MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 5, 5000, 0, 50),
});
_scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });