From 9dbb2cbd5c76d23ed4efa80c492022411064e5b4 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 12 Apr 2026 18:39:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(core):=20Phase=20B.3=20=E2=80=94=20add=20ce?= =?UTF-8?q?ntroid=20+=20radius=20bounds=20to=20PortalPlane=20crossing=20te?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Portal planes are infinite — IsCrossing only checked if positions were on opposite sides of the plane, without verifying the crossing point was near the actual portal opening. Walking 50m from a building but crossing the same plane extension triggered false indoor transitions, sinking the player into underground cell floors. Fix: FromVertices now computes centroid (average of 3 vertices) and bounding radius (max vertex distance + 2 unit padding). IsCrossing rejects crossings where both positions are further than 2×radius from the centroid. Only nearby crossings (within doorway range) trigger a transition. Also fixes jump-not-landing: the false portal transitions were producing wrong resolvedGroundZ values during the jump arc, making the landing check (candidateZ <= resolvedGroundZ) never true. 283 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.Core/Physics/PortalPlane.cs | 35 +++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/AcDream.Core/Physics/PortalPlane.cs b/src/AcDream.Core/Physics/PortalPlane.cs index f81611b..acdfd96 100644 --- a/src/AcDream.Core/Physics/PortalPlane.cs +++ b/src/AcDream.Core/Physics/PortalPlane.cs @@ -11,11 +11,15 @@ public readonly record struct PortalPlane( float D, uint TargetCellId, // OtherCellId — the cell on the far side (0xFFFF = outdoor) uint OwnerCellId, // the EnvCell that owns this portal - ushort Flags) // PortalFlags value + ushort Flags, // PortalFlags value + Vector3 Centroid, // center of the portal polygon in world space + float Radius) // bounding radius of the portal polygon { /// /// Construct a PortalPlane from three coplanar vertices (winding order - /// determines the normal direction via cross product). + /// determines the normal direction via cross product). Also computes + /// the centroid and bounding radius from the vertices so + /// can reject crossings far from the portal. /// public static PortalPlane FromVertices( Vector3 v0, Vector3 v1, Vector3 v2, @@ -25,21 +29,36 @@ public readonly record struct PortalPlane( var edge2 = v2 - v0; var normal = Vector3.Normalize(Vector3.Cross(edge1, edge2)); float d = -Vector3.Dot(normal, v0); - return new PortalPlane(normal, d, targetCellId, ownerCellId, flags); + + // Centroid = average of the three vertices. + var centroid = (v0 + v1 + v2) / 3f; + // Bounding radius = max distance from centroid to any vertex + padding. + float r0 = Vector3.Distance(centroid, v0); + float r1 = Vector3.Distance(centroid, v1); + float r2 = Vector3.Distance(centroid, v2); + float radius = MathF.Max(r0, MathF.Max(r1, r2)) + 2f; // 2 unit padding + + return new PortalPlane(normal, d, targetCellId, ownerCellId, flags, centroid, radius); } /// /// Returns true when the movement from to - /// crosses this plane (the two positions are on - /// strictly opposite sides). A position exactly on the plane (distance = 0) - /// does NOT count as a crossing. + /// crosses this plane AND both positions are + /// within the portal's bounding radius of its centroid. Without the + /// radius check, infinite planes cause false transitions when walking + /// far from a doorway but on the same plane extension. /// public bool IsCrossing(Vector3 oldPos, Vector3 newPos) { + // Quick reject: if both positions are far from the portal, skip + // the plane test entirely. Use the closer of the two positions. + float distOld = Vector3.Distance(oldPos, Centroid); + float distNew = Vector3.Distance(newPos, Centroid); + float minDist = MathF.Min(distOld, distNew); + if (minDist > Radius * 2f) return false; + float oldDist = Vector3.Dot(Normal, oldPos) + D; float newDist = Vector3.Dot(Normal, newPos) + D; - // Strictly negative product → opposite signs → crossed the plane. - // If either distance is exactly 0 the product is 0, not negative → no crossing. return oldDist * newDist < 0f; } }