test(phys): A6.P3 #98 — harness diagnostic + initial Z lift experiment

Adds two diagnostic-only tests:
- Harness_DiagnosticDump_FirstTenTicks: prints trajectory + resolve
  probe lines for the seeded-body path
- Harness_DiagnosticDump_NoBodySeed: same but with body=null, isolating
  whether the CP seed contributes to the airborne-at-tick-1 issue

Also adjusts InitialSphereWorld to lift the sphere by 0.05m above
cellar floor (sphere bottom at Z=91.00, not Z=90.95). The lift
should give the engine a clean step-down on tick 1 instead of an
exact-boundary contact.

Experimental finding: NEITHER the no-body-seed path NOR the 0.05m
lift changes the airborne-at-tick-1 behavior. With sphere center
at world Z=91.48 (0.05m + radius above cellar floor at 90.95):
- Tick 1: in=(141.5, 9.5, 91.48), out=(141.5, 9.5, 91.48) — Y move
  rejected. hit=yes n=(0,0,1) walkable=False.
- Tick 2+: Y advances by 0.1/tick, Z stays put, onGround stays False.

The hit normal (0,0,1) at tick 1 means the engine treats the cellar
floor polygon as a NON-WALKABLE collision target when the sphere is
seeded grounded above it. The walkability classifier returns False
even though Normal.Z=1.0 > FloorZ=0.6642. This is a real engine bug
worth investigating in a future session — independent of the cellar-up
freeze.

The synthetic ramp polygon registered via RegisterStairRampGfxObj is
NOT reached because the sphere is now airborne and floats over the
cellar floor without contacting the ramp.

Next session pickup options:
1. Debug the airborne-at-tick-1 issue (likely in TransitionTypes
   FindEnvCollisions indoor BSP path — why does a flat (0,0,1) hit
   return walkable=False?). Once fixed, the harness should reproduce
   cellar-up freeze.
2. Pivot to a different M1.5 issue with cleaner reproduction.
3. Use the harness mechanics elsewhere — the synthetic-GfxObj +
   ShadowEntry pattern is reusable for any indoor-static-collision
   test (corpse pickup boundaries, door swings, etc.).

Test baseline: 1167 + 5 (harness) = 1172 + 8 pre-existing failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-23 17:51:42 +02:00
parent 3d2d10b331
commit 227a77522a

View file

@ -96,14 +96,23 @@ public class CellarUpTrajectoryReplayTests
private const float StepDownHeight = 0.04f;
/// <summary>
/// Sphere center starts above cellar floor by exactly the radius
/// (bottom resting on floor). Y=9.5 is ~0.75 m before the ramp foot
/// at Y=8.75 (live-capture ramp plane equation:
/// <c>0.719·y + 0.695·z = 69.5035</c> → y=8.75 at z=90.95).
/// Sphere center starts slightly ABOVE its resting position on the
/// cellar floor (offset by an additional 0.05 m above sphere-bottom-
/// on-floor) to avoid the BSP query's floating-point boundary at
/// exact contact. With sphere center at exactly Z=floor+radius, the
/// engine reports hit=yes (back-face contact) and the body goes
/// airborne; with a 0.05 m lift, step-down on tick 1 should snap
/// the sphere cleanly to the floor.
///
/// <para>
/// Y=9.5 is ~0.75 m before the ramp foot at Y=8.75 (live-capture
/// ramp plane: <c>0.719·y + 0.695·z = 69.5035</c> → y=8.75 at z=90.95).
/// X=141.5 matches the live capture's X.
/// </para>
/// </summary>
private const float InitialZLift = 0.05f;
private static readonly Vector3 InitialSphereWorld =
new(141.5f, 9.5f, CellarFloorZ + SphereRadius);
new(141.5f, 9.5f, CellarFloorZ + SphereRadius + InitialZLift);
/// <summary>
/// Per-tick forward offset (Y direction toward the ramp).
@ -168,6 +177,60 @@ public class CellarUpTrajectoryReplayTests
}
}
/// <summary>
/// Experiment: drive without a PhysicsBody (no CP seeding, no
/// cross-tick state). Tests whether the airborne-at-tick-1 issue
/// is caused by the seeded CP creating a false collision against
/// the cellar floor.
/// </summary>
[Fact]
public void Harness_DiagnosticDump_NoBodySeed()
{
PhysicsDiagnostics.ProbeResolveEnabled = true;
try
{
var (engine, _) = BuildEngineWithCellarFixtures();
uint cellId = CellarId;
bool isOnGround = true;
Vector3 pos = InitialSphereWorld;
var trajectory = new List<TrajectoryPoint>
{
new(0, pos, cellId, isOnGround, false),
};
for (int tick = 1; tick <= 10; tick++)
{
Vector3 target = pos + PerTickOffset;
var result = engine.ResolveWithTransition(
pos, target, cellId,
SphereRadius, SphereHeight,
StepUpHeight, StepDownHeight,
isOnGround,
body: null, // ← no body, no CP seed
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
pos = result.Position;
cellId = result.CellId;
isOnGround = result.IsOnGround;
trajectory.Add(new(tick, pos, cellId, isOnGround, false));
}
var msg = "No-body trajectory (10 ticks):\n " +
string.Join("\n ", trajectory.Select(p =>
$"tick={p.Tick} pos=({p.Position.X:F4},{p.Position.Y:F4},{p.Position.Z:F4}) " +
$"onGround={p.IsOnGround}"));
Assert.True(true, msg);
}
finally
{
PhysicsDiagnostics.ProbeResolveEnabled = false;
}
}
/// <summary>
/// Documents finding #2: at the initial grounded position, the
/// engine reports the cellar floor as a non-walkable collision