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
@@ -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.