Adversarial pre-code review (wf_c7575828-934; 23 agents, 0 failures) over the Phase 1
combat-gym design raised 19 findings (8 CONFIRMED + 11 PARTIAL, 0 refuted) and caught
design-breakers before code:
- SpawnId bit budget exhausted (owner16|fireCount12|fork4) -> repack owner14|socket2|
fireCount12|fork4 so 4 sockets don't collide/desync.
- "KEEP as-is" client feel layer is a real migration (CombatFeedback/PlayerAnimationDrive/
HUD read the reworked AbilityRef/AbilityCooldown) + needs a new replicated class signal.
- predict-spawn is Projectile-only (Aoe/Hitscan/decoy = Cone-precedent server-only spawn);
Blink = velocity-blink in its own system (not a transform teleport); SocketCooldown hot
not cold; EffectiveAbilityStats 4-wide; windup needs a rollback-safe resolve tick;
Harpooner reel + player zone = new server-only code, not salvage.
Verdict: pass-with-changes on all 3 lenses, no redesign. Phase1_Combat_Gym_Build_Spec =
the implementation contract (8-step build order, wire/bake churn, open risks, ledger).
DR-050 records it; pre-review design -> superseded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
> **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.**
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:**
-`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<PlayerInput>`, `RefRO<PlayerFacing>`, `RefRO<LocalTransform>`, `RefRO<GhostOwner>` + `.WithEntityAccess()`, filters `.WithAll<Simulate>().WithDisabled<Dead>()`.
- Read inside the main-thread `foreach`, keyed by the player entity, via lookups (mirroring the existing `m_BoonEffectsLookup``ComponentLookup` at `:131` and `GetBuffer<InputBufferData<PlayerInput>>(entity)` at `:192`):
- 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).
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. |
| **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.
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).
-`ClassPrepPortalHudSystem:88`, `MetaShopHudSystem:81` → read the new `FrameId`/`PlayerClass``[GhostField]` instead of `ClassForAbility(AbilityRef.Id)`.
- 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.
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). |
| 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). |
- [[Lantern_Operator_Questions]] — the locked forks · [[validate-netcode-design-before-coding]] — the ritual
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.