Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using CultMath;
|
|
|
|
|
using GameCult.Caching;
|
|
|
|
|
using Xunit;
|
|
|
|
|
using static CultMath.math;
|
|
|
|
|
using float3 = CultMath.float3;
|
|
|
|
|
using Random = CultMath.Random;
|
|
|
|
|
|
|
|
|
|
// Cut 2 (docs/fire-control-cut.md): pins the targeting subsystem, the reveal rule between the two info
|
|
|
|
|
// thresholds, the selection slot/predicate, and the required-equipment ruling (Q4). Builds its own
|
|
|
|
|
// hull/catalog fixtures, the same style FireControlTests uses for Cut 1, rather than reusing that file's
|
|
|
|
|
// or LoadoutTests' private helpers.
|
|
|
|
|
public sealed class TargetingSystemTests : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-targeting-" + Guid.NewGuid().ToString("N"));
|
|
|
|
|
private string Catalog => Path.Combine(_root, "Aetheria.cc");
|
|
|
|
|
|
|
|
|
|
public TargetingSystemTests() => Directory.CreateDirectory(_root);
|
|
|
|
|
|
|
|
|
|
private CultCache _openCache;
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_openCache?.Dispose();
|
|
|
|
|
Directory.Delete(_root, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(),
|
|
|
|
|
TargetGearInfoThreshold = .9f,
|
|
|
|
|
UnaidedAccuracy = .1f,
|
|
|
|
|
FiringArc = 120
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v };
|
|
|
|
|
|
|
|
|
|
// Min=0 (not Min=Max): a PowerSupply term can only be observed to degrade something with room to move
|
|
|
|
|
// between floor and authored ceiling (BrownoutTests' own convention).
|
|
|
|
|
private static PerformanceStat Curved(float max, float exponent) =>
|
|
|
|
|
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
|
2026-10-13 02:01:20 +02:00
|
|
|
// more Tool items, one 2-cell and one 1-cell: a hardpoint weapon, a wide interior item and a narrow one.
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
// `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).
|
|
|
|
|
private (ItemManager items, Ship target, EquippedItem weapon, EquippedItem wideItem, EquippedItem narrowItem, EquippedItem targetingItem)
|
|
|
|
|
BuildTarget(GameplaySettings settings, TargetingSystemData targetingData = null)
|
|
|
|
|
{
|
|
|
|
|
var hullShape = new Shape(6, 4);
|
|
|
|
|
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
|
|
|
|
var hullData = new HullData
|
|
|
|
|
{
|
|
|
|
|
Name = "TestHull", HullType = HullType.Ship, Shape = hullShape, Durability = 1, Mass = 1000,
|
|
|
|
|
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using (var buildCache = AetheriaStores.Open(Catalog, catalogWritable: true))
|
|
|
|
|
{
|
2026-10-10 07:18:00 +02:00
|
|
|
buildCache.Upsert(new VerseGrammar { Revision = 1 });
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
buildCache.Upsert(hullData);
|
|
|
|
|
buildCache.Upsert(new GearData
|
|
|
|
|
{
|
|
|
|
|
Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Durability = 1,
|
|
|
|
|
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
|
|
|
|
Behaviors = { new InstantWeaponData() }
|
|
|
|
|
});
|
|
|
|
|
var wideShape = new Shape(2, 1);
|
|
|
|
|
wideShape[new int2(0, 0)] = true;
|
|
|
|
|
wideShape[new int2(1, 0)] = true;
|
|
|
|
|
buildCache.Upsert(new GearData
|
|
|
|
|
{
|
|
|
|
|
Name = "Wide", Hardpoint = HardpointType.Tool, Shape = wideShape, Durability = 1,
|
|
|
|
|
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
|
|
|
|
|
});
|
|
|
|
|
buildCache.Upsert(new GearData
|
|
|
|
|
{
|
|
|
|
|
Name = "Narrow", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 1,
|
|
|
|
|
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
|
|
|
|
|
});
|
|
|
|
|
if (targetingData != null)
|
|
|
|
|
buildCache.Upsert(new GearData
|
|
|
|
|
{
|
|
|
|
|
Name = "Targeting", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 1,
|
|
|
|
|
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
|
|
|
|
|
Behaviors = { targetingData }
|
|
|
|
|
});
|
|
|
|
|
buildCache.FlushAsync().Wait();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_openCache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
|
|
|
|
var ledger = new ProvenanceLedger();
|
|
|
|
|
ledger.Lots[1] = new Lot { Origin = new Attributed(), Quality = 1 }; // hull
|
|
|
|
|
ledger.Lots[2] = new Lot { Origin = new Attributed(), Quality = 1 }; // gun
|
|
|
|
|
ledger.Lots[3] = new Lot { Origin = new Attributed(), Quality = 1 }; // wide
|
|
|
|
|
ledger.Lots[4] = new Lot { Origin = new Attributed(), Quality = 1 }; // narrow
|
|
|
|
|
ledger.Lots[5] = new Lot { Origin = new Attributed(), Quality = 1 }; // targeting (unused unless authored above)
|
|
|
|
|
var items = new ItemManager(_openCache, ledger, settings, _ => { });
|
|
|
|
|
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null);
|
|
|
|
|
|
|
|
|
|
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("TestHull"));
|
|
|
|
|
var hull = new EquippableItem { Data = hullRef, Durability = 1, Lot = 1 };
|
|
|
|
|
var target = new Ship(items, zone, hull, new EntitySettings());
|
|
|
|
|
|
2026-09-19 19:43:57 +02:00
|
|
|
var narrowRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("Narrow"));
|
|
|
|
|
var narrow = new EquippableItem { Data = narrowRef, Durability = 1, Lot = 4 };
|
|
|
|
|
Assert.True(target.TryFindSpace(narrow, out var narrowPos));
|
|
|
|
|
Assert.True(target.TryEquip(narrow, narrowPos));
|
|
|
|
|
var narrowItem = target.Equipment.Single(e => e.EquippableItem == narrow);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
|
|
|
|
var wideRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("Wide"));
|
|
|
|
|
var wide = new EquippableItem { Data = wideRef, Durability = 1, Lot = 3 };
|
|
|
|
|
Assert.True(target.TryFindSpace(wide, out var widePos));
|
|
|
|
|
Assert.True(target.TryEquip(wide, widePos));
|
|
|
|
|
var wideItem = target.Equipment.Single(e => e.EquippableItem == wide);
|
|
|
|
|
|
2026-09-19 19:43:57 +02:00
|
|
|
var gunRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("Gun"));
|
|
|
|
|
var gun = new EquippableItem { Data = gunRef, Durability = 1, Lot = 2 };
|
|
|
|
|
Assert.True(target.TryEquip(gun, new int2(0, 0)));
|
|
|
|
|
var weapon = target.Equipment.Single(e => e.EquippableItem == gun);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
|
|
|
|
EquippedItem targetingItem = null;
|
|
|
|
|
if (targetingData != null)
|
|
|
|
|
{
|
|
|
|
|
var targetingRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("Targeting"));
|
|
|
|
|
var targeting = new EquippableItem { Data = targetingRef, Durability = 1, Lot = 5 };
|
|
|
|
|
Assert.True(target.TryFindSpace(targeting, out var targetingPos));
|
|
|
|
|
Assert.True(target.TryEquip(targeting, targetingPos));
|
|
|
|
|
targetingItem = target.Equipment.Single(e => e.EquippableItem == targeting);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
zone.Entities.Add(target);
|
|
|
|
|
target.Activate();
|
|
|
|
|
|
|
|
|
|
return (items, target, weapon, wideItem, narrowItem, targetingItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A bare-hull observer sharing the target's zone, with no equipment of its own -- only its
|
|
|
|
|
// EntityInfoGathered dictionary and Target/TrySelectTargetItem matter to these tests.
|
|
|
|
|
private Ship BuildObserver(ItemManager items, Zone zone)
|
|
|
|
|
{
|
|
|
|
|
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("TestHull"));
|
|
|
|
|
var hull = new EquippableItem { Data = hullRef, Durability = 1, Lot = 1 };
|
|
|
|
|
var observer = new Ship(items, zone, hull, new EntitySettings());
|
|
|
|
|
zone.Entities.Add(observer);
|
|
|
|
|
observer.Activate();
|
|
|
|
|
return observer;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
// 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.
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void SelectionNeedsReveal()
|
|
|
|
|
{
|
|
|
|
|
var (items, target, weapon, _, _, _) = BuildTarget(TestSettings());
|
|
|
|
|
var observer = BuildObserver(items, target.Zone);
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
observer.SetTarget(target);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
Assert.False(observer.TrySelectTargetItem(weapon)); // nothing seen yet
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.Null(observer.TargetItem.Value);
|
|
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.True(observer.TrySelectTargetItem(weapon));
|
|
|
|
|
Assert.Same(weapon, observer.TargetItem.Value);
|
|
|
|
|
Assert.Same(weapon, observer.ResolvedTargetItem);
|
|
|
|
|
|
|
|
|
|
// A Target change nulls the aim point.
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
observer.SetTarget(TargetRef.None);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.Null(observer.TargetItem.Value);
|
|
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
// 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.
|
Mining Cut 3: a chunk is a target, detected by reflected light
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:39 +02:00
|
|
|
observer.SetTarget(target);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.True(observer.TrySelectTargetItem(weapon));
|
2026-10-13 02:01:20 +02:00
|
|
|
weapon.EquippableItem.Durability = 0f;
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.Same(weapon, observer.TargetItem.Value);
|
|
|
|
|
Assert.Null(observer.ResolvedTargetItem);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
// 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).
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
[Fact]
|
2026-10-13 02:01:20 +02:00
|
|
|
public void GearIsRevealedByWhatItDid()
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
{
|
|
|
|
|
var (items, target, weapon, wideItem, narrowItem, _) = BuildTarget(TestSettings());
|
|
|
|
|
var observer = BuildObserver(items, target.Zone);
|
2026-10-13 02:01:20 +02:00
|
|
|
observer.SetTarget(target);
|
|
|
|
|
|
|
|
|
|
observer.RecordReveal(target, weapon, SignatureChannel.LaunchFlash);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
|
|
|
|
Assert.True(FireControl.IsRevealed(observer, weapon));
|
|
|
|
|
Assert.False(FireControl.IsRevealed(observer, wideItem));
|
|
|
|
|
Assert.False(FireControl.IsRevealed(observer, narrowItem));
|
2026-10-13 02:01:20 +02:00
|
|
|
Assert.True(observer.TrySelectTargetItem(weapon));
|
|
|
|
|
Assert.False(observer.TrySelectTargetItem(wideItem));
|
|
|
|
|
Assert.False(observer.TrySelectTargetItem(narrowItem));
|
|
|
|
|
}
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
// 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 WatchingRevealsNoGear()
|
|
|
|
|
{
|
|
|
|
|
var (items, target, weapon, wideItem, narrowItem, _) = BuildTarget(TestSettings());
|
|
|
|
|
var observer = BuildObserver(items, target.Zone);
|
|
|
|
|
observer.EntityInfoGathered[target] = 1f;
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
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));
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
// 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.
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
[Fact]
|
2026-10-13 02:01:20 +02:00
|
|
|
public void ADestroyedItemDropsOut()
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
{
|
2026-10-13 02:01:20 +02:00
|
|
|
var (items, target, weapon, wideItem, _, _) = BuildTarget(TestSettings());
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
var observer = BuildObserver(items, target.Zone);
|
2026-10-13 02:01:20 +02:00
|
|
|
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
|
|
|
|
|
}
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
// 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);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.True(FireControl.IsRevealed(observer, weapon));
|
|
|
|
|
|
2026-10-13 02:01:20 +02:00
|
|
|
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);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.True(FireControl.IsRevealed(observer, weapon));
|
2026-10-13 02:01:20 +02:00
|
|
|
|
|
|
|
|
weapon.EquippableItem.Durability = 0f;
|
|
|
|
|
|
|
|
|
|
Assert.Contains(observer.RevealsOf(target), r => r.Item == weapon);
|
|
|
|
|
Assert.False(FireControl.IsRevealed(observer, weapon));
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Q4: the subsystem is what supplies accuracy. No targeting system, a destroyed one, and a disabled one
|
|
|
|
|
// all resolve the authored unaided figures. Mutation: fall back to the (destroyed/offline) system's own
|
|
|
|
|
// stats instead of the unaided ones.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void UnaidedFiresWorse()
|
|
|
|
|
{
|
|
|
|
|
var settings = TestSettings();
|
|
|
|
|
|
|
|
|
|
var targetingData = new TargetingSystemData
|
|
|
|
|
{
|
|
|
|
|
Accuracy = Constant(.8f), Resolution = Constant(.6f), Precision = Constant(.7f), Tracking = Constant(.5f)
|
|
|
|
|
};
|
|
|
|
|
var (items, target, _, _, _, targetingUnit) = BuildTarget(settings, targetingData);
|
|
|
|
|
var equippedTargeting = targetingUnit;
|
|
|
|
|
var system = equippedTargeting.Behaviors.OfType<TargetingSystem>().Single();
|
|
|
|
|
system.Execute(0f);
|
|
|
|
|
|
|
|
|
|
// No targeting system at all: a second, bare-hull ship in the same catalog/zone (one CultCache
|
|
|
|
|
// handle stays open per test -- BuildTarget is not called twice).
|
|
|
|
|
var bareShip = BuildObserver(items, target.Zone);
|
|
|
|
|
Assert.Equal(settings.UnaidedAccuracy, FireControl.Accuracy(bareShip), 4);
|
Cut 6d: the dart-throw kernel replaces the coin-flip landing rule
FireControl.Commit used to place a hit with a coin flip between exactly
the aimed item and a uniform-random hull cell -- no spatial term, so a
near-miss on a stern item was as likely to land in the bow as the stern.
One Gaussian kernel, in hull-schematic cell units, now decides both
halves of "where will this shot go": w(cell) = exp(-d^2/2*sigma^2) over
every occupied cell, sigma = 1/Precision. HitProbability folds the
kernel's on-hull mass share (pOnHull) into PBase at fire time (R10/Q6
unaffected); Commit draws the landing cell from the same weights. A shot
that passes the roll always lands on metal -- the off-hull chance is
priced into that one roll, never resolved as a second miss stage. The
uniform-random branch is deleted outright, not demoted.
Precision now feeds every shot's pOnHull, aimed or not, so its unaided
fallback (FireControl.Precision) reads GameplaySettings.UnaidedPrecision
instead of a bare 0 -- the same shape as the existing UnaidedAccuracy/
UnaidedTracking floors. pOnHull is saturated: the discrete sum over-
estimates the continuous kernel's integral once sigma drops below about
one cell, and a mass share cannot exceed the whole kernel.
Existing fixtures across FireAuthorityTests, FireControlCut4/5/6Tests and
TargetingSystemTests authored Precision as 0 (or relied on the old 0
fallback) purely as a don't-care value under the coin-flip rule, where it
only gated the aimed-item branch. Under the kernel it drives every shot's
sigma, so those fixtures are re-authored to keep pOnHull near its
ceiling and stay pinned on what they actually test.
mutation_tests_fire_control_cut3.sh's two anchors into HitProbability's
return line and the deleted coin-flip branch are updated to the
equivalent lines/mutations on the new code.
2026-09-19 21:37:39 +02:00
|
|
|
// Cut 6d (docs/fire-control-cut.md): Precision now falls back to UnaidedPrecision, the same shape as
|
|
|
|
|
// Accuracy/Tracking above -- it used to fall back to a bare 0, which was fine when Precision only fed
|
|
|
|
|
// the old coin-flip's aimed-item branch and meant nothing to a shot with no aim point. Now it derives
|
|
|
|
|
// every shot's sigma, aimed or not, so the unaided floor has to be an authored number, not a bare zero.
|
|
|
|
|
Assert.Equal(settings.UnaidedPrecision, FireControl.Precision(bareShip), 4);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.Equal(1f, FireControl.Resolution(bareShip), 4);
|
|
|
|
|
|
|
|
|
|
// Bring it online to prove the fixture can actually supply its own stats...
|
|
|
|
|
equippedTargeting.UpdatePerformance();
|
|
|
|
|
Assert.True(equippedTargeting.Active.Value);
|
|
|
|
|
Assert.Equal(.8f, FireControl.Accuracy(target), 4);
|
|
|
|
|
Assert.Equal(.7f, FireControl.Precision(target), 4);
|
|
|
|
|
|
|
|
|
|
// ...then destroy it: durability gone, still falls back.
|
|
|
|
|
equippedTargeting.EquippableItem.Durability = 0f;
|
|
|
|
|
equippedTargeting.UpdatePerformance();
|
|
|
|
|
Assert.False(equippedTargeting.Active.Value);
|
|
|
|
|
Assert.Equal(settings.UnaidedAccuracy, FireControl.Accuracy(target), 4);
|
Cut 6d: the dart-throw kernel replaces the coin-flip landing rule
FireControl.Commit used to place a hit with a coin flip between exactly
the aimed item and a uniform-random hull cell -- no spatial term, so a
near-miss on a stern item was as likely to land in the bow as the stern.
One Gaussian kernel, in hull-schematic cell units, now decides both
halves of "where will this shot go": w(cell) = exp(-d^2/2*sigma^2) over
every occupied cell, sigma = 1/Precision. HitProbability folds the
kernel's on-hull mass share (pOnHull) into PBase at fire time (R10/Q6
unaffected); Commit draws the landing cell from the same weights. A shot
that passes the roll always lands on metal -- the off-hull chance is
priced into that one roll, never resolved as a second miss stage. The
uniform-random branch is deleted outright, not demoted.
Precision now feeds every shot's pOnHull, aimed or not, so its unaided
fallback (FireControl.Precision) reads GameplaySettings.UnaidedPrecision
instead of a bare 0 -- the same shape as the existing UnaidedAccuracy/
UnaidedTracking floors. pOnHull is saturated: the discrete sum over-
estimates the continuous kernel's integral once sigma drops below about
one cell, and a mass share cannot exceed the whole kernel.
Existing fixtures across FireAuthorityTests, FireControlCut4/5/6Tests and
TargetingSystemTests authored Precision as 0 (or relied on the old 0
fallback) purely as a don't-care value under the coin-flip rule, where it
only gated the aimed-item branch. Under the kernel it drives every shot's
sigma, so those fixtures are re-authored to keep pOnHull near its
ceiling and stay pinned on what they actually test.
mutation_tests_fire_control_cut3.sh's two anchors into HitProbability's
return line and the deleted coin-flip branch are updated to the
equivalent lines/mutations on the new code.
2026-09-19 21:37:39 +02:00
|
|
|
Assert.Equal(settings.UnaidedPrecision, FireControl.Precision(target), 4);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
Assert.Equal(1f, FireControl.Resolution(target), 4);
|
|
|
|
|
|
|
|
|
|
// Repair it, then disable it directly instead: same fallback.
|
|
|
|
|
equippedTargeting.EquippableItem.Durability = 1f;
|
|
|
|
|
equippedTargeting.UpdatePerformance();
|
|
|
|
|
Assert.True(equippedTargeting.Active.Value);
|
|
|
|
|
equippedTargeting.Enabled.Value = false;
|
|
|
|
|
Assert.False(equippedTargeting.Active.Value);
|
|
|
|
|
Assert.Equal(settings.UnaidedAccuracy, FireControl.Accuracy(target), 4);
|
Cut 6d: the dart-throw kernel replaces the coin-flip landing rule
FireControl.Commit used to place a hit with a coin flip between exactly
the aimed item and a uniform-random hull cell -- no spatial term, so a
near-miss on a stern item was as likely to land in the bow as the stern.
One Gaussian kernel, in hull-schematic cell units, now decides both
halves of "where will this shot go": w(cell) = exp(-d^2/2*sigma^2) over
every occupied cell, sigma = 1/Precision. HitProbability folds the
kernel's on-hull mass share (pOnHull) into PBase at fire time (R10/Q6
unaffected); Commit draws the landing cell from the same weights. A shot
that passes the roll always lands on metal -- the off-hull chance is
priced into that one roll, never resolved as a second miss stage. The
uniform-random branch is deleted outright, not demoted.
Precision now feeds every shot's pOnHull, aimed or not, so its unaided
fallback (FireControl.Precision) reads GameplaySettings.UnaidedPrecision
instead of a bare 0 -- the same shape as the existing UnaidedAccuracy/
UnaidedTracking floors. pOnHull is saturated: the discrete sum over-
estimates the continuous kernel's integral once sigma drops below about
one cell, and a mass share cannot exceed the whole kernel.
Existing fixtures across FireAuthorityTests, FireControlCut4/5/6Tests and
TargetingSystemTests authored Precision as 0 (or relied on the old 0
fallback) purely as a don't-care value under the coin-flip rule, where it
only gated the aimed-item branch. Under the kernel it drives every shot's
sigma, so those fixtures are re-authored to keep pOnHull near its
ceiling and stay pinned on what they actually test.
mutation_tests_fire_control_cut3.sh's two anchors into HitProbability's
return line and the deleted coin-flip branch are updated to the
equivalent lines/mutations on the new code.
2026-09-19 21:37:39 +02:00
|
|
|
Assert.Equal(settings.UnaidedPrecision, FireControl.Precision(target), 4);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The brownout ruling (docs/stats-and-power-cut.md) applies to Accuracy same as any other stat: a
|
|
|
|
|
// half-grant halves it (PowerSupply exponent 1), not a switch-off. Reactor + a real PowerBus tick, the
|
|
|
|
|
// same fixture shape BrownoutTests uses, because Item.PowerSupply's setter is internal to a different
|
|
|
|
|
// assembly here and can only be driven through a live grant.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void StarvedTargetingRollsWorse()
|
|
|
|
|
{
|
|
|
|
|
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
2026-10-10 07:18:00 +02:00
|
|
|
cache.Upsert(new VerseGrammar { Revision = 1 });
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
var hullShape = new Shape(5, 5);
|
|
|
|
|
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
|
|
|
|
cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = hullShape, Durability = 10, Mass = 1000 });
|
|
|
|
|
cache.Upsert(new GearData
|
|
|
|
|
{
|
|
|
|
|
Name = "Reactor", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 10,
|
|
|
|
|
MinimumTemperature = 0, MaximumTemperature = 1000, OptimalTemperature = 280, PlateauWidth = 400,
|
|
|
|
|
Behaviors = { new ReactorData { Charge = Constant(0), Efficiency = Constant(1), OverloadEfficiency = Constant(1), ThrottlingFactor = Constant(2) } }
|
|
|
|
|
});
|
|
|
|
|
cache.Upsert(new GearData
|
|
|
|
|
{
|
|
|
|
|
Name = "Targeting", Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 10,
|
|
|
|
|
MinimumTemperature = 0, MaximumTemperature = 1000, OptimalTemperature = 280, PlateauWidth = 400,
|
|
|
|
|
Behaviors =
|
|
|
|
|
{
|
|
|
|
|
new EnergyDrawData { EnergyDraw = Constant(100) },
|
|
|
|
|
new TargetingSystemData { Accuracy = Curved(1, 1), Resolution = Constant(1), Precision = Constant(1), Tracking = Constant(1) }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
cache.FlushAsync().Wait();
|
|
|
|
|
|
|
|
|
|
EquippableItem Mint(ItemData design) => new EquippableItem
|
|
|
|
|
{
|
|
|
|
|
Data = cache.RefOf<ItemData>(design),
|
|
|
|
|
Durability = design is EquippableItemData equippable ? equippable.Durability : 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ship BuildShip(float reactorCharge, out ItemManager items)
|
|
|
|
|
{
|
|
|
|
|
items = new ItemManager(cache, new ProvenanceLedger(), TestSettings(), _ => { });
|
|
|
|
|
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null);
|
|
|
|
|
var hullData = cache.GetByName<HullData>("Skiff");
|
|
|
|
|
var hull = Mint(hullData);
|
|
|
|
|
hull.Lot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(hullData), Origin = new Attributed(), Quality = .5f, Roles = new System.Collections.Generic.List<RoleFill>() });
|
|
|
|
|
var ship = new Ship(items, zone, hull, new EntitySettings());
|
|
|
|
|
|
|
|
|
|
var reactorData = cache.GetByName<GearData>("Reactor");
|
|
|
|
|
((ReactorData) reactorData.Behaviors[0]).Charge = Constant(reactorCharge);
|
|
|
|
|
var reactor = Mint(reactorData);
|
|
|
|
|
reactor.Lot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(reactorData), Origin = new Attributed(), Quality = .5f, Roles = new System.Collections.Generic.List<RoleFill>() });
|
|
|
|
|
Assert.True(ship.TryEquip(reactor));
|
|
|
|
|
|
|
|
|
|
var targetingData = cache.GetByName<GearData>("Targeting");
|
|
|
|
|
var targeting = Mint(targetingData);
|
|
|
|
|
targeting.Lot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(targetingData), Origin = new Attributed(), Quality = .5f, Roles = new System.Collections.Generic.List<RoleFill>() });
|
|
|
|
|
Assert.True(ship.TryEquip(targeting));
|
|
|
|
|
|
|
|
|
|
zone.Entities.Add(ship);
|
2026-10-07 12:33:20 +02:00
|
|
|
ship.Aim = float3(0, 0, 1);
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
ship.Activate();
|
|
|
|
|
return ship;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float ResolveAccuracy(float reactorCharge)
|
|
|
|
|
{
|
|
|
|
|
var ship = BuildShip(reactorCharge, out _);
|
|
|
|
|
ship.Update(1f); // one tick: PowerBus.Step grants, then Execute resolves Accuracy against it
|
|
|
|
|
return FireControl.Accuracy(ship);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var full = ResolveAccuracy(1000); // demand 100, generation far exceeds it -> ratio 1
|
|
|
|
|
var half = ResolveAccuracy(50); // demand 100, generation 50 -> ratio .5
|
|
|
|
|
|
|
|
|
|
Assert.Equal(1f, full, 3);
|
|
|
|
|
Assert.Equal(.5f, half, 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Q4: required equipment for anything that can fire. Across several seeds, every generated loadout with
|
|
|
|
|
// a weapon also carries a targeting system. Mutation: drop `required: true` on the targeting-system
|
|
|
|
|
// RandomProduct call in LoadoutGenerator.FillInterior.
|
|
|
|
|
[Fact]
|
|
|
|
|
public void EveryArmedLoadoutGetsATargetingSystem()
|
|
|
|
|
{
|
|
|
|
|
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
|
2026-10-10 07:18:00 +02:00
|
|
|
cache.Upsert(new VerseGrammar { Revision = 1 });
|
Pin the targeting subsystem, reveal rule and Q4's required-equipment rule
TargetingSystemTests: selection needs a real reveal (and drops it again
on decay, with no clearing loop), the reveal ranking (hardpoint before
interior, larger before smaller), the two thresholds are the ones
actually read, unaided fire falls back correctly for no/destroyed/
disabled systems, a starved system's Accuracy halves with the grant
(a real Reactor + PowerBus tick, since PowerSupply's setter is internal
to a different assembly here), and every armed generated loadout gets a
targeting system across ten seeds.
LoadoutTests' shared fixture now authors a targeting-system product too:
its "Twin" hull can randomly draw the weapon-carrying "Gun" onto its
Sensors hardpoint, and FillInterior's new required item would otherwise
throw for a fixture that predates this cut.
174 tests before this cut; 180 after (174 unchanged + 6 new), all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:03:12 +02:00
|
|
|
var maker = cache.Upsert(new Faction { Name = "Maker", ShortName = "MKR" });
|
|
|
|
|
|
|
|
|
|
var hullShape = new Shape(5, 5);
|
|
|
|
|
foreach (var cell in hullShape.AllCoordinates) hullShape[cell] = true;
|
|
|
|
|
var hull = cache.Upsert(new HullData
|
|
|
|
|
{
|
|
|
|
|
Name = "Armed", HullType = HullType.Ship, Shape = hullShape, Price = 100,
|
|
|
|
|
Hardpoints = { new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = new Shape() } }
|
|
|
|
|
});
|
|
|
|
|
var gun = cache.Upsert(new GearData { Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1, Behaviors = { new InstantWeaponData() } });
|
|
|
|
|
var cargo = cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = new Shape(), Price = 5 });
|
|
|
|
|
var capacitor = cache.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
|
|
|
|
|
var targeting = cache.Upsert(new GearData { Name = "Targeting", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new TargetingSystemData() } });
|
|
|
|
|
|
|
|
|
|
foreach (var (name, design) in new[]
|
|
|
|
|
{
|
|
|
|
|
("Armed by Maker", hull.Key), ("Gun by Maker", gun.Key), ("Crate by Maker", cargo.Key),
|
|
|
|
|
("Cap by Maker", capacitor.Key), ("Targeting by Maker", targeting.Key)
|
|
|
|
|
})
|
|
|
|
|
cache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = maker });
|
|
|
|
|
cache.FlushAsync().Wait();
|
|
|
|
|
|
|
|
|
|
for (var seed = 1; seed <= 10; seed++)
|
|
|
|
|
{
|
|
|
|
|
var random = new Random((uint) seed);
|
|
|
|
|
var items = new ItemManager(cache, new ProvenanceLedger(), TestSettings(), _ => { });
|
|
|
|
|
var generator = new LoadoutGenerator(ref random, items, null, null, null, .5f);
|
|
|
|
|
var pack = generator.GenerateShipLoadout(candidate => candidate.Name == "Armed");
|
|
|
|
|
Assert.NotNull(pack);
|
|
|
|
|
Assert.Contains(pack.Equipment, e => (cache.Get(e.item.Data) as GearData)?.Behaviors.Any(b => b is TargetingSystemData) ?? false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|