885 lines
58 KiB
Markdown
885 lines
58 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.
|