diff --git a/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs b/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs index cc40c59..fc992a2 100644 --- a/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs @@ -230,30 +230,38 @@ public class IndoorContactPlaneRetentionTests public void IndoorFlatFloorWalking_60Frames_ProducesAtMost5ExtraCpWrites() { // ── Arrange ─────────────────────────────────────────────────────────── - // Post-fix grounded model: + // Geometry: sphere center 5 cm BELOW the floor so that the synthesis + // path's distance guard passes and TryFindIndoorWalkablePlane would + // actually find the floor polygon if synthesis were re-introduced. // - // SpherePath.InitPath sets LocalSphere[0].Origin = (0,0,sphereRadius). + // 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). + // worldPos + (0,0,SphereRadius) = (worldPosX, 0, -0.05 + 0.48 = 0.43). // - // A correctly-grounded sphere has its bottom exactly at the floor: - // sphereBottom = sphereCenter.Z - SphereRadius - // = worldPosZ + SphereRadius - SphereRadius - // = worldPosZ + // PolygonHitsSpherePrecise distance guard (BSPQuery.cs line ~117): + // dist = Dot(normal=(0,0,1), center=(x,0,0.43)) + D(=0) = 0.43 + // rad = SphereRadius - EPSILON = 0.48 - 0.002 = 0.478 + // |dist| = 0.43 < 0.478 → guard passes → polygon is tested → FOUND. + // So TryFindIndoorWalkablePlane WOULD call ValidateWalkable → CP write + // if the synthesis call were present. With the strip in place it is never + // called → ≤5 additional writes → PASS. // - // 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. + // With the post-5f7722a setup (worldPosZ = floorZ = 0): + // dist = 0.48, rad = 0.478, |dist| = 0.48 > 0.478 → guard FIRES → + // TryFindIndoorWalkablePlane returns false even WITH synthesis code. + // That setup was not a regression sentinel; this one is. // - // 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 + // Path 5 (BSP Contact branch, BSPQuery.cs ~line 1732): + // The loop advances only in X, so movement = (0.001, 0, 0). + // PosHitsSphere culls hits where Dot(movement, normal) >= 0. + // Dot((0.001,0,0), (0,0,1)) = 0 >= 0 → floor polygon is ALWAYS + // rejected by the front-face cull, even though the sphere center is + // below the floor. Path 5 exits OK with no CP write regardless of + // whether the Contact flag is set. Leaving Contact set keeps this + // test on the realistic grounded-mover path. + const float floorZ = 0f; + const float worldPosZ = floorZ - 0.05f; // 5 cm below floor + const float sphereCenterZ = worldPosZ + SphereRadius; // = 0.43 var floorPlane = new Plane(Vector3.UnitZ, -floorZ); // N·p + D = 0 → D = 0 var worldPos = new Vector3(0f, 0f, worldPosZ); @@ -278,7 +286,7 @@ public class IndoorContactPlaneRetentionTests // indoor branch would call it each physics frame. for (int frame = 0; frame < SimulatedFrames; frame++) { - // Advance position by 1 mm forward — same Z (grounded on floor). + // Advance position by 1 mm forward — same Z (sphere center 5 cm below floor). var newPos = new Vector3(frame * 0.001f, 0f, worldPosZ); t.SpherePath.SetCheckPos(newPos, IndoorCellId);