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>
86 lines
3.5 KiB
C#
86 lines
3.5 KiB
C#
using System.Numerics;
|
|
|
|
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// A portal plane derived from an EnvCell's CellPortal polygon.
|
|
/// Used to detect when a player crosses from one cell into another.
|
|
/// </summary>
|
|
public readonly record struct PortalPlane(
|
|
Vector3 Normal,
|
|
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
|
|
Vector3 Centroid, // center of the portal polygon in world space
|
|
float Radius) // bounding radius of the portal polygon
|
|
{
|
|
/// <summary>
|
|
/// Construct a PortalPlane from ALL polygon vertices (winding order of the
|
|
/// first three determines the normal). Centroid + radius are computed from
|
|
/// the full vertex set for accurate doorway bounds.
|
|
/// </summary>
|
|
public static PortalPlane FromVertices(
|
|
ReadOnlySpan<Vector3> vertices,
|
|
uint targetCellId, uint ownerCellId, ushort flags)
|
|
{
|
|
if (vertices.Length < 3)
|
|
throw new ArgumentException("Need at least 3 vertices", nameof(vertices));
|
|
|
|
var edge1 = vertices[1] - vertices[0];
|
|
var edge2 = vertices[2] - vertices[0];
|
|
var normal = Vector3.Normalize(Vector3.Cross(edge1, edge2));
|
|
float d = -Vector3.Dot(normal, vertices[0]);
|
|
|
|
// Centroid = average of ALL vertices.
|
|
var sum = Vector3.Zero;
|
|
foreach (var v in vertices) sum += v;
|
|
var centroid = sum / vertices.Length;
|
|
|
|
// Bounding radius = max distance from centroid to any vertex.
|
|
// NO padding — we rely on the tight radius to prevent wall-bounce.
|
|
float maxR = 0f;
|
|
foreach (var v in vertices)
|
|
{
|
|
float r = Vector3.Distance(centroid, v);
|
|
if (r > maxR) maxR = r;
|
|
}
|
|
|
|
return new PortalPlane(normal, d, targetCellId, ownerCellId, flags, centroid, maxR);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convenience overload for 3-vertex portals (backwards-compatible with
|
|
/// existing test call sites).
|
|
/// </summary>
|
|
public static PortalPlane FromVertices(
|
|
Vector3 v0, Vector3 v1, Vector3 v2,
|
|
uint targetCellId, uint ownerCellId, ushort flags)
|
|
{
|
|
ReadOnlySpan<Vector3> verts = stackalloc Vector3[] { v0, v1, v2 };
|
|
return FromVertices(verts, targetCellId, ownerCellId, flags);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the movement from <paramref name="oldPos"/> to
|
|
/// <paramref name="newPos"/> 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.
|
|
/// </summary>
|
|
public bool IsCrossing(Vector3 oldPos, Vector3 newPos)
|
|
{
|
|
// Quick reject: 2D (XY) distance from the portal centroid.
|
|
// Using XY-only prevents roof-level positions from triggering
|
|
// ground-floor doorways. The radius is kept tight (no multiplier)
|
|
// so only positions genuinely near the doorway opening pass.
|
|
float dx = MathF.Min(MathF.Abs(oldPos.X - Centroid.X), MathF.Abs(newPos.X - Centroid.X));
|
|
float dy = MathF.Min(MathF.Abs(oldPos.Y - Centroid.Y), MathF.Abs(newPos.Y - Centroid.Y));
|
|
float minDist2D = MathF.Sqrt(dx * dx + dy * dy);
|
|
if (minDist2D > Radius) return false;
|
|
|
|
float oldDist = Vector3.Dot(Normal, oldPos) + D;
|
|
float newDist = Vector3.Dot(Normal, newPos) + D;
|
|
return oldDist * newDist < 0f;
|
|
}
|
|
}
|