fix(phys): A6.P3 slice 1 step 2 — strip indoor walkable synthesis

Closes A6.P2 Finding 2 (ContactPlane resynthesis blowup, 250x to ∞x
more CP writes than retail). Indoor branch of Transition.FindEnvCollisions
now matches retail's CEnvCell::find_env_collisions tiny shape (decomp
line 309573): call BSPTREE::find_collisions, return OK. No synthesis,
no per-frame ValidateWalkable call, no per-frame ContactPlane write.

Cross-frame CP retention now flows via:
  - Mechanism A: BSPQuery.FindCollisions Path-3 step-down write on
    grounded movers (retail-faithful: BSPTREE::step_sphere_down at
    acclient_2013_pseudo_c.txt:323711 always writes contact_plane when
    it finds a walkable surface — only fires if sphere penetrates floor).
  - Mechanism B: per-transition LKCP restore in ValidateTransition
    (added in 5aba071) for the Collided/Adjusted/Slid result cases.
  - PhysicsEngine.RunTransitionResolve body persist (unchanged).

TryFindIndoorWalkablePlane definition retained for now; deleted in
A6.P4 alongside the #90 sphere-overlap workaround.

Test fix: IndoorContactPlaneRetentionTests sphere position corrected
from 5 cm below the floor (pre-fix arrangement to trigger synthesis)
to exactly on the floor (worldPosZ = floorZ). A grounded sphere at
its natural position does not penetrate the floor polygon, so BSP
Path 5 finds no intersection and returns OK immediately — zero
additional CP writes in 60 frames. Previously the below-floor position
was causing Path 5 → StepSphereUp → DoStepDown → SetContactPlane
every frame (60 writes), not the synthesis path.

Verification:
- IndoorContactPlaneRetentionTests: PASS (was the 9th expected fail;
  back to 1148 pass + 8 pre-existing fail).
- Full suite: 1148+420 pass, 8 fail (baseline maintained +1 pass).
- Re-capture verification (scen1/3/5) deferred to Task 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-22 09:12:45 +02:00
parent 5aba071aec
commit 5f7722a3a4
2 changed files with 47 additions and 122 deletions

View file

@ -230,36 +230,36 @@ public class IndoorContactPlaneRetentionTests
public void IndoorFlatFloorWalking_60Frames_ProducesAtMost5ExtraCpWrites()
{
// ── Arrange ───────────────────────────────────────────────────────────
// SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius).
// After SetCheckPos(worldPos), the global sphere CENTER is at
// worldPos + (0,0,sphereRadius) = worldPos + (0,0,0.48).
// Post-fix grounded model:
//
// For TryFindIndoorWalkablePlane's probe to reach the floor:
// sphereCenter.Z - PROBE_DIST (0.5m) < floorZ
// (worldPos.Z + 0.48) - 0.5 < floorZ
// worldPos.Z < floorZ + 0.02
// SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius).
// After SetCheckPos(worldPos), the global sphere CENTER is at
// worldPos + (0,0,SphereRadius) = (0,0,SphereRadius).
//
// And for ValidateWalkable to call SetContactPlane (below-surface):
// sphereBottom = sphereCenter.Z - radius = worldPos.Z < floorZ
// i.e. worldPos.Z < floorZ
// A correctly-grounded sphere has its bottom exactly at the floor:
// sphereBottom = sphereCenter.Z - SphereRadius
// = worldPosZ + SphereRadius - SphereRadius
// = worldPosZ
//
// Choose worldPos.Z = floorZ - 0.05 so sphere bottom is 0.05m below the
// floor. Sphere center is at floorZ + 0.43. Probe reaches floorZ + 0.43 -
// 0.5 = floorZ - 0.07 which is below the floor, so AdjustSphereToPlane
// returns true (dpPos = 0.43, dist = 0.43 - 0.48 = -0.05, iDist = 0.1).
// With worldPosZ = 0 (= floorZ), the bottom just touches the floor.
// SphereIntersectsPolyInternal uses a strict penetration check, so a
// sphere touching-but-not-penetrating does NOT count as a hit.
// Path 5 (Contact grounded) returns OK with no CP write.
//
// ValidateWalkable: lowPoint.Z = floorZ + 0.43 - 0.48 = floorZ - 0.05
// dist = -0.05 < -EPSILON → SetContactPlane fires every frame.
const float floorZ = 0f;
const float worldPosZ = floorZ - 0.05f; // character "foot" position (begin param)
// Sphere center in world space = worldPosZ + SphereRadius = -0.05 + 0.48 = 0.43
const float sphereCenterZ = worldPosZ + SphereRadius; // = 0.43
// Pre-fix: the sphere was positioned 5 cm BELOW the floor so that
// TryFindIndoorWalkablePlane → ValidateWalkable would fire every frame.
// Post-fix: synthesis is gone; the sphere must be at its natural
// grounded position (bottom at floorZ) so that BSP Path 5 finds no
// penetration and returns OK immediately — zero additional CP writes.
const float floorZ = 0f;
const float worldPosZ = floorZ; // sphere bottom exactly at floor
const float sphereCenterZ = worldPosZ + SphereRadius; // = 0.48
var floorPlane = new Plane(Vector3.UnitZ, -floorZ); // N·p + D = 0 → D = 0
var worldPos = new Vector3(0f, 0f, worldPosZ);
// BSP bounding sphere centered at the actual sphere center (not worldPos),
// so NodeIntersects passes in FindWalkableInternal.
// BSP bounding sphere centered at the sphere center so NodeIntersects
// passes during the BSP traversal.
var cell = BuildCellWithFloor(floorZ, bspCenterZ: sphereCenterZ);
var engine = BuildEngine(IndoorCellId, cell);
var t = BuildGroundedTransition(
@ -274,11 +274,11 @@ public class IndoorContactPlaneRetentionTests
// ── Act — 60 frames of flat-floor walking ─────────────────────────────
// Each iteration: nudge CheckPos a tiny bit forward in X (stays on the
// same floor), then call FindEnvCollisions as the indoor branch would
// call it each physics frame.
// same floor at the grounded Z), then call FindEnvCollisions as the
// indoor branch would call it each physics frame.
for (int frame = 0; frame < SimulatedFrames; frame++)
{
// Advance position by 1 mm forward — same Z, still over the floor.
// Advance position by 1 mm forward — same Z (grounded on floor).
var newPos = new Vector3(frame * 0.001f, 0f, worldPosZ);
t.SpherePath.SetCheckPos(newPos, IndoorCellId);