TestCatalogGlobal existed only to give the test host one [CultGlobal] routed to the catalog. VerseGrammar is the
game's first, so the test-only type is deleted and every fixture authors a VerseGrammar instead (63 call sites; the
missing-global tests assert on aetheria.verse_grammar). Stale comments about the test type are rewritten.
Builds (not yet run when committed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AetherDriveData and AetherDrive, Union(12) (now a retired-do-not-reuse comment, as 26 is),
HardpointType.AetherDrive (last member, nothing shifts), Ship's drive sets, aggregates and
Axis write, the five request-field rows, GameplaySettings Aether multipliers, ShipInstance's
drive particles, SchematicDisplay's RPM display and AetherDriveUi, the undock refusal's drive
clause, the drive prefab and material, and AetherDb's drive roles and brownout row. The
design is kept at parked/aether-drive.
Adds EveryShippedShipHullHasATopSpeed. ARPG.unity still holds stale serialized references to
the removed SchematicDisplay fields and the RPM Display object; the next commit removes them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ship.Update reads Turn and feeds the rotation thrusters and aether drives; it no longer reads the
look direction or snaps Direction toward it. Steering.Toward is the one law from heading to demand
(a heading astern now turns at full demand). HelmInput owns the face-the-aim state. Accelerate,
Combat and MoveTo write Turn through Steering.Toward. Entity.LookDirection is renamed Aim in the
simulation, the Unity readers and the tests. Unity player input follows in the next commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- 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>
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inspect.PBase is HitProbability's own answer instead of a second product of
the factors. New behavioural tests pin a beam shot carrying its override
damage, a fitted targeting system forgiving a jink, a shot orphaned before
commit committing as a plain miss, and the HUD's arrival countdown. The
tautological damage assertion in BeamRollsPerInterval is deleted.
Stryker on FireControl.cs: 51 survivors to 43, all triaged in the map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Most of Stryker's surviving mutants in FireControl.cs traced to a handful of
fixture constants that make a mutant compute exactly the same number as the
original -- the same defect class as the zero-velocity target and the damage
that happened to equal an item's durability, found here as a pattern.
- The kernel fixture's shooter sat at float3.zero, where `target - source` and
`target + source` are the same vector. HitProbabilityMatchesInspect gains a
configuration with the shooter far from the origin: the correct separation is
500, in range; the mutated sum is 1300, out of range.
- The kernel fixture's Resolution was 1000, which saturates pSensor to exactly 1
above the detection threshold, so multiplying and dividing by it agreed. Now 2.
- EvasionCountsUntilCommitAndNotAfter fired at zone time 0, so `now - FireTime`
equalled `now + FireTime`. Cut 5 gave this test a moving target to defend its
own declared mutation and it still survived; the clock now advances first.
- Every fixture used zero spread and a cell size of 1, leaving the whole spread
term untested. PSpreadIsTheSilhouettesShareOfTheSpreadCone pins what pSpread
means -- a silhouette filling the cone scores 1, half the cone scores .5, and
a bigger one saturates -- rather than how it is computed.
Triaged as equivalent, not chased: the float-threshold boundary flips, and the
three MixSeed mutants that turn a right shift left -- measured, the die stays
fair under all three (worst deviation from p .017-.030 against .022 correct), so
killing them would mean pinning the mixer's bit arithmetic, not its behaviour.
Recorded, not fixed: FireControl.Apply's derivation of hitDirection from the two
positions has no geometry test through the FireControl path, and
StatResolverTests.AModifierThatTargetsItsOwnMagnitudeStatIsRefusedAtEquip fails
intermittently in the full suite -- likely cross-class pollution through
CultCache's process-wide document registry.
241 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The diagnostics refactor (72c0109c) kept the debug HUD and the simulation on one
fire-control model by routing HitProbability THROUGH Inspect. That was the right
invariant implemented at the wrong level: every AI and turret evaluation, per
weapon per tick, built the full diagnostic struct and ran IsRevealed's sort, the
aim-point cell walk and HullKernel's allocations and per-cell exp -- and only
then checked whether the target was even in range. A Soul pass measured 9.87 us
and 448 B per call on paths that used to bail in nanoseconds. Operator: it made
the hot path look like a diagnostic one.
One model is now shared at the level of its factors. HitProbability runs its
cheap gates first and bails, exactly as it did before the refactor, and computes
PSensor, PSpread and POnHull only for a shot that is actually possible. Inspect
calls the same four factor functions and fills the struct unconditionally,
because a HUD wants to see why a shot is impossible. The HUD panel and its one
test consumer are unchanged.
Two tests make the design enforced rather than conventional:
- HitProbabilityMatchesInspect: exact equality across 21 configurations covering
every gate the hot path bails on plus Precision and info sweeps. Dies when a
factor changes in only one caller.
- GatedOutHitProbabilityAllocatesNothing: 1000 out-of-range calls allocate zero
bytes. Dies when HitProbability is routed back through Inspect.
Both mutations verified RED and distinguished from a compile failure, with
restores checked against a hash taken before mutating.
231 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.1, blocking. FireControl seeded CultMath's xorshift32 per shot and drew once.
NextFloat returns the top 24 bits of the FIRST round, and one round cannot carry
ShotId's low-bit variation into the high bits -- so every zone's hit roll spanned
about an eighth, with the threshold fixed at zone creation. A shot was a step
function of p: below the band it never hit, above it always did. An unaided
shooter hit 0 of 18,926 shots. That is the operator's "no hits registering," and
being stable per zone is why it came and went.
The seed derivation is unchanged -- still a pure function of (zone, shot id),
Cut 6b's rule -- and only its diffusion into the first output changes, via an
fmix32 finalizer. Measured across six zone seeds: hit fraction at p=.3 went from
0.0% in five of six zones to 29-31%, and at p=.6 to 58-61%, with the roll
spanning the full range.
The defect survived three cuts because nothing in the campaign measured a
distribution -- every fixture uses accuracy 1 or 0, and a constant satisfies a
determinism test perfectly. TheDieIsUniform now asserts the observed hit
fraction is within .08 of p across several zone seeds, which is the test whose
absence was the real defect.
9.2. Sigma is floored at half a cell. HullKernel approximates a continuous
Gaussian with a discrete sum over occupied cells, and that sum collapses once
sigma falls well under one cell against an aim point that is not exactly on a
cell centre -- true of every shipped hull. Past Precision ~4, pOnHull went to
zero and a better targeting system made the weapon unable to hit anything.
9.3. An active entity's Target is now always a live entity it has info on.
Cut 8's _active guard could never have fixed the operator's LockWeapon crash --
that code only runs on active entities, and cut8's own harness header says so.
The live route was dock, target dies, undock; Deactivate disposed the
subscription that nulls a stale Target and Activate reseeded EntityInfoGathered
without reconciling it. Still not a TryGetValue in LockWeapon.
229 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>