using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Pins retail CTransition::step_down (0x0050B2A0): every /// successfully supported candidate is re-tested with /// , regardless of whether the caller is /// ordinary contact maintenance or StepUp. /// public sealed class RetailStepDownPlacementTests { private const uint Cell = 0xA9B40001u; private const float Radius = 0.48f; [Theory] [InlineData(false)] [InlineData(true)] public void OrdinaryContactMaintenance_AlwaysRunsFinalPlacement(bool twoSpheres) { Transition transition = MakeGroundedTransition(twoSpheres); int supportPasses = 0; int placementPasses = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase != TransitionCellCollisionPhase.Environment) return actual; if (candidate.SpherePath.StepDown) { supportPasses++; candidate.CollisionInfo.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); } else if (candidate.SpherePath.InsertType == InsertType.Placement) { placementPasses++; } return actual; }, }; TransitionState result = transition.TransitionalInsertForTest(1, engine); Assert.Equal(TransitionState.OK, result); Assert.Equal(1, supportPasses); Assert.Equal(1, placementPasses); Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType); Assert.False(transition.SpherePath.StepDown); } [Fact] public void SupportedCandidate_OverlappingDuringPlacement_IsRejected() { Transition transition = MakeGroundedTransition(twoSpheres: true); int placementPasses = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase != TransitionCellCollisionPhase.Environment) return actual; if (candidate.SpherePath.StepDown) { candidate.CollisionInfo.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); return actual; } if (candidate.SpherePath.InsertType == InsertType.Placement) { placementPasses++; return TransitionState.Collided; } return actual; }, }; bool accepted = transition.DoStepDownForTest( stepDownHeight: 0.04f, walkableZ: PhysicsGlobals.FloorZ, engine); Assert.False(accepted); Assert.Equal(1, placementPasses); Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType); Assert.False(transition.SpherePath.StepDown); } [Fact] public void StepUp_UsesTheSameFinalPlacementPass() { Transition transition = MakeGroundedTransition(twoSpheres: true); int placementPasses = 0; var engine = new PhysicsEngine { TransitionCellCollisionTestHook = (candidate, phase, _, actual) => { if (phase != TransitionCellCollisionPhase.Environment) return actual; if (candidate.SpherePath.StepDown) { candidate.CollisionInfo.SetContactPlane( new Plane(Vector3.UnitZ, 0f), Cell); } else if (candidate.SpherePath.InsertType == InsertType.Placement) { placementPasses++; } return actual; }, }; bool accepted = transition.DoStepUp(Vector3.UnitX, engine); Assert.True(accepted); Assert.Equal(1, placementPasses); Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType); Assert.False(transition.SpherePath.StepUp); Assert.False(transition.SpherePath.StepDown); } [Theory] [InlineData(false)] [InlineData(true)] public void PlacementDispatcher_AllowsExactWallTangency_AndRejectsOverlap( bool twoSpheres) { (PhysicsBSPNode root, Dictionary resolved) = BuildWall(); FlatPhysicsBsp flat = FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved); float tangentY = Radius; TransitionState graphTangent = RunPlacement( root, resolved, flat: null, tangentY, twoSpheres); TransitionState flatTangent = RunPlacement( root: null, resolved, flat, tangentY, twoSpheres); TransitionState graphOverlap = RunPlacement( root, resolved, flat: null, Radius - PhysicsGlobals.EPSILON * 2f, twoSpheres); TransitionState flatOverlap = RunPlacement( root: null, resolved, flat, Radius - PhysicsGlobals.EPSILON * 2f, twoSpheres); Assert.Equal(TransitionState.OK, graphTangent); Assert.Equal(graphTangent, flatTangent); Assert.Equal(TransitionState.Collided, graphOverlap); Assert.Equal(graphOverlap, flatOverlap); } private static TransitionState RunPlacement( PhysicsBSPNode? root, Dictionary resolved, FlatPhysicsBsp? flat, float centerY, bool twoSpheres) { var foot = new Sphere { Origin = new Vector3(0f, centerY, 0f), Radius = Radius, }; Sphere? head = twoSpheres ? new Sphere { Origin = new Vector3(0f, centerY, 0.875f), Radius = Radius, } : null; var transition = new Transition(); transition.SpherePath.InitPath( begin: Vector3.Zero, end: Vector3.Zero, Cell, Radius, sphereHeight: twoSpheres ? 1.355f : 0f); transition.SpherePath.InsertType = InsertType.Placement; return flat is null ? BSPQuery.FindCollisions( root, resolved, transition, foot, head, foot.Origin, Vector3.UnitZ, 1f) : FlatBspQuery.FindCollisions( flat, transition, foot, head, foot.Origin, Vector3.UnitZ, 1f); } private static Transition MakeGroundedTransition(bool twoSpheres) { Vector3 current = new(2f, 3f, 4f); Vector3 target = current + new Vector3(0.1f, 0f, 0f); var transition = new Transition(); transition.SpherePath.InitPath( current, target, Cell, Radius, sphereHeight: twoSpheres ? 1.835f : 0f); transition.SpherePath.SetCheckPos(target, Cell); transition.ObjectInfo.State = ObjectInfoState.Contact | ObjectInfoState.OnWalkable; transition.ObjectInfo.StepDown = true; transition.ObjectInfo.StepDownHeight = 0.04f; transition.ObjectInfo.StepUpHeight = 0.60f; transition.CollisionInfo.LastKnownContactPlane = new Plane(Vector3.UnitZ, 0f); transition.CollisionInfo.LastKnownContactPlaneValid = true; return transition; } private static ( PhysicsBSPNode Root, Dictionary Resolved) BuildWall() { Vector3[] vertices = [ new(-2f, 0f, -2f), new(-2f, 0f, 2f), new( 2f, 0f, 2f), new( 2f, 0f, -2f), ]; var root = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = Vector3.Zero, Radius = 4f, }, }; root.Polygons.Add(1); var resolved = new Dictionary { [1] = new ResolvedPolygon { Id = 1, Vertices = vertices, Plane = new Plane(Vector3.UnitY, 0f), NumPoints = vertices.Length, SidesType = CullMode.None, }, }; return (root, resolved); } }