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
|
|
@ -444,6 +444,93 @@ public sealed class PlayerMovementController
|
|||
// Apply resolved position.
|
||||
_body.Position = resolveResult.Position;
|
||||
|
||||
// L.3a (2026-04-30): retail wall-bounce / velocity reflection.
|
||||
//
|
||||
// Retail's CPhysicsObj::handle_all_collisions runs after every
|
||||
// SetPositionInternal. It reads the wall normal that the
|
||||
// transition's slide computed and reflects the body's velocity:
|
||||
//
|
||||
// v_new = v - (1 + elasticity) * dot(v, n) * n
|
||||
//
|
||||
// This is what gives retail its "bouncy" feel — fast head-on
|
||||
// jumps push the player back from the wall, glancing angles
|
||||
// produce a small deflection. acdream's transition resolver
|
||||
// SLID position correctly but never updated velocity, so the
|
||||
// player kept driving into walls until the controller's input
|
||||
// changed direction. Felt sticky / fragile.
|
||||
//
|
||||
// Suppression rule (apply_bounce): grounded movement on a wall
|
||||
// SHOULDN'T bounce — sliding along a corridor is expected. Only
|
||||
// airborne wall hits reflect. Mirrors retail's `var_10_1` guard
|
||||
// and ACE PhysicsObj.cs:2656-2660 `apply_bounce`.
|
||||
//
|
||||
// Inelastic flag (spell projectiles, missiles) zeros velocity
|
||||
// entirely instead of reflecting. The player never has it set.
|
||||
//
|
||||
// Sources:
|
||||
// acclient_2013_pseudo_c.txt:282699-282715 (handle_all_collisions)
|
||||
// acclient.h:2834 (INELASTIC_PS = 0x20000)
|
||||
// ACE PhysicsObj.cs:2656-2721 (line-for-line port)
|
||||
// PhysicsGlobals.DefaultElasticity = 0.05f, MaxElasticity = 0.1f
|
||||
if (resolveResult.CollisionNormalValid)
|
||||
{
|
||||
bool prevOnWalkable = _body.OnWalkable;
|
||||
bool nowOnWalkable = resolveResult.IsOnGround;
|
||||
|
||||
// apply_bounce: bounce ONLY when the body stays airborne both
|
||||
// before and after this step. That is: jumping into a wall
|
||||
// mid-flight, hitting a ceiling, etc. Specifically NOT:
|
||||
//
|
||||
// - prev grounded + now grounded → wall-slide along corridor
|
||||
// (bounce would feel sticky on every wall touch).
|
||||
// - prev airborne + now grounded → terrain landing
|
||||
// (terrain normal is mostly +Z; reflecting downward velocity
|
||||
// would push the body upward and prevent the landing snap
|
||||
// from firing — player perpetually micro-bouncing on the
|
||||
// floor instead of resting).
|
||||
// - prev grounded + now airborne → walked off cliff
|
||||
// (gravity should take over, not lateral bounce).
|
||||
//
|
||||
// Sledding mode reverts to retail's broader rule (bounce
|
||||
// unless both grounded), since sledding intentionally bounces
|
||||
// off ramps.
|
||||
//
|
||||
// This is more conservative than retail's strict
|
||||
// `!(prev && now && !sledding)` rule — retail bounces on
|
||||
// landing too, but at elasticity 0.05 the visual effect is
|
||||
// imperceptible there. acdream's per-frame architecture
|
||||
// amplifies the artifact (the post-reflection upward Z
|
||||
// defeats the controller's `Velocity.Z <= 0` landing-snap
|
||||
// gate), so we suppress it on landing to avoid the
|
||||
// micro-bounce death spiral.
|
||||
bool applyBounce = _body.State.HasFlag(PhysicsStateFlags.Sledding)
|
||||
? !(prevOnWalkable && nowOnWalkable)
|
||||
: (!prevOnWalkable && !nowOnWalkable);
|
||||
|
||||
if (applyBounce)
|
||||
{
|
||||
if (_body.State.HasFlag(PhysicsStateFlags.Inelastic))
|
||||
{
|
||||
// Full stop on impact. Spell projectiles / missiles.
|
||||
_body.Velocity = Vector3.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
var v = _body.Velocity;
|
||||
var n = resolveResult.CollisionNormal;
|
||||
float dotVN = Vector3.Dot(v, n);
|
||||
if (dotVN < 0f)
|
||||
{
|
||||
// Reflect the into-wall component back out.
|
||||
// Player elasticity is 0.05 → 105% of perpendicular
|
||||
// velocity reflects (subtle bounce).
|
||||
float k = -(dotVN * (_body.Elasticity + 1f));
|
||||
_body.Velocity = v + n * k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool justLanded = false;
|
||||
if (resolveResult.IsOnGround)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue