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)
@@ -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.
35. 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.
@@ -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>()``GameObject.Find` alone will not show you the history.