acdream/docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md
Erik 80881ed6f1 docs(#182): crowd-collision investigation outcome + velocity-model rebuild design
The #182 CSphere port (96ae2740) failed its visual gate and introduced an
airborne "stuck in the falling animation" regression. A player-attributed retail
cdb trace (tools/cdb/retail-crowd-jump3.cdb) proved retail's LOCAL client fully
runs player-vs-creature collision (76 land_on_sphere, 188 COLLIDED, 130 SLID,
~78% OK, glides across) -- NOT server-authoritative (an earlier unfiltered
land_on_sphere=0 read was a false lead the attributed trace refuted).

acdream's same-repro capture: 50.9% OK, 22.4% stuck, 115 airborne-stuck. Root
divergence: retail CPhysicsObj::UpdateObjectInternal (0x005156b0, pc:283688) sets
cached_velocity = (resolved - old)/dt -- velocity from ACTUAL movement, so a
blocked jump collapses to ~0 -> gravity -> the player falls/glides. acdream
integrates velocity + reflects on collision (PlayerMovementController ~:1008-1069),
so the jump velocity (~18) persists against the creature -> hang.

Fix = verbatim rebuild of the per-frame player-physics loop (UpdateObjectInternal
chain), velocity model first, transition internals kept. Full design +
retail function inventory + the capture apparatus + retail target numbers:
docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md.
Implementation deferred to a fresh session (user decision). Also files #183
(floating distant scenery, observed during testing). #182 stays as the base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:07:56 +02:00

167 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design: verbatim rebuild of the per-frame player physics update loop
**Date:** 2026-07-07 · **Status:** DESIGN APPROVED (implementation deferred to a fresh session)
**Driving issue:** #182 (player-vs-monster crowd collision) + the airborne "stuck in
the falling animation" regression it exposed.
**Brainstorm decisions (user):** verbatim rebuild (highest fidelity); scope = the
per-frame update LOOP, keep everything already faithful; stage the velocity model
first and measure; do the implementation in a NEW session.
---
## 1. Problem (from the user, live)
1. **Crowd jam** — packed in a group of monsters, the player gets stuck and can't
push through / wiggle out. Retail lets you shove monsters aside and keep moving.
2. **Airborne stuck** — jumping up into a crowd, retail lets you land/**glide across**
the monster tops; acdream **hangs in the falling animation** unable to rise or fall.
The #182 CSphere-family port (committed `96ae2740`) was faithful but aimed a layer
too low: it fixed the per-contact sphere *response*, did NOT fix the crowd feel, and
**introduced the airborne-stuck regression** (before #182 the hand-rolled
de-penetration shoved the player up onto monsters).
## 2. Evidence — the traces (the load-bearing part; do not re-derive)
### Retail, player-attributed cdb trace (`tools/cdb/retail-crowd-jump3.cdb`)
Filtered to the local player's `CPhysicsObj` (captured from `set_local_velocity`),
during a jump-into-a-crowd repro:
```
pValidate=1482 pCOLLIDED=188 (12.7%) pSLID=130 (8.8%) pLand=76 → ~78% OK
```
- The retail **local client fully runs player-vs-creature collision** — 76
`CSphere::land_on_sphere` (airborne creature landings), 188 COLLIDED, 130 SLID —
and still nets **~78% OK**. **NOT server-authoritative.** (An earlier v2 unfiltered
read of `land_on_sphere=0` was a false lead — the attributed trace refuted it.)
- The player **glides across / lands on** the crowd via local physics.
### acdream, `ACDREAM_CAPTURE_RESOLVE` JSONL capture (same repro, current #182 build)
Of the player's move-intent resolves (2883 of 41832 total):
| | acdream | retail |
|---|---|---|
| OK (reached target) | **50.9%** | ~78% |
| partial / slid | 26.7% | ~9% |
| **stuck (reverted, no advance)** | **22.4%** | ~13% |
| airborne-stuck | **115 frames** | (glides off) |
acdream jams ~2× harder, and the 115 airborne-stuck frames ARE the falling-animation
bug. In those frames: velocity `(0,0,~18)` (a jump), a **near-horizontal creature
collision normal** (e.g. `(-0.96,-0.25,-0.15)`), the transition reverts (no advance),
and the **sliding normal flips frame-to-frame** — the #137 anti-parallel-absorb wedge.
## 3. Root divergence (verified against decomp + capture)
**Retail** `CPhysicsObj::UpdateObjectInternal` (0x005156b0, pc:283611) recomputes the
player's velocity from the ACTUAL collided movement every frame:
```
newFrame = UpdatePositionInternal(this, dt) // integrate velocity + gravity
transition = CPhysicsObj::transition(this, m_position, newFrame, 0) // collision sweep
if transition == null: set_frame(newFrame); cached_velocity = 0
else:
offset = get_offset(m_position, transition.sphere_path.curr_pos)
cached_velocity = offset / dt // ← VELOCITY = ACTUAL MOVEMENT / dt (0x005158cb-005158ff)
SetPositionInternal(transition)
```
So when the player is **blocked** (resolved ≈ old position), `cached_velocity → ~0`,
gravity resumes next frame, and the player **falls / settles / glides** off the
obstacle. The collision naturally bleeds the velocity — there is no reflection step.
**acdream** uses a different model: `PhysicsBody` integrates velocity/gravity, then
`PlayerMovementController` (`~:1008-1069`) **reflects** the into-wall component on
collision (`Velocity = v + n·k`) and clamps Z on landing. For a straight-up jump into
a creature whose collision normal is horizontal, the reflection barely touches +Z, so
the jump velocity **persists** — the player keeps shoving up into the creature and
hangs. This is the airborne-stuck, and the same "velocity doesn't bleed on block"
shape drives the general 2× jam.
**This velocity model is the centerpiece of the fix.**
## 4. Design — verbatim rebuild of the per-frame loop
Port retail's `UpdateObjectInternal` chain verbatim into a single owner of the
per-frame player physics. **Reuse the already-faithful transition internals** as the
collision primitive — do not touch them.
### 4.1 Retail functions to port (chain of `UpdateObjectInternal`)
| Function | Address | Role |
|---|---|---|
| `CPhysicsObj::UpdateObjectInternal` | 0x005156b0 | per-frame entry: integrate → transition → commit → velocity |
| `CPhysicsObj::UpdatePositionInternal` | (grep) | integrate velocity + gravity + forces → candidate frame |
| `CPhysicsObj::calc_acceleration` | 0x00510950 | gravity / friction / buoyancy |
| `CPhysicsObj::handle_all_collisions` | 0x00514780 | retail's collision velocity response (verify role vs the cached_velocity model) |
| `CPhysicsObj::SetPositionInternal` | 0x00515330 / 0x00515bd0 / 0x00516040 (3 overloads) | commit the transition's resolved position |
| `CPhysicsObj::transition` | (grep) | collision sweep — **maps to acdream `ResolveWithTransition`, reused as-is** |
### 4.2 In scope (rebuilt verbatim)
The per-frame player physics loop: velocity + gravity integration
(`UpdatePositionInternal`/`calc_acceleration`), the transition orchestration
(call → commit), the **velocity-from-movement recompute** (`cached_velocity =
(resolved old)/dt`), and `handle_all_collisions`.
### 4.3 OUT of scope (kept — already faithful, per the user "keep everything faithful")
The transition INTERNALS: `ResolveWithTransition` and everything under it — BSPQuery,
the CSphere/CylSphere collision families (incl. the #182 port), cell membership,
terrain, streaming. Retail's `transition` calls these primitives and so does ours.
### 4.4 Architecture
A new `PhysicsObjUpdater` (or a `PhysicsBody.UpdateObjectInternal(dt)` method) owns
the retail chain verbatim. It **absorbs**: `PhysicsBody`'s ad-hoc gravity/friction
integration AND `PlayerMovementController`'s post-resolve bounce/reflect/clamp block
(`~:1008-1069`). `PlayerMovementController` shrinks to: input → velocity/jump intent →
hand off. The retail velocity-from-movement model **replaces** reflect-and-clamp
wholesale. Register a divergence-register row retiring the reflect model + any
adaptation rows the port introduces.
### 4.5 Staging (velocity model first, measured)
- **Slice 1:** port `UpdatePositionInternal` + `calc_acceleration` + the
`cached_velocity = (resolvedold)/dt` recompute, replacing the integrate+reflect
path. A/B **against the crowd capture**: require the airborne-stuck count → 0 and OK%
to climb toward retail's ~78%. Gate (suites + a visual pass on ordinary
jump/walk/fall) before continuing.
- **Slice 2:** `handle_all_collisions` verbatim (once Slice 1 shows what residual the
velocity model leaves) + any remaining `UpdateObjectInternal` orchestration.
- **Slice 3+:** close the residual ground-jam gap if the velocity model alone doesn't
reach ~78% OK; re-measure each slice against the capture.
## 5. Validation
- **Apparatus (built this session, reuse):** `ACDREAM_CAPTURE_RESOLVE=<path>` JSONL of
every player resolve; the `py` histogram (OK / partial / stuck / airborne-stuck; see
§2). Retail target: **~78% OK, 12.7% COLLIDED, 8.8% SLID, 0 airborne-stuck**. Retail
cdb scripts: `tools/cdb/retail-crowd-jump{2,3}.cdb` (v3 = player-attributed; safe
auto-detach via `validate_transition` terminal + top-level `qd`).
- **Conformance tests:** velocity-from-movement (blocked → velocity → ~0); jump +
gravity integration; the `UpdateObjectInternal` sequence; a crowd replay.
- **Suites stay green:** Core 2603 / App 741 (no regression to walking/stairs/slopes/
walls/streaming — the transition internals are untouched).
- **Visual gates:** (a) crowd glide/land like retail (the #182 symptom); (b) a
regression pass on normal locomotion — flat walking, slopes, stairs, jumping, falling,
wall-slide.
## 6. Risk
Highest-risk option: it replaces the core of every jump/fall/step. Mitigation: the
scope boundary (transition internals untouched), the measured slicing (Slice 1 behind
an A/B compare), and heavy gating. If Slice 1 regresses ordinary locomotion, bisect
within the velocity model before proceeding.
## 7. Open items for the new session (verify during writing-plans)
1. Read `UpdatePositionInternal`, `calc_acceleration` (0x00510950), and
`handle_all_collisions` (0x00514780) fully — confirm the integration + the role of
`handle_all_collisions` vs the cached_velocity model (does retail ALSO reflect, or
is velocity purely movement-derived?).
2. Confirm which `SetPositionInternal` overload the player path uses (3 exist).
3. Decide whether the general ground-jam (22% stuck vs retail 13%) is fully explained
by the velocity model or needs a second divergence (de-penetration / sliding-normal
provenance — the #137/TS-45 wedge family). Measure after Slice 1.
4. #182 stays as the base (user decision). NOTE: until this rebuild lands, the codebase
carries the airborne-stuck regression #182 introduced.
## 8. Pointers
- Physics/collision SSOT: `claude-memory/project_physics_collision_digest.md` (updated
with the #182 + this-investigation banner).
- #182 issue: `docs/ISSUES.md`. CSphere pseudocode: `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`.
- Retail debugger toolchain + the safe-detach recipe: `memory/project_retail_debugger.md` + CLAUDE.md.