From 642a61ebbfbf0eaa5de0795cff1dd68540bbc4d8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 13 Oct 2026 00:27:22 +0200 Subject: [PATCH] Visibility 4a: the ping test ticks once so Ping() fires; a departure counts light on every channel The ping test never pinged (a sensor's cooldown opens on its first tick), so a restored ping bump survived. ADepartureCountsLightOnEveryChannel pins the departure to the subject's whole Visibility. Builds. Co-Authored-By: Claude Opus 5.5 --- .../ContactRevealTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs index b24efdec2..10ed63fc6 100644 --- a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs +++ b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs @@ -397,6 +397,7 @@ public sealed class ContactRevealTests : IDisposable var p = t.Add(float3(0, 0, 1000), new[] { "Eye" }); var o = t.Add(float3(0, 0, 0), new[] { "Eye" }); var sensor = p.Equipment.SelectMany(i => i.Behaviors).OfType().Single(); + t.Tick(.1f); // a sensor's first tick opens its ping cooldown; Ping() is refused before it if (ping) sensor.Ping(); var light = new List { p.Visibility }; for (var tick = 0; tick < 10; tick++) @@ -659,6 +660,30 @@ public sealed class ContactRevealTests : IDisposable Assert.Equal(ended ? 0 : 1, o.RevealsOf(e).Count()); } + // The departure records the subject's whole Visibility: no channel is excepted. The light at removal sits on each + // channel in turn, a hair above the observer's gate, and ends the reveals whichever channel carries it. + [Theory] + [InlineData(SignatureChannel.Thermal)] + [InlineData(SignatureChannel.DrivePlume)] + [InlineData(SignatureChannel.Reflection)] + [InlineData(SignatureChannel.LaunchFlash)] + public void ADepartureCountsLightOnEveryChannel(SignatureChannel channel) + { + var e = Add(float3(0, 0, 100), new[] { "Gun" }); + var o = Add(float3(0, 0, 0), new[] { "Eye" }); + Fire(e, Guns(e)[0]); + Tick(.01f); + Assert.Single(o.RevealsOf(e)); + var gate = _items.GameplaySettings.TargetDetectionInfoThreshold / o.PassiveInfo(e.Position.xz, 1f); + foreach (var source in e.VisibilitySources.Keys.ToArray()) e.SetSignature(source, SignatureChannel.Reflection, 0f, Guns(e)[0]); + e.SetSignature(new object(), channel, gate * 1.01f, Guns(e)[0]); + + _zone.Entities.Remove(e); + Tick(.1f); + + Assert.Empty(o.RevealsOf(e)); + } + // The gate is strict, as the light-act gate is: a light at removal that lands exactly on the threshold ends nothing. // At 32 the observer's info per unit light is 4 / 32, a power of two, so threshold x 8 is the light whose product // with it is exactly the threshold.