Import art/VFX asset packs + game-feel systems; normalize texture extensions to lowercase for LFS

Add BefourStudios SciFi environment packs, Gabriel Aguiar VFX, and the
ShaderCrew Toon Shader embedded packages, plus combat/enemy/wave/death
gameplay systems and supporting vault docs/screenshots.

Rename 11 vendor textures from uppercase .PNG/.HDR to lowercase so the
case-sensitive Git LFS filters (*.png/*.hdr) match on case-sensitive
filesystems (Linux CI, case-sensitive macOS), not just locally where
core.ignorecase=true masks the gap. Each .meta moved with its asset so
GUID references are preserved. All ~1000 binaries tracked via LFS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 22:50:43 -07:00
parent dd0064c377
commit e362aaeb43
4830 changed files with 1293057 additions and 38 deletions
@@ -0,0 +1,47 @@
---
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<TrainingDummyTag>()``.WithAny<TrainingDummyTag, EnemyTag>()` — 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.
@@ -0,0 +1,63 @@
---
date: 2026-06-02
type: session
tags: [session, dots, netcode, game-feel, identity, enemy-ai, presentation, hud, urp, m5]
permalink: gamevault/07-sessions/2026/2026-06-02-gamefeel-identity
---
# Session 2026-06-02 — "First Blood": game-feel & identity slice
## Goal
Before more systems work, **make the project feel like a game — in feel, look, and play — and give it an identity.** Diagnosis: M1M5 are systems-complete but identity-less — pillars are mechanical (no fiction), and there is **no feedback layer** (grep confirmed zero audio/particles/VFX/shake/UI), the only enemy is an inert dummy, and there's no death/respawn or HUD. Operator chose: identity = **sci-fi frontier colony**, first slice = the **full game-feel pass** ("First Blood"), proceed plan→approve→build. Locked in [[DR-009_GameFeel_Identity_FirstBlood]]; fiction in [[Identity]].
## Process
- **Explore + propose** (read-only): diagnosed the look/feel/play gap against [[Pillars]] + the M1M5 vault, surfaced the "mechanics without a fiction or a feedback layer" core, and proposed a spine (Identity) + 3 axes (Feel/Look/Play) with a recommended first slice. Operator picked the fiction + full slice via a 3-question gate.
- **context7 WAS reachable this session** (vs [[DR-008_M5_HomeBase_BaseLayer_Storage]]). A **4-agent ctx7 verification workflow** (Netcode / Entities-enableable / URP / DOTS→GameObject presentation) returned high-confidence verified API shapes + a key correction: **an interpolated enemy ghost must be moved server-only in the plain `SimulationSystemGroup`, NOT in the predicted loop**, and `Dead` is cleanest **derived** from replicated Health (no replicated enabled bit).
- **Implementation** (sequential via MCP `create_script`/`script_apply_edits` — single editor, domain-reload-ordered; NOT a parallel swarm) in 5 stages, each compiled + console-checked + (where applicable) EditMode-tested + runtime-validated before the next.
- **Adversarial review workflow** over the diff (12 agents: 4 dimensions → per-finding adversarial verification). It **verified the architecture correct** — the Husk is a properly interpolated/ownerless ghost (`HasOwner=0`, `DefaultGhostMode=Interpolated`), the derived `Dead` is rollback-safe (no GhostField), the local player's Health is never client-predicted, and presentation randomness is outside the sim — every netcode-desync suspicion came back **not-a-bug**. **3 confirmed low-severity findings**, all the same root: the `uint` cooldown "0 = ready" sentinel can collide at tick wraparound (`RespawnMath` already guarded it; the three cooldown writers + the HUD didn't). **Fixed** via a shared `TickUtil.NonZero` guard at the three writes (`EnemyAISystem`/`EnemySpawnSystem`/`AbilityFireSystem`) and a wrap-safe `NetworkTick` compare in the HUD cooldown bar.
## Done
**Stage 1 — Husk enemy** (`ProjectM.Simulation/Combat`, `ProjectM.Server/Combat`, `ProjectM.Authoring/Combat`): `EnemyTag`/`EnemyStats`/`EnemyAttackCooldown`/`EnemySpawner` + pure `EnemyAIMath` (SeekVelocity/InAttackRange/RingPosition); `EnemyAISystem` (server-only, plain `SimulationSystemGroup` `[UpdateBefore(PredictedSimulationSystemGroup)]` — seeks nearest living player, writes `LocalTransform`, appends `DamageEvent` on a `NetworkTick`-gated strike); `EnemySpawnSystem` (persistent server spawner, sustains `MaxAlive` on a deterministic ring around `BaseAnchor`); `HealthApplyDamageSystem` +`EnemyTag` death; `EnemyAuthoring` + `EnemySpawnerAuthoring`. `Enemy.prefab` duplicated from `UpgradePickup.prefab` (interpolated ownerless ghost) → `EnemyAuthoring`; `EnemySpawner` GameObject added to `Gameplay.unity`.
**Stage 2 — Player death/respawn** (`ProjectM.Simulation/Player`, `ProjectM.Server/Combat`): `Dead` (enableable, **derived** from Health) + `RespawnState` + pure `RespawnMath`; `PlayerDeathStateSystem` (both worlds, predicted, before movement/aim/fire — sets `Dead` + zeroes velocity); `PlayerRespawnSystem` (server-only, after the predicted group — schedule on death, refill+reposition on due). `PlayerControlSystem`/`PlayerAimSystem`/`AbilityFireSystem` gained `.WithDisabled<Dead>()`; `PlayerAuthoring` bakes `Dead` (disabled) + `RespawnState`.
**Stage 3 — Combat juice** (`ProjectM.Client/Presentation`): `CombatFeedbackSystem` (client-only managed `SystemBase`, `PresentationSystemGroup`) — edge-detects replicated `Health` → pooled billboard `TextMesh` damage numbers + runtime `ParticleSystem` hit/death/muzzle bursts + **procedural** `AudioClip` SFX + camera shake; Husk death via despawn-prune; local-fire muzzle via `AbilityCooldown` edge. `PrototypeCameraRig` +`AddShake` (presentation-only decayed shake).
**Stage 4 — HUD** (`ProjectM.Client/Presentation`): `HudSystem` (code-built uGUI overlay — `RawImage` health + cooldown bars over `Texture2D.whiteTexture`, legacy `Text` threat count + DOWNED/RESPAWNING overlay) driven from the local player ghost. `ProjectM.Client.asmdef` +`UnityEngine.UI`.
**Stage 5 — Look** (assets/editor): URP global `Volume` (Bloom/ACES Tonemapping/ColorAdjustments/Vignette) + dark flat ambient + exp fog + cool key light + URP HDR color grading; emissive re-tint (teal crew, cyan projectiles, orange Husks via new `M_Husk`, dark-metal walls/ground). Husk balance tuned (dmg 8→5, strike 36→48 ticks, speed 3.5→3.0).
**Docs:** [[Identity]] (the fiction) + linked from [[Pillars]]/Home; [[DR-009_GameFeel_Identity_FirstBlood]].
## Validation
- **EditMode 74/74 green** (+12 vs M5's 62 — 7 `EnemyAIMath`, 5 `RespawnMath`). Console clean of code/Burst/ghost errors after every stage.
- **Runtime (single in-editor client, 6.4.7):** Husks **spawn (6) + replicate server↔client (6==6)**, **chase** from the ring to the player, **deal contact damage** (player → 0 HP). **Death→respawn loop verified** by polling: `Health=0`/`Dead=True`/`RespawnTick` scheduled → recovered at base full-HP/`Dead=False`. **HUD renders** (health bar, "HUSKS N", DOWNED overlay). **Look confirmed** via screenshots: dark metal world + cyan crew + orange Husks + bloom (after fixing the `M_Dummy`-as-wall-material orange-walls bug).
- **Adversarial review (12 agents):** architecture verified correct; 3 confirmed low-severity findings (the `uint` cooldown 0-sentinel wraparound), all fixed (`TickUtil.NonZero` + HUD `NetworkTick` compare). **EditMode 74/74 after fixes**, console clean.
## Diagnosis notes (for future me)
- **Interpolated ghost ≠ predicted.** Move an ownerless interpolated enemy ghost **server-only in plain `SimulationSystemGroup`** (`[WorldSystemFilter(ServerSimulation)]`), NOT in `PredictedSimulationSystemGroup`. `[UpdateAfter(PredictedSimulationSystemGroup)]` (NOT `[UpdateBefore]` — the predicted group is OrderFirst, so UpdateBefore is silently ignored; corrected in the deepening pass) so a contact `DamageEvent` drains the following tick (~16ms, fine for melee). Stock `LocalTransform` replication carries its position — no hand-written `[GhostField]`.
- **Derive enableable state, don't replicate it, when it's a pure function of already-replicated data.** `Dead = Health<=0` derived every predicted tick in both worlds is rollback-correct and dodges the `[GhostEnabledBit]` surface — same idiom as `EffectiveCharacterStats`. To toggle a disabled enableable component you must still visit the entity: `.WithPresent<Dead>()` (write) vs `.WithDisabled<Dead>()` (alive-only run). `Simulate` (also enableable) ANDs independently — keep `.WithAll<Simulate>()`.
- **A Baker can bake an enableable component DISABLED** (`AddComponent<Dead>(e); SetComponentEnabled<Dead>(e, false);`) → instances spawn alive with zero first-tick work (instantiated entities inherit the prefab's enabled state).
- **Presentation belongs in a client-only managed `SystemBase` in `PresentationSystemGroup`** (once per frame, no rollback double-fire). Read ECS state via `SystemAPI.Query` INSIDE `OnUpdate` (registers job deps) + `EntityManager.CompleteDependencyBeforeRO<T>()` — NOT from a MonoBehaviour `LateUpdate` (the job-safety exception the camera rig hit). `Entity` is a stable client dict key for a ghost's lifetime; **prune the cache each frame** (a pruned enemy = a kill → death VFX at its last position; never `DestroyEntity` a ghost from the client — `GhostDespawnSystem` owns that).
- **Netcode-safe "hit-stop" = a camera punch, never `Time.timeScale`** (which would corrupt the deterministic sim).
- **Code-built uGUI with no assets:** `RawImage` over `Texture2D.whiteTexture` (anchor-driven fill, no sprite) + legacy `Text` with `Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf")`. Procedural `AudioClip.Create` + runtime `ParticleSystem` (Sprites/Default material, HDR start color to bloom) keep the slice asset-free.
- **Watch shared-material bleed when re-tinting:** the walls glowed orange because `M_Dummy` doubled as the wall material — gave Husks their own `M_Husk` instead. Editing a prefab asset's component in code needs `PrefabUtility.LoadPrefabContents` → modify → **`SaveAsPrefabAsset`** (NOT `SavePrefabAsset`, which rejects the contents root) → `UnloadPrefabContents`.
- **ACES tonemapping needs the URP asset's color grading mode = HDR** (`m_ColorGradingMode = 1`), else HDR bloom/emission won't tonemap right.
- **Editor `is_focused` matters again:** play-enter domain reload + boot took ~1015s while unfocused; entity inspection via `execute_code` had to be retried until worlds booted. (`Application.runInBackground` keeps Play ticking but Edit mode still throttles unfocused.)
## Open / deferred
- Generalize `AbilityFireSystem` auto-target from `TrainingDummyTag` to `EnemyTag` (Husks are hittable via raw-aim swept hit; only the aim-assist snap is missing).
- Respawn safety (brief invuln / Husk push-back) so respawn isn't instant re-death.
- Real assets (audio, stylized crew/structure models, biomech Husks); SSAO renderer feature.
- Enemy variety + a wave/threat director (ranged Husk, brute, broodmaker).
- Multi-client validation of remote-player death VFX + per-client HUD.
## Next
Either **deepen the loop** (enemy variety + wave director + respawn safety + auto-target on Husks) to make the combat sustain, or resume the milestone track at **M6 — server-authoritative grid build placement** ([[DR-008_M5_HomeBase_BaseLayer_Storage]]). The presentation + server-AI patterns from this slice are the reusable foundation for the former.