diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index be3f76c..f8eefb4 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -264,39 +264,43 @@ public sealed class PhysicsEngine // ── Cell-stickiness check (A6.P3 slice 3, 2026-05-22) ── // Before re-resolving via FindCellList, check if the fallback - // CellId still validly contains the sphere. If yes, prefer it - // over any FindCellList result that might pick a different - // overlapping cell — fixes issue #98 (cellar-up stuck at last - // step due to CellId ping-ponging between adjacent cells whose - // BSPs all overlap the sphere). + // CellId still validly contains the sphere CENTER (point-in). + // If yes, prefer it over any FindCellList result that might + // pick a different cell whose BSP also contains the point — + // fixes issue #98 (cellar-up stuck at last step due to CellId + // ping-ponging between adjacent cells in iteration-order races). // - // Mechanism: when the sphere is on a cell boundary where it - // overlaps multiple cells, FindCellList's candidate-iteration - // order (HashSet, implementation-defined) determines which - // cell wins. That order may shift tick-to-tick → ping-pong. - // Stickiness ensures we keep the CURRENT cell as long as the - // sphere still validly overlaps it, only switching when the - // sphere has truly left. + // Mechanism: when the sphere is on a cell boundary where the + // CENTER is in multiple cells geometrically (overlapping BSPs), + // FindCellList's candidate-iteration order (HashSet, + // implementation-defined) determines which cell wins. That + // order may shift tick-to-tick → ping-pong. Stickiness keeps + // the CURRENT cell as long as the sphere center is still + // inside it, only switching when the center has moved out. + // + // Uses POINT-IN (not sphere-overlap). Sphere-overlap stickiness + // (the first slice 3 attempt) over-corrected — held the player + // in the fallback cell even when the center had transitioned to + // an adjacent cell, blocking legitimate cell transitions at + // stair tops + portal exits. Point-in matches FindCellList's + // own semantics for "which cell are you in." // // Retail oracle: cell-array hysteresis pattern from // CObjCell::find_cell_list Position-variant at - // acclient_2013_pseudo_c.txt:308742-308783. Retail's cell - // resolution preserves cell membership when the sphere is - // close to (but slightly past) cell boundaries. + // acclient_2013_pseudo_c.txt:308742-308783. // // Likely closes/obsoletes: // - #98 (cellar ascent stuck at last step) — direct target // - #97 (phantom collisions + fall-through on 2nd floor) — // same instability family hypothesized - // - #90 (sphere-overlap stickiness workaround in this same - // function below) — superseded; can be removed after - // visual verification (deferred to A6.P4) + // - #90 (sphere-overlap workaround below) — superseded; + // can be removed after visual verification (A6.P4) var fallbackCell = DataCache.GetCellStruct(fallbackCellId); if (fallbackCell?.CellBSP?.Root is not null) { var fallbackLocal = Vector3.Transform(worldPos, fallbackCell.InverseWorldTransform); - if (BSPQuery.SphereIntersectsCellBsp(fallbackCell.CellBSP.Root, fallbackLocal, sphereRadius)) - return fallbackCellId; // sphere still overlaps; stick. + if (BSPQuery.PointInsideCellBsp(fallbackCell.CellBSP.Root, fallbackLocal)) + return fallbackCellId; // center still inside; stick. } // Fallback cell no longer valid → re-resolve via portal-graph BFS.