diff --git a/src/AcDream.Core/World/Cells/LandCell.cs b/src/AcDream.Core/World/Cells/LandCell.cs
new file mode 100644
index 0000000..f6d4f55
--- /dev/null
+++ b/src/AcDream.Core/World/Cells/LandCell.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Numerics;
+using AcDream.Core.Physics; // TerrainSurface
+
+namespace AcDream.Core.World.Cells;
+
+///
+/// Outdoor terrain cell — synthesized on demand from a landblock's
+/// (retail CLandCell is positionally resolved, not stored).
+/// Retail anchor: CLandCell (acclient.h:31886) / CSortCell (acclient.h:31880).
+///
+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; }
+ /// CSortCell building bridge ref (population logic is Stage 2). Always null in Stage 1.
+ 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(), Array.Empty(), 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;
+ }
+}
diff --git a/tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs b/tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs
new file mode 100644
index 0000000..df245bb
--- /dev/null
+++ b/tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs
@@ -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)));
+ }
+}