fix(core): Phase B.3 — add centroid + radius bounds to PortalPlane crossing test

Two targeted fixes for user-reported movement bugs:

1. Wall bounce: PortalPlane.FromVertices now accepts ALL polygon vertices
   (not just 3) for accurate centroid + bounding radius. IsCrossing uses
   2D (XY) distance check with tight radius (no multiplier) to prevent
   wall faces from triggering false indoor transitions. Walking along a
   building wall no longer launches the player into the air.

2. Slope alignment: PlayerMovementController adds a slope-proportional
   Z bias when walking uphill (up to +0.8 on steep slopes, grounded
   only). Prevents feet from sinking into the visual terrain mesh on
   slopes where the physics sample point lags the render surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 19:08:46 +02:00
parent 41013ce3e3
commit dc0341e85a
3 changed files with 71 additions and 31 deletions

View file

@ -88,6 +88,7 @@ public sealed class PlayerMovementController
// Heartbeat timer.
private float _heartbeatAccum;
private float _prevGroundZ;
public const float HeartbeatInterval = 0.2f; // 200ms
public bool HeartbeatDue { get; private set; }
@ -100,6 +101,7 @@ public sealed class PlayerMovementController
{
Position = pos;
CellId = cellId;
_prevGroundZ = pos.Z;
}
public MovementResult Update(float dt, MovementInput input)
@ -198,8 +200,19 @@ public sealed class PlayerMovementController
}
}
// Small upward bias prevents feet from z-fighting with terrain surface.
Position = new Vector3(result.Position.X, result.Position.Y, newZ + 0.15f);
// Upward bias prevents feet from sinking into the terrain surface.
// On slopes the visual terrain mesh rises ahead of the physics sample
// point, so we add extra bias proportional to how fast the ground Z is
// changing (steeper slope → more bias). Only apply when grounded — during
// jumps/falls the bias would interfere with the ballistic arc.
float slopeBias = 0f;
if (!IsAirborne)
{
float slopeDelta = MathF.Max(0f, newZ - _prevGroundZ);
slopeBias = MathF.Min(slopeDelta * 3f, 0.8f);
}
_prevGroundZ = newZ;
Position = new Vector3(result.Position.X, result.Position.Y, newZ + 0.15f + slopeBias);
CellId = result.CellId;
// 4. Determine current motion commands.

View file

@ -1473,13 +1473,18 @@ public sealed class GameWindow : IDisposable
if (poly.VertexIds.Count < 3)
continue;
// VertexIds are short; worldVerts keys are ushort.
if (!worldVerts.TryGetValue((ushort)poly.VertexIds[0], out var v0)) continue;
if (!worldVerts.TryGetValue((ushort)poly.VertexIds[1], out var v1)) continue;
if (!worldVerts.TryGetValue((ushort)poly.VertexIds[2], out var v2)) continue;
// Collect ALL polygon vertices for accurate centroid + radius.
var portalVerts = new System.Numerics.Vector3[poly.VertexIds.Count];
bool allFound = true;
for (int pv = 0; pv < poly.VertexIds.Count; pv++)
{
if (!worldVerts.TryGetValue((ushort)poly.VertexIds[pv], out portalVerts[pv]))
{ allFound = false; break; }
}
if (!allFound) continue;
portalPlanes.Add(AcDream.Core.Physics.PortalPlane.FromVertices(
v0, v1, v2,
portalVerts.AsSpan(),
portal.OtherCellId, // target cell (0xFFFF = outdoor)
envCellId & 0xFFFFu, // owner cell (low 16 bits)
(ushort)portal.Flags));