From a722c29759774cbbb09aea1a0bf4cdaf003bbd65 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 13 Apr 2026 14:05:03 +0200 Subject: [PATCH] feat(physics): re-enable indoor transitions with containment validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 2 of the audit remediation plan. Re-enables the outdoor→indoor portal transition in PhysicsEngine with an added containment check: after detecting a portal plane crossing, verify the target cell's floor polygon actually covers the candidate position AND the floor Z is within step height of the player's Z. This prevents the wall-bounce bug (where portal planes on upper floors captured outdoor positions) while allowing genuine doorway transitions. Without full CellBSP, the SampleFloorZ + Z-proximity check is the best available approximation per the indoor transition research (docs/research/acclient_indoor_transitions_pseudocode.md). Source: ACE EnvCell.find_transit_cells validates via sphere_intersects_cell in the target cell's local space. Our SampleFloorZ + Z check is the equivalent without BSP. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.Core/Physics/PhysicsEngine.cs | 28 ++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 802b470..36dacfc 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -187,7 +187,13 @@ public sealed class PhysicsEngine if (crossedPortal is not null) { - // Outdoor → Indoor: enter the OwnerCellId. + // Outdoor → Indoor: enter the OwnerCellId IF the target cell + // actually contains the candidate position. Without CellBSP, + // we verify by checking that SampleFloorZ returns non-null + // (position is within the cell's floor polygon bounds) AND the + // floor Z is close to the player's current Z (not a basement + // 30m below). This prevents the wall-bounce bug where portal + // planes on upper floors captured outdoor positions. uint enterCellIndex = crossedPortal.Value.OwnerCellId & 0xFFFFu; CellSurface? enterCell = null; foreach (var c in physics.Cells) @@ -195,8 +201,24 @@ public sealed class PhysicsEngine if ((c.CellId & 0xFFFFu) == enterCellIndex) { enterCell = c; break; } } float? enterFloorZ = enterCell?.SampleFloorZ(candidatePos.X, candidatePos.Y); - targetZ = enterFloorZ ?? terrainZ; - targetCellId = enterCellIndex; + + // Validate: floor must exist AND be within step height of current Z. + // This rejects transitions to basements, upper floors, and cells + // whose floor polygon doesn't actually cover this position. + bool validTransition = enterFloorZ is not null + && MathF.Abs(enterFloorZ.Value - currentPos.Z) < stepUpHeight + 2f; + + if (validTransition) + { + targetZ = enterFloorZ!.Value; + targetCellId = enterCellIndex; + } + else + { + // Portal crossed but target cell doesn't contain us — stay outdoor. + targetZ = terrainZ; + targetCellId = physics.Terrain.ComputeOutdoorCellId(localCandX, localCandY); + } } else {