diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 5061f34..eab87de 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -261,7 +261,32 @@ public sealed class PhysicsEngine // Indoor branch needs DataCache to look up cells; outdoor uses // _landblocks (no DataCache dependency). if (DataCache is null) return fallbackCellId; - return CellTransit.FindCellList(DataCache, worldPos, sphereRadius, fallbackCellId); + uint indoorResult = CellTransit.FindCellList(DataCache, worldPos, sphereRadius, fallbackCellId); + + // ISSUES #83 / Phase A1.7 (2026-05-21): verify the indoor result + // actually contains the player. CellTransit.FindCellList falls back + // to currentCellId when no candidate cell's CellBSP contains the + // sphere center — but this happens even when the player has walked + // OUTSIDE the entire portal-connected indoor cell graph (e.g., + // exited through an unblocked wall or doorway gap). In that state + // the player's CellId is stuck on an indoor cell whose BSP is + // far away, every indoor-bsp query returns OK (NodeIntersects + // fails at root), and no walls block. + // + // If the resolved indoor cell's BSP does NOT contain the sphere + // center, fall through to the outdoor cell resolution below — it + // will compute the correct landcell from the terrain grid and + // optionally re-enter an indoor cell via CheckBuildingTransit. + var indoorCell = DataCache.GetCellStruct(indoorResult); + if (indoorCell?.CellBSP?.Root is null) + return indoorResult; // Can't verify (no CellBSP); trust FindCellList. + + var localCenter = Vector3.Transform(worldPos, indoorCell.InverseWorldTransform); + if (BSPQuery.PointInsideCellBsp(indoorCell.CellBSP.Root, localCenter)) + return indoorResult; + + // Fall through to outdoor resolution: player has left the indoor + // portal-connected graph entirely. } // Outdoor seed: use terrain grid to compute the prefixed cell id.