diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index daf78d5..0ad63c3 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -162,14 +162,12 @@ namespace CounterDrone.Core.Algorithms 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; - // 此单元的所有弹药锁定同一个 Y 车道 int yLane = currentLane; var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth); float unitBaseTime = refEvt[0].FireTime; @@ -180,8 +178,10 @@ namespace CounterDrone.Core.Algorithms { float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing; var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth); + // 车道内序号 = 该车道已分配的弹药数 + int roundInLane = singleNeeded - laneNeeded[currentLane]; foreach (var e in fe) - e.FireTime = unitBaseTime + ch * stagger; + e.FireTime = unitBaseTime + roundInLane * stagger; fevents.AddRange(fe); eventIdx++; } diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index f3d396e..fc27bc7 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -52,6 +52,21 @@ namespace CounterDrone.Core.Tests var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefaultDefensePlanner(_ammoCatalog)); engine.Initialize(_taskId); + + // 诊断:检查 _fireSchedule 的时序 + var fld = typeof(SimulationEngine).GetField("_fireSchedule", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (fld != null) + { + var schedule = fld.GetValue(engine) as System.Collections.IList; + if (schedule != null && schedule.Count > 0) + { + var ftimes = schedule.Cast().Select(e => ((dynamic)e).FireTime).Cast().ToList(); + System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\schedule_dump.txt", + $"count={schedule.Count} first={ftimes.Min():F3} last={ftimes.Max():F3} span={ftimes.Max()-ftimes.Min():F3}\n" + + string.Join("\n", ftimes.Select((t,i) => $"{t:F3}"))); + } + } + engine.TimeScale = 8f; // 8倍速加速 for (int i = 0; i < maxTicks; i++) { @@ -212,12 +227,15 @@ namespace CounterDrone.Core.Tests var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); - var msg = $"Piston: launched={launched} clouds={clouds} drones={eng.Drones.Count}"; + var times = eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched).Select(e => e.OccurredAt).OrderBy(t => t).ToList(); + var timeInfo = $"times=[{times.First():F2}..{times.Last():F2}] span={times.Last()-times.First():F2}s"; + + var msg = $"Piston: launched={launched} clouds={clouds} drones={eng.Drones.Count} {timeInfo}"; foreach (var d in eng.Drones) msg += $" [{d.Status} hp={d.Hp:F2}]"; VerifyAndExportReport(eng, eng.Drones[0]); Assert.True(launched > 0, msg); - Assert.All(eng.Drones, d => Assert.Equal(DroneStatus.Destroyed, d.Status)); + Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg); } // ═══════════════════════════════════════════════