fix(physics): #265 landing-bounce family - retail check_contact seed + velocity-free landing commit
Retail jump landings BOUNCE: the floor touch records both a contact plane (grounding) AND a collision normal (collided_with_environment), and handle_all_collisions reflects the unmodified impact velocity off it at 5% elasticity (v += -(v.n)(elasticity+1).n, DEFAULT_ELASTICITY 0.05 @0x007c6a7c). Our transition already recorded both facts; the bounce was suppressed by the AD-25 adaptation stack in the per-tick commit: a Velocity.Z<=0 landing gate (needed because the resolver glued ascending movers to the ground) plus a landing Velocity.Z=0 hand-zero whose stated purpose was making the reflect a no-op. Downhill glided instead of bouncing, flat-ground landings had no pop, and uphill jumps flapped between grounded/airborne against the animation machine. Three retail mechanisms replace the stack: - check_contact (0x0050f5b0) seeding in ResolveWithTransition: a body in CONTACT seeds the transition's contact only while v.contactPlane.N <= 0.0002; moving away seeds the last-known plane alone (get_object_info 0x00511cc0). Ascending jumps therefore run contact-free (ballistic, no glue) - the gate's reason-for-being is gone. The plane requirement is strict: Contact-without-plane is unrepresentable in retail. - SetPositionInternal-shaped commit (0x00515330, byte-read end-to-end, velocity-sign-FREE): contact purely from the transition's contact plane, HitGround on the airborne->walkable edge, HandleAllCollisions with unmodified impact velocity. Whole commit gated on Ok && candidateMoved (retail pc:283657 skips SetPositionInternal entirely when the candidate did not move) - a standing body's contact state is never re-derived, which is what keeps rest bit-stable (AD-41 updated). - Byte decodes: gate override state&0x800000=Sledding, zero branch state&0x20000=Inelastic, reflect strictly dot<0 - our port already had all three correct. Settle: real landings (>=0.25 m/s) bounce and decay geometrically; smaller impacts are consumed by retail's unconditional small-velocity zero, so standing never micro-bounces. Re-baselines documented in place: landing-survival pin measures decay post-settle; LiveCompare_Tick0/376 pin the new IsOnGround=false on zero-move ticks (captured true was the retired seed echo; tick 376's captured body carries an 11.8 m/s grounded velocity from the deleted get_state_velocity-overwrite era); de-overlap fixture now carries the plane real grounded bodies always have. New pins: LandingBounceSeedingTests (ascent no-seed, rest keeps contact, strict plane, slope 5% reversal + tangential preservation, Sledding override). Investigation + implementation record: docs/research/2026-07-30-landing-bounce-family.md. Complete Release suite: 10,031 passed / 5 skips / 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7fcc7db1d1
commit
2d611b2b01
10 changed files with 620 additions and 84 deletions
|
|
@ -606,12 +606,25 @@ public class CellarUpTrajectoryReplayTests : IDisposable
|
|||
/// motion). The simplest test case: replay the call and verify the
|
||||
/// harness produces the same ResolveResult + bodyAfter state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// #265 bounce rework (2026-07-30,
|
||||
/// docs/research/2026-07-30-landing-bounce-family.md): the capture's
|
||||
/// <c>Result.IsOnGround=true</c> on this ZERO-MOVE tick was the old
|
||||
/// seed's echo — the caller's isOnGround came straight back through the
|
||||
/// seeded oi flags. The retail check_contact seed (get_object_info
|
||||
/// 0x00511cc0) requires a stored contact plane, and this captured body
|
||||
/// has <c>cpV=false</c>, so the modern resolve reports IsOnGround=false
|
||||
/// here. Production no longer consumes IsOnGround on no-move frames
|
||||
/// (the candidateMoved commit gate — retail pc:283657 skips
|
||||
/// SetPositionInternal entirely), so the echo is intentionally gone;
|
||||
/// the comparison pins the NEW value instead.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void LiveCompare_Tick0_Spawn()
|
||||
{
|
||||
var (engine, cache) = BuildEngineWithCellarFixtures();
|
||||
var captured = LoadCapturedRecord(record => record.Tick == 0);
|
||||
AssertCallMatchesCapture(engine, captured);
|
||||
AssertCallMatchesCapture(engine, captured, expectStaleIsOnGroundEcho: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -621,12 +634,24 @@ public class CellarUpTrajectoryReplayTests : IDisposable
|
|||
/// the same walkable polygon + ResolveResult, the ramp geometry is
|
||||
/// loaded correctly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// #265 bounce rework (2026-07-30): same stale IsOnGround echo as
|
||||
/// <see cref="LiveCompare_Tick0_Spawn"/>, with a second stale-era quirk:
|
||||
/// the captured bodyBefore carries Velocity=(0.64, 11.83, 0) while
|
||||
/// GROUNDED on the ramp — a value from the deleted
|
||||
/// get_state_velocity-overwrite era (root motion owns walking now;
|
||||
/// grounded Velocity stays ~0). dot(v, rampNormal)=+8.5 rightly fails
|
||||
/// retail check_contact (0x0050f5b0: contact holds only while
|
||||
/// v·n ≤ 0.0002), so the modern resolve refuses the stale contact on
|
||||
/// this zero-move tick. Geometry checks (the ramp polygon) still
|
||||
/// compare exactly.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void LiveCompare_Tick376_OnRamp()
|
||||
{
|
||||
var (engine, _) = BuildEngineWithCellarFixtures();
|
||||
var captured = LoadCapturedRecord(record => record.Tick == 376);
|
||||
AssertCallMatchesCapture(engine, captured);
|
||||
AssertCallMatchesCapture(engine, captured, expectStaleIsOnGroundEcho: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -838,7 +863,8 @@ public class CellarUpTrajectoryReplayTests : IDisposable
|
|||
/// </summary>
|
||||
private static void AssertCallMatchesCapture(
|
||||
PhysicsEngine engine,
|
||||
ResolveCaptureRecord captured)
|
||||
ResolveCaptureRecord captured,
|
||||
bool expectStaleIsOnGroundEcho = false)
|
||||
{
|
||||
Assert.NotNull(captured.BodyBefore);
|
||||
Assert.NotNull(captured.BodyAfter);
|
||||
|
|
@ -867,8 +893,19 @@ public class CellarUpTrajectoryReplayTests : IDisposable
|
|||
AddIfDifferent(divergences, "Result.CellId",
|
||||
$"0x{captured.Result.CellId:X8}",
|
||||
$"0x{harnessResult.CellId:X8}");
|
||||
AddIfDifferent(divergences, "Result.IsOnGround",
|
||||
captured.Result.IsOnGround, harnessResult.IsOnGround);
|
||||
if (expectStaleIsOnGroundEcho)
|
||||
{
|
||||
// #265 bounce rework: the capture's grounded echo on this tick is
|
||||
// the retired pre-check_contact seed. Pin the NEW deterministic
|
||||
// value (false) so any further drift still fails loudly.
|
||||
AddIfDifferent(divergences, "Result.IsOnGround(post-#265)",
|
||||
false, harnessResult.IsOnGround);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddIfDifferent(divergences, "Result.IsOnGround",
|
||||
captured.Result.IsOnGround, harnessResult.IsOnGround);
|
||||
}
|
||||
AddIfDifferent(divergences, "Result.CollisionNormalValid",
|
||||
captured.Result.CollisionNormalValid,
|
||||
harnessResult.CollisionNormalValid);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue