Visibility 4b: a ping or a lock paint is an emission heard within its range over its intercept factor
Entity.Emit posts an ActiveEmission (item, range / InterceptFactor) from Sensor's sweep and LockWeapon's Track paint; Sense hears every other ship's emissions within HearRange x HearingMultiple x obscuration through RecordReveal, bearing only, feeding no track. SensorData key 13 and LockWeaponData key 34 carry InterceptFactor (absent reads 1). Unverified at this commit: the headless suite has not run yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
642a61ebbf
commit
9de7756006
6 changed files with 419 additions and 22 deletions
|
|
@ -23,6 +23,11 @@ public class LockWeaponData : InstantWeaponData
|
|||
|
||||
[Inspectable, JsonProperty("decay"), Key(25)]
|
||||
public PerformanceStat Decay = new PerformanceStat();
|
||||
|
||||
// The ratio of how far this lock sees to how far others hear it (ruling sensor-stat-set); a design without it
|
||||
// reads as 1.
|
||||
[Inspectable, JsonProperty("interceptFactor"), Key(34)]
|
||||
public PerformanceStat InterceptFactor;
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
|
|
@ -50,6 +55,7 @@ public class LockWeapon : InstantWeapon
|
|||
public float LockAngle { get; private set; }
|
||||
public float DirectionImpact { get; private set; }
|
||||
public float Decay { get; private set; }
|
||||
public float InterceptFactor => _data.InterceptFactor == null ? 1f : Evaluate(_data.InterceptFactor);
|
||||
|
||||
public override float Progress => saturate(_cooldown > 0 ? _cooldown : _lock);
|
||||
|
||||
|
|
@ -103,6 +109,7 @@ public class LockWeapon : InstantWeapon
|
|||
var lerp = 1 - unlerp(0, 90, degrees);
|
||||
_lock = saturate(_lock + pow(lerp, DirectionImpact) * dt * LockSpeed * pow(Entity.EntityInfoGathered[target], SensorImpact));
|
||||
target.ReceivePaint(Entity, Item, PaintMode.Track, Lock);
|
||||
Entity.Emit(Item, Range / InterceptFactor);
|
||||
}
|
||||
else _lock = saturate(_lock - dt * Decay);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ public class SensorData : BehaviorData
|
|||
// how much a ping reveals is a stat). 360 or more is omnidirectional; a design without it reads as 360.
|
||||
[Inspectable, JsonProperty("beamWidth"), Key(12)]
|
||||
public PerformanceStat BeamWidth;
|
||||
|
||||
// The ratio of how far this sensor sees to how far others hear it (ruling sensor-stat-set); a design without it
|
||||
// reads as 1.
|
||||
[Inspectable, JsonProperty("interceptFactor"), Key(13)]
|
||||
public PerformanceStat InterceptFactor;
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
|
|
@ -86,6 +91,8 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
|
||||
public float BeamWidth => _data.BeamWidth == null ? 360f : Evaluate(_data.BeamWidth);
|
||||
|
||||
public float InterceptFactor => _data.InterceptFactor == null ? 1f : Evaluate(_data.InterceptFactor);
|
||||
|
||||
public float2 Facing => Direction.xz;
|
||||
|
||||
// Whether the offset `diff` (length `dist`) from this sensor lies inside its beam.
|
||||
|
|
@ -169,6 +176,7 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer
|
|||
_pingLerp += dt / _data.PingDuration;
|
||||
_pingRadius = lerp(0, Evaluate(_data.PingRange), pow(_pingLerp, _data.PingRadiusExponent));
|
||||
_reached.IntersectWith(Entity.Zone.Entities);
|
||||
Entity.Emit(Item, Evaluate(_data.PingRange) / InterceptFactor);
|
||||
if (_pingLerp > 1) EndPing();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,9 @@ public abstract class Entity
|
|||
private readonly List<PingReturn> _returnInbox = new List<PingReturn>();
|
||||
private readonly List<PingReturn> _pingReturns = new List<PingReturn>();
|
||||
internal IReadOnlyList<PingReturn> PingReturns => _pingReturns;
|
||||
// What this ship's active sensors and lock paints emit this tick (Emit), heard by other ships' Sense (ruling
|
||||
// sensor-stat-set): written in Update, read by Sense in the same tick, cleared by the next PublishInbox.
|
||||
private readonly List<ActiveEmission> _emissions = new List<ActiveEmission>();
|
||||
// What this observer has seen each ship's items do (ruling visibility-per-capability-reveal), keyed (subject,
|
||||
// item). Written only by RecordReveal (an act enters) and EndReveals (an end the observer perceives, or a departed
|
||||
// subject's entries aged past BearingMemory), cleared by Deactivate (ruling reveal-ends-on-perception).
|
||||
|
|
@ -689,6 +692,10 @@ public abstract class Entity
|
|||
return _activeSensors;
|
||||
}
|
||||
|
||||
// How much of an emission's reach this observer's instruments hear it at (ruling sensor-stat-set: listeners hear at
|
||||
// a multiple of an emitter's own range): 1 with an active sensor, 0 without. Listener gear extends it.
|
||||
private int HearingMultiple() => ActiveSensors().Count > 0 ? 1 : 0;
|
||||
|
||||
// Cut 12.3 (docs/fire-control-cut.md): "armour absorbs first" -- the per-cell armour phase, split out of
|
||||
// what used to be Absorb's own first half (moved verbatim from DamageSchematic's per-cell body, Cut 3) so
|
||||
// Cut 12.3's fix batch (proportional multi-lane item absorption, below) can call the armour phase once per
|
||||
|
|
@ -858,6 +865,7 @@ public abstract class Entity
|
|||
_incomingPaints.Clear();
|
||||
_returnInbox.Clear();
|
||||
_pingReturns.Clear();
|
||||
_emissions.Clear();
|
||||
_reveals.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -866,6 +874,7 @@ public abstract class Entity
|
|||
// one tick later for every ordering.
|
||||
internal void PublishInbox()
|
||||
{
|
||||
_emissions.Clear(); // written in Update, read by Sense in the same tick
|
||||
_incomingPaints.Clear();
|
||||
foreach (var paint in _paintInbox)
|
||||
{
|
||||
|
|
@ -884,6 +893,15 @@ public abstract class Entity
|
|||
_returnInbox.Add(pingReturn);
|
||||
}
|
||||
|
||||
// The callers are Sensor.Execute (a sweeping ping) and LockWeapon.Execute (a lock paint): this ship's `item` emits this
|
||||
// tick and is heard by another ship's Sense within `hearRange` (the emitter's range over its intercept factor,
|
||||
// fixed here), bearing only.
|
||||
internal void Emit(EquippedItem item, float hearRange)
|
||||
{
|
||||
if (!_active) return;
|
||||
_emissions.Add(new ActiveEmission(item, hearRange));
|
||||
}
|
||||
|
||||
// The callers are Sensor.Execute (a Search paint, Strength 0) and LockWeapon.Execute (a Track paint, Strength
|
||||
// the emitter's own Lock at that paint). The emitter's class is the item's data (null with no item: a
|
||||
// consumable's effect, which has no item and so is never revealed).
|
||||
|
|
@ -962,6 +980,7 @@ public abstract class Entity
|
|||
{
|
||||
if (!_active) return;
|
||||
var threshold = ItemManager.GameplaySettings.TargetDetectionInfoThreshold;
|
||||
var hearing = HearingMultiple();
|
||||
var entities = Zone.Entities;
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
|
|
@ -974,6 +993,18 @@ public abstract class Entity
|
|||
if (perSignature < 0f) perSignature = PassiveInfo(other.Position.xz, 1f);
|
||||
if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel);
|
||||
}
|
||||
// Every emission this ship made this tick that reaches this observer is revealed, bearing only (ruling
|
||||
// sensor-stat-set), through RecordReveal's one gate. It feeds no track.
|
||||
if (hearing > 0 && other._emissions.Count > 0)
|
||||
{
|
||||
var planar = length(other.Position.xz - Position.xz);
|
||||
var obscuration = Zone.Obscuration(Position.xz, other.Position.xz);
|
||||
for (var j = 0; j < other._emissions.Count; j++)
|
||||
{
|
||||
var emission = other._emissions[j];
|
||||
if (planar <= emission.HearRange * hearing * obscuration) RecordReveal(other, emission.Item, SignatureChannel.Emission);
|
||||
}
|
||||
}
|
||||
// The observer resolves this ship's gear (PerceivesDestroyed), so it perceives an item of it destroyed and
|
||||
// that reveal ends here; RecordReveal refuses the same item's lingering flash.
|
||||
if (_reveals.TryGetValue(other, out var book))
|
||||
|
|
|
|||
|
|
@ -58,6 +58,20 @@ public readonly struct PingReturn
|
|||
}
|
||||
}
|
||||
|
||||
// An item's emission this tick, heard by another ship's Sense within `HearRange` x its HearingMultiple x obscuration;
|
||||
// it decides nothing by itself.
|
||||
public readonly struct ActiveEmission
|
||||
{
|
||||
public readonly EquippedItem Item;
|
||||
public readonly float HearRange;
|
||||
|
||||
public ActiveEmission(EquippedItem item, float hearRange)
|
||||
{
|
||||
Item = item;
|
||||
HearRange = hearRange;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct PaintWarning
|
||||
{
|
||||
public readonly float2 Bearing;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ using int2 = CultMath.int2;
|
|||
// its info at S x 2 / (d x TargetInfoDecay), and is revealed exactly when that exceeds TargetDetectionInfoThreshold
|
||||
// (.1). Signatures decay at once (VisibilityDecay 100), so an act is a single tick's light and builds no track.
|
||||
// The observer senses after every ship has updated, so the zone's list order never decides when an act is revealed.
|
||||
public sealed class ContactRevealTests : IDisposable
|
||||
public sealed partial class ContactRevealTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-contact-" + Guid.NewGuid().ToString("N"));
|
||||
private string Catalog => Path.Combine(_root, "Aetheria.cc");
|
||||
|
|
@ -55,6 +55,10 @@ public sealed class ContactRevealTests : IDisposable
|
|||
});
|
||||
cache.Upsert(Eye("Eye", sensitivity: 2f));
|
||||
cache.Upsert(Eye("Ear", sensitivity: 4f));
|
||||
// Narrow beams (10 degrees, facing +z) so a hearer off the beam is never painted: the ping is heard at its
|
||||
// PingRange (300) over the intercept factor.
|
||||
cache.Upsert(Eye("Shout", sensitivity: 2f, interceptFactor: 2f, beamWidth: 10f));
|
||||
cache.Upsert(Eye("Roar", sensitivity: 2f, interceptFactor: .5f, beamWidth: 10f));
|
||||
cache.Upsert(Tool("Reactor", new ReactorData { Charge = Const(1000), Efficiency = Const(1), OverloadEfficiency = Const(1), ThrottlingFactor = Const(2) }));
|
||||
cache.Upsert(Tool("Targeting", new TargetingSystemData { Accuracy = Const(1), Resolution = Const(1000f), Precision = Const(1000f), Tracking = Const(1000000f) }));
|
||||
cache.Upsert(Tool("Thruster", new ThrusterData { Thrust = Const(100), Visibility = Const(10), Heat = Const(0), EnergyUsage = Const(10) }));
|
||||
|
|
@ -82,22 +86,8 @@ public sealed class ContactRevealTests : IDisposable
|
|||
Fuse = WeaponFuse.Proximity, BlastRadius = 30f,
|
||||
Behaviors = { gun }
|
||||
});
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "Locker", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors =
|
||||
{
|
||||
new LauncherData
|
||||
{
|
||||
Damage = Const(0), Range = Const(1000), MinRange = Const(0), Velocity = Const(0),
|
||||
Spread = Const(0), DamageSpread = Const(0), Penetration = Const(0), Count = Const(1),
|
||||
BurstTime = Const(0), Cooldown = Const(1000),
|
||||
DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } },
|
||||
LockSpeed = Const(.6f), SensorImpact = Const(1), LockAngle = Const(30), DirectionImpact = Const(1), Decay = Const(.1f)
|
||||
}
|
||||
}
|
||||
});
|
||||
cache.Upsert(Locker("Locker", interceptFactor: null));
|
||||
cache.Upsert(Locker("Hushed", interceptFactor: 4f));
|
||||
cache.Upsert(new ConsumableItemData
|
||||
{
|
||||
Name = "Pinger", Duration = 30f, Shape = new Shape(),
|
||||
|
|
@ -143,18 +133,39 @@ public sealed class ContactRevealTests : IDisposable
|
|||
|
||||
private static PerformanceStat Const(float v) => new PerformanceStat { Min = v, Max = v };
|
||||
|
||||
private static SensorData SensorBehavior(float sensitivity) => new SensorData
|
||||
{
|
||||
Sensitivity = Const(sensitivity),
|
||||
SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, // the same gain at every bearing
|
||||
PingBoost = Const(.0001f), PingEnergy = Const(0), PingRange = Const(300), PingCooldown = Const(4)
|
||||
};
|
||||
|
||||
private static GearData Eye(string name, float sensitivity) => new GearData
|
||||
// A launcher with Range 1000, heard at that Range over its intercept factor while it paints.
|
||||
private static GearData Locker(string name, float? interceptFactor) => new GearData
|
||||
{
|
||||
Name = name, Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { SensorBehavior(sensitivity) }
|
||||
Behaviors =
|
||||
{
|
||||
new LauncherData
|
||||
{
|
||||
Damage = Const(0), Range = Const(1000), MinRange = Const(0), Velocity = Const(0),
|
||||
Spread = Const(0), DamageSpread = Const(0), Penetration = Const(0), Count = Const(1),
|
||||
BurstTime = Const(0), Cooldown = Const(1000),
|
||||
DamageCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } },
|
||||
LockSpeed = Const(.6f), SensorImpact = Const(1), LockAngle = Const(30), DirectionImpact = Const(1), Decay = Const(.1f),
|
||||
InterceptFactor = interceptFactor.HasValue ? Const(interceptFactor.Value) : null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static SensorData SensorBehavior(float sensitivity, float? interceptFactor = null, float? beamWidth = null) => new SensorData
|
||||
{
|
||||
Sensitivity = Const(sensitivity),
|
||||
SensitivityCurve = new BezierCurve { Keys = new[] { float4(0, 1, 0, 0), float4(1, 1, 0, 0) } }, // the same gain at every bearing
|
||||
PingBoost = Const(.0001f), PingEnergy = Const(0), PingRange = Const(300), PingCooldown = Const(4),
|
||||
InterceptFactor = interceptFactor.HasValue ? Const(interceptFactor.Value) : null,
|
||||
BeamWidth = beamWidth.HasValue ? Const(beamWidth.Value) : null
|
||||
};
|
||||
|
||||
private static GearData Eye(string name, float sensitivity, float? interceptFactor = null, float? beamWidth = null) => new GearData
|
||||
{
|
||||
Name = name, Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
||||
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
||||
Behaviors = { SensorBehavior(sensitivity, interceptFactor, beamWidth) }
|
||||
};
|
||||
|
||||
private static GearData Tool(string name, BehaviorData behavior) => new GearData
|
||||
|
|
@ -1185,10 +1196,19 @@ public sealed class ContactRevealTests : IDisposable
|
|||
for (var s = 0; s < 10; s++) ship.SetSignature(new object(), SignatureChannel.Reflection, 1000f, item);
|
||||
gunItems.Add(item);
|
||||
}
|
||||
// One ship holds a lock paint on another, so the observer's sense hears an emission on every pass (the observer's
|
||||
// own Add runs the zone's last update, which is where the emission is made; no update follows).
|
||||
var painted = Add(float3(40, 0, 300));
|
||||
var painter = Add(float3(0, 0, 300), new[] { "Locker" });
|
||||
painter.SetIff(painted, true);
|
||||
painter.EntityInfoGathered[painted] = 1f;
|
||||
painter.SetTarget(painted);
|
||||
painter.Aim = float3(1, 0, 0);
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var sense = SenseOf(o);
|
||||
for (var i = 0; i < 10; i++) sense();
|
||||
Assert.Equal(19, _zone.Entities.Where(x => x != o).Count(x => o.RevealsOf(x).Any())); // fixture: every ship is revealed
|
||||
Assert.Equal(19, _zone.Entities.Where(x => x != o && x != painter && x != painted).Count(x => o.RevealsOf(x).Any())); // fixture: every ship is revealed
|
||||
Assert.Single(o.RevealsOf(painter)); // fixture: the lock's emission is heard
|
||||
// One of them is identified by o and its gun is destroyed while it still glows, so the pass ends that reveal
|
||||
// and the flash cannot re-enter; one has left, quiet, so its entries await the prune and its departure sits
|
||||
// in the zone's list.
|
||||
|
|
|
|||
317
tests/Aetheria.Shared.Tests/EmissionTests.cs
Normal file
317
tests/Aetheria.Shared.Tests/EmissionTests.cs
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CultMath;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Visibility cut 4 (rulings sensor-stat-set, visibility-per-capability-reveal, vapour-cloud-obscuration,
|
||||
// reveal-ends-on-perception): a ping or a lock paint is an emission, heard by any ship with an active sensor within the
|
||||
// emitter's range over its intercept factor (thinned by the fog on the line), bearing only, on the tick it is made. The
|
||||
// numbers are the catalog's own: a ping reaches 300 and a launcher 1000; "Shout" has intercept factor 2 (heard at 150),
|
||||
// "Roar" .5 (heard at 600), and both beam 10 degrees facing +z, so a hearer off the beam is never painted and its reveal
|
||||
// is the hearing alone. Every fixture drives the production path (Sensor.Execute and LockWeapon.Execute post through
|
||||
// Entity.Emit; Sense hears), ticking inside Zone.Update.
|
||||
public sealed partial class ContactRevealTests
|
||||
{
|
||||
private static Sensor SensorOf(Entity e) => e.Equipment.SelectMany(i => i.Behaviors).OfType<Sensor>().Single();
|
||||
|
||||
private static EquippedItem SensorItemOf(Entity e) => e.Equipment.Single(i => i.Behaviors.Any(b => b is Sensor));
|
||||
|
||||
// The pinger's first tick opens its ping cooldown; Ping() is refused before it. Returns the sweeping tick's clock.
|
||||
private float PingAt(Entity pinger)
|
||||
{
|
||||
Tick(.1f);
|
||||
SensorOf(pinger).Ping();
|
||||
Tick(.1f);
|
||||
return _zone.Time;
|
||||
}
|
||||
|
||||
// A launcher at the origin locking a ship 200 ahead of it. Two ticks: the first paint is published on the second.
|
||||
private (Ship locker, Ship target) LockOn(string launcher)
|
||||
{
|
||||
var locker = Add(float3(0, 0, 0), new[] { launcher });
|
||||
var target = Add(float3(0, 0, 200));
|
||||
locker.SetIff(target, true);
|
||||
locker.EntityInfoGathered[target] = 1f;
|
||||
locker.SetTarget(target);
|
||||
locker.Aim = float3(0, 0, 1);
|
||||
return (locker, target);
|
||||
}
|
||||
|
||||
// ---- APingIsHeardWithinRangeOverAlpha ----
|
||||
|
||||
[Fact]
|
||||
public void APingIsHeardWithinRangeOverAlpha()
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var within = Add(float3(149, 0, 0), new[] { "Eye" }); // PingRange 300 / alpha 2 = 150
|
||||
var beyond = Add(float3(0, 0, -151), new[] { "Eye" });
|
||||
|
||||
var at = PingAt(p);
|
||||
|
||||
var reveal = Assert.Single(within.RevealsOf(p));
|
||||
Assert.Same(SensorItemOf(p), reveal.Item);
|
||||
Assert.Equal(SignatureChannel.Emission, reveal.Channel);
|
||||
AssertBearing(-1f, 0f, reveal.Bearing); // normalize((P - hearer).xz)
|
||||
Assert.Equal(at, reveal.Time);
|
||||
Assert.Empty(within.IncomingPaints); // fixture: off the beam, never painted
|
||||
Assert.Empty(beyond.RevealsOf(p));
|
||||
}
|
||||
|
||||
// ---- ALoudSensorIsHeardBeyondItsReach ----
|
||||
|
||||
[Fact]
|
||||
public void ALoudSensorIsHeardBeyondItsReach()
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Roar" }); // PingRange 300 / alpha .5 = 600
|
||||
var within = Add(float3(599, 0, 0), new[] { "Eye" });
|
||||
var beyond = Add(float3(0, 0, -601), new[] { "Eye" });
|
||||
|
||||
PingAt(p);
|
||||
|
||||
Assert.Single(within.RevealsOf(p));
|
||||
Assert.Empty(beyond.RevealsOf(p));
|
||||
Assert.Empty(within.IncomingPaints); // fixture: beyond the ping's radius, so never painted
|
||||
}
|
||||
|
||||
// ---- ALockPaintIsHeardByThirdParties ----
|
||||
|
||||
// The launcher's Range is 1000; "Hushed" has alpha 4, heard at 250.
|
||||
[Theory]
|
||||
[InlineData("Locker", 999f, true)]
|
||||
[InlineData("Locker", 1001f, false)]
|
||||
[InlineData("Hushed", 249f, true)]
|
||||
[InlineData("Hushed", 251f, false)]
|
||||
public void ALockPaintIsHeardByThirdParties(string launcher, float distance, bool heard)
|
||||
{
|
||||
var (locker, target) = LockOn(launcher);
|
||||
var third = Add(float3(distance, 0, 0), new[] { "Eye" });
|
||||
|
||||
Tick(.1f);
|
||||
Tick(.1f);
|
||||
|
||||
Assert.Single(target.IncomingPaints); // fixture: the lock painted B
|
||||
var warned = Assert.Single(target.RevealsOf(locker)); // B records the launcher through its paint either way
|
||||
Assert.Equal(SignatureChannel.Emission, warned.Channel);
|
||||
if (!heard)
|
||||
{
|
||||
Assert.Empty(third.RevealsOf(locker));
|
||||
return;
|
||||
}
|
||||
var reveal = Assert.Single(third.RevealsOf(locker));
|
||||
Assert.Same(locker.Equipment.Single(i => i.Behaviors.Any(b => b is LockWeapon)), reveal.Item);
|
||||
Assert.Equal(SignatureChannel.Emission, reveal.Channel);
|
||||
AssertBearing(-1f, 0f, reveal.Bearing);
|
||||
Assert.Equal(_zone.Time, reveal.Time);
|
||||
Assert.Empty(third.IncomingPaints);
|
||||
}
|
||||
|
||||
// ---- AFogBankShortensHearing ----
|
||||
|
||||
// Alpha 2 reaches 150; a bank of opacity .5 across the line passes half of it: 75.
|
||||
[Theory]
|
||||
[InlineData(74f, true)]
|
||||
[InlineData(76f, false)]
|
||||
public void AFogBankShortensHearing(float distance, bool heard)
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var hearer = Add(float3(distance, 0, 0), new[] { "Eye" });
|
||||
Hand(_zone, float2(30, 0), 5f, .5f);
|
||||
Assert.Equal(.5f, _zone.Obscuration(p.Position.xz, hearer.Position.xz), 4); // fixture: the bank is on the line
|
||||
|
||||
PingAt(p);
|
||||
|
||||
Assert.Equal(heard ? 1 : 0, hearer.RevealsOf(p).Count());
|
||||
}
|
||||
|
||||
// ---- ADeafShipHearsNothing ----
|
||||
|
||||
[Fact]
|
||||
public void ADeafShipHearsNothing()
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var deaf = Add(float3(100, 0, 0));
|
||||
var hearing = Add(float3(-100, 0, 0), new[] { "Eye" });
|
||||
|
||||
PingAt(p);
|
||||
|
||||
Assert.Single(hearing.RevealsOf(p));
|
||||
Assert.Empty(deaf.RevealsOf(p));
|
||||
}
|
||||
|
||||
// ---- AnEmissionIsHeardOnItsOwnTickWhateverTheListOrder ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f, 0f)]
|
||||
[InlineData(30f, -20f)]
|
||||
public void AnEmissionIsHeardOnItsOwnTickWhateverTheListOrder(float vx, float vz)
|
||||
{
|
||||
var before = Add(float3(-30, 0, 40), new[] { "Eye" });
|
||||
var p = Add(float3(30, 0, 40), new[] { "Shout" });
|
||||
var after = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var start = p.Position;
|
||||
p.Velocity = float2(vx, vz);
|
||||
|
||||
var at = PingAt(p);
|
||||
|
||||
if (vx != 0f || vz != 0f) Assert.True(length(p.Position - start) > 1f, "fixture: the pinger moved");
|
||||
foreach (var observer in new Entity[] { before, after })
|
||||
{
|
||||
var reveal = Assert.Single(observer.RevealsOf(p));
|
||||
Assert.Equal(at, reveal.Time);
|
||||
var toward = normalize((p.Position - observer.Position).xz);
|
||||
AssertBearing(toward.x, toward.y, reveal.Bearing);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- AnEmissionEndsWithItsAct ----
|
||||
|
||||
[Fact]
|
||||
public void AnEmissionEndsWithItsAct()
|
||||
{
|
||||
var p = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var heard = Add(float3(100, 0, 0), new[] { "Eye" });
|
||||
var late = Add(float3(0, 0, -1000), new[] { "Eye" });
|
||||
var ended = false;
|
||||
SensorOf(p).OnPingEnd += () => ended = true;
|
||||
PingAt(p);
|
||||
for (var tick = 0; tick < 40 && !ended; tick++) Tick(.1f);
|
||||
Assert.True(ended, "fixture: the ping ended");
|
||||
var last = Freshest(heard, p);
|
||||
Assert.Equal(_zone.Time, last); // the ending tick still emitted
|
||||
|
||||
late.Position = float3(-100, 0, 0); // moved into range after the act is over
|
||||
for (var tick = 0; tick < 3; tick++) Tick(.1f);
|
||||
|
||||
Assert.Equal(last, Freshest(heard, p));
|
||||
Assert.Empty(late.RevealsOf(p));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ALockEmissionEndsWhenTheLockLeavesItsAngle()
|
||||
{
|
||||
var (locker, target) = LockOn("Locker");
|
||||
var third = Add(float3(500, 0, 0), new[] { "Eye" });
|
||||
Tick(.1f);
|
||||
Tick(.1f);
|
||||
var last = Freshest(third, locker);
|
||||
Assert.Equal(_zone.Time, last); // fixture: heard on the lock's tick
|
||||
|
||||
locker.Aim = float3(0, 0, -1); // 180 degrees off the target, past LockAngle
|
||||
for (var tick = 0; tick < 3; tick++) Tick(.1f);
|
||||
|
||||
Assert.Equal(last, Freshest(third, locker));
|
||||
}
|
||||
|
||||
// ---- HearingFeedsNoTrack ----
|
||||
|
||||
[Fact]
|
||||
public void HearingFeedsNoTrack()
|
||||
{
|
||||
var (heardTrack, heardReveals) = RunHeard(ping: true);
|
||||
var (controlTrack, controlReveals) = RunHeard(ping: false);
|
||||
|
||||
Assert.Equal(1, heardReveals); // fixture: the emission was heard
|
||||
Assert.Equal(0, controlReveals);
|
||||
Assert.Equal(controlTrack, heardTrack, 6);
|
||||
}
|
||||
|
||||
private static (float track, int reveals) RunHeard(bool ping)
|
||||
{
|
||||
using var t = new ContactRevealTests();
|
||||
var p = t.Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var h = t.Add(float3(100, 0, 0), new[] { "Eye" });
|
||||
if (ping) t.PingAt(p);
|
||||
else
|
||||
{
|
||||
t.Tick(.1f);
|
||||
t.Tick(.1f);
|
||||
}
|
||||
return (Track(h, p), h.RevealsOf(p).Count());
|
||||
}
|
||||
|
||||
// ---- AnAllyCreditingAReturnEmitsNothing ----
|
||||
|
||||
// PaintModeTests.AnAllyHearsTheReturnSilently's geometry: A pings T, ally C hears the return. X stands near C and far
|
||||
// from A, so X hears C if and only if C emitted.
|
||||
[Fact]
|
||||
public void AnAllyCreditingAReturnEmitsNothing()
|
||||
{
|
||||
var a = Add(float3(0, 0, 0), new[] { "Shout" });
|
||||
var t = Add(float3(0, 0, 100));
|
||||
var c = Add(float3(30, 0, 140), new[] { "Eye" });
|
||||
c.Faction = a.Faction;
|
||||
var x = Add(float3(100, 0, 200), new[] { "Eye" });
|
||||
var returns = typeof(Entity).GetProperty("PingReturns", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
Tick(.1f);
|
||||
SensorOf(a).Ping();
|
||||
var credited = false;
|
||||
for (var tick = 0; tick < 24; tick++)
|
||||
{
|
||||
Tick(.1f);
|
||||
credited |= ((System.Collections.ICollection) returns.GetValue(c)).Count > 0;
|
||||
}
|
||||
|
||||
Assert.True(credited, "fixture: C was handed A's return");
|
||||
Assert.NotEmpty(t.RevealsOf(a)); // fixture: T was painted
|
||||
Assert.Empty(x.RevealsOf(c));
|
||||
Assert.Empty(x.RevealsOf(a)); // fixture: X is beyond A's 150
|
||||
}
|
||||
|
||||
// ---- TheInterceptFactorKeys ----
|
||||
|
||||
private static MessagePackSerializerOptions EmissionWire => CultDocumentMessagePackSerialization.OptionsFor(typeof(ZonePack).Assembly);
|
||||
|
||||
// InterceptFactor is slot 13 of the sensor record and slot 34 of the lock weapon (a launcher is its stored arm; its
|
||||
// own slot 26 is the guidance curve), and the JSON property interceptFactor. Read as bytes, so a renumbered key or a
|
||||
// renamed property cannot pass because writer and reader move together.
|
||||
[Fact]
|
||||
public void TheInterceptFactorKeys()
|
||||
{
|
||||
var sensor = MessagePackSerializer.Serialize<BehaviorData>(new SensorData { InterceptFactor = Const(2) }, EmissionWire);
|
||||
var reader = new MessagePackReader(sensor);
|
||||
Assert.Equal(2, reader.ReadArrayHeader());
|
||||
Assert.Equal(5, reader.ReadInt32());
|
||||
Assert.Equal(14, reader.ReadArrayHeader());
|
||||
for (var slot = 0; slot < 13; slot++) reader.Skip();
|
||||
Assert.Equal(2f, MessagePackSerializer.Deserialize<PerformanceStat>(ref reader, EmissionWire).Min);
|
||||
Assert.Equal(2f, JObject.Parse(JsonConvert.SerializeObject(new SensorData { InterceptFactor = Const(2) }))["interceptFactor"].ToObject<PerformanceStat>().Min);
|
||||
|
||||
var launcher = new LauncherData { InterceptFactor = Const(3), GuidanceCurve = new[] { float4(0, 1, 0, 0) } };
|
||||
var bytes = MessagePackSerializer.Serialize<BehaviorData>(launcher, EmissionWire);
|
||||
reader = new MessagePackReader(bytes);
|
||||
Assert.Equal(2, reader.ReadArrayHeader());
|
||||
reader.Skip(); // the arm's union index
|
||||
Assert.Equal(35, reader.ReadArrayHeader());
|
||||
for (var slot = 0; slot < 34; slot++) reader.Skip();
|
||||
Assert.Equal(3f, MessagePackSerializer.Deserialize<PerformanceStat>(ref reader, EmissionWire).Min);
|
||||
Assert.Equal(3f, JObject.Parse(JsonConvert.SerializeObject(launcher))["interceptFactor"].ToObject<PerformanceStat>().Min);
|
||||
|
||||
var back = Assert.IsType<LauncherData>(MessagePackSerializer.Deserialize<BehaviorData>(bytes, EmissionWire));
|
||||
Assert.Equal(3f, back.InterceptFactor.Min);
|
||||
Assert.Single(back.GuidanceCurve);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnAbsentInterceptFactorReadsAsOne()
|
||||
{
|
||||
var plain = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
var shout = Add(float3(10, 0, 0), new[] { "Shout" });
|
||||
var locker = Add(float3(20, 0, 0), new[] { "Locker" });
|
||||
var hushed = Add(float3(30, 0, 0), new[] { "Hushed" });
|
||||
|
||||
Assert.Equal(1f, SensorOf(plain).InterceptFactor);
|
||||
Assert.Equal(2f, SensorOf(shout).InterceptFactor);
|
||||
Assert.Equal(1f, locker.Equipment.SelectMany(i => i.Behaviors).OfType<LockWeapon>().Single().InterceptFactor);
|
||||
Assert.Equal(4f, hushed.Equipment.SelectMany(i => i.Behaviors).OfType<LockWeapon>().Single().InterceptFactor);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue