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.
|
||||
///
|
||||
/// <para>
|
||||
/// Sets the StepUp flag on SpherePath with the collision normal.
|
||||
/// The Transition's outer loop will pick this up and attempt the step.
|
||||
/// If StepUp is already pending, falls back to setting the collision normal
|
||||
/// directly (StepUpSlide equivalent).
|
||||
/// Calls <see cref="Transition.DoStepUp"/> which probes upward then steps
|
||||
/// down to find a walkable landing surface. If the step-up succeeds the
|
||||
/// sphere's CheckPos is already updated and we return OK. If it fails we
|
||||
/// fall back to StepUpSlide: clear the contact plane and slide along the
|
||||
/// collision normal.
|
||||
/// </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>
|
||||
private static TransitionState StepSphereUp(
|
||||
Transition transition,
|
||||
Vector3 collisionNormal)
|
||||
Transition transition,
|
||||
Vector3 collisionNormal,
|
||||
PhysicsEngine engine)
|
||||
{
|
||||
var path = transition.SpherePath;
|
||||
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;
|
||||
if (transition.DoStepUp(collisionNormal, engine!))
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
// StepUpSlide: can't step up, set collision normal and report adjusted.
|
||||
ci.SetCollisionNormal(collisionNormal);
|
||||
return TransitionState.Adjusted;
|
||||
return transition.SpherePath.StepUpSlide(transition.CollisionInfo);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -1364,7 +1358,8 @@ public static class BSPQuery
|
|||
Vector3 localCurrCenter,
|
||||
Vector3 localSpaceZ,
|
||||
float scale,
|
||||
Quaternion localToWorld = default)
|
||||
Quaternion localToWorld = default,
|
||||
PhysicsEngine? engine = null)
|
||||
{
|
||||
if (root is null) return TransitionState.OK;
|
||||
// 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
|
||||
// ACE retail uses StepSphereUp here, deferring to a retry loop that
|
||||
// executes the step-up motion. We haven't ported that execution, so
|
||||
// we apply the same wall-slide response as Path 6 — this at least
|
||||
// gives correct blocking + sliding behavior for walls, buildings,
|
||||
// and tree trunks while the player is on the ground.
|
||||
// Path 5: Contact (grounded) — sphere_intersects_poly + step_sphere_up
|
||||
//
|
||||
// A grounded mover hits a polygon. Retail calls BSPTREE::step_sphere_up,
|
||||
// which runs CTransition::step_up (upward probe + step-down scan). If the
|
||||
// obstacle is short enough the sphere climbs it; if too tall, it falls back
|
||||
// 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))
|
||||
{
|
||||
|
|
@ -1470,26 +1468,12 @@ public static class BSPQuery
|
|||
|
||||
if (hit0 || hitPoly0 is not null)
|
||||
{
|
||||
// Wall-slide response (same as Path 6 below).
|
||||
var localNormal = hitPoly0!.Plane.Normal;
|
||||
var localMovement = sphere0.Center - localCurrCenter;
|
||||
var worldNormal = L2W(hitPoly0!.Plane.Normal);
|
||||
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) + 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);
|
||||
// No engine available (env-cell path without engine param) —
|
||||
// fall back to wall-slide so existing indoor geometry still blocks.
|
||||
collisions.SetCollisionNormal(worldNormal);
|
||||
collisions.SetSlidingNormal(worldNormal);
|
||||
return TransitionState.Slid;
|
||||
|
|
@ -1505,25 +1489,10 @@ public static class BSPQuery
|
|||
|
||||
if (hit1 || hitPoly1 is not null)
|
||||
{
|
||||
var localNormal = hitPoly1!.Plane.Normal;
|
||||
var localMovement = sphere1.Center - localCurrCenter;
|
||||
var worldNormal = L2W(hitPoly1!.Plane.Normal);
|
||||
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.SetSlidingNormal(worldNormal);
|
||||
return TransitionState.Slid;
|
||||
|
|
@ -1553,50 +1522,19 @@ public static class BSPQuery
|
|||
hitPoly0!, contact0, scale, localToWorld);
|
||||
}
|
||||
|
||||
// ─── Wall-slide response ─────────────────────────────────
|
||||
// Instead of just pushing the sphere out of penetration
|
||||
// (which undoes the whole step), compute the wall-slide
|
||||
// position: where the sphere WOULD be if the movement had
|
||||
// been projected along the wall tangent.
|
||||
// ─── SetCollide response ─────────────────────────────────
|
||||
// Airborne sphere hits a polygon. Per retail, call SetCollide
|
||||
// which saves backup position, records StepUpNormal = worldNormal,
|
||||
// and sets WalkInterp=1. TransitionalInsert's Collide branch will
|
||||
// then re-test as Placement to confirm we can land on the surface.
|
||||
//
|
||||
// In local space:
|
||||
// curr = localCurrCenter
|
||||
// target = sphere0.Center
|
||||
// movement = target - curr
|
||||
// normal = polygon plane normal (outward)
|
||||
// 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);
|
||||
// ACE: BSPTree.find_collisions default branch → SpherePath.SetCollide
|
||||
// + return Adjusted.
|
||||
// Named-retail: BSPTREE::find_collisions airborne branch → set_collide.
|
||||
var worldNormal0 = L2W(hitPoly0!.Plane.Normal);
|
||||
path.SetCollide(worldNormal0);
|
||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
||||
collisions.SetCollisionNormal(worldNormal);
|
||||
collisions.SetSlidingNormal(worldNormal);
|
||||
return TransitionState.Slid;
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
||||
if (sphere1 is not null)
|
||||
|
|
@ -1609,29 +1547,11 @@ public static class BSPQuery
|
|||
|
||||
if (hit1 || hitPoly1 is not null)
|
||||
{
|
||||
// Head sphere hit: apply the same wall-slide as above.
|
||||
var localNormal = hitPoly1!.Plane.Normal;
|
||||
var localMovement = sphere1.Center - localCurrCenter;
|
||||
|
||||
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.SetSlidingNormal(worldNormal);
|
||||
return TransitionState.Slid;
|
||||
// Head sphere hit: same SetCollide response.
|
||||
var worldNormal1 = L2W(hitPoly1!.Plane.Normal);
|
||||
path.SetCollide(worldNormal1);
|
||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue