diff --git a/Docs/Vault/00_Home/Home.md b/Docs/Vault/00_Home/Home.md index 9be0b5772..180d4ba4a 100644 --- a/Docs/Vault/00_Home/Home.md +++ b/Docs/Vault/00_Home/Home.md @@ -21,7 +21,7 @@ Multiplayer game on **Unity DOTS** (Entities) + **Netcode for Entities** (server - **Game Design** → [[Systems_Index]] — per-system design docs - **Roadmap** → **[[Roadmap_Lantern_Slice]]** (operative) · [[Lantern_Strip_Mothball_Inventory]] (salvage map) · [[Lantern_World_Model_Spike]] (★review-first) · [[Backlog]] / [[Path_to_Fun]] / [[Milestones]] / [[Iteration_2026-07_CoopHades]] (history). - **Sessions** → `07_Sessions/2026/` — dated work logs (latest: [[2026-07-13_Lantern_Phase0_Design_Pass_B]] · [[2026-07-13_Lantern_Phase0_Adoption_Pivot]]) -- **Decisions** → `07_Sessions/_Decisions/` — decision records DR-001 … DR-049 · [[DR-001_Netcode_Test_Harness]] · latest [[DR-049_Lantern_World_Model_Design]] (world-model, review-hardened) · [[DR-048_Lantern_Adoption_Full_Pivot]] (the pivot) +- **Decisions** → `07_Sessions/_Decisions/` — decision records DR-001 … DR-050 · [[DR-001_Netcode_Test_Harness]] · latest [[DR-050_Lantern_Phase1_Combat_Design]] (Phase 1 combat, review-hardened) · [[DR-049_Lantern_World_Model_Design]] · [[DR-048_Lantern_Adoption_Full_Pivot]] - **Meta** → [[Documentation_Protocol]] · [[Tags]] - **Templates** → [[Session_Log_Template]] · [[Decision_Record_Template]] diff --git a/Docs/Vault/06_Roadmap/Lantern_Phase1_Combat_Gym_Build_Spec.md b/Docs/Vault/06_Roadmap/Lantern_Phase1_Combat_Gym_Build_Spec.md index 033118279..e2fab42e2 100644 --- a/Docs/Vault/06_Roadmap/Lantern_Phase1_Combat_Gym_Build_Spec.md +++ b/Docs/Vault/06_Roadmap/Lantern_Phase1_Combat_Gym_Build_Spec.md @@ -8,6 +8,13 @@ permalink: gamevault/06-roadmap/lantern-phase1-combat-gym-build-spec # LANTERN — Phase 1: Combat Gym (design/build spec) +> [!warning] SUPERSEDED by [[Phase1_Combat_Gym_Build_Spec]] (review-hardened, 2026-07-14, [[DR-050_Lantern_Phase1_Combat_Design]]). +> The adversarial review (19 findings) reworked this pre-review design — notably the `SpawnId` bit budget +> (repack for sockets), the "KEEP as-is" client feel layer (it's a real migration + a new class `[GhostField]`), +> the predict-spawn boundary (Projectile-only), Blink (velocity-blink not teleport), and the Harpooner reel + +> player zone (new code, not salvage). **Read the Build Spec, not this**, for the implementation contract. +> Kept as the record of the pre-review design + the review's input. + > Phase 1 of [[Roadmap_Lantern_Slice]]: *the primary verb first — two frames that feel like Shape of Dreams. > No world, no lanterns — a gym.* This is a **design spec written from ground-truth code** (a combat/ability/ > boon salvage pass), NOT a build contract: the 4-socket kit and light-relevancy prototype are **netcode- diff --git a/Docs/Vault/06_Roadmap/Phase1_Combat_Gym_Build_Spec.md b/Docs/Vault/06_Roadmap/Phase1_Combat_Gym_Build_Spec.md new file mode 100644 index 000000000..d054902fa --- /dev/null +++ b/Docs/Vault/06_Roadmap/Phase1_Combat_Gym_Build_Spec.md @@ -0,0 +1,264 @@ +--- +title: LANTERN Phase 1 — Combat Gym Build Spec (review-hardened) +date: 2026-07-14 +tags: [roadmap, lantern, combat, phase-1, build-spec, netcode] +status: review-hardened +permalink: gamevault/06-roadmap/phase1-combat-gym-build-spec +--- + +> Pre-code gate output for Phase 1, adopted under [[DR-050_Lantern_Phase1_Combat_Design]]. Synthesized by the +> adversarial design review (`wf_c7575828-934`, 23 agents, 0 failures — 19 findings, 8 CONFIRMED + 11 PARTIAL, +> 0 refuted) over [[Lantern_Phase1_Combat_Gym_Build_Spec]] (the pre-review design) + the ground-truth combat +> code. It **supersedes that design spec** as the implementation contract. No Phase 1 code is written outside it. + +# LANTERN Phase 1 — Combat Gym Build Spec (review-hardened) + +> **Status: pre-code gate output.** This is the synthesized product of the adversarial design review (netcode / reuse / determinism lenses) over `Lantern_Phase1_Combat_Gym_Build_Spec.md`. It supersedes the pre-review design spec as the implementation contract. 19 findings (8 CONFIRMED, 11 PARTIAL, 0 REFUTED) are folded in below. No Phase 1 code is written outside this document. + +## Locked decisions + +**Operator forks (2026-07-14, `Lantern_Operator_Questions`) — unchanged, all validated sound by the review:** + +- **Socket input = 4 discrete ability buttons.** `PlayerInput` gains four independent `Socket0..3` `InputEvent`s (not a single `Fire` + `ActiveSocket` byte). The review confirms this is the correct feel choice and that it *forces* the `AbilityFireSystem` 7-type-query restructure — accepted as the single biggest code change of Phase 1. +- **Aim = manual; soft auto-target CUT.** Every socket fires along raw `PlayerInput.Aim` (`PlayerFacing.Direction`). Keep the reticle (`AimReticleSystem`) and keep the plumbing. Default per-socket `AutoTargetRange = 0`. The existing server-only gamepad assist cone (`AbilityFireSystem.cs:206-215`) survives as an *optional* per-Spark in-arc assist only. +- **Harpooner reel = pull the target to you.** The signature verb yanks the hit enemy toward the caster. The review CONFIRMED (RS-6) the salvaged pull flag does **not** implement this — see the reel contract below; it becomes new code, not a salvage. +- **5 Sparks** = decoy-wisp, hook/pull, vortex, blink, one zone. + +**Design points the review CONFIRMED sound (build on these as-is):** + +- The combat substrate ports almost whole. The predicted-spawn projectile pipeline, `AbilityDatabase`/`AbilityDefBlob`/`AbilityArchetype` blob dispatch, `MeleeComboSystem`, `DashSystem`, and the client feel systems are the correct chassis. +- **`DashSystem` is the correct Blink template** — and the correct template *because* it drives `CharacterControl.MoveVelocity` every predicted pass and never writes `LocalTransform` (`DashSystem.cs:77-93`). "DashSystem-style teleport" in the pre-review spec is a wording bug; Blink is a velocity blink, not a transform write (DB-5, NP-3). +- **The Cone archetype is the correct precedent for every non-projectile effect** — predict the cooldown on both worlds, gate the effect behind `if (isServer)` (`AbilityFireSystem.cs:143-172`). Aoe/Hitscan/decoy follow it, NOT the projectile predict-spawn path (NP-6, RS-4). +- **`AbilitySocket` modelled on `EquipmentSlot`** (cold, server-sole-writer, socketed in the hub) is correct. `SocketCooldown` is **not** an `EquipmentSlot`-shaped cold buffer — it is a hot owner-predicted cooldown; see contract (NP-4). +- **Mutations stay Phase 4.** Sparks are `AbilityDefBlob` rows; the `BoonEffects` substrate warps them later with no re-bake. Phase 1 ships sockets + Spark defs only. + +## Review verdict + +- **Netcode — pass-with-changes** (3 CONFIRMED / 3 PARTIAL). The pipeline salvages; two HIGH mandatory changes: the `SpawnId` bit budget must reserve socket bits (NP-1), and the "KEEP as-is" client feel layer actually depends on the reworked components (NP-5). +- **Reuse — pass-with-changes** (3 CONFIRMED / 4 PARTIAL). Three HIGH: the same `SpawnId` collision (RS-1), the Aoe/spawn archetype must not predict-spawn on the client (RS-4), and the "KEEP as-is" feel layer is a real migration (RS-2). Two salvages are overstated (Harpooner reel RS-6, player-owned geyser chassis RS-5) — both become new code. +- **Determinism — pass-with-changes** (2 CONFIRMED / 4 PARTIAL). HIGH: `SpawnId` budget (DB-3) and the single-ghost-type classifier (DB-4). The 7-arg cap has a proven in-file fix (DB-1); Blink and windup need rollback-safe idempotent resolution (DB-5, DB-6). + +No lens requires a full redesign; the substrate is sound and the socket grammar reworks on top of it. Every HIGH clusters on two coupled issues: **the `SpawnId` key** and **the "keep vs rework" mislabel of the client feel layer.** + +## The contract + +### 1. `SpawnId` repack — the coupled netcode core (NP-1, NP-2, RS-1, DB-3, DB-4) + +The current key is fully consumed and carries no socket discriminator: + +``` +// AbilityFireSystem.cs:232 (current — obsolete under 4 sockets) +uint spawnId = (owner << 16) | ((absoluteFireCount & 0x0FFF) << 4) | (s & 0xF); +// 16 owner 12 fireCount 4 fork +``` + +With 4 discrete sockets each carrying an **independent** `InputEvent.Count`, two sockets firing the same-prefab projectile on one tick at equal per-socket counts (near-guaranteed early: both `0→1`) produce **identical `SpawnId`s** → `ProjectileClassificationSystem` (matches purely on `incoming.SpawnId == ProjectileLookup[predictedEntity].SpawnId`, `:171`, first hit `RemoveAtSwapBack`, `:177`) mis-binds one predicted entity and orphans the other → visible mis-prediction snap. + +**LOCKED — repack to a 32-bit layout that reserves socket bits without touching the fork bits:** + +``` +owner(14) | socket(2) | fireCount(12) | fork(4) = 32 bits exact +``` + +- `socket(2)` = the firing socket index 0..3. +- `fireCount(12)` sourced from the **firing socket's own** `InputEvent.Count` (`applied.InternalInput.Socket{n}.Count`), NOT a shared `Fire.Count`. Wraps at 4096 shots — consistent with the existing 12-bit wrap philosophy. +- `fork(4)` **untouched** — Fork mutations are Phase 4 (`AbilityFireSystem.cs:222` caps forks at 8) and still need all 4 bits. Do **not** steal them for socket. +- `owner(14)` = 16383 range, ample for co-op. +- Update the two doc comments that still describe the obsolete `(ownerNetId << 16) | absoluteFireCount` form: `Projectile.cs:21-24` and `ProjectileClassificationSystem.cs:14-16`. + +A single monotonic per-player counter alone does **not** fix this — two shots on the same tick read the same counter value; per-shot socket bits are required regardless. + +**Projectile ghost-prefab model — LOCKED to the single shared prefab (option A):** every Projectile-archetype Spark (including hook/pull — its pull is a `ProjectileEffectState` flag, not a distinct prefab) shares the one `ProjectileSpawner.Prefab` ghost type, differing only by snapshotted `Projectile` stats + `ProjectileEffectState` seeds (`AbilityFireSystem.cs:244-259` already sets these at spawn). This keeps `ProjectileClassificationSystem`'s single resolved `m_GhostType` (`:82-98`) valid. **Bake-time guard:** warn/fail if any `AbilityPrefabElement.Prefab` for an `Archetype == Projectile` ability `!= ProjectileSpawner.Prefab`. Generalizing the classifier to N ghost types is an open risk (deferred; see §implementer-watch DB-4) — only if art requires distinct projectile meshes. + +**Regression test (L2):** two sockets fire the same-prefab projectile on one tick with equal per-socket counts → assert two **distinct** `SpawnId`s and that each predicted entity classifies to its own matching server ghost (assert adopted-entity identity, not ghost count — the failure mode is a cross-adopt swap, not a double-spawn). + +### 2. `AbilityFireSystem` socket restructure + the 7-type-cap fix (DB-1) + +The fire query is at exactly 7 type args today (`AbilityFireSystem.cs:108-113`). Adding the socket buffers as query args = 9 args → `SystemAPI.Query` source-gen (`*.g.cs`) fails per the CLAUDE.md 7-arg rule. The proven in-file fix: + +- **DROP `AbilityRef` and `AbilityCooldown` from the query** (both are replaced by the socket buffers). New query = **4 type args**: `RefRO`, `RefRO`, `RefRO`, `RefRO` + `.WithEntityAccess()`, filters `.WithAll().WithDisabled()`. +- Read inside the main-thread `foreach`, keyed by the player entity, via lookups (mirroring the existing `m_BoonEffectsLookup` `ComponentLookup` at `:131` and `GetBuffer>(entity)` at `:192`): + - `AbilitySocket` — read-only `BufferLookup`. + - `SocketCooldown` — read-**write** `BufferLookup` (per-socket `NextFireTick` write). + - per-socket `EffectiveAbilityStats` — read-only `BufferLookup` (see §7). + - `BoonEffects` — existing `ComponentLookup`. + - These are `DynamicBuffer`s → **`BufferLookup`, not `ComponentLookup`.** Declare/`.Update()` in `OnCreate`/`OnUpdate` like `m_BoonEffectsLookup`. +- **Loop sockets 0..3 per player** inside the `foreach`. Per-socket RW cooldown write stays main-thread (no parallel-buffer aliasing) → **no `IJobEntity` rewrite required.** +- **Hard rule:** never add `AbilitySocket`/`SocketCooldown`/`EffectiveAbilityStats`-buffer as `SystemAPI.Query` type args. +- Keep `Unity.Transforms` + `Unity.Physics` as DIRECT `ProjectM.Simulation` asmdef refs (already true) so `LocalTransform` source-gen in `*.g.cs` doesn't regress to CS0246/CS8377. +- Step 1 need not literally keep `AbilityRef` in the query — bake the new buffers alongside the untouched legacy query, then step 2 swaps the query wholesale. There is no intermediate state needing both. + +### 3. Per-socket data model + +- **`AbilitySocket`** — `[GhostComponent(SendToOwnerType.All)] [InternalBufferCapacity(4)] IBufferElementData`, one row per socket = `byte SparkId` (0 = empty). Server-sole-writer, socketed in the hub, **not predicted**. Cold — this is the genuine `EquipmentSlot` analog (`EquipSystem` writes it). +- **`SocketCooldown`** (NP-4) — a **HOT, owner-PREDICTED** cooldown. Its proven shape is the scalar `AbilityCooldown` `[GhostField]`, **NOT** the cold `EquipmentSlot` buffer — do not cite `EquipmentSlot` as its precedent. Preferred shape: a single `IComponentData` holding a fixed 4-slot cooldown that stays index-addressable — `unsafe fixed uint Next[4]` or a 4-`[GhostField]` fixed-array wrapper with an indexer — scoped `[GhostComponent(SendToOwnerType.All)]` (an improvement over today's `AbilityCooldown`, which has no `[GhostComponent]` and implicitly sends to all). Rationale: firing is by socket index (Burst can't index named scalar fields without a switch), and a scalar rollback path is lighter/more proven than a per-tick-mutating `[GhostField] IBufferElementData`. If a `[GhostField]` buffer is kept for indexing convenience, do **not** lean on `EquipmentSlot` — it is a predicted buffer, and the mandatory L3 rollback validation below applies. Route every stored tick through `TickUtil.NonZero`, compare via `NetworkTick.IsNewerThan` (never raw `uint`). +- **4 `Socket0..3` `InputEvent`s** on `PlayerInput` (`IInputComponentData`). Each carries its own `.Count`. Touches the command serializer → command-collection hash must match across peers (wire churn below). + +### 4. Manual-aim grammar + windup resolve tick (DB-6) + +Every socket fires along raw `PlayerFacing.Direction`. Add a static `WindupTicks` to the ability def so player abilities telegraph (reuse the enemy telegraph vocabulary client-side). + +`AbilityFireSystem` today fires on the same tick as the press (`Fire.IsSet`, `:116`) and holds no cast state. Deferring the spawn by `WindupTicks` **must** resolve on a tick that is a pure function of replicated data on both worlds. A non-replicated local `fireAtTick` (like `DashState`, which is not restored on rollback — `DashSystem.cs:78-80`) would resolve on different ticks under reprediction → the projectile spawns at different positions, and the `fireCount`-keyed `SpawnId` **cannot detect** the divergence (it silently classifies as a match then snaps). + +**LOCKED (stateless, minimal delta):** each predicted tick, resolve socket S if the historical command at `(serverTick − WindupTicks)` had socket S's event set — reuse the `GetDataAtTick` pattern (`:193` already does `GetDataAtTick(serverTick, …)`; change to `serverTick − WindupTicks`). Take the `fireCount` for the `SpawnId` from **that** historical command; spawn using the **current** tick's player position/aim; keep the existing `IsFirstTimeFullyPredictingTick` spawn gate (`:72`) and the per-socket `SocketCooldown` gate. Treat `GetDataAtTick == false` (history gap, e.g. `WindupTicks` older than the command buffer window) as no-fire. Alternative (fallback): store a per-socket resolve tick as a `[GhostField]` routed through `TickUtil.NonZero` so rollback restores it. **Never** store a plain non-replicated `fireAtTick`. **L2/L3 test:** fire with a windup that straddles a server correction (a mid-windup mispredicted knockback) and assert the predicted + server projectiles spawn on the same `serverTick` so `SpawnId → position` reconciliation is a no-op. + +### 5. Archetype-dispatch invariant (NP-6, RS-4) — the predict-spawn boundary + +**Predict-spawn + `SpawnId` classification is reserved STRICTLY for the Projectile archetype.** The current fall-through at `AbilityFireSystem.cs:174-175` does **not** advance a cooldown — the new branches must **add** the both-worlds cooldown advance, not merely relax the `continue`. + +| Archetype | Cooldown | Effect / spawn | +|---|---|---| +| **Projectile** (hook/pull) | predict both worlds | predict-spawn ghost on both worlds; `SpawnId`-classified. The ONLY predict-spawned archetype. | +| **Cone** (Bathynaut secondary) | predict both worlds (`:170-171`) | effect server-only (`if (isServer)`, `:143-172`). Existing precedent — verbatim template. | +| **Aoe/spawn** (decoy-wisp) | advance per-socket `SocketCooldown` via `TickUtil.NonZero(serverTick + cd)` on **both** worlds (outside `isServer`) | `ecb.Instantiate(...)` **only inside `if (isServer)`**. The client must NEVER instantiate the ghost. | +| **Aoe/zone** (vortex, zone) | same — both worlds | same — server-only spawn/effect. | +| **Hitscan** | predict both worlds | **no ghost on either world**. Swept-ray damage server-only; client-only VFX beam (presentation layer). | +| **movement** (Blink) | own path (§6) | NOT dispatched by `AbilityFireSystem` — falls through; `BlinkSystem` handles it. | + +The decoy/zone/vortex entity = an **interpolated ownerless ghost** (`GhostAuthoring`: interpolated, no `GhostOwner` as a predicting owner, stock `LocalTransform` replication), instantiated server-only, with **no `SpawnId` and no classifier** — deliberately nothing to reconcile (mirrors enemy/pickup + the server-only geyser/barrel chassis). Predicting these gains no responsiveness (their trajectory is not input-deterministic like a projectile's) and would risk unclassified-predicted-spawn flicker/leak. "player-owned zone" means **server-attributed-to-owner** (a source `NetworkId` field on the effect component for aggro/damage credit), NOT an owner-predicted ghost. **Build-check (L2/L3):** assert the client world never instantiates a decoy/zone/hitscan prefab (the Aoe/spawn `Instantiate` call is reachable only when `state.WorldUnmanaged.IsServer()`). + +### 6. Blink — the second dash (NP-3, RS-3, DB-5) + +Blink is a `DashSystem` clone, **not** a dispatch archetype inside `AbilityFireSystem` and **not** a `LocalTransform` write. + +- **Own system:** a `[BurstCompile]` predicted `ISystem` in `PredictedSimulationSystemGroup`, `[UpdateAfter(PlayerControlSystem)]` and `[UpdateAfter(DashSystem)]`, sibling of `DashSystem`. **No `IsFirstTimeFullyPredictingTick` guard.** +- **Own state:** non-replicated `BlinkState` (own `StartTick` + half-open window). Do **not** reuse `DashState` (a same-tick dash+blink would clobber the single window). +- **START** = idempotent pure function of the replicated Blink-socket `InputEvent` + tick (mirror `DashSystem.cs:57-75`). +- **Drive `CharacterControl.MoveVelocity`** (large speed + high `GroundedMovementSharpness`), re-applied on **every** predicted pass, lower-bounded on the half-open `[StartTick, …)` window (mirror `DashSystem.cs:82-93`). **Never write `LocalTransform`** — a raw write bypasses the CC collide-and-slide processor that owns `LocalTransform` integration (blink through subscene-only Environment colliders → the no-player-pathfinding soft-lock class), fights per-tick integration, and resets `Scale=1` (`LocalTransform.FromPosition`). +- **Cover distance with an N-tick window**, not one huge single-tick velocity, so the CC validates each collide-and-slide step (no wall tunnel). +- **MoveVelocity precedence:** `BlinkSystem` early-outs its override while a `DashState` window is active this tick (dash wins), evaluated every pass so rollback re-simulation converges. (Equivalently, blink-start refuses while a dash window is open and vice-versa.) +- **Cooldown:** `BlinkSystem` is the **sole writer of its socket's `SocketCooldown` row** (`AbilityFireSystem` skips movement-archetype sockets via fall-through, so no row contention) — keeps the HUD's per-socket bar uniform. This is a disjoint-row shared-buffer write; order the two systems and treat it as a watched dependency (§implementer-watch). Keep "Blink occupies an `AbilitySocket` row" purely as HUD/socketing data; its firing is a separate predicted code path. +- **Out of scope unless requested:** a *true* instant/wall-crossing MOBA-Flash reposition. If ever wanted, it must set the character body position via the **CC API** (not a raw transform write) **and** sweep the segment against `CollisionWorld` to clamp short of geometry (reuse the swept-hit pattern). Phase 1 Blink is the dash clone. + +### 7. Per-socket effective stats (RS-7, DB-2) + +`StatRecomputeSystem` today folds ONE `AbilityRef` into ONE `EffectiveAbilityStats` per player every predicted tick (`StatRecomputeSystem.cs:37-53`), and `AbilityFireSystem` reads that single struct for Damage/Range/Cooldown/ProjectileSpeed/AutoTarget. Four sockets each holding a different Spark cannot share one struct. + +**LOCKED — option (a): a 4-wide `EffectiveAbilityStats` `[InternalBufferCapacity(4)]` buffer**, one row per socket, folded every tick in `StatRecomputeSystem` from each socket's `SparkId` blob base + the shared per-player `StatModifier` band. Chosen over the fold-inline option (b) because the HUD/anim/feedback readers all need per-socket data anyway. + +- **Scope:** only `EffectiveAbilityStats` goes per-socket. `EffectiveCharacterStats` (MoveSpeed/MaxHealth/TurnRate) is per-player and stays single. +- **Only tunable scalars** need per-socket resolution: Damage, ProjectileSpeed, Range, AutoTargetRange, AutoTargetConeRadians, CooldownTicks. Archetype + projectile-prefab lookup already dispatch off the blob per `SparkId` (`AbilityFireSystem.cs:137-138, 179-186`), NOT off `EffectiveAbilityStats`. +- **Band semantics (LOCKED):** the single per-player `StatModifier` band (class traits + boons) applies **uniformly** to all 4 sockets — `StatMath.Apply` folds it against each socket's blob base. Per-socket/per-Spark warping is Phase 4 (`BoonEffects`). +- **Keep the fold unconditional every tick** — no dirty flag / change filter (`StatRecomputeSystem.cs:14-18`: `Effective*` is not in the snapshot, so a filter goes stale across reprediction). +- `StatRecomputeSystem` stays at **5 query args** (`:37-40`); the 7-arg-cap work is scoped to `AbilityFireSystem` only. +- Re-point the three presentation readers at per-socket data: `HudSystem.cs:543`, `CombatFeedbackSystem.cs:442`, `PlayerAnimationDriveSystem.cs:161/216`. + +### 8. The client feel layer + replicated class signal (NP-5, RS-2) + +**This is a real migration, not "KEEP as-is."** Four client systems hard-depend on the single `AbilityRef`/`AbilityCooldown` being reworked, and one loses its only class signal. + +- **New replicated class source (REQUIRED):** add a `[GhostField] byte FrameId` (`SendToOwner`) on the player, **or** promote the server-only `PlayerClass` (`MetaComponents.cs:67-75`, currently NOT replicated) to a `SendToOwner` `[GhostField]`. Required because in the socket model no single ability id maps 1:1 to a class. Re-point `ClassPrepPortalHudSystem.cs:88` and `MetaShopHudSystem.cs:81` from `ClassTraits.ClassForAbility(AbilityRef.Id)` to the new field. +- **`CombatFeedbackSystem`** (move OUT of KEEP): the muzzle-flash edge (`:333-343`, `AbilityCooldown.NextFireTick`) and cone-cue edge (`:434`) must iterate the 4 `SocketCooldown` rows (per-socket `NextFireTick` edge with a last-tick-per-socket cache). The `localIsCone` gate (`:321-327`) and cone `Archetype` gate (`:439`) must resolve from the **fired socket's `SparkId`** via the `AbilityDatabase` blob, not `AbilityRef.Id`. +- **`PlayerAnimationDriveSystem`** (move OUT of KEEP): `FireActive` (`:131-138`, read at `:170-171`/`:231-232`) reads `AbilityCooldown.NextFireTick` + `EffectiveAbilityStats.CooldownTicks` → must become per-socket (which socket fired + its cooldown row + its per-socket effective `CooldownTicks`). `IsCone` (`:182-188`/`:235-241`) from `AbilityRef` → from the fired socket's `SparkId` blob. Note the `Execute` in-args are `IJobEntity` params (`:153-164`), not `SystemAPI.Query` args, so the 7-arg cap does not bind — but `AbilityCooldown`→`SocketCooldown` and `AbilityRef`→`AbilitySocket` become buffer params. +- **KEEP genuinely as-is:** `EnemyDangerTelegraphSystem`, `AimReticleSystem`, `DynamicLightSystem`, the ambient/underwater stack — none reads `AbilityRef`/`AbilityCooldown`. + +### 9. Harpooner reel — new homing state (RS-6) + +The pre-review "reuses the `ProjectileEffectState` pull flag" is a scaffold, not the finished reel. + +- **Gym must spawn LIVE Husk-type enemies** (`EnemyTag` + baked `KnockbackState` per `EnemyAuthoring.cs:53` + under `EnemyAISystem` control) as the reel/knockback targets. An inert dummy carries neither `KnockbackState` nor a mover → pull/knockback silently no-ops (`ProjectileDamageSystem.cs:169` guard; `EnemyAISystem` is the sole applier, `:129-142`). Plain damage + client hit-flash **do** work on any `Health`+`HitRadius` dummy (damage is unconditional at `ProjectileDamageSystem.cs:161`; flash edge-detects replicated `Health`) — dummies are fine for damage-feel, not reel-feel. +- **Do NOT ship the pull flag as the signature reel.** It stamps `KnockbackState.Dir = -Projectile.Direction` (back toward the firing *origin*, not the caster's current position) at fixed `Tuning.KnockbackSpeed` for a fixed window (`ProjectileDamageSystem.cs:169-178`), and `EnemyAISystem.cs:135` drives a constant-velocity nudge along that **frozen** `Dir` — it neither homes on the caster nor lands the target at the caster. +- **Implement the reel as a NEW server-only homing state** — a sibling of `LungeState`/`KnockbackState` so it doesn't fight the seek and preserves `EnemyAISystem` as the sole mover. Store a hooked-target→caster link; each server tick recompute `Dir` toward the caster's **current** `LocalTransform.Position`; drive until within melee/leash, then release. Route scheduled ticks through `TickUtil.NonZero`; gate movement with `IsNewerThan`. +- **Bosses are knockback-immune (A4)** — the gym's feelability of the reel rests entirely on non-boss Husks. + +### 10. Player zone / vortex — new server-only periodic-AoE (RS-5) + +"Reuse the geyser/hazard AoE chassis, player-owned" means borrow the pattern, not the systems. `GeyserEruptSystem`/`HazardExplosionSystem` are environment hazards: they damage living **players** too (`GeyserEruptSystem.cs:69-98`), stamp `DamageEvent.SourceNetworkId = -1` (`:79,:95` — deliberately unattributed), and gate players on `RegionTag == Expedition` (`:73`). + +**A NEW server-only periodic-AoE system that:** (1) borrows ONLY the scheduling/sweep skeleton (`= now + period`; the **inverted** invalid-tick guard so a born-0 `[GhostField]` tick never storm-fires, `GeyserEruptSystem.cs:54-57`; `NativeList` gather + radius-sq sweep); (2) stamps `DamageEvent.SourceNetworkId = casterId` (**not -1**) so kills credit the caster and feed `KillRewardSystem`'s Siphon heal / Frenzy row / whiff-punish; (3) **drops the player-damage loop entirely** (no friendly fire) — keep only the `EnemyTag` + `Health > 0` loop, which is already region-agnostic (`GeyserEruptSystem.cs:84-98` has no region check), so **no `RegionId` gate** for a no-world gym. Net reuse: the ~15-line scheduling/sweep skeleton; attribution + enemy-only filter are new code. + +### 11. Underwater feel pass (client-only, zero netcode) + +Unchanged from the pre-review spec — reuse `AmbientMotionSystem`/`AmbientLifeSystem`/`WorldAtmosphereSystem`: buoyant knockback decay tuning, slow debris drift, murk fog, procedural `AudioClip.Create` hydrophone bed. No sim change. + +## Salvage & touch map + +**KEEP as-is (verified — no `AbilityRef`/`AbilityCooldown` dependency):** +- `EnemyDangerTelegraphSystem`, `AimReticleSystem`, `DynamicLightSystem`, `DashSystem`, `MeleeComboSystem`, the ambient/underwater stack. +- `StatMath.Apply` / the `StatModifier` band mechanism (band stays single-per-player; only the *fold target* goes per-socket). + +**KEEP the pipeline, REWORK the read/grammar:** +- `AbilityFireSystem` — query restructure (§2), archetype dispatch (§5), `SpawnId` repack (§1), windup resolve (§4). +- `ProjectileClassificationSystem` — `SpawnId` doc update (`:14-16`); single `m_GhostType` stays valid under the shared-prefab lock; generalization deferred (open risk DB-4). +- `StatRecomputeSystem` → 4-wide `EffectiveAbilityStats` fold (§7); stays at 5 query args. +- `Projectile` — `SpawnId` doc comment update (`:21-24`). + +**REWORK — per-socket data model:** +- `AbilityRef{[GhostField] byte Id}` → `AbilitySocket` 4-wide buffer (`EquipmentSlot`-modelled, cold). +- `AbilityCooldown` → `SocketCooldown` 4-wide owner-predicted cooldown (scalar-shaped, NOT `EquipmentSlot`-shaped). +- `PlayerInput.Fire` → `Socket0..3` `InputEvent`s. +- `EffectiveAbilityStats` single → 4-wide buffer. +- `EquipSystem` — writes `AbilitySocket` (the socketing action) instead of / alongside `AbilityRef`. + +**REWORK — per-socket feedback + replicated class signal (new build step 2.5):** +- `CombatFeedbackSystem` (muzzle `:335`, cone `:434`, archetype gates `:325`/`:439`) → per-socket. +- `PlayerAnimationDriveSystem` (`FireActive` `:170-171`/`:231-232`, `IsCone` `:186`/`:239-241`) → per-socket + fired-socket blob. +- `ClassPrepPortalHudSystem:88`, `MetaShopHudSystem:81` → read the new `FrameId`/`PlayerClass` `[GhostField]` instead of `ClassForAbility(AbilityRef.Id)`. +- `HudSystem.cs:543` (ability-bar `EffectiveAbilityStats` read) → per-socket. + +**NEW code (not a salvage):** +- `BlinkSystem` (§6). +- Harpooner reel homing state + its driver in `EnemyAISystem` (§9). +- Player zone/vortex server-only periodic-AoE (§10). +- Aoe/Hitscan archetype branches in `AbilityFireSystem` (§5) — implement the declared-but-unhandled archetypes. +- Decoy-wisp server-spawned interpolated ghost + a decoy aggro tag (reuse enemy target-selection). + +**Prefabs/data:** 5 `AbilityDefBlob` rows; the decoy/zone/vortex interpolated ghost prefabs (greybox via the "duplicate a configured ownerless ghost" recipe); the shared projectile prefab stays single. + +## Build order + +Each step ends **"compile + `read_console` clean"** (L1: source-gen + Burst entry points verified — a Burst ICE corrupts the cache, so a clean compile + green L2 confirms the code). Wire/bake churn classified per step. + +1. **Socket data model, no behaviour.** Add `AbilitySocket` + `SocketCooldown` (+ `BlinkCooldown`) baked 4-wide; add the class-signal `[GhostField]` (`FrameId` or promoted `PlayerClass`). Leave the legacy `AbilityRef`/`AbilityCooldown` query untouched. **Wire/bake: player-ghost re-bake (new `[GhostField]`s).** L1 only. + - **1b. Per-socket effective-stat resolution.** 4-wide `EffectiveAbilityStats` buffer + `StatRecomputeSystem` per-socket fold (§7). Uniform band. **Bake: none (Effective\* not in snapshot).** L1 + an L2 fold test. +2. **`AbilityFireSystem` socket restructure.** Query drop to 4 args + `BufferLookup`s (§2); `SpawnId` repack (§1); per-socket cooldown; Cone + Projectile paths first. **Wire/bake: command-serializer hash change (`Socket0..3` `InputEvent`s) + player-ghost re-bake — must match across peers; re-mean bytes where possible.** L1 + the `SpawnId` uniqueness L2 test. + - **2.5. Client feel + class signal migration (§8).** `CombatFeedbackSystem`, `PlayerAnimationDriveSystem`, `ClassPrepPortalHudSystem`, `MetaShopHudSystem`, `HudSystem` → per-socket + new class field. **Bake: consumes the step-1 class `[GhostField]` re-bake; no new bake.** L1 + L3 visual smoke. +3. **Aoe / Hitscan archetype branches** (§5) — both-worlds cooldown advance + server-only spawn/effect; the decoy/zone/vortex interpolated ghosts. **Bake: new ghost prefabs (additive).** L1 + the "client never instantiates" L2/L3 check. +4. **The 5 Spark defs + prefabs** (greybox) + **Blink** (`BlinkSystem`, §6) + **Harpooner reel** homing state (§9) + **player zone** periodic-AoE (§10). **Bake: `AbilityDefBlob` rows (additive config); reel/zone are server-only, no ghost re-bake beyond step 3.** L2 (reel homing, zone attribution) + L3 (feel). +5. **Two frames** = class-chassis reskin + per-frame socket defaults + movement-feel knobs. **Bake: config only.** +6. **Manual-aim grammar:** `WindupTicks` telegraphs (§4); auto-target off by default. **Bake: config only.** L2 windup resolve-tick determinism test. +7. **Underwater feel pass** (client-only, §11). No sim/bake change. +8. **Light-relevancy prototype** — ONLY after the world-model Build Spec lands; validated by the gamma test. Shares that review; not coded here. + +**Wire/bake churn summary:** steps 1–2 + 2.5 re-bake the player ghost (socket/cooldown/class `[GhostField]`s) and change the command-collection hash (`Socket0..3` `InputEvent`s) — the RPC/command collection hash must match across all peers. Step 3 adds interpolated-ghost prefabs (additive). Everything else is additive config + client-only. + +**L1–L3 verification approach:** +- **L1** — `refresh_unity scope=scripts` then `read_console` after every code-touch step; Burst entry points verified (no "not a known Burst entry point"); a focused editor for Burst-affecting edits. +- **L2** — plain-Entities EditMode tests (`ProjectM.Tests.EditMode`): `SpawnId` cross-socket uniqueness (§1), per-socket cooldown gate, per-socket stat fold, windup resolve-tick determinism (§4), "client world never instantiates decoy/zone/hitscan" (§5), reel homing convergence. +- **L3** — focused-editor two-player MPPM Play: fire all 4 sockets same tick (no flicker/desync), **induced rollback / forced packet loss** to validate `SocketCooldown` predicted-buffer restore (no double-fire/desync — mandatory before relying on predicted-buffer restore, NP-4), reel against LIVE Husks, dash+blink same-tick precedence, windup-across-correction no-pop. + +## Open risks (§implementer-watch) + +- **NP-2 / DB-4 — classifier hardwired to one ghost type.** `ProjectileClassificationSystem.cs:82-98` resolves a single `m_GhostType`; `:154` filters on it; `:167`/`:171` match on it. The shared-prefab lock (§1) keeps this valid for Phase 1. **If** distinct projectile meshes are ever needed: build a membership set of ghost-type indices from `AbilityPrefabElement`, change the OUTER filter (`:154`) to a set-membership test, but change the INNER scan (`:167`) to `!= newSpawn.GhostType` (the *specific* incoming type, not the set) — else two projectile types cross-match on a colliding `SpawnId`. **Watch:** `ProjectileClassificationSystem.cs:154, 167, 171`. +- **NP-4 — `SocketCooldown` predicted-buffer restore.** If a `[GhostField] IBufferElementData` buffer shape is chosen over the scalar fixed-array, its per-tick rollback restore is unproven here. **Gate:** the L3 induced-rollback test is mandatory before relying on it. **Watch:** the `SocketCooldown` definition + `AbilityFireSystem` cooldown write. +- **NP-3 / RS-3 — dash+blink MoveVelocity precedence.** Two systems write `CharacterControl.MoveVelocity` in the predicted group; precedence must be evaluated **every** pass so rollback converges. **Watch:** `BlinkSystem` `[UpdateAfter(DashSystem)]` + the dash-window early-out; `DashSystem.cs:88-93`. +- **RS-3 — Blink `SocketCooldown` shared-row write.** `BlinkSystem` writes its socket's row while `AbilityFireSystem` writes the others — disjoint rows, but a shared-buffer write dependency across two systems; order them and verify no aliasing throw. **Watch:** both systems' `SocketCooldown` `BufferLookup` RW. +- **DB-6 — windup resolve-tick divergence is undetectable by `SpawnId`.** A `fireCount`-keyed `SpawnId` classifies a client/server windup-tick disagreement as a "match" then snaps. **Gate:** the reprediction test (§4). **Watch:** `AbilityFireSystem.cs:193` (`GetDataAtTick` offset) + `:232` (`SpawnId` pack). +- **RS-5 — player zone attribution/friendly-fire regression.** Copying `GeyserEruptSystem`/`HazardExplosionSystem` verbatim would friendly-fire allies (`GeyserEruptSystem.cs:69-82`), fail to credit the caster (`SourceNetworkId = -1`, `:79/:95`), and region-lock to Expedition (`:73`). **Watch:** the new system must stamp `casterId`, drop the player loop, add no region gate. +- **RS-6 — reel unfeelable against inert dummies.** The pull no-ops without baked `KnockbackState` + an `EnemyAISystem` mover. **Watch:** gym spawns LIVE Husks (`EnemyAuthoring.cs:53`); `ProjectileDamageSystem.cs:169` guard; `EnemyAISystem.cs:135` frozen-`Dir` nudge is *not* a homing reel. +- **7-arg cap regression on new systems.** `BlinkSystem` (needs socket buffers + CC + input) and the reworked `PlayerAnimationDriveSystem` can approach the cap. **Watch:** read socket buffers via `BufferLookup`, never as `SystemAPI.Query` type args. +- **System-ordering cycle is invisible to EditMode tests.** Adding `[UpdateAfter]` edges (BlinkSystem, reel) — re-audit existing `[Update*]` attributes and **Play-validate** (a cycle only throws at world creation). + +## Findings ledger + +| ID | Lens | Sev | Verdict | Resolution | +|---|---|---|---|---| +| NP-1 | netcode | HIGH | CONFIRMED | Repack `SpawnId` to `owner14\|socket2\|fireCount12\|fork4`; source count from firing socket; don't touch fork bits (§1). | +| NP-2 | netcode | HIGH | PARTIAL | Lock single shared projectile prefab + bake-time guard; classifier generalization deferred (§1, open risk). | +| NP-3 | netcode | MED | PARTIAL | Blink = velocity-driven `DashState` chassis, own `BlinkState`/cooldown, dash-wins precedence (§6). | +| NP-4 | netcode | MED | PARTIAL | `SocketCooldown` = hot owner-predicted scalar-shaped cooldown (NOT `EquipmentSlot`), `SendToOwner`; L3 rollback gate (§3, open risk). | +| NP-5 | netcode | HIGH | CONFIRMED | Client feel layer moves to REWORK; new replicated class signal (§8, step 2.5). | +| NP-6 | netcode | MED | CONFIRMED | Predict-spawn is Projectile-only; Aoe/Hitscan/decoy = server-only spawn, cooldown both worlds (§5). | +| RS-1 | reuse | HIGH | PARTIAL | Same as NP-1; socket bits mandatory; regression test (§1). | +| RS-2 | reuse | MED | CONFIRMED | Same as NP-5; re-file 4 client systems, add class `[GhostField]`, per-socket edges (§8). | +| RS-3 | reuse | HIGH | PARTIAL | Blink is a separate predicted system, not an `AbilityFireSystem` dispatch; sole writer of its cooldown row (§6, open risk). | +| RS-4 | reuse | HIGH | CONFIRMED | Aoe/spawn advances cooldown both worlds but `Instantiate` server-only; new branch must add the cooldown advance (§5). | +| RS-5 | reuse | MED | PARTIAL | Player zone = new server-only AoE; borrow scheduling only, stamp casterId, enemy-only, no region gate (§10, open risk). | +| RS-6 | reuse | MED | CONFIRMED | Reel = new homing state on LIVE Husks; pull flag is scaffold; bosses immune (§9, open risk). | +| RS-7 | reuse | MED | PARTIAL | 4-wide `EffectiveAbilityStats` fold, uniform band, `StatRecomputeSystem` stays 5 args; re-point 3 readers (§7). | +| DB-1 | determinism | MED | PARTIAL | Drop `AbilityRef`+`AbilityCooldown` from query → 4 args + `BufferLookup`s; no `IJobEntity` rewrite (§2). | +| DB-2 | determinism | MED | PARTIAL | Same as RS-7; per-socket `EffectiveAbilityStats` is explicit REWORK, bounded inside the socket restructure (§7). | +| DB-3 | determinism | HIGH | PARTIAL | Same as NP-1; assert adopted-entity identity (swap, not double-spawn) in the test (§1). | +| DB-4 | determinism | MED | CONFIRMED | Classifier single-type valid under shared-prefab lock; if generalized, inner scan matches `newSpawn.GhostType` (§1, open risk). | +| DB-5 | determinism | MED | PARTIAL | Fix "teleport" wording → velocity blink; forbid raw `LocalTransform` write; true Flash out of scope (§6). | +| DB-6 | determinism | MED | CONFIRMED | Windup resolve tick = pure fn of `GetDataAtTick(serverTick − WindupTicks)`; reprediction test (§4, open risk). | diff --git a/Docs/Vault/06_Roadmap/Roadmap_Lantern_Slice.md b/Docs/Vault/06_Roadmap/Roadmap_Lantern_Slice.md index fd14920d6..95bc36dce 100644 --- a/Docs/Vault/06_Roadmap/Roadmap_Lantern_Slice.md +++ b/Docs/Vault/06_Roadmap/Roadmap_Lantern_Slice.md @@ -189,7 +189,7 @@ The deeper technical pass (next sessions) takes each phase in turn: context7-ver - [[Identity_Lantern]] — the design this implements (scope ladder = this roadmap's contract; readability law = A-track law) - **Phase 0 artifacts:** [[DR-048_Lantern_Adoption_Full_Pivot]] · [[Lantern_Strip_Mothball_Inventory]] · [[World_Model_Build_Spec]] + [[DR-049_Lantern_World_Model_Design]] (+ [[Lantern_World_Model_Spike]] / [[Lantern_World_Model_Design_Proposal]]) · [[Art_Direction_Lantern]] + [[Lantern_Art_Pipeline_Recipes]] -- **Phase design specs (all phases covered):** [[Lantern_Phase1_Combat_Gym_Build_Spec]] · [[Lantern_Phase4_Mutations_Design]] · [[Lantern_Phase5_Death_Recovery_Design]] · [[Lantern_Phase6_7_Finale_Station_Design]] · [[Lantern_Currency_ReMean]] · [[Lantern_Tone_Bible]] (Phase 7) +- **Phase design specs (all phases covered):** [[Phase1_Combat_Gym_Build_Spec]] (review-hardened, [[DR-050_Lantern_Phase1_Combat_Design]]) · [[Lantern_Phase4_Mutations_Design]] · [[Lantern_Phase5_Death_Recovery_Design]] · [[Lantern_Phase6_7_Finale_Station_Design]] · [[Lantern_Currency_ReMean]] · [[Lantern_Tone_Bible]] (Phase 7) - **Running:** [[Lantern_Operator_Questions]] — decisions parked for the operator - [[Theme_Direction_Candidates]] — research underneath the direction - [[Synty_Asset_Inventory]] — placeholder quarry during the transition diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-050_Lantern_Phase1_Combat_Design.md b/Docs/Vault/07_Sessions/_Decisions/DR-050_Lantern_Phase1_Combat_Design.md new file mode 100644 index 000000000..be58d47c8 --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-050_Lantern_Phase1_Combat_Design.md @@ -0,0 +1,72 @@ +--- +id: DR-050 +title: LANTERN Phase 1 combat gym — 4-socket manual-aim kit design (review-hardened) +status: accepted +date: 2026-07-14 +tags: [decision, lantern, combat, phase-1, netcode, review] +permalink: gamevault/07-sessions/decisions/dr-050-lantern-phase1-combat-design +--- + +# DR-050 — LANTERN Phase 1 combat gym design (review-hardened) + +## Context + +Phase 1 of [[Roadmap_Lantern_Slice]] is the primary verb — two suit-frames + a 4-socket **manual-aim +skillshot kit** + the first 5 Sparks, in a gym (no world). Operator selected it as the first build target +(2026-07-14) with forks locked in [[Lantern_Operator_Questions]]: 4 discrete ability buttons · manual aim +(soft auto-target cut) · Harpooner reel = pull-target-to-you · the roadmap 5 Sparks. It's netcode-touching +(new player ghost fields + input surface), so per the standing ★-review authorization it went through the +adversarial pre-code design review before any code. + +## Decision + +**Adopt [[Phase1_Combat_Gym_Build_Spec]] (review-hardened) as the implementation contract.** No Phase 1 code +is written outside it. + +The review (`wf_c7575828-934`; 23 agents, **0 failures** — findings verified real) ran netcode/reuse/ +determinism lenses with refute-first critics: **19 findings, 8 CONFIRMED + 11 PARTIAL, 0 refuted.** Verdict: +**pass-with-changes on all three lenses — no redesign; the substrate is sound and the socket grammar reworks +on top.** The HIGHs cluster on two coupled issues, both now resolved in the spec: + +1. **The `SpawnId` key is exhausted (NP-1/RS-1/DB-3).** It packs `owner(16)|fireCount(12)|fork(4)` = all 32 + bits; 4 sockets carry no discriminator → same-tick multi-socket projectiles collide → mis-classify/desync. + **LOCKED: repack to `owner(14)|socket(2)|fireCount(12)|fork(4)`**, count sourced from the firing socket, + fork bits untouched (Phase-4 mutations still need them). Projectile Sparks stay one shared ghost prefab + (bake-time guard); classifier generalization deferred. +2. **The "KEEP as-is" client feel layer isn't (NP-5/RS-2).** `CombatFeedbackSystem`, `PlayerAnimationDriveSystem`, + `ClassPrepPortalHudSystem`, `MetaShopHudSystem`, `HudSystem` all resolve archetype/cooldown/class off the + single `AbilityRef`/`AbilityCooldown` being reworked → a real migration, + a **new replicated class signal** + (`FrameId`/promoted `PlayerClass` `[GhostField]`) since no ability id maps 1:1 to a class under sockets. + +Other confirmed reworks: predict-spawn is **Projectile-only** — Aoe/Hitscan/decoy follow the **Cone precedent** +(server-only spawn/effect, cooldown both worlds) never predict-spawn (NP-6/RS-4); **Blink is a velocity-blink** +in its own `BlinkSystem` (never a `LocalTransform` write, never inside the predict-spawn gate — DB-5/NP-3/RS-3); +`SocketCooldown` is a **hot owner-predicted scalar-shaped** cooldown (NOT the cold `EquipmentSlot` — NP-4); +`EffectiveAbilityStats` goes **4-wide** (RS-7/DB-2); the 7-arg cap is fixed by **dropping `AbilityRef`/ +`AbilityCooldown` from the query + `BufferLookup`s** (DB-1); windups need a **rollback-safe resolve tick** via +`GetDataAtTick(serverTick − WindupTicks)` (DB-6); the **Harpooner reel** (RS-6) and **player zone** (RS-5) are +**new server-only code**, not salvage (the pull flag doesn't home on the caster; the geyser chassis friendly- +fires + doesn't credit the caster). + +## Consequences + +- **Build order (spec §Build order):** 1 socket data model (+1b per-socket stats) → 2 `AbilityFireSystem` + restructure + `SpawnId` repack (+2.5 client-feel/class migration) → 3 Aoe/Hitscan branches + decoy/zone + ghosts → 4 Spark defs + Blink + reel + zone → 5 two frames → 6 manual-aim windups → 7 underwater feel → + 8 light-relevancy (deferred to the world-model review). Steps 1–2/2.5 **re-bake the player ghost + change + the command-collection hash** (must match across peers). Everything else is additive. +- **Implementation proceeds incrementally** — each step ends compile+`read_console` clean + its L2 test; the + wire-churn steps (1–2) get an L3 two-player Play smoke + an induced-rollback test for the predicted + `SocketCooldown` (NP-4 gate) before relying on it. +- **Open risks tracked** (spec §implementer-watch): classifier single-type (shared-prefab lock holds), + `SocketCooldown` predicted-buffer restore, dash+blink MoveVelocity precedence, windup resolve-tick + divergence, zone attribution, reel-needs-live-Husks, 7-arg regressions, sort-cycle (Play-only). +- **Revisit when:** implementation contradicts the spec (mini-review), or the 15-min two-player gym fun-gate + fails. + +## Related + +- [[Phase1_Combat_Gym_Build_Spec]] — the durable contract this accepts (19 findings folded) +- [[Lantern_Phase1_Combat_Gym_Build_Spec]] — the pre-review design (superseded) +- [[DR-048_Lantern_Adoption_Full_Pivot]] · [[DR-049_Lantern_World_Model_Design]] · [[Roadmap_Lantern_Slice]] Phase 1 +- [[Lantern_Operator_Questions]] — the locked forks · [[validate-netcode-design-before-coding]] — the ritual