feat(core): UCG Stage 1 — LandCell synthesized from TerrainSurface
Outdoor terrain cell (retail CLandCell) synthesized on demand from a landblock's TerrainSurface. Factory Synthesize() samples four quad corners to establish Z bounds; PointInCell() tests the 24 m XY quad in world-local space. BuildingCellId stub is null (Stage 2). 2/2 tests RED→GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
03f08f00c1
commit
b4c4318c8b
2 changed files with 91 additions and 0 deletions
55
src/AcDream.Core/World/Cells/LandCell.cs
Normal file
55
src/AcDream.Core/World/Cells/LandCell.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics; // TerrainSurface
|
||||
|
||||
namespace AcDream.Core.World.Cells;
|
||||
|
||||
/// <summary>
|
||||
/// Outdoor terrain cell — synthesized on demand from a landblock's
|
||||
/// <see cref="TerrainSurface"/> (retail CLandCell is positionally resolved, not stored).
|
||||
/// Retail anchor: CLandCell (acclient.h:31886) / CSortCell (acclient.h:31880).
|
||||
/// </summary>
|
||||
public sealed class LandCell : ObjCell
|
||||
{
|
||||
public const float CellSize = 24f; // TerrainSurface 24 m cell
|
||||
|
||||
public TerrainSurface Terrain { get; }
|
||||
public int Cx { get; }
|
||||
public int Cy { get; }
|
||||
/// <summary>CSortCell building bridge ref (population logic is Stage 2). Always null in Stage 1.</summary>
|
||||
public uint? BuildingCellId { get; }
|
||||
|
||||
private readonly Vector3 _worldOrigin;
|
||||
|
||||
private LandCell(uint id, TerrainSurface terrain, Vector3 worldOrigin, int cx, int cy,
|
||||
Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform,
|
||||
Vector3 localBoundsMin, Vector3 localBoundsMax)
|
||||
: base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax,
|
||||
Array.Empty<CellPortal>(), Array.Empty<uint>(), seenOutside: false)
|
||||
{
|
||||
Terrain = terrain; Cx = cx; Cy = cy; _worldOrigin = worldOrigin; BuildingCellId = null;
|
||||
}
|
||||
|
||||
public static LandCell Synthesize(uint id, TerrainSurface terrain, Vector3 worldOrigin, int cx, int cy)
|
||||
{
|
||||
float ox = cx * CellSize, oy = cy * CellSize;
|
||||
float z0 = terrain.SampleZ(ox, oy), z1 = terrain.SampleZ(ox + CellSize, oy);
|
||||
float z2 = terrain.SampleZ(ox, oy + CellSize), z3 = terrain.SampleZ(ox + CellSize, oy + CellSize);
|
||||
float zMin = MathF.Min(MathF.Min(z0, z1), MathF.Min(z2, z3));
|
||||
float zMax = MathF.Max(MathF.Max(z0, z1), MathF.Max(z2, z3));
|
||||
var min = new Vector3(ox, oy, zMin);
|
||||
var max = new Vector3(ox + CellSize, oy + CellSize, zMax);
|
||||
|
||||
var transform = Matrix4x4.CreateTranslation(worldOrigin);
|
||||
Matrix4x4.Invert(transform, out var inverse);
|
||||
return new LandCell(id, terrain, worldOrigin, cx, cy, transform, inverse, min, max);
|
||||
}
|
||||
|
||||
public override bool PointInCell(Vector3 worldPoint)
|
||||
{
|
||||
float lx = worldPoint.X - _worldOrigin.X;
|
||||
float ly = worldPoint.Y - _worldOrigin.Y;
|
||||
return lx >= Cx * CellSize && lx < (Cx + 1) * CellSize
|
||||
&& ly >= Cy * CellSize && ly < (Cy + 1) * CellSize;
|
||||
}
|
||||
}
|
||||
36
tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs
Normal file
36
tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World.Cells;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.World.Cells;
|
||||
|
||||
public class LandCellTests
|
||||
{
|
||||
private static TerrainSurface FlatTerrain()
|
||||
=> new TerrainSurface(new byte[81], new float[256], landblockX: 0, landblockY: 0);
|
||||
|
||||
[Fact]
|
||||
public void Synthesize_SetsCellIndicesAndQuadBounds()
|
||||
{
|
||||
var origin = new Vector3(1000f, 2000f, 0f);
|
||||
var cell = LandCell.Synthesize(0xA9B40014u, FlatTerrain(), origin, cx: 2, cy: 3);
|
||||
Assert.Equal(2, cell.Cx);
|
||||
Assert.Equal(3, cell.Cy);
|
||||
Assert.Equal(2 * 24f, cell.LocalBoundsMin.X);
|
||||
Assert.Equal(3 * 24f, cell.LocalBoundsMin.Y);
|
||||
Assert.Equal(3 * 24f, cell.LocalBoundsMax.X);
|
||||
Assert.Equal(4 * 24f, cell.LocalBoundsMax.Y);
|
||||
Assert.False(cell.IsEnv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointInCell_TestsWorldXyAgainstThe24mQuad()
|
||||
{
|
||||
var origin = new Vector3(1000f, 2000f, 0f);
|
||||
var cell = LandCell.Synthesize(0xA9B40014u, FlatTerrain(), origin, cx: 2, cy: 3);
|
||||
Assert.True(cell.PointInCell(new Vector3(1060f, 2080f, 12.3f)));
|
||||
Assert.False(cell.PointInCell(new Vector3(1000f, 2080f, 0f)));
|
||||
Assert.False(cell.PointInCell(new Vector3(1060f, 2100f, 0f)));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue