From 7eadea88abdb322d2c84944b8a438ec0d4ec6472 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 25 Sep 2026 03:14:15 +0200 Subject: [PATCH] Cut 12.2 Soul fix batch 2, S4: pin flight time for an invisible target F8 (Soul's earlier fix batch, 2026-09-24) moved range computation ahead of the visibility gate in FireControl.PFire so Fire's own flight time stays real even when the shot prices at PFire 0, but no test observed that behaviour -- N4 (Soul's own mutation set, reverting F8) survived every existing test. InvisibleTargetStillResolvesAtItsRealFlightTime fires at a target removed from VisibleEntities and checks the shot does not resolve before its real 5s arrival (N4's bug would resolve it instantly, at range 0) and resolves as a miss once it does. Manually verified against the exact N4 patch: it now fails (arrival at ~1s instead of ~6s, i.e. range treated as 0). Co-Authored-By: Claude Opus 5.5 --- .../FireControlCut12Tests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs index a634bec05..29090e85c 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs @@ -446,6 +446,33 @@ public sealed class FireControlCut12Tests : IDisposable Assert.Equal(expected, p, 4); } + // S4 fix batch (Hands, 2026-09-25): F8 (Soul's earlier fix batch, 2026-09-24) moved range/flightTime + // computation ahead of the visibility gate, so Fire's own flight time reflects the real distance even when + // PFire prices the shot at 0 -- but nothing behavioural pinned that. N4 (Soul's mutation set: "range after + // visibility gate", reverting F8) survived every existing test. A shot at a target the shooter cannot + // currently see must still resolve, as a miss (PFire gates it to 0), at the arrival time the real + // range/velocity imply -- not immediately (range 0) and not never (flight time 0 means instant resolve). + [Fact] + public void InvisibleTargetStillResolvesAtItsRealFlightTime() + { + var e = Build(TestSettings(), SolidShape(5, 5), precision: 1f, velocity: 20); // range 100 -> 5s flight + e.Shooter.VisibleEntities.Remove(e.Target); + + var shotId = FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); + var pending = e.Zone.PendingShots.Single(s => s.ShotId == shotId); + Assert.Equal(0f, pending.PFire); // fixture precondition: the visibility gate is actually closed + var expectedArrival = pending.FireTime + 100f / 20f; // the real range (100) over the real velocity (20) + Assert.Equal(expectedArrival, pending.ArrivalTime, 3); + + ShotOutcome outcome = null; + using var r = e.Zone.ShotResolved.Subscribe(o => outcome = o); + e.Zone.Update(4.9f); + Assert.Null(outcome); // not yet: N4's bug (range 0) would have resolved this instantly + e.Zone.Update(.2f); // past the real 5s arrival + Assert.NotNull(outcome); + Assert.False(outcome.Hit, "fixture: PFire 0 (not visible) must resolve as a miss"); + } + // Stryker survivors: ResolveAimPoint's fallback (aimed but currently occupying zero cells -> hull centre // of mass) and its multi-cell average were both untested -- every existing marker fixture uses the // catalog's 1x1 "Marker" stub, where sum/length and sum*length agree at length 1, and no fixture ever