using System;
namespace AcDream.Core.Physics;
///
/// Outdoor terrain height resolver for a single landblock. Performs
/// per-triangle barycentric Z interpolation matching the visual terrain
/// mesh's triangle split direction (AC2D's FSplitNESW formula).
///
///
/// Each cell (24×24 units) is split into two triangles along either the
/// SW→NE or SE→NW diagonal. The split direction is determined by the
/// same formula the render mesh uses (0x0CCAC033 constants from AC2D),
/// so the Z this class produces matches the visual terrain surface exactly.
///
///
public sealed class TerrainSurface
{
private const int HeightmapSide = 9;
private const float CellSize = 24f;
private const int CellsPerSide = 8; // 192 / 24
private readonly float[,] _z; // pre-resolved heights [x, y]
private readonly uint _landblockX;
private readonly uint _landblockY;
public TerrainSurface(byte[] heights, float[] heightTable,
uint landblockX = 0, uint landblockY = 0)
{
ArgumentNullException.ThrowIfNull(heights);
ArgumentNullException.ThrowIfNull(heightTable);
if (heights.Length < 81)
throw new ArgumentException("heights must have 81 entries", nameof(heights));
if (heightTable.Length < 256)
throw new ArgumentException("heightTable must have 256 entries", nameof(heightTable));
_landblockX = landblockX;
_landblockY = landblockY;
// Pre-resolve all 81 heights so SampleZ is a pure lookup + lerp.
_z = new float[HeightmapSide, HeightmapSide];
for (int x = 0; x < HeightmapSide; x++)
for (int y = 0; y < HeightmapSide; y++)
_z[x, y] = heightTable[heights[x * HeightmapSide + y]];
}
///
/// Triangle-aware terrain Z at (localX, localY) in landblock-local
/// coordinates (0..192 range). Uses the AC2D FSplitNESW formula to
/// determine which triangle the point falls in, then does barycentric
/// interpolation within that triangle. This matches the visual terrain
/// mesh exactly.
///
/// Triangle layout (from LandblockMesh.cs index buffer):
/// SWtoNE: tri1 = {BL,TL,BR}, tri2 = {BR,TL,TR} — shared edge TL→BR (x+y=1 boundary)
/// SEtoNW: tri1 = {BL,TR,BR}, tri2 = {BL,TL,TR} — shared edge BL→TR (y=x boundary)
///
/// NOTE: The SWtoNE "cut" exposes the SW(BL) and NE(TR) corners as isolated
/// vertices — the hypotenuse runs NW(TL)→SE(BR), so the dividing test is
/// x+y=1 (not y=x). Confusing naming aside, the formula below matches
/// TerrainGeometryGenerator.GetHeight (ACME WorldBuilder-ACME-Edition) which
/// was verified against the mesh index buffer.
///
public float SampleZ(float localX, float localY)
{
// Which cell?
float fx = Math.Clamp(localX / CellSize, 0f, CellsPerSide - 0.001f);
float fy = Math.Clamp(localY / CellSize, 0f, CellsPerSide - 0.001f);
int cx = (int)fx;
int cy = (int)fy;
cx = Math.Clamp(cx, 0, CellsPerSide - 1);
cy = Math.Clamp(cy, 0, CellsPerSide - 1);
// Fractional position within the cell [0, 1]
float tx = fx - cx;
float ty = fy - cy;
// Four corner heights (BL, BR, TR, TL)
float hBL = _z[cx, cy ];
float hBR = _z[cx + 1, cy ];
float hTR = _z[cx + 1, cy + 1];
float hTL = _z[cx, cy + 1];
// Split direction using the AC2D render formula
bool splitSWtoNE = IsSplitSWtoNE(_landblockX, (uint)cx, _landblockY, (uint)cy);
if (splitSWtoNE)
{
// Mesh: {BL,TL,BR} and {BR,TL,TR}. Shared hypotenuse = TL(0,1)→BR(1,0).
// Dividing line: tx + ty = 1.
if (tx + ty <= 1f)
return hBL + (hBR - hBL) * tx + (hTL - hBL) * ty; // BL+BR+TL triangle
else
return hTR + (hTL - hTR) * (1f - tx) + (hBR - hTR) * (1f - ty); // TR+TL+BR triangle
}
else
{
// Mesh: {BL,TR,BR} and {BL,TL,TR}. Shared hypotenuse = BL(0,0)→TR(1,1).
// Dividing line: ty = tx.
if (ty <= tx)
return hBL + (hBR - hBL) * tx + (hTR - hBR) * ty; // BL+BR+TR triangle
else
return hBL + (hTR - hTL) * tx + (hTL - hBL) * ty; // BL+TR+TL triangle
}
}
///
/// Compute the outdoor cell ID for the given landblock-local position.
///
public uint ComputeOutdoorCellId(float localX, float localY)
{
int cx = Math.Clamp((int)(localX / CellSize), 0, CellsPerSide - 1);
int cy = Math.Clamp((int)(localY / CellSize), 0, CellsPerSide - 1);
return (uint)(1 + cx * CellsPerSide + cy);
}
///
/// AC2D's FSplitNESW render formula. Returns true for SW→NE diagonal.
/// Uses global cell coordinates (landblockX*8+cellX, landblockY*8+cellY).
///
private static bool IsSplitSWtoNE(uint landblockX, uint cellX, uint landblockY, uint cellY)
{
uint x = landblockX * 8 + cellX;
uint y = landblockY * 8 + cellY;
uint dw = unchecked(x * y * 0x0CCAC033u - x * 0x421BE3BDu + y * 0x6C1AC587u - 0x519B8F25u);
return (dw & 0x80000000u) != 0;
}
}