Further Tests & Progress

This commit is contained in:
2026-06-04 11:35:57 -07:00
parent 5c11ff4fad
commit 51401d2c2b
65 changed files with 2784 additions and 45 deletions
@@ -0,0 +1,75 @@
---
date: 2026-06-04
type: session
tags: [session, polish, backlog, testing, hygiene, netcode, audio]
---
# Session 2026-06-04 — Polish & Backlog-Clear Pass (Stages AG)
## Goal
Operator asked to **clear the open backlog and do a comprehensive polish pass on everything**, executed **sequentially** at full scope. A critical read (3 explore audits + direct verification) found the game systems-complete through M6, single-client runtime-validated, console clean, netcode well-built and convention-compliant. The plan (`~/.claude/plans/melodic-yawning-hearth.md`) sequences the work into 9 dependency-ordered stages **A→I**, each landing cleanly (compile → console → tests → runtime spot-check).
This log covers **Stages A, B, C, D, and E(audio)**. Stages AC were done on the unfocused editor; the operator then focused Unity, enabling the Burst-edit + ghost-re-bake + Play-mode validation for **D** (one new netcode surface, validated server==client in Play) and **E(audio)** (validated running in Play). The remaining Stage E *feel* tweaks + Stages FI continue next.
## Done
### Stage A — Hygiene, reconcile, const-centralization
- **Backlog reconciled** (`06_Roadmap/Backlog.md`): M1-revalidation → moot/subsumed (M1M6 validated on stable 6.4.7); duplicate "template cleanup" checked off; M5 subscene-split/streaming → **superseded by [[DR-013_M6_Aether_Cycle_Region_Split]]** (region + GhostRelevancy; do not build streaming).
- **Milestones**: added the **2026-06-04 Polish & backlog-clear pass** row.
- **Home MOC** un-stale'd: latest session pointer + decisions range DR-001…DR-015.
- **`Tuning.cs`** created (`Simulation/Tuning.cs`) — central, Burst-safe home for the previously-buried balance consts.
- Verified **no stale "74" test-count claim** in CLAUDE.md (audit misattribution; the "74/74" is the historical M5.5 Milestones row, left as-is).
### Stage B — System-level EditMode tests for M6 systems
**+31 cases / 9 files** (86 → **117** green): `StorageOpReceiveSystemTests`, `TurretFireSystemTests`, `ExpeditionGateSystemTests`, `CyclePhaseSystemTests`, `PlayerRespawnSystemTests`, `ResourceHarvestSystemTests` (incl. N-projectile at-most-once destroy), `WaveSystemTests`, `BuildPlaceSystemTests` (incl. **co-op same-cell atomicity**), `RegionTransitSystemTests`. Plain-Entities pattern; immediate-ECB-playback so no separate ECB system.
### Stage C — wire systems to `Tuning` (code)
- `ResourceHarvestSystem.k_ProjectileRadius` and `AbilityUpgradeSystem` `UpgradeSourceId`/`TierStep`/`CostAmount` now derive from `Tuning.*` (value-identical const-from-const; comments preserved; not a query-set change → no Burst-binary hazard). 117/117 green.
### Stage D — replicated wave number (the ONE new netcode surface) ✅ Play-validated
- Added `[GhostField] int WaveNumber` to `CycleState`; `CyclePhaseSystem` is the single writer (syncs it each tick from the server-only `WaveState`); `HudSystem` shows "WAVE N — M HUSKS" during Defend.
- +1 EditMode test (118 green). **Play-mode validated server==client**: both worlds reported identical `CycleState` (Wave 0 at rest, and **Wave 7 after seeding the server `WaveState`** → synced + replicated). **No "not a known Burst entry point" spam** after the forced CycleDirector re-bake (focused editor) and **no errors** — only the pre-existing in-editor "Server Tick Batching" warnings (a known backlog item, not a regression).
- **TMP HUD migration deferred** — would add a TMP font-asset dependency, against the project's asset-free HUD convention. Kept legacy `Text`; documented as optional future polish.
### Stage E(audio) — procedural ambient + phase stingers ✅ Play-validated
- New **`AmbientAudioSystem`** (`Client/Presentation`, client-only `SystemBase` in `PresentationSystemGroup`, observer-only): a low (vol 0.10) **seamless-looping procedural drone** (frequencies snapped to integer cycles/buffer so the loop has no click) + short procedural **phase-change stingers** (Expedition/Defend/Build, with a tenser "wave incoming" cue + a Defend volume swell). Asset-free (`AudioClip.Create`, mirrors `CombatFeedbackSystem.MakeClip`); never mutates the sim.
- **Play-validated**: `~AmbientAudio` AudioSource `isPlaying=true, loop=true, vol=0.10, clip=176400 samples (4s)`, no runtime/job-safety errors.
### Stage E(feel) — combat juice (4 client-only features) ✅ operator-approved
- New **`FeelConfig`** static (`Client/Presentation/FeelConfig.cs`) — ~22 live-pokeable knobs, reset on play-enter via `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` (the `AimPresentation` precedent). Client-presentation only; never read from Burst.
- **Implemented (observer-only, validated in Play):** (1) hit camera-punch routed through `FeelConfig` + a netcode-safe FOV "hit-stop" kick (`PrototypeCameraRig.PunchFov` — NEVER `Time.timeScale`); (2) kill-shot fanfare (amplified Husk-death-on-prune burst/shake/SFX); (3) respawn shimmer (local Health 0→positive edge in `CombatFeedbackSystem`); (4) reticle lock-on tether in `AimReticleSystem` (client computes nearest living Husk itself — no replicated target exists — gamepad-gated `LineRenderer`).
- Defaults **adversarially reviewed** (3-critic + synthesis Workflow) and **operator-approved live** ("feels good") in a forced Defend wave (10 Husks, server==client). Console clean.
### Stage F — ghost-prop reskin + post-processing ✅ (values verified)
- **Prop reskin** — distinct material *assets* (persist into Play, no shared-material bleed; assigned via `PrefabUtility.SaveAsPrefabAsset`, the CLAUDE.md-safe prefab-asset edit) so props stop reading as identical "batteries": Storage → new **`M_Storage`** (dark steel + cool-blue emissive), Turret → new **`M_Turret`** (green ally-tech, distinct from orange Husks), ResourceNode → new **`M_ResourceNode`** (amber/gold), UpgradePickup keeps its glow. Found `M_Env_Storage` was a blank auto-stub (internal name "Universal Render Pipeline/Lit", white base) — replaced. Verified each prop's material/shader/base+emission **values** (not just a render).
- **Post-processing verified already complete:** SSAO renderer feature present + `isActive` on the active `PC_Renderer`; URP `colorGradingMode=HighDynamicRange` (correct for ACES); `PostFX_DarkSciFi` = Bloom + Tonemapping(ACES) + ColorAdjustments + Vignette, all active. Backlog "add SSAO" satisfied.
- **Deferred (diminishing returns, plan-noted):** reflection probe, ORM-repack / deeper material fidelity, a proper turret MESH (battery mesh kept — distinct material is the 80/20; a real turret mesh needs a Synty asset + Entities-Graphics verification). Materials are assets → instantly re-tintable on operator request.
### Stage G — new gameplay (in progress; design-reviewed) ✅ timed modifiers · knockback · telegraph
- **Adversarial design review** (4-agent Workflow: netcode/determinism · reuse · test-plan → synthesis) gave per-feature specs (re-bake?/determinism/files/tests). No-re-bake: timed modifiers, knockback, debug-RPC, storage-gate, pickup, multi-prefab. Re-bake: Spitter (new ghost types) + telegraph (one `[GhostField]` on the Husk).
- **Timed/removable modifiers ✅** — server-only `TimedModifier{SourceId,UntilTick}` buffer (StatModifier layout untouched → provably no re-bake) + `TimedModifierExpirySystem` (removes the matching StatModifier when due; replicates via the existing buffer; `StatRecomputeSystem` unchanged) + `TimedModifierUtil.RemoveBySourceId` (clear-by-type). +4 tests.
- **Enemy knockback ✅** — server-only `KnockbackState{Dir,Speed,UntilTick}` (no re-bake; Husk position already replicates), stamped by `ProjectileDamageSystem` on hit (backward-compatible via `TryGetSingleton<NetworkTime>` so existing tests pass), applied by `EnemyAISystem` as the SOLE position writer (recoil replaces seek + suppresses the strike). Tunable `Tuning.KnockbackSpeed`(8, 0=off) / `KnockbackDurationTicks`(8). +3 tests.
- **Husk attack telegraph ✅ (re-bake)** — replicated `[GhostField] AttackWindup.WindUpUntilTick` on the Husk; `EnemyAISystem` restructured to a 2-phase strike (commit wind-up when in-range + cooldown-ready → strike at expiry; cancel on leave-range; a knocked Husk doesn't wind up); client cue in `CombatFeedbackSystem` (observer, warns on the wind-up-start edge). Tunable `Tuning.AttackWindupTicks`(18 ≈ 0.3s, 0/1 = instant). +2 tests. **Re-bake Play-validated: server==client (4 Husks, 2 winding up, identical maxWindTick), no Burst-cache spam, no errors. 127 EditMode green.**
### Aim-drift fix (operator request, end of session) ✅
- **Symptom:** holding the cursor near the player, the aim/reticle swims without mouse movement when the character turns / the camera pans — feels inaccurate. **Cause:** the camera look-ahead led toward `PlayerFacing` (aim) → turning to face a near-cursor panned the camera → the live cursor screen-ray re-projected onto a different ground point → aim drifted (worst near the player, short lever arm). **Fix:** `PrototypeCameraRig` now leads toward **MOVEMENT** (`PlayerInput.Move`), not aim — a stationary aim no longer pans the camera; the cam still anticipates where you're going. Researched (gamedeveloper.com dual-stick; Relic Hunters Zero "ignore the crosshair unless not moving"). EditMode **127/127**, console clean; "feels accurate now" = operator feel-test (tunable: `AimLeadDistance` 0 = no lead). Recorded in [[DR-012_Aim_Controls_Cursor_Gamepad]] Refinement 2.
## Decisions
- **Created [[DR-016_Stage_G_Combat_Gameplay]]** (timed-modifier / knockback / telegraph architecture + the deferred Spitter / multi-prefab) and **amended [[DR-012_Aim_Controls_Cursor_Gamepad]]** (Refinement 2: movement-based camera look-ahead supersedes the facing-based look-ahead).
- **Debug systems kept `#if UNITY_EDITOR`-gated in place** (not moved to a new Editor asmdef). Already build-stripped; tightly coupled to their Client/Server worlds; a separate Editor asmdef risks DOTS system-discovery surprises for no functional gain. `execute_code` statics preserved.
- **RegionRelevancySystem is integration/operator-validated, not unit-tested.** `Unity.NetCode.GhostRelevancy` has an **internal** constructor + `readonly` `GhostRelevancySet` → the singleton genuinely cannot be constructed from the test assembly. Relies on existing runtime validation ([[DR-013_M6_Aether_Cycle_Region_Split]]) + the Stage-I multi-client checklist. (Verified type shapes via `unity_reflect`.)
## Open / deferred
- **Enemy knockback + Husk attack-telegraph → re-homed to Stage G** (server/netcode, not client presentation): the 3-critic review proved both inherently touch the sim/netcode surface — knockback fights `EnemyAISystem`'s per-tick `LocalTransform` write on the interpolated ghost (needs a server knockback-state component + EditMode test); telegraph needs a NEW replicated wind-up signal (`[GhostEnabledBit]`/`[GhostField]` on the Husk → re-bake) because `EnemyAttackCooldown`/`EnemyStats`/`AttackRange` are server-only. Each behind an adversarial review + test.
- **Stage C decor-LOD client-only split** — verify deferred (wants the gameplay subscene open; tick-budget optimization, not correctness).
- **Stages FI**: ghost-prop reskin + post-processing (F); new content — Spitter/boss/timed-modifiers/multi-prefab-abilities/storage-proximity/standalone-debug-RPC (G); controls — rebind + ability slots + ability UI (H); validation harness + operator-required live runs — two-build LAN co-op, live fire, standalone server perf (I).
## Next
- **Polish stages AF are DONE + validated** (118 EditMode green; the one new netcode surface — `CycleState.WaveNumber` — proven server==client in Play; feel operator-approved; props reskinned + post verified).
- **Stage G done so far:** timed modifiers, knockback, attack telegraph (all validated). **Remaining:** the ranged **Spitter** (the large one — new `EnemySpitter` + new interpolated `EnemyProjectile` ghost prefabs + spit-fire/move/damage-vs-players systems; re-bake = new ghost TYPES, not an existing-ghost serializer change) and **multi-prefab abilities** (generalize the non-Burst `ProjectileClassificationSystem` to a ghost-type SET; core correctness — no owner-client double-spawn — is Play-only). Small fold-ins still open: standalone-server debug RPC, storage proximity-gate, pickup auto-grant (confirm intent).
- **Stage H** (rebindable controls + ability slots + ability icon/UI) and **Stage I** (thin-client/MPPM harness + operator-required live runs: two-build LAN co-op, live fire, standalone server perf — the standing ~1.251.75 ticks/frame question).
@@ -52,6 +52,12 @@ Operator feedback after the first pass: make KBM aiming feel more natural / worl
- **Skipped** (with reasons): constant-screen-size scaling (fixed-distance cam), aim line (redundant with the ring), KBM enemy-magnetism (would undermine the intentional gamepad-only assist).
- Validated: EditMode **86/86**, console clean; runtime (focused) KBM ring active at the cursor ground point, OS cursor hidden, `TargetFacing` published, no exceptions.
## Refinement 2 — movement-based camera look-ahead (aim-drift fix, 2026-06-04)
Operator feedback: holding the mouse cursor NEAR the player, the aim/reticle "swims" without moving the mouse when the character turns or the camera pans — feels inaccurate. **Root cause:** the look-ahead in Refinement 1 led the framed point toward `PlayerFacing` (the aim). The KBM reticle/aim is the LIVE cursor screen-ray re-projected onto the ground each frame, so turning to face a near-cursor moved the camera (aim → facing → look-ahead → camera pan), and a stationary mouse then re-projected to a DIFFERENT ground point → the aim direction drifted. Worst near the player (short lever arm → high angular sensitivity). Research (gamedeveloper.com dual-stick controls; Relic Hunters Zero) confirmed that coupling the camera to the crosshair/aim causes exactly this and is conditioned-out in shipped games.
**Fix (supersedes the facing-based look-ahead in Refinement 1):** the camera look-ahead now leads toward **MOVEMENT** (`PlayerInput.Move`), not aim/facing. `PrototypeCameraTargetSystem` publishes `PrototypeCameraRig.TargetMoveDir`; the rig leads toward it. A stationary aim no longer pans the camera → the reticle is rock-stable while turning/aiming; the camera still anticipates where the player is MOVING. `AimLeadDistance` (default 2.5, tunable, 0 = off) is unchanged in magnitude — only its direction source changed. EditMode **127/127**, console clean. Live "feels accurate now" = operator (focused Game view).
## Open / deferred
- The **real mouse-cursor path + live device auto-switch** need a **focused** Game view — the unfocused editor can't inject mouse position / device actuation (validated the replication, math, gate, and reticle headlessly via `DebugInputInjectionSystem` + forced scheme). Operator focused click-test pending.
@@ -0,0 +1,35 @@
---
id: DR-016
title: Stage G combat gameplay slice — timed modifiers, knockback, attack telegraph (+ deferred Spitter/multi-prefab)
status: accepted
date: 2026-06-04
tags:
- decision
- gameplay
- combat
- netcode
- enemies
- modifiers
permalink: gamevault/07-sessions/decisions/dr-016-stage-g-combat-gameplay
---
# DR-016 — Stage G combat gameplay slice
## Context
The 2026-06-04 polish pass ([[2026-06-04_Polish_Backlog_Pass]]) reached a "new gameplay" stage after polish stages AF. The operator selected a slice: ranged **Spitter** enemy, **knockback + attack telegraph**, and **timed/removable modifiers + multi-prefab abilities** (small fold-ins — storage proximity-gate, pickup auto-grant, standalone-debug RPC — riding along). Each is netcode-touching, so a **4-agent adversarial design-review Workflow** (netcode/determinism · reuse · test-plan → synthesis) ran first and produced per-feature specs (re-bake?, determinism, files, EditMode tests). This DR records the architecture the review locked and what shipped.
## Decision
- **Timed/removable modifiers — a SEPARATE server-only `TimedModifier{SourceId,UntilTick}` buffer, NOT a field on the replicated `StatModifier`.** Adding ANY member (even non-`[GhostField]`) to a `[GhostField]` buffer element regenerates its serializer/stride/hash = an effective re-bake; the separate buffer keeps `StatModifier` byte-identical. `TimedModifierExpirySystem` (server, plain `SimulationSystemGroup`) removes the matching `StatModifier` by `SourceId` when due (wrap-safe `NetworkTick`); the shortened `[GhostField]` buffer auto-replicates and `StatRecomputeSystem` reverts the effective stat unchanged. `TimedModifierUtil.RemoveBySourceId` is the clear-by-type helper. **No re-bake.**
- **Enemy knockback — server-only `KnockbackState{Dir,Speed,UntilTick}`, applied INSIDE `EnemyAISystem`.** Husk position already replicates (stock `LocalTransform` variant), so knockback needs NO new `[GhostField]` (no re-bake). The one desync trap is two writers of the Husk's `Position`; `EnemyAISystem` is the sole writer, so knockback is blended there (recoil REPLACES seek + suppresses the strike for the window). `ProjectileDamageSystem` stamps `KnockbackState` on hit (it has the projectile heading; backward-compatible via `TryGetSingleton<NetworkTime>` so existing tests pass). Tuned by `Tuning.KnockbackSpeed`(8, 0=off) / `KnockbackDurationTicks`(8). **No re-bake.**
- **Husk attack telegraph — replicated `[GhostField] AttackWindup.WindUpUntilTick` on the Husk (a re-bake) + a 2-phase strike.** The client has NONE of the timing inputs (`EnemyStats` / `EnemyAttackCooldown` are server-only), so the wind-up signal MUST be replicated. A `uint` tick (not a `[GhostEnabledBit]`) so the client cue can ramp/countdown and survive a missed snapshot. `EnemyAISystem` restructured: when first in-range + cooldown-ready it commits `WindUpUntilTick = now + Tuning.AttackWindupTicks` and damages nothing; strikes when the tick elapses; cancels on leave-range; a knocked Husk doesn't wind up. Client cue = observer in `CombatFeedbackSystem` (warns on the wind-up-start edge). Tuned by `Tuning.AttackWindupTicks`(18 ≈ 0.3s, 0/1 = instant). **Re-bake** (done on a focused editor).
- **Deferred (still selected, not yet built):** ranged **Spitter** (two NEW ghost prefabs — `EnemySpitter` + an interpolated `EnemyProjectile` — + spit-fire/move/damage-vs-players systems; the enemy projectile must NOT reuse the player's predicted projectile or `ProjectileDamageSystem`'s faction-blind target query; the plain-group sweep must use a stored `LastStep`, not `DeltaTime`, per the DR-013 dt-trap) and **multi-prefab abilities** (generalize the non-Burst `ProjectileClassificationSystem` to a ghost-type SET; core correctness — no owner-client double-spawn — is Play-only, `NetCodeTestWorld` being internal in 1.13.2). Small fold-ins (storage proximity-gate, pickup auto-grant, standalone-debug RPC) also pending.
## Consequences
- **Validated (6.4.7):** EditMode **86 → 127** (timed-mods +4, knockback +3, telegraph +2; the M6 system tests from Stage B are the rest). Console clean throughout. The telegraph re-bake was **Play-validated server==client** (4 Husks both worlds, 2 winding up, identical `maxWindTick`; no "not a known Burst entry point" spam; no errors). Knockback's existing-test backward-compat confirmed.
- **Reusable rulings:** never mutate a replicated buffer-element layout (use a separate server-only tracking buffer); server-only state that drives an already-replicated field needs no re-bake; a replicated signal is mandatory when the client lacks the timing inputs; keep ONE writer of an interpolated ghost's transform; `replace_method` (MCP `script_apply_edits`) does NOT handle `struct` ISystems — use line/anchor edits.
- **Tunables** let the operator dial or disable each gameplay change (knockback speed 0 = off; windup ticks 0/1 = instant/legacy).
See [[2026-06-04_Polish_Backlog_Pass]]. Builds on [[DR-009_GameFeel_Identity_FirstBlood]] (Husk/feel), [[DR-013_M6_Aether_Cycle_Region_Split]] (dt-trap, region gating), [[DR-004_M3_DataDriven_Abilities_Modifiers]] (StatModifier).