Add the inspection scanner: a held scan on a tracked target reveals all its gear
Scanner (ScannerData, Union 51) builds Progress while the ship's Target is tracked within Range, paints the target (PaintMode.Scan), emits at Range / InterceptFactor and decays in IAlwaysUpdatedBehavior.Update when not held. At 1 Entity.RecordInspection records every fitted non-hull non-destroyed item through RecordReveal with channel null and the subject's claim. Reveal.Channel and RecordReveal take SignatureChannel?. Scanner.cs.meta is hand-written with a fresh guid; Unity has not compiled this yet. Builds on the headless test project only if the next verify says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b836af4850
commit
86843648a7
7 changed files with 415 additions and 9 deletions
|
|
@ -191,6 +191,7 @@ public interface IPowerConsumer
|
|||
Union(47, typeof(MunitionFuelData)),
|
||||
Union(48, typeof(PickupData)),
|
||||
Union(49, typeof(LaunchSuppressorData)),
|
||||
Union(51, typeof(ScannerData)),
|
||||
JsonObject(MemberSerialization.OptIn)]
|
||||
public abstract class BehaviorData
|
||||
{
|
||||
|
|
|
|||
91
Assets/Scripts/ServerShared/Behaviors/Scanner.cs
Normal file
91
Assets/Scripts/ServerShared/Behaviors/Scanner.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using static CultMath.math;
|
||||
|
||||
// Inspection (campaign aetheria-release, rulings quiet-gear-identifiable-hull-layout-known, scan-held-beam,
|
||||
// scan-is-an-act): a gear that, held on the ship's tracked target within Range for ScanTime, reveals everything fitted to
|
||||
// that target, quiet gear that never acts included. The scan is an emission like a lock: the target is painted and
|
||||
// third parties hear it at Range over the intercept factor.
|
||||
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
|
||||
public class ScannerData : BehaviorData
|
||||
{
|
||||
// The planar distance to the target within which a scan builds.
|
||||
[Inspectable, JsonProperty("range"), Key(1), RuntimeInspectable]
|
||||
public PerformanceStat Range = new PerformanceStat();
|
||||
|
||||
// Seconds a held scan takes to finish; a broken scan decays over the same span.
|
||||
[Inspectable, JsonProperty("scanTime"), Key(2), RuntimeInspectable]
|
||||
public PerformanceStat ScanTime = new PerformanceStat();
|
||||
|
||||
// The ratio of how far this scanner sees to how far others hear it (ruling sensor-stat-set); a design without it
|
||||
// reads as 1.
|
||||
[Inspectable, JsonProperty("interceptFactor"), Key(3)]
|
||||
public PerformanceStat InterceptFactor;
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
return new Scanner(this, item);
|
||||
}
|
||||
public override Behavior CreateInstance(ConsumableItemEffect item)
|
||||
{
|
||||
return new Scanner(this, item);
|
||||
}
|
||||
}
|
||||
|
||||
public class Scanner : Behavior, IAlwaysUpdatedBehavior, IProgressBehavior
|
||||
{
|
||||
private readonly ScannerData _data;
|
||||
private Entity _target;
|
||||
private bool _held;
|
||||
|
||||
public float Progress { get; private set; }
|
||||
|
||||
public Scanner(ScannerData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
public Scanner(ScannerData data, ConsumableItemEffect item) : base(data, item)
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
|
||||
// Runs only while the item is Active. The scan builds while the ship's own Target is a tracked ship within Range, and
|
||||
// is painted and emitted on every tick it builds; at 1 the target's fitted items enter the book and the scan starts over.
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
var target = Entity.Target.Value.Entity;
|
||||
if (_target != target)
|
||||
{
|
||||
Progress = 0;
|
||||
_target = target;
|
||||
}
|
||||
|
||||
var range = Evaluate(_data.Range);
|
||||
if (target != null && Entity.VisibleEntities.Contains(target) && Entity.TargetRange <= range)
|
||||
{
|
||||
Progress = saturate(Progress + dt / Evaluate(_data.ScanTime));
|
||||
target.ReceivePaint(Entity, Item, PaintMode.Scan, Progress);
|
||||
Entity.Emit(Item, range / (_data.InterceptFactor == null ? 1f : Evaluate(_data.InterceptFactor)));
|
||||
_held = true;
|
||||
if (Progress >= 1f)
|
||||
{
|
||||
Entity.RecordInspection(target);
|
||||
Progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every tick, after Execute: a scan not held this tick (out of range, untracked, or the item inactive so Execute did
|
||||
// not run) decays over ScanTime (ruling scan-held-beam). The one decay site.
|
||||
public void Update(float delta)
|
||||
{
|
||||
if (!_held) Progress = saturate(Progress - delta / Evaluate(_data.ScanTime));
|
||||
_held = false;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Behaviors/Scanner.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Behaviors/Scanner.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bf9e034c7a31410ea1c516f46b43825c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -247,7 +247,7 @@ public abstract class Entity
|
|||
// 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
|
||||
// item). Written only by RecordReveal (an act, or a scan through RecordInspection, 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).
|
||||
private readonly Dictionary<Entity, Dictionary<EquippedItem, Reveal>> _reveals = new Dictionary<Entity, Dictionary<EquippedItem, Reveal>>();
|
||||
private readonly List<EquippedItem> _endedItems = new List<EquippedItem>();
|
||||
|
|
@ -934,14 +934,14 @@ public abstract class Entity
|
|||
_paintInbox.Add(new PendingPaint(new PaintWarning(normalize((emitter.Position - Position).xz), emitter.ClaimOf(item), mode, strength), emitter, item));
|
||||
}
|
||||
|
||||
// The reveal book's one way in and its one membership gate (callers: Sense and PublishInbox, both in this file):
|
||||
// this observer saw `item` of `subject` act on `channel`, now, from this bearing. An item-less act (hull
|
||||
// The reveal book's one way in and its one membership gate (callers: Sense, PublishInbox and RecordInspection, all in
|
||||
// this file): this observer saw `item` of `subject` act on `channel`, now, from this bearing. An item-less act (hull
|
||||
// radiation, a consumable) reveals nothing, and neither does a subject that is not in the zone: a departed
|
||||
// subject's entries are never created or refreshed, from any caller, and neither does an item this observer
|
||||
// perceives destroyed (PerceivesDestroyed). A later act of the same item replaces the
|
||||
// earlier one. `claimed` is the class the carrier (signature, emission or paint) stamped: what this observer's
|
||||
// classifying readers will believe of the act; `item` stays the aim and book key.
|
||||
internal void RecordReveal(Entity subject, EquippedItem item, SignatureChannel channel, EquippableItemData claimed)
|
||||
internal void RecordReveal(Entity subject, EquippedItem item, SignatureChannel? channel, EquippableItemData claimed)
|
||||
{
|
||||
if (!_active || item == null || !Zone.Entities.Contains(subject) || PerceivesDestroyed(subject, item)) return;
|
||||
if (!_reveals.TryGetValue(subject, out var book))
|
||||
|
|
@ -949,6 +949,21 @@ public abstract class Entity
|
|||
book[item] = new Reveal(item, channel, normalize((subject.Position - Position).xz), Zone.Time, claimed);
|
||||
}
|
||||
|
||||
// A finished scan (Scanner.Execute; rulings scan-is-an-act, quiet-gear-identifiable-hull-layout-known: quiet gear
|
||||
// that never acts stays identifiable through special gear): this observer learns every fitted item of `subject`,
|
||||
// channel null. It records what the item claims, not what it is (ClaimOf, as every act's carrier stamps it: the
|
||||
// aetheria-ew one judge, classify-from-belief), skips the hull (its class is the track's to give) and a destroyed
|
||||
// item, and goes through RecordReveal's gate like any act. It calls no EndReveals: a scan ends nothing
|
||||
// (reveal-ends-on-perception).
|
||||
internal void RecordInspection(Entity subject)
|
||||
{
|
||||
foreach (var item in subject.Equipment)
|
||||
{
|
||||
if (item == subject.EquippedHull || item.EquippableItem.IsDestroyed) continue;
|
||||
RecordReveal(subject, item, null, subject.ClaimOf(item));
|
||||
}
|
||||
}
|
||||
|
||||
// This observer resolves `subject`'s gear (ContactOf's Identified gate) and `item` of it is destroyed: the one rule
|
||||
// for a destroyed item an observer perceives, read by the book's entry (RecordReveal) and its end (Sense).
|
||||
private bool PerceivesDestroyed(Entity subject, EquippedItem item) =>
|
||||
|
|
|
|||
|
|
@ -38,11 +38,13 @@ public readonly struct Signature
|
|||
|
||||
// What a painted ship learns of a beam on it (ruling sensor-stat-set item 4): a Search paint is a ping sweeping
|
||||
// it (Strength 0); a Track paint is a lock (Strength is the emitter's own lock progress, 0..1, ruling
|
||||
// lock-warning-strength). Never summed.
|
||||
// lock-warning-strength). A Scan paint is a held scanner: Strength is its scan progress 0..1 (ruling scan-is-an-act).
|
||||
// Never summed.
|
||||
public enum PaintMode
|
||||
{
|
||||
Search,
|
||||
Track
|
||||
Track,
|
||||
Scan
|
||||
}
|
||||
|
||||
// An ally's ping crossed `Subject` at `IlluminatedRange` through `IlluminatedObscuration`; `MaxRange` is that
|
||||
|
|
@ -100,16 +102,17 @@ public readonly struct PaintWarning
|
|||
// bearing at that act and the zone clock then. Held per (subject, item) in the observer's Entity; the latest act
|
||||
// replaces the earlier one. Position is never carried: only the track gives it. `Item` stays the true item: it is the
|
||||
// book's key and what aim reads. `Claimed` is the class the act claimed, and is what every classifying reader reads
|
||||
// (aetheria-ew, classify-from-belief).
|
||||
// (aetheria-ew, classify-from-belief). A null `Channel` is a reveal by the observer's own scan (Entity.RecordInspection),
|
||||
// whose `Claimed` is the subject's claim for the item.
|
||||
public readonly struct Reveal
|
||||
{
|
||||
public readonly EquippedItem Item;
|
||||
public readonly EquippableItemData Claimed;
|
||||
public readonly SignatureChannel Channel;
|
||||
public readonly SignatureChannel? Channel;
|
||||
public readonly float2 Bearing;
|
||||
public readonly float Time;
|
||||
|
||||
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time, EquippableItemData claimed)
|
||||
public Reveal(EquippedItem item, SignatureChannel? channel, float2 bearing, float time, EquippableItemData claimed)
|
||||
{
|
||||
Item = item;
|
||||
Claimed = claimed;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
cache.Upsert(SpannedWeapon("SpanBeam", Spanned(new ConstantWeaponData())));
|
||||
cache.Upsert(Locker("Locker", interceptFactor: null));
|
||||
cache.Upsert(Locker("Hushed", interceptFactor: 4f));
|
||||
// Scanners (cut inspection-scanner): range 200 and a two second scan; "QuietScanner" is heard at a quarter of it.
|
||||
cache.Upsert(Tool("Scanner", ScannerBehavior(range: 200f, scanTime: 2f)));
|
||||
cache.Upsert(Tool("QuietScanner", ScannerBehavior(range: 200f, scanTime: 2f, interceptFactor: 4f)));
|
||||
cache.Upsert(new ConsumableItemData
|
||||
{
|
||||
Name = "Pinger", Duration = 30f, Shape = new Shape(),
|
||||
|
|
|
|||
282
tests/Aetheria.Shared.Tests/ScannerTests.cs
Normal file
282
tests/Aetheria.Shared.Tests/ScannerTests.cs
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
// Inspection (rulings quiet-gear-identifiable-hull-layout-known, scan-held-beam, scan-is-an-act, reveal-ends-on-perception,
|
||||
// sensor-stat-set): a scanner held on its ship's tracked target within Range for ScanTime reveals everything fitted to it,
|
||||
// as claimed, and is itself an emission the target is warned of and a third party can hear. The fixtures are the
|
||||
// catalog's: "Scanner" has Range 200 and ScanTime 2, "QuietScanner" the same with intercept factor 4 (heard at 50). A tick
|
||||
// is 1/8 s, so a scan builds 1/16 a tick and finishes on the sixteenth with no float drift. The scanner's ship keeps
|
||||
// its track on the target at .5 each tick (above the detection gate, below the one at which gear is resolved, so
|
||||
// Sense neither loses the track nor perceives a destruction). Every fixture drives the production path, ticking inside
|
||||
// Zone.Update.
|
||||
public sealed partial class ContactRevealTests
|
||||
{
|
||||
private const float ScanStep = .125f;
|
||||
|
||||
private static ScannerData ScannerBehavior(float range, float scanTime, float? interceptFactor = null) => new ScannerData
|
||||
{
|
||||
Range = Const(range), ScanTime = Const(scanTime),
|
||||
InterceptFactor = interceptFactor.HasValue ? Const(interceptFactor.Value) : null
|
||||
};
|
||||
|
||||
private static Scanner ScannerOf(Entity e) => e.Equipment.SelectMany(i => i.Behaviors).OfType<Scanner>().Single();
|
||||
|
||||
private static EquippedItem ScannerItemOf(Entity e) => e.Equipment.Single(i => i.Behaviors.Any(b => b is Scanner));
|
||||
|
||||
// A scanner ship at the origin, with a passive eye and the named scanner, tracking and targeting a ship `distance`
|
||||
// ahead that carries a reactor, a radiator and a suppressor, none of which ever acts.
|
||||
private (Ship scanner, Ship target) ScanOn(string scanner = "Scanner", float distance = 100f)
|
||||
{
|
||||
var target = Add(float3(0, 0, distance), tools: new[] { "Reactor", "Radiator", "SuppressorCold" });
|
||||
var s = Add(float3(0, 0, 0), new[] { "Eye" }, new[] { scanner });
|
||||
s.EntityInfoGathered[target] = .5f;
|
||||
s.SetTarget(target);
|
||||
Assert.Same(target, s.Target.Value.Entity); // fixture
|
||||
return (s, target);
|
||||
}
|
||||
|
||||
// Ticks `ticks` steps, holding the observer's track on each of `tracked` at `info` before each.
|
||||
private void Hold(Entity observer, int ticks, float info, params Entity[] tracked)
|
||||
{
|
||||
for (var i = 0; i < ticks; i++)
|
||||
{
|
||||
foreach (var t in tracked) observer.EntityInfoGathered[t] = info;
|
||||
Tick(ScanStep);
|
||||
}
|
||||
}
|
||||
|
||||
private void Hold(Entity observer, int ticks, params Entity[] tracked) => Hold(observer, ticks, .5f, tracked);
|
||||
|
||||
private static EquippedItem[] FittedBesidesTheHull(Entity e) => e.Equipment.Where(i => i != e.EquippedHull).ToArray();
|
||||
|
||||
// ---- AScanRevealsQuietGear ----
|
||||
|
||||
[Fact]
|
||||
public void AScanRevealsQuietGear()
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
var quiet = FittedBesidesTheHull(target);
|
||||
Assert.Equal(3, quiet.Length);
|
||||
Assert.Empty(s.RevealsOf(target)); // fixture: nothing acts
|
||||
|
||||
Hold(s, 15, target);
|
||||
|
||||
Assert.Empty(s.RevealsOf(target));
|
||||
Assert.Equal(15f / 16f, ScannerOf(s).Progress, 4);
|
||||
|
||||
Hold(s, 1, target);
|
||||
|
||||
foreach (var item in quiet)
|
||||
{
|
||||
var reveal = Assert.Single(s.RevealsOf(target), r => r.Item == item);
|
||||
Assert.Null(reveal.Channel);
|
||||
AssertBearing(0f, 1f, reveal.Bearing); // normalize((target - scanner).xz)
|
||||
Assert.Equal(_zone.Time, reveal.Time);
|
||||
}
|
||||
Assert.Equal(0f, ScannerOf(s).Progress); // a finished scan starts over
|
||||
}
|
||||
|
||||
// ---- AScanSkipsTheHull ----
|
||||
|
||||
[Fact]
|
||||
public void AScanSkipsTheHull()
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
Assert.Contains(target.EquippedHull, target.Equipment); // fixture: the hull is an item of Equipment
|
||||
|
||||
Hold(s, 16, target);
|
||||
|
||||
Assert.Equal(3, s.RevealsOf(target).Count()); // fixture: the scan finished
|
||||
Assert.DoesNotContain(s.RevealsOf(target), r => r.Item == target.EquippedHull);
|
||||
}
|
||||
|
||||
// ---- AScanSkipsDestroyedGear ----
|
||||
|
||||
[Fact]
|
||||
public void AScanSkipsDestroyedGear()
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
var items = FittedBesidesTheHull(target);
|
||||
items[0].EquippableItem.Durability = 0f;
|
||||
Assert.True(items[0].EquippableItem.IsDestroyed);
|
||||
|
||||
Hold(s, 16, target);
|
||||
|
||||
Assert.DoesNotContain(s.RevealsOf(target), r => r.Item == items[0]);
|
||||
Assert.Equal(2, s.RevealsOf(target).Count());
|
||||
}
|
||||
|
||||
// ---- AScanRecordsTheClaim ----
|
||||
|
||||
[Fact]
|
||||
public void AScanRecordsTheClaim()
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
var reactor = target.Equipment.Single(i => i.Behaviors.Any(b => b is Reactor));
|
||||
var radiator = DataOf("Radiator");
|
||||
target.Claim(reactor, radiator);
|
||||
|
||||
Hold(s, 16, target);
|
||||
|
||||
var reveal = Assert.Single(s.RevealsOf(target), r => r.Item == reactor);
|
||||
Assert.Same(radiator, reveal.Claimed);
|
||||
Assert.NotSame(reactor.Data, reveal.Claimed);
|
||||
}
|
||||
|
||||
// ---- AScanNeedsATrackAndRange ----
|
||||
|
||||
[Theory]
|
||||
[InlineData(199f, true, true)]
|
||||
[InlineData(201f, true, false)]
|
||||
[InlineData(100f, false, false)]
|
||||
public void AScanNeedsATrackAndRange(float distance, bool tracked, bool builds)
|
||||
{
|
||||
var (s, target) = ScanOn(distance: distance);
|
||||
var third = Add(float3(40, 0, 0), new[] { "Eye" }); // within 200 of the scanner: it hears an emission
|
||||
var info = tracked ? .5f : 0f;
|
||||
s.EntityInfoGathered[target] = info;
|
||||
Assert.Equal(tracked, s.VisibleEntities.Contains(target)); // fixture
|
||||
|
||||
Hold(s, 4, info, target);
|
||||
|
||||
Assert.Equal(builds, ScannerOf(s).Progress > 0f);
|
||||
Assert.Equal(builds, target.IncomingPaints.Count > 0);
|
||||
Assert.Equal(builds, target.RevealsOf(s).Any());
|
||||
Assert.Equal(builds, third.RevealsOf(s).Any());
|
||||
}
|
||||
|
||||
// ---- ABrokenScanDecays ----
|
||||
|
||||
// A scan at .5 broken for half of ScanTime (one second) falls to 0, a quarter in half of that: by range (Execute
|
||||
// runs and does not hold) and by the item switched off (Execute does not run at all).
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void ABrokenScanDecays(bool byRange)
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
Hold(s, 8, target);
|
||||
Assert.Equal(.5f, ScannerOf(s).Progress, 4);
|
||||
if (byRange) target.Position = float3(0, 0, 400);
|
||||
else ScannerItemOf(s).Enabled.Value = false;
|
||||
|
||||
Hold(s, 4, target);
|
||||
Assert.Equal(.25f, ScannerOf(s).Progress, 4);
|
||||
|
||||
Hold(s, 4, target);
|
||||
Assert.Equal(0f, ScannerOf(s).Progress, 4);
|
||||
Hold(s, 2, target);
|
||||
Assert.Equal(0f, ScannerOf(s).Progress, 4); // never below 0
|
||||
}
|
||||
|
||||
// ---- ATargetChangeResetsTheScan ----
|
||||
|
||||
[Fact]
|
||||
public void ATargetChangeResetsTheScan()
|
||||
{
|
||||
var (s, first) = ScanOn();
|
||||
var second = Add(float3(100, 0, 0));
|
||||
Hold(s, 11, first, second);
|
||||
Assert.Equal(11f / 16f, ScannerOf(s).Progress, 4);
|
||||
|
||||
s.SetTarget(second);
|
||||
Hold(s, 1, first, second);
|
||||
|
||||
Assert.Equal(1f / 16f, ScannerOf(s).Progress, 4); // reset, then one tick on the new target
|
||||
Assert.Empty(s.RevealsOf(first));
|
||||
}
|
||||
|
||||
// ---- TheScannedShipIsWarned ----
|
||||
|
||||
// A paint made in a tick is published the next one, so after four ticks the target holds the third tick's paint.
|
||||
[Fact]
|
||||
public void TheScannedShipIsWarned()
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
|
||||
Hold(s, 4, target);
|
||||
|
||||
var paint = Assert.Single(target.IncomingPaints);
|
||||
Assert.Equal(PaintMode.Scan, paint.Mode);
|
||||
Assert.Equal(3f / 16f, paint.Strength, 4);
|
||||
Assert.Same(DataOf("Scanner"), paint.EmitterClass);
|
||||
AssertBearing(0f, -1f, paint.Bearing); // normalize((scanner - target).xz)
|
||||
var warned = Assert.Single(target.RevealsOf(s));
|
||||
Assert.Same(ScannerItemOf(s), warned.Item);
|
||||
Assert.Equal(SignatureChannel.Emission, warned.Channel);
|
||||
}
|
||||
|
||||
// ---- AThirdPartyHearsTheScanner ----
|
||||
|
||||
[Theory]
|
||||
[InlineData("QuietScanner", 49f, true)]
|
||||
[InlineData("QuietScanner", 51f, false)]
|
||||
[InlineData("Scanner", 199f, true)]
|
||||
[InlineData("Scanner", 201f, false)]
|
||||
public void AThirdPartyHearsTheScanner(string scanner, float distance, bool heard)
|
||||
{
|
||||
var (s, target) = ScanOn(scanner);
|
||||
var third = Add(float3(-distance, 0, 0), new[] { "Eye" });
|
||||
|
||||
Hold(s, 3, target);
|
||||
|
||||
Assert.Empty(third.IncomingPaints); // fixture: only the target is painted
|
||||
if (!heard)
|
||||
{
|
||||
Assert.Empty(third.RevealsOf(s));
|
||||
return;
|
||||
}
|
||||
var reveal = Assert.Single(third.RevealsOf(s));
|
||||
Assert.Same(ScannerItemOf(s), reveal.Item);
|
||||
Assert.Equal(SignatureChannel.Emission, reveal.Channel);
|
||||
AssertBearing(1f, 0f, reveal.Bearing); // normalize((scanner - third).xz)
|
||||
}
|
||||
|
||||
// ---- AFinishedScanMakesTheReactorSelectable ----
|
||||
|
||||
[Fact]
|
||||
public void AFinishedScanMakesTheReactorSelectable()
|
||||
{
|
||||
var (s, target) = ScanOn();
|
||||
var reactor = target.Equipment.Single(i => i.Behaviors.Any(b => b is Reactor));
|
||||
Hold(s, 15, target);
|
||||
Assert.False(FireControl.IsRevealed(s, reactor));
|
||||
Assert.False(s.TrySelectTargetItem(reactor));
|
||||
|
||||
Hold(s, 1, target);
|
||||
|
||||
Assert.True(FireControl.IsRevealed(s, reactor));
|
||||
Assert.True(s.TrySelectTargetItem(reactor));
|
||||
}
|
||||
|
||||
// ---- AScanEndsNothing ----
|
||||
|
||||
// A gun revealed by firing and then destroyed, while the observer's track stays between the detection and gear
|
||||
// thresholds (so Sense perceives no destruction): a scan neither ends its reveal nor refreshes it.
|
||||
[Fact]
|
||||
public void AScanEndsNothing()
|
||||
{
|
||||
var target = Add(float3(0, 0, 100), new[] { "Gun" });
|
||||
var s = Add(float3(0, 0, 0), new[] { "Eye" }, new[] { "Scanner" });
|
||||
var gun = Guns(target)[0];
|
||||
var firedAt = Fire(target, gun);
|
||||
var before = Assert.Single(s.RevealsOf(target), r => r.Item == gun);
|
||||
Assert.Equal(firedAt, before.Time); // fixture: the gun is in the book
|
||||
gun.EquippableItem.Durability = 0f;
|
||||
s.EntityInfoGathered[target] = .5f;
|
||||
s.SetTarget(target);
|
||||
|
||||
Hold(s, 16, target);
|
||||
|
||||
var after = Assert.Single(s.RevealsOf(target), r => r.Item == gun);
|
||||
Assert.Equal(firedAt, after.Time);
|
||||
Assert.Equal(SignatureChannel.LaunchFlash, after.Channel);
|
||||
Assert.Contains(s.RevealsOf(target), r => r.Item != gun && r.Channel == null); // fixture: the scan finished
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue