using System.Collections.Generic; using System.Numerics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Unit tests for . /// /// Indoor walking Phase 2 follow-up (2026-05-19): the helper synthesizes /// a walkable contact plane from cell floor polys so the resolver does not /// fall through to outdoor terrain when the player is standing indoors. /// /// Task 3 (2026-05-19): refactored to route through BSPQuery.FindWalkableSphere. /// Fixtures now include a PhysicsBSPTree with a Leaf node listing all polygon ids, /// and calls pass sphereRadius explicitly. PointInPolygonXY tests removed since /// that helper was deleted (it was the dead linear-scan body). /// public class IndoorWalkablePlaneTests { // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- /// /// Build a BSP Leaf node that lists the given polygon ids, with a bounding /// sphere large enough to always contain the test geometry. /// private static PhysicsBSPTree BuildLeafBsp(IEnumerable polyIds, Vector3 center, float radius) { var node = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = center, Radius = radius }, }; foreach (var id in polyIds) node.Polygons.Add(id); return new PhysicsBSPTree { Root = node }; } /// /// Builds a CellPhysics with a single upward-facing floor polygon /// (a 10×10 square in the XY plane at local Z=0), plus identity transforms /// and a BSP leaf that covers all polygons. /// private static CellPhysics BuildCellWithFloor(float floorZ = 0f) { var verts = new[] { new Vector3(-5f, -5f, floorZ), new Vector3( 5f, -5f, floorZ), new Vector3( 5f, 5f, floorZ), new Vector3(-5f, 5f, floorZ), }; var normal = new Vector3(0f, 0f, 1f); // straight up float D = -Vector3.Dot(normal, verts[0]); // = -floorZ var floorPoly = new ResolvedPolygon { Vertices = verts, Plane = new Plane(normal, D), NumPoints = 4, SidesType = CullMode.None, }; var resolved = new Dictionary { [0] = floorPoly }; var bsp = BuildLeafBsp(new ushort[] { 0 }, new Vector3(0f, 0f, floorZ), 10f); return new CellPhysics { BSP = bsp, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = resolved, }; } // ----------------------------------------------------------------------- // TryFindIndoorWalkablePlane // ----------------------------------------------------------------------- [Fact] public void TryFindIndoorWalkablePlane_PlayerDirectlyOverFloor_ReturnsTrue() { var cell = BuildCellWithFloor(floorZ: 0f); var transition = new Transition(); // Foot sphere centre at Z=0.4, radius=0.48 → overlaps floor at Z=0. var localFoot = new Vector3(0f, 0f, 0.4f); bool found = transition.TryFindIndoorWalkablePlane( cell, localFoot, sphereRadius: 0.48f, out _, out _, out _); Assert.True(found); } [Fact] public void TryFindIndoorWalkablePlane_PlayerDirectlyOverFloor_PlaneNormalIsUp() { var cell = BuildCellWithFloor(floorZ: 0f); var transition = new Transition(); var localFoot = new Vector3(0f, 0f, 0.4f); transition.TryFindIndoorWalkablePlane( cell, localFoot, sphereRadius: 0.48f, out var plane, out _, out _); // The floor's normal must point up (Z close to 1). Assert.True(plane.Normal.Z > 0.99f, $"Expected plane.Normal.Z > 0.99, got {plane.Normal.Z}"); } [Fact] public void TryFindIndoorWalkablePlane_PlayerDirectlyOverFloor_PlaneAtFloorZ() { const float floorZ = 2.5f; var cell = BuildCellWithFloor(floorZ); var transition = new Transition(); // Foot sphere overlaps floor: centre at floorZ + 0.4, radius=0.48 → dist=0.4 < 0.48. var localFoot = new Vector3(0f, 0f, floorZ + 0.4f); transition.TryFindIndoorWalkablePlane( cell, localFoot, sphereRadius: 0.48f, out var plane, out _, out _); // With identity transform and an upward normal, plane.D = -floorZ. // The plane equation: normal·p + D = 0 → p.Z = floorZ when normal=(0,0,1). Assert.True(MathF.Abs(plane.D - (-floorZ)) < 1e-4f, $"Expected plane.D ≈ {-floorZ}, got {plane.D}"); } [Fact] public void TryFindIndoorWalkablePlane_PlayerOutsidePolygonXY_ReturnsFalse() { var cell = BuildCellWithFloor(); var transition = new Transition(); // XY = (20, 20) is far outside the 10×10 square (-5..5 in both axes). var localFoot = new Vector3(20f, 20f, 0.4f); bool found = transition.TryFindIndoorWalkablePlane( cell, localFoot, sphereRadius: 0.48f, out _, out _, out _); Assert.False(found); } [Fact] public void TryFindIndoorWalkablePlane_NoBsp_ReturnsFalse() { // CellPhysics without a BSP → BSP?.Root is null → early return false. var cell = new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), }; var transition = new Transition(); bool found = transition.TryFindIndoorWalkablePlane( cell, new Vector3(0f, 0f, 0.4f), sphereRadius: 0.48f, out _, out _, out _); Assert.False(found); } [Fact] public void TryFindIndoorWalkablePlane_EmptyResolved_ReturnsFalse() { // BSP leaf exists but references no polygons → FindWalkableSphere returns false. var bsp = BuildLeafBsp(System.Array.Empty(), Vector3.Zero, 10f); var cell = new CellPhysics { BSP = bsp, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), }; var transition = new Transition(); bool found = transition.TryFindIndoorWalkablePlane( cell, new Vector3(0f, 0f, 0.4f), sphereRadius: 0.48f, out _, out _, out _); Assert.False(found); } [Fact] public void TryFindIndoorWalkablePlane_WithWorldTranslation_PlaneInWorldSpace() { // Cell is translated 100 units in X and 200 units in Y. var translation = Matrix4x4.CreateTranslation(100f, 200f, 94f); Matrix4x4.Invert(translation, out var inv); var localVerts = new[] { new Vector3(-5f, -5f, 0f), new Vector3( 5f, -5f, 0f), new Vector3( 5f, 5f, 0f), new Vector3(-5f, 5f, 0f), }; var floorPoly = new ResolvedPolygon { Vertices = localVerts, Plane = new Plane(new Vector3(0f, 0f, 1f), 0f), NumPoints = 4, SidesType = CullMode.None, }; var resolved = new Dictionary { [0] = floorPoly }; var bsp = BuildLeafBsp(new ushort[] { 0 }, Vector3.Zero, 10f); var cell = new CellPhysics { BSP = bsp, WorldTransform = translation, InverseWorldTransform = inv, Resolved = resolved, }; // The player's local foot sphere centre at (0,0,0.4) overlaps the floor at Z=0. var localFoot = new Vector3(0f, 0f, 0.4f); var transition = new Transition(); bool found = transition.TryFindIndoorWalkablePlane( cell, localFoot, sphereRadius: 0.48f, out var plane, out var worldVerts, out _); Assert.True(found); // World normal should still be (0,0,1). Assert.True(plane.Normal.Z > 0.99f); // World vertex[0] should be at local (-5,-5,0) + translation = (95, 195, 94). Assert.True(MathF.Abs(worldVerts[0].X - 95f) < 1e-3f); Assert.True(MathF.Abs(worldVerts[0].Y - 195f) < 1e-3f); Assert.True(MathF.Abs(worldVerts[0].Z - 94f) < 1e-3f, $"Expected worldVerts[0].Z ≈ 94, got {worldVerts[0].Z}"); } }