Docs: DR-052 SoD facing + underwater feel — build spec, session log, gotchas archive 07-16, skinned-kit cookbook recipe, CLAUDE.md facing contract (net-zero condensations)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 00:54:53 -07:00
parent 4fa2cb0de4
commit a083e77a9e
6 changed files with 207 additions and 9 deletions
+10 -9
View File
@@ -9,7 +9,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
- **Size check** — bash: `wc -c CLAUDE.md` · PowerShell: `(Get-Item CLAUDE.md).Length`. Must be `< 40960`.
- **Archive, don't delete.** When trimming, append the verbose / least-hot detail to the obsidian reference note `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md` under a **new dated heading** (never overwrite an older snapshot), and leave a one-line pointer + the relevant `[[DR-###]]` link here.
- **Net-zero rule:** every addition is paid for by a condensation elsewhere. Keep only the hottest, highest-recurrence operational rules inline (flag them **★**); depth lives in the archive + DRs.
- Condensation history (detail → archive dated headings): 06-17 · 07-04 · 07-04b · 07-04c · 07-06 · 07-07b (pays the attribute-swallow gotcha) · 07-07c (game-state narrative → invariants+DR pointers; ~3 KB freed, operator-ordered) · 07-13 (co-op-Hades core-loop bullet → LANTERN direction pointer; [[DR-048_Lantern_Adoption_Full_Pivot]]) · 07-15 (siege-era/automation/GoalProgress bullets retired with the DR-051 purge).
- Condensation history → archive dated headings: 06-17 · 07-04(ac) · 07-06 · 07-07(b,c) · 07-13 · 07-15 · 07-16 (what each paid is noted in the archive).
## Stack — Unity 6.5.1 (`6000.5.1f1`, stable) as of 2026-06-27
@@ -26,7 +26,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
| `com.unity.mathematics` | 1.4.0 | (transitive) |
| `com.rukhanka.animation` | **2.9.0** | Local pkg (`Packages/com.rukhanka.animation`). ECS skeletal animation (Burst CPU/GPU skinning). Resolves on 6.5.0 via SemVer floor. Netcode replication **OFF** → client-derived. See [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. |
Values match `packages-lock.json` (reconciled 2026-06-17; URP 17.5.0, test-framework 1.7.0, ugui 2.5.0, multiplayer.center 1.0.1; `com.unity.ai.assistant` REMOVED 07-04 — console noise). **History:** 6.6.0a6 transport bug — [[DR-002_Unity66_Alpha_Netcode_Transport]] + archive.
Values match `packages-lock.json` (reconciled 06-17; URP 17.5.0; `ai.assistant` removed 07-04). **History:** 6.6.0a6 transport bug — [[DR-002_Unity66_Alpha_Netcode_Transport]] + archive.
## Namespaces & assembly split
@@ -94,7 +94,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
### Presentation / juice / VFX
- **All juice/HUD = client-only observe-only `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire), never mutates the sim. Read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()` — NOT MonoBehaviour `LateUpdate` (job-safety throw). `Entity` = a stable client dict key per ghost lifetime — **prune the cache each frame** (a pruned ghost = a kill/loss → death VFX); **never `DestroyEntity` a ghost client-side** (`GhostDespawnSystem` owns despawn). Hit-stop = camera punch, **never `Time.timeScale`**.
- **Asset-free presentation:** procedural `AudioClip.Create` SFX; runtime `ParticleSystem` pool (Sprites/Default + HDR start color); code-built **UI Toolkit** HUD/menus. Edit a prefab asset's component in code via `PrefabUtility.LoadPrefabContents`modify**`SaveAsPrefabAsset(root, path)`** → `UnloadPrefabContents`. Watch **shared-material bleed** when re-tinting. ACES tonemapping needs URP color grading mode = HDR (`m_ColorGradingMode=1`).
- **Asset-free presentation:** procedural `AudioClip.Create` SFX; runtime `ParticleSystem` pool; code-built **UI Toolkit**. Prefab-asset edits: `LoadPrefabContents`modify`SaveAsPrefabAsset``Unload`. Watch shared-material bleed on re-tint; ACES needs URP grading mode HDR. Detail → archive 07-16.
- **Prototype glue lives in `ProjectM.Client` as MonoBehaviours:** `PrototypeCameraRig` (player-following ARPG cam), `VFXConfig` (static `Instance` + prefab fields bridging authored VFX to `CombatFeedbackSystem`; keep a procedural fallback). A **static presentation bridge must reset on play-enter** via `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` (statics survive fast-enter-playmode reloads → stale flash).
- **UITK HUD + menus ★:** `MenuUi` owns the palette/factories/`PanelSettings`/`EventSystem` plumbing; `HudSystem` = a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, root `pickingMode = Ignore`, tree built once `rootVisualElement != null`). **Runtime UITK needs `PanelSettings` WITH a `themeStyleSheet` AND an `EventSystem` + `InputSystemUIInputModule`** or buttons are silently dead. The build palette (lazy from the client `StructureCatalog`) drives click-to-place: green/red `BuildPreviewMath` ghost → `BuildPlaceRequest` RPC, right-click/Esc cancel, `[`/`]`/R rotate. See [[DR-021_HUD_UITK_BuildPalette]].
- **HUD skin = build-safe `HudTheme` SO of serialized sprite refs** (runtime `Resources.Load` by name is build-stripped); tint MULTIPLIES, never set `unitySlice*` on 9-slices → archive 2026-07-06 + [[DR-024_HUD_Synty_Skin_Theme]].
@@ -102,15 +102,15 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
### Art import (HDRP store packs → URP)
- BefourStudios HDRP art: convert via `EnvArtTools.cs`, never switch pipelines (HDRP breaks EG); Synty = URP-native → archive 2026-07-06.
- **World = the LANTERN murk ★ (DR-051; Synty biomes deleted):** ONE look — `PostFX_Lantern.asset` (ACES; needs URP HDR grading) + `Env_SeabedKit.prefab` (ArtStaging-sourced) + unified RenderSettings (NO skybox; Exp² teal fog {0.02,0.10,0.12}; flat ambient {0.03,0.055,0.08}; density knob 0.035 play / 0.075 staging; camera clearFlags **SolidColor deep-water** — else no-skybox corners bleed blue). `ScenePolicy.IsGameplayScene()` gates the dynamic-look systems — never re-add `scene.name` string checks. `WorldAtmosphereSystem` = water-column murk.
- **A dark-lit screenshot MASKS material bugs — verify material *values*.** `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture` (`S_General`'s `_BaseColorMultiply` is a float → `GetColor` returns black). Gate emission on the `_Emissive` flag + a fixture name; keep converted env metallic low (0.10.2).
- **An EG per-instance tint (`URPMaterialPropertyBaseColor`) darkens a ghost ONLY if the shader's `_BaseColor` is Hybrid-Per-Instance** (ShaderGraph `overrideHLSLDeclaration:true`+`hlslDeclarationOverride:2`). Enemies flash because `AnimatedLitShader` is DOTS-authored; a **stock Synty prop graph (`Generic_Basic`) is Unity-Per-Material → the override RENDERS but silently no-ops.** Check the graph before planning a per-instance tint on a baked/prop mesh; else use procedural decal quads (07-12 cover damage-cracks, Part P).
- **A dark-lit screenshot MASKS material bugs — verify material *values*** (`GetPropertyType`-guard before `GetColor`/`GetFloat`; detail → archive 07-16).
- **EG per-instance tint (`URPMaterialPropertyBaseColor`) works ONLY on a Hybrid-Per-Instance `_BaseColor` graph** (AnimatedLitShader yes; stock Synty `Generic_Basic` = Unity-Per-Material → renders but silently no-ops) — check the graph first; else procedural decals. Detail → archive 07-16.
- **VolumeProfile.Add persistence + the URP `m_AssetVersion` build blocker** → archive 2026-07-06 (+ native memory `urp-global-settings-version-blocks-build`).
- **`LocalTransform.FromPosition()` resets Scale=1** — server spawners read the prefab's baked `LocalTransform`, override only Position (Scale is a `[GhostField]` → consistent-but-wrong).
- **Static decor → gameplay subscene** (EG renders only baked entities); **strip colliders from cosmetic props** + no `GhostAuthoring` on scenery (classic-URP colliders are inert to the DOTS PhysicsWorld). **World collision = subscene-only ★:** `Environment`-layer boundary ring + landmark colliders (player blocked via the layer matrix); enemies slide via a server `CollisionWorld.SphereCast` in `EnemyAISystem`. **★ enemy slide has NO pathfinding — a near-vertical wall normal or an embedded spawn FROZE Husks on cover rocks (soft-locks a room on one leftover); fixed 07-07 via `EnemyMoveUtil.Depenetrate` + tangent-slide + an `EnemyNavState` nudge backstop. Re-validate movers aren't frozen when adding Environment cover. 07-10: the nudge is COVER-AWARE — a live destructible-cover ghost (`BlightClutter` carrier) SUPPRESSES the phase-through (breakable ⇒ no soft-lock); static wedges still nudge.** Boundary = `SM_Env_Rock_Cliff` rim. See [[2026-06-08_World_Collision_HUD_Scaling]].
- **A GA "projectile" prefab self-propels** — strip to particles before `Start` (`CombatFeedbackSystem.StripCosmetic`); verify *components*, not the name.
### Aim controls
- Client-derived aim on `PlayerInput.Aim`; scheme byte KBM=0/Gamepad=1; reticle re-raycasts inside `AimReticleSystem` → full detail: archive 2026-07-06 section + the source files.
### Aim / facing (SoD model — DR-052) ★
- **`PlayerFacing` is body-yaw ONLY** (moves→face movement; cast window→turn to aim; idle→hold; never passively track the cursor). **Every gameplay direction reads `FacingMath.ResolveAim(PlayerInput.Aim, facing)`** (all AbilityFireSystem archetypes + assist seed + melee cleave) and aim-readout presentation reads the SAME resolver; Movement-archetype sockets never open a cast window (`TickWindowMath`); PlayerAimSystem stays UN-gated (integrator over the snapshot-restored [GhostField]). Scheme byte KBM=0/Gamepad=1; KBM reticle re-raycasts. [[DR-052_SoD_Facing_Underwater_Feel]] + archive 2026-07-06.
### Animation (Rukhanka) ★
Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_Animation_MonsterMash]] · [[Synty_Asset_Inventory]]. Skeletal animation = **Rukhanka 2.9** (the only maintained Entities-native option on 6.4). **Netcode replication OFF** (`RUKHANKA_WITH_NETCODE` undefined) → **client-derived**: `PlayerAnimationDriveSystem` (client-only `SystemBase`, `[WorldSystemFilter(LocalSimulation|ClientSimulation)]` + `[UpdateBefore(RukhankaAnimationSystemGroup)]`) reads replicated state and writes params via `AnimatorParametersAspect`/`FastAnimatorParameter`. No new `[GhostField]`s; no `DefaultVariant` strip (define off → ghost hash unchanged).
@@ -121,6 +121,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_A
- **The server runs Rukhanka unless you strip it** — its **deformation** systems are `[WorldSystemFilter(Default)]` (⊇ ServerSimulation). **`ServerStripAnimationSystem`** (server-only one-shot) disables every `Rukhanka.Runtime` system on the server (group-disable cascades; matched by assembly name → no type ref). *Only Play-validation caught this.*
- **Build the controller via the `AnimatorController` API** (`manage_animation` drops enum/Vector blend-tree fields). **Skeleton-root = walk up from a bone to the soldier's direct child**, NOT `SkinnedMeshRenderer.rootBone` (the *bounds* root — head SMR's is `Spine_03` → destroys the lower skeleton).
- **The rig pipeline is HUMANOID** (muscle clips retarget onto player + monster rigs; the old "Generic" note was WRONG). **Blender clips:** per-action FBX (Key All Bones + Force Start/End + FBX Units Scale); import `CreateFromThisModel` (**CopyFromOther FAILS on Blender's extra `Armature` node**); bake root motion into pose. Optimize Game Objects **OFF**; root motion **OFF** (CC owns the transform).
- **★ Skinned ATTACHMENTS onto an existing rig (07-16 suit-kit recipe):** Blender rigid-skin (vgroup w=1 + armature modifier; **UNHIDE the armature before selection-export or the FBX is silently skinless**) → `PlayerRigTools.GraftSmr` REBASES on rebind (verts→rest-world; bindposes = inverse of **rigid scale-stripped** rest matrices — a Blender roundtrip imports cm bones/0.01 armature, raw bindpose reuse explodes ×100; **`RecalculateTangents()` or Rukhanka/BRG registration fails and the whole rig vanishes**). Hand-written deformation shaders: DOTS-instanced `_DeformedMeshIndex` block **BEFORE** the `ComputeDeformedVertex.hlsl` include + the property ALSO in the Properties block (the baker validates `HasProperty`). [[DR-052_SoD_Facing_Underwater_Feel]].
- **ENEMIES reuse the player pipeline** — a Husk = ownerless interpolated ghost = a remote player, so `EnemyAnimationDriveSystem` mirrors the REMOTE path (`LocalTransform` delta velocity + prevPos cache; `IsAttacking = AttackWindup != 0`). **Drop `[RequireMatchingQueriesForUpdate]`** so the prune runs every frame (else a cache entry leaks per kill). Build enemy prefabs via **`EnemyRigTools`**, GUID-preserving (`DeleteAsset+CopyAsset` orphans subscene refs); `WaveSystem` uses `baked.WithPosition` (not `FromPosition` → Scale reset). See [[DR-023_Enemy_Animation_MonsterMash]].
### MCP / editor workflow ★
@@ -128,7 +129,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_A
- **`apply_text_edits` with MULTIPLE non-adjacent edits in one call can MISALIGN** — one edit per call (or strict bottom-first), always with `precondition_sha256` (it returns the current SHA on mismatch). **★ One edit can SWALLOW an adjacent attribute/comment line** (07-06 `_portalMat` NRE · 07-07 `[RuntimeInitializeOnLoadMethod]` off `WorldFeelConfig.ResetDefaults` → feedback slice silently dead) — re-read neighbors after editing beside attributes; silent presentation slice → probe its config's `Enabled` in-play. **`create_script` won't overwrite**; full-file rewrites = whole-span `apply_text_edits` (its brace-balance validator guards botched spans) or `manage_script delete`+`create_script` (NON-GUID-referenced files only — systems/tests, never authoring MonoBehaviours). `script_apply_edits replace_method` is safe for class methods but **can't target a `struct : ISystem`**. [[DR-017_Persistent_Base_Player_Driven_Pacing]]
- **`execute_code` runs as a method body** — no `using` directives (parsed as statements); fully-qualify every type. Identify worlds by `world.Name == "ServerWorld"/"ClientWorld"` (flags overlap a shared `Game` bit).
- **`manage_gameobject create` / `manage_prefabs modify_contents` `component_properties` SILENTLY DROP enum + Vector3 fields** — set those via a follow-up `manage_components set_property` and VERIFY through `mcpforunity://scene/gameobject/{id}/component/{Type}` (or read the baked component in `execute_code` after Play). `manage_material set_renderer_color` uses a runtime PropertyBlock that does NOT persist into Play — create + assign a material asset instead.
- **New ghost prefab recipe:** `manage_asset duplicate` an existing correctly-configured ghost (e.g. `UpgradePickup.prefab`) → `manage_prefabs modify_contents` to swap the authoring MonoBehaviour (strip MeshFilter+MeshRenderer for an invisible state-holder) — its ownerless/interpolated `GhostAuthoringComponent` + `LinkedEntityGroupAuthoring` come free. **Runtime-spawn shared ghosts** via a one-shot server spawner (dodges the prespawn handshake); wire a baked spawner into the subscene via `manage_scene load additive``set_active_scene Gameplay` → create+verify → `save``close_scene`.
- **New ghost prefab recipe:** `manage_asset duplicate` a correctly-configured ghost (`UpgradePickup.prefab`) → swap the authoring MB (ownerless/interpolated `GhostAuthoring` + LEG come free). **Runtime-spawn shared ghosts** via a one-shot server spawner (dodges the prespawn handshake); wire baked spawners via `manage_scene load additive``set_active`→create→`save``close_scene`. Detail → archive 07-16.
- **An UNFOCUSED editor throttles Edit mode to near-idle** (MCP pings time out, bridge looks hung — it still queues; `telemetry_ping` succeeds) and stalls EditMode test INIT (pass `run_tests(init_timeout=120000)`, retry). `Application.runInBackground` only helps in **Play** mode. Prefer `refresh_unity scope=scripts` for code-only changes. Ask the operator to **focus Unity** for heavy build/test/Burst sessions.
- **Run an adversarial design-review Workflow (netcode/relevancy · determinism/prediction · reuse/scope → synthesize) BEFORE coding a netcode-heavy slice** — it has pre-caught relevancy traps, singleton collisions, dt-traps, double-destroys.
@@ -165,7 +166,7 @@ Full rules: `.claude/skills/dots-dev/references/dots-conventions.md` (in-repo; t
## Memory — three layers (which tool when)
Full protocol + per-layer detail: [[Documentation_Protocol]] (`Docs/Vault/_Meta/Documentation_Protocol.md`). The three layers: **in-repo vault** `Docs/Vault/` (design docs, DRs, session logs — committed) · **basic-memory** MCP (semantic/wikilink recall over the vault) · **native Claude memory** (`memory/`, `MEMORY.md` — machine-local). (serena REMOVED 2026-07-07 — unused in practice + flaky on Unity.)
Full protocol + per-layer detail: [[Documentation_Protocol]] (`Docs/Vault/_Meta/Documentation_Protocol.md`). The three layers: **in-repo vault** `Docs/Vault/` (design docs, DRs, session logs — committed) · **basic-memory** MCP (semantic/wikilink recall over the vault) · **native Claude memory** (`memory/`, `MEMORY.md` — machine-local). (serena removed 07-07.)
- Where is X / who calls it → **Grep/Glob**. What did we decide / how does Z work → **basic-memory** → read the vault note. Current DOTS API → **context7**. Conventions → this file. Long-form build lessons → the gotchas archive.
- **Cross-machine rule:** durable truth → the **vault** or **this file** (both committed); native `memory/` is local-only, never the sole home of a decision.