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;