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);
}
}