fix(physics): L.2.3d/e/f — wall slide, edge block, step-up diagnostic
Three follow-up fixes from live testing of the L.2.3 step-height pass. L.2.3d — StepUpSlide actually applies the slide Previously SpherePath.StepUpSlide only set ci.SlidingNormal as a flag and returned Slid; the CURRENT step's CheckPos was never adjusted, so the sphere stopped dead at the wall. ValidateTransition's "default to UnitZ" branch then propagated UnitZ into SlidingNormal, overwriting the wall normal entirely. Net effect: stop-at-wall, no horizontal slide. ACE's StepUpSlide (SpherePath.cs:309-317) calls Sphere.SlideSphere which computes the actual slide offset against the contact-plane / wall-normal crease and applies it to CheckPos. acdream already had the same logic in Transition.SlideSphere as a private helper. Exposed as internal SlideSphereInternal; routed StepUpSlide through it. L.2.3e — step-down failure returns Collided (always-on edge block) When walking forward off a balcony / cliff, the step-down probe in TransitionalInsert searches stepDownHeight below CheckPos for a walkable surface. On failure the previous code returned OK, which ValidateTransition accepted — the player walked off the edge anyway, with `RestoreCheckPos` reverting only to the position right after the outer step's offset (still post-edge). Per ACE Transition.cs:268-320 (EdgeSlide), retail's always-on default for OnWalkable + !EdgeSlide-flag movers is to reject the move. Returning Collided here makes ValidateTransition revert CheckPos to CurPos (pre-step), giving the retail-faithful "stop at edge" behavior — both on terrain cliffs and on building/balcony edges. L.2.3f — diagnostic instrumentation for steep-roof investigation GameWindow logs the player's actual StepUpHeight + StepDownHeight at world-entry (along with the raw Setup.* values for comparison) so we can confirm whether the dat-derived value matches retail's spec (~0.4m) or is overriding to something larger. Transition.DoStepUp logs the polygon's collision-normal Z (gated on ACDREAM_DUMP_STEPUP=1 to keep cold-path noise low) so we can tell whether step-up is being triggered against truly-walkable polygons (Z >= FloorZ ≈ 0.66) or whether something steeper is sneaking through. Tests: 825/825 still pass. The L.2 conformance fixtures cover the slide path; D1 + D2 regression tests still pass with the StepUpSlide port. Live verification needed for: - #2 Wall slide: running close to a wall should slide along it. - #4 Edge block: running off a balcony should stop at the edge. - #3 Steep roof: launch with ACDREAM_DUMP_STEPUP=1 and report the "stepup: normal=..." log lines when climbing the offending roof. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2f6067960
commit
8fe178ee5c
3 changed files with 71 additions and 10 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -248,15 +248,29 @@ public sealed class SpherePath
|
|||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>
|
||||
/// L.2.3d (2026-04-29): the previous version only set <see cref="SlidingNormal"/>
|
||||
/// 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 <see cref="CheckPos"/> via <c>Sphere.SlideSphere</c>;
|
||||
/// we delegate to <see cref="Transition.SlideSphereInternal"/> which does
|
||||
/// the same thing.
|
||||
/// </para>
|
||||
///
|
||||
/// ACE: <c>SpherePath.StepUpSlide</c> + <c>Sphere.SlideSphere</c>
|
||||
/// (SpherePath.cs:309-317, Sphere.cs:558-604).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// L.2.3d: exposed as <c>internal</c> so <see cref="SpherePath.StepUpSlide"/>
|
||||
/// can apply the same slide computation ACE's <c>Sphere.SlideSphere</c> uses
|
||||
/// for failed step-up. Mirror of ACE Sphere.cs:558-604 (Plane variant).
|
||||
/// </summary>
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue