diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index d5ee444..b2bd967 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -7034,11 +7034,22 @@ public sealed class GameWindow : IDisposable
_playerController.StepDownHeight = (playerSetup is not null && playerSetup.StepDownHeight > 0f)
? playerSetup.StepDownHeight
: 0.4f;
+ // L.2.3f (2026-04-29): diagnostic — confirm what the actual
+ // values from the player's Setup dat are. Retail's spec says ~0.4 m
+ // for humans, but we want to verify rather than guess. If the
+ // dat-derived value is large (e.g. 1.5 m+) it explains why the
+ // player can mount steep roofs via the step-up scan reach.
+ Console.WriteLine(
+ $"physics: player step heights — StepUp={_playerController.StepUpHeight:F3} m " +
+ $"(Setup.StepUpHeight={(playerSetup?.StepUpHeight ?? 0f):F3}), " +
+ $"StepDown={_playerController.StepDownHeight:F3} m " +
+ $"(Setup.StepDownHeight={(playerSetup?.StepDownHeight ?? 0f):F3})");
}
else
{
_playerController.StepUpHeight = 0.4f;
_playerController.StepDownHeight = 0.4f;
+ Console.WriteLine($"physics: player step heights — defaulting to 0.4 m (no setup dat)");
}
int plbX = _liveCenterX + (int)MathF.Floor(playerEntity.Position.X / 192f);
int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f);
diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs
index eff0222..a0947d0 100644
--- a/src/AcDream.Core/Physics/BSPQuery.cs
+++ b/src/AcDream.Core/Physics/BSPQuery.cs
@@ -1106,7 +1106,7 @@ public static class BSPQuery
if (transition.DoStepUp(collisionNormal, engine!))
return TransitionState.OK;
- return transition.SpherePath.StepUpSlide(transition.CollisionInfo);
+ return transition.SpherePath.StepUpSlide(transition);
}
// -------------------------------------------------------------------------
diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs
index bd34857..2b5834c 100644
--- a/src/AcDream.Core/Physics/TransitionTypes.cs
+++ b/src/AcDream.Core/Physics/TransitionTypes.cs
@@ -248,15 +248,29 @@ public sealed class SpherePath
///
/// Slide fallback when step-up fails. Clears the contact-plane state that
- /// caused the step-up attempt and issues a slide along StepUpNormal.
- /// ACE: SpherePath.StepUpSlide (ACE SpherePath.cs:309-317).
+ /// caused the step-up attempt and runs the full sphere-slide computation
+ /// to actually move the sphere along the wall.
+ ///
+ ///
+ /// L.2.3d (2026-04-29): the previous version only set
+ /// as a flag; it never applied a slide offset. The user observed "running
+ /// close to the wall now I stop" — the sphere stayed pinned at the wall
+ /// and the slide normal got overwritten by ValidateTransition's
+ /// default-to-UnitZ branch. ACE actually computes the slide offset and
+ /// applies it to via Sphere.SlideSphere;
+ /// we delegate to which does
+ /// the same thing.
+ ///
+ ///
+ /// ACE: SpherePath.StepUpSlide + Sphere.SlideSphere
+ /// (SpherePath.cs:309-317, Sphere.cs:558-604).
///
- public TransitionState StepUpSlide(CollisionInfo collisions)
+ public TransitionState StepUpSlide(Transition transition)
{
- collisions.ContactPlaneValid = false;
- collisions.ContactPlaneIsWater = false;
- collisions.SetSlidingNormal(StepUpNormal);
- return TransitionState.Slid;
+ var ci = transition.CollisionInfo;
+ ci.ContactPlaneValid = false;
+ ci.ContactPlaneIsWater = false;
+ return transition.SlideSphereInternal(StepUpNormal, GlobalCurrCenter[0].Origin);
}
///
@@ -635,9 +649,20 @@ public sealed class Transition
}
}
- // Step-down failed: stay at current position.
+ // L.2.3e (2026-04-29): step-down failed — the move would put
+ // the player off an edge with no walkable surface within reach.
+ // Retail's EdgeSlide (ACE Transition.cs:268-320) maps this to
+ // SetEdgeSlide(true, true, OK) which restores CheckPos to the
+ // saved (post-step) position — but our outer ValidateTransition
+ // accepts CheckPos as the new CurPos, defeating the intent.
+ //
+ // Returning Collided here makes ValidateTransition revert to
+ // CurPos (pre-step) — the always-on retail "stop at edge"
+ // behavior. Confirmed against ACE Transition.cs:317 where
+ // EdgeSlide returns Collided when no walkable surface is
+ // found and the EdgeSlide flag is unset (player default).
sp.RestoreCheckPos();
- return TransitionState.OK;
+ return TransitionState.Collided;
}
return TransitionState.OK;
@@ -1082,6 +1107,14 @@ public sealed class Transition
/// normal variant). ACE: Sphere.SlideSphere(Transition, ref Vector3, Vector3).
/// Decompiled: FUN_00538180.
///
+ ///
+ /// L.2.3d: exposed as internal so
+ /// can apply the same slide computation ACE's Sphere.SlideSphere uses
+ /// for failed step-up. Mirror of ACE Sphere.cs:558-604 (Plane variant).
+ ///
+ internal TransitionState SlideSphereInternal(Vector3 collisionNormal, Vector3 currPos)
+ => SlideSphere(collisionNormal, currPos);
+
private TransitionState SlideSphere(Vector3 collisionNormal, Vector3 currPos)
{
var sp = SpherePath;
@@ -1356,6 +1389,23 @@ public sealed class Transition
var ci = CollisionInfo;
var oi = ObjectInfo;
+ // L.2.3f (2026-04-29): diagnostic for steep-roof bug. Log the
+ // collision normal Z so we can confirm whether the polygon being
+ // stepped up onto is "walkable" (normal.Z >= FloorZ ≈ 0.66) or
+ // not (≈ 0.087 LandingZ when not OnWalkable). If we see steep
+ // normals being accepted, the issue is in the find_walkable
+ // threshold rather than the StepUpHeight reach.
+ if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_STEPUP") == "1")
+ {
+ float floor = PhysicsGlobals.FloorZ;
+ string verdict = collisionNormal.Z >= floor ? "WALKABLE" : "STEEP";
+ Console.WriteLine(
+ $"stepup: normal=({collisionNormal.X:F3},{collisionNormal.Y:F3},{collisionNormal.Z:F3}) " +
+ $"|Z|={collisionNormal.Z:F3} vs FloorZ={floor:F3} → {verdict}, " +
+ $"OnWalkable={oi.State.HasFlag(ObjectInfoState.OnWalkable)}, " +
+ $"StepUpHeight={oi.StepUpHeight:F3}");
+ }
+
// L.2.3c (2026-04-29): capture the existing contact plane BEFORE
// clearing it. On step-up failure (too-tall wall) we restore it so
// the mover stays grounded — without this, walking into a wall