Tried bumping calc_friction's gate from `dot >= 0f` to `dot >= 0.25f`
per retail acclient_2013_pseudo_c.txt:276705. Build green but
PlayerMovementControllerTests immediately showed forward motion
dropping from ~3m to ~0.16m over a 1-second simulated walk —
friction now hammers active locomotion in our architecture.
Root cause is deeper than a single threshold. Retail line 276702 has
a state-flag check (`(this->state & ...) == 0`) gating the friction
block that the decompile renders as a corrupted string and we didn't
fully characterize. Best read: retail skips this friction block while
locomotion is actively driving velocity, applying it only to residual
motion after locomotion stops. acdream's controller sets velocity
once per frame from input, then UpdatePhysicsInternal substeps friction
through it — at 0.25 threshold the substep compounding eats most of
the velocity before integration completes.
Reverting to the previous behavior (0.0 threshold). Filing the proper
investigation as L.3c-followup: needs to read retail's `(this->state &
...)` flag at acclient_2013_pseudo_c.txt:276702, identify whether
it gates on an active-locomotion bit, and either honor that gate or
restructure acdream's per-frame locomotion → integration ordering so
friction fires only on residual velocity.
Tests: 1491 still pass after revert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent research agents converged: retail's "bouncy walls"
feel comes from CPhysicsObj::handle_all_collisions (acclient_2013_pseudo_c.txt:
282699-282715, ACE PhysicsObj.cs:2692-2697) which applies the canonical
reflection v_new = v - (1 + e) * dot(v, n) * n to the body's velocity
after every transition resolves. Player elasticity = 0.05 (5% bounce);
INELASTIC_PS = 0x20000 zeros velocity entirely (used by spell projectiles).
acdream had the data plumbed (PhysicsBody.Elasticity = 0.05 was already
set, ci.CollisionNormal was being populated in 8+ code paths) but
ResolveWithTransition discarded the normal before returning. Hence
"sticky walls on jumps" — perpendicular velocity got removed by
SlideSphere's geometric resolution, but never reflected back, so
hitting a wall mid-jump zeroed forward motion entirely instead of
producing a small push-back.
Files:
- PhysicsBody.cs: add PhysicsStateFlags.Inelastic = 0x20000.
- ResolveResult.cs: surface CollisionNormalValid + CollisionNormal.
- PhysicsEngine.cs:599-624: copy ci.CollisionNormal into ResolveResult
before returning (both ok and partial paths).
- PlayerMovementController.cs:445-503: after position commit, apply
reflection per the retail formula. Inelastic → zero velocity;
else → reflect with v += n * -(dot(v,n) * (e + 1)).
apply_bounce rule (more conservative than retail by design):
- Sledding: retail's strict rule — bounce unless both grounded.
- Otherwise: bounce ONLY when both prev and now airborne. Suppress on
landing (prev air, now ground) to avoid micro-bouncing on floor —
the post-reflection upward Z defeats the controller's Velocity.Z<=0
landing-snap gate. Retail's elasticity 0.05 makes the artifact
visually imperceptible there; acdream's per-frame architecture
amplifies it.
Tests: 1491 → 1491 still pass (existing AirborneFrames + WalkOffLedge
tests confirmed the conservative apply_bounce rule keeps landings
clean).
Live verification needed: jump into a wall mid-air — should produce a
visible bounce-back rather than sticking. Walking along corridor with
side-clip should still slide. Landing should still settle without
micro-bounce.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two linked issues both rooted in skipping parts of the retail physics chain.
## 1. Remote staircase on slopes — Euler never integrated between UPs
TickAnimations called rm.Body.update_object(now) for remote integration, but
PhysicsBody.update_object gates on MinQuantum = 1/30s (retail FUN_00515020
early-return). At our 60fps render tick (~16 ms), deltaTime < MinQuantum on
almost every frame → early return AND LastUpdateTime never advances → position
effectively never integrates. Remote Position changed only on UP hard-snap,
producing visible teleport strides uphill (the "staircase" the user reported).
Fix: call UpdatePhysicsInternal(dt) directly for the remote tick — the same
pattern PlayerMovementController.cs:358 uses for the local player. Wire
ResolveWithTransition in afterwards so the remote's Euler-advanced position
gets swept through the same retail collision chain (find_env_collisions +
find_obj_collisions + step_down + 6-path BSP dispatcher) that the local
player already goes through.
New field RemoteMotion.CellId tracks the remote's cell across frames; set
from UpdatePosition.p.LandblockId and updated from transition output.
## 2. Local player floating on downhill slopes — ContactPlane not persisted
Running a character down a slope faster than ~0.5 m/s vertical: per-frame
Euler moves feet horizontally (no Z component since velocity is world-XY).
After Euler, feet are above the new-XY terrain. ValidateWalkable takes the
"above surface" branch without setting a contact plane, DoStepDown probes
~4 cm down (the retail StepDownHeight default), fails to find the surface
8-10 cm below, and the character stays at the old Z. Over a sustained
descent this accumulates into a visible hover.
Retail's PhysicsObj carries ContactPlane + ContactPlaneCellID as persistent
fields (ACE PhysicsObj.cs:2598-2604 get_object_info → InitContactPlane).
Each transition call seeds CollisionInfo.ContactPlane from the previous
frame's plane. That seed is what lets AdjustOffset project horizontal
velocity onto the slope surface — so the Euler offset acquires a Z
component matching the slope and the sphere tracks terrain without needing
step-down to do the catch-up every frame.
Fix: add PhysicsBody.ContactPlane* fields mirroring PhysicsObj's. Extend
ResolveWithTransition with an optional `body` parameter; when provided, seed
the transition's CollisionInfo from body.ContactPlane at the start, copy
back (preferring current, falling back to LastKnown) at the end. Both local
(PlayerMovementController) and remote (TickAnimations) pass their body.
Verified live: DIAG samples showed pre/post/resolved Z all exactly equal
before the MinQuantum bypass (Euler frozen). After bypass, deltas dropped
to floating-point noise on slopes for remotes. Local hover on downhill
resolved in separate visual pass.
All 717 tests green. No API breaks (ResolveWithTransition's body param is
optional, backwards-compatible).
Cross-refs:
- decompile: FUN_00515020 update_object, FUN_005111D0 UpdatePhysicsInternal,
FUN_005148A0 transition init
- ACE: PhysicsObj.cs:2586-2621 get_object_info, Transition.cs:613-620 InitContactPlane
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SmallVelocity threshold (0.25 m/s) in UpdatePhysicsInternal was zeroing
velocity every frame while airborne at the jump apex. With vel~0.01 m/s
and gravity adding only 0.012/frame, the zeroing won every frame and
the character got stuck at peak height forever.
Fix: only apply small-velocity zeroing when OnWalkable (grounded).
While airborne, gravity must accumulate freely through the zero-crossing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>