Commit graph Aetheria/Assets/Scripts/ServerShared/FireControl.cs
Author SHA1 Message Date
Meta Vens
2dcb7b2078 Ballistic flight r3: DrawAhead draws a round from the sim's facts, a hit ends at its impact cell; FireControl draws nothing
FireControl.RoundEnd and RoundAt leave FireControl (RoundEnd to DrawAhead verbatim, RoundAt as DrawAhead's private Line);
DrawAhead.Round takes the published outcome, steers a hit or burst from its frozen line at the commit tick onto its
end (burst point, else the impact cell on the target's drawn pose), and RoundStop is deleted. Projectile caches the
last published ShotOutcome and calls DrawAhead only. BallisticFlightTests move to the new signatures; the frozen-line
moving-target test is replaced by impact-cell, steer and no-drawing-function tests. Unverified: not yet built or run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:27:22 +02:00
Meta Vens
60aacfdc35 FireControl.RoundEnd states a round's end; DrawAhead draws rounds as pure statics
RoundEnd(shot, known) is the one statement of when a ballistic round's drawn
flight ends: arrival for a hit, burst or unpublished outcome, the frozen range
(never short of arrival) for a miss. RoundAt takes the known outcome and clamps
at RoundEnd. DrawAhead gains Round, RoundStop, RoundOver (pure, no state) so the
blend, stop and done rules are headless-testable. Seven new tests (ten cases).

Unity build: Projectile.cs still calls the old RoundAt, so this commit does not
compile in Unity (the headless test project does not include Projectile). The
next commit moves Projectile onto the statics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:31:12 +02:00
Meta Vens
2b03d90812 RoundAt clamps at the weapon's frozen Range (PendingShot.MaxRange), not FireRange
FireRange is the distance to the engaged target and 0 for a round with none, so a no-lock burst round and a
round that misses never flew. MaxRange = weapon.Range is frozen at Fire beside Speed. Unity compile not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:35:37 +02:00
Meta Vens
0e89dd1673 FireControl.RoundAt: the one statement of where a fired round is at a sim time
PendingShot freezes the weapon's Velocity as Speed at Fire; RoundAt(shot, time) reads the frozen origin,
direction, speed and fire time, clamped to the FireRange point. Step, Commit and Apply do not call it.
BallisticFlightTests pin the line against the shooter position, aim and authored velocity.
Builds: yes. Presenters still fly their own rounds at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:19:22 +02:00
Meta Vens
e103b90aea Fire on solutions: FireControl.Solution is each gun's one solution; the trigger fires on it without the aim
Bears is extracted from PFire; ArcPermitsFire passes on a fused gun, a solution, or a free aim in arc.
Solve flies along Solution's direction. ArticulationPoint loses its yaw clamp and follows a per-group
aim point placed from the group's solution. Arcs gets its new brief; Broadside is new.
Unverified: first commit of the cut, not yet built.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 13:25:53 +02:00
Meta Vens
179ab92abc Merge evasion-term: evasion is changes of vector plus a per-gun mount term; authored tracking rates in the catalog; the aether drive leaves the envelope
Soul gate closed at verdict cut-evasion-term.s5 (head 473a4bcc); remaining Low
test gaps are deferred to a follow-up. Merged under operator ruling
aetheria-release:ruling:completed-cuts-merge. Carries the migrated
GameData/Aetheria.cc (LFS sha256 63250cf2...7ae25); master did not change the
catalog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 22:29:51 +02:00
Meta Vens
231cb732e2 Merge origin/master (controls-helm) into sim-mines
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 20:36:32 +02:00
Meta Vens
3fcdc20c00 Merge controls-helm: Ship.Turn is the only facing command; the helm split and face-the-aim
Soul gate closed at verdict cut-controls-helm.s3 (head b644fceb). Merged
under operator ruling aetheria-release:ruling:completed-cuts-merge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 20:08:31 +02:00
Meta Vens
473a4bcccd Evasion fix batch C: drop the unruled catalog thresholds, pin the jink price and the torque trim membership, give an unlimited gun on zero gear one meaning
- Delete ABigGunHitsABigShipAtRange and TheCatalogMeetsTheRuledOutcomes (rulings cshot-rainbowlite-rate, evasion-angular-velocity-r2).
- TheJinkIsForgivenPerShipInThePrice: HitProbability of two guns of different rates carries PDeviation(Evasion, Tracking(shooter)).
- TrackingRate: an unlimited gun is +infinity whatever the gear reads, not inf * 0 = NaN.
- EveryAggregateAndTheCompensationReadLiveThrust: trim membership read from the thrusters' torques, not Ship's private list.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:59:29 +02:00
Meta Vens
23aa848315 Mines in the sim: a mine layer lays a free-floating mine body in Zone
MineLayerData (union 40) lays through InstantWeapon -> FireControl.Fire -> Lay;
Zone.Mines steps each mine as a KinematicBody after the entity loop, arms it,
triggers it on a hostile hull its blast disc touches (FireControl.Touches, the
coverage Detonate uses) and detonates it through FireControl.Detonate at the
fuse delay or lifetime. Hostile trigger, faction-blind blast, no owner leash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:47:21 +02:00
Meta Vens
9f8b3f4536 Merge floating-bodies: the floating body loot and mines share, one gravity law in Zone
Soul gate closed at verdict cut-floating-bodies.s3, head 6299627b, under
operator ruling aetheria-release:ruling:completed-cuts-merge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 19:19:27 +02:00
Meta Vens
b6487afd95 FireControl: the mount factor, AngularVelocity, TrackingRate and PMount, in the price, the freeze, Inspect and the HUD
PMount(AngularVelocity(source, target), TrackingRate(weapon, source)) multiplies HitProbability after the pFire gate;
Fire freezes it into PendingShot.PFire; Inspect and the debug HUD show omega, rate and the factor. TrackingRate is the
one product of the gun's rate and the ship's gear (ruling tracking-per-gun). Tracking(Entity) in metres still alone
forgives the changes of vector (ruling jink-per-ship).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 16:40:00 +02:00
Meta Vens
17f5eb7d8c Retire the Unity mine: Mine, MineManager, the Mine Launcher prefab and ShieldManager's mine branch
Operator ruling mines-back-in-sim-asap: delete now, return as a simulation body.
Mesh, materials and BigExplosion stay for the sim-mines presenter. GridObject
stays for the pickups until loot-1 r2. Comments in FireControl.Detonate and
HullCollider drop the deleted callers. Builds not yet verified at this commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:01:39 +02:00
Meta Vens
9116946874 SimulationDice: fmix32 moves out of FireControl, with the structured per-zone dice stream
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:52:10 +02:00
Meta Vens
c86423936e Helm split, simulation: Ship.Turn is the only facing command; LookDirection becomes Aim
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>
2026-10-07 12:33:20 +02:00
Meta Vens
522b5af0b4 ManoeuvreTrack forgets a whole window after the last change of vector; tests stop resetting it, and pin the three fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:39:41 +02:00
Meta Vens
da2957b81c Evasion term: reachable spread from the thrust and turn envelope, realised by changes of vector
Every hit price gains one factor, PDeviation(Evasion, Tracking): how far the target can
and does move off the line of fire within SolutionWindow (0.75 s). Capability is Reach, an
eight-face closed form over the ship's ManoeuvreEnvelope (summed from what each live
thruster and aether drive reports with its own Execute arithmetic, read live: a thruster's
Thrust property is stale until it fires, which is why the Djinni moved 1.6 times its stats,
M25). Behaviour is ManoeuvreTrack, observed once per Entity.Update from the measured
acceleration; a steady burn, coasting and a dither much faster than the window do not
evade, and sideways speed alone does not (ruling evasion-speed). The commit price adds the
term at any flight time, so an instant weapon feels a jinking target. Forecast owns the
term for HitProbability and Inspect, which gain Evasion and PEvasion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:48:27 +02:00
Meta Vens
5100daa297 FireControl: one PDeviation shape for the deviation price
DeviationProbability's inline saturate becomes PDeviation(deviation, tolerance), defined
for a zero tolerance as well. No behaviour change for a positive tolerance. Delete-first
step of the evasion term: every later deviation-like price reads this one shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:48:14 +02:00
Meta Vens
d9f85dc639 Merge origin/master into mining: resolve five conflicts by owner, delete the belt-task machine
Mining owns belts, chunks and target picking; scenarios owns admission and run
staging; master owns ships and fire control (rule scenarios-cut.md S4).

- AetheriaStores.CatalogTypes: nine-type union, master's list plus FieldKindData, no Scenario.
- tools/AetherDb/Program.cs: both cases (ship-authoring, field-kinds), one usage string.
- Zone.cs: master's MineAsteroid dropped (mining deleted its inputs); scenarios' SettleBelts and its three calls deleted.
- RunStartTests: the two belt-task tests deleted (they read belt.Transforms and MineAsteroid).
- FireControlCut124Tests: master's strict Exact assertions; mining's CompatibleDrift allowance and its comment dropped.
- GameData/Aetheria.cc: master's pointer 70a7b0a9. This commit lacks the Asteroid field kind in the catalog, so
  TheShippedCatalogHasAPickableFieldKind fails here by design; the next commit rebuilds it.

Builds: this commit builds (verified at the end with commit 2); the catalog pin test fails at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:29:29 +02:00
Meta Vens
5ea59ac6b5 Entity: an equipped item keeps the hardpoint it occupies
The placement rule (HardpointAt) already finds the one hardpoint an item lies
in at equip; TryEquip threw that answer away and eighteen readers recomputed it
from the item's origin cell, which need not lie in the hardpoint (Soul's
scenarios batch-4 P30). EquippedItem.Hardpoint now carries the answer, null for
Tool gear and the hull, and every origin-cell lookup reads it instead:
FireControl.ArcFor, the eleven weapon managers, EntityInstance, ShipInstance
and ActionGameManager's articulation grouping. Entity.Hardpoints[,] stays the
per-cell map for armour and display.

