fix(physics): L.2.3h — skip Placement in step-down contact-recovery branch

Live-test bug: player getting "super stuck" near walls without touching
them. Diagnostic showed 0 step-up calls, so the issue wasn't in DoStepUp.

Root cause: my subagent's L.2.1 commit added a Placement validation
inside DoStepDown to prevent step-up-through-walls. That check is right
for DoStepUp's call (the original use case). But DoStepDown is ALSO
called from TransitionalInsert's contact-recovery branch when the per-
sub-step contact plane is briefly lost (e.g., right after a wall-slide
nudges the sphere slightly upward).

For that "maintain contact during normal movement" use, the Placement
check is over-strict. Wall-slide can leave the sphere with sub-EPSILON
overlap of the wall's BSP solid; SphereIntersectsSolid returns Collided
inside Placement; DoStepDown returns false; my L.2.3e then escalates
that to TransitionState.Collided in the outer loop; ValidateTransition
reverts the position to CurPos every frame. Result: player stuck near
the wall without ever touching it.

Fix: add a `bool runPlacement = true` parameter to DoStepDown.
- DoStepUp passes the default (Placement runs — protects step-up).
- TransitionalInsert's contact-recovery branch passes false (Placement
  skipped — accepts whatever walkable surface is found within reach).

This preserves L.2.3e's edge-block (genuine edges return Collided
because no walkable is found, not because Placement rejected) while
unbreaking normal-walking-near-walls.

ACE Transition.cs:731-741 runs Placement unconditionally, but ACE's
pre-step-down state machine is cleaner — acdream's residual wall-slide
artifacts make Placement misfire here.

Test count 825/825 still pass. Build clean.

Live verification needed: walk near a wall, should no longer get stuck.
Walk off a tall (>1.5m) balcony, should still edge-block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 19:13:56 +02:00
parent eed8e8ccaa
commit 4cbfe0a5f8

View file

@ -630,9 +630,17 @@ public sealed class Transition
float radsum = sp.GlobalSphere[0].Radius * 2f;
// L.2.3h (2026-04-29): pass runPlacement=false. This
// branch's job is to maintain ground contact during normal
// movement (e.g., walking over small bumps or near walls).
// The Placement check inside DoStepDown is too strict for
// this use — minor wall overlap from a prior wall-slide
// would fail Placement and trigger the L.2.3e edge-block,
// leaving the player stuck near walls. DoStepUp still runs
// Placement for the step-UP-through-walls protection.
if (radsum >= stepDownHeight)
{
if (DoStepDown(stepDownHeight, zVal, engine))
if (DoStepDown(stepDownHeight, zVal, engine, runPlacement: false))
{
sp.WalkableValid = false;
return TransitionState.OK;
@ -641,8 +649,8 @@ public sealed class Transition
else
{
stepDownHeight *= 0.5f;
if (DoStepDown(stepDownHeight, zVal, engine)
|| DoStepDown(stepDownHeight, zVal, engine))
if (DoStepDown(stepDownHeight, zVal, engine, runPlacement: false)
|| DoStepDown(stepDownHeight, zVal, engine, runPlacement: false))
{
sp.WalkableValid = false;
return TransitionState.OK;
@ -1314,7 +1322,8 @@ public sealed class Transition
/// Ported from pseudocode section 5 (StepDown).
/// ACE: Transition.StepDown(float stepDownHeight, float zVal).
/// </summary>
private bool DoStepDown(float stepDownHeight, float walkableZ, PhysicsEngine engine)
private bool DoStepDown(float stepDownHeight, float walkableZ, PhysicsEngine engine,
bool runPlacement = true)
{
var sp = SpherePath;
@ -1348,6 +1357,22 @@ public sealed class Transition
&& CollisionInfo.ContactPlaneValid
&& CollisionInfo.ContactPlane.Normal.Z >= walkableZ)
{
// L.2.3h (2026-04-29): Placement validation is for the
// DoStepUp use case (prevents climbing through walls by
// stepping up onto ground beyond a tall wall). For the
// "maintain contact during normal movement" use case (called
// from TransitionalInsert's contact-recovery branch), the
// Placement check is over-strict — slight wall overlap from
// a prior wall-slide makes Placement reject, then the caller
// returns Collided (L.2.3e) and the player gets stuck near
// walls without ever touching them.
//
// ACE Transition.cs:731-741 runs Placement here unconditionally,
// but ACE's pre-step-down state is cleaner — we have residual
// wall-slide artifacts that make Placement misfire.
if (!runPlacement)
return true;
// Placement validation: can we actually stand here?
var savedInsert = sp.InsertType;
sp.InsertType = InsertType.Placement;