using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; public class TerrainSurfaceTests { // A height table where index N maps to N * 1.0f (linear). // Makes test assertions predictable: height byte 10 → Z = 10.0. private static float[] LinearHeightTable() { var table = new float[256]; for (int i = 0; i < 256; i++) table[i] = i * 1.0f; return table; } // A flat heightmap where every vertex is height byte 50. private static byte[] FlatHeightmap(byte value = 50) { var heights = new byte[81]; Array.Fill(heights, value); return heights; } [Fact] public void SampleZ_FlatTerrain_ReturnsSameValueEverywhere() { var surface = new TerrainSurface(FlatHeightmap(50), LinearHeightTable()); Assert.Equal(50f, surface.SampleZ(0f, 0f)); Assert.Equal(50f, surface.SampleZ(96f, 96f)); Assert.Equal(50f, surface.SampleZ(191f, 191f)); } [Fact] public void SampleZ_SlopeAlongX_InterpolatesLinearly() { // Heights increase along X: column 0 = byte 10, column 8 = byte 90. // Each column step is (90-10)/8 = 10 bytes. var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)(10 + x * 10); var surface = new TerrainSurface(heights, LinearHeightTable()); // At x=0 (vertex 0): Z = 10 Assert.Equal(10f, surface.SampleZ(0f, 96f), precision: 1); // At x=96 (midpoint, vertex 4): Z = 50 Assert.Equal(50f, surface.SampleZ(96f, 96f), precision: 1); // At x=192 (vertex 8): Z = 90 Assert.Equal(90f, surface.SampleZ(192f, 96f), precision: 1); // At x=48 (between vertex 2 and 3): Z = 30 + 0.5 * 10 = 35 // vertex 2 = byte 30, vertex 3 = byte 40, midpoint = 35 Assert.Equal(35f, surface.SampleZ(60f, 96f), precision: 1); } [Fact] public void SampleZ_ClampsOutOfBounds() { var surface = new TerrainSurface(FlatHeightmap(42), LinearHeightTable()); // Negative coordinates clamp to 0 Assert.Equal(42f, surface.SampleZ(-10f, -10f)); // Beyond 192 clamps to boundary Assert.Equal(42f, surface.SampleZ(300f, 300f)); } [Fact] public void SampleZFromHeightmap_AgreesWithInstance_AcrossWholeLandblock() { // Issue #48 conformance: the static SampleZFromHeightmap (bilinear // fallback used at scenery hydration before physics registers a // landblock) must produce the same Z as the instance SampleZ // (player physics path) at every (x, y). The previous fallback in // GameWindow had its diagonal arms swapped — this test pins both // paths to one source of truth. // // Heightmap with distinct per-(x,y) values so every triangle plane // is genuinely different from the others; flat / planar heightmaps // would mask a triangle-pick bug because all four corners would // give the same interpolated Z. var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)((x * 17 + y * 13) % 256); var hTable = LinearHeightTable(); // Pick a landblock where IsSplitSWtoNE(...) returns BOTH true and // false across the 64 cells — Holtburg coords (0xA9, 0xB3) work. const uint lbX = 0xA9, lbY = 0xB3; var instance = new TerrainSurface(heights, hTable, lbX, lbY); // Sample on a fine grid (~1500 points) covering all 64 cells and // crossing every cell's diagonal boundary. A triangle-pick bug // would show up as a >0.5 m Z mismatch on the diagonal-spanning // cells (the corner heights vary by ~10 bytes = 10 Z each cell). for (float lx = 0.5f; lx < 192f; lx += 5f) for (float ly = 0.5f; ly < 192f; ly += 5f) { float instanceZ = instance.SampleZ(lx, ly); float staticZ = TerrainSurface.SampleZFromHeightmap( heights, hTable, lbX, lbY, lx, ly); Assert.True( Math.Abs(instanceZ - staticZ) < 0.0001f, $"Z mismatch at ({lx:F1},{ly:F1}) lb=(0x{lbX:X},0x{lbY:X}): instance={instanceZ:F4} static={staticZ:F4}"); } } [Fact] public void SampleZFromHeightmap_RejectsBadInputs() { var goodHeights = new byte[81]; var goodTable = LinearHeightTable(); Assert.Throws(() => TerrainSurface.SampleZFromHeightmap(null!, goodTable, 0, 0, 0f, 0f)); Assert.Throws(() => TerrainSurface.SampleZFromHeightmap(goodHeights, null!, 0, 0, 0f, 0f)); Assert.Throws(() => TerrainSurface.SampleZFromHeightmap(new byte[80], goodTable, 0, 0, 0f, 0f)); Assert.Throws(() => TerrainSurface.SampleZFromHeightmap(goodHeights, new float[255], 0, 0, 0f, 0f)); } [Fact] public void SampleSurfacePolygon_ReturnsContainingTriangleVertices() { var heights = FlatHeightmap(50); var surface = new TerrainSurface(heights, LinearHeightTable(), landblockX: 0, landblockY: 0); var sample = surface.SampleSurfacePolygon(2f, 2f); Assert.Equal(3, sample.Vertices.Length); Assert.All(sample.Vertices, v => Assert.Equal(50f, v.Z)); Assert.Equal(1f, sample.Normal.Z, precision: 3); Assert.Contains(sample.Vertices, v => v.X == 0f && v.Y == 0f); } [Fact] public void SampleNormalZFromHeightmap_FlatTerrain_ReturnsOne() { var heights = FlatHeightmap(50); var hTable = LinearHeightTable(); float nz = TerrainSurface.SampleNormalZFromHeightmap(heights, hTable, 0, 0, 96f, 96f); Assert.Equal(1f, nz, precision: 5); } [Fact] public void SampleNormalZFromHeightmap_SlopedTerrain_ReturnsLessThanOne() { var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)(x * 20); var hTable = LinearHeightTable(); float nz = TerrainSurface.SampleNormalZFromHeightmap(heights, hTable, 0, 0, 12f, 12f); Assert.True(nz < 1f, $"Sloped terrain should have nz < 1.0, got {nz}"); Assert.True(nz > 0f, $"nz should be positive, got {nz}"); } [Fact] public void SampleNormalZFromHeightmap_AgreesWithSampleSurface() { var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)((x * 17 + y * 13) % 256); var hTable = LinearHeightTable(); const uint lbX = 0xA9, lbY = 0xB3; var instance = new TerrainSurface(heights, hTable, lbX, lbY); for (float lx = 0.5f; lx < 192f; lx += 8f) for (float ly = 0.5f; ly < 192f; ly += 8f) { var (_, normal) = instance.SampleSurface(lx, ly); float staticNz = TerrainSurface.SampleNormalZFromHeightmap( heights, hTable, lbX, lbY, lx, ly); Assert.True( Math.Abs(normal.Z - staticNz) < 0.0001f, $"NormalZ mismatch at ({lx:F1},{ly:F1}): instance={normal.Z:F4} static={staticNz:F4}"); } } [Fact] public void ComputeOutdoorCellId_Origin_ReturnsFirst() { var surface = new TerrainSurface(FlatHeightmap(), LinearHeightTable()); // Cell (0,0) at position (0,0) → cell ID 0x0001 Assert.Equal(0x0001u, surface.ComputeOutdoorCellId(0f, 0f)); } [Fact] public void ComputeOutdoorCellId_SecondColumn_ReturnsCorrect() { var surface = new TerrainSurface(FlatHeightmap(), LinearHeightTable()); // 24 units in X = cell (1, 0) → cell ID 0x0001 + 1*8 = 0x0009 Assert.Equal(0x0009u, surface.ComputeOutdoorCellId(24f, 0f)); } [Fact] public void ComputeOutdoorCellId_LastCell_Returns0x0040() { var surface = new TerrainSurface(FlatHeightmap(), LinearHeightTable()); // Cell (7,7) at position (191,191) → 0x0001 + 7*8 + 7 = 0x0040 Assert.Equal(0x0040u, surface.ComputeOutdoorCellId(191f, 191f)); } }