TryFindSpace's hardpoint search tries every origin the fit rule can accept,
including ones up and left of the hardpoint, so a design whose shape lacks its
origin cell is found wherever ItemFits would place it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:57:27 +02:00
Meta Vens
15723f4037 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
Meta Vens
136fddf15a Start the cooldown at a burst's first unrefused round; refused charged weapons never charge
A burst whose every round is refused fires nothing and leaves no cooldown: the cooldown moves from
Trigger to the first round Execute does not refuse, and CanFire holds a triggered burst that has not
yet fired. ChargedWeapon.Activate asks FireControl before it starts charging, so a refused weapon pays
no charge heat or audio. InReach is deleted: Designated carries the one range test and the lock gate,
and Combat reads it. GuidedProjectile answers Resolve while alive and keeps its binding through the
shot's flight time, so a faded round still shows the simulation's detonation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:16:52 +02:00
Meta Vens
57c2560fc0 Fuse fixes: arming holds for every burst, no-lock rounds stop on the first hull, settle before user code
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>
2026-09-29 20:31:59 +02:00
Meta Vens
a193853d34 Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.

* Combat and TurretController ask FireControl.AgentFires for each weapon. A
  fused weapon fires whenever its target is designated (Designated, the one
  visible / in-range / locked predicate PFire already used), whatever the arc;
  an agent never fires one at nothing. A direct-hit weapon keeps the
  AgentMinHitProbability threshold. The group's first weapon no longer speaks
  for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
  from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
  weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
  the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
  spend (at the trigger and at each round), ConstantWeapon folds the refusal
  into the gate its power request and Execute already share, so a refused beam
  draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
  blind to hull size. HullAhead's exit, ArmingDistance and
  PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
  the contact fuse. A round fired at a valid target inside the radius is pushed
  out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
  presentation observer costs its own notification and cannot re-commit or
  re-resolve the shot on every later tick.

Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
Meta Vens
24b4071b69 Arming distance: no round bursts nearer than its blast radius past the shooter's hull
Operator ruling 2026-09-30. A fused round's burst is at least BlastRadius
beyond the far edge of the shooter's own hull, measured along the flight line
against the hull as it stood at fire (HullAhead's exit). A close burst -- a
target inside that distance, in or out of arc -- is pushed out along the aim
and carries no target; a contact round is floored to it too.

- The F3 trigger gate is deleted. Fire refuses a round (returns 0) whose
  arming distance exceeds Range, per round, where Range is read after Execute
  refreshed it; InstantWeapon and ConstantWeapon skip a refused round.
- PFire's range, Inspect's range and the range clamp are planar (R7).
- HullEntryDistance becomes HullAhead (entry and exit); the max(...,0) clamp
  on entry is gone, the arming floor covers it.
- ShotOutcome.HasBurstPoint names the rounds that have a frozen burst point.
- SafeAssert: the tests no longer hand xunit an Entity, PendingShot or
  ShotOutcome to format; a failing assertion had been overflowing the test
  host's stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 17:05:14 +02:00
Meta Vens
640fa96787 Fused rounds fire out of arc; safe fuse; proximity misses type as Burst
Operator ruling 2026-09-30: a fused weapon with a selected but out-of-arc
target fires. Its round flies along the arc-clamped aim and bursts at the
target's range. Non-fused weapons still refuse.

- FireControl.ArcPermitsFire is the one trigger arc gate (fused weapons are
  exempt); Fire reads the same InArc through PFire's new designated/inArc
  outs, which replace `bears`. No valid targeting data still bursts at Range.
- F3: a fused weapon whose effective Range does not exceed its BlastRadius
  stays safed at the trigger (FuseCanArm, read from the runtime Range). The
  catalog check on Range.Min is deleted; it read the authored floor, which
  Evaluate can scale below.
- F4: a targeted proximity burst is pulled back along its line to Range.
- F8: a proximity round that detonates commits as Burst (or Hit); Apply's
  Proximity case collapses into the Burst path via Outcome.BurstPoint.
- Tests: arc-only and LockWeapon gates, out-of-arc burst, trigger gate,
  runtime-Range refusal, Range clamp, Burst typing, commit horizon, and
  Soul's adopted probes (aim clamp edges, contact point, target leaves,
  shooter destroyed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 16:27:46 +02:00
Meta Vens
f5c6729e7a No-lock fused rounds: aim, invalid targets, contact stops, outlived shooters
Operator rulings 2026-09-30 after Soul's pass on 8c002637:

- A no-lock round flies along Entity.LookDirection restricted by the mount's
  arc (FireControl.AimDirection: inside InArc's cone as is, outside it turned
  to the cone's edge; no aim falls back to the mount). The simulation had no
  aim clamp before; the old code used the hull-fixed mount direction.
- An invalid target (out of range, out of arc, not visible, not locked) counts
  as no lock for a fused round. PFire's gate is the one decider and now also
  reports `bears`; Fire reads it. The round carries no target afterwards.
- A no-lock Contact round bursts at the first hull its aim line crosses
  before max range, read through Lane's own cell admission (CellsOnLine,
  split out of Lane). Proximity and Delayed keep the max-range burst. Such a
  round commits at Fire, since it has no target facing to read late, and
  arrives when it has flown to the burst point. The seam for mining chunks is
  FireControl.ContactDistance.
- A round outlives its shooter: Step no longer cancels a shot whose source
  left the zone. Resolution reads the zone and the target; the shooter is an
  identity a shot carries. PendingShot.FireOrigin freezes where it left from.
- ShotOutcome.Result (Miss, Hit, Burst) replaces the stored Hit bool, which
  is now derived. A burst without a target is no longer reported as a miss,
  and the debug HUD labels it BURST.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:27:43 +02:00
Meta Vens
8c00263740 A fused shot with no target bursts at max range along the aim line
Operator ruling 2026-09-30: fused weapons without a lock explode at max
range. Fire froze BurstPosition at the shooter's own position with flight
time 0, so a proximity shot detonated on its own shooter and contact and
delayed shots did nothing.

With no target and a fuse, Fire now freezes the burst point as the mount
direction times the weapon's Range stat from the source, and flight time as
Range over Velocity. Apply sends every such shot through Detonate at that
point whatever its fuse. Targeted shots and unfused shots are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:28:10 +02:00
Meta Vens
8b4bf01eac Fire control 12.4 fix batch: tangent disc, candidate cull, lethal blast
F1: RectDiskOverlap counted a whole square when a cell edge sat exactly r from
the disc centre (`h < yhi`, `-h > ylo`). The comparisons are inclusive now, so
the chord bounds the piece. A proximity burst on an integer centre of mass
delivered 127.32 of 100 at BlastRadius 1 and 100.80 at 3.

F2: delete Detonate's distance prefilter and the radius <= 0 guard. The
prefilter measured from the centre of mass with the bounding box's
half-diagonal, which is not a bound on how far a hull's cells reach; it culled
real overlap on an L-shaped hull and on the shipped Longinus corners.
RectDiskOverlap is the single decider. An untouched entity is still skipped
(its covered list is never allocated).

F3: Detonate iterates a snapshot of zone.Entities, as Zone.Update does. A
lethal blast removed the entity from the live collection and the next MoveNext
threw, leaving the shot pending so it detonated again the next tick.

Tests: tangent discs through Fire and Detonate, an L-shaped hull, grazing
blasts at the farthest corner of Longinus and LonginusX (expected values from
a numeric overlap), and a lethal blast that must resolve once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:38:14 +02:00
Meta Vens
7c99909d2b Cut 12.4(b): Detonate replaces Splash and Entity.Absorb
FireControl.Apply now switches on the frozen shot.Fuse: a null fuse keeps
12.3's lane path (ApplyDirectHit), a proximity fuse detonates at arrival with
no roll, and a contact or delayed fuse detonates only on a committed hit
(ApplyBlastHit), locating P through Lane and the new Reach owner (moved
verbatim from Apply's own inline penetration clip; the delayed fuse point
reads the identical rule). Step has no damage branch left.

Detonate (FireControl.cs) is the one owner of blast damage: candidates by
centre-plus-half-diagonal, exact circle/cell overlap shares computed once,
every blast's shield decided live per Q12-9 = A (including the host of a
contact/delayed blast -- Commit now gates its own shield decision on
shot.Fuse == null), armour absorbed per cell, and each item's covered cells
pooled into one ItemAbsorb call. Entity.Absorb and FireControl.Splash are
deleted; Entity gains ToSchematicPoint/ToWorldPoint for the point conversion
Detonate and the delayed fuse point need. Mine.Explode now calls Detonate.

Existing Splash/Absorb-era tests across Cut4/5/6/11/123 are rewritten or
retargeted to the new public owners; behavioural coverage for the blast
model itself lands separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:17:36 +02:00
Meta Vens
2625768f86 Cut 12.4(a): rename AirburstRange to BlastRadius, add WeaponFuse
WeaponItemData.AirburstRange (slot 29) becomes BlastRadius, same slot and
type so CultCache opens the catalog as compatible drift. A new nullable
slot 32 (30 and 31 belong to EquippableItemData) carries WeaponFuse?
Fuse. FireControl.Fire now decides "detonates" once, from BlastRadius
and Fuse directly, and freezes Fuse only when BlastRadius > 0 -- the
WeaponModifiers.Airburst label no longer decides anything (Q12-6).
PendingShot.BurstRadius is renamed BlastRadius and gains a Fuse field
that nothing reads yet; Step's Splash/Apply dispatch is unchanged.

No shipped weapon carries a blast radius or the Airburst label (Cut 12's
Probe4, reconfirmed against GameData/Aetheria.cc), so this is
behaviour-neutral on shipped content. Apply's fuse switch and deleting
Splash are Cut 12.4(b).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:15:26 +02:00
Meta Vens
64d17a290d Cut 12.3 fix batch (F4): one writer for lane completion, delete dead reachability offset
ApplyPooled.Resolve no longer calls FinishLane directly -- AdvanceLane is now
the single place that checks pointer >= walked and finishes a lane, so lane
completion has one writer instead of two. StillReachableByAnotherLane's own
`blocked ? pointer+1 : pointer` collapses to plain `pointer[li2]`: the
alreadyContributed check just above it already excludes a lane that is
blocked on the very item being asked about, so `pointer[li2]` is never that
item's own cell by the time the +1 would have mattered.

Soul's findings (docs/fire-control-cut.md, Cut 12.3): F4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:33:09 +02:00
Meta Vens
e9339a5673 Cut 12.3 fix batch: one apply path for every item (operator ruling)
Replace the survey-then-branch (ApplySequential vs. ApplyWithSharedItems)
with a single pooled walk. Lanes are clipped to penetration once, right
after Lanes returns; every item -- single-cell or multi-cell -- pools
whatever lanes reach it and resolves the moment no other unfinished lane
can still reach it down its own remaining clipped walk, with no separate
reachability survey. A cyclic dependency between two non-convex items
resolves every currently open pool at once, order-independent by
construction. Entity.ItemAbsorb becomes the one absorption function,
gated on the pooled total; Absorb (Splash's only remaining caller) now
funnels through it with a one-contribution pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:01:05 +02:00
Meta Vens
567579136a Cut 12.3 fix batch: proportional absorption for multi-cell items shared between lanes
Operator ruling (2026-09-25): "proportional absorption; go". A multi-cell item
struck by several lanes of one shot now absorbs their combined incoming damage
(up to its own remaining durability) and each lane's leftover is in proportion
to what it brought, replacing the old behaviour where whichever lane Apply
happened to visit first drained the item and the next lane inherited whatever
was left.

Entity.Absorb is split into ArmorAbsorb (per-cell, never shared) and ItemAbsorb
(the single-lane path), so FireControl.Apply can call armour immediately and
defer item resolution only for a cell whose item is reachable by more than one
lane this shot. FireControl.Apply gains a geometric survey (which items are
reachable by >1 lane, decided before any damage is applied) and takes the
untouched sequential walk (ApplySequential) whenever nothing is shared, or a
new coordinated walk (ApplyWithSharedItems) that deposits each lane's
post-armour remainder into a shared item's pool and resolves it once every
contributing lane has arrived, splitting the leftover proportionally
(sum-then-split, order-independent by construction). A deterministic tie-break
(smallest first-contact Entry) breaks the rare "opposite order" deadlock two
non-convex shared items can create between two lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:16:53 +02:00
Meta Vens
cb71a3cfad Cut 12.3 fix batch: unify Lane and Lanes on one membership query
Self's review found that commit 4e6495dd's fix for F1 replaced the per-lane
forward-arithmetic defect with a second, still-independent membership
computation: Lane's own half-open admission (Lo <= s < Hi, from a cell's own
Extent) versus Lanes' ceil((cell.Lo - centreLateral) / laneSpacing) lane
index. The two agree only when a cell's own (Hi - Lo) equals the externally
supplied laneSpacing exactly in float -- the identical two-derivations trap
Cut 12.2's own fix batch already found and removed from Lane itself,
reintroduced one layer up. Confirmed reachable: on commit 4e6495dd, solid5
b=(0,1) s=0.49999997 n=1 leaves the ceil-based centre lane EMPTY while Lane(s)
itself finds all 5 cells of the row -- breaking R3 ("the centre lane always
meets metal", Commit's own guard) and the 12.2 placement contract.

FireControl.Lanes is now built entirely out of FireControl.Lane: it calls
Lane(hull, b, centreLateral + k * laneSpacing, ...) once per lane index, so
Lane(s) is literally this function's n = 0 case rather than a parallel
re-implementation of its rule. Commit's placement and Apply's damage split
now read one function. The narrower question left -- can two individually
correct Lane() calls, at two different lines, still walk the same cell? --
is resolved by ownership (the lane closest to the centre keeps a contested
cell; the centre lane, k = 0, never loses one), not by a competing formula,
so the centre lane's final cell set is always exactly Lane(centreLateral)'s
own output.

Lane() itself is unchanged (still called directly by Commit and by
FireControlCut12Tests' edge-seed and interval-edge tests, all still green:
HitsLandOnTheFacingEdge, LaneSeamIsHalfOpen, LaneNeverEmptiesAtAnIntervalEdge,
Cut12-edge-68442709, Cut12-edge-64078070).

Adds LanesQueryIsDisjointMatchesLaneAtCentreAndNeverEmptyOnHull: now that
Lanes is a public, pure geometric query, the boundary sweep is a behavioural
test of its own contract, no reflection -- over every cell's own shadow edge
and float neighbours, at exact-axis, 45-degree, |bx| > |by| bearings and
Soul's own two reproduction cases, spread 1-2: (a) no cell is walked by two
lanes, (b) the centre lane equals a plain Lane(s) call, (c) the centre lane
is never empty when the centre lateral is inside the hull's own shadow. Hand-
confirmed to fail on 4e6495dd's ceil-based Lanes (property (b), the same
input above) and to pass on this commit (fc123fix2-mut3.log).

Adds CommittedCellIsTheFirstCellTheCentreLaneDamages: an end-to-end check
through Fire -> Commit -> Apply (damageSpread 0, a single lane) that the
committed outcome.Cell is the first cell Apply damages.

Full suite: 286 tests, identical across 3 runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:24:55 +02:00
Meta Vens
4e6495dd63 Cut 12.3 fix batch: F1, decide lane membership once per cell
Soul's second pass (CONFIRMED): even after the F2 shadow-width fix, Apply's
per-lane loop still called Lane() once per lane index, each call re-deriving
its own lateral s(li) = Lateral + (li - n) * laneSpacing by forward float
addition, then testing that value against a cell's Extent interval
independently of every other lane. Two lane lines are only exactly
laneSpacing apart in real arithmetic; accumulating the offset by repeated
addition can shift a side lane by an ulp or two, letting it land inside a
neighbouring cell's half-open interval that its own unrounded value would
never reach -- the same "two derivations of one quantity" defect Cut 12.2's
own fix batch already removed from Lane itself, recurring one layer up in
Apply.

FireControl.Apply now calls a new Lanes() that decides each candidate cell's
lane index with one computation: k = ceil((cell.Lo - centreLateral) /
laneSpacing) is the unique integer for which centreLateral + k * laneSpacing
falls in the cell's own [Lo, Hi) -- derived once, from values no other lane's
arithmetic has touched. A single Ceiling call returns exactly one integer, so
a cell can be assigned to at most one lane by construction, not by float
luck. When k == 0 this is the same condition Lane() tests for centreLateral
itself, so the centre lane's cell set is unchanged (the 12.2 placement
contract, HitsLandOnTheFacingEdge, still holds). laneSpacing is still used,
unchanged, to place each admitted lane's own line for AlongBearing's slab
solve -- geometry, not membership.

