diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 6f73771..cc6944a 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -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). /// - 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;