diff --git a/tests/AcDream.App.Tests/Rendering/UnifiedFloodTests.cs b/tests/AcDream.App.Tests/Rendering/UnifiedFloodTests.cs new file mode 100644 index 00000000..75323d17 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/UnifiedFloodTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Numerics; +using AcDream.App.Rendering; +using Xunit; + +namespace AcDream.App.Tests.Rendering; + +public class UnifiedFloodTests +{ + // Shared building fixture: a building cell whose interior is at Y >= 5. + // The exit portal faces -Y (Normal=(0,-1,0)); interior is where dot<=0 -> Y>=5 (InsideSide=1). + // The outdoor camera is at Y=-3, which is the OUTDOOR side (Y<5). + // OutdoorCellNode.Build flips InsideSide to 0 so the outdoor camera (dot>=0 i.e. Y<5) passes + // the side test and the flood reaches the building. + private static LoadedCell MakeBuildingCell(uint cellId) + { + var building = new LoadedCell { CellId = cellId, SeenOutside = true }; + building.WorldTransform = Matrix4x4.Identity; + building.InverseWorldTransform = Matrix4x4.Identity; + building.Portals.Add(new CellPortalInfo(0xFFFF, 0, 0, 0)); + // InsideSide=1: interior where (0,-1,0)·p+5 <= 0 -> Y>=5 (the building body is at Y>=5). + building.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, -1, 0), D = 5f, InsideSide = 1 }); + building.PortalPolygons.Add(new[] + { + new Vector3(-1, 5, 0), new Vector3(1, 5, 0), new Vector3(1, 5, 2), new Vector3(-1, 5, 2) + }); + return building; + } + + [Fact] + public void Build_RootedAtOutdoorNode_FloodsIntoBuilding() + { + var building = MakeBuildingCell(0xA9B40170); + var node = OutdoorCellNode.Build(0xA9B40031, new[] { building }); + LoadedCell? Lookup(uint id) => (id & 0xFFFFu) == 0x0170 ? building : null; + + var eye = new Vector3(0, -3, 1); + var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ); + var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f); + + var frame = PortalVisibilityBuilder.Build(node, eye, Lookup, view * proj); + + Assert.Contains(0xA9B40031u, frame.OrderedVisibleCells); // the outdoor node itself + Assert.Contains(0xA9B40170u, frame.OrderedVisibleCells); // flooded into the building + } + + [Fact] + public void Build_OutdoorBuildingCycle_Terminates() + { + // Building's exit portal reciprocally points back near the node; assert Build + // returns (does not hang) and the visible set is bounded/small. + var building = MakeBuildingCell(0xA9B40170); + var node = OutdoorCellNode.Build(0xA9B40031, new[] { building }); + LoadedCell? Lookup(uint id) => (id & 0xFFFFu) == 0x0170 ? building + : (id & 0xFFFFu) == 0x0031 ? node : null; + var eye = new Vector3(0, -3, 1); + var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ); + var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f); + + var frame = PortalVisibilityBuilder.Build(node, eye, Lookup, view * proj); + Assert.True(frame.OrderedVisibleCells.Count < 10); // bounded, no runaway + } +}