feat(physics): Phase L.2.1+L.2.2 — BSP step-up and rooftop landing
Port CTransition::step_up (Path 5) and SPHEREPATH::set_collide (Path 6) from the retail decomp, turning wall-slides into proper step-up climbs and airborne-to-roof landings. Path 5 (grounded mover hits polygon): - StepSphereUp calls DoStepUp which runs DoStepDown with StepUp=true - DoStepDown now includes the retail Placement validation step (ACE Transition.cs:731-741) — sphere must not be inside solid geometry after finding a contact plane; this correctly blocks the tall-wall case - FindObjCollisions now allocates a local ShadowEntry list per call to prevent "collection modified" exceptions when DoStepUp recurses back through TransitionalInsert → FindObjCollisions - BSPQuery.FindCollisions passes engine through to StepSphereUp Path 6 (airborne mover hits polygon): - SpherePath.SetCollide: saves backup pos, records StepUpNormal, sets WalkInterp=1 — then returns Adjusted so TransitionalInsert retries - SpherePath.StepUpSlide: clears ContactPlane, sets SlidingNormal for the tall-wall fallback - TransitionalInsert Collide branch: re-tests as Placement when ContactPlaneValid; on failure restores backup and returns Collided Test fixes (BSPStepUpTests.cs + BSPStepUpFixtures.cs): - Tests use foot-position convention (CurPos = foot, sphere center = CurPos + (0,0,r)); from/to corrected from sphere-center to foot coords - MakeTestEngine terrainZ param: 0f for grounded tests (keeps Contact state between sub-steps), -50f for airborne/roof tests - to.X adjusted so sub-steps land sphere inside (not exactly touching) the wall, avoiding the EPSILON-shrink false-negative edge case - All 12 BSPStepUp tests now GREEN; full suite 823/823 Retail refs: CTransition::step_up — acclient_2013_pseudo_c.txt:273099 / ACE:746 CTransition::step_down — acclient_2013_pseudo_c.txt:273069 / ACE:710 SPHEREPATH::set_collide — acclient_2013_pseudo_c.txt:321594 / ACE:279 CTransition::transitional_insert Collide — pseudo_c:273193 / ACE:891 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b0c29454d0
commit
670f892bd3
4 changed files with 341 additions and 179 deletions
|
|
@ -1085,34 +1085,28 @@ public static class BSPQuery
|
||||||
/// BSPTree.step_sphere_up — attempt to step over a low obstacle.
|
/// BSPTree.step_sphere_up — attempt to step over a low obstacle.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Sets the StepUp flag on SpherePath with the collision normal.
|
/// Calls <see cref="Transition.DoStepUp"/> which probes upward then steps
|
||||||
/// The Transition's outer loop will pick this up and attempt the step.
|
/// down to find a walkable landing surface. If the step-up succeeds the
|
||||||
/// If StepUp is already pending, falls back to setting the collision normal
|
/// sphere's CheckPos is already updated and we return OK. If it fails we
|
||||||
/// directly (StepUpSlide equivalent).
|
/// fall back to StepUpSlide: clear the contact plane and slide along the
|
||||||
|
/// collision normal.
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>ACE: BSPTree.cs step_sphere_up.</para>
|
/// <para>
|
||||||
|
/// ACE: BSPTree.step_sphere_up calls transition.StepUp(globNormal);
|
||||||
|
/// on false → SpherePath.StepUpSlide(transition).
|
||||||
|
/// Named-retail: BSPTREE::step_sphere_up.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static TransitionState StepSphereUp(
|
private static TransitionState StepSphereUp(
|
||||||
Transition transition,
|
Transition transition,
|
||||||
Vector3 collisionNormal)
|
Vector3 collisionNormal,
|
||||||
|
PhysicsEngine engine)
|
||||||
{
|
{
|
||||||
var path = transition.SpherePath;
|
if (transition.DoStepUp(collisionNormal, engine!))
|
||||||
var ci = transition.CollisionInfo;
|
|
||||||
|
|
||||||
// ACE calls transition.StepUp(globNormal); if false -> path.StepUpSlide(transition).
|
|
||||||
// In acdream, StepUp is a flag field on SpherePath.
|
|
||||||
// If no StepUp is pending yet, request one.
|
|
||||||
if (!path.StepUp)
|
|
||||||
{
|
|
||||||
path.StepUp = true;
|
|
||||||
path.StepUpNormal = collisionNormal;
|
|
||||||
return TransitionState.OK;
|
return TransitionState.OK;
|
||||||
}
|
|
||||||
|
|
||||||
// StepUpSlide: can't step up, set collision normal and report adjusted.
|
return transition.SpherePath.StepUpSlide(transition.CollisionInfo);
|
||||||
ci.SetCollisionNormal(collisionNormal);
|
|
||||||
return TransitionState.Adjusted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -1364,7 +1358,8 @@ public static class BSPQuery
|
||||||
Vector3 localCurrCenter,
|
Vector3 localCurrCenter,
|
||||||
Vector3 localSpaceZ,
|
Vector3 localSpaceZ,
|
||||||
float scale,
|
float scale,
|
||||||
Quaternion localToWorld = default)
|
Quaternion localToWorld = default,
|
||||||
|
PhysicsEngine? engine = null)
|
||||||
{
|
{
|
||||||
if (root is null) return TransitionState.OK;
|
if (root is null) return TransitionState.OK;
|
||||||
// Default quaternion (0,0,0,0) → treat as identity
|
// Default quaternion (0,0,0,0) → treat as identity
|
||||||
|
|
@ -1453,12 +1448,15 @@ public static class BSPQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Path 5: Contact — sphere_intersects_poly + wall-slide
|
// Path 5: Contact (grounded) — sphere_intersects_poly + step_sphere_up
|
||||||
// ACE retail uses StepSphereUp here, deferring to a retry loop that
|
//
|
||||||
// executes the step-up motion. We haven't ported that execution, so
|
// A grounded mover hits a polygon. Retail calls BSPTREE::step_sphere_up,
|
||||||
// we apply the same wall-slide response as Path 6 — this at least
|
// which runs CTransition::step_up (upward probe + step-down scan). If the
|
||||||
// gives correct blocking + sliding behavior for walls, buildings,
|
// obstacle is short enough the sphere climbs it; if too tall, it falls back
|
||||||
// and tree trunks while the player is on the ground.
|
// to StepUpSlide (clear contact-plane, slide along StepUpNormal).
|
||||||
|
//
|
||||||
|
// ACE: BSPTree.find_collisions → step_sphere_up (BSPTree.cs, path 5 branch).
|
||||||
|
// Named-retail: BSPTREE::find_collisions Contact branch → step_sphere_up.
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
if (obj.State.HasFlag(ObjectInfoState.Contact))
|
if (obj.State.HasFlag(ObjectInfoState.Contact))
|
||||||
{
|
{
|
||||||
|
|
@ -1470,26 +1468,12 @@ public static class BSPQuery
|
||||||
|
|
||||||
if (hit0 || hitPoly0 is not null)
|
if (hit0 || hitPoly0 is not null)
|
||||||
{
|
{
|
||||||
// Wall-slide response (same as Path 6 below).
|
var worldNormal = L2W(hitPoly0!.Plane.Normal);
|
||||||
var localNormal = hitPoly0!.Plane.Normal;
|
if (engine is not null)
|
||||||
var localMovement = sphere0.Center - localCurrCenter;
|
return StepSphereUp(transition, worldNormal, engine);
|
||||||
|
|
||||||
float movementIntoWall = Vector3.Dot(localMovement, localNormal);
|
// No engine available (env-cell path without engine param) —
|
||||||
Vector3 projectedMovement = localMovement - localNormal * movementIntoWall;
|
// fall back to wall-slide so existing indoor geometry still blocks.
|
||||||
|
|
||||||
Vector3 slidPos = localCurrCenter + projectedMovement;
|
|
||||||
float slidDist = Vector3.Dot(slidPos, localNormal) + hitPoly0.Plane.D;
|
|
||||||
float minDist = sphere0.Radius + 0.01f;
|
|
||||||
if (slidDist < minDist)
|
|
||||||
{
|
|
||||||
slidPos += localNormal * (minDist - slidDist);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 localDelta = slidPos - sphere0.Center;
|
|
||||||
Vector3 worldDelta = Vector3.Transform(localDelta, localToWorld) * scale;
|
|
||||||
path.AddOffsetToCheckPos(worldDelta);
|
|
||||||
|
|
||||||
var worldNormal = L2W(localNormal);
|
|
||||||
collisions.SetCollisionNormal(worldNormal);
|
collisions.SetCollisionNormal(worldNormal);
|
||||||
collisions.SetSlidingNormal(worldNormal);
|
collisions.SetSlidingNormal(worldNormal);
|
||||||
return TransitionState.Slid;
|
return TransitionState.Slid;
|
||||||
|
|
@ -1505,25 +1489,10 @@ public static class BSPQuery
|
||||||
|
|
||||||
if (hit1 || hitPoly1 is not null)
|
if (hit1 || hitPoly1 is not null)
|
||||||
{
|
{
|
||||||
var localNormal = hitPoly1!.Plane.Normal;
|
var worldNormal = L2W(hitPoly1!.Plane.Normal);
|
||||||
var localMovement = sphere1.Center - localCurrCenter;
|
if (engine is not null)
|
||||||
|
return StepSphereUp(transition, worldNormal, engine);
|
||||||
|
|
||||||
float movementIntoWall = Vector3.Dot(localMovement, localNormal);
|
|
||||||
Vector3 projectedMovement = localMovement - localNormal * movementIntoWall;
|
|
||||||
|
|
||||||
Vector3 slidPos = localCurrCenter + projectedMovement;
|
|
||||||
float slidDist = Vector3.Dot(slidPos, localNormal) + hitPoly1.Plane.D;
|
|
||||||
float minDist = sphere1.Radius + 0.01f;
|
|
||||||
if (slidDist < minDist)
|
|
||||||
{
|
|
||||||
slidPos += localNormal * (minDist - slidDist);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 localDelta = slidPos - sphere1.Center;
|
|
||||||
Vector3 worldDelta = Vector3.Transform(localDelta, localToWorld) * scale;
|
|
||||||
path.AddOffsetToCheckPos(worldDelta);
|
|
||||||
|
|
||||||
var worldNormal = L2W(localNormal);
|
|
||||||
collisions.SetCollisionNormal(worldNormal);
|
collisions.SetCollisionNormal(worldNormal);
|
||||||
collisions.SetSlidingNormal(worldNormal);
|
collisions.SetSlidingNormal(worldNormal);
|
||||||
return TransitionState.Slid;
|
return TransitionState.Slid;
|
||||||
|
|
@ -1553,50 +1522,19 @@ public static class BSPQuery
|
||||||
hitPoly0!, contact0, scale, localToWorld);
|
hitPoly0!, contact0, scale, localToWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Wall-slide response ─────────────────────────────────
|
// ─── SetCollide response ─────────────────────────────────
|
||||||
// Instead of just pushing the sphere out of penetration
|
// Airborne sphere hits a polygon. Per retail, call SetCollide
|
||||||
// (which undoes the whole step), compute the wall-slide
|
// which saves backup position, records StepUpNormal = worldNormal,
|
||||||
// position: where the sphere WOULD be if the movement had
|
// and sets WalkInterp=1. TransitionalInsert's Collide branch will
|
||||||
// been projected along the wall tangent.
|
// then re-test as Placement to confirm we can land on the surface.
|
||||||
//
|
//
|
||||||
// In local space:
|
// ACE: BSPTree.find_collisions default branch → SpherePath.SetCollide
|
||||||
// curr = localCurrCenter
|
// + return Adjusted.
|
||||||
// target = sphere0.Center
|
// Named-retail: BSPTREE::find_collisions airborne branch → set_collide.
|
||||||
// movement = target - curr
|
var worldNormal0 = L2W(hitPoly0!.Plane.Normal);
|
||||||
// normal = polygon plane normal (outward)
|
path.SetCollide(worldNormal0);
|
||||||
// projectedMovement = movement - (movement · normal) * normal
|
|
||||||
// slidPos = curr + projectedMovement
|
|
||||||
//
|
|
||||||
// Then ensure slidPos is outside the plane by at least radius+eps.
|
|
||||||
var localNormal = hitPoly0!.Plane.Normal;
|
|
||||||
var localMovement = sphere0.Center - localCurrCenter;
|
|
||||||
|
|
||||||
// Project movement along wall tangent
|
|
||||||
float movementIntoWall = Vector3.Dot(localMovement, localNormal);
|
|
||||||
Vector3 projectedMovement = localMovement - localNormal * movementIntoWall;
|
|
||||||
|
|
||||||
// Slid position in local space
|
|
||||||
Vector3 slidPos = localCurrCenter + projectedMovement;
|
|
||||||
|
|
||||||
// Ensure slid position is OUTSIDE the plane by radius + epsilon
|
|
||||||
float slidDist = Vector3.Dot(slidPos, localNormal) + hitPoly0.Plane.D;
|
|
||||||
float minDist = sphere0.Radius + 0.01f;
|
|
||||||
if (slidDist < minDist)
|
|
||||||
{
|
|
||||||
slidPos += localNormal * (minDist - slidDist);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delta from current CheckPos sphere center to slid position (local)
|
|
||||||
Vector3 localDelta = slidPos - sphere0.Center;
|
|
||||||
// Transform to world and apply
|
|
||||||
Vector3 worldDelta = Vector3.Transform(localDelta, localToWorld) * scale;
|
|
||||||
path.AddOffsetToCheckPos(worldDelta);
|
|
||||||
|
|
||||||
var worldNormal = L2W(localNormal);
|
|
||||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
||||||
collisions.SetCollisionNormal(worldNormal);
|
return TransitionState.Adjusted;
|
||||||
collisions.SetSlidingNormal(worldNormal);
|
|
||||||
return TransitionState.Slid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sphere1 is not null)
|
if (sphere1 is not null)
|
||||||
|
|
@ -1609,29 +1547,11 @@ public static class BSPQuery
|
||||||
|
|
||||||
if (hit1 || hitPoly1 is not null)
|
if (hit1 || hitPoly1 is not null)
|
||||||
{
|
{
|
||||||
// Head sphere hit: apply the same wall-slide as above.
|
// Head sphere hit: same SetCollide response.
|
||||||
var localNormal = hitPoly1!.Plane.Normal;
|
var worldNormal1 = L2W(hitPoly1!.Plane.Normal);
|
||||||
var localMovement = sphere1.Center - localCurrCenter;
|
path.SetCollide(worldNormal1);
|
||||||
|
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
||||||
float movementIntoWall = Vector3.Dot(localMovement, localNormal);
|
return TransitionState.Adjusted;
|
||||||
Vector3 projectedMovement = localMovement - localNormal * movementIntoWall;
|
|
||||||
|
|
||||||
Vector3 slidPos = localCurrCenter + projectedMovement;
|
|
||||||
float slidDist = Vector3.Dot(slidPos, localNormal) + hitPoly1.Plane.D;
|
|
||||||
float minDist = sphere1.Radius + 0.01f;
|
|
||||||
if (slidDist < minDist)
|
|
||||||
{
|
|
||||||
slidPos += localNormal * (minDist - slidDist);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 localDelta = slidPos - sphere1.Center;
|
|
||||||
Vector3 worldDelta = Vector3.Transform(localDelta, localToWorld) * scale;
|
|
||||||
path.AddOffsetToCheckPos(worldDelta);
|
|
||||||
|
|
||||||
var worldNormal = L2W(localNormal);
|
|
||||||
collisions.SetCollisionNormal(worldNormal);
|
|
||||||
collisions.SetSlidingNormal(worldNormal);
|
|
||||||
return TransitionState.Slid;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,27 @@ public sealed class ObjectInfo
|
||||||
public bool EdgeSlide => State.HasFlag(ObjectInfoState.EdgeSlide);
|
public bool EdgeSlide => State.HasFlag(ObjectInfoState.EdgeSlide);
|
||||||
public bool PathClipped => State.HasFlag(ObjectInfoState.PathClipped);
|
public bool PathClipped => State.HasFlag(ObjectInfoState.PathClipped);
|
||||||
public bool FreeRotate => State.HasFlag(ObjectInfoState.FreeRotate);
|
public bool FreeRotate => State.HasFlag(ObjectInfoState.FreeRotate);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return the Z threshold for a walkable surface appropriate to the
|
||||||
|
/// current movement context.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Retail: OBJECTINFO::get_walkable_z — returns FloorZ when the mover
|
||||||
|
/// is on a walkable surface (Contact+OnWalkable), LandingZ otherwise.
|
||||||
|
/// ACE: ObjectInfo.GetWalkableZ (Transition.cs:760).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public float GetWalkableZ()
|
||||||
|
=> OnWalkable ? PhysicsGlobals.FloorZ : PhysicsGlobals.LandingZ;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop any accumulated velocity on this object info.
|
||||||
|
/// ACE: ObjectInfo.StopVelocity — clears Velocity on the physics body.
|
||||||
|
/// acdream: velocity is tracked on PhysicsBody, not here. No-op for now;
|
||||||
|
/// will be wired when velocity is threaded through TransitionalInsert.
|
||||||
|
/// </summary>
|
||||||
|
public void StopVelocity() { /* velocity lives on PhysicsBody, not here */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -210,6 +231,34 @@ public sealed class SpherePath
|
||||||
SetCheckPos(BackupCheckPos, BackupCheckCellId);
|
SetCheckPos(BackupCheckPos, BackupCheckCellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when an airborne sphere hits a polygon but the polygon is not yet
|
||||||
|
/// walkable — save backup, record the collision normal in StepUpNormal, and
|
||||||
|
/// flag Collide so TransitionalInsert can re-test as Placement.
|
||||||
|
/// ACE: SpherePath.SetCollide (acclient_2013_pseudo_c.txt ~321594, ACE SpherePath.cs:279-286).
|
||||||
|
/// </summary>
|
||||||
|
public void SetCollide(Vector3 collisionNormal)
|
||||||
|
{
|
||||||
|
Collide = true;
|
||||||
|
BackupCheckPos = CheckPos;
|
||||||
|
BackupCheckCellId = CheckCellId;
|
||||||
|
StepUpNormal = collisionNormal;
|
||||||
|
WalkInterp = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slide fallback when step-up fails. Clears the contact-plane state that
|
||||||
|
/// caused the step-up attempt and issues a slide along StepUpNormal.
|
||||||
|
/// ACE: SpherePath.StepUpSlide (ACE SpherePath.cs:309-317).
|
||||||
|
/// </summary>
|
||||||
|
public TransitionState StepUpSlide(CollisionInfo collisions)
|
||||||
|
{
|
||||||
|
collisions.ContactPlaneValid = false;
|
||||||
|
collisions.ContactPlaneIsWater = false;
|
||||||
|
collisions.SetSlidingNormal(StepUpNormal);
|
||||||
|
return TransitionState.Slid;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the path for a simple point-to-point movement.
|
/// Initialize the path for a simple point-to-point movement.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -491,11 +540,57 @@ public sealed class Transition
|
||||||
|
|
||||||
// ── Phase 3: both env and objects returned OK ──────────────
|
// ── Phase 3: both env and objects returned OK ──────────────
|
||||||
// Handle Collide flag (BSP path 6 set it on a non-contact hit).
|
// Handle Collide flag (BSP path 6 set it on a non-contact hit).
|
||||||
// ACE: if Collide is set, re-test as Placement to confirm position.
|
// ACE: Transition.TransitionalInsert Collide branch (Transition.cs:891-930).
|
||||||
// Simplified: just clear it and accept.
|
// Named-retail: CTransition::transitional_insert Collide branch.
|
||||||
if (sp.Collide)
|
if (sp.Collide)
|
||||||
{
|
{
|
||||||
sp.Collide = false;
|
sp.Collide = false;
|
||||||
|
|
||||||
|
bool reset = false;
|
||||||
|
if (ci.ContactPlaneValid && DoCheckWalkable(PhysicsGlobals.LandingZ, engine))
|
||||||
|
{
|
||||||
|
// CheckPos is walkable — re-test as Placement to snap/validate.
|
||||||
|
var savedInsert = sp.InsertType;
|
||||||
|
sp.InsertType = InsertType.Placement;
|
||||||
|
|
||||||
|
var placeState = TransitionalInsert(numAttempts, engine);
|
||||||
|
|
||||||
|
sp.InsertType = savedInsert;
|
||||||
|
|
||||||
|
if (placeState != TransitionState.OK)
|
||||||
|
{
|
||||||
|
// Placement rejected — fall through to restore.
|
||||||
|
placeState = TransitionState.OK;
|
||||||
|
reset = true;
|
||||||
|
}
|
||||||
|
else if (!reset)
|
||||||
|
{
|
||||||
|
// Placement accepted — return current state.
|
||||||
|
sp.WalkableValid = false;
|
||||||
|
return placeState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
reset = true;
|
||||||
|
|
||||||
|
sp.WalkableValid = false;
|
||||||
|
|
||||||
|
if (reset)
|
||||||
|
{
|
||||||
|
sp.RestoreCheckPos();
|
||||||
|
ci.ContactPlaneValid = false;
|
||||||
|
ci.ContactPlaneIsWater = false;
|
||||||
|
|
||||||
|
if (ci.LastKnownContactPlaneValid)
|
||||||
|
{
|
||||||
|
ci.LastKnownContactPlaneValid = false;
|
||||||
|
oi.StopVelocity();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ci.SetCollisionNormal(sp.StepUpNormal);
|
||||||
|
|
||||||
|
return TransitionState.Collided;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle neg-poly hit (backward-facing polygon contact).
|
// Handle neg-poly hit (backward-facing polygon contact).
|
||||||
|
|
@ -614,7 +709,9 @@ public sealed class Transition
|
||||||
localSphere1,
|
localSphere1,
|
||||||
localCurrCenter,
|
localCurrCenter,
|
||||||
Vector3.UnitZ, // local space Z is up
|
Vector3.UnitZ, // local space Z is up
|
||||||
1.0f); // scale = 1.0 for cell geometry
|
1.0f, // scale = 1.0 for cell geometry
|
||||||
|
Quaternion.Identity,
|
||||||
|
engine); // engine needed for Path 5 step-up
|
||||||
|
|
||||||
if (cellState != TransitionState.OK)
|
if (cellState != TransitionState.OK)
|
||||||
{
|
{
|
||||||
|
|
@ -744,11 +841,6 @@ public sealed class Transition
|
||||||
// Object collision — static BSP objects
|
// Object collision — static BSP objects
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
// Reused per-call to avoid per-step allocation; safe because Transition
|
|
||||||
// is single-threaded per movement resolve.
|
|
||||||
private readonly List<ShadowEntry> _nearbyObjs = new();
|
|
||||||
private static int _debugQueryCount = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query the ShadowObjectRegistry for nearby static objects and run
|
/// Query the ShadowObjectRegistry for nearby static objects and run
|
||||||
/// collision against each using the retail BSPTree.find_collisions 6-path
|
/// collision against each using the retail BSPTree.find_collisions 6-path
|
||||||
|
|
@ -778,23 +870,17 @@ public sealed class Transition
|
||||||
out uint landblockId, out float worldOffsetX, out float worldOffsetY))
|
out uint landblockId, out float worldOffsetX, out float worldOffsetY))
|
||||||
return TransitionState.OK;
|
return TransitionState.OK;
|
||||||
|
|
||||||
|
// Use a local list: DoStepUp calls TransitionalInsert → FindObjCollisions
|
||||||
|
// recursively, so reusing a single field list would corrupt the outer
|
||||||
|
// iteration. Allocate per call (cheap — typically 0-5 entries).
|
||||||
|
var nearbyObjs = new List<ShadowEntry>();
|
||||||
float queryRadius = sphereRadius + movement.Length() + 5f;
|
float queryRadius = sphereRadius + movement.Length() + 5f;
|
||||||
engine.ShadowObjects.GetNearbyObjects(
|
engine.ShadowObjects.GetNearbyObjects(
|
||||||
currPos, queryRadius,
|
currPos, queryRadius,
|
||||||
worldOffsetX, worldOffsetY, landblockId,
|
worldOffsetX, worldOffsetY, landblockId,
|
||||||
_nearbyObjs);
|
nearbyObjs);
|
||||||
|
|
||||||
// Log every 120 frames — tracks player position over time.
|
foreach (var obj in nearbyObjs)
|
||||||
_debugQueryCount++;
|
|
||||||
if (movement.LengthSquared() > 0.0001f && _debugQueryCount % 120 == 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine(
|
|
||||||
$"ObjColl @({currPos.X:F1},{currPos.Y:F1},{currPos.Z:F1}) " +
|
|
||||||
$"lb=0x{landblockId:X8} nearby={_nearbyObjs.Count}/{engine.ShadowObjects.TotalRegistered}");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var obj in _nearbyObjs)
|
|
||||||
{
|
{
|
||||||
// Broad-phase: can the moving sphere reach this object?
|
// Broad-phase: can the moving sphere reach this object?
|
||||||
Vector3 deltaToCurr = currPos - obj.Position;
|
Vector3 deltaToCurr = currPos - obj.Position;
|
||||||
|
|
@ -868,7 +954,8 @@ public sealed class Transition
|
||||||
localCurrCenter,
|
localCurrCenter,
|
||||||
localSpaceZ,
|
localSpaceZ,
|
||||||
obj.Scale, // scale for local→world offsets
|
obj.Scale, // scale for local→world offsets
|
||||||
obj.Rotation); // local→world rotation
|
obj.Rotation, // local→world rotation
|
||||||
|
engine); // engine needed for Path 5 step-up
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1218,16 +1305,145 @@ public sealed class Transition
|
||||||
// 1. Collision detection returned OK
|
// 1. Collision detection returned OK
|
||||||
// 2. A valid contact plane was found
|
// 2. A valid contact plane was found
|
||||||
// 3. The contact plane is walkable (Normal.Z >= walkableZ)
|
// 3. The contact plane is walkable (Normal.Z >= walkableZ)
|
||||||
|
//
|
||||||
|
// ACE StepDown then runs a Placement insertion to confirm the sphere
|
||||||
|
// can actually be placed at the candidate position — it must not be
|
||||||
|
// inside any solid geometry (wall, BSP object, etc.).
|
||||||
|
// Named-retail: CTransition::step_down, acclient_2013_pseudo_c.txt:273069.
|
||||||
|
// ACE: Transition.cs:731-741.
|
||||||
if (transitState == TransitionState.OK
|
if (transitState == TransitionState.OK
|
||||||
&& CollisionInfo.ContactPlaneValid
|
&& CollisionInfo.ContactPlaneValid
|
||||||
&& CollisionInfo.ContactPlane.Normal.Z >= walkableZ)
|
&& CollisionInfo.ContactPlane.Normal.Z >= walkableZ)
|
||||||
{
|
{
|
||||||
return true;
|
// Placement validation: can we actually stand here?
|
||||||
|
var savedInsert = sp.InsertType;
|
||||||
|
sp.InsertType = InsertType.Placement;
|
||||||
|
|
||||||
|
var placeState = TransitionalInsert(1, engine);
|
||||||
|
|
||||||
|
sp.InsertType = savedInsert;
|
||||||
|
return placeState == TransitionState.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Step-up
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempt to step over a low obstacle by probing upward then stepping down.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Retail flow (CTransition::step_up, named-retail ~273099):
|
||||||
|
/// 1. Clear ContactPlane so the step-down probe is unbiased.
|
||||||
|
/// 2. Set StepUp flag so DoStepDown skips the downward offset (we start
|
||||||
|
/// from the sphere's current position and scan down from there).
|
||||||
|
/// 3. Pick stepDownHeight / walkable-Z from ObjectInfo (if OnWalkable,
|
||||||
|
/// use StepUpHeight + FloorZ; else 0.04 + LandingZ).
|
||||||
|
/// 4. Save backup, run DoStepDown, then clear StepUp.
|
||||||
|
/// 5. Return true on success; the caller commits the new CheckPos.
|
||||||
|
/// On failure, RestoreCheckPos and return false.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// ACE: Transition.StepUp (Transition.cs:746-777).
|
||||||
|
/// Named-retail: CTransition::step_up (~273099-273133).
|
||||||
|
/// </summary>
|
||||||
|
internal bool DoStepUp(Vector3 collisionNormal, PhysicsEngine engine)
|
||||||
|
{
|
||||||
|
var sp = SpherePath;
|
||||||
|
var ci = CollisionInfo;
|
||||||
|
var oi = ObjectInfo;
|
||||||
|
|
||||||
|
ci.ContactPlaneValid = false;
|
||||||
|
ci.ContactPlaneIsWater = false;
|
||||||
|
|
||||||
|
sp.StepUp = true;
|
||||||
|
sp.StepUpNormal = collisionNormal;
|
||||||
|
|
||||||
|
// Default values (not on walkable): small step, LandingZ threshold.
|
||||||
|
float stepDownHeight = 0.04f;
|
||||||
|
float zLandingValue = PhysicsGlobals.LandingZ;
|
||||||
|
|
||||||
|
if (oi.State.HasFlag(ObjectInfoState.OnWalkable))
|
||||||
|
{
|
||||||
|
zLandingValue = oi.GetWalkableZ();
|
||||||
|
stepDownHeight = oi.StepUpHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
sp.WalkableAllowance = zLandingValue;
|
||||||
|
sp.SaveCheckPos();
|
||||||
|
|
||||||
|
bool stepDown = DoStepDown(stepDownHeight, zLandingValue, engine);
|
||||||
|
|
||||||
|
sp.StepUp = false;
|
||||||
|
sp.WalkableValid = false;
|
||||||
|
|
||||||
|
if (!stepDown)
|
||||||
|
sp.RestoreCheckPos();
|
||||||
|
|
||||||
|
return stepDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Walkable check
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Probe downward by StepDownHeight to confirm a walkable surface is within
|
||||||
|
/// reach of the current CheckPos — used by the Collide branch in
|
||||||
|
/// TransitionalInsert before re-testing as Placement.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Returns true if a walkable surface was found within reach (i.e. the
|
||||||
|
/// sphere can land here). Returns false if:
|
||||||
|
/// - ObjectInfo.OnWalkable is NOT set (always walkable by convention).
|
||||||
|
/// - CheckWalkables() already confirmed a walkable (skip the probe).
|
||||||
|
/// - The downward probe returned OK (meaning: no walkable was found
|
||||||
|
/// within reach, so we CANNOT land → transitState == OK → return false).
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// ACE: Transition.CheckWalkable (Transition.cs:206-235).
|
||||||
|
/// Named-retail: CTransition::check_walkable.
|
||||||
|
/// </summary>
|
||||||
|
internal bool DoCheckWalkable(float zCheck, PhysicsEngine engine)
|
||||||
|
{
|
||||||
|
var sp = SpherePath;
|
||||||
|
var oi = ObjectInfo;
|
||||||
|
|
||||||
|
if (!oi.State.HasFlag(ObjectInfoState.OnWalkable))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// If the current walkable entry is still valid, skip the probe.
|
||||||
|
if (sp.WalkableValid)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
sp.SaveCheckPos();
|
||||||
|
|
||||||
|
float stepHeight = oi.StepDownHeight;
|
||||||
|
var globSphere = sp.GlobalSphere[0];
|
||||||
|
|
||||||
|
if (sp.NumSphere < 2 && stepHeight > globSphere.Radius * 2f)
|
||||||
|
stepHeight = globSphere.Radius * 0.5f;
|
||||||
|
|
||||||
|
if (stepHeight > globSphere.Radius * 2f)
|
||||||
|
stepHeight *= 0.5f;
|
||||||
|
|
||||||
|
sp.WalkableAllowance = zCheck;
|
||||||
|
sp.CheckWalkable = true;
|
||||||
|
sp.AddOffsetToCheckPos(new Vector3(0f, 0f, -stepHeight));
|
||||||
|
|
||||||
|
var transitState = TransitionalInsert(1, engine);
|
||||||
|
|
||||||
|
sp.CheckWalkable = false;
|
||||||
|
sp.RestoreCheckPos();
|
||||||
|
|
||||||
|
// ACE returns (transitState != OK) — i.e. true when we DID find a
|
||||||
|
// walkable (collision probe returned Adjusted/Collided).
|
||||||
|
return transitState != TransitionState.OK;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Post-step validation
|
// Post-step validation
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@ public static class BSPStepUpFixtures
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>Floor polygon at z = 0, x ∈ [-2, 0.5], y ∈ [-1, 1].</item>
|
/// <item>Floor polygon at z = 0, x ∈ [-2, 0.5], y ∈ [-1, 1].</item>
|
||||||
/// <item>Vertical wall polygon at x = 0.5, z ∈ [0, 0.25], y ∈ [-1, 1], facing -X.</item>
|
/// <item>Vertical wall polygon at x = 0.5, z ∈ [0, 0.25], y ∈ [-1, 1], facing -X.</item>
|
||||||
/// <item>Upper floor polygon at z = 0.25, x ∈ [0.5, 2], y ∈ [-1, 1].</item>
|
/// <item>Upper floor polygon at z = 0.25, x ∈ [0.2, 2], y ∈ [-1, 1] — extends
|
||||||
|
/// left of the wall face so the vertical step-down probe finds it when the
|
||||||
|
/// sphere is at x ≈ 0.3–0.5 (the wall contact zone).</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static (PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved)
|
public static (PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved)
|
||||||
|
|
@ -105,10 +107,14 @@ public static class BSPStepUpFixtures
|
||||||
new Vector3(0.5f, 1f, 0f),
|
new Vector3(0.5f, 1f, 0f),
|
||||||
expectedNormal: new Vector3(-1f, 0f, 0f));
|
expectedNormal: new Vector3(-1f, 0f, 0f));
|
||||||
|
|
||||||
// Upper floor at z=0.25, x∈[0.5,2], y∈[-1,1], normal = +Z
|
// Upper floor at z=0.25, x∈[0.2,2], y∈[-1,1], normal = +Z.
|
||||||
|
// The upper floor extends slightly left of the wall face (x=0.5)
|
||||||
|
// so the step-down probe (vertical, from the wall-contact XY) can
|
||||||
|
// find it when the sphere is at x≈0.3-0.5. Retail BSPs have the
|
||||||
|
// same overlap because geometry is continuous across the step.
|
||||||
resolved[LowStep_UpperFloorId] = MakeFloor(
|
resolved[LowStep_UpperFloorId] = MakeFloor(
|
||||||
new Vector3(0.5f, -1f, 0.25f), new Vector3(2f, -1f, 0.25f),
|
new Vector3(0.2f, -1f, 0.25f), new Vector3(2f, -1f, 0.25f),
|
||||||
new Vector3(2f, 1f, 0.25f), new Vector3(0.5f, 1f, 0.25f));
|
new Vector3(2f, 1f, 0.25f), new Vector3(0.2f, 1f, 0.25f));
|
||||||
|
|
||||||
// Build a flat BSP tree: one internal node with all three polys in a leaf.
|
// Build a flat BSP tree: one internal node with all three polys in a leaf.
|
||||||
// The bounding sphere covers everything.
|
// The bounding sphere covers everything.
|
||||||
|
|
|
||||||
|
|
@ -186,17 +186,27 @@ public class BSPStepUpTests
|
||||||
var (root, resolved) = BSPStepUpFixtures.LowStep();
|
var (root, resolved) = BSPStepUpFixtures.LowStep();
|
||||||
const float stepUpHeight = 0.30f; // larger than step (0.25), so step-up succeeds
|
const float stepUpHeight = 0.30f; // larger than step (0.25), so step-up succeeds
|
||||||
|
|
||||||
float startZ = BSPStepUpFixtures.SphereRadius;
|
// CurPos (foot position) starts at z=0 (on the terrain / BSP floor at z=0).
|
||||||
var from = new Vector3(0.1f, 0f, startZ);
|
// The sphere center is at CurPos + (0, 0, SphereRadius) = (x, 0, 0.2).
|
||||||
var to = new Vector3(0.7f, 0f, startZ); // crosses the wall at x=0.5
|
// lowPoint = sphere_center - (0,0,r) = (x, 0, 0) → on terrain → contact.
|
||||||
|
var from = new Vector3(0.1f, 0f, 0f);
|
||||||
|
// to.X = 0.6 → offset = (0.5, 0, 0), 3 sub-steps of 0.1667 each.
|
||||||
|
// Step 2: CurPos ≈ (0.433, 0, 0), sphere center x ≈ 0.433.
|
||||||
|
// Wall: dist = 0.5 - 0.433 = 0.067 < rad = 0.198 → HIT Path 5 ✓
|
||||||
|
var to = new Vector3(0.6f, 0f, 0f); // foot stays at z=0, crosses wall at x=0.5
|
||||||
|
|
||||||
var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight);
|
var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight);
|
||||||
var engine = MakeTestEngine(root, resolved);
|
// terrainZ=0f: terrain at z=0 keeps the step-down probe grounded between
|
||||||
|
// steps, preserving Contact/OnWalkable across the sub-step boundary.
|
||||||
|
var engine = MakeTestEngine(root, resolved, terrainZ: 0f);
|
||||||
|
|
||||||
bool ok = t.FindTransitionalPosition(engine);
|
bool ok = t.FindTransitionalPosition(engine);
|
||||||
|
|
||||||
// After step-up, the character's Z must be at or above the upper floor + radius.
|
// After step-up, the character's foot (CurPos.Z) must be at or above the
|
||||||
float expectedMinZ = 0.25f + BSPStepUpFixtures.SphereRadius - PhysicsGlobals.EPSILON * 10f;
|
// upper floor (z=0.25). CurPos stores the foot origin; the sphere center is
|
||||||
|
// CurPos.Z + SphereRadius. The lower bound is the upper-floor Z minus a
|
||||||
|
// small epsilon to tolerate floating-point rounding in AdjustSphereToPlane.
|
||||||
|
float expectedMinZ = 0.25f - PhysicsGlobals.EPSILON * 10f;
|
||||||
Assert.True(t.SpherePath.CurPos.Z >= expectedMinZ,
|
Assert.True(t.SpherePath.CurPos.Z >= expectedMinZ,
|
||||||
$"Expected Z >= {expectedMinZ:F4} (stepped up to upper floor at z=0.25), " +
|
$"Expected Z >= {expectedMinZ:F4} (stepped up to upper floor at z=0.25), " +
|
||||||
$"got CurPos.Z = {t.SpherePath.CurPos.Z:F4}. " +
|
$"got CurPos.Z = {t.SpherePath.CurPos.Z:F4}. " +
|
||||||
|
|
@ -222,12 +232,13 @@ public class BSPStepUpTests
|
||||||
var (root, resolved) = BSPStepUpFixtures.TallWall();
|
var (root, resolved) = BSPStepUpFixtures.TallWall();
|
||||||
const float stepUpHeight = 0.04f; // default — cannot scale 5 m wall
|
const float stepUpHeight = 0.04f; // default — cannot scale 5 m wall
|
||||||
|
|
||||||
float startZ = BSPStepUpFixtures.SphereRadius;
|
// Foot at z=0 (on terrain). Same reasoning as B1.
|
||||||
var from = new Vector3(0.1f, 0f, startZ);
|
var from = new Vector3(0.1f, 0f, 0f);
|
||||||
var to = new Vector3(0.7f, 0f, startZ);
|
var to = new Vector3(0.6f, 0f, 0f);
|
||||||
|
|
||||||
var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight);
|
var t = BSPStepUpFixtures.MakeGroundedTransition(from, to, stepUpHeight);
|
||||||
var engine = MakeTestEngine(root, resolved);
|
// terrainZ=0f: keep grounded between steps (same as B1).
|
||||||
|
var engine = MakeTestEngine(root, resolved, terrainZ: 0f);
|
||||||
|
|
||||||
t.FindTransitionalPosition(engine);
|
t.FindTransitionalPosition(engine);
|
||||||
|
|
||||||
|
|
@ -268,12 +279,13 @@ public class BSPStepUpTests
|
||||||
|
|
||||||
var localSphere = new DatReaderWriter.Types.Sphere { Origin = checkPos, Radius = r };
|
var localSphere = new DatReaderWriter.Types.Sphere { Origin = checkPos, Radius = r };
|
||||||
|
|
||||||
// NOTE: After L.2.1 this call gains an optional PhysicsEngine
|
// Pass engine so Path 5 can call DoStepUp → DoStepDown (L.2.1).
|
||||||
// parameter. Until then, the step-up flag is set but DoStepDown
|
// Without engine the fallback wall-slide would return Slid.
|
||||||
// cannot recurse (returns Slid). After L.2.1 result should be OK.
|
var engine = MakeTestEngine(root, resolved);
|
||||||
|
|
||||||
var result = BSPQuery.FindCollisions(
|
var result = BSPQuery.FindCollisions(
|
||||||
root, resolved, t, localSphere, null,
|
root, resolved, t, localSphere, null,
|
||||||
currPos, Vector3.UnitZ, 1.0f);
|
currPos, Vector3.UnitZ, 1.0f, Quaternion.Identity, engine);
|
||||||
|
|
||||||
// After L.2.1 this assertion flips from failing (Slid) to passing.
|
// After L.2.1 this assertion flips from failing (Slid) to passing.
|
||||||
Assert.NotEqual(TransitionState.Slid, result);
|
Assert.NotEqual(TransitionState.Slid, result);
|
||||||
|
|
@ -349,11 +361,17 @@ public class BSPStepUpTests
|
||||||
|
|
||||||
float roofZ = 3f;
|
float roofZ = 3f;
|
||||||
float r = BSPStepUpFixtures.SphereRadius;
|
float r = BSPStepUpFixtures.SphereRadius;
|
||||||
var from = new Vector3(0f, 0f, roofZ + r + 0.1f);
|
// CurPos = foot position. Sphere center = CurPos + (0,0,r).
|
||||||
var to = new Vector3(0f, 0f, roofZ + r - 0.05f); // sphere foot at z~3.0
|
// from: foot at z = roofZ - r + 0.3f → sphere center at roofZ + 0.3 = 3.3 (above roof)
|
||||||
|
// to: foot at z = roofZ - r - 0.05f → sphere center at roofZ - 0.05 = 2.95 (into roof by 0.05)
|
||||||
|
// Roof polygon at z=roofZ, normal=+Z: dist = sphere_center.z - roofZ.
|
||||||
|
// At to: dist = -0.05; |dist| = 0.05 < rad=0.198 → roof hit ✓
|
||||||
|
var from = new Vector3(0f, 0f, roofZ - r + 0.3f);
|
||||||
|
var to = new Vector3(0f, 0f, roofZ - r - 0.05f); // sphere bottom at z ≈ 2.95 (into roof)
|
||||||
|
|
||||||
var t = BSPStepUpFixtures.MakeAirborneTransition(from, to);
|
var t = BSPStepUpFixtures.MakeAirborneTransition(from, to);
|
||||||
var engine = MakeTestEngine(root, resolved);
|
// terrainZ=-50f: airborne mover — terrain must not interfere with roof landing.
|
||||||
|
var engine = MakeTestEngine(root, resolved, terrainZ: -50f);
|
||||||
|
|
||||||
t.FindTransitionalPosition(engine);
|
t.FindTransitionalPosition(engine);
|
||||||
|
|
||||||
|
|
@ -417,22 +435,24 @@ public class BSPStepUpTests
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Build a <see cref="PhysicsEngine"/> that serves one synthetic BSP object
|
/// Build a <see cref="PhysicsEngine"/> that serves one synthetic BSP object.
|
||||||
/// without any interfering terrain. The terrain is set 50 m underground
|
/// <paramref name="terrainZ"/> sets every terrain sample to the given height.
|
||||||
/// so it never fires during test geometry at z ≥ 0.
|
/// Use 0f for grounded tests (terrain flush with the BSP floor at z=0, so the
|
||||||
|
/// step-down probe finds ground and keeps Contact/OnWalkable set between steps).
|
||||||
|
/// Use -50f for tests where terrain must never interfere (airborne / roof landing).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static PhysicsEngine MakeTestEngine(
|
private static PhysicsEngine MakeTestEngine(
|
||||||
PhysicsBSPNode root,
|
PhysicsBSPNode root,
|
||||||
Dictionary<ushort, ResolvedPolygon> resolved,
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
||||||
Vector3? objectPosition = null)
|
Vector3? objectPosition = null,
|
||||||
|
float terrainZ = 0f)
|
||||||
{
|
{
|
||||||
const uint LandblockId = 0xA9B4FFFFu;
|
const uint LandblockId = 0xA9B4FFFFu;
|
||||||
const uint SyntheticGfxId = 0xDEADBEEFu;
|
const uint SyntheticGfxId = 0xDEADBEEFu;
|
||||||
|
|
||||||
// Terrain 50 m underground so FindEnvCollisions never fires push-ups.
|
|
||||||
var heights = new byte[81]; // all zero → uses index 0 from heightTable
|
var heights = new byte[81]; // all zero → uses index 0 from heightTable
|
||||||
var heightTab = new float[256];
|
var heightTab = new float[256];
|
||||||
for (int i = 0; i < 256; i++) heightTab[i] = -50f;
|
for (int i = 0; i < 256; i++) heightTab[i] = terrainZ;
|
||||||
|
|
||||||
var engine = new PhysicsEngine();
|
var engine = new PhysicsEngine();
|
||||||
engine.AddLandblock(
|
engine.AddLandblock(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue