1850 lines
110 KiB
Markdown
1850 lines
110 KiB
Markdown
# Fire Control: Cut Map
|
||
|
||
Date: 2026-09-19
|
||
|
||
Status: Imagination pass, cut map. Nothing here has landed. Ends are owned by
|
||
`docs/three-gates-scope.md` ("Fire control") and
|
||
`docs/shield-presentation-contract.md` ("The commit window"); this document owns
|
||
the means.
|
||
|
||
Anchors are against `codex/item-provenance` HEAD `8a00fc92`, read-only. Nothing was
|
||
built in the main tree and Unity was not run. Claims marked **(probe)** were measured
|
||
by running `tools/AetherDb` (`census`, `hardpoint-fit`) against the live catalog
|
||
`GameData/Aetheria.cc` from the free worktree `F:\Projects\Aetheria-stats`, CultLib
|
||
pinned at the shared `45c2f40` worktree. Claims marked **(read)** come from reading
|
||
source at the cited anchor.
|
||
|
||
**This map supersedes Cut 2 of `docs/headless-playground-cut.md`**
|
||
(`:490-810`, anchored to `562cdcf2`). That draft is now history for three reasons:
|
||
its anchors predate the stats-and-power campaign, which moved most of the code it
|
||
cites; it predates the commit-window ruling of 2026-09-18, which changes the shape of
|
||
a shot from an instant to a lifecycle; and it predates the per-hardpoint arc override
|
||
and 360-degree turret rulings. Its substrate probes are still good and are carried
|
||
forward below with citations. Where the two disagree, this document wins; nobody
|
||
should be reading two live designs.
|
||
|
||
**Scope: fire control only.** What a presentation does with a committed outcome is the
|
||
capability/presentation seam's business (`docs/shield-presentation-contract.md`).
|
||
Faction territory is the campaign after this. §6 names every seam and stops there.
|
||
|
||
---
|
||
|
||
## Rulings this map is built on (do not re-litigate)
|
||
|
||
From `docs/three-gates-scope.md:47-99` and `docs/shield-presentation-contract.md:34-57`:
|
||
|
||
- **R1. Hit detection goes.** Operator, 2026-09-17: "We wanted to get rid of hit
|
||
detection anyway... Just roll the dice based on weapon stats, targeting system stats
|
||
(new subsystem) and sensor state."
|
||
- **R2. Targeting systems are a new subsystem**, interior `Tool` gear.
|
||
- **R3. A shot resolves on arrival**, hit chance reduced by how far the target deviated
|
||
from the predicted intercept. Manoeuvring is evasion.
|
||
- **R4. The outcome commits a short fixed time before impact.** Operator, 2026-09-18:
|
||
"That hybrid is the way." Deviation feeds the roll up to that horizon; after it the
|
||
result stands and the rest of the flight is choreography. The horizon is one authored
|
||
fire-control setting. A commit is authoritative; presentations perform it and never
|
||
predict.
|
||
- **R5. Subsystems reveal between `TargetArmorInfoThreshold` and
|
||
`TargetGearInfoThreshold`**, and both the player and the AI may aim at a revealed
|
||
subsystem.
|
||
- **R6. Per-hardpoint firing arcs.** Mount direction from the item's `ItemRotation`, as
|
||
reaction thrusters do; default 120 degrees from `GameplaySettings`; an optional
|
||
per-hardpoint override; turrets 360.
|
||
- **R7. The simulation is 2D.** The 3D is set dressing.
|
||
- **R8. Unity weapon effects and `HullCollider` become presentation only.** Their
|
||
`Physics.*` queries stop deciding damage.
|
||
- **R9. Every weapon type in the catalog's history stays**
|
||
(`docs/headless-playground-cut.md` fork E). Nothing is deleted for being unused.
|
||
|
||
From `docs/stats-and-power-cut.md:996-1030`, the seam the stats campaign left:
|
||
|
||
- **R10. Fire control must snapshot what it rolled over.** The resolver hands out a
|
||
current value; it does not promise that value is still current a second later.
|
||
- **R11. A starved weapon does not fire** (Cut 4 of that campaign: no fraction of a
|
||
shot). A degraded shot instead is a ruling against that map, not a thing to work
|
||
around here. See Q6.
|
||
|
||
---
|
||
|
||
## 0. Substrate
|
||
|
||
### 0.1 What decides a hit today, end to end
|
||
|
||
Nothing in `ServerShared` does. The whole chain is Unity **(read)**:
|
||
|
||
1. `InstantWeapon.Execute` (`Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs:216-237`)
|
||
walks the burst and raises `OnFire` (`:228`), a parameterless `event Action` declared
|
||
at `:73`. It carries no outcome and nothing in `ServerShared` subscribes.
|
||
2. `Assets/Scripts/Gameplay/EntityInstance.cs:203-204` subscribes and calls
|
||
`InstantWeaponEffectManager.Fire(weapon, item, source, target)`
|
||
(`Assets/Scripts/Gameplay/Weapons/InstantWeaponEffectManager.cs:7`). The constant
|
||
family goes the same way at `:232-235`.
|
||
3. The manager spawns an effect and copies the weapon's resolved numbers onto it.
|
||
`ProjectileManager.cs:18-25` jitters the barrel forward vector by
|
||
`Random.Range(±Spread/2)` on all three Euler axes — that is the entire aiming model.
|
||
4. The effect raycasts. `Projectile.cs:58`, `GuidedProjectile.cs:153`, `Laser.cs:40`,
|
||
`Lightning.cs:34`, `ConstantLaser.cs:74`, `ConstantLightning.cs:54`,
|
||
`HitscanEffect.cs:31`, `Mine.cs:68,92`, plus `Projectile.cs:114` for airburst.
|
||
5. On a hull collider it calls `HullCollider.SendHit`
|
||
(`Assets/Scripts/Gameplay/HullCollider.cs:18-30`), or `SendSplash` (`:32-41`).
|
||
6. `EntityInstance.cs:316-396` subscribes to those subjects, converts a UV texture
|
||
coordinate into schematic cells, expands by `DamageSpread`, marches a penetration ray,
|
||
and calls the local function `DamageSchematic` (`:277-314`), which is the only writer
|
||
of `Armor`, item `Durability` and `Hull.Durability` from combat.
|
||
7. Death: `EntityInstance.cs:405-439` subscribes to `HullDamage`, rolls loot with
|
||
`UnityEngine.Random`, and calls `entity.Zone.Entities.Remove(entity)` (`:437`).
|
||
|
||
**Authority versus presentation in that chain.** Everything from step 3 down is
|
||
authority wearing a presentation costume. The shield branch repeated in all seven
|
||
effects (`Projectile.cs:60-80` and its copies) is the absorb rule, duplicated seven
|
||
times. `DamageSchematic` is the damage rule. `EntityInstance.cs:437` is the death rule.
|
||
Genuine presentation is narrow: trail rendering, line renderers, the lightning compute
|
||
animation, hit effect prefabs, `ShieldManager.ShowHit`, and the destroy effect.
|
||
|
||
**The 2D/3D violation.** `EntityInstance.cs:323` and `:380` call
|
||
`transform.InverseTransformDirection` to get a hit direction into the ship's frame. That
|
||
is a 3D rotation standing in for the planar rotation R7 says the simulation is made of.
|
||
|
||
### 0.2 What already exists of targeting
|
||
|
||
- `Entity.Target` — `Assets/Scripts/ServerShared/Entity.cs:46`, a
|
||
`ReactiveProperty<Entity>`. Cleared when the target leaves `VisibleEnemies` (`:196`,
|
||
`:222`). `TargetRange` is recomputed each tick at `:920`.
|
||
- `Entity.EntityInfoGathered` — `Entity.cs:57`, a `ReactiveDictionary<Entity, float>` in
|
||
[0,1], accumulated with decay by `Sensor.Execute`
|
||
(`Assets/Scripts/ServerShared/Behaviors/Sensor.cs:152-184`).
|
||
- `TargetDetectionInfoThreshold` gates `VisibleEnemies` at `Entity.cs:232-246`.
|
||
- **`TargetArmorInfoThreshold` and `TargetGearInfoThreshold` are authored and read by no
|
||
code.** They are declared at `Assets/Scripts/ServerShared/Settings.cs:212-213` and
|
||
appear nowhere else in `Assets/Scripts` **(read)**. R5's reveal tiers are unwired
|
||
settings.
|
||
- `LockWeapon` — `Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs`. Integrates a
|
||
0..1 `_lock` scaled by `pow(EntityInfoGathered[target], SensorImpact)` (`:92`), gated by
|
||
a `LockAngle` cone off `Entity.LookDirection` (`:88-89`). `CanFire` (`:56`) requires
|
||
`_lock > .99f` and range within `[MinRange, Range]`. This is the closest thing in the
|
||
codebase to a to-hit probability, and it is per-weapon rather than per-ship.
|
||
- `TurretController` — `Assets/Scripts/ServerShared/Behaviors/TurretController.cs`.
|
||
`TurretControllerData` has no fields (`:15-25`). It writes `Entity.LookDirection` to a
|
||
first-order intercept (`:67-72`) and fires each weapon when
|
||
`dot(x.Direction, Entity.LookDirection) > .99f` (`:81-83`).
|
||
- `Sensor` — sensitivity, ping boost/energy/visibility/range/cooldown, all
|
||
`PerformanceStat` (`Sensor.cs:16-34`).
|
||
- Player target selection: `Assets/Scripts/Gameplay/ActionGameManager.cs:368-403`
|
||
(reticle, nearest, next, previous). Player fire is not arc-gated at all.
|
||
- **No targeting-system item, behavior or stat exists anywhere in code or catalog.** A
|
||
repo-wide grep for `Targeting|FireControl|Accuracy|Aim` finds one `#region Targeting`
|
||
label at `ActionGameManager.cs:368` and nothing else; the same grep over the catalog
|
||
returns zero **(probe)**.
|
||
|
||
### 0.3 Where hardpoint rotation and arcs live
|
||
|
||
- `HardpointData` — `Assets/Scripts/ServerShared/ItemData.cs:538-545`: `Type`,
|
||
`Position`, `Shape`, `Transform` (a prefab node name), `Rotation` (`ItemRotation`),
|
||
`Armor`. **There is no arc field on `HardpointData`, `WeaponData`, or any effect.**
|
||
A per-hardpoint override attaches here, as catalog data already authored per hull.
|
||
- `ItemRotation` is four-way (`Assets/Scripts/ServerShared/Enums.cs:7`);
|
||
`Extensions.cs:135-166` gives `None` = forward, `Reversed` = aft, `Clockwise` = right,
|
||
`CounterClockwise` = left. `Agent.cs:72` reads `Clockwise` as right and `Thruster` uses
|
||
the same convention for mount direction, which is the precedent R6 names.
|
||
- `Behavior.Direction` (`Assets/Scripts/ServerShared/Behaviors/Behaviors.cs:24-44`) has
|
||
two paths. The first (`:30-34`) reads `Entity.HardpointTransforms`; the second
|
||
(`:37-38`) is the item-rotation fallback R6 wants.
|
||
- **`Entity.HardpointTransforms` (`Entity.cs:58-59`) is written only by Unity**, at
|
||
`EntityInstance.cs:514-517`, from the first `WeaponHardpoint.FiringPoint` barrel's
|
||
world forward. Headless it is empty. That single dictionary is why
|
||
`Combat.cs:100-102` throws `KeyNotFoundException` on the first combat tick outside
|
||
Unity, and why the simulation's own notion of where a gun points is a readback from a
|
||
renderer.
|
||
- Barrel swing is `ArticulationPoint`, yawing and pitching toward `LookAtPoint`
|
||
(`EntityInstance.cs:400-403`, `:519-520`). Authored limits on the Longinus energy
|
||
barrels are yaw [-1, 1] — effectively fixed forward — while the Turret prefab's yaw
|
||
pivot is [-720, 720] (`docs/headless-playground-cut.md:533-545`, prefab probe, not
|
||
re-measured here). That is presentation and stays.
|
||
- `AgentFiringMinDot` (`Settings.cs:218`) is **declared and read by nothing** **(read)**.
|
||
|
||
### 0.4 What the catalog ships in weapons
|
||
|
||
**(probe)**, `tools/AetherDb census` and `hardpoint-fit` over `GameData/Aetheria.cc`:
|
||
51 designs, 37 products, of which 18 are weapons.
|
||
|
||
| Hardpoint kind | Designs | Sellers |
|
||
|---|---|---|
|
||
| Ballistic | 6 | Zhestokost 2, Lightsail 1, AU 1, DME 1, unsold 1 |
|
||
| Energy | 7 | NiteLife 3, Lucent 1, Adrasteia 1, Alakrita 1, unsold 1 |
|
||
| Launcher | 5 | DME 1, R&D 1, unsold 3 |
|
||
|
||
Unsold, therefore unspawnable: `Autocannon`, `SRMM72`, `LRMM72`, `plight`, `pswarm`.
|
||
The earlier behaviour-class breakdown (8 `ProjectileManager`, 3 `LaserManager` at
|
||
velocity 0, 5 `GuidedProjectileManager`, 1 `LightningGunManager`, and **no design using
|
||
`ConstantWeaponData`, `HitscanEffect`, `Mine`, airburst or MIRV**) is carried from
|
||
`docs/headless-playground-cut.md:500-506` and was not re-measured; the census agent
|
||
could not reproduce it because `census` buckets by `HardpointType` and the one command
|
||
that prints behaviour fields was blocked. **Cut 4's safety argument depends on that
|
||
claim, so Hands re-measures it before Cut 4** (§4, First).
|
||
|
||
**Stats a roll can read today** (`Weapon.cs:22-68`, all `PerformanceStat` unless noted):
|
||
`Damage`, `Penetration`, `DamageSpread`, `MinRange`, `Range`, `Energy`, `Heat`,
|
||
`Visibility`, `Spread` (an angular cone in degrees), `Velocity`; plus `DamageCurve`
|
||
(a `BezierCurve`, damage against normalized range), `MagazineSize`, `ReloadTime`,
|
||
`AmmoType`. `InstantWeaponData` adds `Count`, `BurstTime`, `Cooldown`
|
||
(`InstantWeapon.cs:13-22`). `LockWeaponData` adds `LockSpeed`, `SensorImpact`,
|
||
`LockAngle`, `DirectionImpact`, `Decay` (`LockWeapon.cs:13-25`). The item carries
|
||
`WeaponRange`, `WeaponCaliber`, `WeaponType`, `WeaponFireTypes`, `WeaponModifiers`
|
||
(`ItemData.cs:476-492`).
|
||
|
||
Note against a misreading: **`DamageSpread` is not damage variance.** It is the
|
||
footprint, in schematic cells: `EntityInstance.cs:372-375` expands the hit shape
|
||
`round(hit.Spread)` times, where `hit.Spread` is the weapon's `DamageSpread`.
|
||
|
||
**What is missing from the authored data for a roll to be meaningful:**
|
||
|
||
- **No accuracy stat on anything.** No weapon, hull, cockpit or sensor contributes a
|
||
to-hit number. This is what R2's targeting system exists to supply, and it is the
|
||
whole reason the subsystem is required content rather than a nice-to-have.
|
||
- **No target-size term.** A roll that ignores how big the target is will feel wrong at
|
||
every range. The hull `Shape` and a cell size give an angular radius; `SchematicCellSize`
|
||
does not exist and must be authored (Q4).
|
||
- **No tracking or angular-velocity stat**, so nothing yet reads R3's deviation. The
|
||
targeting system supplies it.
|
||
- **Beams have nothing beam-specific.** `ConstantWeaponData` adds only `AmmoInterval`
|
||
(`ConstantWeapon.cs:15`); `Spread` is inherited and unread by any beam code, and
|
||
`Velocity` is meaningless for one. There is no dwell or wander stat. Unused today, so
|
||
Cut 4 gives beams the plainest possible rule and authors nothing new.
|
||
- **`ChargedWeaponData.ChargeFiringSpreadMultiplier` is a plain `float`**
|
||
(`ChargedWeapon.cs:34`), not a `PerformanceStat`, so unlike every other combat number
|
||
it cannot vary with quality or wear. Noted, not fixed here.
|
||
- **Lock is binary at 0.99** (`LockWeapon.cs:56`). There is no stat turning partial lock
|
||
into hit chance, and `GuidedWeaponData` (`Launcher.cs:37-55`) has no lock stats at all.
|
||
|
||
### 0.5 Can the headless sim step a full engagement today
|
||
|
||
No, and two specific things stop it **(read)**:
|
||
|
||
1. `Combat.cs:100-102` indexes `HardpointTransforms`, which only Unity writes (§0.3).
|
||
`Minion` enters `CombatState` as soon as `VisibleEnemies` is non-empty, so any NPC
|
||
that sees an enemy throws on the tick it sees them.
|
||
2. There is no owner between `InstantWeapon.OnFire` and `Entity.HullDamage`. The only
|
||
`ServerShared` writers of durability are the repair consumable
|
||
(`Behaviors.cs:67,72-73`).
|
||
|
||
Everything else is already there and already exercised.
|
||
`tests/Aetheria.Shared.Tests/IffAndCombatTests.cs:52` (`BuildWorld`) opens the real
|
||
catalog, builds an `ItemManager` and a `Zone`, and `:73` (`NewShip`) equips a gun on a
|
||
hardpoint and activates the ship — two ships, positions, look directions, sensor info,
|
||
targets and IFF, headless. `:241` and `:255` assert on `weapon.Progress` after
|
||
`Activate()`, because a cooldown starting is the only observable consequence of firing
|
||
that exists. That fixture is this campaign's harness; it does not need to be built.
|
||
|
||
`Zone.Update` (`Zone.cs:126-151`) steps orbits, belts, agents, then entities.
|
||
`ItemManager.Random` is still clock-seeded (`ItemManager.cs:19`), so a seeded-roll test
|
||
needs a seed argument — see Q5.
|
||
|
||
The xunit project is `tests/Aetheria.Shared.Tests/Aetheria.Shared.Tests.csproj`
|
||
(net10.0, xunit), referencing `Aetheria.Shared/Aetheria.Shared.csproj`
|
||
(netstandard2.1, `noEngineReferences` mirrored by
|
||
`Assets/Scripts/ServerShared/Aetheria.Shared.Unity.asmdef`). The mutation harnesses are
|
||
`tests/mutation_tests_*.py`, 17 of them, each patching a named source anchor and
|
||
asserting a named test goes red with a no-op control.
|
||
|
||
---
|
||
|
||
## Rulings (operator and Self, 2026-09-19)
|
||
|
||
- **Q4, unaided accuracy: really, really bad.** Operator: "Targeting with the mark one eyeball
|
||
should be really, really bad." A ship with no targeting system can hit something close,
|
||
slow and unaware, and little else. That makes the targeting subsystem **required equipment**
|
||
rather than an upgrade: every hull needs somewhere to put one, the catalog needs them at
|
||
several price points, and a stripped hull is a genuine handicap. Hands picks the other two
|
||
balance numbers; this one is authored to make unaided fire a last resort, and the operator
|
||
tunes it in play.
|
||
- **Q1, turrets: author `FiringArc: 360`.** No new type for what a number already says.
|
||
- **Q2, player fire is arc-gated** like everyone else's. Manual and programmatic must not be
|
||
two truths.
|
||
- **Q3, death removes the ship**, and loot stays as it is until the pickup cut.
|
||
- **Q5, seed the rolls in this campaign.** Two lines here beats waiting on an unstarted cut.
|
||
- **Q6, snapshot at fire, not at commit** (Self, accepting Imagination's counter-proposal to
|
||
the stats map's §8 seam). A shot carries the gun that fired it: its parameters freeze when
|
||
the trigger is pulled, and nothing about the shooter's heat, power or wear changes them
|
||
mid-flight. Evasion still decides the outcome, because deviation is measured at arrival
|
||
against the predicted intercept. `docs/stats-and-power-cut.md` §8 said "snapshot at commit";
|
||
that is superseded, and the resolver's promise (a current value, not a stable one) is
|
||
honoured either way.
|
||
- **Q7, mine proximity physics: leave it.**
|
||
|
||
## 0b. Identity, lifecycle, authority
|
||
|
||
One row per kind this campaign introduces. No cut below is mapped with a cell empty.
|
||
|
||
| Kind | What names it | What happens to it over time | Who decides |
|
||
|---|---|---|---|
|
||
| **A shot in flight** | `PendingShot`, a struct in `Zone.PendingShots` (a `List<PendingShot>`), identified by a monotonic `int ShotId` from `Zone`. Nothing outside `Zone` holds a reference. | Born in `FireControl.Fire` when a weapon's burst step fires, carrying a payload snapshot frozen at that instant. Ages by `dt` in `FireControl.Step`, called from `Zone.Update` after entities. Reaches its commit horizon, then its arrival time, then is removed the same tick it resolves. A shot whose source or target entity leaves the zone resolves as a miss and is removed. Never serialised: a zone saved mid-flight loses its shots, which is correct — a save is a scene boundary. | `Zone` owns the collection and the identity. `FireControl` owns the transitions. Nothing else may add, remove or mutate one. |
|
||
| **A roll** | Nothing. It is not a thing; it is one draw from `ItemManager.Random` inside `FireControl.Commit`, and only its result is named. | Exists for the duration of one call. Its inputs are the payload snapshot plus deviation measured at that instant; its output is written into the shot's `Outcome` and never recomputed. | `FireControl` alone. No behaviour, agent, presentation or UI may draw for a hit. |
|
||
| **A commit** | `ShotOutcome { int ShotId; Entity Source; Entity Target; EquippedItem Weapon; bool Hit; bool Shielded; EquippedItem Aimed; int2 Cell; float ArrivalIn; DamageType DamageType; }`, published on `Zone.ShotCommitted` (a `Subject<ShotOutcome>`). | Created once, at `ArrivalTime - CommitHorizon`, or at fire time when the flight is shorter than the horizon. Immutable from then on. Published once. Read by presentations and by nothing else. Dies with the shot. | `FireControl` creates it and is the only writer. Presentations are the only readers. **Nothing outside presentation may read a commit to change state** (`docs/shield-presentation-contract.md:57`). |
|
||
| **A targeting solution** | Not a stored object. `FireControl.HitProbability(weapon, source, target)` is a pure function, recomputed on demand. | Has no lifetime. AI reads it to decide whether to fire; the HUD reads it to draw a number; `FireControl.Fire` reads it once per shot and freezes the result into the payload. Two reads a tick apart may differ; nothing caches one. | `FireControl`. The targeting-system behaviour owns only its own resolved stats; it does not compute probability. |
|
||
| **Target data / knowledge of a subsystem** | `Entity.EntityInfoGathered[other]` (existing, `Entity.cs:57`) is the knowledge. `FireControl.IsRevealed(observer, item)` is the derived predicate over it. The current aim point is `Entity.TargetItem`, a `ReactiveProperty<EquippedItem>`. | Info accumulates and decays continuously under `Sensor` (`Sensor.cs:152-184`); ownership of that does not change. Reveal is **derived on every read**, never stored and never cached, so it falls back the moment info decays. `TargetItem` is written only by `Entity.TrySelectTargetItem`, nulled when `Target` changes, and **read through a derived accessor that returns null if the item is no longer revealed or no longer belongs to the target** — no loop clears it. | `Sensor` owns info. `FireControl` owns the reveal rule and the thresholds it reads. `Entity` owns the selection slot; `TrySelectTargetItem` is its only writer. Player input, `CombatState` and the test harness are its three callers. |
|
||
| **A hardpoint arc** | `HardpointData.FiringArc`, a `float` on catalog data (`ItemData.cs:538-545`), zero meaning "use the default". Resolved through `FireControl.ArcFor(item)`. | Authored once, in the catalog. Immutable at runtime. Read per shot and per AI fire decision; never stored on the entity. Mount direction is likewise derived per read from `Behavior.Direction`'s item-rotation path. | The catalog authors it; `FireControl` resolves and applies it; `GameplaySettings.FiringArc` supplies the default. **No prefab, transform, articulation limit or renderer may influence it.** |
|
||
|
||
---
|
||
|
||
## Cut order
|
||
|
||
1. **Mount direction and arc become simulation data.** Deletes `HardpointTransforms`
|
||
and the Unity readback; adds the arc gate; unbreaks headless combat.
|
||
2. **Targeting system and reveal.** The new subsystem, the reveal rule, the selection
|
||
slot, and the two dead settings wired up.
|
||
3. **Hit authority moves.** The Physics path in the four effects the catalog actually
|
||
uses is deleted and `FireControl` plus the shot lifecycle replaces it, in one cut, so
|
||
damage is never applied twice.
|
||
4. **The unused kinds join the same path.** Beams, hitscan, mines, splash and airburst
|
||
stop querying Physics. They keep existing (R9).
|
||
|
||
Cuts 3 and 4 cannot be swapped, and 3 cannot be split into "roll" and "delete the old
|
||
path": a cut boundary between them is a cut boundary with two damage owners.
|
||
|
||
---
|
||
|
||
## Cut 1. Mount direction and arc become simulation data
|
||
|
||
- **Repo/branch:** `Aetheria`, branch `codex/fire-control` from `codex/item-provenance`
|
||
`8a00fc92`. Depends on nothing.
|
||
- **First:** update `F:\Projects\Aetheria-stats` to HEAD (it sits 11 commits behind at
|
||
`c4f6029e`); capture `AetherDb census`, `loadout 1`, `dangling` as the before side.
|
||
|
||
**Deletes first:**
|
||
|
||
- `Assets/Scripts/ServerShared/Entity.cs:58-59` — `HardpointTransforms` (2).
|
||
- `Assets/Scripts/Gameplay/EntityInstance.cs:514-517` — its only writer (4).
|
||
- `Assets/Scripts/ServerShared/Behaviors/Behaviors.cs:30-35` — the readback branch in
|
||
`Behavior.Direction` (6). The item-rotation path at `:37-38` becomes the whole body.
|
||
- `Assets/Scripts/ServerShared/Agents/States/Combat.cs:100-102` — the
|
||
`HardpointTransforms` dot test (3).
|
||
- `Assets/Scripts/ServerShared/Behaviors/TurretController.cs:81-83` — the `.99f` dot
|
||
test (3).
|
||
- `Assets/Scripts/ServerShared/Settings.cs:218` — `AgentFiringMinDot`, read by nothing
|
||
(1).
|
||
|
||
**Adds:**
|
||
|
||
- `Assets/Scripts/ServerShared/FireControl.cs` — a new static class, this cut holding
|
||
only geometry: `MountDirection(EquippedItem)` (the planar unit vector from
|
||
`Behavior.Direction`), `ArcFor(EquippedItem)` and
|
||
`bool InArc(EquippedItem weapon, float3 toTarget)`, which tests
|
||
`dot(normalize(weapon.Direction.xz), normalize(toTarget.xz)) >= cos(radians(arc/2))`.
|
||
The test is planar (R7); target height never enters it. An arc of 360 or more passes
|
||
unconditionally.
|
||
- `HardpointData.FiringArc` (`ItemData.cs:545`, next `Key`) — full width in degrees,
|
||
`0` meaning "use `GameplaySettings.FiringArc`".
|
||
- `GameplaySettings.FiringArc = 120` (`Settings.cs`, after `:214`).
|
||
- `GameplaySettings.AgentMinHitProbability` is **not** added here; Cut 1's AI gate is
|
||
`InArc` alone, which is strictly closer to today's behaviour than a probability the
|
||
roll does not yet produce.
|
||
|
||
**Per-file changes (against `8a00fc92`):**
|
||
|
||
- `Combat.cs:100-102`: `shouldFire = FireControl.InArc(testWeapon.Item, toTarget)`. The
|
||
intercept prediction at `:84-98` stays — it still aims `LookDirection`, and under R3 it
|
||
becomes the predicted intercept a deviation is measured against.
|
||
- `TurretController.cs:81-83`: the same call. Under the default arc a turret hull, whose
|
||
`Direction` never rotates (`TurretController` writes only `LookDirection`), would
|
||
engage only within 60 degrees of its spawn facing. R6 says turrets are 360; the turret
|
||
hull's hardpoints author `FiringArc: 360`. See Q1.
|
||
- Catalog: author `FiringArc` on the `Turret` hull's two Ballistic hardpoints. Follows
|
||
the Cut C precedent — a Hands scratch console upserts, Unity and Studio closed.
|
||
|
||
**Authority map:**
|
||
|
||
- **Owner:** `FireControl` owns whether a weapon bears on a point. `HardpointData`
|
||
authors the arc; `ItemRotation` authors the mount direction.
|
||
- **Inputs:** the equipped item's hardpoint rotation, `HardpointData.FiringArc`,
|
||
`GameplaySettings.FiringArc`, the planar vector to the target.
|
||
- **Outputs:** one bool, consumed by `CombatState`, `TurretController`, and from Cut 3
|
||
by `FireControl.Fire`.
|
||
- **Derived state:** `Behavior.Direction` becomes purely derived from item rotation.
|
||
- **Forbidden writers:** no Unity transform, `ArticulationPoint`, `WeaponHardpoint` or
|
||
prefab node may influence aim. `Barrels`, `GetBarrel` and `ArticulationPoint` survive
|
||
as presentation and must never be read by `ServerShared`.
|
||
- **Shared paths:** player fire, AI fire and turret fire all go through `InArc`. Player
|
||
fire is arc-gated for the first time; see Q2.
|
||
- **Deletion line:** `HardpointTransforms` and its writer and readers are gone before
|
||
`FireControl.cs` is written.
|
||
|
||
**Verification:**
|
||
|
||
- builds: `Aetheria.Shared/Aetheria.Shared.csproj`, `tests/Aetheria.Shared.Tests`,
|
||
`tools/AetherDb`.
|
||
- tests, each named with the rule it pins:
|
||
- `ArcFollowsMountRotation` pins *mount direction is the item's rotation, not the
|
||
hull's facing*: a fixture hull with one weapon on a `None` hardpoint and one on a
|
||
`Clockwise` hardpoint; a target 90 degrees to starboard passes only for the
|
||
`Clockwise` weapon, a target dead ahead only for the `None` weapon. Mutation: make
|
||
`InArc` use `Entity.Direction`; swap `Rotate`'s `Clockwise`/`CounterClockwise` cases.
|
||
- `ArcBoundaryIsHalfWidth` pins *`FiringArc` is full width*: at 120, a target 59
|
||
degrees off passes and one at 61 does not. Mutation: compare against
|
||
`cos(radians(arc))`.
|
||
- `ArcIsPlanar` pins R7: a target inside the arc horizontally but far above the firer
|
||
still passes. Mutation: use the 3D vector.
|
||
- `HardpointOverrideBeatsDefault` pins *the per-hardpoint override wins*: the same
|
||
weapon on a hardpoint with `FiringArc: 360` passes at 170 degrees, and fails there on
|
||
a hardpoint with `0`. Mutation: ignore the field; treat `0` as an arc of zero.
|
||
- `CombatStateStepsHeadless` pins *the simulation decides aim without Unity*: a
|
||
`Minion` with a target in range steps through `Zone.Update` without throwing.
|
||
Mutation: restore the `HardpointTransforms` read.
|
||
- negative: `rg -n "HardpointTransforms" Assets/Scripts` empty;
|
||
`rg -n "ArticulationPoint|FiringPoint|WeaponHardpoint" Assets/Scripts/ServerShared`
|
||
empty; `rg -n "AgentFiringMinDot" Assets/Scripts` empty. (`\.99f` is **not** a safe
|
||
negative grep — it occurs legitimately in `LockWeapon.cs:56`. Grep the two files by
|
||
name instead.)
|
||
- operator: in Unity, side-mounted weapons still fire at targets abeam; a turret still
|
||
tracks all the way around; nothing fires through the hull.
|
||
|
||
**Risk:** this cut changes what the AI fires at before the roll exists, so combat feel
|
||
shifts once and shifts again in Cut 3. That is the price of deleting the obsolete
|
||
authority first, and it is the right price.
|
||
|
||
---
|
||
|
||
## Cut 2. Targeting system and reveal
|
||
|
||
- **Repo/branch:** continues Cut 1. Depends on Cut 1 (uses `FireControl.cs`), and on
|
||
provenance Cut C if that has not landed, because Cut C re-upserts every `ItemData`.
|
||
- **First:** `AetherDb census` and `loadout 1` over several seeds, as the before side.
|
||
|
||
**Deletes first:** nothing. This cut is entirely additive, which needs its account
|
||
(CODE IS A LIABILITY): the invariant it buys is R5, which cannot be purchased by moving
|
||
authority, because no authority for it exists — two authored settings are read by
|
||
nothing and there is no subsystem to carry accuracy. Cut 3 is where the subtraction is.
|
||
|
||
**Adds:**
|
||
|
||
- `Assets/Scripts/ServerShared/Behaviors/TargetingSystem.cs` —
|
||
`TargetingSystemData : BehaviorData` (next free `Union` index on `BehaviorData`; Hands
|
||
reads the current list in `Behaviors.cs` rather than trusting the number 39 quoted in
|
||
the superseded draft) plus a `TargetingSystem : Behavior` that resolves and exposes:
|
||
- `Accuracy` (0..1) — the ceiling on hit probability.
|
||
- `Resolution` (detection threshold..1) — the info level at which sensor state stops
|
||
limiting hits.
|
||
- `Precision` (0..1) — the chance a hit lands on the aimed item rather than a random
|
||
cell.
|
||
- `Tracking` — how much deviation it forgives (consumed in Cut 3).
|
||
|
||
All four are `PerformanceStat`, so lot quality, wear, heat and `PowerSupply` reach them
|
||
through `EquippedItem.Evaluate` — that is the brownout ruling applied to accuracy, and
|
||
it is why a starved targeting system rolls worse instead of switching off. Energy and
|
||
heat come from the existing `EnergyDraw` and `Heat` behaviours on the item; the
|
||
subsystem adds no power machinery of its own.
|
||
- `GameplaySettings.UnaidedAccuracy` — what an entity with no working targeting system
|
||
fires with. Such an entity uses `Resolution = 1` and `Precision = 0`: it can still
|
||
select a revealed item, and its hits scatter.
|
||
- `FireControl.IsRevealed(Entity observer, EquippedItem item)` — derived, never cached:
|
||
1. Rank the target's non-hull equipment: hardpoint-mounted first, then size
|
||
descending, then equipment index.
|
||
2. Item `i` of `N` is revealed when
|
||
`info >= lerp(TargetArmorInfoThreshold, TargetGearInfoThreshold, i / max(1, N - 1))`.
|
||
|
||
This is the first and only reader of `Settings.cs:212-213`.
|
||
- `Entity.TargetItem` (`ReactiveProperty<EquippedItem>`, beside `Target` at
|
||
`Entity.cs:46`) and `bool Entity.TrySelectTargetItem(EquippedItem)`, the only writer.
|
||
It accepts null, or an item of `Target.Value` that `IsRevealed`. A `Target` change
|
||
nulls it. The read accessor returns null when the item is no longer revealed, so decay
|
||
drops the aim point without a loop.
|
||
- Player input: a select/cycle command beside `ActionGameManager.cs:368-403`, calling
|
||
`TrySelectTargetItem`. The target schematic
|
||
(`Assets/Scripts/UI/HUD/SchematicDisplay.cs:109-119`, which today lists only weapon
|
||
items) shows revealed items and marks the selection. That is presentation; it decides
|
||
nothing.
|
||
- AI: `CombatState` selects the revealed target `Weapon` item with the highest
|
||
`RangeDamagePerSecond(range)`, or null.
|
||
- Catalog: two targeting-system designs, 1-cell and 2-cell, each with products from at
|
||
least the manufacturers that sell the capacitor, so `IsAvailable` finds one in every
|
||
galaxy. `LoadoutGenerator.FillInterior` picks one with `required: true` for every
|
||
entity with `Weapons` — the starting LonginusX, NPC ships and turrets. Zenith, with no
|
||
weapon hardpoints, gets none.
|
||
|
||
**Authority map:**
|
||
|
||
- **Owner:** `FireControl` owns the reveal rule. `Entity` owns the selection slot.
|
||
`TargetingSystem` owns nothing but its own resolved stats.
|
||
- **Inputs:** `EntityInfoGathered`, the two threshold settings, the target's equipment
|
||
list and item sizes.
|
||
- **Outputs:** a bool per (observer, item); a nullable `EquippedItem` per entity.
|
||
- **Derived state:** reveal is derived on read. `TargetItem`'s *validity* is derived;
|
||
only the slot is stored.
|
||
- **Forbidden writers:** the HUD may not write `TargetItem` except through
|
||
`TrySelectTargetItem`; no code may store a reveal result; no code may read the two
|
||
thresholds except `IsRevealed`.
|
||
- **Shared paths:** player selection, AI selection and the test harness use one writer.
|
||
- **Deletion line:** none, and that is stated rather than hidden.
|
||
|
||
**Verification:**
|
||
|
||
- builds: as Cut 1.
|
||
- tests:
|
||
- `SelectionNeedsReveal` pins *you cannot aim at what you have not resolved*: below
|
||
the item's tier `TrySelectTargetItem` returns false; above it, true; a target change
|
||
nulls it; info decaying below the tier makes the read null. Mutation: `IsRevealed`
|
||
returns true; the `Target` change skips the null; the accessor returns the stored
|
||
value without re-checking.
|
||
- `RevealOrder` pins *the big obvious things resolve first*: hardpoint items reveal
|
||
before interior items, larger before smaller. Mutation: drop the ordering.
|
||
- `RevealSpansBothThresholds` pins *the tiers are the authored ones*: with N items,
|
||
the first reveals at `TargetArmorInfoThreshold` and the last at
|
||
`TargetGearInfoThreshold`. Mutation: swap them; use the detection threshold.
|
||
- `UnaidedFiresWorse` pins *the subsystem is what supplies accuracy*: an entity with
|
||
no targeting system resolves `UnaidedAccuracy` and `Precision` 0; one with a
|
||
destroyed or offline system resolves the same. Mutation: fall back to the system's
|
||
stats when offline.
|
||
- `StarvedTargetingRollsWorse` pins the brownout ruling on accuracy: with an
|
||
`Accuracy` stat carrying a `PowerSupply` term, a half-grant halves it rather than
|
||
switching the item off. Mutation: gate on `PowerSupply <= 1e-4`.
|
||
- `EveryArmedLoadoutGetsATargetingSystem` pins the required-item rule across seeds.
|
||
Mutation: drop `required: true`.
|
||
- negative: `rg -n "TargetArmorInfoThreshold|TargetGearInfoThreshold" Assets/Scripts`
|
||
returns `Settings.cs` and `FireControl.cs` **only**.
|
||
- operator: `AetherDb census` shows 2 more designs; `loadout 1` shows one more interior
|
||
item per armed entity; `dangling` is 0; reopen reports 0 schema issues; CultCache
|
||
Studio renders the new behaviour union (an operator check, because Studio's handling of
|
||
a new union index is not something this map can promise).
|
||
|
||
**Risk:** `LoadoutGenerator.EquipHardpoints` can already fit the Tractor Beam into
|
||
LonginusX's only Sensors slot, leaving an NPC that gathers no info and therefore reveals
|
||
nothing and hits nothing (`docs/headless-playground-cut.md:520-523`). Check `loadout`
|
||
over several seeds. The fix is data or `EquipHardpoints`, not fire control.
|
||
|
||
---
|
||
|
||
## Cut 3. Hit authority moves
|
||
|
||
The rebuild. The old path is deleted and the new owner added in the same cut, because
|
||
any boundary between them is a boundary with two damage owners.
|
||
|
||
- **Repo/branch:** continues Cut 2. Depends on Cuts 1 and 2.
|
||
- **First:** capture a Unity play smoke of current combat feel (kill time against one
|
||
NPC, roughly) as the before side, since this cut changes feel more than anything in
|
||
the campaign.
|
||
|
||
**Deletes first** (paths and line counts against `8a00fc92`):
|
||
|
||
- `Assets/Scripts/Gameplay/EntityInstance.cs:277-396` — `DamageSchematic` and both
|
||
`HullCollider` subscriptions (120).
|
||
- `Assets/Scripts/Gameplay/HullCollider.cs:13-14,18-41,48-65` — `Hit`, `Splash`,
|
||
`SendHit`, `SendSplash` and both argument classes (~45 of 65). `OnCollisionEnter`
|
||
(`:43-46`) stays; ship collision is a deferred Unity-physics surface.
|
||
- `Assets/Scripts/Gameplay/Weapons/Projectile.cs:57-103` — the raycast, shield branch and
|
||
`SendHit` (47). `:109-122` (airburst) is Cut 4's.
|
||
- `Assets/Scripts/Gameplay/Weapons/GuidedProjectile.cs:152-189` — the same (38).
|
||
- `Assets/Scripts/Gameplay/Weapons/Laser.cs:40-75` — the same; the `LineRenderer`
|
||
endpoint writes at `:59,70,77` are presentation and are rebuilt from the outcome (36).
|
||
- `Assets/Scripts/Gameplay/Weapons/Lightning.cs:34-72` — the same; the endpoint capture
|
||
at `:67-71` stays as a visual (39).
|
||
- `Assets/Scripts/Gameplay/EntityInstance.cs:411-431` — the `UnityEngine.Random` loot
|
||
roll, **only if Q3 is answered (a)**; otherwise this cut leaves it and the seam is
|
||
named in §6.
|
||
|
||
**Adds:**
|
||
|
||
- `Entity.DamageSchematic(float damage, Shape hitShape)` — the rule moved verbatim from
|
||
`EntityInstance.cs:277-314`, now a method on `Entity` in `ServerShared`, writing
|
||
`Armor`, item `Durability`, `Hull.Durability` and raising the existing `ArmorDamage`,
|
||
`ItemDamage` and `HullDamage` subjects. Not a redesign: the same arithmetic, the same
|
||
order, the same thresholds.
|
||
- `Entity.ApplyHit(...)` — the shape construction moved from
|
||
`EntityInstance.cs:334-394`, with two changes forced by R7: the hit cell comes from the
|
||
rolled `Cell` rather than a UV texture coordinate, and the penetration march rotates
|
||
the firer-to-target vector into the target's frame with a planar rotation by
|
||
`-target.Direction` instead of `transform.InverseTransformDirection`.
|
||
- `FireControl.HitProbability(weapon, source, target)` — pure, no draw:
|
||
- Zero, with no draw consumed, when there is no target; the target is not in
|
||
`VisibleEntities`; range is outside `[MinRange, Range]`; a `LockWeapon` is not
|
||
locked; or `InArc` fails.
|
||
- Otherwise `p = Accuracy * pSensor * pSpread`, where
|
||
`pSensor = saturate(unlerp(TargetDetectionInfoThreshold, Resolution, info))` and
|
||
`pSpread = Spread > 0 ? saturate(angularRadius / (Spread / 2)) : 1`, with
|
||
`angularRadius = degrees(atan(0.5 * max(Shape.Width, Shape.Height) * SchematicCellSize / range))`.
|
||
- `SchematicCellSize` is the one new tuning constant (Q4).
|
||
- `FireControl.Fire(weapon, item, source)` — called once per burst step from
|
||
`InstantWeapon.Execute` (replacing the bare `OnFire?.Invoke()` at
|
||
`InstantWeapon.cs:228`). It computes the predicted intercept, the flight time
|
||
(`range / Velocity`, or zero when `Velocity` is at or near zero), **freezes the payload
|
||
snapshot** (R10) and appends a `PendingShot` to `Zone.PendingShots`. The snapshot holds
|
||
`Damage`, `Penetration`, `DamageSpread`, `DamageType`, `Aimed`, the resolved targeting
|
||
stats, and the intercept point. Nothing downstream re-evaluates a stat.
|
||
- `FireControl.Step(Zone, dt)` — called from `Zone.Update` after the entity loop
|
||
(`Zone.cs:150`). For each pending shot: age it; at `ArrivalTime - CommitHorizon`
|
||
compute deviation, roll, write `Outcome`, publish `Zone.ShotCommitted`; at
|
||
`ArrivalTime` apply the outcome and remove the shot. A shot whose flight is shorter
|
||
than the horizon commits at fire time, in the same call as `Fire` — that is every
|
||
velocity-0 weapon, which is three of the catalog's eighteen.
|
||
- `GameplaySettings.CommitHorizon` — R4's authored horizon, one number, in seconds.
|
||
- The deviation term: `pDeviation` folds in how far the target is from the predicted
|
||
intercept at commit time, forgiven by the targeting system's `Tracking`. This is R3's
|
||
evasion, and the commit horizon is what bounds how late it counts.
|
||
- The roll: one draw against `p`. On a hit, a second draw against `Precision` decides
|
||
where — on `Aimed`'s footprint cells on success, otherwise on one cell drawn uniformly
|
||
over the hull `Shape`. An active shield that `CanTakeHit` takes the hit instead, and
|
||
the outcome is marked `Shielded`; that rule now exists once, in `FireControl`, rather
|
||
than seven times in the effects.
|
||
- `Zone.ShotCommitted` and `Zone.ShotResolved` (`Subject<ShotOutcome>`), the presentation
|
||
feed.
|
||
- `InstantWeapon.OnFire` becomes `event Action<int>` carrying the `ShotId`, so the
|
||
Unity manager can bind its effect to the shot it belongs to.
|
||
|
||
**Per-file changes:**
|
||
|
||
- `InstantWeapon.cs:228`: `OnFire?.Invoke(FireControl.Fire(this, Item, Entity))`.
|
||
Charge multipliers already fold into `Damage` and `Spread` before this point.
|
||
- `Combat.cs:100-102`:
|
||
`shouldFire = FireControl.HitProbability(...) >= Settings.AgentMinHitProbability`,
|
||
replacing Cut 1's bare `InArc` (which the probability already subsumes, returning zero
|
||
out of arc). `AgentMinHitProbability` is added here, not in Cut 1.
|
||
- `TurretController.cs:81-83`: the same gate and threshold.
|
||
- `EntityInstance.cs:203-204,232-235`: the manager call takes the `ShotId` and looks the
|
||
outcome up, or subscribes to `ShotCommitted`. Effects fly to the rolled cell's world
|
||
point or to a miss offset and play the impact on arrival. **They apply nothing.**
|
||
- `Zone.cs:150`: `FireControl.Step(this, deltaTime)` after the entity loop.
|
||
|
||
**Authority map:**
|
||
|
||
- **Owner:** `FireControl` owns the engage gate, hit probability, the roll, the cell, the
|
||
shield branch, and when each of those happens. `Entity` owns the application of damage
|
||
to its own schematic. `Zone` owns the pending-shot collection.
|
||
- **Inputs:** weapon stats through `EquippedItem.Evaluate`; targeting-system stats;
|
||
`EntityInfoGathered`; `Target` and `TargetItem`; both entities' positions, velocities
|
||
and planar directions; mount direction and arc; `ItemManager.Random`.
|
||
- **Outputs:** `ShotOutcome` on two subjects; `Entity.DamageSchematic`; shield
|
||
`TakeHit`; `Entity.IncomingHit`.
|
||
- **Derived state:** `Behavior.Direction`, reveal, hit probability. Everything a
|
||
presentation shows about a shot is derived from the outcome.
|
||
- **Forbidden writers:** after this cut no `Physics.*` query in
|
||
`Assets/Scripts/Gameplay/Weapons` (except the unused four Cut 4 handles) may reach
|
||
durability; `HullCollider` may not raise a hit; `EntityInstance` may not damage a
|
||
schematic; no effect may call `TakeHit`; nothing outside `FireControl` may draw for a
|
||
hit; nothing outside presentation may read a commit to change state.
|
||
- **Shared paths:** player fire, AI fire, turret fire, instant, charged, guided and
|
||
locked weapons all reach damage through `FireControl.Fire` and `FireControl.Step`.
|
||
There is exactly one commit primitive.
|
||
- **Deletion line:** the six deletions above land before `FireControl.Fire` exists.
|
||
|
||
**Verification:**
|
||
|
||
- builds: `Aetheria.Shared`, `tests/Aetheria.Shared.Tests`, `tools/AetherDb`; and Unity
|
||
batchmode with the editor closed (`docs/item-provenance-cut.md` §6) reporting no
|
||
`error CS`.
|
||
- tests, each with the mutation that must kill it:
|
||
|
||
| Test | Pins | Mutation |
|
||
|---|---|---|
|
||
| `RollsAreSeeded` | *a fight is reproducible* — two zones, same seed, 50 shots, identical outcome sequences | `FireControl` draws from `new Random()` |
|
||
| `ProbabilityFollowsInputs` | *the roll reads what the ruling says it reads* — zero below detection and outside range; rising with info to `Resolution`; falling with `Spread`; capped at `UnaidedAccuracy` with no system | drop `pSensor`; drop the range gate; drop the cap |
|
||
| `OutOfArcConsumesNoDraw` | *the gate is a gate, not a penalty* — the RNG sequence is unchanged by an out-of-arc trigger | roll first and multiply by zero |
|
||
| `ShotResolvesOnArrival` | R3 — no durability change before `range / Velocity`; a target removed mid-flight takes none | apply at fire time |
|
||
| `OutcomeCommitsBeforeImpact` | R4 — `ShotCommitted` fires exactly `CommitHorizon` before `ShotResolved`, once per shot | publish at arrival; publish twice |
|
||
| `EvasionCountsUntilCommitAndNotAfter` | R4's second half — a target that jinks before the horizon changes the outcome distribution; the identical jink after the horizon does not | measure deviation at arrival; measure it at fire |
|
||
| `ShortFlightCommitsAtFire` | *the horizon degrades gracefully* — a velocity-0 weapon commits and resolves in one tick, still publishing both events in order | skip the commit for short flights |
|
||
| `OutcomeIsSnapshotNotReread` | R10, the seam the stats campaign named — a weapon whose `Damage` collapses (power cut, heat spike) between fire and arrival still lands the damage it rolled | re-evaluate `Damage` at arrival |
|
||
| `AimedHitLandsOnSelectedItem` | R5's payoff — `Precision` 1 and `p` 1: only the aimed item's durability falls | the roll ignores `Aimed` |
|
||
| `HardpointHitDamagesItemThenHull` | the damage rule survived the move verbatim — damage above armor plus item durability zeroes the item and the remainder hits the hull | armor not subtracted; hull skipped |
|
||
| `PenetrationMarchIsPlanar` | R7 — the march rotates by the target's planar direction and gives the same cells whatever the height difference | use a 3D rotation |
|
||
| `ShieldTakesHit` | *the absorb rule has one owner* — an active shield absorbs and the schematic is untouched | remove the shield branch |
|
||
| `DeadEntityStopsTakingShots` | *a shot cannot damage a corpse* — a target destroyed before arrival takes nothing and the shot is removed | resolve against a removed entity |
|
||
|
||
- negative greps, each checked against legitimate names before publishing:
|
||
- `rg -n "Physics\." Assets/Scripts/Gameplay/Weapons/Projectile.cs Assets/Scripts/Gameplay/Weapons/GuidedProjectile.cs Assets/Scripts/Gameplay/Weapons/Laser.cs Assets/Scripts/Gameplay/Weapons/Lightning.cs Assets/Scripts/Gameplay/HullCollider.cs`
|
||
— empty. (A repo-wide `Physics\.` grep is **not** valid until Cut 4, and never for
|
||
`TractorBeam.cs`, which stays.)
|
||
- `rg -n "SendHit|SendSplash|DamageSchematic" Assets/Scripts --glob '!**/ServerShared/**'`
|
||
— empty.
|
||
- `rg -n "TakeHit\(" Assets/Scripts --glob '!**/ServerShared/**'` — empty. Note
|
||
`CanTakeHit` shares the substring; the trailing `(` on `TakeHit` distinguishes them,
|
||
and the `Can` prefix must be checked by eye in the first run.
|
||
- operator play smoke: shots show rolled impacts and misses; a miss reads as a
|
||
deliberate near-miss rather than a bug; damage matches the HUD; aiming at a revealed
|
||
subsystem visibly concentrates damage there; kill time is in the same order as before.
|
||
|
||
**Risks.** `UnaidedAccuracy`, `SchematicCellSize`, `CommitHorizon` and the targeting stat
|
||
ranges are first guesses. The headless fixture is the tuning harness, and the operator
|
||
smoke is the arbiter. Second: `Combat.cs:84-98` aims `LookDirection` at a first-order
|
||
intercept, and R3's deviation is measured against `FireControl`'s own predicted
|
||
intercept; if the two predictions differ, an AI will systematically miss. They must be
|
||
one function. Hands makes `FireControl` own the prediction and `CombatState` call it.
|
||
|
||
---
|
||
|
||
## Cut 4. The unused kinds join the same path
|
||
|
||
R9 keeps every weapon type. This cut makes the ones no design currently ships obey the
|
||
same owner, so that authoring one later does not resurrect Unity hit detection.
|
||
|
||
- **Repo/branch:** continues Cut 3. Depends on Cut 3.
|
||
- **First:** **re-measure the claim this cut's safety rests on** — that no live design
|
||
uses `ConstantWeaponData`, `HitscanEffect`, `Mine`, airburst or MIRV (§0.4, carried
|
||
from a superseded map and not reproduced by this campaign's probe). A behaviour-class
|
||
census needs `AetherDb roles-migrate` as a dry run, which the permission classifier
|
||
blocked; the operator can unblock it or Hands can add a read-only `behaviors` command.
|
||
**If any of these is live, it moves into Cut 3 instead.**
|
||
|
||
**Deletes first:**
|
||
|
||
- `Assets/Scripts/Gameplay/Weapons/Projectile.cs:109-122` — airburst `OverlapSphere` and
|
||
`SendSplash` (14).
|
||
- `Assets/Scripts/Gameplay/Weapons/ConstantLaser.cs:74-110` (~37),
|
||
`ConstantLightning.cs:54-70` (~17), `HitscanEffect.cs:31-55` (~25),
|
||
`Mine.cs:68-75,92-117` (~34), `ConstantParticleWeapon.cs:72` and its `_hull` plumbing
|
||
(~8) — every remaining `Physics.*` hit query and `SendHit`/`SendSplash` call in
|
||
`Assets/Scripts/Gameplay/Weapons`.
|
||
|
||
**Adds:**
|
||
|
||
- `ConstantWeapon` rolls once per `GameplaySettings.BeamResolveInterval` for
|
||
`Damage * interval`, through the same `FireControl.Fire`/`Step` pair with a flight time
|
||
of zero, raising `OnBeamShot(int shotId)`. About ten lines; no design uses it yet, and
|
||
it authors no new stat.
|
||
- Splash — airburst and mine blast — as `FireControl.Splash`, applying the existing
|
||
`DamageSchematic` over the directional half-shape the deleted
|
||
`EntityInstance.cs:318-329` computed, against every entity in radius. This is the one
|
||
genuinely new rule in the cut, and it exists because R9 keeps the flak cannon and the
|
||
mine.
|
||
- Nothing else. `Mine`, `TractorBeam` and ship collision stay Unity-physics surfaces for
|
||
*placement and contact*, which is not hit detection; only their damage application
|
||
moves.
|
||
|
||
**Authority map:** unchanged from Cut 3 except that the forbidden-writer list now covers
|
||
every file in `Assets/Scripts/Gameplay/Weapons`, and `FireControl` gains splash.
|
||
|
||
**Verification:**
|
||
|
||
- builds: as Cut 3.
|
||
- tests:
|
||
- `BeamRollsPerInterval` pins *a beam is a sequence of rolls, not a continuous truth*:
|
||
over one second a beam produces `1 / BeamResolveInterval` outcomes totalling
|
||
`Damage`. Mutation: roll per tick; roll once.
|
||
- `SplashHitsEveryEntityInRadius` pins *splash is one rule, not per-effect*: two
|
||
targets in radius both take damage, one outside takes none. Mutation: damage only
|
||
the nearest.
|
||
- `SplashIsDirectional` pins that the moved half-shape rule survived: cells facing the
|
||
blast take it, cells facing away do not. Mutation: damage the whole shape.
|
||
- negative: `rg -n "Physics\." Assets/Scripts/Gameplay/Weapons` returns **only**
|
||
`Mine.cs`'s placement query, if Q7 keeps it — otherwise empty. `TractorBeam.cs:21` is
|
||
outside this directory and stays.
|
||
`rg -n "SendHit|SendSplash" Assets/Scripts` empty.
|
||
`rg -n "class HullHitEventArgs|class HullSplashEventArgs" Assets/Scripts` empty.
|
||
- operator: nothing to check in play, because nothing in the catalog spawns these. The
|
||
check is a Studio authoring pass: author one beam product and one flak product onto a
|
||
test hull and confirm they damage.
|
||
|
||
---
|
||
|
||
## 5. Subtraction ledger
|
||
|
||
Estimates, to be compared with reality in the postmortem.
|
||
|
||
| Cut | Removed | Added | Deps / targets / formats |
|
||
|---|---|---|---|
|
||
| 1 | 19 production (Entity 2, EntityInstance 4, Behaviors 6, Combat 3, TurretController 3, Settings 1) | ~60 production (`FireControl.cs` geometry ~45, `HardpointData.FiringArc` 1, `GameplaySettings.FiringArc` 1, call sites ~13), ~90 test | +1 catalog field, authored on one hull. No new project, package or dependency. |
|
||
| 2 | 0 | ~140 production (`TargetingSystem.cs` ~55, reveal ~25, `Entity.TargetItem` ~25, AI selection ~10, player input ~15, `LoadoutGenerator` ~12), ~160 test | +1 behaviour union index, +2 designs and their products, +1 setting. |
|
||
| 3 | ~285 production (EntityInstance 120, HullCollider 45, Projectile 47, GuidedProjectile 38, Laser 36, Lightning 39) | ~330 production (`Entity.DamageSchematic`/`ApplyHit` ~130 moved, `FireControl` roll and lifecycle ~140, `Zone` pending shots ~35, call sites ~25), ~280 test | +2 settings. **Seven duplicated copies of the shield-absorb rule collapse to one.** |
|
||
| 4 | ~135 production | ~70 production, ~70 test | +1 setting. |
|
||
| **Total** | **~439** | **~600 production, ~600 test** | |
|
||
|
||
Net production about +160. The account: it buys the invariant that the simulation
|
||
resolves combat without Unity — `docs/three-gates-scope.md:101-110` calls today's state
|
||
a velocity compromise — and it collapses seven copies of the absorb rule and one copy of
|
||
the damage rule into single owners. It does **not** buy that with a registry, an adapter,
|
||
a mode flag or a compatibility layer: the old path is deleted in the cut that replaces
|
||
it, and no cut leaves two writers of damage.
|
||
|
||
The line count is the wrong thing to stare at here. The structural delta is: one new
|
||
owner (`FireControl`), one new subsystem (`TargetingSystem`), one field on catalog data,
|
||
four settings, and **minus one whole category of authority** — Unity physics deciding
|
||
gameplay.
|
||
|
||
---
|
||
|
||
## 6. Build budget
|
||
|
||
- **Packages and targets.** `Aetheria.Shared/Aetheria.Shared.csproj` (netstandard2.1),
|
||
`tests/Aetheria.Shared.Tests/Aetheria.Shared.Tests.csproj` (net10.0, xunit),
|
||
`tools/AetherDb/AetherDb.csproj`. Three projects, no new ones. Debug only; no release
|
||
profile, no feature flags, no code generation, no new target platform.
|
||
- **Build host and target are the same machine**: a Windows workstation producing
|
||
managed assemblies for the same runtime. Nothing here is cross-platform, so no claim
|
||
about a Linux artifact is made or needed.
|
||
- **Where.** `F:\Projects\Aetheria-stats` (the free worktree; it must first be advanced
|
||
from `c4f6029e` to HEAD). Never the main tree — the operator plays there. Never
|
||
`F:\Projects\Aetheria-tiers`. CultLib via
|
||
`-p:CultLibRoot=<scratchpad>\cultlib-45c2f40`, the shared pinned worktree, which is
|
||
never removed. `--artifacts-path` into scratch so no `bin`/`obj` lands in a working
|
||
tree.
|
||
- **Footprint.** Three managed projects plus their test host; on the order of a few
|
||
hundred MB of artifacts, comparable to the stats campaign. No expected growth: this
|
||
campaign adds no project, package reference or native component.
|
||
- **Unity.** Batchmode compile check with the editor closed, per
|
||
`docs/item-provenance-cut.md` §6, once per cut that touches `Assets/Scripts/Gameplay`
|
||
(Cuts 1, 3, 4). The operator's play smoke is the only Unity *run*. Imagination did not
|
||
run Unity and did not build.
|
||
- **Mutation harnesses.** One per cut, in the established shape
|
||
(`tests/mutation_tests_fire_control_cut1..4.py`): patch a named anchor, run
|
||
`dotnet test tests/Aetheria.Shared.Tests --cultlib-root <pin>`, assert the named test
|
||
goes red, with a no-op control. Note the scar from `8dddda4d`: later cuts move earlier
|
||
anchors, so Cut 4 re-anchors Cuts 1-3's harnesses before it lands.
|
||
|
||
---
|
||
|
||
## 7. Seams this campaign names and stops at
|
||
|
||
- **Presentation of shots.** `Zone.ShotCommitted` and `Zone.ShotResolved` are the whole
|
||
contract. What a projectile, panel, whip or near-miss *does* with a commit belongs to
|
||
`docs/shield-presentation-contract.md` and the panel and Lariat maps. This campaign
|
||
guarantees only that the commit exists, is authoritative, arrives `CommitHorizon`
|
||
before impact, and carries what the contract says it carries: what happens, where,
|
||
when it arrives, and which capability answers it — not who fired, not damage numbers.
|
||
The contract's open question "where the commit window lives" is answered here:
|
||
`FireControl`, with the horizon in `GameplaySettings`.
|
||
- **Death, removal and loot.** `Entity.Death` is already composed in `ServerShared`
|
||
(`Entity.cs:363-366`), but `Zone.Entities.Remove` and the loot roll live in Unity
|
||
(`EntityInstance.cs:411-437`) and use `UnityEngine.Random`. Fire control makes a
|
||
headless kill *happen*; it does not make the corpse go away. Q3.
|
||
- **Pickup.** `ShieldManager.cs:40-51` stores loot on trigger contact. Untouched.
|
||
- **Ship collision, `TractorBeam`, mine placement.** Deferred Unity-physics surfaces
|
||
(`docs/three-gates-scope.md:98-99`). Untouched.
|
||
- **The navigation planner.** R3 makes manoeuvring evasion, which changes what a good
|
||
flight path is. The planner is a separate item in the shipping scope and this map does
|
||
not anticipate it.
|
||
- **Stance and IFF.** Ruled and partly built (`Weapon.StanceAllowsFire`,
|
||
`Weapon.cs:101`). Fire control reads the gate and does not redesign it.
|
||
- **Faction territory.** The campaign after this.
|
||
|
||
---
|
||
|
||
## 8. Operator questions
|
||
|
||
Most blocking first.
|
||
|
||
**Q1. What makes a turret 360?** R6 says turrets are 360 degrees, but a turret hull's
|
||
`Direction` never rotates — `TurretController` writes only `LookDirection` — so under a
|
||
120-degree default a turret would only engage within 60 degrees of its spawn facing.
|
||
**A:** author `FiringArc: 360` on the `Turret` hull's hardpoints; the override R6 already
|
||
grants does the whole job and no code knows what a turret is. **B:** add
|
||
`HardpointType.Turret` and give it an implicit 360. **C:** give the arc to the hull
|
||
rather than the hardpoint. **Recommended: A**, because it buys the invariant with
|
||
authored data instead of a new type, and because a future partially-traversing turret
|
||
(a 270-degree sponson) is then already expressible. B makes the arc a property of a
|
||
category and loses that.
|
||
|
||
**Q2. Is player fire arc-gated?** Today it is not gated at all; the player fires whatever
|
||
is equipped, wherever they point. Cut 1 makes the arc a shared rule. **A:** yes, gate the
|
||
player exactly as the AI — a side-mounted gun simply does not fire forward, and the HUD
|
||
shows which groups bear. **B:** gate the AI only, leaving the player's rear gun able to
|
||
shoot forward. **Recommended: A.** Under R1 aiming is gone, so what the player does with
|
||
a weapon group *is* the decision, and arcs are what make that decision interesting. B
|
||
also splits the shared path the doctrine cares about: manual and programmatic fire would
|
||
be two truths. It is a real difficulty change and should be felt in the smoke before the
|
||
campaign closes.
|
||
|
||
**Q3. Does this campaign move death removal and loot, or leave them in Unity?** A
|
||
headless kill currently leaves the corpse in `Zone.Entities`, because `Entities.Remove`
|
||
is at `EntityInstance.cs:437`. **A:** move removal and the loot roll into `Zone` in
|
||
Cut 3, using `ItemManager.Random` — about 50 lines, and it makes a headless kill a
|
||
complete event. **B:** move only the removal; leave loot to the run-structure work.
|
||
**C:** leave both; fire control's tests assert on durability, not on removal.
|
||
**Recommended: B.** Removal is the half without which the simulation is lying about its
|
||
own contents, and a seeded loot roll pulls in drop velocity, bay capacity and pickup —
|
||
the run-structure campaign's material. A is more complete but widens this campaign; C
|
||
leaves `ServerShared` unable to say who is alive.
|
||
|
||
**Q4. Three balance numbers need first values:** `CommitHorizon` (R4's "short fixed
|
||
time"), `SchematicCellSize` (metres per schematic cell, which sets how much target size
|
||
matters), and `UnaidedAccuracy`. **A:** Hands picks defaults (0.3s, measured off the
|
||
LonginusX prefab, 0.35) and the operator tunes them in the smoke. **B:** the operator
|
||
states them now. **Recommended: A** for the first two and **B for `UnaidedAccuracy`**,
|
||
because it is not a tuning number — it decides whether a targeting system is a
|
||
requirement or an upgrade, which is a design ruling. If it is low, Cut 2's required-item
|
||
rule is load-bearing; if it is high, the subsystem is a nice-to-have and Cut 2 shrinks.
|
||
|
||
**Q5. What seeds the rolls?** `ItemManager.Random` is clock-seeded
|
||
(`ItemManager.cs:19`), so `RollsAreSeeded` cannot pass without a seed argument.
|
||
**A:** add the seed parameter here, as a two-line change. **B:** wait for the `Run`
|
||
lifecycle work that owns it (`docs/headless-playground-cut.md` Cut 1, unlanded).
|
||
**Recommended: A.** A reproducible fight is how this campaign is tuned and how a
|
||
regression is ever found again; waiting on an unstarted cut for two lines is the
|
||
compensator pattern in miniature.
|
||
|
||
**Q6. When is the snapshot taken — fire, or commit?** `docs/stats-and-power-cut.md:1020`
|
||
says "at commit time". **A:** one snapshot, at fire: the shot carries what left the
|
||
barrel, and only deviation is measured up to the commit. **B:** two snapshots — payload
|
||
at fire, targeting stats at commit — so a targeting system knocked out mid-flight
|
||
degrades the shot already in the air. **Recommended: A.** One rule, one freeze point, and
|
||
over a horizon measured in tenths of a second B's extra fidelity is invisible while its
|
||
extra state is permanent. B is the more simulationist answer and is worth having if the
|
||
operator wants killing the gunnery computer to spoil shots already fired.
|
||
|
||
**Q7. Mines.** Cut 4 moves mine *damage* to `FireControl.Splash` but leaves
|
||
`Mine.cs:68`'s `OverlapSphere` for proximity triggering, which is contact detection
|
||
rather than hit detection. **A:** leave it, alongside `TractorBeam` and ship collision,
|
||
as a named deferred physics surface. **B:** move proximity into `Zone` too, so the last
|
||
`Physics.*` in the weapons directory goes. **Recommended: A.** No design ships a mine,
|
||
and B buys a clean grep rather than an invariant. Revisit when a mine is authored.
|
||
|
||
---
|
||
|
||
## Cut 5: the rules that landed in one path and not its twin
|
||
|
||
Date: 2026-09-19. Branch `codex/fire-control-5` off `ca57819d`.
|
||
|
||
Soul's pass over the whole campaign (2026-09-19) confirmed eleven findings. Most
|
||
of them are the same shape: a rule this map declared, implemented on one path,
|
||
and left off the path beside it. Splash breaks a shield the discrete roll does
|
||
not. `InstantWeapon` gates on arc and `ConstantWeapon` does not. Unity removes a
|
||
corpse from the zone and the simulation does not. This cut closes the twins.
|
||
Catalog authoring (Soul's findings 1 and 2) is Cut 6; the shared-RNG stream
|
||
(finding 6) and airburst (finding 5) are Cut 6 as well; the missing mutation
|
||
harnesses (finding 8) are Cut 7.
|
||
|
||
### 5.1 Tracking is a floor, not a cliff (Soul finding 3)
|
||
|
||
`FireControl.cs:102-106` falls back to `Tracking = 0` with no targeting system,
|
||
and `:273-275` turns that zero into `deviation < .01f ? 1f : 0f`. A probe fired
|
||
334 shots at a target drifting at 5 u/s with `Accuracy` forced to 1 and landed
|
||
none. Q4 asked for unaided fire to be really, really bad; this is a wall.
|
||
|
||
- `Settings.cs`, beside `UnaidedAccuracy`: add `public float UnaidedTracking = 10f;`
|
||
with the same authoring note. Ten world units of deviation forgiveness is the
|
||
first guess and the operator's knob: a target that has strayed five units from
|
||
its fire-time projection halves an already-5% chance.
|
||
- `FireControl.Tracking` falls back to `settings.UnaidedTracking`, exactly as
|
||
`Accuracy` falls back to `UnaidedAccuracy`. One shape for both.
|
||
- `Commit` then deletes the zero branch outright: `p *= saturate(1f - deviation / shot.Tracking)`.
|
||
Tracking is now always positive, so the branch protects nothing.
|
||
|
||
**Authority:** `Settings` owns the unaided figures; `FireControl.Tracking` is the
|
||
only reader; `Commit` no longer carries a special case for a value that cannot occur.
|
||
|
||
### 5.2 A shot the shield cannot absorb breaks it (Soul finding 4)
|
||
|
||
`Commit:299-302` sets `Shielded` only when `CanTakeHit` passes, and `Apply:311-326`
|
||
routes the rest to the hull -- neither calls `Break()`. `Splash:365` does. The
|
||
deleted Unity path did (`git show b7743789^:Assets/Scripts/Gameplay/Weapons/Projectile.cs`).
|
||
So the F4/F5 shield-break mechanic is unreachable for every weapon in the catalog:
|
||
an unabsorbable 5000-damage hit leaves `Broken == false` and the shield absorbs the
|
||
very next shot, refilling at `RefillDuration` instead of the punitive `RestoreDuration`.
|
||
|
||
- `ShotOutcome` gains `public bool ShieldBroken;`.
|
||
- `Commit` decides it where it already asks `CanTakeHit`: shield present, active,
|
||
and `CanTakeHit` false means `ShieldBroken = true`. The decision is frozen with
|
||
the rest of the outcome (R4) -- a shield that recharges during the flight does
|
||
not retroactively survive.
|
||
- `Apply` performs it: `if (shot.Outcome.ShieldBroken) shot.Target.Shield.Break();`
|
||
before the hull damage. `CanTakeHit` is still called exactly once per shot.
|
||
- Presentation gains the signal for free; nothing is required to read it yet.
|
||
|
||
### 5.3 Continuous weapons obey their arc (Soul finding 9)
|
||
|
||
`ConstantWeapon.cs:91,99` gate on `StanceAllowsFire` alone. A side-mounted beam
|
||
fires forward. Q2 says manual and programmatic must not be two truths; instant and
|
||
continuous currently are.
|
||
|
||
- Both gates become `StanceAllowsFire && ArcAllowsFire`, matching `InstantWeapon.cs:105`.
|
||
|
||
### 5.4 One bearing test, including at zero range (Soul finding 11)
|
||
|
||
`Weapon.cs:117` allows a zero-length bearing; `FireControl.InArc:47` normalizes the
|
||
zero vector into NaN and `HitProbability` returns 0. The player's trigger says fire
|
||
and the probability says impossible, so AI and turrets refuse to fire at a
|
||
co-located target.
|
||
|
||
- `InArc` takes the rule: a planar bearing shorter than `1e-6` returns true --
|
||
point-blank cannot fail a bearing test.
|
||
- `Weapon.ArcAllowsFire` drops its own `lengthsq` special case and simply calls
|
||
`InArc`. One owner, one answer.
|
||
|
||
### 5.5 A destroyed subsystem stops being the aim point (Soul finding 10)
|
||
|
||
A destroyed item is never removed from `Equipment` (`Entity.cs:475` only observes
|
||
it), so `IsRevealed` still ranks it and `ResolvedTargetItem` still returns it --
|
||
`Precision` keeps concentrating hits onto a dead item's footprint.
|
||
|
||
- `FireControl.IsRevealed` returns false for an item whose
|
||
`EquippableItem.Durability < .01f`, and the ranking excludes destroyed items so
|
||
the reveal tiers of the survivors close up rather than leaving a gap.
|
||
- `Entity.ResolvedTargetItem` therefore drops a destroyed aim point on its next
|
||
read. Nothing caches it, so no clearing pass is needed.
|
||
|
||
### 5.6 Death removes the ship, in the simulation (Soul finding 7)
|
||
|
||
Operator ruling Q3 says death removes the ship. `Zone.Entities.Remove` for death
|
||
exists only at `EntityInstance.cs:324`, inside Unity's loot-drop subscription. So
|
||
headless, `Step`'s `targetGone` guard never fires, shots land on corpses, `Splash`
|
||
keeps hitting them, the AI keeps engaging them, and the tuning harness this map
|
||
calls its arbiter cannot end a fight. `DeadEntityStopsTakingShots` passes only
|
||
because it calls `Entities.Remove` by hand.
|
||
|
||
- `Zone` subscribes each entity's `Death` observable when the entity joins, and
|
||
removes it from `Entities` on death. The simulation owns the corpse.
|
||
- `EntityInstance.cs:324` deletes its `Entities.Remove` call and keeps the loot
|
||
drop and destroy effect, which are its own business. Forbidden writer: no
|
||
presentation removes an entity from the zone.
|
||
|
||
### 5.7 Delete the two carried-and-unread fields (Soul finding 12)
|
||
|
||
`PendingShot.PredictedIntercept` and `PendingShot.FlightTime` are written at
|
||
`FireControl.cs:204-205` and read nowhere. The intercept in particular is
|
||
carry-weight from Cut 3's named risk: `Combat.cs:109` and `Fire` do share
|
||
`FireControl.PredictedIntercept`, but the roll judges deviation against a
|
||
straight-line projection from fire position, so the stored copy decides nothing.
|
||
|
||
- Both fields are deleted from the struct. The local `flightTime` stays; it still
|
||
computes `ArrivalTime` and `CommitTime`.
|
||
|
||
### Verification
|
||
|
||
Each of 5.1-5.7 gets a test that fails under its own mutation, and the mutation
|
||
harness is committed as `tests/mutation_tests_fire_control_cut5.sh` with a no-op
|
||
control. In addition this cut pins four of the five mutations that survived 197
|
||
green tests in Soul's pass, because they are rules 5.1-5.6 touch:
|
||
|
||
- `FireControl.cs:365` -- delete `shield.Break()` from `Splash`. Must die.
|
||
- `Weapon.cs:117` -- `ArcAllowsFire => true`. Must die.
|
||
- `InstantWeapon.cs:105` -- delete the `if (!ArcAllowsFire) return;` player arc gate
|
||
(Q2, the ruling Cut 1 deferred to Cut 3 and nothing verifies). Must die.
|
||
- `FireControl.cs:270` -- `elapsed = shot.FlightTime` in place of `now - shot.FireTime`.
|
||
This is the map's own declared mutation for `EvasionCountsUntilCommitAndNotAfter`,
|
||
and it survives because that test's fixture gives the target zero velocity, so
|
||
`FireTargetVelocity * elapsed` is identically zero and the mutation is a no-op
|
||
inside it. Give the fixture a moving target. (After 5.7 deletes `FlightTime` the
|
||
mutation's spelling becomes `elapsed = shot.ArrivalTime - shot.FireTime`; the rule
|
||
it attacks is the same.)
|
||
|
||
The fifth survivor -- freezing `source.TargetItem.Value` instead of
|
||
`ResolvedTargetItem` at `FireControl.cs:193`, bypassing the reveal re-check -- is
|
||
Cut 7's, alongside the missing Cut 1-3 harnesses.
|
||
|
||
Negative checks: no `Entities.Remove` for death outside `Zone`; no `Tracking` read
|
||
that can return zero; no `PredictedIntercept` or `FlightTime` field on `PendingShot`.
|
||
|
||
---
|
||
|
||
## Cut 6: the dice stop being shared, and the flak cannon works again
|
||
|
||
Date: 2026-09-19. Branch `codex/fire-control-6` off Cut 5.
|
||
|
||
Cut 6a (catalog authoring, worktree `Aetheria-catalog`, branch
|
||
`codex/fire-control-6-catalog`) runs in parallel and is specified by Soul's
|
||
findings 1 and 2 plus the Cut 2 authoring spec above. This section is Cut 6b, the
|
||
two remaining code findings.
|
||
|
||
### 6.1 A shot's dice belong to the shot (Soul finding 6)
|
||
|
||
`Commit:266,304` draws from `ItemManager.Random`, a single stream shared with
|
||
`ActionGameManager.cs:552,558,567,686,787` (wormhole exit velocity, loadout
|
||
generation, travel) and `Combat.cs:144` (`SampleDps`, drawn per AI per
|
||
evaluation). Q5's reproducible fight therefore holds only inside a hermetic
|
||
fixture: in play, adding an NPC or opening a shop changes every subsequent die.
|
||
Soul's determinism probes passed for exactly that reason, which makes them a
|
||
demonstration of the fixture rather than of the rule.
|
||
|
||
The fix is a deletion, not a second stream. A stream makes the roll depend on
|
||
*how many* draws came before it; what this campaign actually promised is that a
|
||
shot's outcome is a function of the shot. So make it one.
|
||
|
||
- `Zone` exposes `public uint CombatSeed { get; }`, set in the constructor from
|
||
the same expression that already seeds `_random`:
|
||
`galaxyZone?.Name.StableHash() ?? 1337u`. One zone, one stable identity — a
|
||
galaxy seed reproduces it, and nothing else in the zone can perturb it.
|
||
- `Commit` stops touching `ItemManager.Random` entirely. It builds its own local
|
||
`CultMath.Random` from the zone seed and the shot's own id:
|
||
`var random = new Random((zone.CombatSeed * 2654435761u) ^ (uint) shot.ShotId | 1u);`
|
||
The `| 1u` guards the degenerate zero seed. Every draw the commit makes — the
|
||
hit roll, the precision roll, the cell pick — comes from that one local
|
||
generator, and it dies with the call.
|
||
- The write-back at `:304` (`shot.Source.ItemManager.Random = random;`) is
|
||
deleted with it.
|
||
|
||
**Authority:** the roll's randomness is a pure function of `(zone identity, shot
|
||
id)`. **Forbidden writers:** nothing outside `Commit` may seed or advance a
|
||
combat draw, and `Commit` may not read a shared stream. **Consequence worth
|
||
stating:** two shots that happen to share a `ShotId` across two zones roll
|
||
differently, because the zone seed differs; two runs of the same fight from the
|
||
same galaxy seed roll identically no matter what the UI did in between, which is
|
||
the promise Q5 actually made.
|
||
|
||
### 6.2 Airburst resolves in the simulation (Soul finding 5)
|
||
|
||
Cut 4 deleted `Projectile.cs`'s `OverlapSphere`/`SendSplash` and added
|
||
`FireControl.Splash` "because R9 keeps the flak cannon and the mine". Only the
|
||
mine was wired (`Mine.cs:99`). `Projectile.cs:16-17` still carries
|
||
`AirburstDistance`/`AirburstRange`, still written by `ProjectileManager.cs:32`,
|
||
read by nothing — so a flak round flies and does nothing. The stale comment at
|
||
`Projectile.cs:7-8` calls airburst "Cut 4's — untouched here".
|
||
|
||
The wiring does **not** go back into `Projectile`. R8 says presentation does not
|
||
decide damage, and a projectile MonoBehaviour calling `Splash` would be exactly
|
||
the authority Cut 3 spent itself deleting. Airburst is a property of the weapon,
|
||
so the simulation resolves it:
|
||
|
||
- `PendingShot` regains a burst point — `public float3 BurstPosition;` — frozen
|
||
at fire like every other payload field, from `PredictedIntercept` (which Cut 5
|
||
deletes as unread; this is its live consumer, and it comes back named for what
|
||
it is rather than for what computed it).
|
||
- `PendingShot` gains `public float BurstRadius;`, frozen from the weapon's
|
||
authored airburst range, and zero for a weapon without the `Airburst` flag
|
||
(`Enums.cs:101`).
|
||
- `FireControl.Fire` sets both when the weapon's `WeaponData` carries `Airburst`.
|
||
- `FireControl.Step`, at arrival, resolves an airburst shot by calling
|
||
`Splash(zone, shot.BurstPosition, shot.BurstRadius, shot.Damage, shot.DamageType)`
|
||
**instead of** `Apply`. An airburst round does not also roll a discrete hit:
|
||
it is an area effect, which is the whole point of the flag, and applying both
|
||
would be the double-application Soul was told to hunt for.
|
||
- `Projectile.cs` deletes `AirburstDistance` and `AirburstRange` and the stale
|
||
comment; `ProjectileManager.cs:32` deletes the line that wrote one. The
|
||
projectile's remaining job is to fly and disappear, which is all presentation
|
||
should have been doing.
|
||
|
||
**Open tuning question, recorded not asked:** the authored airburst radius has no
|
||
sibling in the catalog to argue from, because no product has ever used it. Cut 6a
|
||
or a later authoring pass sets it; until then the flag is wired and unexercised,
|
||
and the report must say so rather than implying a working flak cannon.
|
||
|
||
### Verification
|
||
|
||
Committed harness `tests/mutation_tests_fire_control_cut6.sh`, no-op control
|
||
required, same byte-exact I/O rules as Cut 5.
|
||
|
||
- `SameFightRollsSameThroughUnrelatedDraws` pins 6.1: run a fixed fight twice from
|
||
one zone seed, drawing an arbitrary number of values from `ItemManager.Random`
|
||
between the two runs, and the outcomes must be identical. Mutation: restore the
|
||
`ItemManager.Random` draw. Must die — and note that this is the test Soul's own
|
||
probe could not be, because a hermetic fixture cannot see the defect.
|
||
- `ShotIdDecidesTheDie` pins that the roll is a function of the shot: two shots
|
||
with the same frozen payload and different ids may differ; the same id in the
|
||
same zone always gives the same result. Mutation: drop `ShotId` from the seed.
|
||
- `AirburstSplashesAndDoesNotAlsoRoll` pins 6.2: an airburst shot damages
|
||
everything in its radius, and its target takes area damage rather than a
|
||
discrete cell hit. Mutation: call `Apply` as well as `Splash`. Must die.
|
||
- `NonAirburstNeverSplashes` pins the other side. Mutation: splash unconditionally.
|
||
- negative: `ItemManager.Random` appears nowhere in `FireControl.cs`;
|
||
`Airburst` appears nowhere in `Assets/Scripts/Gameplay`.
|
||
|
||
---
|
||
|
||
## Cut 6c: the formula gets fixed, and the single point of failure
|
||
|
||
Date: 2026-09-19. Branch off Cut 6a (`e5f1318a`) once Cut 5 has merged — this
|
||
touches `FireControl.cs`, which Cut 5 owns until then.
|
||
|
||
Cut 6a landed the catalog data that Cut 2 never shipped: 53 designs, 43 products,
|
||
`loadout` green on ten seeds, turret arcs migrated. Reviewing it turned up two
|
||
defects it introduced and one it inherited.
|
||
|
||
### 6c.1 Resolution becomes reciprocal, so the name stops lying
|
||
|
||
`HitProbability` computes `pSensor = saturate(unlerp(TargetDetectionInfoThreshold, Resolution(source), info))`.
|
||
With the live `TargetDetectionInfoThreshold` of `.1`, a **higher** Resolution
|
||
means **more** gathered info is needed before sensor state stops limiting hits.
|
||
Resolution is a cost. The field's own comment says so accurately -- "the info
|
||
level at which sensor state stops limiting hits" -- and the word "Resolution"
|
||
says the opposite, because in every other context a higher resolution is a
|
||
better instrument.
|
||
|
||
Cut 6a authored the 1-cell `Targeting Computer` at `.3` and the 2-cell
|
||
`Fire Control Array` at `.75`. Against a target scanned to info `.3`, that gives
|
||
the cheap design `pSensor = 1.0` and the 150000c design `pSensor = 0.31`. The
|
||
premium item is strictly worse until the player has scanned hard, which is not a
|
||
tradeoff anyone authored -- it is the formula misleading an author who had read
|
||
it and said so in the same paragraph.
|
||
|
||
Operator ruling, 2026-09-19: "When the formula is unintuitive, we don't make the
|
||
handle more opaque for correctness, we fix the formula. Plenty of core gameplay
|
||
formulas are reciprocal just for this reason." An earlier draft of this cut
|
||
proposed renaming the field to `SensorDemand`; that is superseded. The name
|
||
stays `Resolution`, higher stays better, and the formula takes the reciprocal:
|
||
|
||
- `HitProbability` derives the ceiling rather than reading it directly:
|
||
`var demandCeiling = detection + (1f - detection) / max(Resolution(source), 1e-3f);`
|
||
then `pSensor = saturate(unlerp(detection, demandCeiling, info))`.
|
||
- Resolution `1` therefore means "needs complete information before sensor data
|
||
stops penalising this system" -- which is precisely what the unaided fallback
|
||
of `1f` should mean. That fallback stays as it is and is now the floor by
|
||
construction instead of by coincidence. The guard against a zero or negative
|
||
authored Resolution is the `max`, and no authored value should ever reach it.
|
||
- Re-author both designs on the new scale: 1-cell `2` (sensor-limited until info
|
||
reaches `.55`), 2-cell `4` (until `.325`). Both comfortably better than unaided,
|
||
the premium one better than the cheap one, and the numbers now read the way a
|
||
designer reaching for the knob expects.
|
||
- `TargetingSystemData.Resolution`'s comment gains one sentence naming the
|
||
direction and the reciprocal, so the next author does not have to derive it.
|
||
|
||
### 6c.2 One seller is a die roll against ship generation
|
||
|
||
Cut 6a sold both targeting designs from NiteLife Energy alone, satisfying the
|
||
Cut 2 spec's letter ("at least the manufacturers that sell the capacitor") while
|
||
missing what that clause was for. `Galaxy.cs:109` selects
|
||
`factions.OrderBy(random).Take(settings.MegaCount)` — a **random subset** of the
|
||
twelve factions — plus the quest faction and the authored neutrals. A galaxy that
|
||
does not draw NiteLife has no targeting product, `IsAvailable`
|
||
(`LoadoutGenerator.cs:154`) filters every candidate out, and
|
||
`FillInterior`'s `required: true` throws `InvalidLoadoutException` again. Cut 6a's
|
||
ten green seeds do not disprove this; they sample the draw.
|
||
|
||
Two changes, because content coverage and structural fragility are different
|
||
problems and only one of them is fixed by authoring:
|
||
|
||
- **The generator degrades instead of throwing.** A `required: true` interior
|
||
item with no available product leaves the entity without one rather than
|
||
failing generation outright. This is only safe *because* Cut 5.1 gave unaided
|
||
fire a floor: such a ship now fires badly instead of not at all. The generator
|
||
logs the gap so it surfaces as content debt rather than silence, and `AetherDb
|
||
loadout` reports it per seed.
|
||
- **Thematic sellers, which also buy coverage.** NiteLife Energy is the roster's
|
||
power-generation faction; targeting belongs to Finch Cybernetics (passive
|
||
sensors) and Lucent Media (active sensors), both of which the roster names for
|
||
exactly this. Each design gains products from both. That is three sellers per
|
||
design, and it puts the item in the hands of the factions whose brand explains
|
||
why they make it.
|
||
|
||
### 6c.3 Recorded, not fixed
|
||
|
||
`Large Drive`'s new product declares no roles because the design declares none.
|
||
That is honest, and it is also a design with no role axes in a game where roles
|
||
are how products differentiate. It belongs to the content pass, not here.
|
||
|
||
### Verification
|
||
|
||
- A test pins 6c.1's direction: two entities, identical but for `Resolution`, at
|
||
the same info level -- the **higher** Resolution has the higher
|
||
`HitProbability`, at every info level between detection and full. Mutation:
|
||
drop the reciprocal and read Resolution as the ceiling directly (the shipped
|
||
behaviour). Must die. This is the assertion whose absence let a premium item
|
||
ship strictly worse than a cheap one.
|
||
- A test pins 6c.2: a galaxy containing no seller of any targeting design still
|
||
generates every hull, and those entities resolve `UnaidedAccuracy`. Mutation:
|
||
restore the throw. Must die.
|
||
- `AetherDb loadout` across at least 50 seeds, plus a run with the faction set
|
||
forced to exclude all three sellers. Report the gap count, not just "no throw".
|
||
- Census unchanged at 53 designs; products rise by 4 (two more sellers per design).
|
||
|
||
---
|
||
|
||
## Cut 7: the harnesses that were prose
|
||
|
||
Date: 2026-09-19. Branch `codex/fire-control-7` off Cut 6.
|
||
|
||
`git log --all -- tests/mutation_tests_fire_control*` returns exactly one commit,
|
||
`034e8d3e`, adding `cut4` alone. §6 of this map declares `cut1..4`. So the Cut 3
|
||
mutation table above (seeding, the commit horizon, the frozen snapshot, the
|
||
shield branch, the penetration march, the roll itself) is prose: it names the
|
||
mutations that should die and nothing has ever run them.
|
||
|
||
That is not a paperwork gap. Soul wrote five mutations against rules this map
|
||
claims to pin and **all five survived 197 green tests**, including
|
||
`InstantWeapon.cs:105`, which is operator ruling Q2 — the one thing Cut 1
|
||
explicitly deferred to Cut 3. Cut 5 pins four of them because it touches their
|
||
rules. This cut pins the fifth and builds the harnesses that should have existed
|
||
since Cut 1, so that every verdict this campaign has recorded becomes reproducible
|
||
rather than remembered.
|
||
|
||
### 7.1 The fifth survivor: the reveal re-check
|
||
|
||
`FireControl.cs:193` freezes `Aimed = source.ResolvedTargetItem`. Mutating it to
|
||
`source.TargetItem.Value` bypasses the reveal re-check — you could aim at a
|
||
subsystem you have lost resolution on — and 197 tests stayed green.
|
||
|
||
- A test pins that an aim point whose reveal has decayed below its tier is **not**
|
||
frozen into the shot: the shot still fires, at a random cell, with no `Aimed`.
|
||
Mutation: read the raw `TargetItem.Value`. Must die.
|
||
- Note the interaction with Cut 5.5: a *destroyed* aimed item must fall out by the
|
||
same path, through `IsRevealed`, not through a second check. If pinning this
|
||
needs two tests, the rule has two owners and that is itself the finding.
|
||
|
||
### 7.2 Harnesses for Cuts 1, 2 and 3
|
||
|
||
Three committed scripts — `tests/mutation_tests_fire_control_cut1.sh`, `cut2.sh`,
|
||
`cut3.sh` — each with a no-op control, byte-exact I/O, anchors matching exactly
|
||
once, and a reverse-write restore. Each runs the mutation table its cut already
|
||
declares in this document; do not invent a new table, and do not quietly
|
||
substitute an easier mutation for one that will not die.
|
||
|
||
**Every survivor is a finding to report, never a test to weaken.** The expected
|
||
cause, seen twice in this campaign already, is a fixture too weak to distinguish
|
||
the mutant: `EvasionCountsUntilCommitAndNotAfter` survived its own declared
|
||
mutation because its target had zero velocity, so the sabotage was arithmetically
|
||
a no-op. When a mutation survives, first ask whether the fixture differs from the
|
||
rule's actual precondition, fix the fixture, and say so in the report.
|
||
|
||
Where a declared mutation genuinely cannot be reached by the current suite, write
|
||
it up as **not yet reached**, never as unreachable, and name what reaching it
|
||
would take. Both times that call was made elsewhere in this project it was wrong,
|
||
and a fifteen-line probe killed the mutant.
|
||
|
||
### 7.3 What the harnesses are allowed to assume
|
||
|
||
Cut 6a authors the first targeting-system designs into the catalog. A harness that
|
||
depends on catalog content is a harness that breaks when content changes, so
|
||
these suites build their own fixtures through the same synthetic-upsert path
|
||
`TargetingSystemTests.cs:368-403` already uses. That file is also a cautionary
|
||
example: it passes by upserting a synthetic design into a throwaway cache, which
|
||
pins the generator and says nothing about whether the shipped catalog can
|
||
generate a ship. Both are worth having; only one of them was, and the map claimed
|
||
the other.
|
||
|
||
So: **one test in this cut asserts against the live catalog** — that
|
||
`AetheriaStores.Open` over `GameData/Aetheria.cc` yields a targeting system the
|
||
player's own hull can be generated with. That is the assertion whose absence let
|
||
Cut 2 ship code without data.
|
||
|
||
### Verification
|
||
|
||
- All four harnesses (cut1-3 plus the existing cut4) run clean: every declared
|
||
mutation dies, every no-op control leaves the suite green.
|
||
- The report carries the full table — cut, mutation, died/survived, and for any
|
||
survivor the fixture defect behind it.
|
||
- `dotnet test` green, with the count.
|
||
|
||
---
|
||
|
||
## Cut 6d: where a hit lands is a dart throw, not a coin flip
|
||
|
||
Date: 2026-09-19. Operator direction, 2026-09-19.
|
||
|
||
### The defect
|
||
|
||
`FireControl.Commit` places a hit like this:
|
||
|
||
```csharp
|
||
if (aimedCells != null && aimedCells.Length > 0 && random.NextFloat() < shot.Precision)
|
||
cell = aimedCells[random.NextInt(aimedCells.Length)];
|
||
else
|
||
cell = coords[random.NextInt(coords.Length)];
|
||
```
|
||
|
||
`Precision` is a coin flip between *exactly on the aimed item* and *uniformly
|
||
anywhere on the hull*. Aim at a stern thruster with Precision `.35` and the 65%
|
||
of hits that miss it are spread evenly over the bow, the cockpit and everything
|
||
else — no nearer the thruster than if the player had never aimed at all. There is
|
||
no spatial term anywhere in it.
|
||
|
||
This is the same shape as 5.1's Tracking cliff: a quantity that should be a
|
||
gradient implemented as a binary, so the failure case is maximally wrong rather
|
||
than slightly wrong.
|
||
|
||
Operator, 2026-09-19: "I would presume that aiming for an item in the stern would
|
||
still tend hit in that general area even if it misses the item. Like throwing a
|
||
dart at the grid."
|
||
|
||
### The rule
|
||
|
||
One Gaussian kernel over the target's hull schematic, in cell units:
|
||
|
||
- **Aim point.** The centroid of the aimed item's cells, or the hull's
|
||
`CenterOfMass` when nothing is aimed at. One path — the uniform-random branch
|
||
is deleted, not demoted.
|
||
- **Sigma.** Derived from the frozen `shot.Precision`, higher Precision giving a
|
||
tighter group. Authored so that the unaided floor (`UnaidedPrecision`, joining
|
||
`UnaidedAccuracy` and `UnaidedTracking` in `GameplaySettings`) is a sigma wide
|
||
enough to read as spraying at the silhouette.
|
||
- **Weights.** `w(cell) = exp(-d² / 2σ²)` for each occupied hull cell, `d` the
|
||
distance from the aim point.
|
||
|
||
### The tradeoff, and why it cannot be a second miss stage
|
||
|
||
Operator, 2026-09-19: "It would also mean that aiming can be a tradeoff; if
|
||
you're not precise enough, you're better off aiming center mass than risking
|
||
misses on a thin limb."
|
||
|
||
That tradeoff only exists if a stray dart can miss the ship. An earlier draft of
|
||
this section clamped a stray to the nearest occupied cell, which destroys it:
|
||
aiming at a wingtip would cost nothing and merely redistribute damage inward.
|
||
|
||
But resolving the stray as a *second* miss stage would break R3 — one roll
|
||
decides damage — and make `HitProbability` a lie, since a shot could pass the
|
||
roll and then fizzle on placement. So the off-hull mass is priced into the single
|
||
roll instead:
|
||
|
||
```
|
||
pOnHull = Σ w(cell) / (2πσ²)
|
||
```
|
||
|
||
the share of the kernel's total mass that lands on metal.
|
||
|
||
- `HitProbability` gains `pOnHull` as a factor. Aiming at a thin limb with a
|
||
loose group drops the number **on the HUD, before the trigger** — the tradeoff
|
||
is legible at decision time rather than discovered afterwards.
|
||
- `Commit` draws the cell as a weighted pick over the same `w(cell)`. A hit
|
||
always lands on metal, because the chance of not landing on metal was already
|
||
charged at the roll.
|
||
|
||
One kernel, computed by one function, used for both the probability and the
|
||
placement. Cut 3's named risk was two functions answering "where will this shot
|
||
go" and eventually disagreeing; this does not reintroduce it.
|
||
|
||
`pOnHull` depends only on the aim point, the hull shape and the frozen
|
||
`Precision`, so it folds into `PBase` at fire time and the freeze discipline
|
||
(R10, Q6) is unchanged.
|
||
|
||
### Consequences, named rather than discovered
|
||
|
||
- **Every hit in the game moves**, not only aimed ones: unaimed fire becomes
|
||
"aim at the centre of mass with the worst sigma", so big central sections soak
|
||
more fire than extremities. That is the intended change, and it is a balance
|
||
change.
|
||
- **A fat hull is inherently easier to hit than a needle**, for free.
|
||
- **The two targeting products' authored `Precision` values (`.05-.2` and
|
||
`.35-.55`) become meaningless as written**, because they are probabilities and
|
||
this makes Precision a grouping tightness. They are re-authored in the same
|
||
pass as Cut 6c's reciprocal `Resolution`.
|
||
- **Double-charging risk.** `HitProbability` already carries `pSpread`, which
|
||
penalises a small target against a wide weapon spread. `pOnHull` also
|
||
penalises small targets. Check whether the two charge the same thing twice
|
||
before authoring numbers; if they do, one of them owns it and the other goes.
|
||
- **AI willingness shifts.** `Combat.cs` and `TurretController.cs` gate on
|
||
`AgentMinHitProbability` against this number, so NPCs will hold fire more than
|
||
they used to the moment `pOnHull` lands. That is a tuning pass, not a bug, but
|
||
it will show up in the first smoke and should not be mistaken for broken AI.
|
||
|
||
### Verification
|
||
|
||
- `AimingAtTheSternHitsTheStern`: with a mid-range Precision, hits aimed at a
|
||
stern item land in the stern half far more often than the bow half. Mutation:
|
||
restore the uniform-random branch. Must die. This is the assertion the shipped
|
||
coin flip would fail.
|
||
- `ThinLimbCostsHitChance`: `HitProbability` against an aim point on a
|
||
single-cell extremity is strictly lower than against the centre of mass, at the
|
||
same Precision — and the gap widens as Precision falls. Mutation: drop
|
||
`pOnHull` from the probability. Must die.
|
||
- `PlacementAndProbabilityShareOneKernel`: the empirical on-hull rate over many
|
||
seeded draws matches the `pOnHull` the probability reported, within tolerance.
|
||
Mutation: perturb sigma in one of the two call sites only. Must die — this is
|
||
the test that would catch the two functions drifting apart.
|
||
- `EveryHitLandsOnMetal`: no committed hit ever resolves to an unoccupied cell,
|
||
across seeds and aim points including extremities.
|
||
|
||
### 7.4 Amendment (2026-09-19): the live-catalog test has a second job
|
||
|
||
7.3 above asks for one test that opens the real `GameData/Aetheria.cc` through
|
||
`AetheriaStores.Open`, because Cut 2 shipped a `required: true` rule with no data
|
||
to satisfy it and every green suite in the campaign missed it.
|
||
|
||
Merging Cuts 6b and 6c gave that test a second and larger job. 6b added
|
||
`WeaponItemData.AirburstRange` as a non-nullable `float` at a new MessagePack
|
||
key. Every weapon record in the shipped catalog was serialized before that key
|
||
existed, so the reader was handed `nil` for the absent slot, a `float` cannot
|
||
take nil, and `AetheriaStores.Open` threw outright — the game could not read its
|
||
own catalog. Fixed at `c896489b` by making the field `float?`.
|
||
|
||
Neither cut could have caught it. 6b verified with unit tests over synthetic
|
||
fixtures and never opened the shipped catalog; 6c opened the catalog constantly
|
||
but on a branch without the field. The defect existed only in the merge, and
|
||
only when something read real data.
|
||
|
||
So the live-catalog test is not a content check that happens to touch a file. It
|
||
is **the campaign's only assertion that the shipped data is readable by the
|
||
shipped code at all**, and it is the one test whose absence let a branch that
|
||
bricks the catalog pass every gate. Write it so it fails loudly on a
|
||
deserialization throw, not only on a missing design:
|
||
|
||
- `ShippedCatalogOpensAndGeneratesAnArmedHull`: open the real catalog read-only
|
||
through `AetheriaStores.Open`, enumerate every `EquippableItemData` so every
|
||
document type is actually deserialized (a lazy read that never touches
|
||
`WeaponItemData` would have passed on the broken merge), and generate a
|
||
loadout for `LonginusX`. Any exception is a failure.
|
||
- Mutation: none needed for the throw path — the shipped catalog either
|
||
deserializes or it does not. For the generation half, the Cut 2 mutation
|
||
applies: drop the targeting designs' products and the test must go red.
|
||
|
||
**The standing rule this cut records**, because it will recur every time a field
|
||
is added: a new non-nullable value-type field on a persisted CultCache document
|
||
cannot read records written before it existed. Nullable, or a migration that
|
||
rewrites every affected record — and the migration cannot run while the read
|
||
still throws, so tolerant deserialization comes first either way.
|
||
|
||
---
|
||
|
||
## Cut 8: a removed entity is a dead entity, and the anchors the diagnostics moved
|
||
|
||
Date: 2026-09-22. Branch `codex/fire-control-8` off `72c0109c`.
|
||
|
||
Operator play smoke, 2026-09-20: heat spike on entry with the ship unable to
|
||
move, a nearby ship likewise glowing hot and motionless, shots that registered no
|
||
hits, and finally a NullReferenceException out of `LockWeapon` reaching
|
||
`Entity.EntityInfoGathered` as the other ship vanished. Diagnosis (Codex, relayed
|
||
by the operator): the other ship's behaviors updated after the entity had been
|
||
cleaned out. Not reproducible afterwards, which makes it a hidden bug rather than
|
||
a fixed one -- the diagnostics commit `72c0109c` changed timing and allocation,
|
||
not the cause.
|
||
|
||
### 8.1 Removal and deactivation are one transition
|
||
|
||
`Entity.Deactivate` (`Entity.cs:445`) disposes every subscription, clears
|
||
`EntityInfoGathered`, `VisibleEntities`, `VisibleEnemies` and `VisibleFriendlies`,
|
||
and sets `_active = false`. `Zone.TryDock` (`Entity.cs:943`) has always paired the
|
||
two: `Zone.Entities.Remove(ship); ship.Deactivate();`. Removal from the zone means
|
||
the entity stops living there.
|
||
|
||
Cut 5.6 added the death path and did only half of it. `Zone.cs:78`:
|
||
|
||
Entities.ObserveAdd().Subscribe(add => add.Value.Death.Subscribe(_ => Entities.Remove(add.Value)));
|
||
|
||
The entity leaves the collection and stays **active**, holding live subscriptions
|
||
and stale references, while every surviving entity's `ObserveRemove` handler
|
||
(`Entity.cs:200-213`) nulls its `Target` and drops it from `EntityInfoGathered`,
|
||
`EntityHostility` and the visibility sets. Half the world has forgotten it and it
|
||
does not know it is dead.
|
||
|
||
- The death subscription becomes `Entities.Remove(e); e.Deactivate();`, matching
|
||
`TryDock`'s existing pairing exactly. One transition, one owner.
|
||
- **Ordering risk to verify, not assume:** `EntityInstance` also subscribes to
|
||
`Entity.Death` (its loot drop and destroy effect, `EntityInstance.cs:300-325`).
|
||
Subscription order decides whether loot drops before or after `Deactivate`.
|
||
`Deactivate` does not touch `Equipment` or `CargoBays`, so loot should survive,
|
||
but this must be confirmed in play rather than reasoned about -- if loot stops
|
||
dropping, that is this cut's fault and the deactivate moves after it.
|
||
|
||
### 8.2 A deactivated entity does not update
|
||
|
||
`Zone.Update` (`Zone.cs:172`) iterates a snapshot:
|
||
|
||
foreach (var entity in Entities.ToArray()) entity.Update(deltaTime);
|
||
|
||
so an entity removed part-way through a frame still receives its `Update` in that
|
||
same frame, after the rest of the world has dropped its references. `Entity.Update`
|
||
(`Entity.cs:1031`) never consults `_active`. Every behaviour then runs against
|
||
torn-down state, and `LockWeapon.cs:98` reaches `Entity.EntityInfoGathered[Entity.Target.Value]`
|
||
through a raw indexer.
|
||
|
||
- `Entity.Update` returns immediately when `!_active`.
|
||
|
||
This is the fix, and the raw indexer is deliberately **not** the fix. Guarding
|
||
`LockWeapon` with `TryGetValue` would silence this one call site and leave every
|
||
other behaviour running on a corpse -- the compensator the doctrine warns about.
|
||
Liveness is the entity's own property and the entity is the one place to enforce
|
||
it. `IsActive` already exists at `Entity.cs:116`; nothing was asking it.
|
||
|
||
### 8.3 The other symptoms are not explained by this, and are not closed
|
||
|
||
The heat spike, the motionless ships and the shots that registered no hits are
|
||
**unexplained**. One coherent story fits all three -- an item's heat driving the
|
||
targeting system offline makes `FireControl.Accuracy` fall back to
|
||
`UnaidedAccuracy` (`.05`), which reads exactly like "I can fire but nothing
|
||
lands," and an overheated thruster reads as "I cannot move" -- but that is a
|
||
hypothesis, not a finding, and nothing here has reproduced it.
|
||
|
||
What can be said from the source: `ActionGameManager.cs:1281` passes
|
||
`Time.deltaTime` into `Zone.Update` unclamped, and `Zone.cs:160` divides by it
|
||
(`orbit.Value.Velocity = (Position - PreviousPosition) / deltaTime`). Unity caps
|
||
`Time.deltaTime` at `Time.maximumDeltaTime`, so a large spike is bounded, but a
|
||
**zero or near-zero** first frame is not bounded from below and that division is
|
||
unguarded. Worth a probe before it is worth a fix.
|
||
|
||
This cut does not chase it. It records the symptom, fixes the defect that is
|
||
proven, and leaves the rest open with the evidence that exists -- an unreproducible
|
||
heat spike is not something to patch speculatively.
|
||
|
||
### 8.4 Re-anchor the four harnesses the diagnostics refactor stranded
|
||
|
||
`72c0109c` routed `HitProbability` through a new `FireControl.Inspect`, replacing
|
||
five early returns with flags and one gate. That is the campaign's own doctrine
|
||
applied correctly -- the operator's comment says it outright, that keeping the
|
||
calculation here stops the debug HUD growing a second, subtly different fire
|
||
control model -- and it rewrote the exact lines four harnesses anchor into.
|
||
|
||
Seven anchors now fail: `cut3` (2), `cut5` (1, its **no-op control**), `cut6c`
|
||
(2, including its control), `cut6d` (2). Every one reports `tree-clean: PASS`, so
|
||
nothing leaked, but a harness whose control cannot run is defending nothing.
|
||
|
||
- Re-anchor all seven onto the current spelling in `Inspect`. The rules they
|
||
attack are unchanged; only the text moved.
|
||
- A control that cannot find its anchor must stay a hard failure. Do not make
|
||
anchors fuzzy to survive refactors -- loudly stale is the property that makes
|
||
these harnesses trustworthy, and it is what surfaced this within one run.
|
||
|
||
### Verification
|
||
|
||
- `DeadEntityDoesNotUpdate`: an entity killed mid-frame receives no further
|
||
`Update`, and a `LockWeapon` on a ship whose target dies the same frame does not
|
||
throw. Mutation: drop the `_active` guard. Must die.
|
||
- `DeathDeactivates`: a dead entity reports `IsActive == false` and holds no live
|
||
subscriptions. Mutation: remove the `Deactivate()` call. Must die.
|
||
- `LootStillDropsOnDeath` if it can be reached headlessly; otherwise an explicit
|
||
operator check, named as such rather than assumed.
|
||
- All seven harnesses clean with tree-clean PASS, controls included.
|
||
- 222 tests plus the new ones; catalog reads; Unity batchmode exit 0.
|
||
|
||
---
|
||
|
||
## Cut 9: the die is not a die
|
||
|
||
Date: 2026-09-22. Branch `codex/fire-control-9` off `dcb8c448`.
|
||
|
||
Soul pass over `ca57819d..dcb8c448` (2026-09-22). Ten findings; this cut takes the
|
||
three that block play. The rest are Cut 10.
|
||
|
||
### 9.1 The hit roll has a range of 0.125 (Soul C1). **Blocking.**
|
||
|
||
`FireControl.cs:356`:
|
||
|
||
var random = new Random((zone.CombatSeed * 2654435761u) ^ (uint) shot.ShotId | 1u);
|
||
...
|
||
var hit = p > 0f && random.NextFloat() < p;
|
||
|
||
`CultMath.Random` is xorshift32 and `NextFloat()` returns the top 24 bits of the
|
||
**first** round (`Random.cs:22-42`). One round does not carry a low-bit seed
|
||
difference into the high bits. `ShotId` varies only in the low bits, so the top
|
||
three bits of the first draw are fixed by the zone seed alone.
|
||
|
||
Measured, 2000 shot ids per zone: **every zone's roll spans 0.1249**. Reproduced
|
||
independently with a different hash function — the span is structural, not a
|
||
property of `StableHash`. The consequence is that a shot is a step function of
|
||
`p` whose threshold is fixed when the zone is created: below the band it never
|
||
hits, above it always does. An unaided shooter hit 0 of 18,926 shots against the
|
||
shipped catalog. Roughly one zone in eight cannot land a fully aided shot at
|
||
p = .74.
|
||
|
||
This is the operator's "I can target and fire with no hits registering at all,"
|
||
and it is stable per zone, which is why it came and went.
|
||
|
||
It is also this map's own prescription (Cut 6b, §6.1), written by Self, shipped
|
||
through three cuts, and never once measured.
|
||
|
||
**The rule:** the roll must be uniform on [0,1). Not merely deterministic —
|
||
determinism is what a constant already gives, and is exactly why three
|
||
determinism tests passed over it.
|
||
|
||
- Mix the seed before the first draw, with a standard 32-bit finalizer
|
||
(`fmix32`/`splitmix32`: xor-shift, multiply, xor-shift, multiply, xor-shift).
|
||
The per-shot seed derivation is unchanged; only its diffusion into the first
|
||
output changes. Determinism, the `(zone, shot id)` purity and the freeze
|
||
discipline all survive untouched.
|
||
- **This is a caller's obligation, not a CultMath defect.** Seeding xorshift with
|
||
a structured value and drawing once is the documented way to get a correlated
|
||
first output. Record a follow-up to give `CultMath.Random` a mixed-seed entry
|
||
point so no other GameCult consumer re-derives this the hard way — that is a
|
||
CultLib cut with its own release, not this one.
|
||
|
||
**Verification, and the test whose absence caused this:**
|
||
|
||
- `TheDieIsUniform`: fire many shots across several zone seeds at an
|
||
intermediate `p` (say .3 and .6) and assert the observed hit fraction is near
|
||
`p`, and that the observed rolls span substantially more than 0.2. Mutation:
|
||
remove the finalizer. Must die.
|
||
- No test anywhere in this campaign measures a distribution. Every behavioural
|
||
fixture uses `accuracy: 1` or `accuracy: 0`, which is why a die stuck on one
|
||
face satisfied all of them. That gap is the finding, not the line of code.
|
||
|
||
### 9.2 `pOnHull` collapses to zero above Precision ~4 (Soul C2). **Blocking.**
|
||
|
||
`FireControl.cs:497-516`. `Sigma = 1/Precision`, and once sigma falls below about
|
||
half a cell every `exp(-d²/2σ²)` underflows against an aim point that is not
|
||
exactly on a cell centre — which is **every shipped hull**, since no centre of
|
||
mass is integral (LonginusX 2.5/6.697, Zenith 5.5/5.5, Turret 3.5/3.5). Measured
|
||
`pOnHull` on LonginusX: `.516` at Precision .3, `1.000` at 1.19, `.112` at 5,
|
||
`.000` at 10 and beyond.
|
||
|
||
So a better targeting system makes you miss, and past Precision ≈ 5 the weapon
|
||
cannot hit anything. Authored content sits at .5-1.3, just under the cliff, with
|
||
a margin of about 4x — and the operator is about to tune this number.
|
||
|
||
- Clamp sigma at a floor of about half a cell, or normalize by the discrete sum
|
||
rather than the continuous 2πσ². Either makes `pOnHull` monotonic in Precision
|
||
over the whole domain instead of only below the cliff.
|
||
- `WeightedPick` (`:535-545`) carries a comment claiming it "can never fall
|
||
through." It can, whenever `totalWeight == 0`, returning a fixed corner cell.
|
||
It is unreachable today only because `pOnHull == 0` forces `PBase == 0` first.
|
||
Fix the cause and delete the false comment.
|
||
- Cut 6d's map section claims to name its consequences; it does not name this.
|
||
Add it.
|
||
|
||
### 9.3 The LockWeapon crash is still reachable (Soul C3). **High.**
|
||
|
||
Cut 8 fixed a real liveness defect and did **not** fix the operator's crash.
|
||
`LockWeapon.cs:98` only runs on an *active* entity, so 8.2's `!_active` guard
|
||
cannot be what prevents it — `tests/mutation_tests_fire_control_cut8.sh:17-19`
|
||
says as much outright, and I merged it anyway.
|
||
|
||
The live precondition is `Target.Value` set but absent from `EntityInfoGathered`
|
||
on an active entity, and docking still reaches it: `Deactivate` (`Entity.cs:445`)
|
||
disposes the `Entities.ObserveRemove` subscription that is the only thing nulling
|
||
a stale `Target`, and nulls nothing itself; `Activate` (`Entity.cs:167`) reseeds
|
||
`EntityInfoGathered` and likewise leaves `Target` alone. Dock, let the target die
|
||
or leave, undock, and the first frame throws.
|
||
|
||
- `Deactivate` nulls `Target` (and `TargetItem`), or `Activate` reconciles it
|
||
against the reseeded `EntityInfoGathered`. One of the two owns it; say which.
|
||
- Still not a `TryGetValue` in `LockWeapon`. The invariant is that an active
|
||
entity's `Target` is always a live entity it has info on.
|
||
- Pin it with the dock/die/undock sequence, not with a unit test on the guard.
|
||
|
||
### Verification
|
||
|
||
- The three tests above, each dying under its own mutation, in a committed
|
||
`tests/mutation_tests_fire_control_cut9.sh` with a no-op control, byte-exact
|
||
I/O, sha256-verified restore and an explicit tree-clean verdict.
|
||
- All nine harnesses sequentially, one at a time, all clean.
|
||
- 226 tests plus the new ones; catalog reads; `dangling` 0.
|
||
- After 9.1 lands, re-measure the fight: Soul's probe, run with a corrected die,
|
||
gave 60.8% hit rate and a 4.3 s median time to kill for an aided shooter, 2.6%
|
||
unaided. Confirm the shipped build now reproduces that, because those are the
|
||
numbers the operator will tune against.
|
||
|
||
### Recorded for Cut 10, not fixed here
|
||
|
||
Soul C4 (a committed hit on a target that dies mid-flight publishes as a hit),
|
||
C5 (`Inspect` is numerically exact but costs 9.87 µs and 448 B per call on paths
|
||
that used to bail — the operator's own comment on the hot path looking like a
|
||
diagnostic one has an empirical answer now), C6 (`RollsAreSeeded` is obsolete —
|
||
Cut 7 retired one such test and missed this one), C7 (`FireControlCut6cTests`
|
||
authors `Precision = 0`, making six of seven checks vacuous), C8 (`Precision =
|
||
1000` fixtures make several kernel-shape mutants unkillable, and `pOnHull`'s
|
||
deletion is caught by exactly one test), C9 (`EveryHitLandsOnMetal` runs one
|
||
configuration, not the range it claims), plus the harness-audit items: the
|
||
tree-clean verdict restores before it measures, a red result cannot distinguish a
|
||
killed mutant from a compile failure, and the no-op control covers only one file
|
||
per harness.
|
||
|
||
---
|
||
|
||
## Cut 11: mutation testing moves to Stryker, and the survivors it found
|
||
|
||
Date: 2026-09-22. Operator, on the hand-written anchor harnesses: "What is the
|
||
point of those mutation anchors? Tests should cover how the code behaves, not how
|
||
it is shaped." And on Stryker: "sounds good; we don't need to run with every
|
||
mutation active, right? > to >= on float thresholds is indeed petty."
|
||
|
||
### Why
|
||
|
||
The Eureka principle (2026-09-15) is sound and stays: every rule needs a test that
|
||
fails when the rule breaks. The mechanism that accreted on it over the following
|
||
week -- literal-text anchors, perl substitution, per-cut bash and Python harnesses
|
||
-- coupled mutation testing to code shape. It stranded anchors on every refactor
|
||
(`72c0109c`, Cut 9, Cut 10), and at Cut 10 an anchor kept matching exactly once
|
||
while silently re-targeting onto `Inspect`, mutating code the tests no longer ran.
|
||
Soul's harness audit also found the tree-clean verdict restoring before it
|
||
measured, and a compile failure counting as a kill.
|
||
|
||
A Stryker spike against `FireControl.cs` alone: 276 mutants, 7m18s, 208 killed,
|
||
67 survived. It killed every mutation the hand-written harnesses defended, never
|
||
edits a source file, and has nothing to re-anchor.
|
||
|
||
### 11.1 Retire the ServerShared harnesses
|
||
|
||
Delete the 23 hand-rolled harnesses (5,816 lines) whose targets are all under
|
||
`Assets/Scripts/ServerShared` -- every `mutation_tests_fire_control_*`, every
|
||
`mutation_tests_stats_power_*`, `mutation_tests_shield_reserve.py`,
|
||
`mutation_tests_condition_ratio_cut8.py` and `mutation_tests.py`. Stryker's scope
|
||
covers all of their targets; the behavioural tests they measured stay.
|
||
|
||
**Kept, recorded, not yet answered:** `mutation_tests_addressables_cut.py` and the
|
||
three `mutation_tests_shield_panel_cut*.py`. They target Unity Editor probes
|
||
(`EngineAssetCheck.cs`, `ShieldPanelCut*Verify.cs`) that Unity compiles outside
|
||
`Aetheria.Shared`, so Stryker cannot reach them. They carry the same anchor
|
||
fragility; the right answer for GPU and editor code is probably that the batchmode
|
||
probes are themselves the behavioural tests, but that is a separate decision.
|
||
|
||
### 11.2 Scope, pinned
|
||
|
||
`tests/Aetheria.Shared.Tests/stryker-config.json` scopes mutation to
|
||
`Assets/Scripts/ServerShared/**` and excludes plugins. Per cut, run against the
|
||
cut's own diff:
|
||
|
||
dotnet tool restore
|
||
cd tests/Aetheria.Shared.Tests
|
||
CULTLIB_ROOT=<admitted CultLib worktree> dotnet stryker --since:<base commit>
|
||
|
||
**Float-threshold boundary flips are known-equivalent.** Stryker files `>`→`>=`
|
||
under the same Equality mutator as `==`→`!=`, and the latter catches real bugs, so
|
||
the mutator stays on. A survivor that only moves a float comparison across its
|
||
boundary is triaged as equivalent and never chased. For the same reason the raw
|
||
mutation score is not a gate; the survivor list is the signal.
|
||
|
||
### 11.3 Degenerate fixtures
|
||
|
||
Most of the 67 survivors trace to three constants shared across the fire-control
|
||
fixtures, each of which makes a family of mutants arithmetically identical to the
|
||
original -- the same defect class as the zero-velocity target and the durability
|
||
coincidence, found here as a pattern instead of one at a time:
|
||
|
||
- **Shooter at `float3.zero`.** `target - source` equals `target + source`, so four
|
||
separate position mutations survive (`FireControl.cs` HitProbability, Inspect,
|
||
Fire, Apply).
|
||
- **Fire time zero.** `now - FireTime` equals `now + FireTime`, so the elapsed-time
|
||
mutation survives -- which means `EvasionCountsUntilCommitAndNotAfter` still does
|
||
not defend the rule Cut 5 gave it a moving target to defend.
|
||
- **Spread zero everywhere.** `PSpread` returns 1 on every fixture, so it is
|
||
effectively untested, and `*` versus `/` against it is indistinguishable.
|
||
|
||
Move shooters off the origin, fire at nonzero times, and give some fixtures real
|
||
spread. Fix the fixture, never weaken the assertion.
|
||
|
||
### 11.4 Behavioural tests the survivors point at
|
||
|
||
- **Splash handedness.** Flipping the sign in `var right = float2(forward.y,
|
||
-forward.x)` passes every test. That is the operator's "damage on the wrong side
|
||
of the ship," and Self said on 2026-09-19 that Cut 6b would add a test pinning
|
||
that a port blast damages port cells; it never landed and nothing checked. Pin it.
|
||
- **The shield loses energy when it absorbs.** Deleting `Shield.TakeHit` from the
|
||
discrete path survives, because tests assert the hull was untouched and never
|
||
that the shield paid for it.
|
||
- **Splash's shield-absorb branch** has no coverage at all.
|
||
- **Zones roll differently.** `CombatSeed *` → `/` makes the seed zone-independent
|
||
and survives; `TheDieIsUniform` checks uniformity within a zone only. Tighten it
|
||
enough that the `MixSeed` weakenings that survive it (`>>`→`<<`, `^=`→`|=`) die.
|
||
- **A shot whose target left the zone resolves as a miss** (Soul C4). Today only
|
||
the uncommitted branch rewrites the outcome; a committed hit on a target that
|
||
died mid-flight is published as a hit. Fix the code and pin it.
|
||
|
||
### Verification
|
||
|
||
Stryker over `FireControl.cs` before and after: every non-equivalent survivor
|
||
above killed, the remainder triaged by name. 231 tests plus the new ones; Unity
|
||
batchmode clean.
|
||
|
||
### Status (2026-09-22)
|
||
|
||
Landed on `codex/fire-control-10`: `8ef3b34c` (Stryker tool), `b305debd` (map,
|
||
config), `724ec5e7` (11.1), `a6543af7` (11.4 and the C4 fix), `1b187db4` (11.3),
|
||
`b0fd3cd0` (second round, below). 244 tests.
|
||
|
||
Stryker over `FireControl.cs`, 278 mutants per run:
|
||
|
||
| Run | Killed | Survived | Score |
|
||
|---|---|---|---|
|
||
| Spike, before Cut 11 | 208 | 67 | 74.6% |
|
||
| After 11.4 | 219 | 58 | 78.6% |
|
||
| After 11.3 fixtures | 225 | 51 | 81.1% |
|
||
| After the second round | 225 | 43 | 83.3% |
|
||
|
||
**The second round** came from reading the 51. Four were real undefended
|
||
behaviour and one was a duplicated formula:
|
||
|
||
- `Inspect` recomputed `PBase` as its own product of the factors, so the gates and
|
||
the product had two owners, and a `*`→`/` mutant of the HUD's copy survived the
|
||
agreement test (every fixture had spread 0, so `PSpread` was 1). `PBase` is now
|
||
`HitProbability`'s own answer; the test became
|
||
`TheHudShowsTheFactorsTheSimulationMultiplies`, over a spread cone that does not
|
||
fill the silhouette. `Fire`'s redundant `target != null ?` guard around
|
||
`HitProbability` went with it.
|
||
- A beam shot's `damageOverride` could be dropped: `BeamRollsPerInterval` computed
|
||
the expected damage from its own roll count, a tautology, now deleted.
|
||
`ADamageOverrideIsWhatTheShotCarries`.
|
||
- `Tracking` could ignore the fitted targeting system: no fixture's target ever
|
||
moved off its predicted line. `AFittedTargetingSystemForgivesAJink`.
|
||
- A shot orphaned before commit could skip `ShotCommitted`, or publish its miss as
|
||
shielded. `ShotAtTargetThatLeavesBeforeCommitCommitsAsAMiss`.
|
||
- `ArrivalIn` (the HUD countdown) was unchecked; asserted in the C4 test.
|
||
|
||
**The 43 that remain, triaged:**
|
||
|
||
- *Boundary flips, equivalent (23):* L52, 54, 56, 75, 135 (two), 157 (two), 193
|
||
(two), 223, 273, 410, 418 (two), 465 (two), 503, 518, 524, 586, 605, 608.
|
||
- *`MixSeed` shift and xor weakenings, measured equivalent (4):* L382, 384, 386
|
||
(two). 11.4 asked for them to die; measured, the die stays uniform and
|
||
zone-distinct under each, so there is no behaviour to pin. They change how the
|
||
mixer is built, not whether the die is fair.
|
||
- *Unreachable through the live callers (8):* L240 and L241
|
||
(`DeviationProbability`'s null-target branch; the engine guards it, only the HUD
|
||
can pass null), L283 (three; airburst has no authored radius anywhere, recorded
|
||
since Cut 6b), L320 (empty-list early return, an optimisation), L410 `||` (a
|
||
nonzero `PBase` implies a target), L158 and L610 (no coverage: an unlocked lock
|
||
weapon reaching this line, and the last-cell fallback of the weighted pick).
|
||
- *Recorded gaps, not yet reached (8):* L135 ternary (two; no test that the AI in
|
||
`Combat.cs` leads a moving target), L435 (three; the aimed flag on an outcome),
|
||
L464 and L465 negate (the `Apply` hit direction through the `FireControl`
|
||
path), L585 and L587 (the aimed item's centroid as aim point). These are "not
|
||
yet reached," not "unreachable."
|