From 41a86b5fb222e1e3acae2931c94e4ff4a0ee4ff2 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Wed, 29 Jul 2026 19:03:11 -0400 Subject: [PATCH] second pass at indigo plateau --- data/voxel_heights.lua | 72 ++++++++++++++++++++++++++++++++++++++++-- lib/Structures.lua | 30 +++++++++++++++--- lib/TileShape.lua | 70 ++++++++++++++++++++++++++++++---------- 3 files changed, 148 insertions(+), 24 deletions(-) diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 1c372d4..4079a53 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -2344,15 +2344,81 @@ return { -- facade, the walls flanking the avenue and every badge-check gate -- down ROUTE_23), and $2E/$2F the white pillar shaft with $20/$21 its -- cap. These four groups are the enclosure. - cliff = { 3, - 13, 14, 15, + cliff = { 3, 13, 32, 33, 46, 47 }, + -- THE GATE WALLS, stacked rather than laid out in depth. $0D/$0F/$0E + -- (13/15/14 -- top band, face, base) draw the League's facade, the + -- walls flanking the avenue and every badge-check gate down ROUTE_23, + -- as FOUR tile rows: 13 / 15 / 15 / 14. That is 32px of artwork + -- depicting one wall, and at one class per row it built four boxes + -- marching north, so the wall was 32px DEEP -- you saw the southmost + -- row's face and the rest hid behind it. + -- + -- `bookcase` collapses the run onto a single one-cell-deep box at its + -- full drawn height: bands from the south are base, face, face, top + -- band, and the two rows behind are vacated. Same mechanism the + -- Mart's back wall uses. + -- ...and the PILASTER stacks with it. $05/$06 is its shaft, and it + -- is drawn only ever inside a pilaster, so it needs no rule. Without + -- this the pale columns stood 16px against a 32px brown wall and + -- their top vertices sat a whole course below the wall's crown. + bookcase = { 14, 15, 5, 6 }, + -- Tile 13 is DUAL-USE and needs resolving per position. It is the + -- gate wall's TOP BAND (block $28's first row) and it is also the + -- BASE COURSE under a column of rock face (blocks $18/$1A/$1B, last + -- row). Pinned `bookcase` outright the second use became a one-row + -- rank -- an 8px stub under the rock, 352 of them over both maps. + -- + -- ABOVE cannot tell them apart, which is what `when_below` is for. + -- Scanned over both maps through the engine's own tileAt, the tile + -- above a 13 is the rock face $03 for 140 base courses AND for 64 + -- gate bands -- so a rule on `above` misfires on those 64 (it did: + -- their runs came out 2 and 3 bands instead of 4). BELOW splits it + -- exactly: the wall's own face $0F sits under the top band and under + -- nothing else, 336 against 352. So 13 defaults to `cliff` and is + -- promoted where the face is drawn beneath it. + -- + -- 14 and 15 need no rule: 14 only ever sits under 15, 15 only ever + -- under 13 or 15. + when_below = { + [13] = { { below = { 15 }, class = "bookcase" } }, + -- The pilaster's CAP ($15/$16) is the same stone as the statue's + -- plinth top -- the artist drew it twice -- so it too resolves per + -- position. Scanned over both maps: a cap with the shaft $05/$06 + -- beneath it is a pilaster (76 of them) and one with the plaque + -- base $30/$31 beneath it is a statue's plinth (47). Default + -- `wall`, promoted here, so the plinth keeps its single course and + -- the statue standing on it still totals the 32px the wall is. + [21] = { { below = { 5 }, class = "bookcase" } }, + [22] = { { below = { 6 }, class = "bookcase" } }, + }, + -- The other two ends of the same pair of drawings, keyed the other + -- way round. A pilaster is capped at BOTH ends, so its lower cap has + -- the shaft ABOVE it (8 placements); and the plaque base $30/$31 is a + -- pilaster's foot when the shaft is above it (68) but a statue's + -- plinth bottom when the cap $15/$16 is (47). + when_above = { + [21] = { { above = { 5 }, class = "bookcase" } }, + [22] = { { above = { 6 }, class = "bookcase" } }, + [48] = { { above = { 5 }, class = "bookcase" } }, + [49] = { { above = { 6 }, class = "bookcase" } }, + }, + -- The vacated rows behind a collapsed wall take the cell ABOVE the + -- run rather than the default hidden floor: here that is more rock + -- face on ROUTE_23 and the plateau's paving or grass on INDIGO_ + -- PLATEAU, so the wall reads as set INTO the terrace instead of + -- standing in front of a trench of synthesized ground. + bookcase_backfill = "above", -- ONE course, and it must stay one: $15/$16 + $05/$06 + $30/$31 is -- the pilaster -- cap, shaft, plaque base -- and $15/$16 over $30/$31 -- IS the statue's plinth, the artist having drawn the same stone -- twice. Raising it would carry every statue standee up to 48px and -- break the very match the cliff height was chosen for. - wall = { 21, 22, 48, 49, 5, 6 }, + -- $05/$06 is NOT here: it moved to `bookcase` above, and a tile + -- listed in two groups resolves by whichever `pairs` order wins -- + -- half the pilasters kept the shaft as `wall`, which broke the run + -- and left their caps as isolated 8px stubs. One group per tile. + wall = { 21, 22, 48, 49 }, -- The statues, and the same bird at the pillar feet. Black-outline -- segmented: the outline and everything it encloses stay, the sky -- and the paving around them flood away. diff --git a/lib/Structures.lua b/lib/Structures.lua index c4ab192..d27b364 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -1222,6 +1222,9 @@ local function bookcaseRank(S, map, tx, northTy, frontTy, capTile) end function Structures.buildBookcases(S, map, x0, x1, y0, y1) + -- What to do with the rows a rank VACATES (see TileShape.bookcaseBackfill). + -- Read once: it is a property of the tileset, not of the column. + local backfill = TileShape.bookcaseBackfill(map.tileset.id) for tx = x0, x1 do local ty = y1 while ty >= y0 do @@ -1249,10 +1252,24 @@ function Structures.buildBookcases(S, map, x0, x1, y0, y1) capTile = S.tileAt[ck] end end + -- The box is one cell deep, so it covers only the run's southmost + -- rows; everything north of that is vacated. By default a vacated + -- row is skipped and painted with synthesized ground -- right for a + -- 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. + local covered = math.min(2, front - top + 1) + local srcK = keyOf(tx, top - 1) + local src = backfill == "above" and S.shapeAt[srcK] or nil for cy = top, front do local tk = keyOf(tx, cy) - S.skip[tk] = true - S.ground[tk] = false + if src and cy <= front - covered then + S.shapeAt[tk] = src + S.tileAt[tk] = S.tileAt[srcK] + else + S.skip[tk] = true + S.ground[tk] = false + end end bookcaseRank(S, map, tx, top, front, capTile) front = top - 1 @@ -1977,8 +1994,13 @@ function Structures.buildObject(S, map, region, cluster, local bs = S.shapeAt[keyOf(cluster.minX, cluster.maxY + 1)] local blocked = not map:isWalkableCell(math.floor(cluster.minX / 2), math.floor(cluster.maxY / 2)) - if blocked and bs and bs.authored and bs.art == "upright" - and (bs.h or 0) > 0 then + -- `bookcase` supports as well as `upright`. A prop drawn above an + -- authored box stands ON it whatever art the box renders with, and a + -- stacked box is still a box: the Plateau's gate pilasters carry a + -- statue on 48 of their tops, and collapsing the pilaster to a stacked + -- run made every one of them fail this test and drop to ground level. + if blocked and bs and bs.authored and (bs.h or 0) > 0 + and (bs.art == "upright" or bs.art == "bookcase") then baseY, support = bs.h, bs end end diff --git a/lib/TileShape.lua b/lib/TileShape.lua index 31ac92b..6cb65ce 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -233,29 +233,42 @@ end -- class = "..." } } }`, evaluated per POSITION in TileShape.at, where -- the map and coordinates are in hand. First match wins; no match keeps -- the tile's ordinary pin. +-- `when_below` is the mirror, and it exists because ABOVE is not always the +-- side that tells the two uses apart. The Plateau's $0D is the case: it is +-- the gate wall's top band AND the base course under a column of rock face, +-- and scanned over both maps the tile above is $03 for 64 of the first and +-- 140 of the second -- no rule on `above` can split them. What is BELOW +-- does, exactly: the wall's own face $0F sits under the top band and under +-- nothing else (336 vs 352, clean). local function authoredConditions(tilesetId, heights) local s = load() local entry = s and s.tilesets and s.tilesets[tilesetId] - local spec = entry and entry.when_above - if type(spec) ~= "table" then return nil end + if type(entry) ~= "table" then return nil end local out, any = {}, false - for tile, rules in pairs(spec) do - if type(tile) == "number" and type(rules) == "table" then - local list = {} - for _, rule in ipairs(rules) do - if type(rule) == "table" and heights[rule.class] - and type(rule.above) == "table" then - local set = {} - for _, t in ipairs(rule.above) do set[t] = true end - list[#list + 1] = { above = set, class = rule.class } + + local function collect(spec, side) + if type(spec) ~= "table" then return end + for tile, rules in pairs(spec) do + if type(tile) == "number" and type(rules) == "table" then + local list = out[tile] or {} + for _, rule in ipairs(rules) do + if type(rule) == "table" and heights[rule.class] + and type(rule[side]) == "table" then + local set = {} + for _, t in ipairs(rule[side]) do set[t] = true end + list[#list + 1] = { side = side, set = set, class = rule.class } + end + end + if #list > 0 then + out[tile] = list + any = true end - end - if #list > 0 then - out[tile] = list - any = true end end end + + collect(entry.when_above, "above") + collect(entry.when_below, "below") return any and out or nil end @@ -357,9 +370,12 @@ function TileShape.at(map, shapes, tile, tx, ty) -- tile and the cell rules below (see authoredConditions) local rules = shapes.cond and shapes.cond[tile] if rules then - local above = map:tileAt(tx, ty - 1) for _, rule in ipairs(rules) do - if above and rule.above[above] then + -- NOTE map:tileAt border-EXTENDS: one row off an edge answers the + -- map's borderBlock, never nil. A rule listing whatever that block + -- draws will fire along that whole edge (it did, on the Marts). + local n = map:tileAt(tx, rule.side == "above" and ty - 1 or ty + 1) + if n and rule.set[n] then -- shapes.condShape, NOT shapes.classes: the canonical class -- shapes are SHARED, and `wall` in particular is the very object -- rule 4 hands every unauthored solid tile. Marking that one @@ -503,6 +519,26 @@ function TileShape.propBg(tilesetId) return bgCache[tilesetId] or nil end +-- What a bookcase rank does with the rows it VACATES -- the ones behind the +-- one-cell-deep box it collapses onto (a tileset entry's +-- bookcase_backfill). Returns the mode name, or nil for the default. +-- +-- "above" hand them the cell immediately above the run: its shape and +-- its art. A wall set INTO a terrace wants this -- the ground +-- behind it is more terrace, not a trench. +-- nil skip them and paint the map's commonest ground underneath, +-- which is right for a free-standing shelf against a wall. +-- +-- Per tileset because it is a statement about what the drawing depicts, and +-- the answer differs: the Mart's racks and Red's shelves stand in a room, +-- the Plateau's gate walls are cut into a hillside. +function TileShape.bookcaseBackfill(tilesetId) + local s = load() + local entry = s and s.tilesets and s.tilesets[tilesetId] + local mode = entry and entry.bookcase_backfill + return mode == "above" and mode or nil +end + -- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset -- record needs the next lookup to re-resolve (hot reload, mod toggle). function TileShape.invalidate()