fix(physics): L.4-cliffslide-priority — steep ContactPlane check before OnWalkable gate

User-reported: "still don't slide down steep roofs" after the previous
trigger-gate fix (52e257d). Traced through the EdgeSlide dispatcher:
the gate IS firing now, but ValidateTransition's L.2.3i FloorZ test
clears OnWalkable as soon as the player is on a steep surface. So
EdgeSlideAfterStepDownFailed enters Branch 1 (`!OnWalkable → restore
+ OK`) and stops the player BEFORE Branch 2's steep-ContactPlane
CliffSlide can fire.

Re-order: check the steep-ContactPlane condition FIRST, before the
Branch 1 OnWalkable gate. If the surface is too steep AND we have a
contact plane on it AND the EdgeSlide flag is set, run CliffSlide
regardless of OnWalkable state. The cross-product deflection plus
gravity produces continuous downhill drift, frame after frame.

Branch 1's "stop at edge" still fires for the original case it was
meant for: walked off into thin air with no contact plane at all.
That should still stop (or fall normally) rather than CliffSlide
against nothing.

Tests: 1491 still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-30 10:29:30 +02:00
parent 52e257d8d7
commit a48883af2d

View file

@ -810,6 +810,35 @@ public sealed class Transition
var ci = CollisionInfo;
var oi = ObjectInfo;
// L.4-cliffslide-priority (2026-04-30): the steep-ContactPlane check
// moved BEFORE the OnWalkable/EdgeSlide gate.
//
// Why: by the time this dispatch runs on subsequent frames (player
// standing on a steep slope), ValidateTransition's L.2.3i FloorZ
// test has already CLEARED OnWalkable (steep slope → not a walkable
// surface). The original Branch 1 (`!OnWalkable → restore + OK`)
// therefore fires every frame, stopping the player dead — exactly
// the "stay on the roof" symptom the user reported.
//
// Re-ordering: if the surface is too steep AND we have a contact
// plane on it, run CliffSlide regardless of OnWalkable. The
// cross(currentNormal, lastKnownNormal) deflection plus gravity
// produces visible downhill drift each frame.
//
// Branch 1 (the !OnWalkable stop) still fires when we DON'T have
// a contact plane — the original "walked off into thin air"
// case, which should still stop or fall normally rather than
// CliffSlide on nothing.
if (ci.ContactPlaneValid && ci.ContactPlane.Normal.Z < zVal && oi.EdgeSlide)
{
var cliffPlane = ci.ContactPlane;
sp.ClearWalkable();
sp.RestoreCheckPos();
ci.ContactPlaneValid = false;
ci.ContactPlaneIsWater = false;
return CliffSlide(cliffPlane);
}
// Retail lets non-EdgeSlide movers continue over the boundary. Player
// movement carries EdgeSlide, so the local avatar takes the slide path.
if (!oi.OnWalkable || !oi.EdgeSlide)