diff --git a/src/AcDream.Core/World/WbSceneryAdapter.cs b/src/AcDream.Core/World/WbSceneryAdapter.cs
new file mode 100644
index 0000000..ca73809
--- /dev/null
+++ b/src/AcDream.Core/World/WbSceneryAdapter.cs
@@ -0,0 +1,45 @@
+using DatReaderWriter.DBObjs;
+using WorldBuilder.Shared.Models;
+
+namespace AcDream.Core.World;
+
+///
+/// Bridges acdream's dat types into WorldBuilder's data shapes for the
+/// Phase N rendering migration. See
+/// docs/architecture/worldbuilder-inventory.md for the full strategy.
+///
+internal static class WbSceneryAdapter
+{
+ private const int VerticesPerSide = 9;
+ private const int TerrainSize = VerticesPerSide * VerticesPerSide; // 81
+
+ ///
+ /// Builds a 9×9 = 81-entry array from a
+ /// 's packed terrain bits + height bytes. WB's
+ /// TerrainUtils.OnRoad / GetNormal / GetHeight
+ /// consume this shape.
+ ///
+ /// Field mapping (TerrainInfo → ):
+ /// TerrainInfo.Road (bits 0-1) →
+ /// TerrainInfo.Type (bits 2-6) →
+ /// TerrainInfo.Scenery (bits 11-15) →
+ /// LandBlock.Height[i] →
+ ///
+ public static TerrainEntry[] BuildTerrainEntries(LandBlock block)
+ {
+ ArgumentNullException.ThrowIfNull(block);
+
+ var entries = new TerrainEntry[TerrainSize];
+ for (int i = 0; i < TerrainSize; i++)
+ {
+ var ti = block.Terrain[i];
+ entries[i] = new TerrainEntry(
+ height: block.Height[i],
+ texture: (byte)ti.Type,
+ scenery: ti.Scenery,
+ road: ti.Road,
+ encounters: null);
+ }
+ return entries;
+ }
+}
diff --git a/tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs b/tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs
new file mode 100644
index 0000000..79fd358
--- /dev/null
+++ b/tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs
@@ -0,0 +1,74 @@
+using AcDream.Core.World;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Types;
+
+namespace AcDream.Core.Tests.World;
+
+///
+/// Tests for . The adapter converts our
+/// LandBlock dat type (Terrain TerrainInfo[81] + Height byte[81]) into
+/// WorldBuilder's [81]
+/// shape, which WB's TerrainUtils / SceneryRenderManager consume.
+///
+/// Bit layout in LandBlock.Terrain[i] (TerrainInfo / ushort):
+/// bits 0-1 : Road (2 bits) → WB TerrainEntry.Road
+/// bits 2-6 : TerrainType (5 bits) → WB TerrainEntry.Type
+/// bits 11-15 : SceneType (5 bits) → WB TerrainEntry.Scenery
+/// Height comes from LandBlock.Height[i] (byte) → WB TerrainEntry.Height.
+///
+public class WbSceneryAdapterTests
+{
+ [Fact]
+ public void BuildTerrainEntries_PreservesRoadTextureSceneryHeight()
+ {
+ var block = new LandBlock();
+
+ // Vertex 0: road=0x3, type=0x00, scenery=0x1F, height=42
+ // raw layout: bits 0-1=11, bits 2-6=00000, bits 7-10=0000, bits 11-15=11111
+ // = 0xF803
+ block.Terrain[0] = (TerrainInfo)0xF803;
+ block.Height[0] = 42;
+
+ // Vertex 80: road=0x0, type=0x1F, scenery=0x00, height=200
+ // raw layout: bits 0-1=00, bits 2-6=11111, bits 11-15=00000
+ // = 0x007C
+ block.Terrain[80] = (TerrainInfo)0x007C;
+ block.Height[80] = 200;
+
+ var entries = WbSceneryAdapter.BuildTerrainEntries(block);
+
+ Assert.Equal(81, entries.Length);
+
+ Assert.Equal((byte)42, entries[0].Height);
+ Assert.Equal((byte)0x3, entries[0].Road);
+ Assert.Equal((byte)0x00, entries[0].Type);
+ Assert.Equal((byte)0x1F, entries[0].Scenery);
+
+ Assert.Equal((byte)200, entries[80].Height);
+ Assert.Equal((byte)0x0, entries[80].Road);
+ Assert.Equal((byte)0x1F, entries[80].Type);
+ Assert.Equal((byte)0x00, entries[80].Scenery);
+ }
+
+ [Fact]
+ public void BuildTerrainEntries_AllZeros_ProducesEmptyEntries()
+ {
+ var block = new LandBlock();
+ // Terrain and Height are already zero-initialized by LandBlock constructor.
+ var entries = WbSceneryAdapter.BuildTerrainEntries(block);
+ Assert.All(entries, e =>
+ {
+ Assert.Equal((byte)0, e.Height);
+ Assert.Equal((byte)0, e.Road);
+ Assert.Equal((byte)0, e.Type);
+ Assert.Equal((byte)0, e.Scenery);
+ });
+ }
+
+ [Fact]
+ public void BuildTerrainEntries_NullBlock_Throws()
+ {
+ Assert.Throws(() =>
+ WbSceneryAdapter.BuildTerrainEntries(null!));
+ }
+}