fix(phys): A6.P6 — cylinder step-over for Contact movers (CCylSphere::step_sphere_up)

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-25 14:29:54 +02:00
parent 3b1ae83931
commit 3d4e63f9c8

View file

@ -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
}
/// <summary>
/// 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.
///
/// <para>
/// A6.P6 (2026-05-25): the step-over path matches retail's
/// <c>CCylSphere::step_sphere_up</c> at
/// <c>acclient_2013_pseudo_c.txt:324516-324538</c>. 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
/// <c>step_up_height &gt;= sphere.radius + cyl.height - offset.z</c>),
/// 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 <c>step_up_slide</c> — the same crease-projection
/// slide the BSP path uses, which produces smoother behavior than
/// the radial push.
/// </para>
/// </summary>
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