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:
Erik 2026-04-29 16:16:39 +02:00
parent b0c29454d0
commit 670f892bd3
4 changed files with 341 additions and 179 deletions

View file

@ -82,7 +82,9 @@ public static class BSPStepUpFixtures
/// <list type="bullet">
/// <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>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.30.5 (the wall contact zone).</item>
/// </list>
/// </summary>
public static (PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved)
@ -105,10 +107,14 @@ public static class BSPStepUpFixtures
new Vector3(0.5f, 1f, 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(
new Vector3(0.5f, -1f, 0.25f), new Vector3(2f, -1f, 0.25f),
new Vector3(2f, 1f, 0.25f), new Vector3(0.5f, 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.2f, 1f, 0.25f));
// Build a flat BSP tree: one internal node with all three polys in a leaf.
// The bounding sphere covers everything.