DramaticShapeVoxelMod archival mirror · read-only
2af88d07cfa2bcba09ddf3dcc50304cf5ea3bf38

fix victory road statues

DramaticShape · 2026-08-09 · parent 47369c24fe

fix victory road statues

Changed files

CHANGELOG.md                  |  49 ++++++++++++++++++
 data/voxel_heights.lua        |  76 ++++++++++++++++++++++-----
 lib/ChunkMesher.lua           |  21 +++++---
 lib/Structures.lua            |  50 ++++++++++++++++--
 lib/TileShape.lua             |  40 +++++++++++----
 tests/dramatic_shape_test.lua | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 tests/voxel_fix_shots.lua     | 137 +++++++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 501 insertions(+), 44 deletions(-)

Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a0426d..c9a9a0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -167,6 +167,55 @@
   measurement of the geometry can recover. Only the top face is
   redirected; the poster still faces the room.
 
+  Five rooms take it. `HOUSE`, `POKECENTER`, `REDS_HOUSE_1` and
+  `REDS_HOUSE_2` name one course for every wall in the atlas -- each of
+  those dresses one kind of room, and a list keyed by the decorated tiles
+  would need extending every time a map hung something new on the same
+  wall. `LOBBY` names the tiles instead (`{ [40] = 93, [56] = 93 }`): the
+  Rocket lift's car doors cap with the cabin frame, and the department
+  store, the Game Corner, Silph's floors and the roof -- all on that one
+  atlas -- keep exactly the tops they had. The doors are also the reason
+  the cap is applied in the mesher's DETECTED-run branch as well as its
+  pinned one; Structures finds them rather than a pin naming them.
+
+- **Lance's room is furnished with the badge gyms' bird statue.** It is
+  the gyms' statue tile for tile on the DOJO atlas -- one cell of figure
+  ($02/$38/$12/$13) over one cell of plinth ($22/$23/$32/$33) -- and left
+  derived the pair merged into one 32px volume wearing the statue folded
+  onto its face. The extruded picture, the same failure the gyms' statues
+  and the Plateau's avenue had, and it takes the same answer: the plinth a
+  solid 16px block, the bird a per-pixel cutout 5 voxels deep riding its
+  top face. Every placement of those eight tiles in the game is a statue
+  -- 18 in LANCES_ROOM and 2 in FIGHTING_DOJO -- and Oak's Lab, the third
+  map on the atlas, places none of them.
+
+- **A wall cut into a terrace inherits terrace, never the statue standing
+  on it.** `bookcase_backfill = "above"` hands a collapsed rank's vacated
+  rows the cell above the run, so the League's gate walls have more
+  hillside behind them rather than a trench. Indigo Plateau's avenue
+  statues stand directly on the pilasters that collapse that way, so what
+  every one of them inherited was the BIRD: the figure's shape and art
+  copied onto two more rows down the shaft, and each statue came out two
+  deep behind itself. Only bodies backfill now -- flat, top and upright.
+  A per-pixel standee above (a statue, a sign, a bush) is an object
+  standing ON the terrace rather than terrace, so the row has nothing to
+  inherit and takes the default synthesized ground.
+
+- **...and a statue on a collapsed pilaster stands ON it.** The duplicate
+  above was masking a second fault. A standee finds its support by reading
+  the cell below its own drawing, and the bookcase collapse MOVES the box
+  it finds: the whole four-row pilaster walks onto its southmost cell,
+  which on the Plateau is a full cell south of where the test looked. So
+  the bird was lifted to the right HEIGHT and left standing over open
+  ground with its pillar behind it -- invisible while the vacated rows
+  were being filled with copies of the bird itself, obvious the moment
+  they were not. Every row of a collapsed rank now records the row its box
+  actually stands on (`S.bookcaseBox`), and a standee supported by one is
+  placed there instead of at its drawn position. Supports that do not move
+  -- the gyms' plinths, furniture, `building` claims -- are unaffected.
+  The plinth keeps its elevation and the statue extends exactly one cell
+  above it.
+
 ## 1.8.0
 
 ### Added
diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua
index 74b605a..12d9beb 100644
--- a/data/voxel_heights.lua
+++ b/data/voxel_heights.lua
@@ -132,15 +132,23 @@
 -- measured rather than drawn, by flooding the panel tile's own stripe out
 -- from behind them; see CLUB below.
 --
--- And `wall_top` (not a class either): the tile every `wall` cell's TOP
--- face wears, whatever the cell itself draws.  An interior wall band is
--- 16px of art folded upright, and a fully folded run has no drawn row left
--- over to lay flat on top -- so the top repeated the FACE, and the town-map
--- poster and the window of a house came out lying across the top of the
--- wall as well as hanging on it.  What is really up there is the wall's own
+-- And `wall_top` (not a class either): what a `wall` cell's TOP face
+-- wears, whatever the cell itself draws.  An interior wall band is 16px of
+-- art folded upright, and a fully folded run has no drawn row left over to
+-- lay flat on top -- so the top repeated the FACE, and the town-map poster
+-- and the window of a house came out lying across the top of the wall as
+-- well as hanging on it.  What is really up there is the wall's own
 -- capping course, which is exactly the plain panel the decorated column's
--- neighbours draw: name that tile and every wall in the tileset caps with
--- it.  The face is untouched -- the poster still faces the room.
+-- neighbours draw.  The face is untouched -- the poster still faces the
+-- room.  Two forms:
+--
+--   wall_top = <id>             every wall in the tileset caps with this,
+--                               for an atlas that dresses one kind of room
+--                               (the houses, the Centers, Red's floors)
+--   wall_top = { [tile] = id }  only the named tiles are redirected, for
+--                               an atlas that dresses several (LOBBY is
+--                               the department store, the Game Corner,
+--                               Silph, the roof AND the Rocket lift)
 --
 -- Whole BUILDINGS are not tile pins -- one drawing packs a roof seen from
 -- above, a facade seen face-on and sloped ends as diagonal silhouettes,
@@ -771,12 +779,38 @@ return {
       signpost = { 33, 34, 49, 50 },
     },
 
-    -- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's
-    -- room, which use none of these tiles).  The free-standing shelf
-    -- ranks: book rows and base pinned; the shared trim tiles above
-    -- (41/42, also the lab tables' corners) are adopted as caps by the
-    -- bookcase builder rather than pinned.
+    -- Oak's Lab, the Fighting Dojo and Lance's room (one tileset).  The
+    -- lab's free-standing shelf ranks: book rows and base pinned; the
+    -- shared trim tiles above (41/42, also the lab tables' corners) are
+    -- adopted as caps by the bookcase builder rather than pinned.  The
+    -- other two rooms are furnished with one thing between them -- the
+    -- BIRD STATUE, and it is the badge gyms' statue exactly.
     DOJO = {
+      -- THE STATUES.  The same drawing as GYM's, tile for tile on this
+      -- atlas: one cell of figure ($02/$38/$12/$13) over one cell of
+      -- plinth ($22/$23/$32/$33), so it takes the same treatment -- the
+      -- plinth a SOLID 16px block, the figure a per-pixel cutout 5 voxels
+      -- deep (the thin `prop` pool) riding the plinth's top face through
+      -- the authored-box support rule and collapsing to the plinth's
+      -- single cell of footprint.
+      --
+      -- Left derived the pair merged into ONE 32px volume wearing the
+      -- statue art folded onto its face -- the extruded picture, the same
+      -- failure the gyms' statues and the Plateau's avenue had.
+      --
+      -- Every placement of these eight tiles in the game is a statue:
+      -- blocks 49/50/114/115 pack figure over plinth in one 2x2-cell
+      -- block, and they are placed 18 times in LANCES_ROOM (the pairs
+      -- lining his aisle, and the two flanking his dais at cells (6,12)
+      -- and (7,12) over (6,13)/(7,13)) and twice in FIGHTING_DOJO.  Oak's
+      -- Lab, the third map on this atlas, places none of them -- and the
+      -- four figure-only and plinth-only blocks are never placed at all.
+      wall = { 34, 35, 50, 51 },
+      prop = { 2, 18, 19, 56 },
+      -- and each stands on the room's main floor ($11) rather than on
+      -- whatever its neighbours vote -- the gyms' rule, for the gyms'
+      -- reason: a statue against a wall would otherwise take the wall.
+      prop_ground = { [2] = 17, [18] = 17, [19] = 17, [56] = 17 },
       bookcase = { 13, 14, 29, 30 },
       -- the lab tables (the starter-ball display and the north tables):
       -- 41/42 are also the shelf trim the bookcase builder adopts as
@@ -800,6 +834,11 @@ return {
     REDS_HOUSE_2 = {
       -- the wall band with its windows stays one 16px face
       wall = { 0, 36, 37, 52, 53 },
+      -- and caps with the blank course.  The windows of Red's 2F sit at
+      -- cells (5,0) and (7,0); without this the panes came out lying
+      -- across the top of the wall as well as glazing its face, where
+      -- (6,0) between them draws the plain panel that belongs up there
+      wall_top = 0,
       -- the bed: a mattress drawn from above, half a block high
       bed = { 45, 46, 47, 61, 62, 63 },
       -- stools: a seat-high box, seat art on top, legs on the front
@@ -838,6 +877,9 @@ return {
       -- table rides these heights, not the 8/12px class defaults
       heights = { stool = 5, table = 6 },
       wall = { 0, 36, 37, 52, 53 },
+      -- the same blank course caps 1F, whose windows are cells (3,0),
+      -- (5,0) and (7,0) -- (6,0) between the last two is the panel
+      wall_top = 0,
       stool = { 2, 3, 18, 19 },
       -- the dining table (38-44/58-60); its top row also caps the
       -- bookcases below
@@ -1615,6 +1657,14 @@ return {
       wall = { 1, 2, 3, 6, 18, 19, 22, 33, 46, 47,
                62, 63, 68, 70, 71, 72, 73, 75, 76, 77, 78, 79, 84,
                88, 89, 91, 92, 93 },
+      -- The Rocket lift's CAR DOORS (40 over 56, cells (2,1) and (3,1) of
+      -- ROCKET_HIDEOUT_ELEVATOR) cap with the cabin frame's lower course
+      -- -- the tile cell (2,0) draws beneath its own top band, and what
+      -- every other column of that wall already caps with. Keyed by tile
+      -- rather than blanket, unlike the houses: this one atlas dresses the
+      -- department store, the Game Corner, Silph's floors and the roof
+      -- too, and none of those wall tops is a lift frame.
+      wall_top = { [40] = 93, [56] = 93 },
       -- 3F's television sets ($0E/$0F/$1E/$1F): the one drawing in this
       -- tileset that is a deliberate object with a body -- a black-framed
       -- cabinet, a bezel and a lit screen, drawn face-on -- and the same
diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua
index d036467..f19a96d 100644
--- a/lib/ChunkMesher.lua
+++ b/lib/ChunkMesher.lua
@@ -304,10 +304,15 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
   -- The capping course an interior wall wears on its TOP face (see
   -- TileShape.wallTop). A wall band folds entirely onto its own face, so
   -- the top had nothing left to lay flat and repeated the face -- a house's
-  -- town-map poster and window, and a Center's pokeball poster, came out
-  -- lying across the top of the wall as well as hanging on it. Only the top
-  -- is redirected: the face still draws what the map draws.
+  -- town-map poster and window, a Center's pokeball poster and the Rocket
+  -- lift's doors came out lying across the top of the wall as well as
+  -- standing in it. Only the top is redirected: the face still draws what
+  -- the map draws.
   local wallTop = TileShape.wallTop(tileset.id)
+  local function capOf(s, tile)
+    if not (wallTop and s.class == "wall") then return nil end
+    return wallTop(tile)
+  end
 
   -- one atlas-rect UV, optionally cropped to art rows [vTop, vBot] of 8
   local function uvRect(tile, vTop, vBot)
@@ -594,6 +599,11 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
                { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, 0.95)
         elseif run then
           local topTile = map:tileAt(tx, ChunkMesher.flatTopRow(run, ty))
+          -- a DETECTED wall volume caps the same way a pinned one does:
+          -- the Rocket lift's cabin doors are found rather than pinned,
+          -- and their drawing lay across the top of the wall they are set
+          -- into (see wallTop)
+          topTile = capOf(s, tile) or topTile
           topQuad(x0, z0, h, topTile, VOLUME_TOP_SHADE)
         else
           local topTile = tile
@@ -657,10 +667,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
               row = (above and above.authored and above.art == "upright")
                     and (north - 1) or north
             end
-            topTile = S.tileAt[keyOf(tx, row)]
-            -- ...unless the tileset names the course its walls cap with,
-            -- which every wall cell wears whatever it draws on its face
-            if wallTop and s.class == "wall" then topTile = wallTop end
+            topTile = capOf(s, tile) or S.tileAt[keyOf(tx, row)]
           end
           -- water's surface, and only water's: the recessed sheet itself,
           -- never the ground's shoreline bands around it. A cell an object
diff --git a/lib/Structures.lua b/lib/Structures.lua
index 33e3fbd..ede84d3 100644
--- a/lib/Structures.lua
+++ b/lib/Structures.lua
@@ -227,7 +227,11 @@ function Structures.forMap(map)
   S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def),
         hideBareRing = hullRingOnly or nil,
         runs = {}, skip = {}, ground = {}, doorFold = {}, objectQuads = {},
-        grassQuads = {}, flowerQuads = {}, roundStamps = {}, figures = {} }
+        grassQuads = {}, flowerQuads = {}, roundStamps = {}, figures = {},
+        -- tile key -> the row a collapsed bookcase rank's box actually
+        -- stands on, so a standee supported by one lands on it rather than
+        -- where the drawing put it (see buildBookcases)
+        bookcaseBox = {} }
   Buildings.build(S, map, pixels(tileset), perRow)
 
   -- Fold doors into their buildings. A door cell is WALKABLE (the player
@@ -1882,6 +1886,12 @@ local function bookcaseRank(S, map, perRow, run, i, j, k, pane, srcU, srcV,
   end
 end
 
+-- The arts a `bookcase_backfill = "above"` row may inherit: terrain and
+-- solid bodies only (see the note at the backfill itself).  Everything
+-- absent here -- billboard, post, cylinder, grass, flower -- is a per-pixel
+-- object STANDING on terrain rather than terrain.
+local BACKFILL_ART = { flat = true, top = true, upright = true }
+
 function Structures.buildBookcases(S, map, x0, x1, y0, y1, data, perRow)
   perRow = perRow or map.tileset.tilesPerRow or 16
   -- What to do with the rows a rank VACATES (see TileShape.bookcaseBackfill).
@@ -1928,11 +1938,32 @@ function Structures.buildBookcases(S, map, x0, x1, y0, y1, data, perRow)
           -- shelf standing in a room.  `bookcase_backfill = "above"` hands it
           -- the cell above the run instead, shape and art, so a wall cut into
           -- a terrace has more terrace behind it rather than a trench.
+          --
+          -- Only BODY above backfills: a vacated row wants more of the
+          -- terrace the wall is cut into, and the terrace is whatever lies
+          -- flat, tops out or stands as a solid face.  A per-pixel STANDEE
+          -- above -- a statue, a sign, a bush -- is an object standing ON
+          -- that terrace, and copying it northward builds a second and a
+          -- third of it: Indigo Plateau's avenue statues sit directly on
+          -- the pilasters that collapse here, so every bird came out
+          -- duplicated twice down the shaft behind itself.  A standee
+          -- above means the row has no terrace to inherit, so it takes the
+          -- default and is painted with synthesized ground.
           local covered = math.min(2, front - top + 1)
           local srcK = keyOf(tx, top - 1)
           local src = backfill == "above" and S.shapeAt[srcK] or nil
+          if src and not BACKFILL_ART[src.art] then src = nil end
+          -- Where the box ACTUALLY ends up, remembered for every row of the
+          -- rank: the collapse walks the whole drawn run onto its southmost
+          -- cell, so anything that has to stand ON the box has to be told
+          -- where the box went.  A statue keys off the cell below its own
+          -- drawing, which is the run's NORTH end -- two rows away from the
+          -- box on a two-cell pilaster, which is exactly the distance the
+          -- Plateau's birds floated by.
+          local boxTop = front - covered + 1
           for cy = top, front do
             local tk = keyOf(tx, cy)
+            S.bookcaseBox[tk] = boxTop
             if src and cy <= front - covered then
               S.shapeAt[tk] = src
               S.tileAt[tk] = S.tileAt[srcK]
@@ -2840,9 +2871,10 @@ function Structures.buildObject(S, map, region, cluster,
   -- Town is where it showed: pinning the cliff's slope chain gave the
   -- posts along the cliff edge an authored 16px box to their south, and
   -- they were hoisted to stand on the clifftop instead of the path.
-  local baseY, support = 0, nil
+  local baseY, support, supportRow = 0, nil, nil
   if force and force ~= "opaque" then
-    local bs = S.shapeAt[keyOf(cluster.minX, cluster.maxY + 1)]
+    local belowK = keyOf(cluster.minX, cluster.maxY + 1)
+    local bs = S.shapeAt[belowK]
     local blocked = not map:isWalkableCell(math.floor(cluster.minX / 2),
                                            math.floor(cluster.maxY / 2))
     -- `bookcase` supports as well as `upright`.  A prop drawn above an
@@ -2859,6 +2891,13 @@ function Structures.buildObject(S, map, region, cluster,
        and (bs.art == "upright" or bs.art == "bookcase"
             or bs.class == "building") then
       baseY, support = bs.h, bs
+      -- A bookcase support has MOVED: the collapse walks the whole drawn
+      -- run onto its southmost cell, and the cell tested above is the run's
+      -- north end.  On the Plateau's two-cell pilasters that is a full cell
+      -- away, and the bird stood at the right HEIGHT over open ground with
+      -- its pillar behind it -- floating.  Stand it on the box's own north
+      -- row instead of one row south of its drawing.
+      supportRow = S.bookcaseBox[belowK]
     end
   end
   local atlasW = map.tileset.imageWidth or 128
@@ -2910,8 +2949,9 @@ function Structures.buildObject(S, map, region, cluster,
     end
   end
   for _, c in ipairs(comps) do
-    c.z0 = cluster.minY * 8 + math.floor(c.lowY / 8) * 8
-           + (support and 8 or 0) + (8 - depth) / 2
+    c.z0 = supportRow and (supportRow * 8 + (8 - depth) / 2)
+           or (cluster.minY * 8 + math.floor(c.lowY / 8) * 8
+               + (support and 8 or 0) + (8 - depth) / 2)
     c.z1 = c.z0 + depth
   end
 
diff --git a/lib/TileShape.lua b/lib/TileShape.lua
index a4270ea..bbfffa3 100644
--- a/lib/TileShape.lua
+++ b/lib/TileShape.lua
@@ -764,8 +764,9 @@ function TileShape.bookcaseRelief(tilesetId)
   return not (entry and entry.bookcase_relief == false)
 end
 
---- What every `wall` cell's TOP face wears in this tileset (a tileset
---- entry's wall_top).  Returns a tile id, or nil to leave the top alone.
+--- What a `wall` cell's TOP face wears in this tileset (a tileset entry's
+--- wall_top).  Returns a function tile -> cap tile id (nil for "leave it
+--- alone"), or nil when the tileset says nothing at all.
 ---
 --- A wall band is 16px of art folded upright over a run two drawn rows
 --- deep, so it folds ENTIRELY onto its face and has no row left to lay
@@ -773,17 +774,38 @@ end
 --- poster and window came out lying across the top of the wall as well as
 --- hanging on it.  What is up there is the wall's capping course, which is
 --- the plain panel the decorated column's own neighbours draw; naming it
---- per tileset is the whole fix, because "plain" is a fact about the
---- drawing that nothing in the geometry can measure.
+--- is the whole fix, because "plain" is a fact about the drawing that
+--- nothing in the geometry can measure.
 ---
---- Per tileset rather than per tile: one room caps with one course, and a
---- list keyed by the decorated tiles would have to be extended every time
---- a map hung something new on a wall already covered.
+--- Two forms, because tilesets differ in how far one answer reaches:
+---
+---   wall_top = <id>            EVERY wall cell caps with this course.
+---                              Right where one atlas dresses one kind of
+---                              room -- the town house, the Centers, Red's
+---                              two floors all cap with their own blank
+---                              panel, and a list keyed by the decorated
+---                              tiles would need extending every time a
+---                              map hung something new on the same wall.
+---   wall_top = { [tile] = id } only these tiles are redirected.  Right
+---                              where one atlas dresses several rooms:
+---                              LOBBY is the department store, the Game
+---                              Corner, Silph's floors, the roof AND the
+---                              Rocket lift, and the lift's cabin frame is
+---                              not what a shop wall caps with.
 function TileShape.wallTop(tilesetId)
   local s = load()
   local entry = s and s.tilesets and s.tilesets[tilesetId]
-  local tile = entry and entry.wall_top
-  return type(tile) == "number" and tile or nil
+  local spec = entry and entry.wall_top
+  if type(spec) == "number" then
+    return function() return spec end
+  end
+  if type(spec) == "table" then
+    return function(tile)
+      local cap = spec[tile]
+      return type(cap) == "number" and cap or nil
+    end
+  end
+  return nil
 end
 
 -- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index aa6cde0..3e0d84a 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -7020,10 +7020,21 @@ end)()
   local Struct = run.loader.exports.DRAMATIC_SHAPE.lib.require("Structures")
   local Shape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape")
 
-  T.eq(Shape.wallTop("HOUSE"), 0,
+  -- the blanket form: whatever a wall cell draws, it caps with the one
+  -- course the atlas's rooms are panelled in
+  T.eq(Shape.wallTop("HOUSE")(45), 0,
     "the town house caps its walls with the blank course")
-  T.eq(Shape.wallTop("POKECENTER"), 40,
+  T.eq(Shape.wallTop("POKECENTER")(2), 40,
     "and a Center with its striped panel -- the tile cell (9,0) draws")
+  T.eq(Shape.wallTop("REDS_HOUSE_2")(36), 0,
+    "Red's bedroom with its own blank panel, over the window")
+  T.eq(Shape.wallTop("REDS_HOUSE_1")(36), 0, "and so does the floor below")
+  -- the keyed form, for an atlas that dresses more than one kind of room
+  T.eq(Shape.wallTop("LOBBY")(40), 93,
+    "the Rocket lift's car door caps with the cabin frame")
+  T.eq(Shape.wallTop("LOBBY")(1), nil,
+    "but the department store's own panel is left exactly as it was -- "
+    .. "one atlas, several rooms, and only the lift is a lift")
   T.eq(Shape.wallTop("DS_NO_SUCH_TILESET"), nil,
     "a tileset that says nothing keeps the top it always had")
 
@@ -7061,7 +7072,11 @@ end)()
         local ax = math.floor(math.min(a[4], b[4], c[4], d[4]) * 128 + 0.5)
         local ay = math.floor(math.min(a[5], b[5], c[5], d[5]) * 48 + 0.5)
         local tx = math.floor(math.min(a[1], b[1], c[1], d[1]) / 8)
-        out[tx] = out[tx] or (math.floor(ay / 8) * 16 + math.floor(ax / 8))
+        local ty = math.floor(math.min(a[3], b[3], c[3], d[3]) / 8)
+        -- keyed by the TILE ROW too: a wall band is two drawn rows deep
+        -- and they can wear different art
+        out[ty * 100 + tx] = out[ty * 100 + tx]
+                             or (math.floor(ay / 8) * 16 + math.floor(ax / 8))
       end
     end
     return out, verts
@@ -7073,9 +7088,12 @@ end)()
     { 0, 0, 45, 46, 36, 36, 0, 0 },
     { 0, 0, 61, 62, 52, 52, 0, 0 },
   })
-  for tx = 0, 7 do
-    T.eq(houseTops[tx], 0,
-      ("column %d of the house's back wall caps with the blank course"):format(tx))
+  for ty = 0, 1 do
+    for tx = 0, 7 do
+      T.eq(houseTops[ty * 100 + tx], 0,
+        ("(%d,%d) of the house's back wall caps with the blank course")
+        :format(tx, ty))
+    end
   end
 
   -- and the FACE is untouched: the poster still hangs in the room, which is
@@ -7095,11 +7113,145 @@ end)()
     { 40, 40, 40, 2, 3, 40, 40, 40 },
     { 40, 40, 40, 18, 19, 40, 40, 40 },
   })
-  for tx = 0, 7 do
-    T.eq(pcTops[tx], 40,
-      ("column %d of the Center's back wall caps with the striped panel")
-      :format(tx))
+  for ty = 0, 1 do
+    for tx = 0, 7 do
+      T.eq(pcTops[ty * 100 + tx], 40,
+        ("(%d,%d) of the Center's back wall caps with the striped panel")
+        :format(tx, ty))
+    end
+  end
+
+  -- ------- the Rocket lift's car doors
+  --
+  -- Detected rather than pinned -- the doors are a volume Structures finds
+  -- -- so the cap has to reach the RUN branch too, not only the pinned one.
+  local liftTops = topTiles("LOBBY", {
+    { 92, 92, 92, 92, 92, 92, 92, 92 },
+    { 93, 93, 93, 93, 93, 93, 93, 93 },
+    {  1,  1, 40, 40, 40, 40,  1,  1 },
+    { 33, 33, 56, 56, 56, 56, 33, 33 },
+  })
+  -- rows 2 and 3 are the doors' own band; the doors span columns 2-5
+  for ty = 2, 3 do
+    for tx = 2, 5 do
+      T.eq(liftTops[ty * 100 + tx], 93,
+        ("(%d,%d) of the lift's door caps with the cabin frame"):format(tx, ty))
+    end
+  end
+  -- and the panelling either side of them is left alone, which is what the
+  -- keyed form buys: the same atlas panels the department store
+  T.eq(liftTops[2 * 100], 1, "the blank course beside the door is untouched")
+  T.eq(liftTops[3 * 100], 33, "and so is its skirting")
+end)()
+
+-- ------- Lance's room is furnished with the badge gyms' statue
+--
+-- The same drawing on a different atlas: one cell of bird over one cell of
+-- plinth. Left derived the pair merged into one 32px volume wearing the
+-- statue folded onto its face -- the extruded picture. The gyms' reading
+-- (solid plinth, per-pixel standee on top) is the right one here too, and
+-- these eight tiles draw nothing else anywhere in the game.
+;(function()
+  local Shape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape")
+  local shapes = Shape.forMap({
+    tileset = { id = "DOJO", tilesPerRow = 16,
+                imageWidth = 128, imageHeight = 48, grassTile = -1 },
+    walkable = { [17] = true },
+  })
+  for _, t in ipairs({ 2, 18, 19, 56 }) do
+    T.eq(shapes[t].class, "prop",
+      ("the statue's figure tile %d stands as a per-pixel cutout"):format(t))
+    T.eq(shapes[t].authored, true, "and it is an authored answer, not derived")
+  end
+  for _, t in ipairs({ 34, 35, 50, 51 }) do
+    T.eq(shapes[t].class, "wall",
+      ("the plinth tile %d stays a solid 16px block"):format(t))
+    T.eq(shapes[t].authored, true,
+      "authored, which is what the standee's support rule tests -- and "
+      .. "what keeps tile 50 ($32) out of the water-fallback trap")
+  end
+  T.eq(shapes[13].class, "bookcase",
+    "and Oak's shelf ranks on the same atlas are untouched")
+end)()
+
+-- ------- a wall cut into a terrace inherits TERRACE, never a statue
+--
+-- `bookcase_backfill = "above"` hands a vacated row the cell above the run,
+-- so masonry set into a hillside has more hillside behind it rather than a
+-- trench. Indigo Plateau's avenue statues stand directly on the pilasters
+-- that collapse this way, so what every one of them inherited was the BIRD:
+-- the figure's shape and art copied onto two more rows down the shaft, and
+-- the statue came out two deep behind itself. A standee above is an object
+-- standing ON the terrace, not terrace -- so there is nothing to inherit.
+;(function()
+  local Struct = run.loader.exports.DRAMATIC_SHAPE.lib.require("Structures")
+  -- the real west-edge statue of INDIGO_PLATEAU, tile for tile: the bird
+  -- (37/38 over 40/41) on the pilaster (cap 21/22, shaft 5/6 twice, foot
+  -- 21/22), standing on the plateau's paving (35)
+  local rows = {
+    { 46, 47 }, { 46, 47 },
+    { 37, 38 }, { 40, 41 },
+    { 21, 22 }, {  5,  6 },
+    {  5,  6 }, { 21, 22 },
+    { 35, 35 }, { 35, 35 },
+    { 35, 35 }, { 35, 35 },
+  }
+  local map = {
+    id = "DS_TEST_PLATEAU_STATUE",
+    tileset = { id = "PLATEAU", image = "gfx/tilesets/ds_test.png",
+                tilesPerRow = 16, imageWidth = 128, imageHeight = 40,
+                blocks = {}, grassTile = -1 },
+    def = { width = 1, height = 3, tileset = "PLATEAU" },
+    walkable = { [35] = true },
+    waterTiles = {},
+    doorTiles = {},
+    tileAt = function(_, tx, ty)
+      local r = rows[math.max(1, math.min(#rows, ty + 1))]
+      return r[(tx % 2) + 1]
+    end,
+    cellTile = function(self, cx, cy) return self:tileAt(cx * 2, cy * 2 + 1) end,
+    isWaterCell = function() return false end,
+    isWalkableCell = function(self, cx, cy)
+      return self:cellTile(cx, cy) == 35
+    end,
+    isDoorTileCell = function() return false end,
+    isGrassCell = function() return false end,
+    inBounds = function(_, cx, cy)
+      return cx >= 0 and cy >= 0 and cx < 2 and cy < 6
+    end,
+  }
+  Struct.invalidate(map.id)
+  local S = Struct.forMap(map)
+  local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end
+
+  -- the bird stands, once, where it is drawn
+  for _, ty in ipairs({ 2, 3 }) do
+    T.eq(S.shapeAt[keyOf(0, ty)].art, "billboard",
+      "the statue's own rows carry the standee")
+  end
+  -- and the pilaster rows it vacates carry NOTHING -- not a second bird
+  for _, ty in ipairs({ 4, 5, 6, 7 }) do
+    local s = S.shapeAt[keyOf(0, ty)]
+    T.check(s == nil or s.art ~= "billboard",
+      ("pilaster row %d did not inherit the statue standing on it"):format(ty))
+  end
+
+  -- ...and the bird stands on the box rather than where it is DRAWN. The
+  -- collapse walks the whole four-row pilaster onto its southmost cell, so
+  -- a standee that trusted its own drawn position ended up a full cell
+  -- north of the pillar holding it up -- at the right height, over open
+  -- ground. Every row of the rank records where the box actually went.
+  for _, ty in ipairs({ 4, 5, 6, 7 }) do
+    T.eq(S.bookcaseBox[keyOf(0, ty)], 6,
+      ("rank row %d knows the box stands on row 6"):format(ty))
+  end
+  local zlo = math.huge
+  for _, q in ipairs(S.objectQuads) do
+    for i = 1, 4 do zlo = math.min(zlo, q[i][3]) end
   end
+  T.eq(math.floor(zlo / 8), 6,
+    "and the statue's own geometry starts on that row -- standing on the "
+    .. "pillar, not hanging in the air two cells in front of it")
 end)()
 
 Pipelines.reset()
diff --git a/tests/voxel_fix_shots.lua b/tests/voxel_fix_shots.lua
new file mode 100644
index 0000000..ab408cb
--- /dev/null
+++ b/tests/voxel_fix_shots.lua
@@ -0,0 +1,137 @@
+-- Scratch driver: one shot of each fixture the wall-top / statue pass
+-- touched, so the change can be looked at rather than reasoned about.
+--
+--   POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/voxel_fix_shots.lua \
+--   SHOT_DIR=mods/DramaticShapeVoxelMod/.claude/voxel_fix AB_TAG=. lovec.exe .
+--
+-- SHOT_DIR is relative to the PROJECT ROOT, which is where lovec runs from --
+-- the mod's own .claude/ is where these belong, so it has to be spelt out.
+--
+-- Every scene stands the player on the nearest WALKABLE cell to the vantage
+-- named below (teleporting into a wall puts the close cameras inside the
+-- geometry), faces the fixture, and shoots at voxel rung 3 -- the higher
+-- top-down camera, which is the one that shows what a wall wears on TOP.
+return function(game)
+  local U = dofile("tests/drivers/util.lua")
+  local Pipelines = require("src.render.Pipelines")
+
+  local ROOT = (os.getenv("SHOT_DIR")
+                or "mods/DramaticShapeVoxelMod/.claude/voxel_fix")
+    .. "/" .. (os.getenv("AB_TAG") or ".")
+
+  local handle = game.mods.exports["DRAMATIC_SHAPE"]
+  if not (handle and handle.lib) then
+    print("[voxfix] DRAMATIC_SHAPE mod not loaded")
+    return
+  end
+  local V = handle.lib
+  local DayNight = V.require("DayNight")
+  local ChunkMesher = V.require("ChunkMesher")
+  local Voxel = V.require("VoxelState")
+  local TileShape = V.require("TileShape")
+
+  -- prove the game is reading THIS tree, not a stale installed copy
+  local ht = TileShape.wallTop and TileShape.wallTop("HOUSE")
+  print(("[voxfix] wallTop present=%s HOUSE(45)=%s")
+        :format(tostring(TileShape.wallTop ~= nil),
+                tostring(ht and ht(45))))
+
+  require("src.world.OverworldController").rollEncounter = function() return nil end
+  local TileRenderer = require("src.render.TileRenderer")
+  TileRenderer.tick = function() end
+  TileRenderer.animFrame = function() return 0 end
+  DayNight.setting:sync("day")
+
+  pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
+  pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
+
+  local Zoom = require("src.render.Zoom")
+  pcall(function()
+    game.save.options.zoom = 1
+    Zoom.applyOptions(game.save.options)
+  end)
+
+  local function settle()
+    for _ = 1, 900 do
+      if ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    for _ = 1, 300 do
+      if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
+      U.wait(1)
+    end
+    U.wait(40)
+  end
+
+  -- the nearest walkable cell to the vantage, searched in rings -- a
+  -- vantage picked off a map dump is often the fixture itself
+  local function place(x, y, face)
+    local m = game.overworld.map
+    for r = 0, 8 do
+      for dy = -r, r do
+        for dx = -r, r do
+          if math.max(math.abs(dx), math.abs(dy)) == r then
+            local px, py = x + dx, y + dy
+            if m:inBounds(px, py) and m:isWalkableCell(px, py) then
+              game.overworld.player.cellX = px
+              game.overworld.player.cellY = py
+              game.overworld.player.facing = face
+              return px, py
+            end
+          end
+        end
+      end
+    end
+    return x, y
+  end
+
+  local SCENES = {
+    { map = "BLUES_HOUSE",             x = 4,  y = 3,  face = "up",
+      label = "house_wall_top" },
+    { map = "VIRIDIAN_POKECENTER",     x = 4,  y = 3,  face = "up",
+      label = "pokecenter_wall_top" },
+    { map = "ROCKET_HIDEOUT_ELEVATOR", x = 2,  y = 3,  face = "up",
+      label = "rocket_lift_wall_top" },
+    { map = "REDS_HOUSE_2F",           x = 6,  y = 3,  face = "up",
+      label = "reds_2f_wall_top" },
+    { map = "REDS_HOUSE_1F",           x = 5,  y = 3,  face = "up",
+      label = "reds_1f_wall_top" },
+    { map = "LANCES_ROOM",             x = 6,  y = 15, face = "up",
+      label = "lances_room_statues" },
+    { map = "INDIGO_PLATEAU",          x = 2,  y = 5,  face = "up",
+      label = "indigo_plateau_statue" },
+    -- the west-edge statue itself, close enough to count the birds on it
+    { map = "INDIGO_PLATEAU",          x = 1,  y = 4,  face = "up",
+      label = "indigo_plateau_statue_close", rung = 5 },
+    { map = "INDIGO_PLATEAU",          x = 2,  y = 4,  face = "up",
+      label = "indigo_plateau_statue_near" },
+  }
+
+  local only = os.getenv("VF_ONLY")
+  local shots = 0
+  for _, s in ipairs(SCENES) do
+   if not (only and only ~= "" and not only:find(s.label, 1, true)) then
+    local ok, err = pcall(function()
+      U.teleport(game, s.map, s.x, s.y, s.face)
+      local px, py = place(s.x, s.y, s.face)
+      Pipelines.setLevel("voxel", s.rung or 3)
+      Pipelines.setLevel("tiltshift", 0)
+      settle()
+      local path = ("%s/%s.png"):format(ROOT, s.label)
+      game.capturePath = path
+      U.wait(8)
+      local f = io.open(path, "rb")
+      if f then
+        f:close()
+        shots = shots + 1
+        print(("[voxfix] %s -> %s (stood %d,%d)"):format(s.map, path, px, py))
+      else
+        print("[voxfix] capture missed: " .. path)
+      end
+    end)
+    if not ok then print("[voxfix] " .. s.map .. " failed: " .. tostring(err)) end
+   end
+  end
+  print(("[voxfix] %d/%d shots into %s"):format(shots, #SCENES, ROOT))
+  love.event.quit()
+end