diff --git a/src/AcDream.Core/Physics/CellTransit.cs b/src/AcDream.Core/Physics/CellTransit.cs
index bd62333..c7d9cf0 100644
--- a/src/AcDream.Core/Physics/CellTransit.cs
+++ b/src/AcDream.Core/Physics/CellTransit.cs
@@ -155,25 +155,19 @@ public static class CellTransit
/// BuildingObj::find_building_transit_cells +
/// EnvCell::check_building_transit. For each portal of the
/// outdoor building, look up the destination interior cell and test
- /// whether the sphere center is inside it via
- /// . If so, add the interior
- /// cell to .
+ /// whether the sphere overlaps it via
+ /// . If so, add the
+ /// interior cell to .
///
///
- /// Retail divergence: retail's check_building_transit
- /// uses CCellStruct::sphere_intersects_cell (radius-aware
- /// BSP-vs-sphere test) which fires the moment ANY part of the sphere
- /// overlaps the destination cell. Our port uses
- /// (radius-less, tests only
- /// the sphere CENTER). Practical effect: entry into a building fires
- /// when the player's foot-sphere center crosses the destination cell
- /// boundary — roughly (~0.48m) DEEPER
- /// into the doorway than retail. If visual verification at the cottage
- /// door shows a noticeable "late entry" effect (player visually inside
- /// the building before walls switch from outdoor-stab to indoor-cell),
- /// port sphere_intersects_cell in a follow-up.
- /// is plumbed through for that future
- /// upgrade; currently unused.
+ /// Issue #89 closed (2026-05-20): uses retail's radius-aware
+ /// CCellStruct::sphere_intersects_cell
+ /// (acclient_2013_pseudo_c.txt:317666) ported as
+ /// . Promotes CellId to
+ /// the interior cell the moment ANY part of the foot-sphere crosses
+ /// the cell boundary — matches retail entry timing exactly and
+ /// closes the login-inside-inn classification race where the player
+ /// would briefly be classified outdoor and walk through walls.
///
///
public static void CheckBuildingTransit(
@@ -198,13 +192,22 @@ public static class CellTransit
}
// Sphere center in the OTHER cell's local space.
+ // Issue #89 closed (2026-05-20): use radius-aware sphere-overlap
+ // (matches retail's CCellStruct::sphere_intersects_cell at
+ // acclient_2013_pseudo_c.txt:317666) instead of point-only. This
+ // promotes the player's CellId to the interior cell the moment
+ // ANY part of the foot-sphere crosses the cell boundary — the
+ // entry-side counterpart to issue #90's sticky-stay fix. Without
+ // it, login-inside-the-inn keeps the player classified outdoor
+ // until they walk further in (sphere center crosses), letting
+ // them run through exterior walls on the way out.
var localCenter = Vector3.Transform(worldSphereCenter, otherCell.InverseWorldTransform);
- bool inside = BSPQuery.PointInsideCellBsp(otherCell.CellBSP.Root, localCenter);
+ bool inside = BSPQuery.SphereIntersectsCellBsp(otherCell.CellBSP.Root, localCenter, sphereRadius);
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
- $"[check-bldg] portal->0x{portal.OtherCellId:X8} wpos=({worldSphereCenter.X:F3},{worldSphereCenter.Y:F3},{worldSphereCenter.Z:F3}) lpos=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) inside={inside}"));
+ $"[check-bldg] portal->0x{portal.OtherCellId:X8} wpos=({worldSphereCenter.X:F3},{worldSphereCenter.Y:F3},{worldSphereCenter.Z:F3}) lpos=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) r={sphereRadius:F3} inside={inside}"));
}
if (inside)