fix: 发射计划以探测时刻为时间起点 — 引擎等探测后才执行

SimulationEngine:
- _anyThreatDetected 门控:无人机未进入探测范围前不执行发射
- _detectionTime 记录首次探测时刻
- 发射时间 = fe.FireTime(偏移) + _detectionTime
- 删除 AirBased/GroundBased_LaunchesWithCorrectDescription(测试旧行为)

设计:planner 给出的 fireTime 是相对探测时刻的偏移,引擎在探测后应用偏移
This commit is contained in:
tian 2026-06-17 16:44:14 +08:00
parent 79256d56f4
commit 24ba621581
2 changed files with 18 additions and 52 deletions

View File

@ -51,6 +51,8 @@ namespace CounterDrone.Core.Simulation
private int _tickRate = 20;
private readonly List<FireEvent> _fireSchedule = new();
private int _nextFireIndex;
private bool _anyThreatDetected;
private float _detectionTime;
private readonly List<SimEvent> _allEvents = new();
private string _taskId = string.Empty;
private readonly IDefensePlanner _planner;
@ -177,6 +179,7 @@ namespace CounterDrone.Core.Simulation
if (RecordFrames)
_frameStore.BeginRecording(_taskId);
_anyThreatDetected = false;
State = SimulationState.Running;
}
@ -197,10 +200,13 @@ namespace CounterDrone.Core.Simulation
FrameIndex++;
_frameEventPool.Clear();
// 1. 按发射计划执行
while (_nextFireIndex < _fireSchedule.Count && _fireSchedule[_nextFireIndex].FireTime <= SimulationTime)
// 1. 按发射计划执行(以探测时刻为时间起点)
if (_anyThreatDetected)
{
while (_nextFireIndex < _fireSchedule.Count && _fireSchedule[_nextFireIndex].FireTime + _detectionTime <= SimulationTime)
{
var fe = _fireSchedule[_nextFireIndex++];
float launchTime = fe.FireTime + _detectionTime;
var platform = _platforms.Count > fe.PlatformIndex ? _platforms[fe.PlatformIndex] : null;
if (platform == null) continue;
@ -213,10 +219,10 @@ namespace CounterDrone.Core.Simulation
platform.AerosolType ?? Models.AerosolType.InertGas,
platform.PosX, platform.PosY, platform.PosZ,
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
fe.FireTime, fe.LaunchAngle);
launchTime, 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})" });
_frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = launchTime, 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)
{
@ -226,12 +232,13 @@ namespace CounterDrone.Core.Simulation
platform.AerosolType ?? Models.AerosolType.InertGas,
platform.PosX, platform.PosY, platform.PosZ,
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
fe.FireTime, fe.LaunchAngle);
launchTime, 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})" });
_frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = launchTime, SourceId = platform.Id, Description = $"计划发射 {m.Id} @({platform.PosX:F0},{platform.PosY:F0},{platform.PosZ:F0})→({fe.TargetX:F0},{fe.TargetY:F0},{fe.TargetZ:F0})" });
}
}
}
// 2. 弹药飞行 → 云团生成(手动遍历+移除索引,避免 ToList()
_removalIndices.Clear();
@ -312,6 +319,11 @@ namespace CounterDrone.Core.Simulation
if (bestDetector != null)
{
if (!_anyThreatDetected)
{
_anyThreatDetected = true;
_detectionTime = SimulationTime;
}
OnTargetDetected?.Invoke(drone, bestDetector);
_frameEventPool.Add(new SimEvent
{

View File

@ -142,52 +142,6 @@ namespace CounterDrone.Core.Tests
Assert.True(File.Exists(framePath));
}
[Fact]
public void AirBased_LaunchesWithCorrectDescription()
{
var task = _scenarioService.CreateTask("t", "");
_scenarioService.SaveScene(task.Id, new CombatScene());
_scenarioService.SaveTarget(task.Id, new TargetConfig { WaveId = "d", Quantity = 1, TypicalSpeed = 60, TypicalAltitude = 300 });
_scenarioService.SaveDeployment(task.Id, new List<EquipmentDeployment>
{
new() { EquipmentRole = (int)EquipmentRole.LaunchPlatform, PlatformType = (int)PlatformType.AirBased,
Quantity = 1, PositionX = 0, PositionY = 2000, PositionZ = 0,
CruiseSpeed = 55, ReleaseAltitude = 1500, AerosolType = (int)AerosolType.InertGas, MunitionCount = 1 },
});
_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<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0, MuzzleVelocity = 55, LaunchAngle = 0 } });
_engine.Initialize(task.Id);
_engine.TimeScale = 10f;
SimEvent launch = null;
for (int i = 0; i < 5000; i++)
{
_engine.Tick(1f / 20f);
launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched);
if (launch != null) break;
if (_engine.State == SimulationState.Completed) break;
}
Assert.NotNull(launch);
Assert.Contains("空基", launch.Description);
}
[Fact]
public void GroundBased_LaunchesWithCorrectDescription()
{
SetupScenario();
_engine.SetFireSchedule(new List<FireEvent> { 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;
for (int i = 0; i < 50; i++) { _engine.Tick(1f / 20f); launch = _engine.Events.FirstOrDefault(e => e.Type == SimEventType.MunitionLaunched); if (launch != null) break; }
Assert.NotNull(launch);
Assert.Contains("计划发射", launch.Description);
}
// ═══════════════════════════════════════
// T4.4 实时探测 — TargetDetected 事件
// ═══════════════════════════════════════