Lane() itself is untouched (still used directly by Commit and by
FireControlCut12Tests' edge-seed tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:07:36 +02:00
Meta Vens
b82b7eab21 Cut 12.3 fix batch: F2 lane spacing by shadow width, F3 penetration boundary fixture
Operator ruling (2026-09-25): overlapping spread lanes should diffuse sideways
rather than double up. FireControl.Apply now spaces lanes by the shadow width
a cell casts at the committed bearing (Extent's own h, doubled) instead of by
one cell -- 1 at axis-aligned bearings (unchanged), up to sqrt(2) at 45
degrees. Reuses Extent, the one function that already owns h for Silhouette
and Lane, rather than re-deriving it a second way.

F3: PenetrationExcludesACellExactlyAtItsOwnBoundary's 1e6-durability marker
absorbed the whole shot before the walk could reach the boundary cell, so the
test could not fail under the `>=`-to-`>` mutant. The marker now carries 10
durability, leaving a remainder that would visibly damage the boundary cell if
the walk incorrectly included it. Adds
PenetrationExcludesCellsBeyondANonIntegerDepth, pinning the ordinary
"< penetration" comparison at a depth (1.5) that isn't an exact-integer
boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:32:02 +02:00
Meta Vens
70c3e0ccdb Cut 12.3, sub-cut A: armour absorbs first down the direct-hit lane
Deletes Entity.DamageSchematic (the even split over the whole hit shape)
and Entity.ApplyHit (the 0.5-step penetration march), replacing both with
Entity.Absorb (armour then item, per cell) and Entity.DamageHull, moved
verbatim from DamageSchematic's own body. FireControl.Apply now builds
2n+1 parallel lanes from the committed Bearing/Lateral (Q12-2 = A),
shares damage evenly over the lanes that meet metal, walks each lane in
order via the already-verified FireControl.Lane, absorbing cell by cell
up to the penetration depth, and sends every lane's own remainder to the
hull where it ends (Q12-3 = A). Splash is rewritten onto Absorb/DamageHull
(arithmetically identical to before) since DamageSchematic is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:55:59 +02:00
Meta Vens
ca1670a216 Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):

- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
  now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
  NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
  Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
  item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
  heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
  scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).

The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
Meta Vens
d38efba3a3 Cut 12.2 Soul fix batch 2, S1/S3: one shadow authority in Lane, sort without a comparer
S1 (HIGH, crash in play): Lane's own shadow prefilter (a re-derivation of
Silhouette's centre/h) and the independent SlabAlongB 2-axis intersection test
could disagree at a float edge, rejecting a cell the shadow sum had already
counted metal for -- an empty lane after a hit passed its roll (R3 violation),
reproducible through Zone.Update. Both are replaced by one shared admission
test (Extent(c, axis), the exact support of a unit square along an axis) that
Silhouette and Lane now both call; SlabAlongB is deleted, and its geometry is
kept only as a non-rejecting AlongBearing() that computes entry/exit for
already-admitted cells (the doc's own equivalence proof -- "s in the shadow" is
exactly "the line intersects" -- makes rejection there unreachable by
construction, not merely unlikely).

S3 (allocation): Interval and LaneCell now implement IComparable<T> (same
order as the deleted IntervalByLoComparer/LaneCellComparer) so Silhouette's and
Lane's sorts use Array.Sort(T[], int, int), which resolves T's own comparison
at JIT time, instead of the (T[], int, int, IComparer<T>) overload, which
wraps even a cached singleton comparer in a fresh delegate per call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:01:21 +02:00
Meta Vens
8bd25f6faa Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
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>
2026-09-24 23:08:19 +02:00
Meta Vens
efe4f7c954 Cut 12.2 Stryker fix batch: one range, five real gaps
The first Stryker pass (375 mutants, 69 survivors on this cut's own changed
lines) found one real split-authority bug and several untested edges:

