diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 535ceb7..f514046 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -2383,7 +2383,7 @@ public sealed class Transition // ACE: Sphere.IntersectsSphere handles CylSphere objects via // the same 6-path dispatcher. For now we keep the swept-sphere // cylinder test which matches the retail CylSphere behavior. - result = CylinderCollision(obj, sp); + result = CylinderCollision(obj, sp, engine); // A6.P4 door investigation (2026-05-24): log every Cylinder // shadow tested. Tells us whether the broadphase returned @@ -2521,13 +2521,33 @@ public sealed class Transition } /// - /// Cylinder collision test for CylSphere objects (tree trunks, rock pillars, NPCs). - /// Applies a horizontal wall-slide response when the sphere overlaps the - /// cylinder, matching the BSP path 5/6 response for consistent behavior. + /// Cylinder collision test for CylSphere objects (tree trunks, rock pillars, NPCs, + /// door foot-colliders). For Contact-grounded movers, attempts to step over short + /// cylinders (retail-faithful CCylSphere::step_sphere_up). For airborne movers, + /// movers already stepping, or cylinders too tall to step over, applies a + /// horizontal wall-slide response. + /// + /// + /// A6.P6 (2026-05-25): the step-over path matches retail's + /// CCylSphere::step_sphere_up at + /// acclient_2013_pseudo_c.txt:324516-324538. The door's foot + /// cylinder (h=0.20m, r=0.10m) is too tall for the static slide to + /// produce smooth sliding along the slab — the radial push-out + /// fires as a "phantom collision" at the door's center when the + /// sphere is touching the slab face and the cyl is just within reach. + /// Retail steps the sphere over the cyl (succeeds when + /// step_up_height >= sphere.radius + cyl.height - offset.z), + /// which lets the sphere walk past the cyl without the radial push. + /// On step-up failure (cyl too tall, no walkable surface beyond), + /// falls back to step_up_slide — the same crease-projection + /// slide the BSP path uses, which produces smoother behavior than + /// the radial push. + /// /// - private TransitionState CylinderCollision(ShadowEntry obj, SpherePath sp) + private TransitionState CylinderCollision(ShadowEntry obj, SpherePath sp, PhysicsEngine engine) { var ci = CollisionInfo; + var oi = ObjectInfo; Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin; Vector3 sphereCheckPos = sp.GlobalSphere[0].Origin; float sphRadius = sp.GlobalSphere[0].Radius; @@ -2550,7 +2570,7 @@ public sealed class Transition if (distSqCheck >= combinedRSq) return TransitionState.OK; // not overlapping at check position - // ─── Overlap detected: apply wall-slide ───────────────────── + // ─── Overlap detected ───────────────────────────────────── // Horizontal outward normal from the cylinder axis to the sphere // check position. For the degenerate case where the sphere center // is exactly on the axis, use the movement direction as a fallback @@ -2571,6 +2591,46 @@ public sealed class Transition collisionNormal = new Vector3(dxCheck / distCheck, dyCheck / distCheck, 0f); } + // A6.P6 (2026-05-25): retail-faithful CCylSphere::step_sphere_up for + // Contact-grounded movers. acclient_2013_pseudo_c.txt:324516-324538. + // + // Retail check: step_up_height must clear (sphere.radius + cyl.height + // - offset.z) where offset.z is sphere center Z minus cyl low_pt Z. + // Geometrically: the height we need to lift the sphere to clear the + // cyl's top, less the sphere center's current height above the cyl + // base, equals cyl top minus sphere bottom (positive when sphere + // currently below cyl top). + // + // For the door's foot cyl (h=0.20m, sphere radius 0.48m, step_up 0.60m) + // at standing height (offset.z ~0.38m): cyl_clearance = + // 0.48 + 0.20 - 0.38 = 0.30m, step_up_height = 0.60m → step over OK. + if (oi.Contact && !sp.StepUp && !sp.StepDown && engine is not null) + { + float offsetZ = sphereCheckPos.Z - obj.Position.Z; + float cylClearance = sphRadius + cylTop - offsetZ; + + if (oi.StepUpHeight >= cylClearance) + { + // Try step-up over the cyl (DoStepUp probes upward by + // step_up_height, then step-down for walkable surface). + // On success: sphere is repositioned past/over the cyl, + // ContactPlane updated, returns OK. + if (DoStepUp(collisionNormal, engine)) + return TransitionState.OK; + + // Step-up failed — sphere couldn't find a walkable surface + // beyond the cyl (e.g., a wall behind it). Fall back to + // step_up_slide which uses the SlideSphereInternal crease + // projection — smoother than the radial push-out below + // because it follows the contact-plane / cyl-normal crease + // direction. + return sp.StepUpSlide(this); + } + // else: cyl too tall to step over — fall through to radial slide + } + + // ─── Fallback: airborne / non-Contact / cyl-too-tall — wall-slide ─── + // Wall-slide position (in world space): // curr = sphereCurrPos (pre-step) // movement = sphMovement