diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index aaf5a48..be3f76c 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -261,6 +261,45 @@ public sealed class PhysicsEngine // Indoor branch needs DataCache to look up cells; outdoor uses // _landblocks (no DataCache dependency). if (DataCache is null) return fallbackCellId; + + // ── 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). + // + // 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. + // + // 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. + // + // 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) + 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. + } + + // Fallback cell no longer valid → re-resolve via portal-graph BFS. uint indoorResult = CellTransit.FindCellList(DataCache, worldPos, sphereRadius, fallbackCellId); // ISSUES #83 / Phase A1.7 (2026-05-21): verify the indoor result