diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 214f0bc2d..e9cc2829f 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -290,9 +290,14 @@ public static class FireControl // R3/R4: the one roll. p_base was frozen at fire; the only thing measured live is how far the target has // actually strayed, by now, from where a straight-line projection of its fire-time velocity said it would - // be -- forgiven by the targeting system's Tracking, likewise frozen at fire. A shot gated to zero at fire - // (out of arc, out of range, unlocked, undetected) draws nothing here (short-circuit below): the RNG - // sequence is exactly as if the shot had never queued. + // be -- forgiven by the targeting system's Tracking, likewise frozen at fire. + // + // Cut 7: the short-circuit below (`p > 0f &&`) is a guard, NOT an invariant. It used to be one -- a shot + // gated to zero at fire had to draw nothing so the shared stream advanced exactly as if it had never + // queued -- but Cut 6b gave every shot its own generator, seeded from (zone, shot id) and discarded here, + // so whether a gated shot draws is unobservable from anywhere. The claim this comment used to make is + // gone with the stream it was about, and the test that asserted it became vacuous the same moment; what + // replaced it pins the live rule instead (combat touches no shared stream, on any path). private static ShotOutcome Commit(Zone zone, PendingShot shot, float now) { // Cut 6b, 6.1 (Soul finding 6): a shot's dice belong to the shot, not to whatever else happened to diff --git a/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs b/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs index 732b35f9f..3f65f24d8 100644 --- a/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs +++ b/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs @@ -256,15 +256,20 @@ public sealed class FireAuthorityTests : IDisposable Assert.Equal(unaided.Items.GameplaySettings.UnaidedAccuracy, FireControl.HitProbability(unaided.Weapon, unaided.Shooter, unaided.Target), 3); } - // The gate is a gate, not a penalty: an out-of-arc commit consumes exactly zero draws from the shared RNG - // stream. Mutation: roll first and multiply the result by zero -- that still advances the stream by one - // draw, which this test would catch by seeing a different next value than an untouched stream produces. - [Fact] - public void OutOfArcConsumesNoDraw() + // Cut 7 (docs/fire-control-cut.md): this was OutOfArcConsumesNoDraw, which asserted that a shot gated to + // zero left ItemManager.Random untouched. Cut 6b (6.1) moved the roll onto a per-shot local generator, so + // Commit stopped touching that stream on EVERY path and the old test passed for a reason that had nothing + // to do with arcs -- it would have passed just as well with the target dead ahead, and no mutation of the + // short-circuit it named could ever turn it red. Rewritten to pin the rule that is actually live: no + // combat path draws from the shared stream, in arc or out. Mutation: restore the shared-stream read in + // Commit; the in-arc case then goes red. + [Theory] + [InlineData(100f, 0f)] // directly abeam: gated to zero, outside the default arc + [InlineData(0f, 100f)] // dead ahead: a real firing solution that actually rolls + public void CombatNeverDrawsFromTheSharedStream(float x, float z) { - var e = Build(TestSettings()); - // Directly abeam -- outside even the wide 170-degree default arc dead ahead. - e.Target.Position = float3(100, 0, 0); + var e = Build(TestSettings(), accuracy: 1, resolution: 1, spread: 0); + e.Target.Position = float3(x, 0, z); const uint seed = 4242u; var untouched = new Random(seed);