using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Conformance tests for — the port of /// retail CPhysicsObj::handle_all_collisions (0x00514780, pc:282647). This is the /// velocity "bleed on block" decision: reflect (fsf≤1) vs zero (fsf>1). /// public class HandleAllCollisionsTests { private static PhysicsBody Airborne(Vector3 v, int fsf = 0) => new PhysicsBody { State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, TransientState = TransientStateFlags.None, Velocity = v, FramesStationaryFall = fsf, }; [Fact] public void Fsf0_AirborneWallHit_ReflectsIntoWallComponent() { var b = Airborne(new Vector3(3f, 0f, 0f)); // moving +X into a wall whose outward normal is -X var n = new Vector3(-1f, 0f, 0f); PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: true, collisionNormal: n, prevContact: false, prevOnWalkable: false, nowOnWalkable: false); // dot = 3*-1 = -3 < 0 → k = -(-3*(0.05+1)) = 3.15 → v += (-1,0,0)*3.15 → x = 3 - 3.15 = -0.15 Assert.Equal(-0.15f, b.Velocity.X, precision: 3); } [Fact] public void Fsf0_MovingAwayFromSurface_DoesNotReflect() { var b = Airborne(new Vector3(0f, 0f, 2f)); // moving up, normal also up (already separating) var n = new Vector3(0f, 0f, 1f); PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: true, collisionNormal: n, prevContact: false, prevOnWalkable: false, nowOnWalkable: false); Assert.Equal(2f, b.Velocity.Z); // dot = +2 >= 0 → no reflection } [Fact] public void Fsf2_ZeroesVelocity_TheAirborneStuckBleed() { // The #182 case: a straight-up jump blocked by a near-horizontal creature normal. // At fsf>1 the whole velocity is zeroed so gravity resumes → the player falls/glides off. var b = Airborne(new Vector3(0f, 0f, 18f), fsf: 2); var n = new Vector3(-0.96f, -0.25f, -0.15f); PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: true, collisionNormal: n, prevContact: false, prevOnWalkable: false, nowOnWalkable: false); Assert.Equal(Vector3.Zero, b.Velocity); } [Fact] public void Fsf3_ZeroesVelocity_EvenWithoutCollisionNormal() { var b = Airborne(new Vector3(1f, 2f, 18f), fsf: 3); PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: false, collisionNormal: default, prevContact: false, prevOnWalkable: false, nowOnWalkable: false); Assert.Equal(Vector3.Zero, b.Velocity); } [Fact] public void StayingOnWalkable_DoesNotReflect_CorridorWallSlidePreserved() { // Grounded before AND after → should_reflect is false → the tangential wall-slide // velocity is preserved (retail's rule; the corridor shuffle, not a sticky bounce). var b = new PhysicsBody { Velocity = new Vector3(3f, 0f, 0f), TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, FramesStationaryFall = 0, }; var n = new Vector3(-1f, 0f, 0f); PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: true, collisionNormal: n, prevContact: true, prevOnWalkable: true, nowOnWalkable: true); Assert.Equal(3f, b.Velocity.X); } [Fact] public void Inelastic_ZeroesInsteadOfReflecting() { var b = Airborne(new Vector3(3f, 0f, 0f)); b.State |= PhysicsStateFlags.Inelastic; // spell projectile / missile var n = new Vector3(-1f, 0f, 0f); PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: true, collisionNormal: n, prevContact: false, prevOnWalkable: false, nowOnWalkable: false); Assert.Equal(Vector3.Zero, b.Velocity); } [Fact] public void LandingReflects_RetailRuleRestored_NotSuppressed() { // prev airborne → now grounded (a landing). Retail reflects here too (AD-25 retired); // at elasticity 0.05 the effect is a tiny, imperceptible deflection. var b = Airborne(new Vector3(0f, 0f, -5f)); // falling var n = new Vector3(0f, 0f, 1f); // floor normal up PhysicsObjUpdate.HandleAllCollisions(b, collisionNormalValid: true, collisionNormal: n, prevContact: false, prevOnWalkable: false, nowOnWalkable: true); // dot = -5 < 0 → k = -(-5*1.05) = 5.25 → v.z = -5 + 5.25 = 0.25 (tiny bounce) Assert.Equal(0.25f, b.Velocity.Z, precision: 3); } }