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