Map: missiles and point defense, with prior art, body facts M1-M14 and the authority map

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-06 21:25:10 +02:00
commit c7432f39a8

View file

@ -3526,3 +3526,298 @@ Source marks as above.
reads `HelmInput.Demand` and decides nothing.
- Forbidden: a second hit price or arc test in Gameplay or UI (the HUD reads `Solution`
and `HitProbability`); a face-aim flag outside `HelmInput`.
## Missiles and point defense
Ruling `missiles-sim-side-light`: "We need missiles sim side, and we can cut that so as to
enable PDC without requiring full entities. Kind of like the asteroids." Ruling
`thresholded-autofire`: "I think we need thresholded autofire as an option, otherwise PDC
would require too much micro." Full munition entities stay later (ruling
`drones-and-munitions-are-entities`, follow-up `drones-munitions-substrate`); nothing here
may foreclose them. Anchors are against `origin/master` `26f21e32`, read in a detached
sparse scratch worktree on 2026-10-06. Cuts: `missile-records`, `munition-shots`,
`autofire-threshold`, `ai-autofire`.
### Prior art
Source marks as in Controls.
- **Nebulous: Fleet Command** (wiki: the community `mechanics:missiles` page, read in full).
Missiles are tracked contacts with hit points by template (10 to 150) and armour as wall
thickness, and a radar cross-section that varies with aspect. Point-defense turrets and
defensive missiles engage them automatically; decoys soak PD attention (turrets take
decoys first). Launch rate is bounded by programming channels, which caps salvo
density. Defensive missiles cannot engage other defensive missiles, so two ships cannot
dump their magazines at each other's interceptors (snippet, glossary search).
- **Starsector** (wiki: `Weapon_data.csv`, read in full). A missile is a projectile with
"proj hitpoints". Weapon hints decide targeting, not whether a round can physically
connect: `PD` targets missiles but prefers missiles in range over ships, `PD_ONLY`
targets only missiles (and fighters with `ANTI_FTR`), `PD_ALSO` takes missiles only when
nothing else is available. Autofire runs per-weapon target AI (snippet, mod README).
Forum consensus (snippet): missile power is non-linear, weak until it saturates PD, then
strong until magazines run dry.
- **FreeSpace 2** (snippet, hard-light wiki `Weapons.tbl` and `Ai_profiles.tbl`). Bombs are
"interceptable": weapons with hit points and an enlarged collision radius. Turrets shoot
bombs; AI profiles stop turrets targeting bombs beyond their own weapon range; bombs are
invulnerable for a short delay after launch.
- **Children of a Dead Earth** (snippet, forum threads). PD is a saturation contest: either
saturate it with many small rounds or send few heavily armoured missiles; small lasers
are enough for PD. The counterplay is economic, not a hard counter.
- **Homeworld** (snippet). Flak and defense-field frigates are the anti-missile and
anti-strike roles; no technical detail found.
**Failure modes to design against.**
1. PD trivialises missiles: perfect interception makes launchers dead weight. Answer: a
PD round is priced and rolled like any shot (spread against a small target, range,
sensor info), a round that cannot arrive before the missile does has no solution, and
heat and ammunition bound how much a group can spend. Saturation then beats PD, as in
Starsector and CDE.
2. PD that never works: interception left to luck the player cannot read. Answer: the HUD
marks detected inbound munitions and the ones an autofire gun is engaging, and an
intercept shows an effect at the munition's simulated position.
3. Unreadable salvos: thirty rounds and thirty tracers. Answer: markers only for detected
hostile munitions within weapon reach, one shape, no per-gun lines.
4. Cost blowups: per-missile physics, per-missile agents, per-gun O(contacts) scans every
frame. Answer: a missile has no stepping state at all (its pose is a function of its shot
and zone time, like a belt rock's), and an autofire gun keeps its subject while it stays
worth firing, so the common tick prices one subject per gun.
5. Recursion: PD missiles shooting PD missiles (Nebulous's explicit rule). Answer: a
munition weapon never takes a munition as its subject.
### Body facts
- **M1. A guided round is an ordinary shot in the simulation.** `LauncherData`
(`Behaviors/Launcher.cs:10`, a `LockWeaponData`, so a `LockWeapon`) and `GuidedWeaponData`
(`:37`, an `InstantWeapon`) fire through `InstantWeapon.Execute` into `FireControl.Fire`
(`InstantWeapon.cs:271`) like any gun. Flight time is `FlightDistance / weapon.Velocity`
(`FireControl.cs:559`); `CommitTime` is `ArrivalTime - CommitHorizon` (0.5 s,
`Settings.cs:250`), or the fire time for a fused round with no target (`:588`); `Commit`
rolls at that horizon (`:707`) and `Step` applies at arrival (`:645`). No ServerShared
code reads `MissileVelocity`, `Thrust`, `DodgeFrequency` or the three curves.
- **M2. The flight is Unity's and decides nothing.** `GuidedProjectile.Update`
(`Gameplay/Weapons/GuidedProjectile.cs:127-212`) homes with `first_order_intercept`, a
noise dodge and the guidance, lift and thrust curves. `GuidedProjectileManager.Bind`
(`GuidedProjectileManager.cs:14-26`) subscribes to `ShotCommitted` (retarget to the
burst point) and `ShotResolved` (burst or fade). The split branch (`GuidedProjectile.cs:166-194`)
spawns visual children that no shot owns. Between fire and arrival a missile is a
`PendingShot` with no position and a Unity transform nothing simulated can reach.
- **M3. The `GuidedWeaponData` branch homes on the aim** (`GuidedProjectileManager.cs:62-80`, the aim at `:79`,
`LookDirection`, renamed by `controls-helm`). An unfused round with no engaged target has
`FlightDistance` 0 (`FireControl.cs:494`) and so resolves the tick it fires.
- **M4. The pending shot.** `PendingShot` (`FireControl.cs:1748-1798`) is a struct in
`Zone.PendingShots` (`Zone.cs:46`), never saved; `TryGetShot` is a linear scan
(`Zone.cs:55-65`). Its `Target` is an `Entity`. `Step` resolves a shot whose target left
the zone as a fresh `Miss`, at any stage, without rewriting the committed outcome
(`:619-629`): the precedent an intercepted missile follows. `ShotResult` is `Miss`,
`Hit`, `Burst` (`:1944-1949`).
- **M5. Mines are not simulation records.** `Mine` moves by a Unity `GridObject`, arms by
`Physics.OverlapSphere` and damages through `FireControl.Detonate`
(`Gameplay/Weapons/Mine.cs`, `MineManager.cs`). `Detonate` (`FireControl.cs:1350`) iterates
`zone.Entities` only. Follow-up `bodies-entity-and-mines` owns mines; no cut here moves them.
- **M6. No point defense exists.** A case-insensitive search for `point defen`, `PDC`,
`\bPD\b` and `anti-?missile` over `Assets/Scripts` and `tests` returns nothing. Nothing can
take a shot as its target.
- **M7. Targets.** `TargetRef` (`Zone.cs:595-621`) is an `Entity` or a `ChunkId`. The zone
registers `EntityTargets` and one `BeltTargets` per belt (`Zone.cs:101`, `:119`). Shots
at chunks are not routed yet (`FireControl.cs:533`, mining Cut 4).
- **M8. A weapon's only subject is its entity's one target.** `Fire` reads
`source.Target` (`FireControl.cs:534`), `Refuses` reads it (`:94`), and
`Weapon.StanceAllowsFire` reads it (`Weapon.cs:107`).
- **M9. AI fire is per weapon.** `Combat` (`Agents/States/Combat.cs:119-124`) and
`TurretController` (`Behaviors/TurretController.cs:83-96`) activate each weapon when
`FireControl.AgentFires` (`FireControl.cs:101-109`) passes: `HitProbability >=
AgentMinHitProbability` (0.2, `Settings.cs:259`) for an unfused weapon, designated and not
refused for a fused one.
- **M10. Presenters fly along the barrel.** `ProjectileManager.Fire` launches along
`barrel.forward`, not the shot's frozen `TravelDirection`; package hulls have no
articulation (C9), so their rounds leave along the hull whatever the simulation decided.
- **M11. EntityPack keys.** The base `EntityPack` uses keys 0 to 16; `ShipPack` uses 17 to
19 and `OrbitalEntityPack` 17 to 20 (`EntitySerializer.cs:153-189`). The next free base key
is 21.
- **M12. Shift is already an action-bar slot.** `GetDefaultPlayerSettings`
(`ActionGameManager.cs:95-105`) binds `<Keyboard>/leftShift` to action-bar slot 0, which
`controls-helm` r2 also binds to Face Aim. Reported to Self; not this section's cut.
- **M13. Launcher content.** GT 3K, pswarm and scorched void policy carry guidance-system,
thruster and warhead roles; LRMM72 and SRMM72 are flat (`docs/stats-power-cut7-roles.md:139-146`).
The Launcher Angles scenario fits a GT 3K and a pswarm (`Scenarios/LauncherAngles.cs`).
- **M14. Cost, by reading rather than a scratch build.** The design below adds no per-tick
work per missile: a pose is a closed-form function evaluated when asked. The costs are the
provider's scan of `PendingShots` (O(shots in the zone) per query) and the autofire
chooser's pricing (one subject per autofire gun per tick while it stays worth firing;
O(munitions plus entities in reach) only when it re-picks). The drones follow-up's
measurement of per-entity cost belongs to full munition entities, which this does not
build. The cuts pin the costs with counters, as the mining index does.
### The design
**A missile is its pending shot.** A shot is a munition when its weapon data is
`LauncherData` or `GuidedWeaponData` (`Munitions.IsMunition(EquippedItem)`, the one test)
and it has not yet arrived. It needs no second record: identity is its `ShotId`, its
damage taken is one runtime field on the shot (`MunitionWear`), and its position is
derived. This is the asteroid model: a belt rock's pose is a function of zone time and its
wear the only stored state; a missile's pose is a function of its shot and zone time, and
its wear the only stored state. Nothing steps a missile.
**The pose.** `Munitions.Pose(Zone zone, in PendingShot shot, float now) -> (float2
position, float2 velocity)`: a quadratic Bezier on the plane from the fire origin, through
a control point ahead of the launcher along its mount (`LaunchDirection`, frozen at fire)
at `MunitionLaunchArc` times the flight distance, to the end point, at `s = (now -
FireTime) / (ArrivalTime - FireTime)`. The end point is the live position of the shot's
target while it has one (so the missile homes), the committed burst point once the commit
named one, and the frozen burst position for a fused round with no target. Velocity is the
derivative with the end point held (good enough for intercept lead). The curve reaches its
end exactly at `ArrivalTime`, so the missile arrives where and when `FireControl` resolves
it. **The pose decides where a missile is, never whether it hits:** the roll stays at
`CommitTime` against the same deviation and silhouette as before, and a missile's
`CommitTime` and `ArrivalTime` are computed by the same lines as any shot's. Height is
presentation: the simulation is planar, so the presenter may lift the round on its
`LiftCurve` without disagreeing with anything simulated.
**Killed in flight.** `FireControl.DamageMunition(Zone, MunitionId, float damage)` adds to
the shot's `MunitionWear`. At the top of each shot's turn in `Step`, a munition whose wear
has reached `MunitionDurability` resolves as `ShotResult.Intercepted` with its burst point
at its pose: published on `ShotCommitted` if not yet committed and on `ShotResolved`,
removed, no damage applied, exactly as a target that left the zone resolves as a fresh
miss. A kill after the commit cancels the committed hit, because the committed outcome is
applied only at arrival. A missile destroyed does not detonate.
**Seen like a rock.** A munition's detection uses the passive-sensor balance `ChunkInfo`
uses, with one visibility for every munition (`GameplaySettings.MunitionVisibility`).
Durability, radius and visibility are global settings because a light record has no gear;
full munition entities will take them from their warheads, thrusters and seekers (ruling
`drones-and-munitions-are-entities`).
**Targetable through the index.** `TargetRef` gains a third case, `MunitionId` (the shot
id). `MunitionTargets : ITargetProvider`, registered beside `EntityTargets`, offers every
live munition as a candidate at its pose, with one region whose visibility ceiling is
`MunitionVisibility`. `Entity.SetTarget` refuses a munition: the player never designates
one by press; munitions are autofire's business.
**Shots at a munition.** `PendingShot.Subject` (a `TargetRef`) replaces the stored
`Target`; `Target` stays as a read-only `Subject.Entity`, so every entity path is
unchanged. For a munition subject `FireControl` decides: designated (detected, in range,
the weapon is neither a `LockWeapon` nor a munition weapon), in arc of its pose, its
intercept from the pose and velocity, and the price `Accuracy * PSensor(info) *
PSpread(spread, 2 * MunitionRadius, range)` with no silhouette, zero when the round cannot
reach the intercept before the munition's `ArrivalTime`. Commit rolls it with the frozen
fire-time prediction against the live pose (a curving missile defeats a slow round).
`Apply` on a hit calls `DamageMunition`. `Detonate` also damages every munition whose pose
lies within its radius plus `MunitionRadius`, so flak and proximity rounds are point
defense too. This is the first shot at a non-entity subject; mining Cut 4 routes chunk
shots through the same `Subject`.
**Thresholded autofire.** A weapon group can be set to autofire with a threshold (default
`AgentMinHitProbability`, 0.2). Each tick, before behaviours, `Autofire.Update` gives each
weapon of an autofire group a subject from `FireControl.ChooseSubject` and activates it
when one is chosen, deactivating it otherwise. `ChooseSubject` keeps the current subject
while it is still worth firing; otherwise it takes, in order, the most urgent detected
hostile munition in reach (least time to arrival), then the designated target, then the
detected hostile entity in reach with the highest price, each only if worth firing.
"Worth firing" is one function, `FireControl.Worth(weapon, shooter, subject, threshold)`:
the price against the threshold for an unfused weapon, designated and not refused for a
fused one. `AgentFires` becomes `Worth` at the agent threshold. `FireControl.SubjectOf(weapon,
shooter)` is the one answer to "what does this weapon fire at": its autofire subject in an
autofire group, else the entity's designated target. `Fire`, `Refuses`, `Solution`,
`ArcPermitsFire` and the stance gate read it.
**What makes a gun point defense** is question `pd-who-engages`. The specs are written for
the recommended `any-gun`: nothing in data. Any autofire gun can engage a munition, and
the price decides: a fast, tight, short-flight gun clears a threshold against a small fast
target, a heavy gun does not. PD is a group the player sets to autofire with light guns in
it, and heat is the waste guard (Self's proposal to the operator).
**The split Self proposed** (manual fire on the designated target, autofire groups picking
their own subjects above the threshold) is what this design builds: manual fire is
`controls-mount-aim` r2 unchanged (`SubjectOf` returns the designated target), and
autofire's own choice is `ChooseSubject`. Question `controls-solution-subject` stays the
operator's: under `designated` nothing here changes; under `any-contact` manual fire would
call the same `ChooseSubject` at threshold zero, an r3 of `controls-mount-aim` and nothing
else.
**The AI.** Agents fire through the same chooser (`ai-autofire`): `Combat` and
`TurretController` treat every group as autofire at the agent threshold, with their chosen
target as the designated one. That is one owner of "which subject, and is it worth it" for
player and AI alike, and AI point defense against the player's missiles falls out of it.
Starsector's ships run the same autofire AI as the player's autofire groups.
**The HUD.** An autofire group's action-bar slot shows a filled mark and its threshold
(Starsector's convention). `ControlsHud` draws a small diamond on each detected hostile
munition within the ship's longest weapon range, ringed while one of your autofire guns
has it as subject, and the group crosshair of an autofire group follows its guns'
solutions as for any group (it reads `Solution` through `SubjectOf`).
### Authority map
- **Owner.** `FireControl` owns every munition transition (fire, commit, damage,
interception, resolution) and every subject question (`SubjectOf`, `ChooseSubject`,
`Worth`). `Munitions` owns the pure facts of a munition (`IsMunition`, `Pose`, detection
info). The player owns which groups are autofire and their thresholds.
- **Inputs.** The frozen shot, zone time, the target's live position, the committed
outcome; the weapon's stats and the shooter's sensors; `Entity.Autofire`.
- **Outputs.** Munition poses for the index, the HUD and the presenter; `Intercepted`
outcomes; weapon activation for autofire groups; the saved autofire settings.
- **Derived state.** A munition's pose and velocity (on every read); its detection info
(on every read); the index's munition region. Never stored, never saved.
- **Demotions.** `GuidedProjectile` is no longer the owner of a missile's flight; it places
its transform at `Munitions.Pose` and owns only how the round looks. `Entity.Target` is no
longer every weapon's subject; it is the designated target, the subject of manual fire.
`PendingShot.Target` is no longer a stored field; it derives from `Subject`.
- **Forbidden writers.** Any homing, intercept or guidance arithmetic in `Gameplay` or
`UI` for a munition. Any write of `MunitionWear` outside `FireControl.DamageMunition`. Any
read of `Entity.Target` as a weapon's subject outside `SubjectOf`. Gameplay or UI code
activating an autofire group's weapons. A second worth-it test beside `Worth`. A second
record type, list or dictionary of missiles beside `Zone.PendingShots`.
- **Shared paths.** Player autofire, `Combat` and `TurretController` choose through
`ChooseSubject` and decide through `Worth`; manual fire, autofire and the AI all fire
through `Fire` reading `SubjectOf`; the presenter, the HUD and the index read the same
`Pose`; a PD hit and a blast reach a munition through the same `DamageMunition`.
- **Deletion line.** Before any new behaviour: `GuidedProjectile`'s homing, dodge, thrust
and split code goes, and the stored `PendingShot.Target` field becomes the `Subject`
property, so every writer must choose a subject.
### Model page rows
| Kind | Identity | Lifecycle | Authority |
|---|---|---|---|
| Munition (light record) | Its `PendingShot`'s `ShotId`, as `MunitionId` in a `TargetRef`. | From fire until arrival or interception; never saved, as pending shots are not. | `FireControl` for transitions; `Munitions.Pose` for where it is. |
| Munition wear | `PendingShot.MunitionWear`. | Raised by PD hits and blasts; gone with its shot. | `FireControl.DamageMunition`. |
| Autofire setting | Per weapon group, `Entity.Autofire` (on, threshold). | Set by the player; saved in `EntityPack` key 21. | The player. |
| Autofire subject | Per weapon, runtime only. | Re-picked by `Autofire.Update` when the current one stops being worth firing. | `FireControl.ChooseSubject`. |
### Rationale
**Why no stepping.** The brief offered stepped homing. A stepped missile has its own
clock of arrival, which would make `FireControl`'s frozen `CommitTime` and `ArrivalTime`
a second answer to "when does it land", or force the commit to follow the missile. A
derived pose keeps one owner of timing and one of outcome and costs nothing per tick. It is
also exactly how belt rocks became simulation state without becoming entities.
**Why the missile homes on the live target but the roll is unchanged.** Homing is what
makes a missile read as a missile and what puts it in PD's arc near the target; the roll
already prices how far the target strays from the fire-time prediction. A target that
jinks therefore still beats a missile at the commit, while the round flies at it until
then.
**Why global durability and signature.** Per-weapon values would need catalog fields on
records the operator has ruled will be replaced by gear-built entities. Global settings
are one place to tune for the demo and nothing to migrate later.
**Why the player cannot designate a missile.** Presses that land on a swarm of rounds
instead of the ship behind them are a classic irritation, and the operator asked for
autofire precisely so that PD needs no micro.
**Cut order.** `missile-records` first: the records, the pose, the provider, interception
and the presenter, with no change to who can shoot them. It branches after
`controls-helm`, whose rename touches the line the presenter rewrite deletes.
`munition-shots` second: the subject widening, munition pricing, PD hits and flak, after
`controls-mount-aim` (it widens `Solution`'s subject). `autofire-threshold` third, after
`controls-hud` (it adds to `ControlsHud`). `ai-autofire` last, after
`controls-ai-bearing` (both change `Combat`).
**Left for later.** Full munition entities (follow-up `drones-munitions-substrate`); per-gear
durability and signature; decoys and soft kill; mines as records
(`bodies-entity-and-mines`); whether launchers draw rounds (`launcher-ammunition`); the
launcher flight fields `missile-records` leaves unread (follow-up `launcher-flight-fields`).
Whether the demo cast fields launchers for the player's PD to meet is content, not a
mechanism: the cuts add a `Point Defense` scenario for the operator check, and the demo
region's hostiles need launchers fitted where the cast allows.