diff --git a/CLAUDE.md b/CLAUDE.md index ed3787833..0c88a33e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` + `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()` — 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) diff --git a/Docs/Vault/07_Sessions/2026/2026-08-13_Combat_Freeze_Diagnosis_Track_B_Allocation.md b/Docs/Vault/07_Sessions/2026/2026-08-13_Combat_Freeze_Diagnosis_Track_B_Allocation.md index 7a5ee6a12..a1c57ba08 100644 --- a/Docs/Vault/07_Sessions/2026/2026-08-13_Combat_Freeze_Diagnosis_Track_B_Allocation.md +++ b/Docs/Vault/07_Sessions/2026/2026-08-13_Combat_Freeze_Diagnosis_Track_B_Allocation.md @@ -52,8 +52,9 @@ Three root causes, all in `PrototypeCameraRig.LateUpdate`: Plus a **one-off 41 ms spike on the first kill** (+44 KB) — first-use warmup of the death VFX/audio path; later kills cost ~15 ms. -A full remediation plan was presented (Track A camera, Track B allocation, Track C feel). **The operator chose -Track B.** Track A and C remain open — see *Next session*. +A full remediation plan was presented (Track A camera, Track B allocation, Track C feel). The operator chose +**Track B** first, then — on the hold fork — chose **drop**, and Track A shipped in the same session (Part 3). +Track C remains open. ## Part 2 — Track B shipped @@ -175,20 +176,53 @@ occurred.* owners. 3–5. Two duplicated comment blocks left by structured edits, and a doc-accuracy nit on the ring-size rationale. +## Part 3 — Track A shipped (the camera), + the dev-tool contaminant + +Operator resolved the fork: **drop the position hold**, impact rides the FOV punch + shake. + +**A2 — the hold is gone.** `PrototypeCameraRig.Hold`/`s_holdFrames`, `CombatFeedbackSystem.TryHold` and its four +call sites, and the `HitStopMaxFrames` / `HitStopFreezeEnabled` / `FinisherHoldFrames` knobs all retired; the +three saved feel profiles drop the dead keys (`FeelProfileService` skips unknown keys with a warning, so this +was safe either way — verified before removing). + +**A1 — the shake channel, which the measurement forced.** Dropping the hold *alone* measured **worse** (max +deviation 0.653 vs 0.447): the hold had been partly masking the real bug by pinning the camera during the +frames shake was loudest. So A1 landed too — the follow now smooths a `_basePos` that shake never touches, and +shake is applied only when writing the transform. + +| | max per-frame step | max deviation | mean deviation | +|---|---|---|---| +| Original | 0.283 | 0.447 (still 0.199 at +24 frames) | — | +| Hold dropped only | 0.429 | 0.653 | — | +| **Both fixes** | 0.428 (the shake impulse itself) | **0.329, back to 0.000 within a few frames** | **0.038** | + +Deviation at f42/f45/f50 after the f40 kill is now literally `0.000` (was 0.269/0.429/0.399). Shake reads as a +crisp transient punch instead of a drift the follow filter spends half a second digesting. + +**`PixelArtDevControls` is now opt-in.** Gated behind an EditorPrefs toggle (checked menu item, mirroring +`ProjectM/Boot Into Menu (Editor)`), off by default. Extra find: the object carries `HideFlags.DontSave`, so it +**survives play-mode exit** while the static `_instance` does not — the old code leaked one instance per domain +reload, and two live strays were cleared out of the editor. + ## Notes / loose ends -- **`Assets/_Project/Shaders/PixelOutline.mat` is modified in the working tree and is NOT mine to commit.** - `PixelArtDevControls` writes straight to the shared material asset, and `_MasterEnabled` flipped 0→1 with - posterize/normal-edges off during the Play sessions. Left untouched in case the toggle was deliberate; - excluded from the Track B commits. It is a real look change — decide before committing. -- **CLAUDE.md is at 39 850 / 40 960 bytes.** A condensation pass is due; this session's long-form lessons went - to the gotchas archive rather than inline. +- `Assets/_Project/Shaders/PixelOutline.mat` was dirtied by `PixelArtDevControls` writing to the shared material + asset during the Play sessions (`_MasterEnabled` 0→1). **Reverted on the operator's instruction** — it was not + an intentional change. The dev tool is now opt-in, so it cannot recur silently. +- **CLAUDE.md is at 39 936 / 40 960 bytes** — exactly the ≥1 KB-headroom target. Two ★ rules were added (camera + feel; pooling + per-frame cost) and paid for under the net-zero rule by retiring the DR-051 build-palette text + (that code died with the purge), the two purge enumerations now carried by their DRs, and prose trims across + the MCP-edit, swept-hit, LANTERN-direction and presentation bullets. It is still tight — a dedicated + condensation pass would buy room for the next few sessions. ## Next session -1. **Track A — the camera** (this is what the operator's report actually describes, and it is still unfixed): - split the base/shake channels, make the hold time-based and shake-safe, de-stack the lethal frame, prewarm - the first-kill VFX. Open fork: keep the camera hold at all, or drop it and carry impact on FOV + shake. -2. **Track C — feel**: enemies have **no collider** (you walk through them); FOV pumps on every hit; the 0.9 s - corpse window wants re-judging once the camera is fixed. +1. **Track A leftovers** (the two smaller items from the original plan, not yet done): **de-stack the lethal + frame** — the hit package and the kill package still both fire on the kill (measured shake 0.10+0.20 stacking, + two FOV punches, two SFX, four bursts) — and **prewarm** the first-kill VFX/audio path to kill the one-off + 41 ms spike. +2. **Track C — feel**: enemies have **no collider** (you walk straight through them, which is the biggest + remaining feel gap); FOV pumps on every hit; the 0.9 s corpse window wants re-judging now the camera is calm. 3. Measure allocation in a **player build** to get a number free of editor contamination. +4. **Eyes-on the camera.** The numbers say it settles; whether the impact still reads as *impact* with the hold + gone is a feel call only the operator can make. diff --git a/Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md b/Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md index 30d0a43e1..834773bc7 100644 --- a/Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md +++ b/Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md @@ -616,3 +616,30 @@ this session (net-zero rule): the six items below stay here, one condensed point 9. **Injecting a kill: append a lethal `DamageEvent`, never write `Health.Current = 0`.** `HealthApplyDamageSystem` early-`continue`s on an empty `DamageEvent` buffer, so a direct Health write is never seen by the death branch and no `Dying` is ever stamped. + +### 2026-08-13b — camera feel (Track A, same session) + +10. **★ Never let a transient offset (shake, recoil, punch) live in the same value your smoothing filter reads + back.** `Lerp(transform.position, desired, k)` where `transform.position` already carries last frame's shake + makes the filter treat shake as real positional error and INTEGRATE it — at `FollowSharpness 8` / ~12 ms + that is ~9 %/frame, so it washes out over half a second while new shake piles on. Measured on a stationary + player (ideal motion = 0): one kill left the camera 0.447 units off-frame, still 0.199 off 24 frames later. + Fix: smooth a private `_basePos` the offset never touches; add the offset only when writing the transform. + After: deviation returns to 0.000 within a few frames, mean 0.038. + +11. **A frame-counted hold is a framerate-dependent freeze.** `FinisherHoldFrames = 7` was documented as + "~117 ms" — true only at 60 fps; it is 49 ms at 144 and 233 ms at 30. Anything that gates on *feel duration* + must be time-based. Worse, the hold branch took its base from `transform.position`, permanently baking that + frame's shake in (a measured 0.283-unit single-frame jump). + +12. **★ A masking fix can make the metric WORSE before it makes it better — measure the intermediate state.** + Dropping the camera hold alone measured worse than leaving it (max deviation 0.653 vs 0.447), because the + hold had been pinning the camera during the frames shake was loudest and thereby hiding the integration bug + in #10. Had I shipped the drop on its own and stopped, the "fix" would have been a regression. Re-measure + after each half of a two-part fix. + +13. **`HideFlags.DontSave` objects SURVIVE play-mode exit.** A self-spawning dev tool created with + `HideAndDontSave` + `DontDestroyOnLoad` outlives the play session while its `static _instance` guard is + cleared by the domain reload — so it leaks one live instance per reload (two were found). If you gate such + a bootstrap off, also sweep the existing strays with + `Resources.FindObjectsOfTypeAll()` — `GameObject.Find` alone will not show you the history.