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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Dead>` 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<PlayerInput>` 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=<stamped>`, `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
|
||||
Reference in New Issue
Block a user