From a62d52b64ee0019cb60b465cdd671014d0c93458 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 19 Sep 2026 06:36:01 +0200 Subject: [PATCH] Map fire control, superseding the playground draft Co-Authored-By: Claude Opus 5 --- docs/fire-control-cut.md | 862 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 862 insertions(+) create mode 100644 docs/fire-control-cut.md diff --git a/docs/fire-control-cut.md b/docs/fire-control-cut.md new file mode 100644 index 000000000..a65553751 --- /dev/null +++ b/docs/fire-control-cut.md @@ -0,0 +1,862 @@ +# 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`. Cleared when the target leaves `VisibleEnemies` (`:196`, + `:222`). `TargetRange` is recomputed each tick at `:920`. +- `Entity.EntityInfoGathered` — `Entity.cs:57`, a `ReactiveDictionary` 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. + +--- + +## 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`), 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`). | 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`. | 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`, 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`), the presentation + feed. +- `InstantWeapon.OnFire` becomes `event Action` 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=\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 `, 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.