From 0cb4c596811e36fe8fdaa033c347f41312db9588 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 May 2026 16:46:12 +0200 Subject: [PATCH] =?UTF-8?q?fix(phys):=20A6.P3=20#98=20=E2=80=94=20gate=20C?= =?UTF-8?q?ontactPlane=20assignment=20by=20Normal.Z=20(Shape=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core/Physics/BSPQuery.cs | 21 +++++++++++++++++++-- src/AcDream.Core/Physics/TransitionTypes.cs | 12 ++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs index e5cddc0..10033e3 100644 --- a/src/AcDream.Core/Physics/BSPQuery.cs +++ b/src/AcDream.Core/Physics/BSPQuery.cs @@ -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) diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 7d5b2b2..5d6d076 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -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; + /// + /// 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. + /// + public const float ContactPlaneFlatThreshold = 0.99f; public const float DefaultStepHeight = 0.01f; public const float Gravity = -9.8f; public const float MaxVelocity = 50.0f;