Files
Project-M/Docs/Vault/07_Sessions/2026/2026-06-09_MC1_Implementation.md
T
2026-06-09 23:26:20 -07:00

10 KiB
Raw Blame History

date, type, tags, permalink
date type tags permalink
2026-06-09 session
session
combat
mc-0
mc-1
netcode
dash
charger
gamevault/07-sessions/2026/2026-06-09-mc1-implementation

MC-0 + MC-1 implementation — dash, Charger, telemetry (code-complete; fun-gate pending)

Implements 2026-06-09_MC1_Build_Spec (the mandatory pre-code adversarial review's output). Direction: Path_to_Fun · locks: DR-029_Path_A_Fork_Locks. Status: all code + tests + structural Play-validation done; the MC-1 FUN-GATE (feel pass + bench + friend read) is the open operator item — MC-1 is NOT "done" until it passes.

What was built (the full uncommitted slice)

MC-0 — instrument the box

  • DevTelemetry (Simulation/Debug) — server-only singleton with the four fun-gate counters + LiveEnemyCount/LastSampleTick proof-of-life. DebugTelemetryReport : IRpcCommandunconditional wire type (RpcCollection hash parity); only the systems are #if UNITY_EDITOR.
  • DevTelemetrySystem (Server) — ensures the singleton, samples each tick, ships the report to every connection every 15 ticks. DevTelemetryReceiveSystem (Client) — drains the RPC into the DevTelemetryReadout static; DebugOverlay renders the live counters.
  • All four counters now wired: DashIFrameNegatedHits (HealthApplyDamageSystem, per negated event) · DashesWasted (DashSystem window-close edge, via a new DashState.NegatedCount server-written field; close-edge is server-gated on the singleton so the client's DashState is never zeroed mid-rollback) · ChargerWhiffWindowsOpened (EnemyAISystem, both whiff sites) · ChargerWhiffPunishesLanded (HealthApplyDamageSystem: player-sourced hit inside a new LungeState.StaggerUntilTick window, scored once per window by zeroing on first punish so punishes:windows ≤ 1).

MC-1 — dash

  • DamageEvent.SourceTick (non-replicated) stamped via TickUtil.NonZero at all THREE append sites (EnemyAISystem melee, ProjectileDamageSystem, TurretFireSystem); SourceTick==0 = unstamped = never i-framed (fail-safe).
  • DashState (predicted, non-replicated, re-simulated from input) + DashCooldown{[GhostField] NextTick} — baked on the player via PlayerAuthoring. PlayerInput.Dash InputEvent (Fire twin, command-hash churn only — no ghost re-bake).
  • DashSystem (predicted, [UpdateAfter(PlayerControlSystem)], Bursted): idempotent start (press + cooldown-ready + not-in-window), HALF-OPEN i-frame window [StartTick, IFrameUntilTick), recovery tail (movement locked, no i-frames), sharpness-override blink (GroundedMovementSharpness 15→200 for the window — NO CharacterProcessor edit, fully headless, exactly as the spec's confirmed correction predicted).
  • Negation in HealthApplyDamageSystem: per-element (not whole-buffer), half-open, NetworkTick comparisons only (wrap-safe). PlayerDeathStateSystem clears the window + restores sharpness on death.
  • Dash juice in CombatFeedbackSystem: dash whoosh SFX + afterimage burst + camera shake + FOV punch on the DashCooldown.NextTick edge (muzzle-flash pattern); i-frame shimmer trail each frame the local window is active; local hit-feedback suppressed during the local i-frame window (masks the documented prediction-reconciliation Health flicker). All knobs in FeelConfig (Feature 5 block, live-pokeable).

MC-1 — Charger

  • LungeState (server-only; component-presence is the discriminator — no enum in the Bursted system) + ChargerAuthoring (composes WITH EnemyAuthoring on the prefab; bakes only LungeState).
  • EnemyAISystem Charger pass (Grunt pass excludes via .WithNone<LungeState>()): precedence knockback (cancels lunge) > lunge-active (SweptMove travel, contact damage, wall-stop + overshoot whiff → stagger) > seek > commit — commit locks Dir at windup-elapse and fires even if the target left range (the punishable tell). Charger windup 30 ticks / lunge 16 u/s / 18 ticks / stagger 36 ticks (per-pass consts).
  • Prefab chain: EnemyCharger.prefab (capsule template: Enemy.prefab duplicate + ChargerAuthoring + tuned stats HP 45 / spd 2.6 / dmg 14 / cd 48 / scale 1.0) → EnemyChargerMuscle.prefab via a new EnemyRigTools "Build Charger (MC-1)" menu item (builds ONLY the charger so the committed Werewolf/Kaiju outputs aren't re-serialized). Model: SM_Chr_Muscle_Male_01 (PolygonSciFiCity — the Synty_Asset_Inventory "verified Generic rig, next-faction" path), atlas PolygonScifi_01_A + red tint (danger read), RootY 1.0. Added as the 4th WaveDirector.EnemyPrefabs round-robin entry in the Gameplay subscene (additive ghost — no re-hash of existing ghosts).

Validation (gates 1 + 2 of three)

  • EditMode: 259/259 green (spec tests incl. half-open boundary, wraparound, per-element, SourceTick-0 fail-safe, idempotent start, cooldown gate, death-mid-dash cleanup, override ordering, Charger commit-out-of-range / whiff-stagger / knockback-cancel, 7 telemetry-counter tests + the post-review rollback-window regression).
  • Play (real netcode session, server+client): zero console errors — no ComponentSystemSorter cycle, no stale-Burst exception. DashState/DashCooldown baked in BOTH worlds; DashSystem sorts directly after PlayerControlSystem (the documented 1-tick fixed-step offset pattern). Live E2E negation: armed a window on the server player, appended in-window + out-of-window strikes → only the outside one applied (100→93), counter +1, close-edge cleanup ran. Live Charger: spawned from the baked pool → seek → 30-tick telegraph → lunge commit (speed 16) → contact damage → repeat; replicates to the client; killed the (stationary) player → death/respawn flow exercised. Telemetry pipe: server counters → RPC → client DevTelemetryReadout → overlay, values matching. Charger material values verified (AnimatedLitShader + SciFi atlas + red tint).
  • Six "wasted" dashes were counted live with serverCd == clientCd — real input-driven dash starts replicated consistently (and wasted-counting works).

Post-build adversarial review (4 lenses → 12 findings → 2 confirmed, both FIXED)

A 29-agent review workflow (netcode/prediction · DOTS/Burst · spec-adherence · edge-cases, each finding adversarially refuted) ran over the full diff. Ten findings were refuted as true-but-mitigated (by design, world placement, an existing test, or the documented deviations). Two were confirmed and fixed in-session:

  1. MAJOR — the dash override lacked the StartTick lower bound. DashState is non-replicated → NOT restored on prediction rollback; after a press at tick D the client re-simulates pre-dash ticks S..D1 with the post-press window visible, and an upper-bound-only iFrameActive stomped dash velocity + sharpness onto ticks that never had them → dash-start overshoot + snap-back under real latency (editor RTT≈0 masked it; the negation had the lower bound, only the movement override didn't — the bug originated in the spec's §4 pseudocode). Fix: inDashWindow = StartTick != 0 && !StartTick.IsNewerThan(serverTick) now gates both override branches; pre-dash re-sim ticks fall to the restore branch. Pinned by Rollback_ReSimulated_PreDash_Tick_Gets_No_Override.
  2. MINOR — DashSystem vs HealthApplyDamageSystem were unordered in the server's predicted group, so a same-tick teammate-projectile vs dash-start negation (src == StartTick; ProjectileDamageSystem appends and the drain runs the SAME tick) was an unconstrained sorter tiebreak. Fix: [UpdateAfter(typeof(DashSystem))] on HealthApplyDamageSystem (chains verified disjoint — no cycle; Play-validated: dash 13 → drain 14, clean world creation).

Notable refuted-but-recorded items: the punish check compares the stagger window against the drain tick (correct today — the only SourceNetworkId>=0 site drains same-tick; revisit if MC-4's cleave appends in the plain group) · the dash displacement consumes velocity one tick after the i-frame window (the documented OrderFirst 1-tick offset; the shimmer matches the true negation window) · the build-server never clears DashState at window-close (intentional — all readers are tick-guarded; pinned by Without_Telemetry_Singleton_The_Close_Edge_Leaves_DashState_Intact) · Charger lunge contact only tests the current nearest player (the spec'd + pre-existing Grunt targeting model; a co-op design note for later).

(Process note: the first review run returned {confirmed: []} because all four agents died on a subagent session limit — an empty result that masquerades as a clean pass. Check the failures list; re-run after the reset via resumeFromRunId.)

Deviations from the build spec (all deliberate)

  1. Input = direct device reads (leftShift / buttonEast wasPressedThisFrame in PlayerInputGatherSystem), NOT a .inputactions Dash action — kills the only focused-editor step (wrapper regen). Migrate to the action map later if rebinding UI ever needs it.
  2. Per-pass consts for the Charger windup (30 ticks in the Charger foreach) instead of an EnemyStats.WindupTicks field — same effect (Grunt feel untouched), one field less; promote to per-prefab/live-singleton when tuning demands it.
  3. LungeState.StaggerUntilTick added for punish scoring (the spec inferred the window from EnemyAttackCooldown, which would conflate post-hit cooldown with stagger and let one window score many punishes).

Open items (operator)

  • The MC-1 fun-gate (gate 3): focused-editor feel pass — dash SNAP test (sharpness 200 must read as a blink, else the documented CharacterProcessor fallback + Burst restart), Charger telegraph readability, RootY feet check on the Muscle rig, then the bench (timed vs spam ≥70% fewer hits) + a friend read at Demo A. Live whiff/punish numbers only emerge with a dodging player.
  • TuningConfig live-singleton (the spec's MC-0 blocker) still does not exist — dash/Charger knobs are baked consts (+ FeelConfig statics for presentation). Decide whether to land it before the tuning pass.
  • Assets/_Recovery/0.unity (2.2 MB, 2026-06-08 23:36) — an untracked Unity scene-recovery artifact; review + delete (with its .meta) if it holds nothing.
  • Wave-spawned Chargers appear only when a siege runs (player-driven pacing) — the round-robin makes 1 in 4 spawns a Charger.

2026-06-09_MC1_Build_Spec · Path_to_Fun · DR-028_Combat_Primary_Verb_Depth_First · DR-029_Path_A_Fork_Locks · DR-023_Enemy_Animation_MonsterMash (rig pipeline) · Synty_Asset_Inventory