- HitProbability recomputed target.Position - source.Position a second time
  for PSpread's range, a second place the same subtraction could flip sign.
  PFire now returns its own gate-computed range by out parameter; HitProbability
  reads that one value instead of a second copy.
- TravelDirection's point-blank fallback and Fire's null-target guard around it
  were only ever exercised at shooter/target pairs where the real direction
  happened to equal the (0,1) fallback by coincidence.
- diagnostic.PFire (presentation) was only ever checked at PSensor == 1, where
  Accuracy * PSensor and Accuracy / PSensor agree.
- CommitProbability's four-factor product was only ever checked with
  pDeviation == 1 (no fixture moved a target between fire and commit), where
  * and / by 1 agree.
- ResolveAimPoint's zero-cells fallback (an aimed item that no longer occupies
  any cell) was never reached; every fixture's marker survives to be hit.

New tests: TravelDirectionMatchesThePredictedInterceptAndFallsBackAtPointBlank,
DiagnosticPFireIsAccuracyTimesPSensor, CommitProbabilityIsTheExactProductOfItsFourFactors,
AimPointFallsBackWhenTheAimedItemNoLongerOccupiesAnyCell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:25:15 +02:00
Meta Vens
7dbb3bcf7a Cut 12.2: where a hit lands is priced and drawn from one silhouette
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>
2026-09-24 20:40:50 +02:00
Meta Vens
b851b0d1c3 Cut 12.1: one frame owner, and the dead outcome flag
Entity.ToSchematic(float2) is the one schematic-frame transform;
Entity.ApplyHit's penetration march and FireControl.Splash's
directional half both call it instead of each carrying an inline
forward/right copy. ShotOutcome.Aimed and its plumbing (Q12-4 = A) are
deleted: the aimed local/flag in Commit, MakeOutcome's aimed
parameter, and the AimedCells half of ResolveAimPoint's tuple, which
now returns float2 directly.

No behaviour change. All 244 tests pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 18:55:53 +02:00
Meta Vens
b0fd3cd06c Cut 11 second round: one owner for PBase, and tests for what the 51 survivors showed
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>
2026-09-22 16:39:52 +02:00
Meta Vens
a6543af741 Cut 11.4: behavioural tests for what Stryker found undefended, and Soul C4
Nine tests, each stating as behaviour a rule a real Stryker mutant survived:

- A shield that absorbs must eventually run dry, on both the discrete path and
  Splash's (whose absorb branch had no coverage at all). Deleting Shield.TakeHit
  survived because earlier tests asserted only that the hull was untouched.
- Splash damages the side the blast came from, across five facings and both
  sides. Flipping the sign of `right` survived every suite, and it survives at
  the default facing (0, 1) too: the flip only changes the damaged side when a
  ship faces more along world x than z. So the test spins the target through
  facings where it bites. The current code is correct; it can no longer regress.
- Different zones roll differently. Replacing `CombatSeed *` with `/` dropped the
  zone from the seed and survived, because the die test checks one zone only.
- A shot whose target left the zone resolves as a miss.

That last one is a real defect (Soul C4), fixed here. Step rewrote the outcome
only for uncommitted shots, so a hit committed inside the horizon and then
orphaned by its target dying was republished as a hit -- a hit marker on a
corpse. The resolution is now a fresh miss; the committed outcome is left
untouched, since R4 makes it immutable.

The fixture puts the shooter off the origin on purpose: at float3.zero,
`target - source` equals `target + source`, which is how four position mutants
survived.

240 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:06:33 +02:00
Meta Vens
b15cbbe5de Cut 10: the hot path stops looking like, and costing like, a diagnostic
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>
2026-09-22 15:18:12 +02:00
Meta Vens
6db6dc78f3 Cut 9: the die is not a die
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>
2026-09-22 02:33:22 +02:00
Meta Vens
72c0109cf1 Add live fire control diagnostics 2026-09-20 21:58:44 +02:00
Meta Vens
d528d88883 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