feat(physics): L.3a — wall-bounce velocity reflection on airborne hits
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>
This commit is contained in:
parent
261322b48e
commit
a1c27b3afb
4 changed files with 191 additions and 3 deletions
|
|
@ -518,8 +518,29 @@ public sealed class PhysicsEngine
|
|||
body.ContactPlaneIsWater);
|
||||
}
|
||||
|
||||
// Retail CPhysicsObj::get_object_info also seeds SlidingNormal when
|
||||
// transient_state has bit 2 set. This matters for one-step/frame hits:
|
||||
// a wall collision at the end of one transition must project the next
|
||||
// frame's movement along the wall instead of hard-stopping again.
|
||||
if (body is not null
|
||||
&& body.TransientState.HasFlag(TransientStateFlags.Sliding)
|
||||
&& body.SlidingNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
||||
{
|
||||
transition.CollisionInfo.SetSlidingNormal(body.SlidingNormal);
|
||||
}
|
||||
|
||||
transition.SpherePath.InitPath(currentPos, targetPos, cellId, sphereRadius, sphereHeight);
|
||||
|
||||
if (isOnGround && body is not null
|
||||
&& body.WalkablePolygonValid
|
||||
&& body.WalkableVertices is { Length: >= 3 })
|
||||
{
|
||||
transition.SpherePath.SetWalkable(
|
||||
body.WalkablePlane,
|
||||
body.WalkableVertices,
|
||||
body.WalkableUp);
|
||||
}
|
||||
|
||||
bool ok = transition.FindTransitionalPosition(this);
|
||||
|
||||
var sp = transition.SpherePath;
|
||||
|
|
@ -548,8 +569,43 @@ public sealed class PhysicsEngine
|
|||
{
|
||||
body.ContactPlaneValid = false;
|
||||
}
|
||||
|
||||
if (sp.HasLastWalkablePolygon && sp.LastWalkableVertices is not null)
|
||||
{
|
||||
body.WalkablePolygonValid = true;
|
||||
body.WalkablePlane = sp.LastWalkablePlane;
|
||||
body.WalkableVertices = (Vector3[])sp.LastWalkableVertices.Clone();
|
||||
body.WalkableUp = sp.LastWalkableUp;
|
||||
}
|
||||
else if (!isOnGround && !ci.ContactPlaneValid && !ci.LastKnownContactPlaneValid)
|
||||
{
|
||||
body.WalkablePolygonValid = false;
|
||||
body.WalkableVertices = null;
|
||||
}
|
||||
|
||||
if (ci.SlidingNormalValid
|
||||
&& ci.SlidingNormal.LengthSquared() > PhysicsGlobals.EpsilonSq)
|
||||
{
|
||||
body.SlidingNormal = ci.SlidingNormal;
|
||||
body.TransientState |= TransientStateFlags.Sliding;
|
||||
}
|
||||
else
|
||||
{
|
||||
body.SlidingNormal = Vector3.Zero;
|
||||
body.TransientState &= ~TransientStateFlags.Sliding;
|
||||
}
|
||||
}
|
||||
|
||||
// L.3a (2026-04-30): surface the wall normal so callers can apply
|
||||
// retail's velocity-reflection bounce (CPhysicsObj::handle_all_collisions
|
||||
// at acclient_2013_pseudo_c.txt:282699-282715, ACE PhysicsObj.cs:
|
||||
// 2692-2697). The reflection itself is applied in
|
||||
// PlayerMovementController after the position commit, gated on
|
||||
// apply_bounce = !(prevOnWalkable && newOnWalkable) — airborne wall
|
||||
// hits bounce, grounded wall slides don't.
|
||||
bool collisionNormalValid = ci.CollisionNormalValid;
|
||||
Vector3 collisionNormal = ci.CollisionNormal;
|
||||
|
||||
if (ok)
|
||||
{
|
||||
bool onGround = ci.ContactPlaneValid
|
||||
|
|
@ -558,7 +614,9 @@ public sealed class PhysicsEngine
|
|||
return new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveOutdoorCellId(sp.CheckPos, sp.CheckCellId),
|
||||
onGround);
|
||||
onGround,
|
||||
collisionNormalValid,
|
||||
collisionNormal);
|
||||
}
|
||||
|
||||
// Transition failed (e.g., stuck in corner, too many steps).
|
||||
|
|
@ -574,6 +632,8 @@ public sealed class PhysicsEngine
|
|||
return new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveOutdoorCellId(sp.CheckPos, partialCellId),
|
||||
partialOnGround);
|
||||
partialOnGround,
|
||||
collisionNormalValid,
|
||||
collisionNormal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue