diff --git a/Aetheria.Shared/Aetheria.Shared.csproj b/Aetheria.Shared/Aetheria.Shared.csproj
index 43d1c257d..a8a4c97bc 100644
--- a/Aetheria.Shared/Aetheria.Shared.csproj
+++ b/Aetheria.Shared/Aetheria.Shared.csproj
@@ -23,6 +23,10 @@
+
+
+
+
diff --git a/Assets/Resources/Settings.asset b/Assets/Resources/Settings.asset
index e1b1933e3..50124154b 100644
--- a/Assets/Resources/Settings.asset
+++ b/Assets/Resources/Settings.asset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:070f5797cb93cad308c38f3c87e05a729cfb21b87004fcdc2e146dba336045d6
-size 13073
+oid sha256:ffc1465cae49e57dbfd25e6c685b30388e9e208ea81ae50a4c560764a12ba016
+size 13039
diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs
index 42d58cc2a..dc0573c76 100644
--- a/Assets/Scripts/ServerShared/Entity.cs
+++ b/Assets/Scripts/ServerShared/Entity.cs
@@ -952,6 +952,10 @@ public abstract class Entity
public IEnumerable RevealsOf(Entity subject) =>
_reveals.TryGetValue(subject, out var book) ? book.Values : Enumerable.Empty();
+ // Whether this observer's book for `subject` holds `item`: the book's membership, no live gear read.
+ internal bool HasReveal(Entity subject, EquippedItem item) =>
+ _reveals.TryGetValue(subject, out var book) && book.ContainsKey(item);
+
// The one derivation of how much this observer holds of `subject`: see Contact.
public Contact ContactOf(Entity subject)
{
diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs
index 83fad8b59..6a1f4e3ba 100644
--- a/Assets/Scripts/ServerShared/FireControl.cs
+++ b/Assets/Scripts/ServerShared/FireControl.cs
@@ -152,35 +152,13 @@ public static class FireControl
};
}
- // Cut 2, R5: whether `observer` has resolved `item` (which must belong to some entity in the zone) well
- // enough to aim at it. Derived fresh from EntityInfoGathered on every call -- nothing caches a reveal
- // result, so decay drops it the moment info falls back below tier, with no clearing loop anywhere.
- //
- // Ranking: the target's own non-hull equipment, hardpoint-mounted gear first (the big obvious things),
- // then by size descending, ties broken by equipment order (OrderByDescending is a stable sort, so this
- // needs no explicit tie-break key). Item i of N reveals once info crosses
- // lerp(TargetArmorInfoThreshold, TargetGearInfoThreshold, i / max(1, N - 1)) -- the first item to reveal
- // does so at the armor tier, the last at the gear tier.
- public static bool IsRevealed(Entity observer, EquippedItem item)
- {
- var target = item.Entity;
- // Cut 5, 5.5 (Soul finding 10): a destroyed item drops out of the ranking entirely rather than merely
- // failing its own tier -- ranked.IndexOf(item) then returns -1 for it (the same "no longer on this
- // target" path below), and the survivors' tiers close up over the gap instead of leaving one.
- var ranked = target.Equipment
- .Where(e => e.Data.HardpointType != HardpointType.Hull && !e.EquippableItem.IsDestroyed)
- .OrderByDescending(e => e.Data.HardpointType != HardpointType.Tool)
- .ThenByDescending(e => e.Data.Shape.Coordinates.Length)
- .ToList();
- var index = ranked.IndexOf(item);
- if (index < 0) return false; // the hull itself, or an item that is no longer on this target
-
- var settings = observer.ItemManager.GameplaySettings;
- var info = observer.EntityInfoGathered.TryGetValue(target, out var gathered) ? gathered : 0f;
- var tier = lerp(settings.TargetArmorInfoThreshold, settings.TargetGearInfoThreshold,
- (float) index / max(1, ranked.Count - 1));
- return info >= tier;
- }
+ // The aiming read, and the only reader of live gear beside the book (rulings visibility-per-capability-reveal,
+ // quiet-gear-identifiable-hull-layout-known, reveal-ends-on-perception): `observer` can aim at `item` when its own
+ // book holds a reveal of it (Entity.HasReveal: it saw the item act) and the item is still fitted to its ship and
+ // not destroyed. The book says what was seen; the live item says whether it can still be aimed at. Derived on
+ // every read, never cached; watching a ship reveals no gear.
+ public static bool IsRevealed(Entity observer, EquippedItem item) =>
+ !item.EquippableItem.IsDestroyed && item.Entity.Equipment.Contains(item) && observer.HasReveal(item.Entity, item);
// Cut 2, Q4: the stats an entity actually fires with. A working targeting system (equipped, online --
// not destroyed, not shut down) supplies its own resolved stats; anything else falls back to the
diff --git a/Assets/Scripts/ServerShared/Settings.cs b/Assets/Scripts/ServerShared/Settings.cs
index 1601a5280..c6ffe93d9 100644
--- a/Assets/Scripts/ServerShared/Settings.cs
+++ b/Assets/Scripts/ServerShared/Settings.cs
@@ -222,7 +222,6 @@ public class GameplaySettings
public float BearingMemory = 6;
public float TargetInfoDecay;
public float TargetDetectionInfoThreshold;
- public float TargetArmorInfoThreshold;
// Also the Identified threshold: the track level at which a ship's hull class, and with it its layout, is known.
public float TargetGearInfoThreshold;
public float ConvergenceMinimumDistance;
diff --git a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs
index a9698eae4..956f67aaa 100644
--- a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs
+++ b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs
@@ -105,7 +105,6 @@ public sealed partial class ContactRevealTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
TargetInfoDecay = .5f,
VisibilityDecay = 100f,
@@ -773,8 +772,8 @@ public sealed partial class ContactRevealTests : IDisposable
// Ruling contact-reads-book-aim-reads-live: what an observer knows of a ship comes from its own book. A gun
// destroyed by a third party out of its sight changes none of its levels and stays in its reveal list, while
// the aiming read, which asks about the item now, refuses the dead gun and still serves the live one. No Sense
- // runs after the destruction here (the aiming read needs info 1, at which a Sense would perceive it); the
- // unseen case with a Sense is AnIdentifiedObserverSeesGearDestroyed's below-threshold case.
+ // runs after the destruction here; the unseen case with a Sense is AnIdentifiedObserverSeesGearDestroyed's
+ // below-threshold case.
[Fact]
public void GearDestroyedOutOfSightLeavesWhatTheObserverKnowsAlone()
{
@@ -785,7 +784,6 @@ public sealed partial class ContactRevealTests : IDisposable
Fire(e, guns[1]);
var before = o.ContactOf(e);
Assert.True(before.Detected && before.Classified && !before.Tracked, "fixture: a bearing and a class, no track");
- o.EntityInfoGathered[e] = 1f; // the aiming read needs info to resolve the target's gear
Assert.True(FireControl.IsRevealed(o, guns[0]) && FireControl.IsRevealed(o, guns[1]), "fixture: both guns are aimable");
guns[0].EquippableItem.Durability = 0f; // destroyed by someone else; o saw nothing
diff --git a/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs b/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs
index 9512be458..c1777109a 100644
--- a/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs
+++ b/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs
@@ -479,7 +479,8 @@ public sealed class FireAuthorityTests : IDisposable
});
var aimedItem = e.Target.Equipment.Single(x => x.EquippableItem == aimedGear);
- e.Shooter.EntityInfoGathered[e.Target] = 1f; // fully revealed
+ e.Shooter.EntityInfoGathered[e.Target] = 1f; // fully tracked
+ e.Shooter.RecordReveal(e.Target, aimedItem, SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(aimedItem));
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs
index ed352a535..237c39851 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs
@@ -40,7 +40,6 @@ public sealed class FireControlCut11Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs
index f935c2edf..be01188b2 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs
@@ -42,7 +42,6 @@ public sealed class FireControlCut123Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs
index a1d5445e8..880f34c77 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs
@@ -44,7 +44,6 @@ public sealed partial class FireControlCut124Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = commitHorizon,
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs
index 2c0623273..fb1fa19fc 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs
@@ -41,7 +41,6 @@ public sealed class FireControlCut12Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
@@ -522,6 +521,7 @@ public sealed class FireControlCut12Tests : IDisposable
var aim = new int2(2, 2);
var e = Build(TestSettings(), SolidShape(6, 6), precision: 1000f, markerCells: new[] { aim });
var aimed = e.Markers[0];
+ e.Shooter.RecordReveal(e.Target, aimed, SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(aimed));
// The aimed item is still equipped (Entity.TryUnequip refuses on an active entity, and reveal/aim
@@ -548,6 +548,7 @@ public sealed class FireControlCut12Tests : IDisposable
public void AimingAtAnItemBiggerThanOneCellCentresOnItsCentroid()
{
var e = Build(TestSettings(), SolidShape(9, 9), precision: 1f, targetFacing: float2(1, 0), equipBigMarker: true);
+ e.Shooter.RecordReveal(e.Target, e.BigMarker, SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(e.BigMarker));
var travelDirection = FireControl.TravelDirection(e.Weapon, e.Shooter, e.Target);
@@ -585,6 +586,7 @@ public sealed class FireControlCut12Tests : IDisposable
// the bounding-box centre along the diagonal, not along either grid axis -- fully onto the lateral
// axis, instead of losing most of it to the axis a purely axis-aligned facing would leave unmeasured.
var e = Build(TestSettings(), SolidShape(9, 9), precision: 1f, targetFacing: normalize(float2(1, 1)), equipLMarker: true);
+ e.Shooter.RecordReveal(e.Target, e.LMarker, SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(e.LMarker));
var travelDirection = FireControl.TravelDirection(e.Weapon, e.Shooter, e.Target);
@@ -975,6 +977,7 @@ public sealed class FireControlCut12Tests : IDisposable
var aim = new int2(1, 1);
var e = Build(settings, hullShape, precision, spread: 0, targetFacing: normalize(float2(1f, 1f)),
markerCells: new[] { aim });
+ e.Shooter.RecordReveal(e.Target, e.Markers[0], SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(e.Markers[0]));
var shotId = FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
@@ -1157,6 +1160,7 @@ public sealed class FireControlCut12Tests : IDisposable
var aim = new int2(1, 1);
var e = Build(TestSettings(), shape, precision: .3f, targetFacing: normalize(float2(.8f, .6f)), markerCells: new[] { aim });
+ e.Shooter.RecordReveal(e.Target, e.Markers[0], SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(e.Markers[0]));
var outcomes = FireMany(e, 1500);
@@ -1190,6 +1194,7 @@ public sealed class FireControlCut12Tests : IDisposable
{
var e = Build(settings, hullShape, precision: .6f, targetFacing: facing, markerCells: new[] { aimCell });
var aimed = e.Markers[0];
+ e.Shooter.RecordReveal(e.Target, aimed, SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(aimed));
Assert.Same(aimed, e.Shooter.ResolvedTargetItem);
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs
index 327b6e9e9..a21e4bef3 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs
@@ -40,7 +40,6 @@ public sealed class FireControlCut5Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
@@ -582,13 +581,13 @@ public sealed class FireControlCut5Tests : IDisposable
return (items, target, bigItem, mediumItem);
}
- // A destroyed item drops out of the ranking entirely: it is never revealed regardless of info, and the
- // survivor's own tier closes up over the gap rather than leaving one. Mutation: drop the Durability filter
- // from IsRevealed's ranking (a destroyed item keeps its old tier and TargetItem keeps aiming at a corpse).
+ // A destroyed item stops being an aim point: IsRevealed is false for it though the observer's book still
+ // holds it, and the survivor the observer saw act stays aimable. Mutation: drop the IsDestroyed filter from
+ // IsRevealed (TargetItem keeps aiming at a corpse).
[Fact]
public void DestroyedItemStopsBeingAimPoint()
{
- var settings = TestSettings(); // Armor .2, Gear .8
+ var settings = TestSettings();
var (items, target, big, medium) = BuildRevealTarget(settings);
var observerHullRef = items.ItemData.RefOf(items.ItemData.GetByName("Hull"));
var observer = new Ship(items, target.Zone, new EquippableItem { Data = observerHullRef, Durability = 1000, Lot = 1 }, new EntitySettings());
@@ -596,20 +595,17 @@ public sealed class FireControlCut5Tests : IDisposable
observer.Activate();
observer.SetTarget(target);
- // With both items alive: Big (index 0) reveals at .2, Medium (index 1) at .8.
- observer.EntityInfoGathered[target] = .3f;
+ observer.RecordReveal(target, big, SignatureChannel.LaunchFlash);
+ observer.RecordReveal(target, medium, SignatureChannel.LaunchFlash);
Assert.True(FireControl.IsRevealed(observer, big));
- Assert.False(FireControl.IsRevealed(observer, medium));
+ Assert.True(FireControl.IsRevealed(observer, medium));
Assert.True(observer.TrySelectTargetItem(big));
- // Destroy Big: it drops out of the ranking outright, so it can never be revealed again regardless of
- // info, and it stops being a valid aim point on the very next read -- no clearing loop needed.
big.EquippableItem.Durability = 0f;
Assert.False(FireControl.IsRevealed(observer, big));
Assert.Null(observer.ResolvedTargetItem);
- // Medium is now the only survivor, so it inherits index 0's own tier (.2), not its old index-1 tier
- // (.8) -- the gap closes rather than leaving Medium stranded behind a tier meant for a corpse.
+ // Medium was seen too, so it stays aimable; the destroyed neighbour changes nothing about it.
Assert.True(FireControl.IsRevealed(observer, medium));
}
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs
index 8f581a33d..888124dd2 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs
@@ -40,7 +40,6 @@ public sealed class FireControlCut6Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs
index 586c658b4..c5f9584a7 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs
@@ -39,7 +39,6 @@ public sealed class FireControlCut6cTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs
index fdf54d5f2..12857f0cf 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs
@@ -41,7 +41,6 @@ public sealed class FireControlCut6dTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
@@ -172,6 +171,7 @@ public sealed class FireControlCut6dTests : IDisposable
if (marker != null)
{
var markerItem = target.Equipment.Single(x => x.EquippableItem == marker);
+ shooter.RecordReveal(target, markerItem, SignatureChannel.LaunchFlash);
Assert.True(shooter.TrySelectTargetItem(markerItem));
Assert.Same(markerItem, shooter.ResolvedTargetItem);
}
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs
index 56b5cc2be..d08db8c65 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs
@@ -35,16 +35,14 @@ public sealed class FireControlCut7Tests : IDisposable
Directory.Delete(_root, true);
}
- // Detection well below the armor tier, which is well below the gear tier, so an aimed interior item can
- // decay out of its own reveal while the weapon itself (index 0, the armor tier) and general detection
- // both stay satisfied -- the shot still fires, only the aim point should be lost.
+ // General detection stays satisfied when the aimed interior item is destroyed -- the shot still fires, only
+ // the aim point is lost.
private static GameplaySettings TestSettings() => new GameplaySettings
{
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .3f,
TargetGearInfoThreshold = .9f,
FiringArc = 170,
CommitHorizon = .5f,
@@ -66,8 +64,8 @@ public sealed class FireControlCut7Tests : IDisposable
}
// A trimmed copy of FireAuthorityTests.Build: one Sensors-hardpoint weapon plus a Tool-slot targeting
- // system on the shooter, a target ship with a matching weapon (index 0 in IsRevealed's ranking) and,
- // through beforeActivate, a second Tool item to aim at (index 1 -- the gear tier).
+ // system on the shooter, a target ship with a matching weapon and,
+ // through beforeActivate, a second Tool item to aim at.
private Engagement Build(GameplaySettings settings, Action beforeActivate)
{
var hullShape = new Shape(5, 5);
@@ -152,13 +150,11 @@ public sealed class FireControlCut7Tests : IDisposable
// source.TargetItem.Value -- the aim point is re-checked against reveal at the instant the trigger is
// pulled, same as every other read of it. Mutation: read TargetItem.Value directly. Must die.
//
- // The target's non-hull equipment ranks the hardpoint weapon first (armor tier, index 0 of 2) and the
- // interior "Targeting" item last (gear tier, index 1 of 2, i.e. exactly TargetGearInfoThreshold). Info is
- // dropped to a level between the two tiers: the weapon stays revealed (so the shot still fires, PBase >
- // 0) but the aimed item's own tier is no longer met -- the reveal re-check, not a global blackout, is
- // what must drop the aim point.
+ // The observer saw the target's interior item act and aims at it; the item is then destroyed. The shot is
+ // still worth taking -- this is no blackout -- but the aim point is re-checked at the instant the trigger is
+ // pulled and is lost.
[Fact]
- public void AimedItemDropsOutWhenRevealDecaysBeforeFire()
+ public void AimedItemDropsOutWhenItIsDestroyedBeforeFire()
{
EquippableItem aimedGear = null;
var settings = TestSettings();
@@ -171,17 +167,16 @@ public sealed class FireControlCut7Tests : IDisposable
});
var aimedItem = e.Target.Equipment.Single(x => x.EquippableItem == aimedGear);
- // Fully reveal and select the interior item as the aim point.
+ // The shooter tracks the target fully and has seen the interior item act; it selects it as the aim point.
e.Shooter.EntityInfoGathered[e.Target] = 1f;
+ e.Shooter.RecordReveal(e.Target, aimedItem, SignatureChannel.LaunchFlash);
Assert.True(e.Shooter.TrySelectTargetItem(aimedItem));
Assert.Same(aimedItem, e.Shooter.ResolvedTargetItem);
- // Decay to halfway between the armor and gear tiers: the weapon (armor tier) stays revealed, the
- // aimed item (gear tier) does not. The raw field still holds the item -- nothing clears it -- but the
- // read accessor must now return null.
- e.Shooter.EntityInfoGathered[e.Target] = (settings.TargetArmorInfoThreshold + settings.TargetGearInfoThreshold) / 2f;
- Assert.Same(aimedItem, e.Shooter.TargetItem.Value); // raw field: untouched by decay
- Assert.Null(e.Shooter.ResolvedTargetItem); // read path: re-checked, and drops it
+ // Destroyed: the raw field still holds the item -- nothing clears it -- but the read accessor returns null.
+ aimedGear.Durability = 0f;
+ Assert.Same(aimedItem, e.Shooter.TargetItem.Value);
+ Assert.Null(e.Shooter.ResolvedTargetItem);
var shotId = FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
var shot = e.Zone.PendingShots.Single(s => s.ShotId == shotId);
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs
index cb81bbe68..5a112de4e 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut8Tests.cs
@@ -39,7 +39,6 @@ public sealed class FireControlCut8Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs
index 2a94080ed..50d17bf88 100644
--- a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs
@@ -42,7 +42,6 @@ public sealed class FireControlCut9Tests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs
index 5617eaa0b..488d2f58e 100644
--- a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs
@@ -41,7 +41,6 @@ public sealed class FireControlPerWeaponTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/FireControlTests.cs b/tests/Aetheria.Shared.Tests/FireControlTests.cs
index 8b0f16e9f..0e3876f1b 100644
--- a/tests/Aetheria.Shared.Tests/FireControlTests.cs
+++ b/tests/Aetheria.Shared.Tests/FireControlTests.cs
@@ -237,6 +237,25 @@ public sealed class FireControlTests : IDisposable
Assert.True(weapon.Firing); // arc + range satisfied: the fight actually decided to fire, not just avoided a crash
}
+ // Combat picks a target item only among the items its ship holds a reveal of: the target's gun is in range and
+ // the target is fully tracked, yet nothing is aimed at until the shooter has seen the gun act. Mutation:
+ // Combat tests weapons without IsRevealed, or IsRevealed falls back to the track.
+ [Fact]
+ public void TheAIAimsOnlyAtWhatItSaw()
+ {
+ var (shooter, target, zone) = BuildCombatEncounter(float3(0, 0, 10));
+ var targetGun = target.Equipment.Single(x => x.Behaviors.Any(b => b is Weapon));
+
+ zone.Update(1f);
+ zone.Update(1f);
+ Assert.Null(shooter.TargetItem.Value);
+
+ shooter.RecordReveal(target, targetGun, SignatureChannel.LaunchFlash);
+ zone.Update(1f);
+
+ Assert.Same(targetGun, shooter.TargetItem.Value);
+ }
+
// Controls cut (helm split): Combat aims the shooter at the intercept (Aim) and commands the hull toward it
// through the one steering law (Turn); it never turns the hull by writing Aim.
[Fact]
diff --git a/tests/Aetheria.Shared.Tests/LockWarningTests.cs b/tests/Aetheria.Shared.Tests/LockWarningTests.cs
index 3b2710660..e876bbf15 100644
--- a/tests/Aetheria.Shared.Tests/LockWarningTests.cs
+++ b/tests/Aetheria.Shared.Tests/LockWarningTests.cs
@@ -46,7 +46,6 @@ public sealed class LockWarningTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/MineTests.cs b/tests/Aetheria.Shared.Tests/MineTests.cs
index f132de9d2..74541bdac 100644
--- a/tests/Aetheria.Shared.Tests/MineTests.cs
+++ b/tests/Aetheria.Shared.Tests/MineTests.cs
@@ -73,7 +73,7 @@ public sealed class MineTests : IDisposable
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
- TargetDetectionInfoThreshold = .1f, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f,
+ TargetDetectionInfoThreshold = .1f, TargetGearInfoThreshold = .8f,
FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 2f,
UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 1000f, AgentMinHitProbability = 0f
};
diff --git a/tests/Aetheria.Shared.Tests/PaintModeTests.cs b/tests/Aetheria.Shared.Tests/PaintModeTests.cs
index 09a421db6..7ab47da79 100644
--- a/tests/Aetheria.Shared.Tests/PaintModeTests.cs
+++ b/tests/Aetheria.Shared.Tests/PaintModeTests.cs
@@ -66,7 +66,6 @@ public sealed class PaintModeTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/SignatureTests.cs b/tests/Aetheria.Shared.Tests/SignatureTests.cs
index f68fa163c..4f818e4a8 100644
--- a/tests/Aetheria.Shared.Tests/SignatureTests.cs
+++ b/tests/Aetheria.Shared.Tests/SignatureTests.cs
@@ -48,7 +48,6 @@ public sealed class SignatureTests : IDisposable
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f,
- TargetArmorInfoThreshold = .2f,
TargetGearInfoThreshold = .8f,
FiringArc = 170,
CommitHorizon = .5f,
diff --git a/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs b/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs
index b621f7b87..a5120c646 100644
--- a/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs
+++ b/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs
@@ -36,7 +36,6 @@ public sealed class TargetingSystemTests : IDisposable
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
- TargetArmorInfoThreshold = .3f,
TargetGearInfoThreshold = .9f,
UnaidedAccuracy = .1f,
FiringArc = 120
@@ -50,9 +49,7 @@ public sealed class TargetingSystemTests : IDisposable
new PerformanceStat { Min = 0, Max = max, Terms = { new StatTerm { Source = StatSource.PowerSupply, Exponent = exponent } } };
// A 6x4 hull with one Sensors hardpoint at (0,0) (hardpoint-mounted weapon) and interior room for two
- // more Tool items, one 2-cell and one 1-cell. Non-hull equipment ends up authored in this order --
- // hardpoint weapon, wide interior item, narrow interior item -- but it is FireControl.IsRevealed's own
- // ranking, not authoring order, that RevealOrder actually pins.
+ // more Tool items, one 2-cell and one 1-cell: a hardpoint weapon, a wide interior item and a narrow one.
// `targetingData`, when given, authors a fourth non-hull item (a Tool gear carrying that behaviour) in
// the same catalog-build pass, so a test that needs a targeting system never has to reopen the catalog
// file after the live, already-open ItemManager exists (CultCache is single-writer).
@@ -114,12 +111,6 @@ public sealed class TargetingSystemTests : IDisposable
var hull = new EquippableItem { Data = hullRef, Durability = 1, Lot = 1 };
var target = new Ship(items, zone, hull, new EntitySettings());
- // Cut 7 (docs/fire-control-cut.md, 7.2): equipped in size-ascending, hardpoint-last order -- the
- // opposite of IsRevealed's own ranking (hardpoint-mounted first, then size descending) -- so that
- // dropping the explicit ranking in FireControl.IsRevealed is actually observable. Equipping in
- // rank order let a "drop the ordering, rank by equipment order alone" mutation pass: Entity.Equipment
- // already happened to enumerate in the correct final order by sheer coincidence of authoring order,
- // so RevealOrder's own assertions never distinguished the ranked read from the raw one.
var narrowRef = items.ItemData.RefOf(items.ItemData.GetByName("Narrow"));
var narrow = new EquippableItem { Data = narrowRef, Durability = 1, Lot = 4 };
Assert.True(target.TryFindSpace(narrow, out var narrowPos));
@@ -165,10 +156,10 @@ public sealed class TargetingSystemTests : IDisposable
return observer;
}
- // R5 / the selection predicate: TrySelectTargetItem accepts only a revealed item of the current target,
- // a Target change nulls the aim point, and decay drops it on read with no loop clearing it.
- // Mutation: IsRevealed returns true unconditionally; the Target-change subscription is removed; or
- // ResolvedTargetItem returns TargetItem.Value without re-checking.
+ // The selection predicate: TrySelectTargetItem accepts only an item of the current target that the observer's
+ // book holds and that is still whole; a Target change nulls the aim point, and destruction drops it on read
+ // with no loop clearing it. Mutation: IsRevealed returns true unconditionally, or ignores the book; the
+ // Target-change subscription is removed; or ResolvedTargetItem returns TargetItem.Value without re-checking.
[Fact]
public void SelectionNeedsReveal()
{
@@ -176,11 +167,10 @@ public sealed class TargetingSystemTests : IDisposable
var observer = BuildObserver(items, target.Zone);
observer.SetTarget(target);
- observer.EntityInfoGathered[target] = .29f; // below the single item's own tier (Armor, N==1)
- Assert.False(observer.TrySelectTargetItem(weapon));
+ Assert.False(observer.TrySelectTargetItem(weapon)); // nothing seen yet
Assert.Null(observer.TargetItem.Value);
- observer.EntityInfoGathered[target] = .3f; // at tier: revealed
+ observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
Assert.True(observer.TrySelectTargetItem(weapon));
Assert.Same(weapon, observer.TargetItem.Value);
Assert.Same(weapon, observer.ResolvedTargetItem);
@@ -189,58 +179,105 @@ public sealed class TargetingSystemTests : IDisposable
observer.SetTarget(TargetRef.None);
Assert.Null(observer.TargetItem.Value);
- // Decay: re-select, then let info fall back below tier without touching TargetItem.Value at all --
- // the raw field still holds the item (no loop cleared it), but the read accessor drops it.
+ // Destruction: re-select, then destroy the item without touching TargetItem.Value at all -- the raw field
+ // still holds the item (no loop cleared it), but the read accessor drops it.
observer.SetTarget(target);
Assert.True(observer.TrySelectTargetItem(weapon));
- observer.EntityInfoGathered[target] = .1f;
+ weapon.EquippableItem.Durability = 0f;
Assert.Same(weapon, observer.TargetItem.Value);
Assert.Null(observer.ResolvedTargetItem);
}
- // The ranking: hardpoint-mounted gear before interior Tool gear, larger before smaller within a group.
- // Mutation: drop the ordering (e.g. rank by equipment order alone).
+ // Rulings visibility-per-capability-reveal and reveal-ends-on-perception: a target with three items, one seen
+ // acting. Only that item is revealed; the others are refused by the same writer. Mutation: IsRevealed true for
+ // any fitted item (book ignored).
[Fact]
- public void RevealOrder()
+ public void GearIsRevealedByWhatItDid()
{
var (items, target, weapon, wideItem, narrowItem, _) = BuildTarget(TestSettings());
var observer = BuildObserver(items, target.Zone);
+ observer.SetTarget(target);
+
+ observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
- observer.EntityInfoGathered[target] = .35f; // above Armor (.3, index 0), below the mid tier (.6, index 1)
Assert.True(FireControl.IsRevealed(observer, weapon));
Assert.False(FireControl.IsRevealed(observer, wideItem));
Assert.False(FireControl.IsRevealed(observer, narrowItem));
-
- observer.EntityInfoGathered[target] = .65f; // above the mid tier, below Gear (.9, index 2)
- Assert.True(FireControl.IsRevealed(observer, weapon));
- Assert.True(FireControl.IsRevealed(observer, wideItem));
- Assert.False(FireControl.IsRevealed(observer, narrowItem));
-
- observer.EntityInfoGathered[target] = .95f; // above Gear: everything revealed
- Assert.True(FireControl.IsRevealed(observer, weapon));
- Assert.True(FireControl.IsRevealed(observer, wideItem));
- Assert.True(FireControl.IsRevealed(observer, narrowItem));
+ Assert.True(observer.TrySelectTargetItem(weapon));
+ Assert.False(observer.TrySelectTargetItem(wideItem));
+ Assert.False(observer.TrySelectTargetItem(narrowItem));
}
- // The tiers really are the two authored settings: the first item reveals exactly at
- // TargetArmorInfoThreshold, the last exactly at TargetGearInfoThreshold. Mutation: swap the two
- // settings, or use TargetDetectionInfoThreshold instead.
+ // Ruling quiet-gear-identifiable-hull-layout-known: watching reveals no gear. An observer whose track is full
+ // (it identifies the ship) on a ship that has done nothing can aim at none of its items, though ContactOf
+ // reads Identified. Mutation: a track-tier or dwell fallback in IsRevealed.
[Fact]
- public void RevealSpansBothThresholds()
+ public void WatchingRevealsNoGear()
{
- var settings = TestSettings();
- var (items, target, weapon, wideItem, narrowItem, _) = BuildTarget(settings);
+ var (items, target, weapon, wideItem, narrowItem, _) = BuildTarget(TestSettings());
var observer = BuildObserver(items, target.Zone);
+ observer.EntityInfoGathered[target] = 1f;
- observer.EntityInfoGathered[target] = settings.TargetArmorInfoThreshold;
- Assert.True(FireControl.IsRevealed(observer, weapon));
- Assert.False(FireControl.IsRevealed(observer, wideItem));
- Assert.False(FireControl.IsRevealed(observer, narrowItem));
+ Assert.True(observer.ContactOf(target).Identified);
+ foreach (var item in new[] { weapon, wideItem, narrowItem })
+ Assert.False(FireControl.IsRevealed(observer, item));
+ Assert.Empty(observer.RevealsOf(target));
+ }
- observer.EntityInfoGathered[target] = settings.TargetGearInfoThreshold;
+ // A revealed item made IsDestroyed drops out of the aim read: IsRevealed false, ResolvedTargetItem null,
+ // TrySelectTargetItem refuses it. Mutation: drop the IsDestroyed filter from IsRevealed.
+ [Fact]
+ public void ADestroyedItemDropsOut()
+ {
+ var (items, target, weapon, wideItem, _, _) = BuildTarget(TestSettings());
+ var observer = BuildObserver(items, target.Zone);
+ observer.SetTarget(target);
+ observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
+ observer.RecordReveal(target, wideItem, SignatureChannel.LaunchFlash);
+ Assert.True(observer.TrySelectTargetItem(weapon));
+
+ weapon.EquippableItem.Durability = 0f;
+
+ Assert.False(FireControl.IsRevealed(observer, weapon));
+ Assert.Null(observer.ResolvedTargetItem);
+ Assert.False(observer.TrySelectTargetItem(weapon));
+ Assert.True(FireControl.IsRevealed(observer, wideItem)); // the survivor stays aimable
+ }
+
+ // A revealed item that is no longer fitted to its ship cannot be aimed at, though the observer's book still
+ // holds it. Mutation: drop the Equipment.Contains check from IsRevealed.
+ [Fact]
+ public void AnUnfittedItemCannotBeAimedAt()
+ {
+ var (items, target, weapon, _, _, _) = BuildTarget(TestSettings());
+ var observer = BuildObserver(items, target.Zone);
+ observer.SetTarget(target);
+ observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
Assert.True(FireControl.IsRevealed(observer, weapon));
- Assert.True(FireControl.IsRevealed(observer, wideItem));
- Assert.True(FireControl.IsRevealed(observer, narrowItem));
+
+ target.Deactivate(); // an active ship refuses to unequip
+ Assert.NotNull(target.TryUnequip(weapon));
+
+ Assert.Contains(observer.RevealsOf(target), r => r.Item == weapon);
+ Assert.False(FireControl.IsRevealed(observer, weapon));
+ }
+
+ // The aim read is live; the book is not. Below the identify threshold the observer perceives nothing of a
+ // destruction, so a revealed gun destroyed by a third party stays in RevealsOf (knowledge) yet cannot be aimed
+ // at. Mutations: IsRevealed without the live filter fails this; a live filter in RevealsOf fails it too.
+ [Fact]
+ public void TheAimReadIsLiveTheBookIsNot()
+ {
+ var (items, target, weapon, _, _, _) = BuildTarget(TestSettings());
+ var observer = BuildObserver(items, target.Zone);
+ observer.EntityInfoGathered[target] = .5f; // below TargetGearInfoThreshold (.9)
+ observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
+ Assert.True(FireControl.IsRevealed(observer, weapon));
+
+ weapon.EquippableItem.Durability = 0f;
+
+ Assert.Contains(observer.RevealsOf(target), r => r.Item == weapon);
+ Assert.False(FireControl.IsRevealed(observer, weapon));
}
// Q4: the subsystem is what supplies accuracy. No targeting system, a destroyed one, and a disabled one
diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs
index e35b311b4..3bd0ec7d1 100644
--- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs
+++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs
@@ -63,7 +63,7 @@ public sealed class VapourCloudTests : IDisposable
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
- TargetDetectionInfoThreshold = Threshold, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f,
+ TargetDetectionInfoThreshold = Threshold, TargetGearInfoThreshold = .8f,
TargetInfoDecay = .5f, VisibilityDecay = 0f,
FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f
};
diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs
index 2185ca077..fec27f55f 100644
--- a/tools/AetherDb/Program.cs
+++ b/tools/AetherDb/Program.cs
@@ -1147,7 +1147,7 @@ public static class Program
// Durability 50, Mass 75, Price 75000, heat response 200-500/276.5/36. GameplaySettings.UnaidedAccuracy
// is .05 (AetherDb settings) and GameplaySettings.UnaidedTracking will be 10 once Cut 5 lands (not yet on
// this branch) -- both designs clear it by 50% or more. Resolution is placed against the live thresholds
- // this branch reads from AetherDb settings: TargetDetectionInfoThreshold .1, TargetArmorInfoThreshold .5,
+ // this branch reads from AetherDb settings: TargetDetectionInfoThreshold .1,
// TargetGearInfoThreshold .8. FireControl.HitProbability reads
// pSensor = saturate(unlerp(TargetDetectionInfoThreshold, Resolution, info)), so a HIGHER Resolution
// needs MORE gathered info before sensor state stops limiting hits -- it is a cost, not a bonus. The