fix(phys): A6.P3 #98 — gate ContactPlane assignment by Normal.Z (Shape 1)

Adds PhysicsGlobals.ContactPlaneFlatThreshold = 0.99f and uses it at
both BSPQuery.AdjustSphereToPlane call sites that previously set CP
unconditionally on any walkable polygon found by FindWalkableInternal.

Backed by the retail cdb capture in cellar_up_capture_1: across 161
set_contact_plane writes during 5 seconds of cellar-up climbing,
EVERY write lands on a flat (Normal.Z = 1.0) plane — cellar floor at
world Z=90.95 or cottage floor at world Z=94. The cellar ramp
(Normal.Z = 0.695, walkable per FloorZ but sloped ~46 degrees) is
never set as CP in retail.

Acdream's prior behavior of setting CP=ramp caused two cascading
issues at the top of the ramp:
1. AdjustOffset's slope-projection produced +Z gain per call (correct
   in isolation) but inflated step-up's responsibility to "find the
   next walkable below the lifted check position".
2. step-up's downward step-down probe found no walkable within 0.6m
   below the proposed check (cottage floor at Z=94 is ABOVE, not
   below), so step-down rejected, sphere rolled back. Infinite freeze
   at world Z ~= 92.80.

With CP only set on flat polygons, sloped surfaces drive collision
detection and walkable-poly tracking (via path.SetWalkable) but
don't override the resting CP. The sphere should now climb the ramp
via step-up over the ramp polygon, with CP staying on the flat
cellar floor until the sphere reaches the flat cottage floor.

Tests: 1167 + 8 baseline maintained. No regression. The Issue98
replay tests still pass — they document the failing-frame geometry
(sphere world Z=92.01 below cottage floor), which doesn't change;
the fix prevents the sphere from getting STUCK at that altitude in
the first place. Live visual verification required next.

If the live test shows new failure modes (sphere stuck somewhere
else, doesn't climb at all, climbs but slides off, etc), the
threshold (0.99) or the gating approach itself may need refining.
This is the conservative empirical version of Shape 1; the named-
decomp research did not conclusively prove the exact retail gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-23 16:46:12 +02:00
parent 8daf7e7e4d
commit 0cb4c59681
2 changed files with 31 additions and 2 deletions

View file

@ -1230,7 +1230,17 @@ public static class BSPQuery
var worldNormal = TransformNormal(polyHit.Plane.Normal, localToWorld);
var worldVertices = TransformVertices(polyHit.Vertices, localToWorld, scale, worldOrigin);
var worldPlane = BuildWorldPlane(worldNormal, worldVertices);
collisions.SetContactPlane(worldPlane, path.CheckCellId, false);
// A6.P3 #98 (2026-05-23): gate ContactPlane assignment by Normal.Z.
// Retail's cdb capture shows CP is only ever set to flat polygons
// (Normal.Z = 1.0); the cellar ramp (Normal.Z = 0.695) is never CP.
// Setting CP=ramp made AdjustOffset slope-project forward motion
// and caused the sphere to wedge at the ramp top when step-up's
// downward probe couldn't find cottage floor (which is ABOVE).
// Polygons that fail this gate still drive collision detection
// and walkable-polygon tracking — they just don't become CP.
if (worldNormal.Z >= PhysicsGlobals.ContactPlaneFlatThreshold)
collisions.SetContactPlane(worldPlane, path.CheckCellId, false);
path.SetWalkable(worldPlane, worldVertices, Vector3.UnitZ);
@ -1770,7 +1780,14 @@ public static class BSPQuery
var worldNormal = TransformNormal(hitPoly.Plane.Normal, localToWorld);
var worldVertices = TransformVertices(hitPoly.Vertices, localToWorld, scale, worldOrigin);
var worldPlane = BuildWorldPlane(worldNormal, worldVertices);
collisions.SetContactPlane(worldPlane, path.CheckCellId, false);
// A6.P3 #98 (2026-05-23): same Normal.Z gate as
// AdjustSphereToPlane above — only flat polygons become CP.
// Sloped walkable polygons (cellar ramp etc.) drive collision
// and walkable tracking but don't override the resting CP.
if (worldNormal.Z >= PhysicsGlobals.ContactPlaneFlatThreshold)
collisions.SetContactPlane(worldPlane, path.CheckCellId, false);
path.SetWalkable(worldPlane, worldVertices, Vector3.UnitZ);
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)

View file

@ -557,6 +557,18 @@ public static class PhysicsGlobals
public const float EpsilonSq = EPSILON * EPSILON;
public const float LandingZ = 0.0871557f;
public const float FloorZ = 0.6642f;
/// <summary>
/// A6.P3 #98 (2026-05-23). Threshold for "flat enough to become a ContactPlane".
/// Retail's cdb capture (cellar_up_capture_1) shows 161 set_contact_plane writes
/// across 5 seconds of cellar climbing — every single one has Normal.Z = 1.0
/// exactly. The cellar ramp polygon (Normal.Z = 0.695, walkable per FloorZ but
/// sloped at ~46°) is never set as CP. Acdream's BSPQuery.AdjustSphereToPlane
/// previously set CP unconditionally on any walkable polygon, causing the
/// sphere to wedge at the top of the cellar ramp (issue #98). 0.99 admits
/// near-axis floor irregularities (slopes up to ~8°) while excluding genuine
/// ramps.
/// </summary>
public const float ContactPlaneFlatThreshold = 0.99f;
public const float DefaultStepHeight = 0.01f;
public const float Gravity = -9.8f;
public const float MaxVelocity = 50.0f;