diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 984d5a8..29cdf9f 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -107,13 +107,20 @@ public sealed class PhysicsEngine float targetZ; uint targetCellId; - bool currentlyIndoor = cellId >= 0x0100; + // Only the low 16 bits of cellId carry the cell index. Outdoor + // cells are 0x0001–0x0040; indoor (EnvCell) cells are 0x0100+. + // The full 32-bit cellId includes the landblock prefix in the + // high 16 bits (e.g., 0xA9B40001), so we MUST mask before + // comparing. Without the mask, every cell looks "indoor" because + // 0xA9B40001 >= 0x0100 → the engine always takes the "stay + // indoors" path and snaps Z to an EnvCell floor 28m below. + bool currentlyIndoor = (cellId & 0xFFFFu) >= 0x0100; if (currentlyIndoor && bestCellZ is not null) { // Stay indoors on the best cell's floor. targetZ = bestCellZ.Value; - targetCellId = bestCell!.CellId; + targetCellId = bestCell!.CellId & 0xFFFFu; } else if (currentlyIndoor && bestCellZ is null) { @@ -126,7 +133,7 @@ public sealed class PhysicsEngine { // Walked into an indoor cell from outdoor — transition to indoor. targetZ = bestCellZ.Value; - targetCellId = bestCell!.CellId; + targetCellId = bestCell!.CellId & 0xFFFFu; } else {