using System.Collections.Generic; using System.Numerics; using DatReaderWriter.Enums; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Unit tests for and /// . /// /// Indoor walking Phase 2 follow-up (2026-05-19): these helpers synthesize /// a walkable contact plane from cell floor polys so the resolver does not /// fall through to outdoor terrain when the player is standing indoors. /// public class IndoorWalkablePlaneTests { // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- /// /// 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. /// 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, }; return new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary { [0] = floorPoly }, }; } // ----------------------------------------------------------------------- // TryFindIndoorWalkablePlane // ----------------------------------------------------------------------- [Fact] public void TryFindIndoorWalkablePlane_PlayerDirectlyOverFloor_ReturnsTrue() { var cell = BuildCellWithFloor(floorZ: 0f); var localFoot = new Vector3(0f, 0f, 0.5f); // centred over the 10×10 square bool found = Transition.TryFindIndoorWalkablePlane( cell, localFoot, out var plane, out var verts, out uint polyId); Assert.True(found); } [Fact] public void TryFindIndoorWalkablePlane_PlayerDirectlyOverFloor_PlaneNormalIsUp() { var cell = BuildCellWithFloor(floorZ: 0f); var localFoot = new Vector3(0f, 0f, 0.5f); Transition.TryFindIndoorWalkablePlane( cell, localFoot, 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 localFoot = new Vector3(0f, 0f, floorZ + 0.5f); Transition.TryFindIndoorWalkablePlane( cell, localFoot, 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(); // XY = (20, 20) is far outside the 10×10 square (-5..5 in both axes). var localFoot = new Vector3(20f, 20f, 0.5f); bool found = Transition.TryFindIndoorWalkablePlane( cell, localFoot, out _, out _, out _); Assert.False(found); } [Fact] public void TryFindIndoorWalkablePlane_NoWalkablePolys_ReturnsFalse() { // A polygon whose normal points sideways (wall) — normal.Z < 0.6664. var wallPoly = new ResolvedPolygon { Vertices = new[] { Vector3.Zero, Vector3.UnitY, Vector3.UnitZ }, Plane = new Plane(new Vector3(1f, 0f, 0f), 0f), // normal.Z = 0 NumPoints = 3, SidesType = CullMode.None, }; var cell = new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary { [1] = wallPoly }, }; bool found = Transition.TryFindIndoorWalkablePlane( cell, new Vector3(0f, 0f, 0.5f), out _, out _, out _); Assert.False(found); } [Fact] public void TryFindIndoorWalkablePlane_EmptyResolved_ReturnsFalse() { var cell = new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), }; bool found = Transition.TryFindIndoorWalkablePlane( cell, new Vector3(0f, 0f, 0.5f), 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 cell = new CellPhysics { WorldTransform = translation, InverseWorldTransform = inv, Resolved = new Dictionary { [0] = floorPoly }, }; // The player's local foot is at (0,0,0.5) in local space. var localFoot = new Vector3(0f, 0f, 0.5f); bool found = Transition.TryFindIndoorWalkablePlane( cell, localFoot, 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}"); } // ----------------------------------------------------------------------- // PointInPolygonXY // ----------------------------------------------------------------------- [Theory] [InlineData( 0f, 0f, true)] // centre [InlineData( 4f, 4f, true)] // near corner, inside [InlineData( 5f, 5f, false)] // on the corner — outside by convention [InlineData(10f, 0f, false)] // clearly outside [InlineData(-4f, -4f, true)] // near opposite corner, inside public void PointInPolygonXY_UnitSquare(float px, float py, bool expected) { var square = new[] { new Vector3(-5f, -5f, 0f), new Vector3( 5f, -5f, 0f), new Vector3( 5f, 5f, 0f), new Vector3(-5f, 5f, 0f), }; bool result = Transition.PointInPolygonXY(new Vector3(px, py, 99f), square); Assert.Equal(expected, result); } [Fact] public void PointInPolygonXY_IgnoresZ() { // Same XY, different Z — should still be inside. var square = new[] { new Vector3(-5f, -5f, 0f), new Vector3( 5f, -5f, 0f), new Vector3( 5f, 5f, 0f), new Vector3(-5f, 5f, 0f), }; // Point has the same XY as the inside case but a very different Z. bool atLowZ = Transition.PointInPolygonXY(new Vector3(0f, 0f, -1000f), square); bool atHighZ = Transition.PointInPolygonXY(new Vector3(0f, 0f, 1000f), square); Assert.True(atLowZ); Assert.True(atHighZ); } }