using System; using System.Numerics; using AcDream.Core.Physics; using Xunit; using Xunit.Abstractions; namespace AcDream.Core.Tests.Physics; /// /// End-to-end (Core) proof of the #182 airborne-stuck fix: a jump velocity blocked by a /// creature BLEEDS to zero within a few frames so gravity can resume — the player falls / /// glides off instead of hanging in the falling animation. Chains the same steps the /// PlayerMovementController per-frame loop runs, minus the App layer: /// integrate (UpdatePhysicsInternal + gravity) → ResolveWithTransition → apply → /// handle_all_collisions (PhysicsObjUpdate). /// Before the rebuild the velocity persisted (only an airborne-only reflect that barely /// touched +Z against a near-horizontal normal); now fs>1 zeros it. /// public class Issue182CrowdJumpTests { private readonly ITestOutputHelper _out; public Issue182CrowdJumpTests(ITestOutputHelper output) => _out = output; private const uint Lb = 0xA9B40000u; private const uint Cell = Lb | 0x0001u; private const float R = 0.48f, H = 1.835f, StepUp = 0.60f, StepDown = 0.04f; private const float Dt = 1f / 30f; // one physics quantum private static PhysicsEngine BuildEngine() { var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock(Lb, new TerrainSurface(new byte[81], new float[256]), Array.Empty(), Array.Empty(), 0f, 0f); return engine; } private static void RegisterCreatureAt(PhysicsEngine e, uint id, Vector3 c) => e.ShadowObjects.Register(id, 0u, c, Quaternion.Identity, R, 0f, 0f, Lb, ShadowCollisionType.Sphere, 0f, 1f, 0u, EntityCollisionFlags.IsCreature, isStatic: false); [Fact] public void BlockedJump_VelocityBleedsToZero_ThenGravityResumes() { var engine = BuildEngine(); RegisterCreatureAt(engine, 0xC0B0u, new Vector3(12f, 10f, 4.5f)); // creature overhead var body = new PhysicsBody { Position = new Vector3(12f, 10f, 3.0f), Orientation = Quaternion.Identity, State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, TransientState = TransientStateFlags.None, // airborne Velocity = new Vector3(0f, 0f, 12f), // a jump: moving straight up }; uint cell = Cell; bool bled = false; for (int frame = 0; frame < 20; frame++) { // 1) integrate velocity + gravity into a candidate (mirrors the controller's §4). var pre = body.Position; body.calc_acceleration(); body.UpdatePhysicsInternal(Dt); var post = body.Position; bool candidateMoved = post != pre; // retail candidate != m_position gate // 2) resolve the candidate against the crowd. var r = engine.ResolveWithTransition(pre, post, cell, R, H, StepUp, StepDown, isOnGround: false, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide); // 3) apply + contact determination (mirrors the controller's SetPositionInternal). bool prevOnWalkable = body.OnWalkable; body.Position = r.Position; cell = r.CellId; if (r.IsOnGround && body.Velocity.Z <= 0f) { body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable; body.calc_acceleration(); if (body.Velocity.Z < 0f) body.Velocity = new Vector3(body.Velocity.X, body.Velocity.Y, 0f); } else { body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable); body.calc_acceleration(); } // 4) handle_all_collisions: reflect (fsf<=1) or zero (fsf>1) — but ONLY when the // candidate moved (retail skips SetPositionInternal otherwise), so gravity can // rebuild the velocity after the bleed instead of it being re-zeroed each frame. if (candidateMoved) PhysicsObjUpdate.HandleAllCollisions(body, r.CollisionNormalValid, r.CollisionNormal, prevContact: false, prevOnWalkable, nowOnWalkable: body.OnWalkable); _out.WriteLine($"frame{frame,2}: z={body.Position.Z:F3} vz={body.Velocity.Z:F3} " + $"fsf={body.FramesStationaryFall}"); // The upward jump velocity must collapse to ~0 (or reverse to falling) within a few // frames of being blocked — the bleed. Before the fix it persisted near +12. if (frame >= 1 && frame <= 6 && body.Velocity.Z <= 0.5f) bled = true; } Assert.True(bled, "the blocked jump velocity must bleed to ~0 within a few frames (fsf>1 → v=0)"); // After bleeding, gravity has taken over — the body is no longer being shoved upward. Assert.True(body.Velocity.Z <= 0.5f, $"velocity should not persist upward; got vz={body.Velocity.Z:F3}"); } }