Merge eureka/aetheria-release-layout-aim
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
3f5cef68e7
20 changed files with 561 additions and 60 deletions
|
|
@ -441,21 +441,21 @@ public class ActionGameManager : MonoBehaviour
|
|||
Input.Player.TargetNext.performed += context => CurrentEntity.TargetNext();
|
||||
Input.Player.TargetPrevious.performed += context => CurrentEntity.TargetPrevious();
|
||||
|
||||
// Cut 2 (docs/fire-control-cut.md): cycles the aim point among the current target's revealed
|
||||
// subsystems -- decides nothing itself, only calls the one writer (TrySelectTargetItem), same
|
||||
// predicate (FireControl.IsRevealed) the AI path uses. Entity-only: a chunk has no subsystems.
|
||||
// Cut 2 (docs/fire-control-cut.md): cycles the aim point among the current target's located
|
||||
// subsystems -- decides nothing itself, only calls the one writer (TrySelectTargetItem), gated on the
|
||||
// same predicate (FireControl.IsLocated) the writer reads. Entity-only: a chunk has no subsystems.
|
||||
Input.Player.CycleTargetItem.performed += context =>
|
||||
{
|
||||
var target = CurrentEntity.Target.Value.Entity;
|
||||
if (target == null) return;
|
||||
var revealed = target.Equipment.Where(x => FireControl.IsRevealed(CurrentEntity, x)).ToArray();
|
||||
if (revealed.Length == 0)
|
||||
var located = target.Equipment.Where(x => FireControl.IsLocated(CurrentEntity, x)).ToArray();
|
||||
if (located.Length == 0)
|
||||
{
|
||||
CurrentEntity.TrySelectTargetItem(null);
|
||||
return;
|
||||
}
|
||||
var currentItemIndex = Array.IndexOf(revealed, CurrentEntity.ResolvedTargetItem);
|
||||
CurrentEntity.TrySelectTargetItem(revealed[(currentItemIndex + 1) % revealed.Length]);
|
||||
var currentItemIndex = Array.IndexOf(located, CurrentEntity.ResolvedTargetItem);
|
||||
CurrentEntity.TrySelectTargetItem(located[(currentItemIndex + 1) % located.Length]);
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -46,8 +46,10 @@ public class Scanner : Behavior, IProgressBehavior
|
|||
// The scan's progress now: what Execute last built, less ScanTime-relative decay over the time since a tick last held
|
||||
// it (ruling scan-held-beam: a broken scan decays over ScanTime, the item inactive or destroyed included). Derived from
|
||||
// the zone clock, never from tick order, so a scanner decays on the tick it breaks whether its host runs Execute
|
||||
// before the always-updated pass (EquippedItem) or after it (ConsumableItemEffect). The one decay site.
|
||||
public float Progress => Remaining(_heldAt);
|
||||
// before the always-updated pass (EquippedItem) or after it (ConsumableItemEffect). The one decay site. It is the
|
||||
// progress against the ship's target now: a scan built on another ship reads 0 the moment the target changes, whether
|
||||
// or not the item is running to reset it.
|
||||
public float Progress => _target == Entity.Target.Value.Entity ? Remaining(_heldAt) : 0f;
|
||||
|
||||
public Scanner(ScannerData data, EquippedItem item) : base(data, item)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,9 +71,9 @@ public abstract class Entity
|
|||
|
||||
// Cut 2 (docs/fire-control-cut.md): the stored aim-point slot. Written only by TrySelectTargetItem
|
||||
// below, and nulled whenever Target changes (subscribed in Activate). Do not read this field directly
|
||||
// outside Entity -- read ResolvedTargetItem, which re-validates on every call, so decay (or the item
|
||||
// leaving the target) drops the aim point the moment it is no longer earned, with no loop that has to
|
||||
// notice and clear it.
|
||||
// outside Entity -- read ResolvedTargetItem, which re-validates on every call (FireControl.IsLocated), so decay
|
||||
// (or the item leaving the target, or being destroyed) drops the aim point the moment it is no longer earned,
|
||||
// with no loop that has to notice and clear it.
|
||||
public ReactiveProperty<EquippedItem> TargetItem = new ReactiveProperty<EquippedItem>((EquippedItem)null);
|
||||
|
||||
// Where the pilot points: reticle, lock cone, tractor, guided rounds, fire direction. Steers nothing; the
|
||||
|
|
@ -408,8 +408,9 @@ public abstract class Entity
|
|||
}
|
||||
|
||||
// Cut 2 (docs/fire-control-cut.md): the only writer of TargetItem. Accepts null (clearing the aim
|
||||
// point) or an item belonging to the current Target that FireControl.IsRevealed resolves true for this
|
||||
// entity as observer. Returns whether the write took effect; a rejected non-null selection leaves
|
||||
// point) or an item belonging to the current Target that FireControl.IsLocated resolves true for this
|
||||
// entity as observer (an item it saw act, or one on a mount of the hull it believes the target to be).
|
||||
// Returns whether the write took effect; a rejected non-null selection leaves
|
||||
// TargetItem unchanged.
|
||||
public bool TrySelectTargetItem(EquippedItem item)
|
||||
{
|
||||
|
|
@ -420,7 +421,7 @@ public abstract class Entity
|
|||
}
|
||||
|
||||
// Mining Cut 3: entity-only. A chunk has no items, so an item never belongs to a chunk target.
|
||||
if (Target.Value.Entity == null || item.Entity != Target.Value.Entity || !FireControl.IsRevealed(this, item))
|
||||
if (Target.Value.Entity == null || item.Entity != Target.Value.Entity || !FireControl.IsLocated(this, item))
|
||||
return false;
|
||||
|
||||
TargetItem.Value = item;
|
||||
|
|
@ -428,10 +429,10 @@ public abstract class Entity
|
|||
}
|
||||
|
||||
// The read path for TargetItem (see the field's own comment): null once the aimed item is no longer
|
||||
// revealed to this entity, or no longer belongs to the current Target, even though nothing wrote
|
||||
// located by this entity, or no longer belongs to the current Target, even though nothing wrote
|
||||
// TargetItem.Value at the moment that became true. Re-checked on every call; never cached.
|
||||
public EquippedItem ResolvedTargetItem =>
|
||||
TargetItem.Value != null && Target.Value.Entity == TargetItem.Value.Entity && FireControl.IsRevealed(this, TargetItem.Value)
|
||||
TargetItem.Value != null && Target.Value.Entity == TargetItem.Value.Entity && FireControl.IsLocated(this, TargetItem.Value)
|
||||
? TargetItem.Value
|
||||
: null;
|
||||
|
||||
|
|
@ -1058,12 +1059,23 @@ public abstract class Entity
|
|||
|
||||
// The rows of `subject`'s schematic as this ship reads it: its own weapons when `subject` is this ship (each row its
|
||||
// own data and live range, nothing to doubt about your own ship), otherwise the revealed weapons (each row the claimed
|
||||
// class and that design's rated range) then the revealed gear (each row the claimed class, no range). The one owner of
|
||||
// which rows, which class and which range an enemy schematic shows; the Unity SchematicDisplay renders it.
|
||||
public IEnumerable<SchematicRow> SchematicRows(Entity subject) =>
|
||||
subject == this
|
||||
? subject.Equipment.Where(i => i.Behaviors.Any(b => b.Data is WeaponData)).Select(i => new SchematicRow(i, i.Data, true))
|
||||
: RevealedWeapons(subject).Concat(RevealedGear(subject)).Select(r => new SchematicRow(r.Item, r.Claimed, false));
|
||||
// class and that design's rated range) then the revealed gear (each row the claimed class, no range), then one mount
|
||||
// row for each mount of the hull this ship believes `subject` to be (LayoutOf) that no claimed-item row sits on
|
||||
// (ruling quiet-gear-identifiable-hull-layout-known). The one owner of which rows, which class and which range an
|
||||
// enemy schematic shows; the Unity SchematicDisplay renders it.
|
||||
public IEnumerable<SchematicRow> SchematicRows(Entity subject)
|
||||
{
|
||||
if (subject == this)
|
||||
return subject.Equipment.Where(i => i.Behaviors.Any(b => b.Data is WeaponData)).Select(i => new SchematicRow(i, i.Data, true));
|
||||
var claimed = RevealedWeapons(subject).Concat(RevealedGear(subject)).Select(r => new SchematicRow(r.Item, r.Claimed, false)).ToList();
|
||||
return claimed.Concat(LayoutOf(subject)
|
||||
.Where(h => !claimed.Any(r => r.Item.Hardpoint == h))
|
||||
.Select(h => new SchematicRow(h)));
|
||||
}
|
||||
|
||||
// Where `subject`'s mounts are as this ship believes them: the hull it is identified as (Contact.Profile, the claimed
|
||||
// hull), never the subject's own HullData; empty below identification. Derived on every read, nothing stored.
|
||||
public IReadOnlyList<HardpointData> LayoutOf(Entity subject) => ContactOf(subject).Profile?.Hardpoints ?? (IReadOnlyList<HardpointData>) Array.Empty<HardpointData>();
|
||||
|
||||
// Whether this observer's book for `subject` holds `item`: the book's membership, no live gear read.
|
||||
internal bool HasReveal(Entity subject, EquippedItem item) =>
|
||||
|
|
|
|||
|
|
@ -156,10 +156,25 @@ public static class FireControl
|
|||
// 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.
|
||||
// every read, never cached; watching a ship reveals no gear. Any aiming choice that reads an item's class or
|
||||
// stats (Combat's target-item choice, its dps) asks this; the selection gate is IsLocated.
|
||||
public static bool IsRevealed(Entity observer, EquippedItem item) =>
|
||||
!item.EquippableItem.IsDestroyed && item.Entity.Equipment.Contains(item) && observer.HasReveal(item.Entity, item);
|
||||
|
||||
// The selection gate (ruling quiet-gear-identifiable-hull-layout-known): `observer` can aim at `item` when it has seen
|
||||
// the item act (IsRevealed's question) or the item sits on a mount of the hull the observer believes its ship to be
|
||||
// (Entity.LayoutOf, by reference: the believed hull's HardpointData are the true item's only while the belief is the
|
||||
// truth, so under a hull lie no gear is located and the ship stays targetable, ruling ew-aim-under-hull-lie-ruled).
|
||||
// Locating reads no class or stats. A Tool item sits on no mount, so only an act locates it.
|
||||
public static bool IsLocated(Entity observer, EquippedItem item) =>
|
||||
!item.EquippableItem.IsDestroyed && item.Entity.Equipment.Contains(item) &&
|
||||
(observer.HasReveal(item.Entity, item) || item.Hardpoint != null && observer.LayoutOf(item.Entity).Contains(item.Hardpoint));
|
||||
|
||||
// Whether the aim at `aimed` is at its mount (located, not revealed): the aim point is then the mount's cells, not
|
||||
// the cells of an item the observer has not learned the size of.
|
||||
public static bool AimsAtMount(Entity observer, EquippedItem aimed) =>
|
||||
aimed != null && aimed.Hardpoint != null && !IsRevealed(observer, aimed);
|
||||
|
||||
// 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
|
||||
// authored unaided figures, which make aiming without one a last resort rather than an alternative.
|
||||
|
|
@ -337,7 +352,7 @@ public static class FireControl
|
|||
Silhouette sil;
|
||||
try
|
||||
{
|
||||
sil = Silhouette(target, targetHull, source.ResolvedTargetItem, bearing, precision, buffer);
|
||||
sil = Silhouette(target, targetHull, source.ResolvedTargetItem, bearing, precision, buffer, AimsAtMount(source, source.ResolvedTargetItem));
|
||||
var settings = source.ItemManager.GameplaySettings;
|
||||
pSpread = PSpread(weapon.Spread, sil.Span, range, settings.SchematicCellSize);
|
||||
}
|
||||
|
|
@ -723,6 +738,7 @@ public static class FireControl
|
|||
Target = engaged,
|
||||
Weapon = item,
|
||||
Aimed = source.ResolvedTargetItem,
|
||||
AimAtMount = AimsAtMount(source, source.ResolvedTargetItem),
|
||||
Damage = damageOverride ?? weapon.Damage,
|
||||
FireMinRange = weapon.MinRange,
|
||||
FireEffectiveRange = weapon.EffectiveRange,
|
||||
|
|
@ -895,7 +911,7 @@ public static class FireControl
|
|||
|
||||
var bearing = Bearing(shot.Target, shot.TravelDirection);
|
||||
var hullData = shot.Target.ItemManager.GetData(shot.Target.Hull) as HullData;
|
||||
sil = Silhouette(shot.Target, hullData, shot.Aimed, bearing, shot.Precision);
|
||||
sil = Silhouette(shot.Target, hullData, shot.Aimed, bearing, shot.Precision, null, shot.AimAtMount);
|
||||
var settings = shot.Target.ItemManager.GameplaySettings;
|
||||
return p * PSpread(shot.Spread, sil.Span, shot.FireRange, settings.SchematicCellSize) * sil.POnHull;
|
||||
}
|
||||
|
|
@ -1763,11 +1779,12 @@ public static class FireControl
|
|||
private static float Sigma(float precision) => max(1f / max(precision, 1e-3f), SigmaFloor);
|
||||
|
||||
// The aim point a silhouette is centred on (R10/Q6: the same frozen Aimed both HitProbability and Fire
|
||||
// read) -- the aimed item's own cell centroid when it currently occupies cells on the target, else the
|
||||
// read) -- the aimed item's own cell centroid when it currently occupies cells on the target (its mount's cells
|
||||
// when the aim was frozen at the mount, atMount: the shooter has not learned what is fitted there), else the
|
||||
// hull's own centre of mass. One path; the uniform-random branch Cut 6d replaced stays deleted.
|
||||
private static float2 ResolveAimPoint(Entity target, HullData hullData, EquippedItem aimed)
|
||||
private static float2 ResolveAimPoint(Entity target, HullData hullData, EquippedItem aimed, bool atMount)
|
||||
{
|
||||
var aimedCells = aimed != null ? CellsOf(target, aimed) : null;
|
||||
var aimedCells = aimed == null ? null : atMount && aimed.Hardpoint != null ? MountCells(aimed.Hardpoint) : CellsOf(target, aimed);
|
||||
if (aimedCells != null && aimedCells.Length > 0)
|
||||
return aimedCells.Aggregate(float2.zero, (total, c) => total + (float2) c) / aimedCells.Length;
|
||||
return hullData.Shape.CenterOfMass;
|
||||
|
|
@ -1787,7 +1804,7 @@ public static class FireControl
|
|||
// call that follows, so it still passes null and gets an owned allocation. A caller-supplied buffer may be
|
||||
// larger than coords.Length (ArrayPool rents "at least"), so every loop below is bounded by coords.Length,
|
||||
// never buffer.Length.
|
||||
public static Silhouette Silhouette(Entity target, HullData hull, EquippedItem aimed, float2 bearing, float precision, Interval[] buffer = null)
|
||||
public static Silhouette Silhouette(Entity target, HullData hull, EquippedItem aimed, float2 bearing, float precision, Interval[] buffer = null, bool atMount = false)
|
||||
{
|
||||
var ell = Lateral(bearing);
|
||||
var coords = hull.Shape.Coordinates;
|
||||
|
|
@ -1809,7 +1826,7 @@ public static class FireControl
|
|||
}
|
||||
|
||||
var sigma = Sigma(precision);
|
||||
var aimPoint = ResolveAimPoint(target, hull, aimed);
|
||||
var aimPoint = ResolveAimPoint(target, hull, aimed, atMount);
|
||||
var a = dot(aimPoint, ell);
|
||||
|
||||
var pOnHull = 0f;
|
||||
|
|
@ -1964,6 +1981,10 @@ public static class FireControl
|
|||
exit = min(exit, max(t1, t2));
|
||||
}
|
||||
|
||||
// The hull-schematic cells a hardpoint covers, laid as Entity builds its Hardpoints[,] grid.
|
||||
private static int2[] MountCells(HardpointData hardpoint) =>
|
||||
hardpoint.Shape.Coordinates.Select(c => hardpoint.Position + c).ToArray();
|
||||
|
||||
// The cells of the target's hull schematic actually occupied by `item` -- GearOccupancy is the one source
|
||||
// of truth for where an equipped item's footprint lands, the same table Detonate and ApplyPooled read.
|
||||
private static int2[] CellsOf(Entity target, EquippedItem item)
|
||||
|
|
@ -1988,6 +2009,9 @@ public struct PendingShot
|
|||
public Entity Target;
|
||||
public EquippedItem Weapon;
|
||||
public EquippedItem Aimed;
|
||||
// Whether the aim was at Aimed's mount rather than the item (FireControl.AimsAtMount at Fire), frozen with it: a
|
||||
// reveal between Fire and Commit does not move where this shot was aimed.
|
||||
public bool AimAtMount;
|
||||
|
||||
// The frozen payload (R10, Q6): the gun's own stats at the instant the trigger was pulled. Nothing
|
||||
// downstream re-evaluates any of these.
|
||||
|
|
|
|||
|
|
@ -127,25 +127,38 @@ public readonly struct Reveal
|
|||
// on the viewer's own ship, the live gear. `Claimed` is the class the row shows. The range label is the live range on
|
||||
// the viewer's own ship and the claimed design's rated range on any other; a row whose claimed class is not a weapon has
|
||||
// no range. The viewer never sees a target item's quality, wear or power. A renderer shows these and decides nothing.
|
||||
// A mount row (`Mount` set, `Item` and `Claimed` null) is a hardpoint of the hull the viewer believes the ship to be, with
|
||||
// nothing of the ship's claimed on it: where something is, not what.
|
||||
public readonly struct SchematicRow
|
||||
{
|
||||
public readonly EquippedItem Item;
|
||||
public readonly EquippableItemData Claimed;
|
||||
public readonly HardpointData Mount;
|
||||
private readonly bool _own;
|
||||
|
||||
internal SchematicRow(EquippedItem item, EquippableItemData claimed, bool own)
|
||||
{
|
||||
Item = item;
|
||||
Claimed = claimed;
|
||||
Mount = null;
|
||||
_own = own;
|
||||
}
|
||||
|
||||
internal SchematicRow(HardpointData mount) : this()
|
||||
{
|
||||
Mount = mount;
|
||||
}
|
||||
|
||||
// What a renderer compares to know the rows changed. Range is a reading, not part of the key.
|
||||
public (EquippedItem, EquippableItemData) Key => (Item, Claimed);
|
||||
public (EquippedItem, EquippableItemData, HardpointData) Key => (Item, Claimed, Mount);
|
||||
|
||||
// Whether this row is the one `aimed` points at: a claimed-item row by its item, a mount row by the mount the aimed
|
||||
// item sits on (an aimed item with a claimed-item row has no mount row).
|
||||
public bool Marks(EquippedItem aimed) => Item != null ? Item == aimed : aimed != null && aimed.Hardpoint == Mount;
|
||||
|
||||
public float? Range => _own
|
||||
? Item.Behaviors.OfType<Weapon>().First().Range
|
||||
: Claimed.Behaviors.OfType<WeaponData>().FirstOrDefault()?.RatedRange;
|
||||
: Claimed?.Behaviors.OfType<WeaponData>().FirstOrDefault()?.RatedRange;
|
||||
}
|
||||
|
||||
// A subject's removal from a zone as the zone recorded it (death, dock, warp, any removal): where it was and how
|
||||
|
|
|
|||
|
|
@ -123,7 +123,9 @@ public class SchematicDisplay : MonoBehaviour
|
|||
.ToArray();
|
||||
foreach (var x in _schematicItems)
|
||||
{
|
||||
if(x.Row.Claimed is WeaponItemData weaponItemData)
|
||||
if (x.Row.Mount != null)
|
||||
x.ListElement.ShowGear(x.Row.Mount.Type, null);
|
||||
else if(x.Row.Claimed is WeaponItemData weaponItemData)
|
||||
x.ListElement.ShowWeapon(weaponItemData);
|
||||
else
|
||||
x.ListElement.ShowGear(x.Row.Claimed.HardpointType, x.Row.Claimed.Name);
|
||||
|
|
@ -208,7 +210,7 @@ public class SchematicDisplay : MonoBehaviour
|
|||
// as info decays, with no event to hook.
|
||||
if (_enemy && _schematicItems != null)
|
||||
foreach (var x in _schematicItems)
|
||||
x.ListElement.SetSelected(x.Row.Item == _player.ResolvedTargetItem);
|
||||
x.ListElement.SetSelected(x.Row.Marks(_player.ResolvedTargetItem));
|
||||
|
||||
if(VisibilityLabel && !_enemy)
|
||||
VisibilityLabel.text = ((int)_entity.Visibility).ToString();
|
||||
|
|
|
|||
|
|
@ -53,6 +53,42 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(2, 0), Shape = new Shape() }
|
||||
}
|
||||
});
|
||||
// Layout-aim: a hull whose mounts are bigger than what is fitted to them. The reactor mount covers the 2x2 block at
|
||||
// (3,3), off the origin and against the hull's far edge, while the reactor is the one cell at (3,3), so the mount's
|
||||
// centre (3.5, 3.5) is not the reactor's (3, 3) and the two price a shot apart.
|
||||
var bayBlock = new Shape(2, 2);
|
||||
foreach (var cell in bayBlock.AllCoordinates) bayBlock[cell] = true;
|
||||
var bayHull = new Shape(5, 5);
|
||||
foreach (var cell in bayHull.AllCoordinates) bayHull[cell] = true;
|
||||
cache.Upsert(new HullData
|
||||
{
|
||||
Name = "Bay", HullType = HullType.Ship, Shape = bayHull, Durability = 1000000, Mass = 1000,
|
||||
Hardpoints =
|
||||
{
|
||||
new HardpointData { Type = HardpointType.Reactor, Position = new int2(3, 3), Shape = bayBlock },
|
||||
new HardpointData { Type = HardpointType.Shield, Position = new int2(3, 0), Shape = new Shape() },
|
||||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 3), Shape = new Shape() }
|
||||
}
|
||||
});
|
||||
// Layout-aim: a hull off the diagonal with two mounts of one type. The first Sensors mount covers (0,0) and (1,0), the
|
||||
// second (2,3) and (3,3): a transposed position gives another cell, and a rule that matches mounts by type cannot
|
||||
// tell the two apart.
|
||||
var wingMount = new Shape(2, 1);
|
||||
foreach (var cell in wingMount.AllCoordinates) wingMount[cell] = true;
|
||||
var wingHull = new Shape(5, 5);
|
||||
foreach (var cell in wingHull.AllCoordinates) wingHull[cell] = true;
|
||||
cache.Upsert(new HullData
|
||||
{
|
||||
Name = "Wing", HullType = HullType.Ship, Shape = wingHull, Durability = 1000000, Mass = 1000,
|
||||
Hardpoints =
|
||||
{
|
||||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = wingMount },
|
||||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(2, 3), Shape = wingMount }
|
||||
}
|
||||
});
|
||||
var bayReactor = Tool("BayReactor", new ReactorData { Charge = Const(1000), Efficiency = Const(1), OverloadEfficiency = Const(1), ThrottlingFactor = Const(2) });
|
||||
bayReactor.Hardpoint = HardpointType.Reactor;
|
||||
cache.Upsert(bayReactor);
|
||||
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
|
||||
|
|
@ -1693,14 +1729,6 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
Assert.Equal(0L, smallest);
|
||||
}
|
||||
|
||||
// The observer's stored decision about a subject (SetIff), read as the entity holds it: a neutral decision (false) is also
|
||||
// what an unset stance derives to here, so IsHostileTo alone cannot tell it kept from dropped.
|
||||
private static bool? StoredStance(Entity observer, Entity subject)
|
||||
{
|
||||
var overrides = (System.Collections.IDictionary) typeof(Entity).GetField("_iffOverrides", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(observer);
|
||||
return overrides.Contains(subject) ? (bool) overrides[subject] : (bool?) null;
|
||||
}
|
||||
|
||||
// A docked subject leaves the zone parented to its dock and rejoins on undock: the observer's decision about it, hostile or
|
||||
// neutral, outlives the lapse of the bearing, and is still there when it rejoins.
|
||||
[Theory]
|
||||
|
|
@ -1711,8 +1739,13 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
var e = Add(float3(0, 0, 100), new[] { "Gun" });
|
||||
var dock = Add(float3(200, 0, 0));
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
// The observer's faction owns the zone and the subject's presence is not permitted, so the stance derived for an
|
||||
// undecided subject is hostile: a neutral decision (false) that is dropped would read hostile, and is seen to be kept.
|
||||
_zone.GalaxyZone.Owner = o.Faction;
|
||||
e.CurrentSecurityLevel.Value = SecurityLevel.Critical;
|
||||
Assert.True(o.IsHostileTo(e), "fixture: an undecided subject derives hostile");
|
||||
o.SetIff(e, hostile);
|
||||
Assert.Equal(hostile, StoredStance(o, e)); // fixture: the decision is stored
|
||||
Assert.Equal(hostile, o.IsHostileTo(e)); // fixture: the decision is stored
|
||||
var gun = Guns(e)[0];
|
||||
Fire(e, gun);
|
||||
ShowOnly(e, 1f, gun);
|
||||
|
|
@ -1721,11 +1754,9 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
_zone.Remove(e);
|
||||
for (var i = 0; i < 100 && o.RevealedSubjects.Contains(e); i++) Tick(.1f);
|
||||
Assert.False(o.RevealedSubjects.Contains(e), "fixture: the book dropped the docked subject");
|
||||
Assert.Equal(hostile, StoredStance(o, e)); // a docked subject keeps the observer's decision
|
||||
Assert.Equal(hostile, o.IsHostileTo(e));
|
||||
e.Parent = null;
|
||||
_zone.Admit(e, piloted: false);
|
||||
Assert.Equal(hostile, StoredStance(o, e)); // and it is still there when the subject rejoins
|
||||
Assert.Equal(hostile, o.IsHostileTo(e));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -562,7 +562,7 @@ public sealed class FireControlCut123Tests : IDisposable
|
|||
var outcome = FireUntilHit(e, damageOverride: 5f);
|
||||
Assert.NotNull(outcome);
|
||||
Assert.True(outcome.Hit);
|
||||
Assert.Same(e.Shooter, source);
|
||||
Assert.True(ReferenceEquals(e.Shooter, source), "the hit names the shooter");
|
||||
}
|
||||
|
||||
// Stryker survivors: FireControl.Apply's two `walked[li] > 0` checks (metalLanes counting, and the
|
||||
|
|
|
|||
|
|
@ -266,8 +266,8 @@ public sealed class FireControlCut8Tests : IDisposable
|
|||
|
||||
s.Locker.Deactivate();
|
||||
Assert.False(s.Locker.Active);
|
||||
Assert.DoesNotContain(s.Bystander, s.Locker.EntityInfoGathered.Keys);
|
||||
Assert.Same(s.Bystander, s.Locker.Target.Value.Entity); // Deactivate does not null the entity's own Target
|
||||
Assert.False(s.Locker.EntityInfoGathered.Keys.Any(k => ReferenceEquals(k, s.Bystander)), "the bystander is dropped from the locker's info");
|
||||
Assert.True(ReferenceEquals(s.Bystander, s.Locker.Target.Value.Entity), "the target is kept"); // Deactivate does not null the entity's own Target
|
||||
|
||||
var ex = Record.Exception(() => s.Locker.Update(.1f));
|
||||
Assert.Null(ex);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,19 @@ public sealed class InspectionScannerCatalogTests
|
|||
Assert.Equal(exponent, term.Exponent, 4);
|
||||
}
|
||||
|
||||
// roles-backfill's map for the design (CB-R1): each figure reads the role it is authored on, so a lot's roles move the
|
||||
// figures the catalog test below reads. Dropping the entry or mapping a figure to the wrong role fails here.
|
||||
[Fact]
|
||||
public void RolesBackfillMapsTheInspectionScannersFiguresToTheirRoles()
|
||||
{
|
||||
using var cache = RestoredHullsTests.OpenCatalog();
|
||||
var map = Program.RoleMapOf(Design(cache));
|
||||
Assert.True(map != null, "roles-backfill finds no role map for the Inspection Scanner design");
|
||||
Assert.Equal(
|
||||
new[] { ("InterceptFactor", Discipline), ("Range", ScanArray), ("ScanTime", ScanArray) },
|
||||
map.Select(p => (p.Key, p.Value)).OrderBy(p => p.Key, StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheCatalogSellsAnInspectionScanner()
|
||||
{
|
||||
|
|
|
|||
381
tests/Aetheria.Shared.Tests/LayoutAimTests.cs
Normal file
381
tests/Aetheria.Shared.Tests/LayoutAimTests.cs
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
using int2 = CultMath.int2;
|
||||
|
||||
// Visibility 5b (rulings quiet-gear-identifiable-hull-layout-known, ew-aim-under-hull-lie-ruled, reveal-ends-on-perception,
|
||||
// schematic-and-model-are-separate-spaces): an observer that identifies a hull knows where its mounts are, and can aim at a
|
||||
// mount it has located without knowing what is fitted there. The fixture hull "Bay" has a reactor mount (the 2x2 block at
|
||||
// (3,3), the reactor the one cell at (3,3) inside it), a shield mount and a weapon mount, and nothing ever acts unless a test
|
||||
// makes it. Identified is TargetGearInfoThreshold (.8) of the observer's info on the ship. Every fixture drives the
|
||||
// production path (Fire, CommitProbability, SchematicRows, TrySelectTargetItem, the AI's Combat state inside Zone.Update).
|
||||
// Entities and items are compared with ReferenceEquals: a failing xunit assertion formats its arguments, and an Entity's
|
||||
// graph overflows the test host.
|
||||
public sealed partial class ContactRevealTests
|
||||
{
|
||||
private const float Identify = .8f;
|
||||
|
||||
// A Bay at `position`: its reactor on the reactor mount, a gun on the weapon mount when asked (with the targeting system
|
||||
// and reactor tool it needs to fire), and `tools` on free cells.
|
||||
private Ship NewShip(string hull)
|
||||
{
|
||||
var hullRef = _items.ItemData.RefOf<ItemData>(_items.ItemData.GetByName<HullData>(hull));
|
||||
return new Ship(_items, _zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = _nextLot++ }, new EntitySettings())
|
||||
{ Faction = new Faction { Name = "F" + Guid.NewGuid().ToString("N") } };
|
||||
}
|
||||
|
||||
private Ship Place(Ship ship, float3 position)
|
||||
{
|
||||
_zone.Admit(ship, piloted: false);
|
||||
ship.Position = position;
|
||||
ship.Aim = float3(0, 0, 1);
|
||||
_zone.Update(0f);
|
||||
return ship;
|
||||
}
|
||||
|
||||
private Ship AddBay(float3 position, bool gun = false, string[] tools = null)
|
||||
{
|
||||
var ship = NewShip("Bay");
|
||||
Assert.True(ship.TryEquip(Make("BayReactor"), new int2(3, 3)));
|
||||
if (gun) Assert.True(ship.TryEquip(Make("Gun"), new int2(0, 3)));
|
||||
foreach (var name in (tools ?? new string[0]).Concat(gun ? new[] { "Targeting", "Reactor" } : new string[0]))
|
||||
{
|
||||
var item = Make(name);
|
||||
Assert.True(ship.TryFindSpace(item, out var at));
|
||||
Assert.True(ship.TryEquip(item, at));
|
||||
}
|
||||
return Place(ship, position);
|
||||
}
|
||||
|
||||
// A Wing at `position` with a gun on each of its two Sensors mounts: gunA on the mount at (0,0), gunB at (2,3).
|
||||
private (Ship wing, EquippedItem gunA, EquippedItem gunB) AddWing(float3 position)
|
||||
{
|
||||
var ship = NewShip("Wing");
|
||||
Assert.True(ship.TryEquip(Make("Gun"), new int2(0, 0)));
|
||||
Assert.True(ship.TryEquip(Make("Gun"), new int2(2, 3)));
|
||||
Place(ship, position);
|
||||
var gunA = ship.Equipment.Single(i => i.Data.Name == "Gun" && i.Position.x == 0);
|
||||
var gunB = ship.Equipment.Single(i => i.Data.Name == "Gun" && i.Position.x == 2);
|
||||
Assert.True(gunA.Hardpoint != null && gunB.Hardpoint != null && !ReferenceEquals(gunA.Hardpoint, gunB.Hardpoint), "fixture: each gun sits on its own mount");
|
||||
return (ship, gunA, gunB);
|
||||
}
|
||||
|
||||
private static EquippedItem Fitted(Entity e, string design) => e.Equipment.Single(i => i.Data.Name == design);
|
||||
|
||||
// An observer at the origin that identifies a Bay 100 ahead (nothing revealed, nothing targeted).
|
||||
private (Ship bay, Ship observer, EquippedItem reactor) Identified(float info = 1f)
|
||||
{
|
||||
var bay = AddBay(float3(0, 0, 100));
|
||||
var observer = Add(float3(0, 0, 0));
|
||||
observer.EntityInfoGathered[bay] = info;
|
||||
return (bay, observer, Fitted(bay, "BayReactor"));
|
||||
}
|
||||
|
||||
private static void Target(Entity observer, Entity subject)
|
||||
{
|
||||
observer.SetTarget(subject);
|
||||
Assert.True(ReferenceEquals(subject, observer.Target.Value.Entity), "fixture: the observer targets the ship");
|
||||
}
|
||||
|
||||
// ---- AnIdentifiedHullShowsItsMounts ----
|
||||
|
||||
// At the identify threshold the observer's layout of the ship is its hull's hardpoints; just below it is empty.
|
||||
[Fact]
|
||||
public void AnIdentifiedHullShowsItsMounts()
|
||||
{
|
||||
var (bay, observer, _) = Identified(Identify);
|
||||
Assert.Equal(3, bay.HullData.Hardpoints.Count);
|
||||
Assert.True(bay.HullData.Hardpoints.SequenceEqual(observer.LayoutOf(bay)), "the layout is the hull's three hardpoints, in order");
|
||||
|
||||
observer.EntityInfoGathered[bay] = Identify - .01f;
|
||||
Assert.Empty(observer.LayoutOf(bay));
|
||||
}
|
||||
|
||||
// ---- TheReactorIsLocatedNotRevealed ----
|
||||
|
||||
// Identified, the reactor never acted: the observer has located it (can aim at it) and learned nothing of it. Below the
|
||||
// identify threshold it can aim at nothing, and a mount aim already taken is dropped when the identification is lost.
|
||||
[Fact]
|
||||
public void TheReactorIsLocatedNotRevealed()
|
||||
{
|
||||
var (bay, observer, reactor) = Identified(Identify - .01f);
|
||||
Target(observer, bay);
|
||||
Assert.False(FireControl.IsLocated(observer, reactor));
|
||||
Assert.False(observer.TrySelectTargetItem(reactor));
|
||||
|
||||
observer.EntityInfoGathered[bay] = Identify;
|
||||
Assert.True(FireControl.IsLocated(observer, reactor));
|
||||
Assert.False(FireControl.IsRevealed(observer, reactor));
|
||||
Assert.True(observer.TrySelectTargetItem(reactor));
|
||||
Assert.True(ReferenceEquals(reactor, observer.ResolvedTargetItem), "the located reactor is the aim point");
|
||||
|
||||
observer.EntityInfoGathered[bay] = Identify - .01f;
|
||||
Assert.True(observer.ResolvedTargetItem == null, "the aim is dropped with the identification");
|
||||
}
|
||||
|
||||
// ---- ADestroyedMountItemIsNotLocated ----
|
||||
|
||||
[Fact]
|
||||
public void ADestroyedMountItemIsNotLocated()
|
||||
{
|
||||
var (bay, observer, reactor) = Identified();
|
||||
Target(observer, bay);
|
||||
Assert.True(observer.TrySelectTargetItem(reactor), "fixture: the live reactor is located and aimed at");
|
||||
|
||||
reactor.EquippableItem.Durability = 0f;
|
||||
Assert.True(reactor.EquippableItem.IsDestroyed, "fixture");
|
||||
Assert.False(FireControl.IsLocated(observer, reactor));
|
||||
Assert.True(observer.ResolvedTargetItem == null);
|
||||
Assert.False(observer.TrySelectTargetItem(reactor));
|
||||
}
|
||||
|
||||
// ---- UnderAHullLieNoGearIsLocated ----
|
||||
|
||||
// Ruling ew-aim-under-hull-lie-ruled: while the lie is believed the observer's layout is the claimed hull's and the true
|
||||
// reactor sits on none of its mounts, so no gear can be aimed at and the ship stays targetable; gear aim opens when the
|
||||
// lie is seen through.
|
||||
[Fact]
|
||||
public void UnderAHullLieNoGearIsLocated()
|
||||
{
|
||||
var (bay, observer, reactor) = Identified();
|
||||
var decoy = new HullData { Name = "Decoy", Hardpoints = { new HardpointData { Type = HardpointType.Reactor, Position = new int2(0, 0), Shape = new Shape() } } };
|
||||
bay.Claim(decoy);
|
||||
|
||||
Assert.True(decoy.Hardpoints.SequenceEqual(observer.LayoutOf(bay)) && observer.LayoutOf(bay).Count == 1, "the layout is the claimed hull's");
|
||||
Target(observer, bay);
|
||||
Assert.False(FireControl.IsLocated(observer, reactor));
|
||||
Assert.False(observer.TrySelectTargetItem(reactor));
|
||||
Assert.True(observer.ResolvedTargetItem == null);
|
||||
Assert.True(ReferenceEquals(bay, observer.Target.Value.Entity), "the ship stays targetable");
|
||||
|
||||
bay.Claim(null);
|
||||
Assert.True(FireControl.IsLocated(observer, reactor));
|
||||
Assert.True(observer.TrySelectTargetItem(reactor));
|
||||
}
|
||||
|
||||
// The lie is a lookalike: the claimed hull is of the true hull's type and size, with a reactor mount of the same type,
|
||||
// position and shape as the true one, and a shield mount elsewhere. What the observer knows is the claimed layout, mount
|
||||
// object by mount object; nothing of the true hull's layout leaks in because the two are alike.
|
||||
[Fact]
|
||||
public void UnderALookalikeLieNoGearIsLocated()
|
||||
{
|
||||
var (bay, observer, reactor) = Identified();
|
||||
var truth = bay.HullData;
|
||||
var lookalike = new HullData
|
||||
{
|
||||
Name = "Lookalike", HullType = truth.HullType, Shape = new Shape(truth.Shape.Width, truth.Shape.Height),
|
||||
Hardpoints =
|
||||
{
|
||||
new HardpointData { Type = HardpointType.Reactor, Position = truth.Hardpoints[0].Position, Shape = truth.Hardpoints[0].Shape },
|
||||
new HardpointData { Type = HardpointType.Shield, Position = new int2(3, 1), Shape = new Shape() }
|
||||
}
|
||||
};
|
||||
bay.Claim(lookalike);
|
||||
Assert.True(truth.Shape.Width == lookalike.Shape.Width && truth.Shape.Height == lookalike.Shape.Height && truth.HullType == lookalike.HullType, "fixture: the two hulls are one size and type");
|
||||
Assert.True(lookalike.Hardpoints[0].Type == reactor.Hardpoint.Type && lookalike.Hardpoints[0].Position.Equals(reactor.Hardpoint.Position), "fixture: the claimed reactor mount looks like the true one");
|
||||
Assert.False(ReferenceEquals(lookalike.Hardpoints[0], reactor.Hardpoint), "fixture: and is not it");
|
||||
|
||||
Assert.True(lookalike.Hardpoints.SequenceEqual(observer.LayoutOf(bay)), "the layout is the claimed hull's, not the true hull's");
|
||||
Target(observer, bay);
|
||||
Assert.False(FireControl.IsLocated(observer, reactor));
|
||||
Assert.False(observer.TrySelectTargetItem(reactor));
|
||||
Assert.True(ReferenceEquals(bay, observer.Target.Value.Entity), "the ship stays targetable");
|
||||
Assert.Equal(2, observer.SchematicRows(bay).Count(r => r.Mount != null));
|
||||
}
|
||||
|
||||
// ---- ToolGearIsNotLocatedByLayout ----
|
||||
|
||||
// Tool gear sits on no mount, so the layout locates nothing of it; an act does.
|
||||
[Fact]
|
||||
public void ToolGearIsNotLocatedByLayout()
|
||||
{
|
||||
var bay = AddBay(float3(0, 0, 100), tools: new[] { "Radiator" });
|
||||
var observer = Add(float3(0, 0, 0));
|
||||
observer.EntityInfoGathered[bay] = 1f;
|
||||
var radiator = Fitted(bay, "Radiator");
|
||||
Assert.True(radiator.Hardpoint == null, "fixture: a tool item sits on no mount");
|
||||
Target(observer, bay);
|
||||
|
||||
Assert.False(FireControl.IsLocated(observer, radiator));
|
||||
Assert.False(observer.TrySelectTargetItem(radiator));
|
||||
|
||||
observer.RecordReveal(bay, radiator, SignatureChannel.LaunchFlash, radiator.Data);
|
||||
Assert.True(FireControl.IsLocated(observer, radiator));
|
||||
}
|
||||
|
||||
// ---- AMountAimIsTheMountsCells ----
|
||||
|
||||
// A shooter that holds a valid solution on a Bay 30 ahead, with the reactor selected as the aim point.
|
||||
private (Ship shooter, Ship bay, EquippedItem reactor, Weapon weapon, EquippedItem gun) Gunner()
|
||||
{
|
||||
var bay = AddBay(float3(0, 0, 30));
|
||||
return GunnerOn(bay, Fitted(bay, "BayReactor"));
|
||||
}
|
||||
|
||||
// A shooter that holds a valid solution on `bay`, with `aimed` (fitted to it) selected as the aim point.
|
||||
private (Ship shooter, Ship bay, EquippedItem reactor, Weapon weapon, EquippedItem gun) GunnerOn(Ship bay, EquippedItem aimed)
|
||||
{
|
||||
var shooter = Add(float3(0, 0, 0), new[] { "Eye", "Gun" });
|
||||
shooter.SetTarget(bay);
|
||||
for (var i = 0; i < 3; i++) Hold(shooter, 1, 1f, bay);
|
||||
Assert.True(shooter.VisibleEntities.Contains(bay), "fixture: the shooter tracks the ship");
|
||||
var gun = Guns(shooter).Single();
|
||||
Assert.True(shooter.TrySelectTargetItem(aimed), "fixture: the located item is aimed at");
|
||||
return (shooter, bay, aimed, (Weapon) gun.Behaviors.Single(b => b is Weapon), gun);
|
||||
}
|
||||
|
||||
private PendingShot FireAt(Weapon weapon, EquippedItem gun, Entity shooter)
|
||||
{
|
||||
var before = _zone.PendingShots.Count;
|
||||
FireControl.Fire(weapon, gun, shooter);
|
||||
Assert.Equal(before + 1, _zone.PendingShots.Count);
|
||||
return _zone.PendingShots[before];
|
||||
}
|
||||
|
||||
// The reactor is one cell inside its two by two mount. Aimed at while located, a shot centres on the mount's cells (3.5, 3.5);
|
||||
// once the reactor is revealed the next shot centres on the reactor's own cell (3, 3). The HUD's forecast reads the same
|
||||
// aim point as the shot it forecasts.
|
||||
[Fact]
|
||||
public void AMountAimIsTheMountsCells()
|
||||
{
|
||||
var (shooter, bay, reactor, weapon, gun) = Gunner();
|
||||
|
||||
var located = FireControl.Inspect(weapon, shooter, bay);
|
||||
var shot = FireAt(weapon, gun, shooter);
|
||||
Assert.True(shot.AimAtMount && ReferenceEquals(reactor, shot.Aimed), "the shot is aimed at the located reactor's mount");
|
||||
Assert.True(FireControl.CommitProbability(shot, _zone.Time, out var mountSil) > 0f, "fixture: the shot has a price");
|
||||
Assert.Equal(3.5f, mountSil.AimPoint.x, 4);
|
||||
Assert.Equal(3.5f, mountSil.AimPoint.y, 4);
|
||||
Assert.Equal(mountSil.POnHull, located.POnHull, 4);
|
||||
|
||||
shooter.RecordReveal(bay, reactor, SignatureChannel.LaunchFlash, reactor.Data);
|
||||
Assert.True(FireControl.IsRevealed(shooter, reactor), "fixture: the reactor is revealed");
|
||||
var revealed = FireControl.Inspect(weapon, shooter, bay);
|
||||
var next = FireAt(weapon, gun, shooter);
|
||||
Assert.False(next.AimAtMount);
|
||||
Assert.True(FireControl.CommitProbability(next, _zone.Time, out var itemSil) > 0f, "fixture: the shot has a price");
|
||||
Assert.Equal(3f, itemSil.AimPoint.x, 4);
|
||||
Assert.Equal(3f, itemSil.AimPoint.y, 4);
|
||||
Assert.Equal(itemSil.POnHull, revealed.POnHull, 4);
|
||||
Assert.True(Math.Abs(mountSil.POnHull - itemSil.POnHull) > 1e-3f, "fixture: the two aim points are priced apart");
|
||||
}
|
||||
|
||||
// The mount is a rectangle off the diagonal: the second Sensors mount covers (2,3) and (3,3), centre (2.5, 3). A position
|
||||
// read with its axes swapped would centre the shot on (3.5, 2).
|
||||
[Fact]
|
||||
public void AMountAimReadsThePositionAxesInOrder()
|
||||
{
|
||||
var (wing, _, gunB) = AddWing(float3(0, 0, 30));
|
||||
var (shooter, _, _, weapon, gun) = GunnerOn(wing, gunB);
|
||||
var shot = FireAt(weapon, gun, shooter);
|
||||
Assert.True(shot.AimAtMount && ReferenceEquals(gunB, shot.Aimed), "fixture: the shot is aimed at the located gun's mount");
|
||||
Assert.True(FireControl.CommitProbability(shot, _zone.Time, out var sil) > 0f, "fixture: the shot has a price");
|
||||
Assert.Equal(2.5f, sil.AimPoint.x, 4);
|
||||
Assert.Equal(3f, sil.AimPoint.y, 4);
|
||||
}
|
||||
|
||||
// ---- TheShotKeepsTheAimItWasFiredWith ----
|
||||
|
||||
[Fact]
|
||||
public void TheShotKeepsTheAimItWasFiredWith()
|
||||
{
|
||||
var (shooter, bay, reactor, weapon, gun) = Gunner();
|
||||
var shot = FireAt(weapon, gun, shooter);
|
||||
Assert.True(shot.AimAtMount, "fixture: fired at the mount");
|
||||
|
||||
shooter.RecordReveal(bay, reactor, SignatureChannel.LaunchFlash, reactor.Data);
|
||||
Assert.True(FireControl.IsRevealed(shooter, reactor), "fixture: revealed before Commit");
|
||||
|
||||
Assert.True(FireControl.CommitProbability(shot, _zone.Time, out var sil) > 0f, "fixture: the shot has a price");
|
||||
Assert.Equal(3.5f, sil.AimPoint.x, 4);
|
||||
Assert.Equal(3.5f, sil.AimPoint.y, 4);
|
||||
}
|
||||
|
||||
// ---- TheSchematicListsUnfilledMounts ----
|
||||
|
||||
// Identified, nothing revealed: a mount row for each hardpoint, naming no item and no class and giving no range. After a
|
||||
// scan the reactor's mount row gives way to its gear row. The aimed reactor is marked on whichever row stands for it, and
|
||||
// no other row is.
|
||||
[Fact]
|
||||
public void TheSchematicListsUnfilledMounts()
|
||||
{
|
||||
var (bay, observer, reactor) = Identified(Identify - .01f);
|
||||
var hardpoints = bay.HullData.Hardpoints;
|
||||
Assert.Empty(observer.SchematicRows(bay));
|
||||
|
||||
observer.EntityInfoGathered[bay] = Identify;
|
||||
var rows = observer.SchematicRows(bay).ToArray();
|
||||
Assert.Equal(3, rows.Length);
|
||||
Assert.Equal(3, rows.Select(r => r.Key).Distinct().Count());
|
||||
Assert.True(hardpoints.SequenceEqual(rows.Select(r => r.Mount)), "one mount row per hardpoint, in order");
|
||||
Assert.All(rows, r =>
|
||||
{
|
||||
Assert.True(r.Item == null && r.Claimed == null, "a mount row names no item and no class");
|
||||
Assert.False(r.Range.HasValue);
|
||||
});
|
||||
|
||||
Target(observer, bay);
|
||||
Assert.True(observer.TrySelectTargetItem(reactor));
|
||||
Assert.Equal(new[] { true, false, false }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
|
||||
observer.RecordInspection(bay);
|
||||
rows = observer.SchematicRows(bay).ToArray();
|
||||
Assert.Equal(3, rows.Length);
|
||||
Assert.Equal(3, rows.Select(r => r.Key).Distinct().Count());
|
||||
Assert.True(rows[0].Mount == null && ReferenceEquals(rows[0].Item, reactor), "the reactor's gear row leads");
|
||||
Assert.True(hardpoints.Skip(1).SequenceEqual(rows.Skip(1).Select(r => r.Mount)), "the other two mounts stay mount rows");
|
||||
Assert.Equal(new[] { true, false, false }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
}
|
||||
|
||||
// Two mounts of one type: the rows tell them apart by mount, not by type. Aiming at one marks its row alone, and revealing
|
||||
// the other gives way only that mount's row to its gear row, leaving this one a mount row.
|
||||
[Fact]
|
||||
public void TwoMountsOfOneTypeAreTwoRows()
|
||||
{
|
||||
var (wing, gunA, gunB) = AddWing(float3(0, 0, 100));
|
||||
var observer = Add(float3(0, 0, 0));
|
||||
observer.EntityInfoGathered[wing] = 1f;
|
||||
var mountA = gunA.Hardpoint;
|
||||
var mountB = gunB.Hardpoint;
|
||||
Target(observer, wing);
|
||||
Assert.True(observer.TrySelectTargetItem(gunB));
|
||||
|
||||
var rows = observer.SchematicRows(wing).ToArray();
|
||||
Assert.Equal(2, rows.Length);
|
||||
Assert.True(ReferenceEquals(mountA, rows[0].Mount) && ReferenceEquals(mountB, rows[1].Mount), "one mount row per mount, in order");
|
||||
Assert.Equal(new[] { false, true }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
|
||||
observer.RecordReveal(wing, gunA, SignatureChannel.LaunchFlash, gunA.Data);
|
||||
rows = observer.SchematicRows(wing).ToArray();
|
||||
Assert.Equal(2, rows.Length);
|
||||
Assert.True(ReferenceEquals(rows[0].Item, gunA) && rows[0].Mount == null, "the revealed gun's row leads");
|
||||
Assert.True(ReferenceEquals(rows[1].Mount, mountB) && rows[1].Item == null, "the other mount of the same type stays a mount row");
|
||||
Assert.Equal(new[] { false, true }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
}
|
||||
|
||||
// ---- TheAIStillChoosesOnlyRevealedWeapons ----
|
||||
|
||||
// An AI ship that identifies a Bay with a gun on its weapon mount aims at no item: Combat chooses among the weapons it has
|
||||
// seen, and the located gun is not one. Once the gun fires in sight, Combat selects it.
|
||||
[Fact]
|
||||
public void TheAIStillChoosesOnlyRevealedWeapons()
|
||||
{
|
||||
var bay = AddBay(float3(0, 0, 40), gun: true);
|
||||
var gun = Guns(bay).Single();
|
||||
var ai = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
_zone.Agents.Add(new Minion(ai));
|
||||
ai.SetTarget(bay);
|
||||
Hold(ai, 12, 1f, bay);
|
||||
Assert.True(ReferenceEquals(bay, ai.Target.Value.Entity), "fixture: the AI keeps the ship as its target");
|
||||
Assert.True(FireControl.IsLocated(ai, gun) && !FireControl.IsRevealed(ai, gun), "fixture: the gun is located and not revealed");
|
||||
Assert.True(ai.ResolvedTargetItem == null, "Combat aims at nothing it has not seen act");
|
||||
|
||||
Fire(bay, gun);
|
||||
for (var i = 0; i < 40 && ai.ResolvedTargetItem == null; i++) Hold(ai, 1, 1f, bay);
|
||||
Assert.True(ReferenceEquals(gun, ai.ResolvedTargetItem), "after the gun fires Combat aims at it");
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ public sealed partial class RunStartTests
|
|||
// The first real sensing tick: the hostile holds no track of the player, and the sim's own passive sensing, asked for
|
||||
// the player's signature at the staged place, settles below the detection threshold: the staging is out of its sight.
|
||||
arena.Update(.1f);
|
||||
Assert.DoesNotContain(player, hostile.VisibleEnemies);
|
||||
Assert.False(hostile.VisibleEnemies.Any(x => ReferenceEquals(x, player)), "the player is out of sight");
|
||||
Assert.DoesNotContain(player.IncomingPaints, paint => paint.Mode == PaintMode.Track);
|
||||
Assert.True(player.Visibility > 0f, "the player shows a signature to be seen by");
|
||||
var threshold = player.ItemManager.GameplaySettings.TargetDetectionInfoThreshold;
|
||||
|
|
|
|||
|
|
@ -224,14 +224,14 @@ public sealed class LockWarningTests : IDisposable
|
|||
var locker = NewShip(float3(0, 0, 0), "Launcher");
|
||||
var b = NewShip(float3(0, 0, 9));
|
||||
b.EntityInfoGathered[locker] = 0;
|
||||
Assert.DoesNotContain(locker, b.VisibleEnemies);
|
||||
Assert.False(b.VisibleEnemies.Any(x => ReferenceEquals(x, locker)), "the locker is not a visible enemy");
|
||||
|
||||
Aim(locker, b);
|
||||
Tick(.5f);
|
||||
Tick(0f);
|
||||
|
||||
Assert.Single(b.IncomingPaints);
|
||||
Assert.DoesNotContain(locker, b.VisibleEnemies);
|
||||
Assert.False(b.VisibleEnemies.Any(x => ReferenceEquals(x, locker)), "the locker is not a visible enemy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -431,7 +431,7 @@ public sealed class LootTests : IDisposable
|
|||
Kill(ship);
|
||||
var count = lab.Zone.FloatingItems.Count;
|
||||
Assert.NotEmpty(lab.Zone.FloatingItems);
|
||||
Assert.DoesNotContain(ship, lab.Zone.Entities);
|
||||
Assert.False(lab.Zone.Entities.Any(x => ReferenceEquals(x, ship)), "the ship is removed from the zone");
|
||||
// A second death signal from the removed entity reaches no subscription.
|
||||
Kill(ship);
|
||||
Assert.Equal(count, lab.Zone.FloatingItems.Count);
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@ public sealed class MineTests : IDisposable
|
|||
lab.Layer.Position = float3(5000, 0, 5000);
|
||||
foreach (var step in Steps(1f, .1f)) lab.Zone.Update(step);
|
||||
lab.Layer.DamageHull(5000000f);
|
||||
Assert.DoesNotContain(lab.Layer, lab.Zone.Entities);
|
||||
Assert.False(lab.Zone.Entities.Any(x => ReferenceEquals(x, lab.Layer)), "the layer is removed from the zone");
|
||||
foreach (var step in Steps(1.5f, .1f)) lab.Zone.Update(step);
|
||||
|
||||
Assert.Equal(0, removed);
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ public sealed partial class MiningCut3Tests
|
|||
Assert.Empty(candidates);
|
||||
s.Zone.Targets.Within(new TargetSearch(here, 500f), candidates);
|
||||
Assert.Empty(candidates);
|
||||
Assert.DoesNotContain(unseen, observer.VisibleEntities);
|
||||
Assert.False(observer.VisibleEntities.Any(x => ReferenceEquals(x, unseen)), "the unseen entity is not visible");
|
||||
}
|
||||
|
||||
private readonly struct NearestChunk : ITargetKey
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ public sealed partial class RunStartTests
|
|||
|
||||
// Death removes the boss from the zone (the one death path), and that alone opens the gate.
|
||||
boss.HeatstrokeDeath.OnNext(Unit.Default);
|
||||
Assert.DoesNotContain(boss, exit.Entities);
|
||||
Assert.False(exit.Entities.Any(x => ReferenceEquals(x, boss)), "the boss is removed from the exit");
|
||||
Assert.False(RunGoal.BossAlive(exit));
|
||||
Assert.True(RunGoal.ExitOpen(exit));
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public sealed partial class ContactRevealTests
|
|||
var s = Add(float3(0, 0, 0), tools: new[] { scanner });
|
||||
s.EntityInfoGathered[target] = info;
|
||||
s.SetTarget(target);
|
||||
Assert.Same(target, s.Target.Value.Entity); // fixture
|
||||
Assert.True(ReferenceEquals(target, s.Target.Value.Entity), "fixture: the scanner's ship targets the ship it scans"); // not Assert.Same: a failing Entity argument is formatted as a graph and overflows the test host
|
||||
return (s, target);
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +214,25 @@ public sealed partial class ContactRevealTests
|
|||
Assert.Empty(s.RevealsOf(first));
|
||||
}
|
||||
|
||||
// ---- AScanReadsNothingOnAnotherTarget ----
|
||||
|
||||
// A scan built on one ship is that ship's: with the scanner switched off at 11/16 the player retargets, and the progress
|
||||
// against the new target is 0 at once, not the old scan decaying (the HUD's outgoing indicator reads it).
|
||||
[Fact]
|
||||
public void AScanReadsNothingOnAnotherTarget()
|
||||
{
|
||||
var (s, first) = ScanOn();
|
||||
var second = Add(float3(100, 0, 0));
|
||||
Hold(s, 11, first, second);
|
||||
Assert.Equal(11f / 16f, ScannerOf(s).Progress, 4);
|
||||
ScannerItemOf(s).Enabled.Value = false;
|
||||
|
||||
s.SetTarget(second);
|
||||
Hold(s, 1, first, second);
|
||||
|
||||
Assert.Equal(0f, ScannerOf(s).Progress, 4);
|
||||
}
|
||||
|
||||
// ---- TheScannedShipIsWarned ----
|
||||
|
||||
// A paint made in a tick is published the next one, so after four ticks the target holds the third tick's paint.
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ public sealed partial class FireControlCut124Tests
|
|||
var other = FireControl.Solution(e.Weapon, e.Shooter, decoy).Direction;
|
||||
Assert.True(FireControl.Bears(e.Weapon, e.Shooter, decoy, out _));
|
||||
Assert.True(length(designated - other) > .3f, "fixture: the two intercepts must differ");
|
||||
Assert.Same(e.Target, e.Shooter.Target.Value.Entity);
|
||||
Assert.True(ReferenceEquals(e.Target, e.Shooter.Target.Value.Entity), "the designated target is kept");
|
||||
|
||||
Near(other, FireControl.Solve(e.Weapon, e.WeaponItem, e.Shooter, decoy).TravelDirection);
|
||||
Near(designated, FireControl.Solve(e.Weapon, e.WeaponItem, e.Shooter, e.Target).TravelDirection);
|
||||
|
|
|
|||
|
|
@ -285,8 +285,12 @@ public static class Program
|
|||
_ => null,
|
||||
};
|
||||
|
||||
// Whether CB-R1 maps this design's stats to roles; the catalog test asks the tool, so it keeps no copy of the kinds.
|
||||
public static bool HasRoleMap(EquippableItemData item) => RoleMapFor(item, KindOf(item)) != null;
|
||||
// The field->role map roles-backfill would apply to this design, or null; the catalog tests ask the tool, so they keep no
|
||||
// copy of the kinds or the maps.
|
||||
public static IReadOnlyDictionary<string, string> RoleMapOf(EquippableItemData item) => RoleMapFor(item, KindOf(item));
|
||||
|
||||
// Whether CB-R1 maps this design's stats to roles.
|
||||
public static bool HasRoleMap(EquippableItemData item) => RoleMapOf(item) != null;
|
||||
|
||||
private sealed record RoleAuthoring(
|
||||
EquippableItemData Item, string Kind, List<(string Behavior, string Field, string Role)> StatsAssigned,
|
||||
|
|
|
|||
Loading…
Reference in a new issue