Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
title, date, tags, permalink
| title | date | tags | permalink | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| G6 socket honesty + G4 saturation budget — Build Spec (review-hardened) | 2026-07-21 |
|
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 reviewwf_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 <CastFacingTicks26 (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, foldedEffectiveSocketStats[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 bothMeleeCleavePending(the shipped melee variant) andConeContactPending— idempotent default-writes, rollback-safe (client copies always zero). - One
FireConehelper shared by legacy/due-fire/early-flush (one resolve path, no drift). - ► Tests need a REAL fixture (no AbilityFireSystem harness existed): GameServer-flagged world +
NetworkTimewithIsFirstTimeFullyPredictingTick(theFlagsfield 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]onZoneEffect.CasterNetworkId(G3 ownership tint),.Radius(quantized ×100; the drawn rim = the TRUE folded damage radius, G2) and.NextTick(fill arrival = the damage moment).DamagePerPulse/ExpireTickstay server-only. Ownerless interpolated ghost → server mutations just propagate. PulsePeriodTicks = 30hoisted toZoneEffect(single source for ZonePulseSystem + the client fill window).ZoneTelegraphSystem(client, PresentationSystemGroup, observe-only): ► templated on GeyserTelegraphSystem (the shipped implementation of exactly this mechanism — sharedBuildDiscunit mesh- MPB alpha + pooled GOs + prune + the
_armed/_lastFiredvalue-latch), NOT the per-frame-mesh enemy-wedge system. Only new primitive:FeedbackFx.BuildRing(the always-on rim). Fill disc scalesRadius × 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).
- MPB alpha + pooled GOs + prune + the
- ► 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 (FireActiverefactored 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 viaEvaluateConeConnect(aim recomputed live). - ►
NearestEnemyInConeextracted — the socket-fire branch,EvaluateMeleeConnectand 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 (DevTuningReceiveSystemis#if UNITY_EDITOR), so player builds readdefault(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 equalsAllyScale(count, start, 2·start, floor)— a later pass can retrofit.- FeelConfig:
AllyFxDegradeStart8 ·AllyFxDegradeFull16 ·AllyFxFloor0.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.SaveCurrentsnapshots 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" (reusesDebugOp.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