feat(#182): PhysicsBody fsf state + CachedVelocity; ungate small-velocity-zero (Slice 1a)

Verbatim UpdatePhysicsInternal (0x00510700): the small-velocity-zero
(<0.25 m/s) fires unconditionally, not gated on OnWalkable (the old acdream
divergence) — gravity re-accelerates the same frame via the unconditional
v += a*dt. Adds TransientStateFlags.Stationary{Fall,Stop,Stuck} (0x10/0x20/0x40)
for the fsf round-trip, plus PhysicsBody.FramesStationaryFall and the separate
CachedVelocity field (retail's two-velocity model — reporting/DR only, never fed
to the integrator). All 1536 physics tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-07 13:39:28 +02:00
parent 8bb8b20411
commit 6e8117741b
2 changed files with 72 additions and 6 deletions

View file

@ -162,6 +162,46 @@ public sealed class PhysicsBodyTests
Assert.False(body.IsActive);
}
[Fact]
public void UpdatePhysicsInternal_zeroes_small_velocity_even_when_airborne()
{
// Retail UpdatePhysicsInternal (0x005107be) zeroes velocity below 0.25 m/s
// UNCONDITIONALLY — NOT gated on OnWalkable. acdream previously gated it on
// OnWalkable; the verbatim rebuild removes the gate. Gravity re-accelerates the
// same frame via the unconditional `Velocity += Acceleration * dt`, so the fall
// still accumulates on Z.
var body = MakeAirborne(); // not Contact, not OnWalkable
body.set_velocity(new Vector3(0.1f, 0f, 0f)); // < 0.25 m/s
body.Acceleration = new Vector3(0f, 0f, PhysicsBody.Gravity);
body.UpdatePhysicsInternal(1f / 30f);
Assert.True(MathF.Abs(body.Velocity.X) < 1e-4f, $"X not zeroed: {body.Velocity.X}");
Assert.True(body.Velocity.Z < 0f, $"gravity did not accumulate: {body.Velocity.Z}");
}
// ════════════════════════════════════════════════════════════════════
// frames_stationary_fall carry state (retail transient_state bits)
// ════════════════════════════════════════════════════════════════════
[Fact]
public void TransientStateFlags_has_stationary_bits()
{
// retail transient_state StationaryFall/Stop/Stuck (handle_all_collisions
// pc:282743/282749/282753; seeded back into transition pc:280940-947).
Assert.Equal(0x10u, (uint)TransientStateFlags.StationaryFall);
Assert.Equal(0x20u, (uint)TransientStateFlags.StationaryStop);
Assert.Equal(0x40u, (uint)TransientStateFlags.StationaryStuck);
}
[Fact]
public void PhysicsBody_has_fsf_and_cached_velocity_defaults()
{
var body = new PhysicsBody();
Assert.Equal(0, body.FramesStationaryFall);
Assert.Equal(Vector3.Zero, body.CachedVelocity);
}
// ════════════════════════════════════════════════════════════════════
// set_velocity — velocity clamping
// ════════════════════════════════════════════════════════════════════