Map fire control, superseding the playground draft
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8a00fc9235
commit
a62d52b64e
1 changed files with 862 additions and 0 deletions
862
docs/fire-control-cut.md
Normal file
862
docs/fire-control-cut.md
Normal file
|
|
@ -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<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.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Loading…
Reference in a new issue