From 3d4e63f9c852b641812625f99ecc20ff4adb58c8 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 25 May 2026 14:29:54 +0200 Subject: [PATCH] =?UTF-8?q?fix(phys):=20A6.P6=20=E2=80=94=20cylinder=20ste?= =?UTF-8?q?p-over=20for=20Contact=20movers=20(CCylSphere::step=5Fsphere=5F?= =?UTF-8?q?up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retail's CCylSphere::intersects_sphere at acclient_2013_pseudo_c.txt :324626-324641 routes the Contact-state branch through step_sphere_up (line 324516), not slide. The step-up check at line 324519-324524: cyl_clearance = sphere.radius + cyl.height - offset.z if (step_up_height < cyl_clearance) → slide (cyl too tall) else → DoStepUp, on failure → step_up_slide For the cottage door's foot cyl (h=0.20m, r=0.10m) at standing height, cyl_clearance = 0.30m and player step_up_height = 0.60m, so the sphere steps over the cyl easily — no radial push-out. Pre-fix bug (live trace door-phantom.utf8.log 2026-05-25 PM): when the player slid along the closed cottage door's slab face, the foot cyl fired Slid with radial outward push at the door's middle X (cn=(0.64,0.77,0) etc.) — a "phantom collision" that broke the slide. Cause: A6.P5's cellSet expansion made the door reliably visible from all approach angles, exposing this pre-existing behavior. Pre-A6.P5 the cyl wasn't visible from many approach angles so the phantom rarely fired; the underlying mismatch with retail was always there. Fix: in CylinderCollision, when oi.Contact && !sp.StepUp && !sp.StepDown and engine is non-null, compute cyl_clearance, and if step_up_height allows it, call DoStepUp with the cyl's radial collision normal. On success the sphere is repositioned past the cyl (returns OK). On failure (no walkable surface beyond — e.g., a wall behind the cyl), fall back to StepUpSlide which uses SlideSphereInternal's crease projection — smoother tangent slide than the radial push. Conformance: - All A6P5 unit tests + Path 5 tests + Apparatus_50cmOffCenter_* + Apparatus_DeadCenter_* + Directional_OutsideIn/InsideOut + issue #98 LiveCompare_FirstCap_FixClosesCottageFloorCap pass in isolation. - Full Core suite failure count unchanged (17 baseline → 17 with-fix); diff is documented static-leak flakiness, no real regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/Physics/TransitionTypes.cs | 72 +++++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) 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