fix(physics): #32 L.2c precipice edge-slide context
Port the first retail precipice-slide slice from named retail/ACE: terrain and BSP walkable hits now preserve polygon vertices, failed step-down edges back-probe to rediscover the walkable polygon, and edge-slide can run precipice/cliff slide instead of only hard-stopping. Adds pseudocode anchors plus regression coverage for terrain polygon context and loaded-terrain boundary edge-slide. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
1ec40f2a4f
commit
261322b48e
10 changed files with 559 additions and 60 deletions
|
|
@ -377,30 +377,33 @@ public static class BSPQuery
|
|||
///
|
||||
/// <para>ACE: Polygon.cs find_crossed_edge.</para>
|
||||
/// </summary>
|
||||
private static bool FindCrossedEdge(
|
||||
ResolvedPolygon poly,
|
||||
CollisionSphere sphere,
|
||||
Vector3 up,
|
||||
ref Vector3 normal)
|
||||
internal static bool FindCrossedEdge(
|
||||
Plane polyPlane,
|
||||
ReadOnlySpan<Vector3> verts,
|
||||
Vector3 sphereCenter,
|
||||
Vector3 up,
|
||||
out Vector3 normal)
|
||||
{
|
||||
float angleUp = Vector3.Dot(poly.Plane.Normal, up);
|
||||
normal = Vector3.Zero;
|
||||
|
||||
float angleUp = Vector3.Dot(polyPlane.Normal, up);
|
||||
if (MathF.Abs(angleUp) < PhysicsGlobals.EPSILON) return false;
|
||||
|
||||
float angle = (Vector3.Dot(poly.Plane.Normal, sphere.Center) + poly.Plane.D) / angleUp;
|
||||
var center = sphere.Center - up * angle;
|
||||
float angle = (Vector3.Dot(polyPlane.Normal, sphereCenter) + polyPlane.D) / angleUp;
|
||||
var center = sphereCenter - up * angle;
|
||||
|
||||
int n = poly.Vertices.Length;
|
||||
int n = verts.Length;
|
||||
int prevIdx = n - 1;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var v = poly.Vertices[i];
|
||||
var lv = poly.Vertices[prevIdx];
|
||||
var v = verts[i];
|
||||
var lv = verts[prevIdx];
|
||||
prevIdx = i;
|
||||
|
||||
var edge = v - lv;
|
||||
var disp = center - lv;
|
||||
var cross = Vector3.Cross(poly.Plane.Normal, edge);
|
||||
var cross = Vector3.Cross(polyPlane.Normal, edge);
|
||||
|
||||
if (Vector3.Dot(disp, cross) < 0f)
|
||||
{
|
||||
|
|
@ -412,6 +415,47 @@ public static class BSPQuery
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool FindCrossedEdge(
|
||||
ResolvedPolygon poly,
|
||||
CollisionSphere sphere,
|
||||
Vector3 up,
|
||||
ref Vector3 normal)
|
||||
{
|
||||
if (!FindCrossedEdge(poly.Plane, poly.Vertices, sphere.Center, up, out var crossedNormal))
|
||||
return false;
|
||||
|
||||
normal = crossedNormal;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vector3 TransformNormal(Vector3 normal, Quaternion localToWorld)
|
||||
{
|
||||
var worldNormal = Vector3.Transform(normal, localToWorld);
|
||||
return worldNormal.LengthSquared() > PhysicsGlobals.EpsilonSq
|
||||
? Vector3.Normalize(worldNormal)
|
||||
: Vector3.UnitZ;
|
||||
}
|
||||
|
||||
private static Vector3[] TransformVertices(
|
||||
ReadOnlySpan<Vector3> vertices,
|
||||
Quaternion localToWorld,
|
||||
float scale,
|
||||
Vector3 worldOrigin)
|
||||
{
|
||||
var result = new Vector3[vertices.Length];
|
||||
for (int i = 0; i < vertices.Length; i++)
|
||||
result[i] = Vector3.Transform(vertices[i] * scale, localToWorld) + worldOrigin;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Plane BuildWorldPlane(Vector3 worldNormal, ReadOnlySpan<Vector3> worldVertices)
|
||||
{
|
||||
float d = worldVertices.Length > 0
|
||||
? -Vector3.Dot(worldNormal, worldVertices[0])
|
||||
: 0f;
|
||||
return new Plane(worldNormal, d);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// adjust_to_placement_poly
|
||||
// ACE: Polygon.cs adjust_to_placement_poly
|
||||
|
|
@ -1037,7 +1081,8 @@ public static class BSPQuery
|
|||
CollisionSphere checkPos,
|
||||
Vector3 up,
|
||||
float scale,
|
||||
Quaternion localToWorld = default)
|
||||
Quaternion localToWorld = default,
|
||||
Vector3 worldOrigin = default)
|
||||
{
|
||||
if (localToWorld == default) localToWorld = Quaternion.Identity;
|
||||
|
||||
|
|
@ -1061,14 +1106,12 @@ public static class BSPQuery
|
|||
var offset = Vector3.Transform(adjusted, localToWorld) * scale;
|
||||
path.AddOffsetToCheckPos(offset);
|
||||
|
||||
var worldNormal = Vector3.Transform(polyHit.Plane.Normal, localToWorld);
|
||||
collisions.SetContactPlane(
|
||||
new Plane(worldNormal, polyHit.Plane.D * scale),
|
||||
path.CheckCellId, false);
|
||||
var worldNormal = TransformNormal(polyHit.Plane.Normal, localToWorld);
|
||||
var worldVertices = TransformVertices(polyHit.Vertices, localToWorld, scale, worldOrigin);
|
||||
var worldPlane = BuildWorldPlane(worldNormal, worldVertices);
|
||||
collisions.SetContactPlane(worldPlane, path.CheckCellId, false);
|
||||
|
||||
path.WalkableValid = true;
|
||||
path.WalkablePlane = new Plane(worldNormal, polyHit.Plane.D * scale);
|
||||
path.WalkableAllowance = PhysicsGlobals.FloorZ;
|
||||
path.SetWalkable(worldPlane, worldVertices, Vector3.UnitZ);
|
||||
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
|
@ -1359,7 +1402,8 @@ public static class BSPQuery
|
|||
Vector3 localSpaceZ,
|
||||
float scale,
|
||||
Quaternion localToWorld = default,
|
||||
PhysicsEngine? engine = null)
|
||||
PhysicsEngine? engine = null,
|
||||
Vector3 worldOrigin = default)
|
||||
{
|
||||
if (root is null) return TransitionState.OK;
|
||||
// Default quaternion (0,0,0,0) → treat as identity
|
||||
|
|
@ -1410,7 +1454,7 @@ public static class BSPQuery
|
|||
// ----------------------------------------------------------------
|
||||
if (path.StepDown)
|
||||
{
|
||||
return StepSphereDown(root, resolved, transition, sphere0, localSpaceZ, scale, localToWorld);
|
||||
return StepSphereDown(root, resolved, transition, sphere0, localSpaceZ, scale, localToWorld, worldOrigin);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
|
@ -1433,14 +1477,12 @@ public static class BSPQuery
|
|||
var worldOffset = L2W(localOffset) * scale;
|
||||
path.AddOffsetToCheckPos(worldOffset);
|
||||
|
||||
var worldNormal = L2W(hitPoly.Plane.Normal);
|
||||
collisions.SetContactPlane(
|
||||
new Plane(worldNormal, hitPoly.Plane.D * scale),
|
||||
path.CheckCellId, false);
|
||||
var worldNormal = TransformNormal(hitPoly.Plane.Normal, localToWorld);
|
||||
var worldVertices = TransformVertices(hitPoly.Vertices, localToWorld, scale, worldOrigin);
|
||||
var worldPlane = BuildWorldPlane(worldNormal, worldVertices);
|
||||
collisions.SetContactPlane(worldPlane, path.CheckCellId, false);
|
||||
|
||||
path.WalkableValid = true;
|
||||
path.WalkablePlane = new Plane(worldNormal, hitPoly.Plane.D * scale);
|
||||
path.WalkableAllowance = PhysicsGlobals.FloorZ;
|
||||
path.SetWalkable(worldPlane, worldVertices, Vector3.UnitZ);
|
||||
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ using System.Numerics;
|
|||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
internal readonly record struct TerrainWalkableSample(
|
||||
System.Numerics.Plane Plane,
|
||||
Vector3[] Vertices,
|
||||
float WaterDepth,
|
||||
bool IsWater,
|
||||
uint CellId);
|
||||
|
||||
/// <summary>
|
||||
/// Top-level physics resolver that combines <see cref="TerrainSurface"/> and
|
||||
/// <see cref="CellSurface"/> to resolve entity movement with step-height
|
||||
|
|
@ -162,6 +169,51 @@ public sealed class PhysicsEngine
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sample the outdoor terrain walkable triangle at the given world-space
|
||||
/// XY position. This carries the same plane as <see cref="SampleTerrainPlane"/>
|
||||
/// plus world-space triangle vertices for retail precipice-slide.
|
||||
/// </summary>
|
||||
internal TerrainWalkableSample? SampleTerrainWalkable(float worldX, float worldY)
|
||||
{
|
||||
foreach (var kvp in _landblocks)
|
||||
{
|
||||
var lb = kvp.Value;
|
||||
float localX = worldX - lb.WorldOffsetX;
|
||||
float localY = worldY - lb.WorldOffsetY;
|
||||
if (localX >= 0f && localX < 192f && localY >= 0f && localY < 192f)
|
||||
{
|
||||
var sample = lb.Terrain.SampleSurfacePolygon(localX, localY);
|
||||
var vertices = new Vector3[sample.Vertices.Length];
|
||||
for (int i = 0; i < sample.Vertices.Length; i++)
|
||||
{
|
||||
var v = sample.Vertices[i];
|
||||
vertices[i] = new Vector3(
|
||||
v.X + lb.WorldOffsetX,
|
||||
v.Y + lb.WorldOffsetY,
|
||||
v.Z);
|
||||
}
|
||||
|
||||
var normal = sample.Normal;
|
||||
float d = -Vector3.Dot(normal, vertices[0]);
|
||||
var plane = new System.Numerics.Plane(normal, d);
|
||||
|
||||
float waterDepth = lb.Terrain.SampleWaterDepth(localX, localY);
|
||||
bool isWater = waterDepth >= 0.45f;
|
||||
uint lowCellId = lb.Terrain.ComputeOutdoorCellId(localX, localY);
|
||||
uint fullCellId = (kvp.Key & 0xFFFF0000u) | lowCellId;
|
||||
|
||||
return new TerrainWalkableSample(
|
||||
plane,
|
||||
vertices,
|
||||
waterDepth,
|
||||
isWater,
|
||||
fullCellId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the outdoor cell id that owns a world-space position.
|
||||
/// Indoor ids are preserved because EnvCell ownership still comes from
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
public readonly record struct TerrainSurfacePolygon(
|
||||
float Z,
|
||||
Vector3 Normal,
|
||||
Vector3[] Vertices);
|
||||
|
||||
/// <summary>
|
||||
/// Outdoor terrain height resolver for a single landblock. Performs
|
||||
/// per-triangle barycentric Z interpolation matching the visual terrain
|
||||
|
|
@ -250,6 +256,72 @@ public sealed class TerrainSurface
|
|||
return (z, normal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sample the terrain triangle at (localX, localY), including the three
|
||||
/// local-space vertices that bound the sampled point. Edge-slide needs
|
||||
/// these vertices so the retail crossed-edge test can identify which edge
|
||||
/// the sphere left when a step-down probe fails.
|
||||
/// </summary>
|
||||
public TerrainSurfacePolygon SampleSurfacePolygon(float localX, float localY)
|
||||
{
|
||||
float fx = Math.Clamp(localX / CellSize, 0f, CellsPerSide - 0.001f);
|
||||
float fy = Math.Clamp(localY / CellSize, 0f, CellsPerSide - 0.001f);
|
||||
int cx = Math.Clamp((int)fx, 0, CellsPerSide - 1);
|
||||
int cy = Math.Clamp((int)fy, 0, CellsPerSide - 1);
|
||||
|
||||
float tx = fx - cx;
|
||||
float ty = fy - cy;
|
||||
|
||||
float hBL = _z[cx, cy ];
|
||||
float hBR = _z[cx + 1, cy ];
|
||||
float hTR = _z[cx + 1, cy + 1];
|
||||
float hTL = _z[cx, cy + 1];
|
||||
|
||||
bool splitSWtoNE = IsSplitSWtoNE(_landblockX, (uint)cx, _landblockY, (uint)cy);
|
||||
|
||||
Vector3 bl = new(cx * CellSize, cy * CellSize, hBL);
|
||||
Vector3 br = new((cx + 1) * CellSize, cy * CellSize, hBR);
|
||||
Vector3 tr = new((cx + 1) * CellSize, (cy + 1) * CellSize, hTR);
|
||||
Vector3 tl = new(cx * CellSize, (cy + 1) * CellSize, hTL);
|
||||
|
||||
float z;
|
||||
Vector3[] vertices;
|
||||
|
||||
if (splitSWtoNE)
|
||||
{
|
||||
if (tx > ty)
|
||||
{
|
||||
z = hBL + (hBR - hBL) * tx + (hTR - hBR) * ty;
|
||||
vertices = new[] { bl, br, tr };
|
||||
}
|
||||
else
|
||||
{
|
||||
z = hBL + (hTR - hTL) * tx + (hTL - hBL) * ty;
|
||||
vertices = new[] { bl, tr, tl };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tx + ty <= 1f)
|
||||
{
|
||||
z = hBL + (hBR - hBL) * tx + (hTL - hBL) * ty;
|
||||
vertices = new[] { bl, br, tl };
|
||||
}
|
||||
else
|
||||
{
|
||||
z = hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty);
|
||||
vertices = new[] { br, tr, tl };
|
||||
}
|
||||
}
|
||||
|
||||
var normal = Vector3.Normalize(
|
||||
Vector3.Cross(vertices[1] - vertices[0], vertices[2] - vertices[0]));
|
||||
if (normal.Z < 0f)
|
||||
normal = -normal;
|
||||
|
||||
return new TerrainSurfacePolygon(z, normal, vertices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail per-point water depth in meters — the amount the character's
|
||||
/// feet are allowed to sink below the contact plane before the
|
||||
|
|
|
|||
|
|
@ -188,7 +188,10 @@ public sealed class SpherePath
|
|||
// Walkable tracking
|
||||
public bool WalkableValid;
|
||||
public Plane WalkablePlane;
|
||||
public Vector3[]? WalkableVertices;
|
||||
public Vector3 WalkableUp = Vector3.UnitZ;
|
||||
public float WalkableAllowance = PhysicsGlobals.FloorZ;
|
||||
public bool HasWalkablePolygon => WalkableValid && WalkableVertices is { Length: >= 3 };
|
||||
|
||||
// Backup for restore
|
||||
public Vector3 BackupCheckPos;
|
||||
|
|
@ -246,6 +249,21 @@ public sealed class SpherePath
|
|||
WalkInterp = 1.0f;
|
||||
}
|
||||
|
||||
public void SetWalkable(Plane plane, Vector3[] vertices, Vector3 up)
|
||||
{
|
||||
WalkableValid = true;
|
||||
WalkablePlane = plane;
|
||||
WalkableVertices = (Vector3[])vertices.Clone();
|
||||
WalkableUp = up;
|
||||
WalkableAllowance = PhysicsGlobals.FloorZ;
|
||||
}
|
||||
|
||||
public void ClearWalkable()
|
||||
{
|
||||
WalkableValid = false;
|
||||
WalkableVertices = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slide fallback when step-up fails. Clears the contact-plane state that
|
||||
/// caused the step-up attempt and runs the full sphere-slide computation
|
||||
|
|
@ -273,6 +291,40 @@ public sealed class SpherePath
|
|||
return transition.SlideSphereInternal(StepUpNormal, GlobalCurrCenter[0].Origin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slide along the edge of the walkable polygon the mover just left.
|
||||
/// Retail anchor: <c>SPHEREPATH::precipice_slide</c>
|
||||
/// (<c>acclient_2013_pseudo_c.txt:274316</c>).
|
||||
/// </summary>
|
||||
public TransitionState PrecipiceSlide(Transition transition)
|
||||
{
|
||||
if (!HasWalkablePolygon || WalkableVertices is null)
|
||||
{
|
||||
ClearWalkable();
|
||||
return TransitionState.Collided;
|
||||
}
|
||||
|
||||
if (!BSPQuery.FindCrossedEdge(
|
||||
WalkablePlane,
|
||||
WalkableVertices,
|
||||
GlobalSphere[0].Origin,
|
||||
WalkableUp,
|
||||
out var collisionNormal))
|
||||
{
|
||||
ClearWalkable();
|
||||
return TransitionState.Collided;
|
||||
}
|
||||
|
||||
ClearWalkable();
|
||||
StepUp = false;
|
||||
|
||||
var offset = GlobalSphere[0].Origin - GlobalCurrCenter[0].Origin;
|
||||
if (Vector3.Dot(collisionNormal, offset) > 0f)
|
||||
collisionNormal = -collisionNormal;
|
||||
|
||||
return transition.SlideSphereInternal(collisionNormal, GlobalCurrCenter[0].Origin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the path for a simple point-to-point movement.
|
||||
/// </summary>
|
||||
|
|
@ -583,14 +635,14 @@ public sealed class Transition
|
|||
else if (!reset)
|
||||
{
|
||||
// Placement accepted — return current state.
|
||||
sp.WalkableValid = false;
|
||||
sp.ClearWalkable();
|
||||
return placeState;
|
||||
}
|
||||
}
|
||||
else
|
||||
reset = true;
|
||||
|
||||
sp.WalkableValid = false;
|
||||
sp.ClearWalkable();
|
||||
|
||||
if (reset)
|
||||
{
|
||||
|
|
@ -653,7 +705,7 @@ public sealed class Transition
|
|||
{
|
||||
if (DoStepDown(stepDownHeight, zVal, engine, runPlacement: false))
|
||||
{
|
||||
sp.WalkableValid = false;
|
||||
sp.ClearWalkable();
|
||||
return TransitionState.OK;
|
||||
}
|
||||
}
|
||||
|
|
@ -663,7 +715,7 @@ public sealed class Transition
|
|||
if (DoStepDown(stepDownHeight, zVal, engine, runPlacement: false)
|
||||
|| DoStepDown(stepDownHeight, zVal, engine, runPlacement: false))
|
||||
{
|
||||
sp.WalkableValid = false;
|
||||
sp.ClearWalkable();
|
||||
return TransitionState.OK;
|
||||
}
|
||||
}
|
||||
|
|
@ -681,8 +733,7 @@ public sealed class Transition
|
|||
// we are missing precipice context, a steep contact plane, or
|
||||
// merely the EdgeSlide flag.
|
||||
DumpEdgeSlideStepDownFailed(stepDownHeight, zVal);
|
||||
sp.RestoreCheckPos();
|
||||
return TransitionState.Collided;
|
||||
return EdgeSlideAfterStepDownFailed(engine, stepDownHeight, zVal);
|
||||
}
|
||||
|
||||
return TransitionState.OK;
|
||||
|
|
@ -693,6 +744,105 @@ public sealed class Transition
|
|||
return TransitionState.Slid;
|
||||
}
|
||||
|
||||
private TransitionState EdgeSlideAfterStepDownFailed(
|
||||
PhysicsEngine engine,
|
||||
float stepDownHeight,
|
||||
float zVal)
|
||||
{
|
||||
var sp = SpherePath;
|
||||
var ci = CollisionInfo;
|
||||
var oi = ObjectInfo;
|
||||
|
||||
// Retail lets non-EdgeSlide movers continue over the boundary. Player
|
||||
// movement carries EdgeSlide, so the local avatar takes the slide path.
|
||||
if (!oi.OnWalkable || !oi.EdgeSlide)
|
||||
{
|
||||
sp.ClearWalkable();
|
||||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
if (ci.ContactPlaneValid && ci.ContactPlane.Normal.Z < zVal)
|
||||
{
|
||||
var cliffPlane = ci.ContactPlane;
|
||||
sp.ClearWalkable();
|
||||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return CliffSlide(cliffPlane);
|
||||
}
|
||||
|
||||
if (sp.HasWalkablePolygon)
|
||||
{
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return sp.PrecipiceSlide(this);
|
||||
}
|
||||
|
||||
if (ci.ContactPlaneValid)
|
||||
{
|
||||
sp.ClearWalkable();
|
||||
sp.RestoreCheckPos();
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
// Retail back-probes from the current sphere center to rediscover the
|
||||
// walkable polygon we just left, then restores the failed candidate and
|
||||
// runs precipice_slide against that polygon.
|
||||
Vector3 backToCurrent = sp.GlobalCurrCenter[0].Origin - sp.GlobalSphere[0].Origin;
|
||||
sp.AddOffsetToCheckPos(backToCurrent);
|
||||
|
||||
_ = DoStepDown(stepDownHeight, zVal, engine, runPlacement: false);
|
||||
|
||||
ci.ContactPlaneValid = false;
|
||||
ci.ContactPlaneIsWater = false;
|
||||
sp.RestoreCheckPos();
|
||||
|
||||
if (sp.HasWalkablePolygon)
|
||||
return sp.PrecipiceSlide(this);
|
||||
|
||||
sp.ClearWalkable();
|
||||
return TransitionState.Collided;
|
||||
}
|
||||
|
||||
private TransitionState CliffSlide(Plane contactPlane)
|
||||
{
|
||||
var sp = SpherePath;
|
||||
var ci = CollisionInfo;
|
||||
|
||||
if (!ci.LastKnownContactPlaneValid)
|
||||
return TransitionState.OK;
|
||||
|
||||
Vector3 contactNormal = Vector3.Cross(contactPlane.Normal, ci.LastKnownContactPlane.Normal);
|
||||
contactNormal.Z = 0f;
|
||||
|
||||
Vector3 collideNormal = new(-contactNormal.Y, contactNormal.X, 0f);
|
||||
if (collideNormal.LengthSquared() < PhysicsGlobals.EpsilonSq)
|
||||
return TransitionState.OK;
|
||||
|
||||
collideNormal = Vector3.Normalize(collideNormal);
|
||||
|
||||
Vector3 offset = sp.GlobalSphere[0].Origin - sp.GlobalCurrCenter[0].Origin;
|
||||
float angle = Vector3.Dot(collideNormal, offset);
|
||||
|
||||
if (angle <= 0f)
|
||||
{
|
||||
sp.AddOffsetToCheckPos(collideNormal * angle);
|
||||
ci.SetCollisionNormal(collideNormal);
|
||||
}
|
||||
else
|
||||
{
|
||||
sp.AddOffsetToCheckPos(collideNormal * -angle);
|
||||
ci.SetCollisionNormal(-collideNormal);
|
||||
}
|
||||
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
||||
private void DumpEdgeSlideStepDownFailed(float stepDownHeight, float zVal)
|
||||
{
|
||||
if (!DumpEdgeSlideEnabled) return;
|
||||
|
|
@ -703,7 +853,7 @@ public sealed class Transition
|
|||
|
||||
Console.WriteLine(
|
||||
System.FormattableString.Invariant(
|
||||
$"edge-slide: stepdown-failed cur={Fmt(sp.CurPos)} check={Fmt(sp.CheckPos)} cell=0x{sp.CheckCellId:X8} edgeFlag={oi.EdgeSlide} contactFlag={oi.Contact} onWalkable={oi.OnWalkable} contactPlane={ci.ContactPlaneValid} lastPlane={ci.LastKnownContactPlaneValid} walkableValid={sp.WalkableValid} stepDown={stepDownHeight:F3} zVal={zVal:F3}"));
|
||||
$"edge-slide: stepdown-failed cur={Fmt(sp.CurPos)} check={Fmt(sp.CheckPos)} cell=0x{sp.CheckCellId:X8} edgeFlag={oi.EdgeSlide} contactFlag={oi.Contact} onWalkable={oi.OnWalkable} contactPlane={ci.ContactPlaneValid} lastPlane={ci.LastKnownContactPlaneValid} walkableValid={sp.WalkableValid} walkablePoly={sp.HasWalkablePolygon} stepDown={stepDownHeight:F3} zVal={zVal:F3}"));
|
||||
}
|
||||
|
||||
private static string Fmt(Vector3 value) =>
|
||||
|
|
@ -800,10 +950,10 @@ public sealed class Transition
|
|||
//
|
||||
// ACE reference: Landblock.GetZ (Landblock.cs:125-137) calls
|
||||
// find_terrain_poly and uses walkable.Plane — the actual triangle's
|
||||
// plane, not a reconstructed flat one. SampleTerrainPlane returns
|
||||
// the same thing analytically from the triangle's corner heights.
|
||||
var planeOpt = engine.SampleTerrainPlane(footCenter.X, footCenter.Y);
|
||||
if (planeOpt is null)
|
||||
// plane, not a reconstructed flat one. SampleTerrainWalkable returns
|
||||
// that plane plus the triangle vertices needed by precipice slide.
|
||||
var terrainWalkable = engine.SampleTerrainWalkable(footCenter.X, footCenter.Y);
|
||||
if (terrainWalkable is null)
|
||||
return TransitionState.OK; // no terrain loaded here — allow pass-through
|
||||
|
||||
// Per-point water depth: 0.9 on fully water cells, 0.45 on partial-
|
||||
|
|
@ -813,12 +963,11 @@ public sealed class Transition
|
|||
// contact plane before the push-up fires. In retail, this is what
|
||||
// makes characters appear submerged in water — there is NO separate
|
||||
// water surface mesh; the character just sits lower than terrain.
|
||||
float waterDepth = engine.SampleWaterDepth(footCenter.X, footCenter.Y);
|
||||
bool isWater = waterDepth >= 0.45f;
|
||||
|
||||
return ValidateWalkable(footCenter, sphereRadius, planeOpt.Value,
|
||||
isWater, waterDepth,
|
||||
cellId: sp.CheckCellId);
|
||||
return ValidateWalkable(footCenter, sphereRadius, terrainWalkable.Value.Plane,
|
||||
terrainWalkable.Value.IsWater,
|
||||
terrainWalkable.Value.WaterDepth,
|
||||
cellId: terrainWalkable.Value.CellId,
|
||||
walkableVertices: terrainWalkable.Value.Vertices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -829,12 +978,19 @@ public sealed class Transition
|
|||
/// </summary>
|
||||
private TransitionState ValidateWalkable(Vector3 sphereCenter, float sphereRadius,
|
||||
System.Numerics.Plane contactPlane,
|
||||
bool isWater, float waterDepth, uint cellId)
|
||||
bool isWater, float waterDepth, uint cellId,
|
||||
Vector3[]? walkableVertices = null)
|
||||
{
|
||||
var sp = SpherePath;
|
||||
var ci = CollisionInfo;
|
||||
var oi = ObjectInfo;
|
||||
|
||||
void CacheWalkableContext()
|
||||
{
|
||||
if (walkableVertices is not null && contactPlane.Normal.Z >= PhysicsGlobals.FloorZ)
|
||||
sp.SetWalkable(contactPlane, walkableVertices, Vector3.UnitZ);
|
||||
}
|
||||
|
||||
// Low point of the sphere.
|
||||
var lowPoint = sphereCenter - new Vector3(0f, 0f, sphereRadius);
|
||||
|
||||
|
|
@ -857,7 +1013,10 @@ public sealed class Transition
|
|||
// Resting on surface: record contact plane.
|
||||
bool walkableNormal = contactPlane.Normal.Z >= sp.WalkableAllowance;
|
||||
if (sp.StepDown || !oi.OnWalkable || walkableNormal)
|
||||
{
|
||||
ci.SetContactPlane(contactPlane, cellId, isWater);
|
||||
CacheWalkableContext();
|
||||
}
|
||||
|
||||
if (!oi.Contact && !sp.StepDown)
|
||||
{
|
||||
|
|
@ -879,6 +1038,7 @@ public sealed class Transition
|
|||
if (sp.StepDown || !oi.OnWalkable || walkable)
|
||||
{
|
||||
ci.SetContactPlane(contactPlane, cellId, isWater);
|
||||
CacheWalkableContext();
|
||||
|
||||
if (sp.StepDown)
|
||||
{
|
||||
|
|
@ -1020,7 +1180,8 @@ public sealed class Transition
|
|||
localSpaceZ,
|
||||
obj.Scale, // scale for local→world offsets
|
||||
obj.Rotation, // local→world rotation
|
||||
engine); // engine needed for Path 5 step-up
|
||||
engine,
|
||||
worldOrigin: obj.Position);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1499,8 +1660,8 @@ public sealed class Transition
|
|||
|
||||
bool stepDown = DoStepDown(stepDownHeight, zLandingValue, engine);
|
||||
|
||||
sp.StepUp = false;
|
||||
sp.WalkableValid = false;
|
||||
sp.StepUp = false;
|
||||
sp.ClearWalkable();
|
||||
|
||||
// L.2.3f: log the result + landing plane if step-up succeeded.
|
||||
// This is the actual surface the player ended up on, which may
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue