--- date: 2026-06-02 type: session tags: [session, dots, netcode, enemy-ai, wave-director, game-feel, m5] permalink: gamevault/07-sessions/2026/2026-06-02-gamefeel-deepening --- # Session 2026-06-02 — Game-feel deepening (waves, variety, invuln, auto-target) ## Goal Continue the [[2026-06-02_GameFeel_Identity|First Blood]] slice's suggested next steps — turn the flat Husk swarm into a **sustaining combat loop**: (1) **auto-target on Husks**, (2) **respawn safety (invulnerability)**, (3) **enemy variety + a wave/threat director**. Same constraints (server-authoritative, deterministic, ctx7-verified, adversarially reviewed). Decisions extend [[DR-009_GameFeel_Identity_FirstBlood]]. ## Done - **Auto-target on Husks:** `AbilityFireSystem` soft auto-target candidate query `.WithAll()` → `.WithAny()` — Husks are now aim-assist targets (they were already raw-aim hittable). - **Respawn invulnerability:** new replicated `RespawnInvuln { [GhostField] uint UntilTick }` + `RespawnState.InvulnTicks`. `PlayerRespawnSystem` (server) sets the window on recovery; `HealthApplyDamageSystem` (server, predicted group) nullifies player damage while active; `HudSystem` (client) shows a cyan health bar + "SHIELDED". Stops instant re-death into the swarm. - **Wave/threat director:** `WaveDirector` (baked config) + `WaveEnemyPrefab` (buffer pool) + `WaveState` (runtime singleton) + `WaveSystem` (server state machine): **Lull → Spawning `BaseCount + (wave-1)*CountPerWave` Husks, one every `SpawnIntervalTicks` at a deterministic ring, round-robin over the prefab pool → wait for the field clear (no `EnemyTag`) → Lull(`LullTicks`)**. Replaced the flat `EnemySpawner` sustain (deleted `EnemySpawner`/`EnemySpawnSystem`/`EnemySpawnerAuthoring`). Tick gating uses `TickUtil.NonZero` + wrap-safe `NetworkTick`. - **Husk variety:** 3 interpolated-ghost variants spawned round-robin — **Grunt** (`Enemy`, 30 HP), **Swarmer** (`EnemySwarmer`, 15 HP, fast/weak, small), **Brute** (`EnemyBrute`, 80 HP, slow/tanky/hard-hitting, large) — each = different baked `EnemyStats` + its own emissive material (`M_HuskSwarmer` yellow, `M_HuskBrute` deep-red) + scale. Subscene `EnemySpawner` GameObject swapped to `WaveDirector` (wired to all three). - **Cleanup (from runtime):** `EnemyAISystem` `[UpdateBefore(PredictedSimulationSystemGroup)]` → `[UpdateAfter(...)]` — the predicted group is **OrderFirst**, so `UpdateBefore` was silently **ignored** (console warning); contact damage now intentionally drains the **following** tick (~16ms melee latency, accepted). Removed a spurious `ParticleSystem.duration` set (warning). ## Validation - **EditMode 74/74 green** (no new pure-math helpers; wave logic is stateful/integration-validated at runtime). Console clean of compile errors. - **Runtime (single in-editor client, 6.4.7):** - **Wave director:** wave 1 spawned **4 Husks** (BaseCount), replicated server↔client (4==4); cleared the field → **Lull** → **wave 2 escalated to 6 Husks** (BaseCount + CountPerWave). Full spawn→clear→lull→bigger-wave cycle confirmed. - **Variety:** distinct baked `maxHP` **30 / 15 / 80** among live Husks (Grunt/Swarmer/Brute), round-robin from the pool; distinct materials + scales confirmed by screenshot (yellow Swarmers, orange Grunts, a large red Brute). - **Invulnerability:** `RespawnInvuln.UntilTick` set on the player's respawn (server-authoritative, replicated for the HUD cue). - **Adversarial review (7 agents):** verified the wave director, variety, invuln replication, and auto-target **correct** — the "wave deadlock", "escalation overflow", and "0-sentinel conflation" suspicions all came back **not-a-bug** (the empty-server stall self-heals on respawn + Husks teleport-seek so can't wall-stick; overflow needs wave ~1 billion; writes are `TickUtil.NonZero`-guarded). **1 confirmed low-severity finding:** `RespawnMath.IsDue` used a raw `now >= respawnTick` compare — the lone wrap-safety holdout (the slice's tick *writes* were guarded by `TickUtil.NonZero`, but this read-side *compare* was missed). **Fixed** with a wrap-safe signed-delta `(int)(now - respawnTick) >= 0` (kept pure-uint, so the 5 `RespawnMath` unit tests stay valid). EditMode 74/74 after the fix. ## Diagnosis notes (for future me) - **`[UpdateBefore(PredictedSimulationSystemGroup)]` is IGNORED** — the predicted group is **OrderFirst** in `SimulationSystemGroup`, and OrderFirst/OrderLast outrank `UpdateBefore`/`After` (Unity logs "Ignoring invalid UpdateBefore… because OrderFirst/OrderLast has higher precedence"). A plain-`SimulationSystemGroup` server system therefore always runs **after** the predicted group; to read this tick's post-predicted state use `[UpdateAfter(PredictedSimulationSystemGroup)]` (and accept that anything it appends for the predicted group drains next tick). This corrects the "same-tick" claim in [[DR-009_GameFeel_Identity_FirstBlood]] — `EnemyAISystem` contact damage is a 1-tick-late (~16ms, imperceptible for melee) event, NOT same-tick. The runtime warning caught what the first review missed. - **Wave state machine — avoid the deadlock:** only re-enter Lull when `RemainingToSpawn==0` AND the field is clear (`EnemyTag` count 0). The headless (input-less) player can't kill Husks, so the field never clears and the cycle stalls in Spawning — that's a test-harness artifact, not a bug (a real player clears it; validated by killing Husks via `execute_code` → the director advanced to wave 2). - **Editing a subscene authoring component in code:** open the subscene additively, `GameObject.GetComponent`/`DestroyImmediate` the old authoring + `AddComponent` the new + set fields (incl. `GameObject[]` prefab arrays, which the MCP property setter handles awkwardly) → `EditorSceneManager.MarkSceneDirty`+`SaveScene` → close. `AssetDatabase.DeleteAsset` is blocked by `execute_code` safety checks — guard duplicate-creates with a `LoadAssetAtPath != null` check instead. - **Server tick-batching warnings in-editor** are the known perf artifact (multiple worlds in one process + presentation + earlier unfocused throttle), not a code bug — reliable server state (wave director, RPCs) is robust to it; only one-shot predicted `InputEvent`s drop under batching. ## Open / deferred - **Ranged Husk (Spitter):** a server-spawned enemy projectile subsystem for tactical (dodge) depth — the next variety step (melee-only today). - **Wave number on the HUD:** needs a replicated game-state ghost (`WaveState` is server-only); the live "HUSKS N" count conveys threat for now. - **Boss / mini-boss + per-wave composition weighting** (brutes later, swarms early) instead of flat round-robin. - **Server performance:** investigate the in-editor tick-batching under the wave load (Burst cache health, world count); validate in a real build. ## Next Add the **ranged Spitter** (enemy projectile) for combat depth, or resume the milestone track at **M6 — server-authoritative grid build placement** ([[DR-008_M5_HomeBase_BaseLayer_Storage]]). The wave director + variety pool make adding new enemy types data-cheap.