fix(physics): L.2.3i — use FloorZ (not LandingZ) for OnWalkable test

Two parallel research agents converged on this bug. acdream's
ValidateTransition was setting OnWalkable based on `Normal.Z >= LandingZ`
(0.087, ~85° permissive) instead of `Normal.Z >= FloorZ` (0.664, ~49°
strict). Effect: a 60° roof slope (normal.Z = 0.5) was being marked
OnWalkable, letting the player walk freely up surfaces retail blocks.

Per retail PhysicsObj::is_valid_walkable
(acclient_2013_pseudo_c.txt:277180-277193) and ACE
PhysicsObj.cs:2861, the canonical "walkable" predicate is FloorZ.
LandingZ is the more permissive threshold used only in airborne→ground
transitions (Path 6 Collide handler) where we want to accept a brief
landing before the next frame's strict FloorZ check rejects the surface
and CliffSlide kicks in.

Three sites fixed:
1. Step-down branch's `zVal` initial value (was unconditional LandingZ;
   now `oi.GetWalkableZ()` returns FloorZ when OnWalkable, LandingZ
   otherwise — matches retail's transitional_insert step-down OK
   branch at acclient_2013_pseudo_c.txt:273258-273265).
2. ValidateTransition's live-contact OnWalkable test (LandingZ → FloorZ).
3. ValidateTransition's LastKnown-fallback OnWalkable test (LandingZ →
   FloorZ).

After this commit:
  - Walking horizontally INTO a 60° slope: step-up's WalkableAllowance
    is FloorZ (when OnWalkable), find_walkable rejects the slope's
    polygon, step-up fails, StepUpSlide. Player blocked from climbing.
  - Jumping ONTO a 60° roof: Path 6 still uses LandingZ (correct, we
    want to land), so the player lands. Next frame: ValidateTransition
    sees Normal.Z=0.5 < FloorZ → OnWalkable cleared. Player is Contact
    but not OnWalkable. Currently this leaves them STUCK on the roof
    (no CliffSlide yet to push them off). That's still better than
    walking up the roof.

Full slide-off-roof + edge-slide-along-balcony behaviors require
porting CliffSlide + PrecipiceSlide + adding Walkable polygon
reference — that's Phase L.4 (~12-20h, sketched out by both research
agents). This commit unblocks the worst of the steep-walk-up behavior
while the bigger port is being designed.

Test count 825/825 still pass. Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 19:28:30 +02:00
parent 4cbfe0a5f8
commit e44d24cec6

View file

@ -623,7 +623,15 @@ public sealed class Transition
if (!ci.ContactPlaneValid && oi.Contact && !sp.StepDown
&& sp.CheckCellId != 0 && oi.StepDown)
{
float zVal = PhysicsGlobals.LandingZ;
// L.2.3i (2026-04-29): retail uses FloorZ when OnWalkable,
// LandingZ when not. acdream was unconditionally LandingZ —
// which let the step-down probe accept steep polygons
// (~85° permissive instead of ~49° strict) as the player's
// new contact, contributing to the "walks up steep roofs"
// bug. Per CTransition::transitional_insert step-down OK
// branch (acclient_2013_pseudo_c.txt:273258-273265) and
// ACE Transition.cs:849-856.
float zVal = oi.GetWalkableZ();
float stepDownHeight = oi.StepDownHeight;
sp.WalkableAllowance = zVal;
sp.SaveCheckPos();
@ -1638,7 +1646,14 @@ public sealed class Transition
ci.LastKnownContactPlaneIsWater = ci.ContactPlaneIsWater;
oi.State |= ObjectInfoState.Contact;
if (ci.ContactPlane.Normal.Z >= PhysicsGlobals.LandingZ)
// L.2.3i (2026-04-29): use FloorZ (~49°) NOT LandingZ (~85°)
// for the OnWalkable test. The previous LandingZ check was
// far too permissive — a 60° roof (normal.Z=0.5) was being
// marked OnWalkable, letting the player walk up steep slopes
// they shouldn't reach. Retail's PhysicsObj::is_valid_walkable
// uses FloorZ unconditionally (acclient_2013_pseudo_c.txt:277180-277193,
// ACE PhysicsObj.cs:2861).
if (ci.ContactPlane.Normal.Z >= PhysicsGlobals.FloorZ)
oi.State |= ObjectInfoState.OnWalkable;
else
oi.State &= ~ObjectInfoState.OnWalkable;
@ -1651,7 +1666,8 @@ public sealed class Transition
// last-known plane. Without this, every wall bump dropped the
// player into the falling animation for one frame.
oi.State |= ObjectInfoState.Contact;
if (ci.LastKnownContactPlane.Normal.Z >= PhysicsGlobals.LandingZ)
// L.2.3i: same FloorZ correction as the live-contact branch.
if (ci.LastKnownContactPlane.Normal.Z >= PhysicsGlobals.FloorZ)
oi.State |= ObjectInfoState.OnWalkable;
else
oi.State &= ~ObjectInfoState.OnWalkable;