Visibility 4b: pin the departure's item-less hull light, the Emission guard's item-less and zero-strength forms, and that the ping test's ping started

Follow-up ping-signature-test-gaps (departure-hull-light-unpinned, guard-null-item-unpinned,
ping-test-never-asserts-it-fired). Tests only; unverified until the suite runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 01:57:28 +02:00
commit 8be15c6ac1
2 changed files with 34 additions and 3 deletions

View file

@ -408,8 +408,11 @@ public sealed partial 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<Sensor>().Single();
var fired = false;
sensor.OnPingStart += () => fired = true;
t.Tick(.1f); // a sensor's first tick opens its ping cooldown; Ping() is refused before it
if (ping) sensor.Ping();
Assert.Equal(ping, fired); // fixture: the ping started exactly when asked, so the run is not a control in disguise
var light = new List<float> { p.Visibility };
for (var tick = 0; tick < 10; tick++)
{
@ -695,6 +698,30 @@ public sealed partial class ContactRevealTests : IDisposable
Assert.Empty(o.RevealsOf(e));
}
// The hull's own radiation names no item, and the departure still counts it: the light at removal sits on an item-less
// entry alone, a hair above the observer's gate (ends the reveals) or a hair below (ends nothing).
[Theory]
[InlineData(1.01f, true)]
[InlineData(.99f, false)]
public void ADepartureCountsTheHullsOwnLight(float against, bool ended)
{
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(), SignatureChannel.Thermal, gate * against, null);
Assert.Equal(gate * against, e.Visibility, 4);
Assert.All(e.VisibilitySources.Values.Where(v => v.Strength > 0f), v => Assert.Null(v.Item)); // fixture: the deciding light names no item
_zone.Entities.Remove(e);
Tick(.1f);
Assert.Equal(ended ? 0 : 1, o.RevealsOf(e).Count());
}
// 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.

View file

@ -326,15 +326,19 @@ public sealed class SignatureTests : IDisposable
// Emission is active emission, never light: SetSignature refuses it before writing, and a Visibility gear
// authored on it throws on the Zone.Update that first executes it.
[Fact]
public void SetSignatureRefusesEmission()
[Theory]
[InlineData(true, 5f)]
[InlineData(false, 5f)]
[InlineData(true, 0f)]
[InlineData(false, 0f)]
public void SetSignatureRefusesEmission(bool withItem, float strength)
{
var r = Build(Armament.Gun, 0f, null);
Fire(r);
var before = r.Ship.VisibilitySources.ToDictionary(kv => kv.Key, kv => kv.Value);
Assert.NotEmpty(before);
Assert.Throws<ArgumentException>(() => r.Ship.SetSignature(new object(), SignatureChannel.Emission, 5f, r.WeaponItem));
Assert.Throws<ArgumentException>(() => r.Ship.SetSignature(new object(), SignatureChannel.Emission, strength, withItem ? r.WeaponItem : null));
Assert.Equal(before.Count, r.Ship.VisibilitySources.Count);
foreach (var kv in before) Assert.Equal(kv.Value, r.Ship.VisibilitySources[kv.Key]);