From a5302a7eaea6caefe5717cc8b0bed4230a2647ab Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Tue, 21 Jul 2026 12:35:29 -0700 Subject: [PATCH] Docs: 07-21 G6+G4 build spec + session log (H); guidelines shipped-notes; CLAUDE.md tick-source clause; validation-harness fixture notes Co-Authored-By: Claude Fable 5 --- .../dots-dev/references/validation-harness.md | 11 ++ CLAUDE.md | 4 +- .../Combat_Attack_Feel_Guidelines.md | 13 ++ .../Socket_Honesty_Saturation_Build_Spec.md | 128 ++++++++++++++++++ ...07-21_G6_G4_Socket_Honesty_Saturation_H.md | 86 ++++++++++++ 5 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 Docs/Vault/03_Design/Socket_Honesty_Saturation_Build_Spec.md create mode 100644 Docs/Vault/07_Sessions/2026/2026-07-21_G6_G4_Socket_Honesty_Saturation_H.md diff --git a/.claude/skills/dots-dev/references/validation-harness.md b/.claude/skills/dots-dev/references/validation-harness.md index be2f0a8e8..61e537240 100644 --- a/.claude/skills/dots-dev/references/validation-harness.md +++ b/.claude/skills/dots-dev/references/validation-harness.md @@ -87,6 +87,17 @@ DOES propagate into parented children's `LocalToWorld` (verified live on 6.5.0). - Forcing an animation pose: disable the drive system, write params via `AnimatorParametersAspect`; measure ground-contact via feet-Y only on non-deformed geometry. +## EditMode fixtures for prediction-gated systems (07-21, AbilityFireSystemConeTests) + +- **`NetworkTime.Flags` is INTERNAL** — a system gated on `IsFirstTimeFullyPredictingTick` silently no-ops in any + plain test world (default Flags = 0). Fixture fix: box the struct + `typeof(NetworkTime).GetField("Flags", + NonPublic|Instance).SetValue(boxed, NetworkTimeFlags.IsInPredictionLoop | IsFirstTimeFullyPredictingTick)`. + Test-only; never reflect internals in shipped code. +- **Input-buffer presses**: an `InputBufferData` entry with a set `InputEvent` keeps answering + `GetDataAtTick` for every later tick — it re-fires the moment a cooldown re-opens (the MeleeComboTests C6 + lesson, buffer-shaped). Always push a press at T **and a release (default command) at T+1**. +- Damage paths gated `WorldUnmanaged.IsServer()` need `new World(name, WorldFlags.Game | WorldFlags.GameServer)`. + ## Test-run mechanics - `run_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"])` → poll `get_test_job` diff --git a/CLAUDE.md b/CLAUDE.md index 9960f1737..475c692f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,9 +70,9 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **One-off shared-state actions belong on an `IRpcCommand`, not a predicted `InputEvent`** (RPCs are reliable; one-shot `InputEvent`s — like `Fire` — drop under server tick-batching). RPC payloads are plain blittable scalars (`int CellX/CellZ`, not `int2`; no `[GhostField]`). For a SINGLE shared target resolve a **server singleton** — never put an `Entity` in the command; use ghost-id+spawn-tick (`SpawnedGhostEntityMap`) only for many targets. - **Apply server-only RPC effects in the server `SimulationSystemGroup`, NOT the predicted loop** (rollback would double-apply). Mutating a `DynamicBuffer` is not a structural change, so it's safe while iterating a different query. - **A system-ordering CYCLE is INVISIBLE to plain-Entities EditMode tests** (they register systems individually, unsorted) — it only throws `ComponentSystemSorter` "circular dependency cycle" at **world creation (Play)**. When you add cross-system `[UpdateBefore/After]`, re-audit the EXISTING `[Update*]` attributes of the systems you order around and **always Play-validate**. [[DR-017_Persistent_Base_Player_Driven_Pacing]] -- **A dev/debug `IRpcCommand` wire TYPE must be UNCONDITIONAL (no `#if`)** — the reflection-built RpcCollection hash must match across release/dev peers or the handshake refuses; `#if UNITY_EDITOR`-gate only the send/receive SYSTEMS, never the request struct. **Re-mean bytes, don't rename**: unchanged byte VALUES keep the `[GhostField]` serializer identical → re-bake-free (only authoring *default-value* edits re-bake the subscene). +- **A dev/debug `IRpcCommand` wire TYPE must be UNCONDITIONAL** — the RpcCollection hash must match across release/dev peers; `#if`-gate only the send/receive SYSTEMS, never the struct. **Re-mean bytes, don't rename**: unchanged byte VALUES keep the `[GhostField]` serializer identical → re-bake-free (only authoring *default-value* edits re-bake the subscene). - **Derive enableable gates instead of replicating them.** e.g. player `Dead` = a LOCAL enableable derived every predicted tick from replicated `Health<=0` (rollback-correct, no `[GhostEnabledBit]`). To write the bit on a disabled entity the query must visit it (`.WithPresent()`); **bake the enableable DISABLED** so instances spawn off. Respawn/death *timing* is server-only. -- **Cooldown/spawn "next tick" sentinels:** route every stored tick through **`TickUtil.NonZero(...)`** (a computed `ServerTick+delay` can wrap to 0, the "ready" sentinel) and compare with `NetworkTick.IsNewerThan` / `.TicksSince`, **never** raw `uint <` / subtraction. **★ A BAKED `[GhostField]` scheduled-tick defaults to 0 → the "invalid-tick ⇒ fire" guard that's safe for a runtime-ADDED fuse STORMS it (0 fails `IsValid`, falls through, fires every tick); for a baked/periodic tick INVERT it (0 = not-ready → skip/lazy-stamp) + stamp born-correct at spawn. Client cues off a periodic tick ride the ABSOLUTE tick + a value-latch + a was-counting-down arm-guard — never edge-detect the field increment (phantom-fires on `0→stamp` + relevancy re-entry).** See [[Geyser_Build_Spec]]. +- **Cooldown/spawn "next tick" sentinels:** route every stored tick through **`TickUtil.NonZero(...)`** (a computed `ServerTick+delay` can wrap to 0, the "ready" sentinel) and compare with `NetworkTick.IsNewerThan` / `.TicksSince`, **never** raw `uint <` / subtraction. **★ A BAKED `[GhostField]` scheduled-tick defaults to 0 → an "invalid ⇒ fire" guard STORMS it; for a baked/periodic tick INVERT (0 = not-ready → skip/lazy-stamp) + stamp born-correct. Client cues off a periodic tick ride the ABSOLUTE tick + value-latch + was-counting-down arm-guard — never edge-detect (phantom-fires on `0→stamp` / relevancy re-entry). Tick SOURCE: a predicted-player threat reads `nt.ServerTick` (Geyser); an INTERPOLATED ghost's own effect reads `InterpolationTick` (ZoneTelegraph — predicted pins the fill ~RTT wrong, invisible on loopback).** See [[Geyser_Build_Spec]]. - **`GhostRelevancy` for region splits:** use `GhostRelevancyMode.SetIsIrrelevant` (not `SetIsRelevant`) so untagged/global ghosts stay relevant for free — only enumerate cross-region ghosts to hide. `RegionTag{byte Region}` is **server-only, NOT a `[GhostField]`**. **★ A 2nd region sharing an EXISTING tag (`EnemyTag`) → re-audit every query/cull over it: once-safe global despawns/cleared-checks then wipe or block cross-region (DR-031, DR-040).** `RelevantGhostForConnection` = `{int Connection=NetworkId.Value; int Ghost=ghostId}`. See [[DR-013_M6_Aether_Cycle_Region_Split]]. - **Shared GLOBAL state (resource ledger, `RunInfo`, meta tiers) rides the UNTAGGED director ghost**, never a region-tagged one (`SetIsIrrelevant` would hide it cross-region). Resolve the ledger via its DISTINCT `ResourceLedger` tag (the multi-`StorageEntry` "multiple instances" rule). - **Frontend world lifecycle (menu → on-demand worlds) ★:** use `CreateClientWorld`/`CreateServerWorld` (they register the `ServerWorld`/`ClientWorld` statics the UI reads; `CreateLocalWorld` was internal pre-6.5, PUBLIC on 6.5.0); menu world via `DefaultWorldInitialization.Initialize(name, false)`. **Never dispose/create worlds inside an ECS system** — do it on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`). The gameplay subscene streams in ONLY if a netcode world is the `DefaultGameObjectInjectionWorld` at `LoadScene` time. See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. diff --git a/Docs/Vault/02_Game_Design/Combat_Attack_Feel_Guidelines.md b/Docs/Vault/02_Game_Design/Combat_Attack_Feel_Guidelines.md index 936a7848c..011ce74c9 100644 --- a/Docs/Vault/02_Game_Design/Combat_Attack_Feel_Guidelines.md +++ b/Docs/Vault/02_Game_Design/Combat_Attack_Feel_Guidelines.md @@ -168,6 +168,11 @@ Priority when pixels compete: **enemy telegraphs > local player contact/impact > Audio reinforces but never solely carries a threat (SoD accessibility rule; our per-kind windup voices are the reinforcement layer, the decal is the message). +> ✅ **SHIPPED 07-21 (H)**: the budget rule — `SaturationMath.AllyScale` off the live enemy count degrades +> ally-attributed FX only (remote arc brightness + remote shakes; FeelConfig `AllyFxDegradeStart/Full/Floor` +> 8/16/0.35); enemy telegraphs structurally never degrade; the standing saturation test = DebugOverlay +> `- Saturation (G4) -` (Spawn 12 Drowners + the 3-fake-caster stress toggle). [[Socket_Honesty_Saturation_Build_Spec]] + ### G5 — Impact ladder (hit-stop canon mapped to our camera-only stack) `Time.timeScale` stays banned (netcode); our hit-stop = camera `Hold` + FOV kick + flash — same ladder, per R5: - **Scale with significance:** chip hit = flash only; solid melee hit = 2-frame hold (current) + directional @@ -190,6 +195,14 @@ Priority when pixels compete: **enemy telegraphs > local player contact/impact > - **Skillshots stay honest**: no homing-by-default; assist only where a Spark explicitly buys it (the existing `AutoTargetRange = 0` default per DR-050). +> ✅ **SHIPPED 07-21 (H)** — the cone + zone halves: the Cone socket (SpecialSlam) damages at its CONTACT tick +> (`ConeContactPending`, knob 32, 0 = legacy; cue latched to contact, arc reveal ends at contact) and zones draw +> the WildStar-grade fill decal (ZoneEffect GhostFields Caster/Radius/NextTick; `ZoneTelegraphSystem` — rim = true +> radius, fill arrival = the damage moment, on the INTERPOLATED timeline; mine warm/teal vs ally dim-blue). +> NOTE: no current frame loadout sockets a Cone spark — the archetype is honest-but-dormant until one does. +> Projectile tracer + movement trail were already true; per-Spark G1 time signatures remain a designer pass. +> [[Socket_Honesty_Saturation_Build_Spec]] + ### G7 — Responsiveness contract (buffers + cancels, netcode-shaped) - **Input buffer ≈ 6–10 ticks (0.1–0.17 s)** for melee-chain and socket presses: a press during the last ~10t of a lock fires on the first legal tick (R7 buffer method; our `InputEvent`s already latch per tick — the buffer is diff --git a/Docs/Vault/03_Design/Socket_Honesty_Saturation_Build_Spec.md b/Docs/Vault/03_Design/Socket_Honesty_Saturation_Build_Spec.md new file mode 100644 index 000000000..e134142c6 --- /dev/null +++ b/Docs/Vault/03_Design/Socket_Honesty_Saturation_Build_Spec.md @@ -0,0 +1,128 @@ +--- +title: G6 socket honesty + G4 saturation budget — Build Spec (review-hardened) +date: 2026-07-21 +tags: [build-spec, combat, sockets, netcode, presentation, lantern, guidelines] +permalink: gamevault/03-design/socket-honesty-saturation-build-spec +--- + +# G6 socket honesty + G4 saturation budget — Build Spec + +> The review-hardened design for the 07-21 pass applying [[Combat_Attack_Feel_Guidelines]] **G6** (cast grammar +> for the 4-socket kit: SpecialSlam damage-at-contact + the zone fill telegraph) and **G4** (the co-op saturation +> budget). Pre-code adversarial review `wf_98bf1268-51b` (3 lenses → per-finding refutation critics): **13 +> confirmed / 1 refuted** — every confirmed finding folded in below (marked ►). Post-impl diff review +> `wf_9757d214-b91`: **5 confirmed (all low) / 6 refuted**, all five fixed same-session (corpse-free census + +> solo gate on the ally scale; client connect-latch death-clears; ClassSelectReceiveSystem pending-clear parity; +> the no-slot-fallback + live-steer contract tests). Final suite **429/429**. +> Implementation log: [[2026-07-21_G6_G4_Socket_Honesty_Saturation_H]]. + +## Slice 1 — Cone socket (SpecialSlam) damage-at-contact + +The Cone archetype (`AbilityFireSystem` Cone branch) damaged AT FIRE, ~0.35 s before the slam animation's visual +contact — the same dishonesty melee fixed 07-20 (`TuningAuditTools` carried the NOTE). Fix = the +`MeleeCleavePending` schedule-and-consume idiom on the socket kit: + +- **`ConeContactPending { uint ResolveTick; byte Socket }`** — server-only, NOT replicated, baked zeroed on the + player (PlayerAuthoring, next to MeleeCleavePending). +- **Knob 32 `ConeContactTicks`** (Count 32→33), default **21** (~0.35 s), clamp ≥ 0, **0 = legacy at-fire** + (the MeleeContactTicks sentinel contract). Doc-pinned: keep < `CastFacingTicks` 26 (a resting gamepad stick + resolves a MOVE-facing slam past the cast window) and note default 21 sits 1 tick under WarriorCone cooldown 22. + `DebugTuningReport` +1 field (dev-protocol bump; wire type stays unconditional). +- **Fire-due check BEFORE the socket loop** (per player, isServer + hasPending): wrap-safe + `ResolveTick != 0 && !IsNewerThan(serverTick)` → rebuild the cone from LIVE state (ResolveAim now, folded + `EffectiveSocketStats[Socket]` now, live target snapshot) → DamageEvent + knockback → consume by zeroing. +- ► **Re-validate the armed socket at resolve** (bounds + SparkId still maps to a Cone archetype) — the dev + SetClass op `Clear()`s the loadout mid-flight; mismatch = consume-drop, never a slam with foreign stats. + SetClass also zeroes the pending next to its SocketCooldown reset. +- **Early-flush**: a recast with a still-armed pending fires it first (re-validated), then re-arms — no knob + combination can lose a slam (the melee C0/C12 contract; reachable: cooldown-reduction mods under cooldown 22). +- ► **hasPending fallback**: knob 0 OR a missing pending slot (plain test worlds) → the legacy immediate path. +- ► **Death-clear** (the review's biggest catch — a SHIPPED bug): melee's "death gate" never existed. An armed + pending FREEZES through death (`WithDisabled` merely skips) and fires from the RESPAWN position once Dead + re-disables. `PlayerDeathStateSystem`'s isDead branch now zeroes **both** `MeleeCleavePending` (the shipped + melee variant) and `ConeContactPending` — idempotent default-writes, rollback-safe (client copies always zero). +- **One `FireCone` helper** shared by legacy/due-fire/early-flush (one resolve path, no drift). +- ► **Tests need a REAL fixture** (no AbilityFireSystem harness existed): GameServer-flagged world + + `NetworkTime` with `IsFirstTimeFullyPredictingTick` (the `Flags` field is **internal** — test-only reflection + write) + AbilityDatabase blob with a Cone def + `InputBufferData` press/release command pairs + (a held InputEvent re-fires across the cooldown edge — the MeleeComboTests C6 lesson, buffer-shaped). + +## Slice 2 — Zone fill telegraph (WildStar-grade honesty for player zones) + +Zones (Vortex/LightZone) had NO client telegraph — server-only `ZoneEffect`, first pulse one full period +(30 t) after spawn. + +- **Wire change** (ghost-hash churn, dev peers rebuild together): `[GhostField]` on `ZoneEffect.CasterNetworkId` + (G3 ownership tint), `.Radius` (quantized ×100; the drawn rim = the TRUE folded damage radius, G2) and + `.NextTick` (fill arrival = the damage moment). `DamagePerPulse`/`ExpireTick` stay server-only. Ownerless + interpolated ghost → server mutations just propagate. +- `PulsePeriodTicks = 30` hoisted to `ZoneEffect` (single source for ZonePulseSystem + the client fill window). +- **`ZoneTelegraphSystem`** (client, PresentationSystemGroup, observe-only): ► templated on + **GeyserTelegraphSystem** (the shipped implementation of exactly this mechanism — shared `BuildDisc` unit mesh + + MPB alpha + pooled GOs + prune + the `_armed`/`_lastFired` value-latch), NOT the per-frame-mesh enemy-wedge + system. Only new primitive: `FeedbackFx.BuildRing` (the always-on rim). Fill disc scales `Radius × fill`, + `fill = 1 − saturate(lead / PulsePeriodTicks)`; each server re-stamp naturally resets it (the persistent-zone + encoding). Pulse cue on the countdown CROSSING, latched per NextTick value + was-counting-down arm-guard + (the Geyser ★ contract — never edge-detect). +- ► **Evaluated on `NetworkTime.InterpolationTick`** (fallback ServerTick), a DELIBERATE divergence from the + Geyser precedent: a geyser threatens the PREDICTED local player; a zone's observables (enemy HP drops, vortex + pull) live on the INTERPOLATED timeline the zone renders on. The predicted tick would complete the fill ~RTT + early and pin it at full — invisible on loopback, 20-40 % of the bar wrong at internet RTTs. +- Tint: mine = warm (LightZone) / teal (Vortex); ally = dim cool-blue, same shapes; never red (G3). + +## Slice 3 — Cone client-cue honesty (CombatFeedbackSystem) + +- ► **`TickWindowMath.FireStartRaw(nextFireRaw, cooldownTicks)`** — the ONE home of the window-start + reconstruction (`FireActive` refactored onto it; unit-pinned incl. the wrap-to-zero NonZero coercion). +- ► **Contact latched ONCE at the socket fire edge** (`_pendingConeConnectTick = FireStartRaw + knob`, the melee + C14 idiom) — per-frame reconstruction can skip/double-fire the one-shot when a cooldown stat-mod lands + mid-window. Arc reveal life derives from the knob (reveal completes AT contact, C13). Connect package + (bite/thunk/kick/rumble) fires at the contact tick via `EvaluateConeConnect` (aim recomputed live). +- ► **`NearestEnemyInCone` extracted** — the socket-fire branch, `EvaluateMeleeConnect` and the new deferred + cone eval all route through one scan (the third inline copy was the B4/B5 duplication class). +- ► **`TuningConfig.Defaults()` fallback at every client tcfg read site** — fixes a SHIPPED release-build bug: + the dev TuningConfig singleton is editor-only (`DevTuningReceiveSystem` is `#if UNITY_EDITOR`), so player + builds read `default(TuningConfig)` → contact knobs 0 → connect cues fired ~0.35 s before server damage. + Release cue timing now matches the release server's Defaults()-driven damage timing. + +## Slice 4 — G4 co-op saturation budget + +- **`SaturationMath.AllyScale(enemies, start, full, floor)`** — ► in **ProjectM.Client** (the HudVisualMath + presentation-math precedent; the tests asmdef references Client). Note: EnemyMarkerSystem's pip fade equals + `AllyScale(count, start, 2·start, floor)` — a later pass can retrofit. +- FeelConfig: `AllyFxDegradeStart` 8 · `AllyFxDegradeFull` 16 · `AllyFxFloor` 0.35 (+ ResetDefaults stamps). +- ► **Degrade set = what is actually ALLY-attributed** (the plan's "remote emit counts" targeted a path that + doesn't exist — remote swings draw arc meshes, not particles; enemy hit/death bursts are UNATTRIBUTED + health-edge FX shared with the local player): remote slash arc **brightness** (life stays contact-honest per + C13), remote-player shakes (`HitShakeRemote`, `RemotePlayerDeathShake`), and the stress package's own emits. + Local-player FX untouched; **enemy telegraphs never degrade** (structurally separate system — no knob). +- ► **`CombatStressDebug.StressAllyFx`** — editor-only static in the Debug family with a + `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` reset (the static-presentation-bridge ★ rule), NOT in + FeelConfig (`FeelProfileService.SaveCurrent` snapshots every FeelConfig field — a captured profile would + re-arm stress mode on Apply). While on, CombatFeedbackSystem synthesizes the ally package at 3 orbiting fake + casters every ~0.5 s. +- DebugOverlay `- Saturation (G4) -`: "Spawn 12 Drowners" (reuses `DebugOp.SpawnEnemy = 14`; valid in DevSandbox + because GymSub bakes GymTag + GymEnemyRoster) + the stress toggle. + +## Loadout note (surfaced by the live proof) + +No current frame loadout sockets `WarriorCone` (Bathynaut = Vortex/Blink/HookPull/LightZone; Harpooner = +HookPull/Blink/DecoyWisp/LightZone) — the Cone/SpecialSlam path is **dormant in-game** until a loadout carries +it. The pass still fixed it now (G6 says every socket declares its grammar; the archetype is live for any future +Spark), and the live proof injected the spark directly. + +## Validation record + +L1 console clean · L2 **427/427** EditMode (5 cone-schedule tests incl. death-clear + socket-swap drop; 3 +FireStartRaw; 5 SaturationMath; knob 32 auto-covered by the TuningConfig round-trip) · **live server proof**: +pending stamped tick 9101 → resolve 9122; dummy HP 340 held through 9121, 340→318 across the 9122 boundary with +the pending consumed atomically, under real ~4-tick batching. Zone GhostFields live-verified on the client +(`caster=1 radius=2.5 next=`, `DamagePerPulse` correctly NOT replicated) · telegraph rendering +screenshot-verified (teal local rim at true radius) · L3 overlay rows verified. + +## Related + +- [[Combat_Attack_Feel_Guidelines]] — the grammar this implements (G2/G3/G4/G6 sections) +- [[2026-07-20_Melee_Feel_Forks_B]] — the melee predecessor (idioms C0/C12/C13/C14 reused here) +- [[Geyser_Build_Spec]] — the periodic-tick telegraph contract (the ★ latch rule) +- [[2026-07-21_G6_G4_Socket_Honesty_Saturation_H]] — implementation session log diff --git a/Docs/Vault/07_Sessions/2026/2026-07-21_G6_G4_Socket_Honesty_Saturation_H.md b/Docs/Vault/07_Sessions/2026/2026-07-21_G6_G4_Socket_Honesty_Saturation_H.md new file mode 100644 index 000000000..7e6171f6d --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-07-21_G6_G4_Socket_Honesty_Saturation_H.md @@ -0,0 +1,86 @@ +# 2026-07-21 (H) — G6 socket honesty + G4 saturation budget (the no-eyes queue) + +**Operator: "do the next few things that don't require eyes."** Skipped the finisher-reach ×1.39 eyes-on and the +player hurt flinch (both operator-visual), ran the two autonomous items as one Feature-track pass: +**G6** (SpecialSlam/Cone damage-at-contact + the zone fill telegraph) and **G4** (the co-op saturation budget) +from [[Combat_Attack_Feel_Guidelines]]. Design → [[Socket_Honesty_Saturation_Build_Spec]] (the durable spec). + +## Reviews (the sandwich) + +- **Pre-code** `wf_98bf1268-51b`: 3 lenses → 14 deduped findings → per-finding refutation critics: **13 + confirmed / 1 refuted**, all folded in. Two catches were SHIPPED bugs beyond this slice's scope: + the **melee death-strand** (an armed `MeleeCleavePending` survives death — `PlayerDeathStateSystem` cleared + `MeleeCombo` but not the pending — and fires the cleave from the RESPAWN ring) and the **release-build cue + dishonesty** (the client TuningConfig singleton is editor-only; player builds read `default(TuningConfig)` → + contact knobs 0 → melee connect cues ~0.35 s before server damage). Both fixed in this pass. +- **First run's critics all died on the session quota and returned a CLEAN-LOOKING empty result** — the + [[workflow-agent-quota-failures-look-clean]] trap, caught by the failures list; resumed after reset with + `resumeFromRunId` (lenses replayed from cache). ALSO: my dedup key truncated at 120 chars and the plan's long + temp path swallowed 6 distinct findings — recovered from the journal; key fixed (basename + 160) in the + post-impl script. +- **Post-impl** `wf_9757d214-b91`: lenses done (11 deduped findings); critics hit the quota again — resumed + after the 10am reset (see the addendum below for verdicts). Two findings pre-verified by inspection + fixed + immediately: the **base** class-swap path (`ClassSelectReceiveSystem`) lacked the pending clear (dev SetClass + parity), and `ZoneAuthoring`'s doc still said "no hand-written [GhostField]". + +## What shipped (14 modified + 6 new files; detail in the build spec) + +1. **Cone damage-at-contact**: `ConeContactPending` (server-only, baked zeroed) + knob 32 `ConeContactTicks` + (default 21, 0 = legacy; Count 33; DebugTuningReport +1 = dev-protocol bump) + the schedule/early-flush/ + re-validate/consume machinery in `AbilityFireSystem` (one shared `FireCone`). Death + BOTH class-swap paths + drop an armed pending; the melee pending got the same death-clear. +2. **Zone fill telegraph**: `[GhostField]` on `ZoneEffect.CasterNetworkId/Radius/NextTick` (ghost-hash churn; + DamagePerPulse/ExpireTick stay server-only); `PulsePeriodTicks` hoisted shared; new `ZoneTelegraphSystem` + (Geyser-templated latch + arm-guard, `BuildRing` rim = TRUE radius, fill arrival = the damage moment, + **evaluated on `InterpolationTick`** — the zone's observables are interpolated; predicted tick would pin the + fill wrong by ~RTT), mine warm/teal vs ally dim-blue tint. +3. **Cone cue honesty**: `TickWindowMath.FireStartRaw` (single-sourced), contact latched once at the fire edge + (C14), arc reveal ends at contact (C13), `NearestEnemyInCone` shared by 3 call sites, and the + `TuningConfig.Defaults()` fallback at every client read site (the release-build fix). +4. **G4 saturation budget**: `SaturationMath.AllyScale` (Client, HudVisualMath precedent) + FeelConfig + `AllyFxDegradeStart/Full/Floor` (8/16/0.35); degrade set = genuinely ally-attributed FX only (remote arc + BRIGHTNESS — life stays contact-honest — + remote shakes); enemy telegraphs structurally exempt; + `CombatStressDebug.StressAllyFx` (Debug family + SubsystemRegistration reset — NOT FeelConfig, profiles would + capture it) + DebugOverlay `- Saturation (G4) -` rows (Spawn 12 Drowners via DebugOp 14; the stress toggle). + +## Validation + +- **L1** console clean throughout (edits, bake, Play session). +- **L2 427/427** EditMode (13 new: 5 cone-schedule incl. death-clear + socket-swap drop; 3 FireStartRaw; + 5 SaturationMath; knob 32 auto-covered). New fixture lesson: **`NetworkTime.Flags` is internal** — the cone + tests set `IsFirstTimeFullyPredictingTick` via a boxed-reflection write (recorded in validation-harness.md). +- **Live server proof** (guardian sampling): pending stamped tick 9101 → resolve 9122; dummy HP 340 held through + 9121, **340→318 exactly across the 9122 boundary, pending consumed atomically**, under real ~4-tick batching. + Zone GhostFields verified on the LIVE client (`caster=1 radius=2.5 next=`; `DamagePerPulse` correctly + absent) + the vortex pulse drain (400→340) + `~ZoneTelegraphFX` drawing. +- **L3** screenshot: the teal local-caster rim at the true radius + the new overlay rows. + +## Surfaced along the way + +- **No current frame loadout sockets `WarriorCone`** (Bathynaut = Vortex/Blink/HookPull/LightZone; Harpooner = + HookPull/Blink/DecoyWisp/LightZone) — the Cone/SpecialSlam archetype is **dormant in-game** until a loadout + carries it. The honesty fix is in place for whenever it returns; the live proof injected the spark directly. + Worth an operator call: give a frame the slam back, or park the archetype until a Spark needs it. + +## Post-impl review addendum (critics resumed after the 10am reset) + +`wf_9757d214-b91` final: **5 confirmed / 6 refuted / 0 failures** (all confirmed = low). All five fixed same-session: +1. **Saturation census counted corpses** (Dying is server-only; dead ghosts linger ~1 s in the FX cache) → count + only `Hp > 0` entries. +2. **Solo play degraded the local player's own hit shake** (the health-drop shake for enemy victims rode the + ally scale with no co-op gate) → `_remotePlayersQuery` census: 0 remote players ⇒ `_allyFxScale = 1`. +3. **The client connect-cue latches survived local death** (server dropped the damage; the corpse still played + the full connect package at the contact tick) → the death branch zeroes `_pendingConnectTick` + + `_pendingConeConnectTick` — fixes the pre-existing melee variant too. (Two lenses found this independently.) +4. **Base class-swap path lacked the pending clear** (`ClassSelectReceiveSystem` — only the dev SetClass op got + it; the DR-046 "cannot drift" coupling drifted) → parity clear added (pre-verified by inspection while the + critics waited on quota). +5. **Two contracts unpinned**: the no-pending-slot fallback + live-steer-at-contact → two new tests + (`Cone_Missing_Pending_Slot_Falls_Back_To_Immediate`, `Cone_Resolves_With_The_Aim_At_Contact_Not_Cast`). +Plus the `ZoneAuthoring` doc-drift fix ("no hand-written [GhostField]" was no longer true). Suite after fixes: +**429/429 green.** Refuted (correctly): the InterpolationTick rim-flash concern, the two-cone-sockets case, the +FeelProfileService int round-trip, the FireStartRaw-consumer concern, and the remote-arc life scaling. + +**Next:** the two eyes-on items (finisher reach ×1.39 · player hurt flinch additive layer) + the G6 remainder +that needs a designer eye (per-Spark G1 time signatures) + the operator fork: no frame loadout sockets a Cone +spark — re-socket the slam or park the archetype.