fix(physics): #265/#166 - stop zeroing grounded residual velocity, wire GroundNormal
Capture bisect (docs/research/2026-07-30-265-capture-bisect.md, mined from artifacts/matrix-session2-resolve.jsonl records 3415-3434) traced #265's lost roof slides / permanent landing freeze and #166's missing downhill sled to a pre-existing (2026-07-20, ten days before Campaign P - not a regression) mechanism in PlayerMovementController.cs's grounded quantum block: it hand-zeroed Velocity.X/Y to exactly zero every tick once OnWalkable whenever animation root motion drives the walk (the production graphical local-player path), discarding any residual horizontal momentum a fall left on the body before calc_friction (AP-7/AD-55, already correctly ported) or PhysicsBody. UpdatePhysicsInternal's Euler integrator ever got a chance to act on it. Two changes: 1. PhysicsEngine.cs now syncs body.GroundNormal (the vector calc_friction dots velocity against, per retail CPhysicsObj::calc_friction 0x0050ee70's `contact_plane.Normal` read) from the committed ContactPlane.Normal at the same commit point that already publishes ContactPlane. GroundNormal had zero production writers before this and silently defaulted to Vector3.UnitZ forever - even surviving velocity would have been tested against a fake flat-ground normal on any real slope. Core-level, so player, remote, ordinary, and projectile movers all benefit uniformly. 2. PlayerMovementController.cs's grounded block no longer reconstructs Velocity at all for the animation-root-motion case (only the headless/test-controller get_state_velocity fallback still does, unchanged). Root motion continues to fully own commanded locomotion; this only stops destroying whatever Velocity already holds, letting it compose with root motion through the same ResolveWithTransition sweep exactly as retail's CPhysicsObj::UpdatePositionInternal composes both channels. Symptom (a), the uphill-jump bounce, traces to a SEPARATE, byte-exact (re-verified against acclient_2013_pseudo_c.txt:282647-282760), already-closed retail mechanism (AD-25, PhysicsObjUpdate. HandleAllCollisions's shouldReflect gate) - confirmed orthogonal to this fix, not addressed here (see the research doc's as-fixed addendum §9.5). Issue265SteepSlopeCaptureBisectTests.cs gains a composed harness (ReplayRealRoofLandingComposed) mirroring PlayerMovementController.cs's per-tick composition against Core types only, proving: the old model reproduces the mined freeze exactly; the new model survives the landing and slides continuously (the real captured geometry glides at constant velocity per retail's own dot>=0.25 early-return - AP-7); a synthetic dot<0.25 case shows genuine exponential decay via calc_friction; and a synthetic uphill-bounce case proves the fix changes nothing about HandleAllCollisions's reflection decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
61e959169b
commit
06c76009f1
5 changed files with 673 additions and 36 deletions
|
|
@ -1865,20 +1865,46 @@ public sealed class PlayerMovementController
|
|||
* tickDt;
|
||||
}
|
||||
|
||||
if (_body.OnWalkable)
|
||||
// #265/#166 (2026-07-30, docs/research/2026-07-30-265-capture-bisect.md
|
||||
// §4): retail CPhysicsObj::UpdatePositionInternal (0x00512C30) composes
|
||||
// BOTH channels every quantum -- the root-motion Frame just written into
|
||||
// pmDelta.Origin above (commanded locomotion) AND the integrated physics
|
||||
// Velocity (residual momentum: jump arcs, landing slides) -- via the SAME
|
||||
// candidate position that PhysicsBody.UpdatePhysicsInternal's Euler step
|
||||
// (calc_friction + v*dt, below) and the ResolveWithTransition sweep both
|
||||
// see. This block used to hand-zero Velocity.X/Y to EXACTLY zero on every
|
||||
// single grounded tick whenever animation root motion drives the walk (the
|
||||
// production graphical local-player path since R6) -- regardless of why
|
||||
// the body was OnWalkable. That discarded any horizontal momentum a fall
|
||||
// or collision had just left on the body (retail settles it via
|
||||
// calc_friction over subsequent ticks; PhysicsBody.GroundNormal is now
|
||||
// synced to the real contact-plane normal by PhysicsEngine so calc_friction
|
||||
// has real slope data to act on) before the integrator ever ran -- a mover
|
||||
// that landed on a walkable roof/slope with residual horizontal velocity
|
||||
// had that velocity vanish the very next tick and never moved again. Root
|
||||
// motion still fully owns COMMANDED locomotion (walking/running
|
||||
// displacement comes from pmDelta.Origin above, not from Velocity), so
|
||||
// this does not reintroduce command- or packet-cadence-derived grounded
|
||||
// translation -- it only stops DESTROYING whatever Velocity already holds.
|
||||
// Ordinary walking is unaffected: Velocity is already ~0 while grounded
|
||||
// with no fall/collision in flight (nothing else writes it), so removing
|
||||
// this zero is a no-op on that path -- see
|
||||
// GroundedRootMotion_FrictionThreshold_DoesNotHammerLocomotionTests
|
||||
// (PhysicsBodyTests.cs) and Update_AnimationRootMotion_WalkSpeedUnaffected
|
||||
// ByResidualVelocityFix (PlayerMovementControllerTests.cs).
|
||||
//
|
||||
// The headless/test-controller fallback below (no animation runtime --
|
||||
// get_state_velocity's doc comment) is unchanged: it still directly
|
||||
// writes the commanded state velocity into the body every grounded tick,
|
||||
// exactly as before -- that model has no separate root-motion channel to
|
||||
// compose with, so overwriting IS its correct per-tick behavior.
|
||||
if (_body.OnWalkable && !hasAnimationRootMotion)
|
||||
{
|
||||
float savedWorldVz = _body.Velocity.Z;
|
||||
if (hasAnimationRootMotion)
|
||||
{
|
||||
_body.Velocity = new Vector3(0f, 0f, savedWorldVz);
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 stateVelocity = _motion.get_state_velocity();
|
||||
_body.set_local_velocity(
|
||||
new Vector3(stateVelocity.X, stateVelocity.Y, savedWorldVz),
|
||||
autonomous: _body.LastMoveWasAutonomous);
|
||||
}
|
||||
Vector3 stateVelocity = _motion.get_state_velocity();
|
||||
_body.set_local_velocity(
|
||||
new Vector3(stateVelocity.X, stateVelocity.Y, savedWorldVz),
|
||||
autonomous: _body.LastMoveWasAutonomous);
|
||||
}
|
||||
|
||||
var preIntegratePos = _body.Position;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue