Docs: 2026-08-13 — Track A (camera) shipped alongside Track B

Session log gains Part 3: the operator picked "drop" on the hold fork, and both
halves of the camera fix landed. Records the measurement that forced the second
half — dropping the hold ALONE was worse (max deviation 0.653 vs 0.447) because the
hold had been masking the shake-integration bug — plus the before/after table and
the PixelArtDevControls opt-in.

Gotchas archive gains four more entries: never let a transient offset live in the
value your smoothing filter reads back; a frame-counted hold is a framerate-
dependent freeze; a masking fix can make the metric worse before better, so
re-measure the intermediate state; HideFlags.DontSave objects survive play-mode
exit and leak one per domain reload.

CLAUDE.md gains a Camera-feel ★ line, paid for by trimming the MCP-edit, swept-hit,
LANTERN-direction, harvest, bootstrap and presentation bullets. Now 39936 bytes —
exactly the >=1 KB headroom target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 23:09:53 -07:00
parent 150f30f56c
commit 5dced78de2
3 changed files with 85 additions and 23 deletions
+11 -10
View File
@@ -83,22 +83,23 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
- **The player is a Unity Character Controller kinematic character** (NOT a dynamic Rigidbody; M5's `PlayerMoveSystem`/`PlayerPlanarConstraintSystem` deleted, predicted-physics infra kept). `PlayerControlSystem` maps input → `CharacterControl`; `CharacterProcessor` collide-and-slides in the relocated `KinematicCharacterPhysicsUpdateGroup`. CC 1.4.2 API = `IKinematicCharacterProcessor<T>` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*` (verify with `unity_reflect`).
- **`KinematicCharacterUtilities.BakeCharacter` aborts with a `Rigidbody`** and needs uniform (1,1,1) scale. **`CharacterInterpolation` must be PredictedClient-only** (a `DefaultVariantSystemBase` strips it from server + interpolated prefabs) — else double-interp on remotes. **Do NOT copy the CC sample's global `LocalTransform → DontSerializeVariant`** (project-wide; breaks non-character ghosts that rely on stock `LocalTransform` replication).
- **Top-down CC config:** `SnapToGround=false`, `InterpolateRotation=false` (rotation owned by `PlayerAimSystem`), `SimulateDynamicBody=false`; gravity handled by feeding `float3.zero` to `Update_GroundPushing`.
- **Hit/area tests must be SWEPT, not point checks** — a point check tunnels when the per-tick step exceeds the target radius (high speed *or* tick-batching); test the segment traversed this tick. **In a PLAIN `SimulationSystemGroup` system do NOT use `SystemAPI.Time.DeltaTime`** (wall-frame delta, not the fixed step) — store the per-tick step on the projectile (`Projectile.LastStep`, written in the fixed-step group) and rebuild the segment as `cur - dir*LastStep`. `ecb.DestroyEntity` **at-most-once** per tick (destroyed-bitset; double destroy throws at Playback). **TWO target types in one pass: UNIFY into one best-target loop + one shared bitset** (separate sweeps double-destroy a projectile overlapping both — DR-018). **A per-hit yield `(int)` cast that also gates despawn is an immortal-sink** (sub-1.0→0→no deposit, shot still consumed): guard `math.max(1,(int)yield)` + `[Min(1f)]` authoring.
- **Hit/area tests must be SWEPT, not point checks** — a point check tunnels when the per-tick step exceeds the target radius; test the segment traversed this tick. **In a PLAIN `SimulationSystemGroup` system do NOT use `SystemAPI.Time.DeltaTime`** (wall-frame delta, not the fixed step) — store the per-tick step on the projectile (`Projectile.LastStep`, written in the fixed-step group) and rebuild the segment as `cur - dir*LastStep`. `ecb.DestroyEntity` **at-most-once** per tick (destroyed-bitset; double destroy throws at Playback). **TWO target types in one pass: UNIFY into one best-target loop + one shared bitset** (DR-018). **A per-hit yield `(int)` cast that also gates despawn is an immortal-sink**: guard `math.max(1,(int)yield)` + `[Min(1f)]` authoring.
### Build / structures / grid
- **Grid math** (`BaseGridMath`, still live for spawn rings/respawn/lights): corner-origin, center-returning, **half-open** cell bounds, `math.floor`; lock cell size as a coordinate space once. Structures/placement themselves are deleted — recipe + atomicity rules in [[DR-014_M6_Build_Structures_Automation_Foundation]] if buildables return.
- **Ledger spends:** afford→act else SOFT-FAIL (no cooldown-burn), read LIVE in-loop (no hoist); a Health-less entity silently drops OUT of an aggro snapshot (snapshot ABOVE the early-return).
- **DR-051 purge (07-15) ★:** the dead direction was **DELETED** (git = the archive; enumerated in the DR). **Retired byte VALUES stay reserved, never renumbered** (`StructureType` 1-4, `ResourceId.Charge`, `DebugOp` 3/10/11, `TuningKnob` 20-23); `DebugOp.SpawnWave`/`EndSiege` RE-MEANT (force-wave / quiet-arena). **Waves UNGATED** — a baked `WaveDirectorAuthoring` decides by placement. Sockets are THE ability model (frame loadout seeded unconditionally at spawn) — **the Spark defs must be in EVERY gameplay subscene's `AbilityDatabaseAuthoring`; a socket whose SparkId is missing from the baked blob silently reads Damage/Range/Cooldown 0** (audit H2, live-proven). `FrameKind` = Bathynaut(2)/Harpooner(3); `PlayerClass` is gone (FrameId is the single frame identity). [[DR-051_Lantern_Realignment_Purge]] · **2026-08-07 audit purge deleted the whole base/expedition shell**: [[DR-054_Audit_Purge_2026-08]].
- **Harvest is single-sink** (→ the shared ledger, via `HarvestMath.DepositYield`). The personal-bag/equipment layer was deleted 2026-08-07; reintroduce LANTERN's carried-vs-banked split *inside HarvestMath*, not at its two call sites.
- **Harvest is single-sink** (→ the shared ledger, via `HarvestMath.DepositYield`; the personal-bag layer died 08-07). Reintroduce LANTERN's carried-vs-banked split *inside HarvestMath*, not at its two call sites.
- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned) ★:** **born-correct load**`CycleDirectorSpawnSystem` (now the ledger host only) applies the menu-staged `PendingSave` AT SPAWN. **v7 = a FRESH EPOCH: `MinLoadableVersion = CurrentVersion = 7`**; additive going forward — the save now carries only the ledger (structure/meta fields persist empty so v7 files still load). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
### 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; code-built **UI Toolkit**. Prefab-asset edits: `LoadPrefabContents`→modify→`SaveAsPrefabAsset``Unload`. Watch shared-material bleed on re-tint. 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. See [[DR-021_HUD_UITK_BuildPalette]] (its build-palette half died with DR-051).
- **Pooling + per-frame cost ★:** a per-frame value-CHANGE gate must derive its rendered text FROM the quantised key (`Mathf.RoundToInt` = half-to-EVEN vs `ToString("0.0")` = half-away → the label latches stale). A pooled `AudioSource``PlayClipAtPoint`: set `spatialBlend=1` (default is 2D), `dopplerLevel=0` (pooled voices teleport), root `DontDestroyOnLoad` + a `SubsystemRegistration` reset. `AudioClip.Create` clips aren't owned by the FX root — `FeedbackFx.DestroyClip`. **`GC.GetTotalMemory` quantises to 4 KB; attribute with `ProfilerRecorder("GC Allocated In Frame")`.** → archive 2026-08-13.
- **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]].
- **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. Detail → archive 07-16.
- **Prototype glue lives in `ProjectM.Client` as MonoBehaviours:** `PrototypeCameraRig` (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; a `HideFlags.DontSave` object survives play EXIT → leaks one per reload).
- **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. See [[DR-021_HUD_UITK_BuildPalette]] (build-palette half died with DR-051).
- **Camera feel ★:** shake/punch is a TRANSIENT offset — never let the follow filter read it back (`Lerp(transform.position, …)` integrates it as real error → the cam wanders); smooth a `_basePos` the offset never touches. No frame-counted holds (framerate-dependent); the position-hold hit-stop was DROPPED 08-13 (impact = FOV + shake).
- **Pooling + per-frame cost ★:** a per-frame value-CHANGE gate must derive its text FROM the quantised key (`Mathf.RoundToInt` = half-to-EVEN vs `ToString("0.0")` = half-away → the label latches stale). A pooled `AudioSource``PlayClipAtPoint`: `spatialBlend=1` (default is 2D), `dopplerLevel=0` (voices teleport), root `DontDestroyOnLoad` + `SubsystemRegistration` reset. `AudioClip.Create` clips aren't FX-root-owned — `FeedbackFx.DestroyClip`. **`GC.GetTotalMemory` quantises to 4 KB — attribute with `ProfilerRecorder("GC Allocated In Frame")`.** → archive 08-13.
- **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 → [[DR-024_HUD_Synty_Skin_Theme]].
### Art import (HDRP store packs → URP)
- Synty = URP-native. (BefourStudios HDRP pack deleted 2026-08-07 — 4 reachable textures kept in `_Project/Textures/Env`.)
@@ -126,7 +127,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_A
### MCP / editor workflow ★
- **Edit Assets `.cs` ONLY via MCP `apply_text_edits` / `create_script`** (Unity's scripting pipeline) — the raw `Write` tool does NOT reliably trigger a recompile on an unfocused editor → tests/`execute_code` run a **stale assembly**; a raw-`Write`-created NEW `.cs` gets **no `.meta` / no test-discovery** until `refresh_unity scope=all mode=force`. (`Write`/`Edit` are fine for non-asset files: this vault, asmdef JSON, etc.) `script_apply_edits` **`anchor_replace`** (regex) + **`delete_method`** work even on a `struct : ISystem`.
- **`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]]
- **`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 — ALWAYS re-read neighbors after editing** (2 shipped bugs → archive 07-06/07-07); a silently-dead presentation slice → probe its config's `Enabled` in-play. **`create_script` won't overwrite**; full-file rewrites = whole-span `apply_text_edits` or `manage_script delete`+`create_script` (NON-GUID-referenced files only). `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` 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.
@@ -137,9 +138,9 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_A
## Bootstrap & worlds
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + per-world ConnectionControlSystems). **Editor default = instant-into-game + MPPM** (creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`)); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + per-world ConnectionControlSystems). **Editor default = instant-into-game + MPPM** (creates `ServerWorld`/`ClientWorld`); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
- **Scenes (the DR-051 contract — exactly these four):** `MainMenu.unity` (build 0, UITK frontend) · `Game.unity` (build 1, the seabed arena; subscene `Gameplay.unity`) · `DevSandbox.unity` (renamed from Gym; dev tooling + subscene `GymSub.unity`; the `DebugOverlay`/F1-F2 dev scripts gate on this scene NAME) · `ArtStaging.unity` (art viewing, no player; the look's source of truth). All share the LANTERN look (see World bullet). The on-demand lifecycle (`WorldLauncher`/`SessionRunner`/`MainMenuController`) creates the right worlds per menu choice (Single/Host/Join), THEN `LoadScene(Game)` (subscene-streaming rule above).
- **Direction = LANTERN ★ — pivot LOCKED 2026-07-13 ([[DR-048_Lantern_Adoption_Full_Pivot]]).** Co-op action-RPG, *light is territory* (seed-pinned pocket-graph; SoD manual-aim skillshots; suit-frames + Sparks + wild mutations). Operative roadmap [[Roadmap_Lantern_Slice]]; surviving code is **QUARRY, not foundation** ([[Lantern_Strip_Mothball_Inventory]]). World-model review **PASSED** ([[DR-049_Lantern_World_Model_Design]]) → build against [[World_Model_Build_Spec]], **re-anchoring it first** (its step 1 assumes SaveData `MinLoadableVersion` < 7, which DR-051 shipped past; its RegionTag→PocketTag anchors moved). Engine fork **PARKED — Unity stays** ([[DR-053_Engine_Fork_Bevy_Parked]], operator 08-07). The co-op-Hades core loop was DELETED 08-07 (audit H1); git is the archive. ★ **a serialized prefab/component value ignores the C# initializer — change it on the instance.**
- **Direction = LANTERN ★ — pivot LOCKED 2026-07-13 ([[DR-048_Lantern_Adoption_Full_Pivot]]).** Co-op action-RPG, *light is territory* (seed-pinned pocket-graph; SoD manual-aim skillshots; suit-frames + Sparks + wild mutations). Operative roadmap [[Roadmap_Lantern_Slice]]; surviving code is **QUARRY, not foundation** ([[Lantern_Strip_Mothball_Inventory]]). World-model review **PASSED** ([[DR-049_Lantern_World_Model_Design]]) → build against [[World_Model_Build_Spec]], **re-anchoring it first** (its SaveData/RegionTag anchors moved past DR-051). Engine fork **PARKED — Unity stays** ([[DR-053_Engine_Fork_Bevy_Parked]]). ★ **a serialized prefab/component value ignores the C# initializer — change it on the instance.**
## DOTS / ECS conventions (authoritative summary)