From fdcd5f1fceab99360a202ca0d4dcdba82da6ba4a Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Sun, 26 Jul 2026 23:30:32 -0400 Subject: [PATCH 01/16] fix flowers --- lib/Structures.lua | 199 ++++++++++++++++++++++++++++++++++ lib/TerrainAtlas.lua | 80 +++++++++++++- lib/TileShape.lua | 38 ++++++- tests/dramatic_shape_test.lua | 92 ++++++++++++++++ tests/voxel_anim_probe.lua | 18 ++- 5 files changed, 419 insertions(+), 8 deletions(-) diff --git a/lib/Structures.lua b/lib/Structures.lua index 8019046..c262841 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -413,6 +413,9 @@ function Structures.forMap(map) -- renderer never draws a neighbour's ring, and standing scenery past a -- map's edge would poke into the map next door ---- Structures.buildGrass(S, map, 0, tw - 1, 0, th - 1, data) + + -- ---- flowers: the animated meadow tile stands as a 1px cutout ---- + Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data) end -- unresolved claimed ground (a hull with no art match, headless @@ -1982,6 +1985,202 @@ function Structures.buildGrass(S, map, x0, x1, y0, y1, data) end end +-- ---- flowers ---- + +-- The animated flower tile stands up as a billboard ONE VOXEL deep, cut +-- to the drawing's darkest tones PLUS everything they enclose -- the +-- round-scenery hull's rule: flood the tile border through every +-- non-dark pixel, and what the flood cannot reach is the flower, its +-- pale petal insides included. The mesh is static and the flower is +-- not, so the geometry spans the UNION of that mask over the base art +-- and every animation frame, and TerrainAtlas rewrites the tile's slot +-- each step with only the CURRENT frame's mask opaque -- the rest keyed +-- to alpha, which the voxel shader discards. The standing silhouette +-- trims itself frame by frame in texture space; the sway animates +-- without a vertex moving, off the same engine clock as the flat path. +-- +-- The ground beneath is synthesized from the commonest flat neighbour, +-- like the ground under a detected prop: the tile's own slot no longer +-- holds art anyone can draw flat. +local FLOWER_THICK = 1 + +local function flowerFrames(tileset, tileId) + local out = {} + local ok, declared = pcall(function() + if tileset.animatedTiles then return tileset.animatedTiles end + local TileRenderer = require("src.render.TileRenderer") + return TileRenderer.defaultAnimatedTiles(tileset) + end) + if not ok then return out end + for _, spec in ipairs(type(declared) == "table" and declared or {}) do + if spec.kind == "frames" and spec.tile == tileId then + for _, path in pairs(spec.images or {}) do + local okF, frame = pcall(Assets.imageData, path) + if okF and frame then out[#out + 1] = frame end + end + end + end + return out +end + +local function flowerTemplate(map, data, tileId) + local tileset = map.tileset + local perRow = tileset.tilesPerRow or 16 + local atlasW = tileset.imageWidth or 128 + local atlasH = tileset.imageHeight or 48 + local ax0 = (tileId % perRow) * 8 + local ay0 = math.floor(tileId / perRow) * 8 + + -- per image: dark tones, then the border flood that finds what they + -- enclose. Each image closes over ITS OWN outline before the union -- + -- a pocket two frames only enclose together is not part of either. + local dark = {} + local function markMask(img, ox, oy) + local d, reach, stack = {}, {}, {} + for py = 0, 7 do + for px = 0, 7 do + local r, g, b, a = img:getPixel(ox + px, oy + py) + if a > 0 and math.min(r, g, b) <= 0.5 then + d[py * 8 + px] = true + end + end + end + for i = 0, 7 do + for _, s in ipairs({ i, 56 + i, i * 8, i * 8 + 7 }) do + if not d[s] and not reach[s] then + reach[s] = true + stack[#stack + 1] = s + end + end + end + while #stack > 0 do + local p = table.remove(stack) + local px, py = p % 8, math.floor(p / 8) + for _, dir in ipairs(DIRS4) do + local nx, ny = px + dir[1], py + dir[2] + if nx >= 0 and nx < 8 and ny >= 0 and ny < 8 then + local ni = ny * 8 + nx + if not d[ni] and not reach[ni] then + reach[ni] = true + stack[#stack + 1] = ni + end + end + end + end + for i = 0, 63 do + if d[i] or not reach[i] then dark[i] = true end + end + end + markMask(data, ax0, ay0) + for _, frame in ipairs(flowerFrames(tileset, tileId)) do + pcall(markMask, frame, 0, 0) + end + + local function on(px, py) + if px < 0 or px > 7 or py < 0 or py > 7 then return false end + return dark[py * 8 + px] == true + end + + local quads = {} + local zB = 4 - FLOWER_THICK / 2 -- one slab at the tile's middle + local zF = zB + FLOWER_THICK + for py = 0, 7 do + Budget.tick() + local yTop, yBot = 8 - py, 7 - py + local ix = 0 + while ix < 8 do + if on(ix, py) then + local ix2 = ix + while ix2 + 1 < 8 and on(ix2 + 1, py) do ix2 = ix2 + 1 end + local u0 = (ax0 + ix + 0.05) / atlasW + local u1 = (ax0 + ix2 + 0.95) / atlasW + local v0 = (ay0 + py + 0.05) / atlasH + local v1 = (ay0 + py + 0.95) / atlasH + quads[#quads + 1] = { -- front + { ix, yBot, zF }, { ix2 + 1, yBot, zF }, + { ix2 + 1, yTop, zF }, { ix, yTop, zF }, + uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, + shade = OBJ_SHADE.front, + } + quads[#quads + 1] = { -- back + { ix2 + 1, yBot, zB }, { ix, yBot, zB }, + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, + uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, + shade = OBJ_SHADE.back, + } + -- petal tips: a top strip where the row above is clear. The + -- strip samples its own row's texel, so a tip that is not in + -- the current frame discards with the face beneath it + if not on(ix, py - 1) then + quads[#quads + 1] = { + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, + { ix2 + 1, yTop, zF }, { ix, yTop, zF }, + uv = { { u0, v0 }, { u1, v0 }, { u1, v0 }, { u0, v0 } }, + shade = OBJ_SHADE.top, + } + end + ix = ix2 + 1 + else + ix = ix + 1 + end + end + end + return quads +end + +function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data) + local templates = {} + local quads = S.objectQuads + for ty = y0, y1 do + for tx = x0, x1 do + Budget.tick() + local k = keyOf(tx, ty) + local s = S.shapeAt[k] + if s and s.art == "flower" then + -- the tile's atlas slot carries only the standing cutout now, so + -- EVERY flower position -- ring included -- paints synthesized + -- ground instead of its own art: the commonest flat neighbour + -- that is not itself a flower, else the map's commonest ground + -- (forMap's end-of-build vote resolves the `false`) + S.skip[k] = true + local votes, best, bestN = {}, nil, 0 + for _, d in ipairs(DIRS4) do + local nk = keyOf(tx + d[1], ty + d[2]) + local ns = S.shapeAt[nk] + if ns and ns.flat and ns.class ~= "void" + and ns.class ~= "flower" then + local t = S.tileAt[nk] + votes[t] = (votes[t] or 0) + 1 + if votes[t] > bestN then best, bestN = t, votes[t] end + end + end + S.ground[k] = best or false + + -- standee BODY only, like grass: standing scenery past a map's + -- edge would poke into the map next door + if tx >= 0 and ty >= 0 and tx < tw and ty < th then + local tileId = S.tileAt[k] + local tpl = templates[tileId] + if not tpl then + tpl = flowerTemplate(map, data, tileId) + templates[tileId] = tpl + end + local wx, wz = tx * 8, ty * 8 + for _, q in ipairs(tpl) do + quads[#quads + 1] = { + { q[1][1] + wx, q[1][2], q[1][3] + wz }, + { q[2][1] + wx, q[2][2], q[2][3] + wz }, + { q[3][1] + wx, q[3][2], q[3][3] + wz }, + { q[4][1] + wx, q[4][2], q[4][3] + wz }, + uv = q.uv, shade = q.shade, + } + end + end + end + end + end +end + -- Drop one map's analysis (Cut changed the block layer) or everything. -- Hull templates key on art content (tileset + tiles), which a block edit -- cannot change, so only the full drop clears them (atlas reload). diff --git a/lib/TerrainAtlas.lua b/lib/TerrainAtlas.lua index ca3abf2..8d12e36 100644 --- a/lib/TerrainAtlas.lua +++ b/lib/TerrainAtlas.lua @@ -27,6 +27,9 @@ -- the first place (home/vcopy.asm rewrites the tile's VRAM bytes); the 2D -- overdraw is the port's workaround, not the original. +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + local Assets = require("src.render.Assets") local TileRenderer = require("src.render.TileRenderer") local PaletteFX = require("src.render.PaletteFX") @@ -146,6 +149,8 @@ local function learnShades(raw, baked, tile, perRow) return map end +local DIRS4 = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } } + -- One animated entry's tile slot, written into `out` at step `step`. local function patch(out, entry, spec, step) local perRow, tile = entry.perRow, spec.tile @@ -167,12 +172,59 @@ local function patch(out, entry, spec, step) local ok, frame = pcall(Assets.imageData, path) if not ok or not frame then return end local shades = entry.shades and entry.shades[tile] + -- a `cut` tile is the flower billboard's slot (Structures' + -- buildFlowers): only the frame's darkest tones AND what they + -- enclose stay opaque -- the round-scenery hull's rule, flooding + -- the frame border through every non-dark pixel so the pale petal + -- insides survive with the outline. The true background is keyed + -- to alpha; nothing else samples this slot (the ground under a + -- flower is synthesized), and the shader's discard is what lets + -- the billboard's silhouette change per frame under geometry that + -- never moves. + local mask = nil + if entry.cut and entry.cut[tile] then + local dark, reach, stack = {}, {}, {} + for y = 0, 7 do + for x = 0, 7 do + local r, _, _, a = frame:getPixel(x, y) + if a > 0 and shadeOf(r) >= 3 then dark[y * 8 + x] = true end + end + end + for i = 0, 7 do + for _, s in ipairs({ i, 56 + i, i * 8, i * 8 + 7 }) do + if not dark[s] and not reach[s] then + reach[s] = true + stack[#stack + 1] = s + end + end + end + while #stack > 0 do + local p = table.remove(stack) + local px, py = p % 8, math.floor(p / 8) + for _, d in ipairs(DIRS4) do + local nx, ny = px + d[1], py + d[2] + if nx >= 0 and nx < 8 and ny >= 0 and ny < 8 then + local ni = ny * 8 + nx + if not dark[ni] and not reach[ni] then + reach[ni] = true + stack[#stack + 1] = ni + end + end + end + end + mask = {} + for i = 0, 63 do mask[i] = dark[i] or not reach[i] end + end for y = 0, 7 do for x = 0, 7 do local r, g, b, a = frame:getPixel(x, y) - local col = shades and shades[shadeOf(r)] - if col and a > 0 then r, g, b = col[1], col[2], col[3] end - out:setPixel(dx + x, dy + y, r, g, b, a) + if mask and not mask[y * 8 + x] then + out:setPixel(dx + x, dy + y, 0, 0, 0, 0) + else + local col = shades and shades[shadeOf(r)] + if col and a > 0 then r, g, b = col[1], col[2], col[3] end + out:setPixel(dx + x, dy + y, r, g, b, a) + end end end end @@ -427,6 +479,28 @@ local function newEntry(map, base, baked) end end end + -- a frame-animated tile that resolved to the `flower` class stands as + -- a 1px billboard (Structures.buildFlowers), and its slot in THIS + -- copy carries only each frame's dark tones with the rest keyed to + -- alpha -- see patch(). Gated on the resolved shape so a profile that + -- pins the tile to something solid keeps a fully opaque slot, and on + -- the tileset art being readable: without pixels Structures cannot + -- have stood the billboard up (or synthesized the ground), and an + -- alpha-keyed slot under an ordinary flat quad is a hole in the world. + for _, spec in ipairs(specs) do + if spec.kind == "frames" then + local okCut, isFlower = pcall(function() + local okA, art = pcall(Assets.imageData, tileset.image) + if not (okA and art and art.getPixel) then return false end + local sh = V.require("TileShape").forMap(map)[spec.tile] + return sh ~= nil and sh.class == "flower" + end) + if okCut and isFlower then + e.cut = e.cut or {} + e.cut[spec.tile] = true + end + end + end return e end) return (ok and entry) or false diff --git a/lib/TileShape.lua b/lib/TileShape.lua index 00deba6..5384105 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -60,6 +60,7 @@ local FALLBACK_HEIGHTS = { signpost = 16, post = 16, grass = 0, + flower = 0, -- interior furniture: face-on drawings the detector would otherwise -- raise to wall height (or merge into the wall). A bed is drawn from -- above and lies low; tables and desks are boxes at their real height; @@ -110,6 +111,11 @@ local ART = { signpost = "billboard", post = "post", grass = "grass", + -- animated flowers: flat synthesized ground PLUS a standing cutout of + -- the drawing's darkest tones, one voxel deep (see Structures' + -- buildFlowers). Height 0 so a build with no pixel access degrades to + -- the flat tile it always drew, not a box + flower = "flower", -- furniture: a bed's art depicts its top surface; tables and desks are -- boxes whose fronts fold up (the mesher's authored-fold rule). -- Stools, `prop` and `cutout` are standee pools alongside `billboard` @@ -188,9 +194,11 @@ end local function shapeFor(class, heights, authored) return { class = class, h = heights[class] or 0, art = ART[class] or "upright", - -- grass draws its flat ground base like any walkable tile; the - -- standing tuft rows are additive geometry from Structures - flat = ART[class] == "flat" or class == "grass", + -- grass and flowers draw a flat ground base like any walkable + -- tile; the standing tufts and cutouts are additive geometry + -- from Structures + flat = ART[class] == "flat" or class == "grass" + or class == "flower", authored = authored or false } end @@ -210,6 +218,28 @@ function TileShape.forMap(map) local count = math.floor((tileset.imageWidth or 128) / 8) * math.floor((tileset.imageHeight or 48) / 8) + -- derived pin: a tile the tileset animates by FRAME REWRITE (the + -- overworld's flower) is already named by its animation spec, so like + -- tall grass it needs no profile entry anywhere. Hand-authoring still + -- wins -- a mod animating a wall tile this way keeps its wall by + -- listing it. Guarded because the spec seam is engine data a stub map + -- may not carry. + local flowerTiles = {} + do + local ok, declared = pcall(function() + if tileset.animatedTiles then return tileset.animatedTiles end + local TileRenderer = require("src.render.TileRenderer") + return TileRenderer.defaultAnimatedTiles(tileset) + end) + if ok then + for _, spec in ipairs(type(declared) == "table" and declared or {}) do + if spec.kind == "frames" and spec.tile then + flowerTiles[spec.tile] = true + end + end + end + end + local shapes = { classes = {} } for class in pairs(FALLBACK_HEIGHTS) do shapes.classes[class] = shapeFor(class, heights) @@ -222,6 +252,8 @@ function TileShape.forMap(map) -- derived pin: every tileset already names its tall-grass tile, so -- the standing-tuft treatment needs no profile entry anywhere shapes[t] = shapeFor("grass", heights, true) + elseif flowerTiles[t] then + shapes[t] = shapeFor("flower", heights, true) elseif map.waterTiles and map.waterTiles[t] then shapes[t] = shapes.classes.water elseif map.walkable and map.walkable[t] then diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua index cc752fd..5b2320b 100644 --- a/tests/dramatic_shape_test.lua +++ b/tests/dramatic_shape_test.lua @@ -420,6 +420,98 @@ T.check(okClock and type(clockVal) == "number", "a clock that throws falls back to a working one rather than propagating") TileRenderer.animFrame = nil +-- ------- the flower's slot carries an animated SILHOUETTE, not a tile +-- +-- The flower tile stands in voxel mode as a billboard one voxel deep +-- (Structures.buildFlowers), cut to the drawing's darkest tones. Meshes +-- are static, so the geometry spans the union of every frame's dark +-- pixels and the animation lives in the atlas: patch() keys everything +-- lighter to alpha 0, the shader discards it, and the silhouette trims +-- itself frame by frame. Two halves to pin down: the CLASS is derived +-- (any frames-animated tile resolves `flower` with no profile entry), +-- and the PATCH writes alpha where the frame is not dark. + +local TileShape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape") + +local flowerSet = { + id = "T_FLOWER_PIN", image = "assets/tilesets/stub.png", + tilesPerRow = 16, imageWidth = 128, imageHeight = 48, + animatedTiles = { { tile = 0x03, kind = "frames", period = 20, + images = { "stub_flowerframe.png" }, + sequence = { 1 } } }, +} +local flowerShapes = TileShape.forMap({ tileset = flowerSet }) +T.eq(flowerShapes[0x03].class, "flower", + "a frames-animated tile is pinned `flower` with no profile entry, like grass") +T.check(flowerShapes[0x03].flat, + "the flower cell still counts as flat ground for its neighbours") +T.eq(flowerShapes[0x03].h, 0, + "and carries no height, so a build with no pixel access degrades to the flat tile") +T.check(flowerShapes[0x03].authored, + "the pin is authored-strength: cell walkability cannot re-file it as plain ground") + +-- the patch, observed through a recording atlas copy: a crafted frame +-- whose dark pixels form a diamond ring around one light pixel -- the +-- billboard must keep the ring AND the pale pixel it encloses, and key +-- the reachable background (light or transparent) to alpha +local slotPx = {} +local sectionNewImageData = love.image.newImageData +do + local crafted = fakePixels() + local ring = { ["1,0"] = true, ["0,1"] = true, + ["2,1"] = true, ["1,2"] = true } + local frame = fakePixels() + function frame:getPixel(x, y) + if ring[x .. "," .. y] then return 0.3, 0.3, 0.3, 1 end -- dark outline + if x == 7 and y == 0 then return 0.9, 0.9, 0.9, 0 end -- transparent + return 1, 1, 1, 1 -- light: petal inside at (1,1), + end -- background everywhere else + love.image.newImageData = function(a, b) + if type(a) == "number" then + local d = fakePixels(a, b) + function d:setPixel(x, y, r, g, b2, al) + slotPx[x .. "," .. y] = { r, g, b2, al } + end + return d + end + if tostring(a):find("flowerframe") then return frame end + return crafted + end +end + +TerrainAtlas.invalidate() +local fmap = { + id = "T_FLOWER_PATCH_MAP", + tileset = { + id = "T_FLOWER_PATCH", image = "assets/tilesets/stub2.png", + tilesPerRow = 16, imageWidth = 128, imageHeight = 48, + animatedTiles = { { tile = 0x03, kind = "frames", period = 20, + images = { "stub_flowerframe.png" }, + sequence = { 1 } } }, + }, + renderer = { image = base }, +} +local okFlower, flowerImg = pcall(TerrainAtlas.animate, fmap, nil, base, false) +T.check(okFlower and flowerImg ~= nil, + "a flower map animates: " .. tostring(flowerImg)) + +local fdx = (0x03 % 16) * 8 +local function slotAlpha(x, y) + local p = slotPx[(fdx + x) .. "," .. y] + return p and p[4] +end +T.eq(slotAlpha(1, 0), 1, + "a dark frame pixel lands opaque -- it is the standing cutout") +T.eq(slotAlpha(1, 1), 1, + "and so does the pale pixel the outline encloses -- the petal's inside " + .. "rides the billboard, not just its outline") +T.eq(slotAlpha(5, 5), 0, + "a background pixel is keyed to alpha, so the billboard's shader discards it") +T.eq(slotAlpha(7, 0), 0, + "a transparent frame pixel stays clear rather than painting black") + +love.image.newImageData = sectionNewImageData + TerrainAtlas.invalidate() love.image, love.graphics.newImage = realImage, realNewImage diff --git a/tests/voxel_anim_probe.lua b/tests/voxel_anim_probe.lua index 5b6c5ff..fd926d7 100644 --- a/tests/voxel_anim_probe.lua +++ b/tests/voxel_anim_probe.lua @@ -117,12 +117,26 @@ return function(game) end -- one animation period is 20 logic frames at 60Hz, and TileRenderer.tick - -- runs on wall-clock steps, so ~22 rendered frames lands the next step + -- runs on wall-clock steps, so ~22 rendered frames lands the next step. + -- animFrame is an OPTIONAL engine seam this build may not export; the + -- mod's own clock (TerrainAtlas._animFrame) reads the same counter by + -- whatever route exists, so ask it first. + local function clock() + if TerrainAtlas and TerrainAtlas._animFrame then + local ok, f = pcall(TerrainAtlas._animFrame) + if ok and type(f) == "number" then return f end + end + if TileRenderer.animFrame then + local ok, f = pcall(TileRenderer.animFrame) + if ok and type(f) == "number" then return f end + end + return -1 + end for i = 1, shots do game.capturePath = ("%s/anim_%s_L%d_%02d.png"):format(DIR, mapId, level, i) wait(3) print(("[anim] shot %d at animFrame %d step %d"):format( - i, TileRenderer.animFrame(), math.floor(TileRenderer.animFrame() / 20) % 8)) + i, clock(), math.floor(clock() / 20) % 8)) dumpAtlas(tostring(i)) wait(22) end From cd3c4300aea6a15d2756c7a73d6a70b5b37cc349 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 00:00:48 -0400 Subject: [PATCH 02/16] fix shoreline slits beside claimed tiles and dropped edge-row facades Two voxel-mode holes, both in ChunkMesher's keep/emit rules: 1. A tile claimed by a standing object (building footprint, sign standee, pond bushes) painted only its flat synthesized ground and never emitted side faces, so beside recessed water the 2px step was an open slit into the sky. The skip branch now emits the same below-ground side bands ordinary ground does, cut from the synthesized ground's art. 2. A profiled building whose front row is the map's last tile row has its facade exactly on the shared boundary plane (Saffron's row houses seen from Route 6), where the strict overBody test and the closed neighbour mask dropped it. Outward-facing quads on the body's boundary planes are now kept, told apart from ring scraps -- the thing the mask exists to kill -- by their winding. --- CHANGELOG.md | 28 ++++++++++++++++++++++ lib/ChunkMesher.lua | 58 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b4294..7086592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## Unreleased + +### Fixed + +- **The shoreline no longer opens into the sky beside buildings and + signs.** Water recesses 2px below the ground, and the ground tile beside + it closes the step with a small below-ground side band -- but a tile + CLAIMED by a standing object (a building footprint, a sign standee, the + bushes ringing Fuchsia's ponds) only painted its synthesized flat ground + and never emitted sides. Along every stretch where such a tile met + water, the two-pixel step was an open slit straight through to the sky + behind the mesh. The skip branch now emits the same below-ground bands + ordinary ground does, cut from the synthesized ground's own art, so the + shoreline lip is continuous whatever stands on the bank. + +- **Edge-row buildings keep their facades.** The south wall of Saffron's + row houses -- profiled buildings whose front row is the map's last tile + row -- lies exactly on the boundary plane shared with Route 6, and the + prebuilt-quad keep rules dropped it: the strict body test excludes the + plane, and the closed neighbour mask (which exists to kill ring scraps + whose rects sit exactly on that line) swallowed what was left, so from + Route 6 the houses stood hollow. The two cases are geometrically + identical degenerate rects, but they FACE opposite ways: a face pointing + away from the body is this map's own facade and nothing in the + neighbour will ever draw that plane, while a face pointing into the + body is the scrap the mask is for. The mesher now reads the winding and + keeps outward faces on the body's boundary planes, on all four edges. + ## 1.0.3 ### Added diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index 77c2d4b..eab718f 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -430,7 +430,36 @@ local function runGeometry(map, bodyOnly, masks, sink) if s and S.skip[k] then -- an object stands here; paint its synthesized ground and let the -- prebuilt prism quads (appended below) carry the art - if S.ground[k] then topQuad(tx * 8, ty * 8, 0, S.ground[k], 1) end + local g = S.ground[k] + if g then + topQuad(tx * 8, ty * 8, 0, g, 1) + -- the claimed tile is still ground at height 0, and water next + -- door still recesses below it: without the same below-ground + -- side bands ordinary ground emits, the two-pixel shoreline + -- face is a slit into the sky behind the mesh -- which is + -- exactly what a building plot or a sign standing at the + -- waterline showed. Same bands, cut from the synthesized + -- ground's own art + for _, side in ipairs(SIDES) do + local nh = heightAt(tx + side[1], ty + side[2]) + if nh < 0 then + local d = side[3] + local lat = LATERAL[d] + local hl = lat and heightAt(tx + lat[1], ty + lat[2]) or 0 + local hr = lat and heightAt(tx + lat[3], ty + lat[4]) or 0 + for band = math.floor(nh / 8), -1 do + local y0 = math.max(nh, band * 8) + local y1 = math.min(0, band * 8 + 8) + if y1 > y0 then + sideQuad(d, tx * 8, ty * 8, y0, y1, g, + (band * 8 + 8) - y1, (band * 8 + 8) - y0, + sideShades(hl, hr, y0, y1, y0 <= nh, + Voxel3D.FACE_SHADE[d])) + end + end + end + end + end elseif s then local run = S.runs[k] local h = run and run.h or s.h @@ -610,6 +639,31 @@ local function runGeometry(map, bodyOnly, masks, sink) return overBody or not maskedClosed(x0, z0, x1, z1) end + -- A face lying EXACTLY on a body boundary plane is ambiguous to the + -- rect tests above: a body structure's outward facade (a Saffron row + -- house whose front row is the map's last row, its south wall on the + -- shared plane with Route 6) and the inward face of a ring scrap + -- occupy the same degenerate rect, and the strict overBody plus the + -- closed mask dropped BOTH -- which is why those facades were missing. + -- The winding tells them apart: a face pointing AWAY from the body + -- belongs to this map's own edge-row structure and nothing in the + -- neighbour will ever draw that plane, so it stays; a face pointing + -- INTO the body is the scrap the mask rules exist to kill, and falls + -- through to them. + local function outwardOnEdge(q, x0, z0, x1, z1) + if z0 == z1 and (z0 == 0 or z0 == bh) and x1 > 0 and x0 < bw then + local nz = (q[2][1] - q[1][1]) * (q[3][2] - q[1][2]) + - (q[2][2] - q[1][2]) * (q[3][1] - q[1][1]) + return (z0 == bh and nz > 0) or (z0 == 0 and nz < 0) + end + if x0 == x1 and (x0 == 0 or x0 == bw) and z1 > 0 and z0 < bh then + local nx = (q[2][2] - q[1][2]) * (q[3][3] - q[1][3]) + - (q[2][3] - q[1][3]) * (q[3][2] - q[1][2]) + return (x0 == bw and nx > 0) or (x0 == 0 and nx < 0) + end + return false + end + local scUV = { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } } local function quadUV(q) if q.uv then return q.uv end @@ -625,7 +679,7 @@ local function runGeometry(map, bodyOnly, masks, sink) local x1 = math.max(q[1][1], q[2][1], q[3][1], q[4][1]) local z0 = math.min(q[1][3], q[2][3], q[3][3], q[4][3]) local z1 = math.max(q[1][3], q[2][3], q[3][3], q[4][3]) - if keepQuad(x0, z0, x1, z1) then + if outwardOnEdge(q, x0, z0, x1, z1) or keepQuad(x0, z0, x1, z1) then push({ q[1], q[2], q[3], q[4] }, quadUV(q), groundShades(q, q.shade)) end end From dcf078c2e014a1ae43491aaa0c65c2c96b74ef09 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 00:11:06 -0400 Subject: [PATCH 03/16] 1.0.4: flower/bush/statue/trash-can billboards, in-place mesh refresh Flowers stand as 1px animated cutouts on their own pulled mesh (own-cell flowers hide behind the walker, the cell south still overdraws feet). Cuttable bushes, gym statues (single-block plinth) and Lt. Surge's trash cans stand as 5-voxel prop cutouts; prop_ground names the tile painted beneath. Block edits refresh meshes in place instead of blinking to 2D. --- CHANGELOG.md | 104 +++++++++++++++++++++++++++ data/voxel_heights.lua | 39 ++++++++++ lib/ChunkMesher.lua | 129 ++++++++++++++++++++++++++++------ lib/Structures.lua | 41 ++++++++++- lib/VoxelScene.lua | 25 +++++++ main.lua | 8 ++- manifest.json | 2 +- tests/dramatic_shape_test.lua | 13 +++- tests/voxel_anim_probe.lua | 3 + 9 files changed, 334 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b4294..7acff95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,109 @@ # Changelog +## 1.0.4 + +### Added + +- **Flowers stand up, and keep swaying.** The animated meadow tile + ($03, the one tile the overworld animates by frame rewrite) now + renders as a billboard one voxel deep: the drawing's darkest tones + plus everything they enclose are cut out per pixel, and the ground + beneath is synthesized from the commonest flat neighbour, exactly + like the ground under a detected prop. + + The interesting part is that the cutout still animates. A mesh is + static, so the geometry spans the UNION of the mask over the base art + and all three animation frames, and the animation lives entirely in + the texture: TerrainAtlas already rewrites the flower's slot in the + private animated atlas each step, and for this tile it now writes + only the current frame's mask opaque with everything else keyed to + alpha 0 -- which the voxel shader discards, and the shadow pass with + it. The standing silhouette trims itself frame by frame in texture + space, off the same engine clock as the flat path, without a vertex + moving. The class is derived, not authored: any frames-animated tile + resolves to the new `flower` class with no profile entry, the same + way tall grass derives from `grassTile` (hand-authoring still wins). + +- **The cuttable bush is a standing cutout.** The four tiles Cut + deletes ($2D/$2E/$3D/$3E -- across the whole tileset they appear only + in the five cut-tree blocks) are pinned to the thin `prop` pool: a + per-pixel standee 5 voxels deep, black-outline segmented with its + enclosed pixels kept, the drawn grass dither flooding away. It + stands on plain grass ($2C) -- the very tile Cut leaves behind per + field.cutTreeSwaps -- via the profile's new `prop_ground` key, which + names the tile painted under a pinned prop instead of whatever flat + tile its neighbours vote in. + +- **Gym statues: a solid plinth, a standing bird.** The statue pair + flanking every badge gym's aisle (and Bruno's room) is one cell of + figure over one cell of plinth. The plinth ($22/$23/$32/$33) is + pinned `wall`: a solid 16px block. The figure ($02/$38/$12/$13) is + pinned `prop`: a 5-voxel cutout that stands ON the plinth through the + authored-box support rule, its checkered background flooded away and + the pixels its outline encloses kept. + + The whole statue keeps ONE cell of footprint. The support rule used + to extend the box under the claimed cell (the monitor-on-desk path), + which marched the plinth a second block backwards; a figure whose + support is a FULL-HEIGHT block now collapses instead -- the drawn + figure cell becomes synthesized floor, since the block below already + carries the whole base. Furniture supports keep the extension: their + drawn cell is the furniture's own upper rows, and floor there would + amputate the desk. The round boulder drawn beside some statues is + deliberately NOT pinned -- it also tiles wall-to-wall as Pewter's + rock rows, which are scenery for the detector. + +- **Lt. Surge's trash cans stand up.** The can ($0B/$0C/$1B/$1C, the + lone graphic of blocks 38/39) takes the same treatment as the + cuttable bush: a 5-voxel `prop` cutout, black-outline segmented with + its enclosed pixels kept, standing on the gyms' main floor tile + ($11) via `prop_ground`. + +### Fixed + +- **Cut trees now vanish in voxel mode -- and grow back.** The + engine's Cut path swapped the block with a raw `setBlock` + renderer + rebuild, never emitting `world.block_replaced` -- so this mod's + listener (which rebuilds the map's mesh exactly for this) never + heard about it, and the diorama kept showing the tree. The engine + now routes Cut through `replaceBlock` + (src/world/OverworldController.lua), whose whole purpose -- per its + own comment, "Victory Road barriers, Cut trees" -- is that same swap + plus the event. The regrowth path had the same hole one door away: + cut trees are restored block by block when the map is re-entered, + and the card-key doors are stamped closed on floor load, both + through the same silent `setBlock` -- with the mesh cache staying + warm across a round trip (that is what prevLive is for), the world + kept showing the stump you left. Both paths now announce each block. + +- **A block edit no longer blinks the world down to 2D.** The + listener used to drop the edited map's mesh outright, and mesh + builds are asynchronous -- so cutting a tree (or stepping out of a + door onto a map whose trees just regrew) flashed the flat 2D world + for the frames the rebuild took. `ChunkMesher.refresh` rebuilds in + place instead: the stale mesh keeps drawing, the replacement cooks + in the background, and each slot swaps as its build lands -- the + tree pops out (or back in) with the scene never leaving 3D. + +- **Flowers cull the player correctly from every angle.** The flower + billboards were baked into the terrain mesh, which draws without + the characters' camera-ward pull -- so a walker standing among + flowers won the depth test against ALL of them, including the + flower south of their feet that should overdraw them. The flower + quads now ride their own mesh, drawn after the characters with + exactly the characters' pull (the tall-grass trick): the flower in + front of a walker occludes their feet, the one behind them hides, + at every camera angle. Unlike grass the flower mesh still casts + shadows -- it is a handful of cutouts per meadow, not thousands of + tufts. + +- The `voxel_anim_probe` driver crashed on engine builds without the + optional `TileRenderer.animFrame` seam, and again on tilesets whose + animation list carries a "toggle" entry (spinner rooms), which claims + a tile LIST rather than one slot. It now reads the clock through the + mod's own fallback chain and skips toggle entries in the placement + census. + ## 1.0.3 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 2757fa9..5fe4b67 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -65,6 +65,11 @@ -- monitor on its desk). prop is a second pool -- so two touching cutouts stay separate -- +-- A tileset entry may also carry `prop_ground` (not a class): a map of +-- prop tile id -> ground tile id naming the tile painted under that +-- pinned prop, overriding the flat-neighbour vote -- the cuttable bush +-- stands on the grass Cut leaves behind, whatever borders it. +-- -- 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, -- and no single class covers that. They live in the `buildings` list at @@ -124,6 +129,40 @@ return { -- fence-textured tower. `post` extracts each cell alone, so these -- render as the same thin posts, marching north post = { 14, 85 }, + -- the cuttable bush ($2D/$2E/$3D/$3E, the four tiles Cut deletes + -- -- across the whole tileset they appear only in the five + -- cut-tree blocks): a standing per-pixel cutout 5 voxels deep, + -- black-outline segmented with the pixels the outline encloses + -- kept, its drawn grass dither flooding away as background + prop = { 45, 46, 61, 62 }, + -- the ground painted under those pinned props, by the prop tile's + -- own id: the bush stands on plain grass ($2C) -- the very tile + -- Cut leaves behind (field.cutTreeSwaps' after-blocks) -- rather + -- than whatever flat tile its neighbours vote + prop_ground = { [45] = 44, [46] = 44, [61] = 44, [62] = 44 }, + }, + + -- The badge gyms and Bruno's room (one tileset): the bird statues + -- that flank every gym's aisles, and Lt. Surge's trash cans. A + -- statue is one cell of figure ($02/$38/$12/$13) drawn over one + -- cell of plinth ($22/$23/$32/$33); the trash can is one cell + -- ($0B/$0C/$1B/$1C, blocks 38/39 only). Across the whole blockset + -- none of these tiles appears anywhere else. The plinth stays a + -- SOLID 16px block; the figure stands ON it as a per-pixel cutout + -- 5 voxels deep (the thin `prop` pool), collapsing to the plinth's + -- single cell of footprint (Structures' wall-support rule); the + -- can is the same 5-voxel cutout at floor level. Both are + -- black-outline segmented -- backgrounds flood away, the pixels + -- the outline encloses stay. Everything stands on the gyms' main + -- floor tile ($11), which is also Bruno's floor. + -- (The round boulder drawn beside some statues, $07/$08/$17/$18, + -- is NOT pinned: it also tiles wall-to-wall as Pewter's rock rows, + -- which are scenery for the detector, not a lone prop.) + GYM = { + wall = { 34, 35, 50, 51 }, + prop = { 2, 56, 18, 19, 11, 12, 27, 28 }, + prop_ground = { [2] = 17, [56] = 17, [18] = 17, [19] = 17, + [11] = 17, [12] = 17, [27] = 17, [28] = 17 }, }, -- ledge pairs from data.field.tilePairs diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index 77c2d4b..f742a43 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -704,15 +704,10 @@ function ChunkMesher.build(map, bodyOnly, masks) return sink.finish() end --- The tall-grass rows as their own mesh: VoxelScene draws it AFTER the --- characters so the southern row of a grass cell still overdraws a --- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary --- terrain could never do this). -local function buildGrassMesh(map) - local S = Structures.forMap(map) - if #S.grassQuads == 0 then return nil end +local function quadsMesh(quads) + if #quads == 0 then return nil end local verts, indices, n = {}, {}, 0 - for _, q in ipairs(S.grassQuads) do + for _, q in ipairs(quads) do for i = 1, 4 do local c = q[i] local uv = q.uv and q.uv[i] or { q.u, q.v } @@ -724,6 +719,33 @@ local function buildGrassMesh(map) return Voxel3D.newMesh(verts, indices) end +-- The tall-grass rows as their own mesh: VoxelScene draws it AFTER the +-- characters so the southern row of a grass cell still overdraws a +-- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary +-- terrain could never do this). +local function buildGrassMesh(map) + return quadsMesh(Structures.forMap(map).grassQuads) +end + +-- The flower billboards as their own mesh, for the same reason as the +-- grass one: it draws AFTER the characters WITH the same camera-ward +-- pull, so a flower south of a walker occludes their feet and one north +-- of them hides behind them. Baked into the terrain mesh they lost that +-- depth fight against the pulled character card whenever the player +-- stood among flowers. Unlike grass this mesh still CASTS shadows (the +-- sun pass draws it): a handful of flowers per meadow, not thousands of +-- tufts. +local function buildFlowerMesh(map) + return quadsMesh(Structures.forMap(map).flowerQuads) +end + +-- Replace a cached slot, releasing whatever mesh it held. +local function swapSlot(c, slot, mesh) + local old = c[slot] + if old and old ~= mesh and old.release then pcall(old.release, old) end + c[slot] = mesh +end + -- ------------------------------------------------------------- the cache local function entry(id) @@ -736,11 +758,12 @@ local function entry(id) end local function releaseEntry(c) - for _, slot in ipairs({ "full", "body", "grass" }) do + for _, slot in ipairs({ "full", "body", "grass", "flowers" }) do local mesh = c[slot] if mesh and mesh.release then pcall(mesh.release, mesh) end c[slot] = nil end + c.stale = nil end -- ---------------------------------------------------------- async builds @@ -778,26 +801,48 @@ end local function runJob(job) local map = job.map local c = entry(job.id) - if c.grass == nil then + if c.grass == nil or c.flowers == nil or (c.stale and c.stale.aux) then local okG, grass = pcall(buildGrassMesh, map) - if (gen[job.id] or 0) ~= job.gen then return end - c.grass = (okG and grass) or false + local okF, flowers = pcall(buildFlowerMesh, map) + if (gen[job.id] or 0) ~= job.gen then + if okG and grass and grass.release then pcall(grass.release, grass) end + if okF and flowers and flowers.release then + pcall(flowers.release, flowers) + end + return + end + swapSlot(c, "grass", (okG and grass) or false) + swapSlot(c, "flowers", (okF and flowers) or false) + if c.stale then c.stale.aux = nil end end local sink = newSink() runGeometry(map, job.slot == "body", job.masks, sink) local mesh = sink.finish() - if (gen[job.id] or 0) ~= job.gen then return end - c[job.slot] = mesh or false + if (gen[job.id] or 0) ~= job.gen then + if mesh and mesh.release then pcall(mesh.release, mesh) end + return + end + swapSlot(c, job.slot, mesh or false) + if c.stale then + c.stale[job.slot] = nil + if not (c.stale.full or c.stale.body or c.stale.aux) then + c.stale = nil + end + end end -- Queue a build unless the slot is already cached or queued. Returns the -- cached mesh when there is one (false-cached misses return nil). -- `urgent` marks the current map's meshes: pump() gives those a bigger --- slice and runs them before neighbour jobs. +-- slice and runs them before neighbour jobs. A slot refresh() marked +-- stale queues its rebuild AND keeps handing back the old mesh, so a +-- one-block edit never drops the scene to the flat 2D path while the +-- replacement cooks. function ChunkMesher.request(map, bodyOnly, masks, urgent) local slot = bodyOnly and "body" or "full" local c = cache[map.id] - if c and c[slot] ~= nil then return c[slot] or nil end + local stale = c and c.stale and (c.stale[slot] or c.stale.aux) + if c and c[slot] ~= nil and not stale then return c[slot] or nil end local key = jobKey(map.id, slot) local job = jobIndex[key] if not job then @@ -808,7 +853,7 @@ function ChunkMesher.request(map, bodyOnly, masks, urgent) elseif urgent then job.urgent = true end - return nil + return (c and c[slot]) or nil end function ChunkMesher.pending() @@ -871,17 +916,26 @@ end function ChunkMesher.get(map, bodyOnly, masks) local slot = bodyOnly and "body" or "full" local c = entry(map.id) - if c.grass == nil then + if c.grass == nil or c.flowers == nil or (c.stale and c.stale.aux) then local okG, grass = pcall(buildGrassMesh, map) - c.grass = (okG and grass) or false + local okF, flowers = pcall(buildFlowerMesh, map) + swapSlot(c, "grass", (okG and grass) or false) + swapSlot(c, "flowers", (okF and flowers) or false) + if c.stale then c.stale.aux = nil end end - if c[slot] == nil then + if c[slot] == nil or (c.stale and c.stale[slot]) then local ok, mesh = pcall(ChunkMesher.build, map, bodyOnly, masks) if not ok then print("[warn] voxel mesh build failed for " .. tostring(map.id) .. ": " .. tostring(mesh)) end - c[slot] = (ok and mesh) or false + swapSlot(c, slot, (ok and mesh) or false) + if c.stale then + c.stale[slot] = nil + if not (c.stale.full or c.stale.body or c.stale.aux) then + c.stale = nil + end + end local key = jobKey(map.id, slot) local job = jobIndex[key] if job then finishJob(job, true) end @@ -901,6 +955,39 @@ function ChunkMesher.grass(map) return c and c.grass or nil end +function ChunkMesher.flowers(map) + local c = cache[map.id] + return c and c.flowers or nil +end + +-- Rebuild a map's meshes IN PLACE: the stale meshes keep drawing while +-- replacements cook, and each slot swaps as its build lands. This is +-- the block-edit path (a cut tree, a door stamp) -- invalidate() drops +-- the mesh outright, and until the async rebuild landed the scene fell +-- to the flat 2D path, a whole-world blink for a one-block edit. +function ChunkMesher.refresh(mapId) + if not mapId then return ChunkMesher.invalidate() end + local c = cache[mapId] + -- nothing drawable cached: the plain drop costs nothing visible + if not (c and (c.full or c.body)) then + return ChunkMesher.invalidate(mapId) + end + Structures.invalidate(mapId) + gen[mapId] = (gen[mapId] or 0) + 1 + for i = #jobs, 1, -1 do + local job = jobs[i] + if job.id == mapId then + jobIndex[jobKey(job.id, job.slot)] = nil + table.remove(jobs, i) + end + end + -- false-cached slots count as stale too: a retry after a failed build + -- is exactly a rebuild + c.stale = { aux = true, + full = (c.full ~= nil) or nil, + body = (c.body ~= nil) or nil } +end + -- Evict everything outside `live` (a set of map ids): far maps' meshes -- are released -- GPU buffer and LOVE's CPU copy both -- and their -- Structures analysis dropped. The live set is the current map plus its diff --git a/lib/Structures.lua b/lib/Structures.lua index c262841..127d3c0 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -193,7 +193,7 @@ function Structures.forMap(map) -- terrain.) S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def), runs = {}, skip = {}, ground = {}, objectQuads = {}, - grassQuads = {}, roundStamps = {} } + grassQuads = {}, flowerQuads = {}, roundStamps = {} } Buildings.build(S, map, pixels(tileset), perRow) -- Fold doors into their buildings. A door cell is WALKABLE (the player @@ -418,6 +418,27 @@ function Structures.forMap(map) Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data) end + -- ---- authored ground under pinned props ---- + -- The profile can name the tile a pinned prop stands on (a tileset + -- entry's prop_ground: prop tile id -> ground tile id), overriding + -- the neighbour vote. The cuttable bush stands on the plain grass + -- Cut itself leaves behind, not on whatever path its neighbours + -- happen to vote in. + do + local okP, prof = pcall(V.data, "voxel_heights") + local entry = okP and type(prof) == "table" and prof.tilesets + and prof.tilesets[tileset.id] + local pg = entry and entry.prop_ground + if type(pg) == "table" then + for k, skipped in pairs(S.skip) do + if skipped then + local g = pg[S.tileAt[k]] + if g then S.ground[k] = g end + end + end + end + end + -- unresolved claimed ground (a hull with no art match, headless -- cylinders): no flat neighbour to vote with, so fall back to the -- map's commonest ground tile @@ -1847,7 +1868,17 @@ function Structures.buildObject(S, map, region, cluster, end for _, c in ipairs(cluster.tiles) do local k = keyOf(c[1], c[2]) - if support then + if support and support.class == "wall" then + -- a figure drawn above a FULL-HEIGHT block (the gym statue on its + -- plinth) is a statue on a pillar with ONE cell of footprint: the + -- block below already carries the whole base, so the drawn cell + -- becomes synthesized floor rather than a second block marching + -- the base backwards. Furniture supports (a monitor on its desk) + -- keep the box-extension below -- their drawn cell is the + -- furniture's own upper rows, and floor there would amputate it. + S.skip[k] = true + S.ground[k] = best + elseif support then -- the claimed tile keeps rendering as the box the prop stands on, -- wearing the art its own ROW would have without the drawing (the -- trim row stays trim); only when the whole row is the prop does @@ -2130,7 +2161,11 @@ end function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data) local templates = {} - local quads = S.objectQuads + -- flowerQuads, not objectQuads: flowers sit on WALKABLE cells, so + -- their mesh draws after the characters with the character pull + -- (ChunkMesher's flower mesh) -- terrain-baked they lose the depth + -- fight against the pulled card whenever the player stands among them + local quads = S.flowerQuads for ty = y0, y1 do for tx = x0, x1 do Budget.tick() diff --git a/lib/VoxelScene.lua b/lib/VoxelScene.lua index c4d0742..d8b584b 100644 --- a/lib/VoxelScene.lua +++ b/lib/VoxelScene.lua @@ -460,6 +460,14 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh, ShadowMap.draw(nbMesh[i], atlasFor(nb.map), Mat4.translate(nb.ox, 0, nb.oy)) end + -- flower billboards live outside the terrain mesh (they draw after the + -- characters, pulled -- see render), but the sun still sees them: a + -- handful of cutouts per meadow, unlike the grass left out below + ShadowMap.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil) + for _, nb in ipairs(state.neighbors or {}) do + ShadowMap.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map), + Mat4.translate(nb.ox, 0, nb.oy)) + end for _, p in ipairs(posed) do local def = p.sprite.def local frame, mirror = frameFor(def, p.facing, p.phase, p.flip) @@ -559,6 +567,23 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor) Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map), Mat4.translate(nb.ox, 0, nb.oy), pull) end + -- flower billboards: pulled like the characters and the grass, MINUS + -- the depth of 8 world pixels along the view (8 sin a -- the camera + -- looks along (0, -cos a, -sin a), so that is exactly one tile row of + -- northness). A pure depth handicap with zero screen drift: every + -- flower is judged as if it stood one tile row further north. The + -- character card's feet plane sits at its cell's MIDDLE (py + 8), so + -- a flower on the walker's own cell (z +4 or +12 across the cell) + -- lands behind the card and the player obscures the patch they stand + -- ON, while the nearest flower of the cell south (+20) stays in front + -- and keeps overdrawing their feet. + local fpull = math.max(0, pull - 8 * math.sin(math.max(Voxel.angle, 0.05))) + Voxel3D.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil, + fpull) + for _, nb in ipairs(state.neighbors or {}) do + Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map), + Mat4.translate(nb.ox, 0, nb.oy), fpull) + end return Voxel3D.endScene() end diff --git a/main.lua b/main.lua index 94f7fd8..9b01be9 100644 --- a/main.lua +++ b/main.lua @@ -344,9 +344,13 @@ end) -- tree that is no longer there. The 2D tile renderer invalidates its own -- caches off the same edit. +-- refresh, not invalidate: the stale mesh keeps drawing while the +-- replacement builds in the background, so a one-block edit (Cut, a +-- door stamp, the tree regrowing on re-entry) repopulates in place +-- instead of blinking the whole scene down to the flat 2D path mod.events:on("world.block_replaced", function(payload) local mapId = payload and (payload.mapId or (payload.map and payload.map.id)) - if mapId then ChunkMesher.invalidate(mapId) end + if mapId then ChunkMesher.refresh(mapId) end end) -- A reloaded map is rebuilt from scratch (warps that re-enter the same map, @@ -372,7 +376,7 @@ mod.events:on("map.reloaded", function(payload) if mapId then ChunkMesher.invalidate(mapId) end end) -mod.exports.version = "1.0.3" +mod.exports.version = "1.0.4" -- exposed so a companion mod can pin its own tiles' shapes or read the -- camera without reaching into this mod's file layout mod.exports.lib = V diff --git a/manifest.json b/manifest.json index be6dca3..fa53bbb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "DRAMATIC_SHAPE", "name": "Dramatic Shape Voxel Mod", - "version": "1.0.3", + "version": "1.0.4", "api": 2, "entry": "main.lua", "profile": "content", diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua index 5b2320b..5447507 100644 --- a/tests/dramatic_shape_test.lua +++ b/tests/dramatic_shape_test.lua @@ -535,8 +535,10 @@ local ChunkMesher = run.loader.exports.DRAMATIC_SHAPE.lib.require("ChunkMesher") local Runtime = require("src.mods.Runtime") local realInvalidate = ChunkMesher.invalidate -local dropped = {} +local realRefresh = ChunkMesher.refresh +local dropped, refreshed = {}, {} ChunkMesher.invalidate = function(id) dropped[#dropped + 1] = id or "" end +ChunkMesher.refresh = function(id) refreshed[#refreshed + 1] = id or "" end Runtime.emit("map.reloaded", { mapId = "PALLET_TOWN", reason = "colors" }) T.eq(#dropped, 0, @@ -551,11 +553,16 @@ T.eq(dropped[1], "PALLET_TOWN", "and drops exactly the map that reloaded") Runtime.emit("map.reloaded", { mapId = "VIRIDIAN_CITY" }) T.eq(#dropped, 2, "a reload with no stated reason is treated as a real one") --- and the edits that genuinely change geometry are untouched by any of this +-- a block edit (Cut, a door stamp, the tree regrowing on re-entry) is a +-- REFRESH, not a drop: the stale mesh keeps drawing while the rebuild +-- cooks, so the scene never blinks down to the flat 2D path Runtime.emit("world.block_replaced", { mapId = "PALLET_TOWN" }) -T.eq(#dropped, 3, "a replaced block still drops the mesh it changed") +T.eq(#dropped, 2, "a replaced block does not drop the mesh outright") +T.eq(#refreshed, 1, "it refreshes the mesh in place instead") +T.eq(refreshed[1], "PALLET_TOWN", "and refreshes exactly the edited map") ChunkMesher.invalidate = realInvalidate +ChunkMesher.refresh = realRefresh -- ------- the non-colour palette modes must not come through as SGB -- diff --git a/tests/voxel_anim_probe.lua b/tests/voxel_anim_probe.lua index fd926d7..3ff4475 100644 --- a/tests/voxel_anim_probe.lua +++ b/tests/voxel_anim_probe.lua @@ -94,6 +94,8 @@ return function(game) if S then local def = ow.map.def for _, spec in ipairs(specs) do + if not spec.tile then goto continue end -- "toggle" specs claim + -- tile LISTS, not one slot local flat, prop, run, first = 0, 0, 0, nil for ty = 0, def.height * 4 - 1 do for tx = 0, def.width * 4 - 1 do @@ -112,6 +114,7 @@ return function(game) .. " %d inside a prop%s"):format(spec.tile, flat, run, prop, first and (" -- first at cell " .. math.floor(first[1] / 2) .. "," .. math.floor(first[2] / 2)) or "")) + ::continue:: end end end From 5a94dfe85a7a96293beb7d9e1f1f6b0809ca07fe Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 00:21:52 -0400 Subject: [PATCH 04/16] keep edge-row buildings' eave overhangs across the seam A body-anchored building's eave juts frontEave voxels past the map boundary; the neighbour-body mask read that overhang as a ring scrap and opened the roof rim into the sky from across the seam. Building placements only scan the body, so their quads now carry an `own` flag the edge keep-rules never touch. --- CHANGELOG.md | 10 ++++++++++ lib/Buildings.lua | 7 +++++++ lib/ChunkMesher.lua | 8 +++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af8005..242e5ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,16 @@ body is the scrap the mask is for. The mesher now reads the winding and keeps outward faces on the body's boundary planes, on all four edges. + The roof RIM had the same problem one step further out: an edge-row + house's eave overhangs `frontEave` voxels PAST the boundary plane into + the neighbour's airspace, and those quads are neither on the plane + (the winding rescue) nor over the body -- the neighbour-body mask ate + them as ring scraps, so from across the seam the roof edge was open + sky at low camera angles. Building placements only ever scan the map + BODY, so every building quad is this map's own structure by + construction: they now carry an `own` flag the edge keep-rules never + touch. + ## 1.0.3 ### Added diff --git a/lib/Buildings.lua b/lib/Buildings.lua index 11ad190..050a312 100644 --- a/lib/Buildings.lua +++ b/lib/Buildings.lua @@ -669,6 +669,13 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh) { q[3][1] + mx, q[3][2], q[3][3] + mz }, { q[4][1] + mx, q[4][2], q[4][3] + mz }, uv = q.uv, shade = q.shade, + -- placements only ever scan the BODY, so a building is always this + -- map's own structure: the mesher's edge keep-rules must not eat + -- the parts that poke past the boundary (an edge-row house's eave + -- juts frontEave voxels into the neighbour's airspace, and the + -- neighbour-body mask read that overhang as a ring scrap -- which + -- opened the roof rim into the sky from across the seam) + own = true, } end end diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index abd5d12..1f22c02 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -679,7 +679,13 @@ local function runGeometry(map, bodyOnly, masks, sink) local x1 = math.max(q[1][1], q[2][1], q[3][1], q[4][1]) local z0 = math.min(q[1][3], q[2][3], q[3][3], q[4][3]) local z1 = math.max(q[1][3], q[2][3], q[3][3], q[4][3]) - if outwardOnEdge(q, x0, z0, x1, z1) or keepQuad(x0, z0, x1, z1) then + -- q.own: a body-anchored structure's own quad (a building placed by + -- Buildings.build, whose scan never leaves the body). Exempt from + -- the edge keep-rules entirely: its eave legitimately overhangs the + -- boundary plane into the neighbour's airspace, and no variant of + -- the neighbour will ever draw that geometry + if q.own or outwardOnEdge(q, x0, z0, x1, z1) + or keepQuad(x0, z0, x1, z1) then push({ q[1], q[2], q[3], q[4] }, quadUV(q), groundShades(q, q.shade)) end end From da0fc491768f1300d3b93eb68b9419fb6127c0b9 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 00:37:56 -0400 Subject: [PATCH 05/16] level the cliff mounds: pin the east slope chain, region-adopt doorways The cliff drawing's dark east slope ($02/$24/$34) broke the repeat scan at its corners and rose to 32px towers; a folded doorway column read its own drawn extent and towered a block over the 16px mound around Diglett's Cave. The slope chain is pinned to one 16px course, and a doorway column now takes height AND top from its region: roofed among drawn facades (houses), flat among flat repeats (the mound). Adds the voxel_mound_probe driver that prints per-column detector decisions. --- CHANGELOG.md | 19 +++++++++++ data/voxel_heights.lua | 9 +++++ lib/Structures.lua | 41 ++++++++++++++++++++--- tests/voxel_mound_probe.lua | 65 +++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 tests/voxel_mound_probe.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 242e5ff..1df9b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,25 @@ construction: they now carry an `own` flag the edge keep-rules never touch. +- **Diglett's Cave mounds (and cliffs everywhere) stop sprouting + towers.** Two detector misreadings stacked up on the cave-entrance + mound. The dark east slope of the cliff drawing ($02/$24/$34) is one + texture repeated over the mound's whole height, but its corner tiles + break the repeat scan, so those columns rose to 32px -- the rock + pillar beside the entrance. And a folded doorway column reads its own + drawn extent (the door plus everything above it), which is a house's + real height when the door is a house's, but a 32px tower over a 16px + plateau when the door is a cave mouth -- the entrance jumped a block + above the mound around it. The slope chain is now profile-pinned to + one 16px course, and a doorway column answers to its REGION entirely: + height from the region's dominant column, top flat when those columns + are flat repeats (the mound) and roofed when they are drawn facades + (a house). Both cave entrances -- and every cliff built from the same + slope tiles -- now read as one level mesa with the cave mouth at + ground level. A new `voxel_mound_probe` driver prints the detector's + per-column class and height over any rectangle, which is how this was + diagnosed. + ## 1.0.3 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 5fe4b67..a0a3e39 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -129,6 +129,15 @@ return { -- fence-textured tower. `post` extracts each cell alone, so these -- render as the same thin posts, marching north post = { 14, 85 }, + -- the cliff-mound's dark east slope and its corners ($02 the NE + -- corner, $24 the slope column, $34 the slope-to-rim seat). Their + -- drawn runs span the whole mound drawing, so the detector raised + -- them to 32px towers -- the rock pillar beside Diglett's Cave -- + -- and the doorway column, which adopts its REGION's height, + -- inherited the same 32 and put the cave entrance a block above + -- the mound around it. Pinned to one 16px course they match the + -- plateau body, and the doorway drops with them. + wall = { 2, 36, 52 }, -- the cuttable bush ($2D/$2E/$3D/$3E, the four tiles Cut deletes -- -- across the whole tileset they appear only in the five -- cut-tree blocks): a standing per-pixel cutout 5 voxels deep, diff --git a/lib/Structures.lua b/lib/Structures.lua index 127d3c0..d55ee24 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -192,7 +192,7 @@ function Structures.forMap(map) -- still overdraws a walker's feet even though characters stamp over -- terrain.) S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def), - runs = {}, skip = {}, ground = {}, objectQuads = {}, + runs = {}, skip = {}, ground = {}, doorFold = {}, objectQuads = {}, grassQuads = {}, flowerQuads = {}, roundStamps = {} } Buildings.build(S, map, pixels(tileset), perRow) @@ -213,7 +213,12 @@ function Structures.forMap(map) if ns and ns.art == "upright" then for dy = 0, 1 do for dx = 0, 1 do - shapeAt[keyOf(cx * 2 + dx, cy * 2 + dy)] = shapes.classes.wall + local dk = keyOf(cx * 2 + dx, cy * 2 + dy) + shapeAt[dk] = shapes.classes.wall + -- remembered for buildVolume: a folded doorway column + -- answers to its REGION for height and top, not to its + -- own drawn extent (see the door adoption there) + S.doorFold[dk] = true end end end @@ -1314,6 +1319,7 @@ function Structures.buildVolume(S, map, tiles) local runs = {} local heightVotes = {} + local repeatVotes = {} for tx, ys in pairs(cols) do -- visit each contiguous vertical run in this column local sorted = {} @@ -1345,11 +1351,19 @@ function Structures.buildVolume(S, map, tiles) end end end + local isDoor = false + for ty = north, front do + if S.doorFold[keyOf(tx, ty)] then + isDoor = true + break + end + end local run = { front = front, north = north, extent = extent, - unit = unit, fromRepeat = repeatRead } + unit = unit, fromRepeat = repeatRead, door = isDoor } runs[#runs + 1] = { tx = tx, run = run } local h = unit * 8 heightVotes[h] = (heightVotes[h] or 0) + 1 + if repeatRead then repeatVotes[h] = (repeatVotes[h] or 0) + 1 end end end @@ -1361,11 +1375,27 @@ function Structures.buildVolume(S, map, tiles) for h, n in pairs(heightVotes) do if n > modeN or (n == modeN and h > modeH) then modeH, modeN = h, n end end + -- whether the region's dominant columns are flat repeats (a cliff + -- mound's plateau) rather than drawn facades (a house's front) + local modeRepeat = (repeatVotes[modeH] or 0) * 2 > modeN for _, r in ipairs(runs) do local run = r.run local h = run.unit * 8 local adopted = false - if run.fromRepeat and modeH > h then + local flatDoor = false + if run.door then + -- A folded doorway column answers to its region ENTIRELY. Its own + -- reading spans the door plus everything drawn above it -- a + -- house's full height when the door is a house's, but a 32px + -- tower over a 16px plateau when the door is a cave mouth cut + -- into a cliff mound (Diglett's Cave: the entrance jumped a block + -- above the mound around it). Height and top both come from the + -- region: the mode height, roofed like a facade when the mode + -- columns are drawn facades, flat when they are flat repeats. + h = modeH + adopted = not modeRepeat + flatDoor = modeRepeat + elseif run.fromRepeat and modeH > h then h = modeH adopted = true end @@ -1383,7 +1413,8 @@ function Structures.buildVolume(S, map, tiles) -- whole roof area -- and a rooftop tilted into a 48px ramp reads -- wrong instantly. Distinct top rows -> slope; repeated -> level top. local roofRows = 0 - if S.outdoor and (not run.fromRepeat or adopted) and h >= 16 then + if S.outdoor and (not run.fromRepeat or adopted) and h >= 16 + and not flatDoor then roofRows = math.min(2, math.floor(h / 8) - 1) if roofRows > 0 and map:tileAt(r.tx, run.north) == map:tileAt(r.tx, run.north + 1) then diff --git a/tests/voxel_mound_probe.lua b/tests/voxel_mound_probe.lua new file mode 100644 index 0000000..3bbcb2a --- /dev/null +++ b/tests/voxel_mound_probe.lua @@ -0,0 +1,65 @@ +-- Driver: what did the detector decide over a rectangle of tiles? +-- +-- Prints, for every tile of MOUND_RECT on MOUND_MAP, the resolved shape +-- class and the volume run height Structures gave it -- the ground truth +-- for diagnosing cliff/mound columns that extrude to the wrong height +-- (Diglett's Cave entrance ridge, bug of 2026-07-27). Then a shot. +-- +-- POKEPORT_DRIVER=mods/DRAMATIC_SHAPE/tests/voxel_mound_probe.lua lovec . +-- +-- knobs (env): +-- MOUND_MAP map id (default ROUTE_2) +-- MOUND_RECT "tx0,ty0,tx1,ty1" in tiles (default 16,12,35,23) +-- MOUND_SPOT "x,y[,facing]" player cell (default 12,12,up) +-- SHOT_DIR output directory, must exist (default "shots") +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local DIR = os.getenv("SHOT_DIR") or "shots" + local mapId = os.getenv("MOUND_MAP") or "ROUTE_2" + local rx0, ry0, rx1, ry1 = (os.getenv("MOUND_RECT") or "16,12,35,23") + :match("^(%-?%d+),(%-?%d+),(%-?%d+),(%-?%d+)$") + rx0, ry0, rx1, ry1 = tonumber(rx0), tonumber(ry0), tonumber(rx1), tonumber(ry1) + local sx, sy, facing = (os.getenv("MOUND_SPOT") or "12,12,up") + :match("^%s*(%d+)%s*,%s*(%d+)%s*,?%s*(%a*)") + facing = (facing ~= "" and facing) or "up" + + local Zoom = require("src.render.Zoom") + Zoom.reset() + Pipelines.setLevel("tiltshift", 0) + U.teleport(game, mapId, tonumber(sx), tonumber(sy), facing) + U.wait(20) + Pipelines.setLevel("voxel", 3) + U.wait(30) + + local V = game.mods.exports["DRAMATIC_SHAPE"] + V = V and V.lib + local Structures = V and V.require("Structures") + local ow = game.overworld + if Structures and ow and ow.map then + local S = Structures.forMap(ow.map) + local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end + print(("[mound] %s tiles (%d,%d)-(%d,%d): tile/class/h per column") + :format(mapId, rx0, ry0, rx1, ry1)) + for ty = ry0, ry1 do + local row = {} + for tx = rx0, rx1 do + local k = keyOf(tx, ty) + local s = S.shapeAt[k] + local run = S.runs[k] + local h = run and run.h or (s and s.h) or 0 + if S.skip[k] then h = 0 end + row[#row + 1] = ("%02X%s%02d"):format(S.tileAt[k] or 0xFF, + s and s.class:sub(1, 1) or "?", h) + end + print("[mound] " .. table.concat(row, " ")) + end + end + + game.capturePath = ("%s/mound_%s.png"):format(DIR, mapId) + U.wait(5) + Pipelines.setLevel("voxel", 0) + U.wait(5) + print("[mound] done") +end From cbcffeea08875eee63f5596854279a5e6cd2e9c7 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 00:43:18 -0400 Subject: [PATCH 06/16] read repeats through a one-row trim foot: no more cliff fins The volume repeat scan anchors at a column's front tile; a plateau column ending in a rounded corner tile ($13/$35) never matched and read its whole capped extent -- 48px fins and tents out of the 16px mesas on Routes 3 and 4. When the two rows directly above the front are identical, the column is that repeat wearing a trim foot: unit is one course plus the trim. Doorway columns still answer to their region first. --- lib/Structures.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/Structures.lua b/lib/Structures.lua index d55ee24..858f636 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -1350,6 +1350,21 @@ function Structures.buildVolume(S, map, tiles) break end end + -- A one-row TRIM at the column's foot hides a repeat from the + -- scan above, which anchors at the front tile: a cliff plateau + -- ends its south edge in a rounded corner tile, the corner + -- never recurs, and the column read its whole capped extent -- + -- a 48px fin (or a whole tent of them) sticking out of a 16px + -- mesa on Routes 3 and 4. When the two rows directly above the + -- front are IDENTICAL, the column is that repeat wearing a trim + -- foot: one course plus the trim is its drawn unit. Doorway + -- columns are untouched -- their run answers to the region + -- (see below) before the unit matters. + if not repeatRead and extent > 2 + and map:tileAt(tx, front - 1) == map:tileAt(tx, front - 2) then + unit = 2 + repeatRead = true + end end local isDoor = false for ty = north, front do From c8a432ee86eb3a01f4da44a0cf00dc77d8f49a69 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 00:43:46 -0400 Subject: [PATCH 07/16] changelog: the trim-foot repeat fix --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df9b24..700c59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,15 @@ per-column class and height over any rectangle, which is how this was diagnosed. + Routes 3 and 4 had a third variant of the same misreading: the repeat + scan anchors at a column's FRONT tile, and a plateau column that ends + in a one-off rounded corner tile ($13/$35) never matched -- it read + its whole capped extent and shot up as a 48px fin (several together + made a tent). When the two rows directly above the front are + identical, the column is now read as that repeat wearing a trim foot: + its unit is one course plus the trim. Doorway columns still answer to + their region first, so houses are untouched. + ## 1.0.3 ### Added From 7b727aa68406dfd47c3b8a9a4240235d37cba4e5 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 01:01:10 -0400 Subject: [PATCH 08/16] furnish the Poke Marts to the finished interiors' standard The MART tileset shares the Center's atlas but not its pins, so marts were raw detector output: 32px walls, the racks fused into one deep monolith, the clerk booth a 48px slab. Pinned like the references -- 16px wall band with the built-in cases, bookcase-collapsed racks, half-cell counter with the poster on top, register standing on it. --- CHANGELOG.md | 14 ++++++++++++++ data/voxel_heights.lua | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700c59e..59541dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,20 @@ its enclosed pixels kept, standing on the gyms' main floor tile ($11) via `prop_ground`. +- **The Poke Marts furnished to the Center's standard.** The MART + tileset shares the Center's atlas image but is its own id, so none + of the Center's pins applied, and every mart was raw detector + output: a 32px double-height display band for a back wall, the two + shelf racks fused into one four-tile-deep monolith, and the clerk's + booth towered into a 48px slab wearing the juice poster. Pinned the + way the finished interiors are -- the back wall's SALE cases and + drink fridges one 16px face like the Center's healing consoles, the + racks collapsed to one-cell-deep shelves at drawn height like Red's + bookcases, the counter half a cell with the poster riding its top + like the nurse's tray, and the cash register standing ON the counter + through the authored-box support rule. One 4x4 layout serves every + city, so this covers all eight marts. + ### Fixed - **Cut trees now vanish in voxel mode -- and grow back.** The diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index a0a3e39..a8e477e 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -357,6 +357,38 @@ return { -- accepted trade for a real plant silhouette prop = { 32, 33, 34, 35, 48, 49, 50, 51 }, }, + + -- The Poke Marts (one 4x4 layout serves every city; the tileset + -- shares the Center's atlas image but is its own id, so the pins + -- above do not carry over). Same failures as everywhere indoors: + -- the detector towered the whole north display band to 32px, merged + -- the clerk's booth into a 48px slab wearing the juice poster, and + -- boxed the two free-standing shelf racks into one 4-tile-deep + -- monolith. + MART = { + -- the wall band stays one 16px face: the trim and case tops + -- (40/90/91), the SALE cases (78/79) with their feet (23/29), and + -- the glass drink fridges (44-47) with their feet (62/63) -- all + -- drawn built INTO the back wall, exactly like the Center's + -- healing consoles + wall = { 23, 29, 40, 44, 45, 46, 47, 62, 63, 78, 79, 90, 91 }, + -- the clerk's counter, half a cell high like every service + -- counter: the south front (24/25), the east-facing display run + -- (16/41/89 panels, 14/15/30/31 the juice-poster case whose art + -- rides the top like the nurse's tray), and the dark section + -- beside the register (56, near-black -- pinned or the void rule + -- flattens it) + counter = { 14, 15, 16, 24, 25, 30, 31, 41, 56, 89 }, + -- the cash register stands ON the pinned counter through the + -- authored-box support rule, black-outline segmented + billboard = { 8 }, + -- the free-standing shelf racks: TALL drawings, not deep ones -- + -- each rank collapses onto a one-cell-deep shelf at its drawn + -- height (the Dojo/Red's-house treatment). 64/65/67 and 80/81/83 + -- are the bottle rows the clerk's booth also wears as its top + -- display; 68/69/71 and 84/85/87 the goods rows below + bookcase = { 64, 65, 67, 68, 69, 71, 80, 81, 83, 84, 85, 87 }, + }, }, -- Buildings whose whole sprite is voxelized band by band (lib/Buildings.lua, From d94b4dcf75a3c18becced9665e92f7a08c31aba4 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 01:30:40 -0400 Subject: [PATCH 09/16] grow Viridian Forest real trees, stumps and signs The forest was raw detector output: ragged mixed-height tree boxes with 0px rim collars, stump rows fused into crate walls, and signs broken by the $32 water-fallback trap. Every tile of the tree drawing and the stumps now takes the per-cell voxel hull the border forest wears (a tree's four quarter-hulls tile into one lumpy canopy), signs take the signpost slab, and the white filler is flat ground. --- CHANGELOG.md | 18 ++++++++++++++++++ data/voxel_heights.lua | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59541dc..074c15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ ### Added +- **Viridian Forest grows real trees.** Nearly everything drawn in the + forest is ROUND, and the detector was boxing all of it: the big trees + came out as ragged mixed-height volumes (their sparse canopy-rim + tiles read 0px against 32px bodies, leaving gap-toothed hedge walls), + the stump rows merged into 16px crate walls wearing folded stump art, + and the trail signs were broken piles -- their $32 tile is the + water-fallback trap and recessed into a pond lip in the middle of the + woods. All of it is now profile-pinned to the treatments the rest of + the world already uses: every tile of the tree drawing (ball, rim + wisps, feet) and the stumps take the per-cell voxel HULL the overworld + border forest wears -- a tree spans 2x2 cells, so its four + quarter-hulls tile into one big lumpy canopy, and each stump becomes a + round bollard; the signs take the standing thin-slab `signpost` + treatment every town sign gets; and the white sparkle filler inside + the tree masses is flat ground instead of an invisible zero-height + box. The whole map now resolves to hulls, signs, ledges and ground -- + a detector sweep finds no stray boxed column anywhere. + - **Flowers stand up, and keep swaying.** The animated meadow tile ($03, the one tile the overworld animates by frame rewrite) now renders as a billboard one voxel deep: the drawing's darkest tones diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index a8e477e..16b3352 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -178,8 +178,33 @@ return { CAVERN = { ledge = { 5 }, }, + -- Viridian Forest. Nearly everything drawn here is ROUND, and the + -- detector was boxing all of it: the big trees came out as ragged + -- mixed-height volumes (their sparse canopy-edge tiles read 0px, + -- their bodies 32px), the stump rows merged into 16px crate walls + -- wearing folded stump art, and the trail sign was a broken pile -- + -- its $32 tile is the water-fallback trap and recessed into a pond + -- lip in the middle of the woods. FOREST = { ledge = { 46 }, + -- the big trees -- the ball ($05/$06/$15/$16/$25/$26), its rim + -- wisps ($04/$07/$17/$23/$24/$27) and its feet ($35/$36) -- and + -- the stumps ($02/$03/$12/$13): one voxel hull per 16x16 cell, + -- cut from the cell's own art, the treatment the overworld border + -- forest already wears. A tree spans 2x2 cells, so its four + -- quarter-hulls tile into one big lumpy canopy. Every tile of + -- the drawing is listed: the cell-rounding is per TILE, and a rim + -- tile left out keeps its detector reading -- which is exactly + -- the ragged 0/8/16/32px box collar the trees wore + cylinder = { 2, 3, 4, 5, 6, 7, 18, 19, 21, 22, 23, + 35, 36, 37, 38, 39, 53, 54 }, + -- the white sparkle filler inside the tree masses: flat, not an + -- invisible zero-height box + ground = { 0 }, + -- the trail signs: the standing thin-slab treatment every town + -- sign gets ($32 pinned is also what lifts it out of the water + -- trap) + signpost = { 33, 34, 49, 50 }, }, -- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's From 5712f854b539dc91f6e9591955ab1d22e39cfba5 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 01:43:08 -0400 Subject: [PATCH 10/16] one tall hull per forest tree; project stump ring faces onto round tops The trees' 2x2-cell drawings unfolded onto the ground as quarter-hull pancakes -- each tree read as cut in half. The carver is now parametric over its canvas: a `canopy` anchor tile carves the whole 2x2-cell drawing as ONE 32px hull, and stamps carry their footprint radius. The stumps' drawn tops are a cut face, not body: the `stump` class builds the hull from the bark rows and projects the ring ellipse across the round flat top, near arc south (stump_cap = the ellipse's drawn rows). --- CHANGELOG.md | 14 +++ data/voxel_heights.lua | 27 +++-- lib/ChunkMesher.lua | 14 +-- lib/Structures.lua | 232 +++++++++++++++++++++++++++++++---------- lib/TileShape.lua | 11 ++ 5 files changed, 229 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074c15e..9fcdf78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,20 @@ box. The whole map now resolves to hulls, signs, ledges and ground -- a detector sweep finds no stray boxed column anywhere. + Two refinements over the first cut, both new hull-builder abilities. + A tree's drawing spans 2x2 CELLS, and per-cell hulls unfolded it onto + the ground -- the ball's top half sat one cell north of its bottom + half at the same elevation, reading as a tree cut in half. The new + `canopy` class pins the drawing's corner tile as a group anchor and + the whole 2x2-cell drawing carves as ONE 32px hull, so every tree is + a single tall round canopy (the carver is now parametric over its + canvas size, and hull stamps carry their footprint radius). And the + stumps' drawn tops are a CUT FACE -- an ellipse of growth rings seen + at an angle, not body: the new `stump` class builds the hull from the + bark rows alone and projects the ellipse across the round flat top, + near arc to the south (`stump_cap` names the ellipse's drawn height), + so the rings ride the round part in perspective. + - **Flowers stand up, and keep swaying.** The animated meadow tile ($03, the one tile the overworld animates by frame rewrite) now renders as a billboard one voxel deep: the drawing's darkest tones diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 16b3352..622c56a 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -187,17 +187,24 @@ return { -- lip in the middle of the woods. FOREST = { ledge = { 46 }, - -- the big trees -- the ball ($05/$06/$15/$16/$25/$26), its rim - -- wisps ($04/$07/$17/$23/$24/$27) and its feet ($35/$36) -- and - -- the stumps ($02/$03/$12/$13): one voxel hull per 16x16 cell, - -- cut from the cell's own art, the treatment the overworld border - -- forest already wears. A tree spans 2x2 cells, so its four - -- quarter-hulls tile into one big lumpy canopy. Every tile of - -- the drawing is listed: the cell-rounding is per TILE, and a rim - -- tile left out keeps its detector reading -- which is exactly - -- the ragged 0/8/16/32px box collar the trees wore - cylinder = { 2, 3, 4, 5, 6, 7, 18, 19, 21, 22, 23, + -- the big trees: the whole 2x2-CELL drawing carves as ONE 32px + -- voxel hull, so a tree is a single tall canopy instead of four + -- ground-level quarter-pancakes ("cut in half", as the first + -- attempt read). `canopy` pins the drawing's top-left corner + -- tile ($04) as the group anchor; every other tile of the + -- drawing stays `cylinder` so the group build can verify and + -- claim its cells -- and so a stray partial drawing degrades to + -- per-cell hulls rather than boxes + canopy = { 4 }, + cylinder = { 5, 6, 7, 21, 22, 23, 35, 36, 37, 38, 39, 53, 54 }, + -- the stumps ($02/$03/$12/$13): a hull whose drawn top is a CUT + -- FACE. The body builds from the bark rows alone, and the drawn + -- ellipse of growth rings projects onto the hull's round flat + -- top -- its bottom arc to the south, the way the 2D art means + -- it. stump_cap is the ellipse's height in art rows + stump = { 2, 3, 18, 19 }, + stump_cap = 7, -- the white sparkle filler inside the tree masses: flat, not an -- invisible zero-height box ground = { 0 }, diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index 1f22c02..f17ea51 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -703,15 +703,17 @@ local function runGeometry(map, bodyOnly, masks, sink) -- round-tree stamps: the shared hull template translated per cell, -- through reusable scratch corners so expansion allocates nothing. - -- A hull spans at most its own 16px cell, so one rect test usually - -- answers for the whole stamp: strictly interior stamps keep every - -- quad, ring stamps buried under a neighbour body (or, body-only, ring - -- stamps full stop) skip without touching their quads. Only stamps - -- crossing a boundary walk quad by quad. + -- A hull spans at most its own footprint -- one 16px cell unless the + -- stamp carries a wider radius (the 2x2-cell canopy groups) -- so one + -- rect test usually answers for the whole stamp: strictly interior + -- stamps keep every quad, ring stamps buried under a neighbour body + -- (or, body-only, ring stamps full stop) skip without touching their + -- quads. Only stamps crossing a boundary walk quad by quad. local sc = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 } } for _, st in ipairs(S.roundStamps or {}) do local mx, mz = st.mx, st.mz - local sx0, sz0, sx1, sz1 = mx - 8, mz - 8, mx + 8, mz + 8 + local sr = st.r or 8 + local sx0, sz0, sx1, sz1 = mx - sr, mz - sr, mx + sr, mz + sr local interior = sx0 > 0 and sx1 < bw and sz0 > 0 and sz1 < bh local overBody = sx1 > 0 and sx0 < bw and sz1 > 0 and sz0 < bh local keepAll, skipAll diff --git a/lib/Structures.lua b/lib/Structures.lua index 858f636..bd62a2d 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -508,15 +508,17 @@ end local ROUND_SHADE = { front = 1.0, back = 0.68, side = 0.78, top = 1.0, bottom = 0.55 } -local function roundTemplate(S, map, data, cx, cy, groundTiles) +local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows) + N = N or 16 -- art canvas: 16 = one cell, 32 = 2x2 cells + local N2 = N / 2 local perRow = map.tileset.tilesPerRow or 16 local atlasW = map.tileset.imageWidth or 128 local atlasH = map.tileset.imageHeight or 48 - -- cell-space art access (16x16, row 0 = top) + -- cell-space art access (NxN, row 0 = top), anchored at cell (cx, cy) local function tileOf(px, py) - return S.tileAt[keyOf(cx * 2 + (px >= 8 and 1 or 0), - cy * 2 + (py >= 8 and 1 or 0))] + return S.tileAt[keyOf(cx * 2 + math.floor(px / 8), + cy * 2 + math.floor(py / 8))] end local function texel(px, py) local tile = tileOf(px, py) @@ -524,18 +526,18 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) math.floor(tile / perRow) * 8 + py % 8 end - -- shade class of every cell pixel, indexed py * 16 + px + -- shade class of every canvas pixel, indexed py * N + px local cls = {} - for py = 0, 15 do - for px = 0, 15 do + for py = 0, N - 1 do + for px = 0, N - 1 do local ax, ay = texel(px, py) local r, g, b, a = data:getPixel(ax, ay) - cls[py * 16 + px] = a == 0 and "off" - or Structures.shadeClass(math.min(r, g, b)) + cls[py * N + px] = a == 0 and "off" + or Structures.shadeClass(math.min(r, g, b)) end end - -- 4-connected flood from the cell border through `passable` classes + -- 4-connected flood from the canvas border through `passable` classes local function floodOutside(passable) local out, stack = {}, {} local function seed(i) @@ -544,16 +546,16 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) stack[#stack + 1] = i end end - for i = 0, 15 do - seed(i); seed(240 + i); seed(i * 16); seed(i * 16 + 15) + for i = 0, N - 1 do + seed(i); seed(N * (N - 1) + i); seed(i * N); seed(i * N + N - 1) end while #stack > 0 do local i = table.remove(stack) - local px = i % 16 + local px = i % N if px > 0 then seed(i - 1) end - if px < 15 then seed(i + 1) end - if i >= 16 then seed(i - 16) end - if i < 240 then seed(i + 16) end + if px < N - 1 then seed(i + 1) end + if i >= N then seed(i - N) end + if i < N * (N - 1) then seed(i + N) end end return out end @@ -562,23 +564,48 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local out = floodOutside({ off = true, dark = true, light = true, white = true }) local mask, enclosed = {}, 0 - for i = 0, 255 do + for i = 0, N * N - 1 do if not out[i] then mask[i] = true if cls[i] ~= "black" then enclosed = enclosed + 1 end end end - if enclosed < 32 then + if enclosed < N * N / 8 then out = floodOutside({ off = true, light = true, white = true }) mask = {} - for i = 0, 255 do + for i = 0, N * N - 1 do if not out[i] and cls[i] ~= "off" then mask[i] = true end end end local any = nil - for i = 0, 255 do any = any or mask[i] end + for i = 0, N * N - 1 do any = any or mask[i] end if not any then return {} end + -- a CAPPED hull (the stump): the top capRows rows of the mask are the + -- drawn cut face -- a surface seen at an angle, not body. Strip them + -- from the mask and remember their art span; the top-face quads below + -- project that ellipse across the round cap. + local capY0, capY1 = nil, nil + if capRows and capRows > 0 then + local top = nil + for iy = 0, N - 1 do + for ix = 0, N - 1 do + if mask[iy * N + ix] then top = iy break end + end + if top then break end + end + if top then + capY0 = top + capY1 = math.min(top + capRows - 1, N - 2) + for iy = capY0, capY1 do + for ix = 0, N - 1 do mask[iy * N + ix] = nil end + end + any = nil + for i = 0, N * N - 1 do any = any or mask[i] end + if not any then return {} end + end + end + -- the ground the ball stands on: the drawing's own background names -- it. Score every flat ground tile the map places against the cell's -- unmasked light pixels and keep the closest -- mid-forest trees have @@ -593,9 +620,9 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local ox = (t % perRow) * 8 local oy = math.floor(t / perRow) * 8 local score, n = 0, 0 - for py = 0, 15 do - for px = 0, 15 do - local i = py * 16 + px + for py = 0, N - 1 do + for px = 0, N - 1 do + local i = py * N + px local c = cls[i] if not mask[i] and (c == "light" or c == "white") then local ax, ay = texel(px, py) @@ -618,10 +645,10 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local z0, z1, src = {}, {}, {} local loRow, hiRow = {}, {} local yBot = nil - for iy = 0, 15 do + for iy = 0, N - 1 do local lo, hi = nil, nil - for ix = 0, 15 do - if mask[iy * 16 + ix] then + for ix = 0, N - 1 do + if mask[iy * N + ix] then lo = lo or ix hi = ix end @@ -632,7 +659,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local c = (lo + hi + 1) / 2 local hw = (hi - lo + 1) / 2 for ix = lo, hi do - local i = iy * 16 + ix + local i = iy * N + ix if mask[i] then local dx = ix + 0.5 - c local n = 1 @@ -640,7 +667,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) n = math.max(1, math.floor(2 * math.sqrt(hw * hw - dx * dx) + 0.5)) end - z0[i] = math.floor(8 - n / 2 + 0.5) + z0[i] = math.floor(N2 - n / 2 + 0.5) z1[i] = z0[i] + n src[i] = iy end @@ -650,28 +677,46 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- foot: rows under the mask repeat the bottom row's discs, wearing the -- bottom row's (outline-dark) pixels - for iy = yBot + 1, 15 do + for iy = yBot + 1, N - 1 do loRow[iy], hiRow[iy] = loRow[yBot], hiRow[yBot] for ix = loRow[yBot], hiRow[yBot] do - local b = yBot * 16 + ix + local b = yBot * N + ix if z0[b] then - local i = iy * 16 + ix + local i = iy * N + ix z0[i], z1[i], src[i] = z0[b], z1[b], yBot end end end + -- the round cap's top row and z extent, for the stump's ring + -- projection below + local capTopRow, capZ0, capZ1 = nil, nil, nil + if capY0 then + for iy = 0, N - 1 do + if loRow[iy] then capTopRow = iy break end + end + if capTopRow then + for ix = loRow[capTopRow], hiRow[capTopRow] do + local i = capTopRow * N + ix + if z0[i] then + capZ0 = math.min(capZ0 or z0[i], z0[i]) + capZ1 = math.max(capZ1 or z1[i], z1[i]) + end + end + end + end + local function solidAt(ix, iy, iz) - if ix < 0 or ix > 15 or iy < 0 or iy > 15 then return false end - local i = iy * 16 + ix + if ix < 0 or ix > N - 1 or iy < 0 or iy > N - 1 then return false end + local i = iy * N + ix return z0[i] ~= nil and iz >= z0[i] and iz < z1[i] end -- cap interiors sample the canopy a couple of rows below the rim, -- skipping outline-dark pixels local function deepTexel(ix, iy) - for iy2 = iy + 2, math.min(15, iy + 4) do - local i = iy2 * 16 + ix + for iy2 = iy + 2, math.min(N - 1, iy + 4) do + local i = iy2 * N + ix if mask[i] and cls[i] ~= "black" then return texel(ix, iy2) end end return texel(ix, iy) @@ -683,12 +728,12 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- ball paints solid black the moment the camera turns. The foot rows -- stay dark on purpose: their whole source row is outline-black. local function sideTexel(ix, iy) - local r = src[iy * 16 + ix] + local r = src[iy * N + ix] local dir = ix + ix < loRow[iy] + hiRow[iy] and 1 or -1 for step = 0, 3 do local x2 = ix + dir * step - local i2 = r * 16 + x2 - if x2 < 0 or x2 > 15 or not mask[i2] then break end + local i2 = r * N + x2 + if x2 < 0 or x2 > N - 1 or not mask[i2] then break end if cls[i2] ~= "black" then return texel(x2, r) end end return texel(ix, r) @@ -696,20 +741,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local quads = {} - for iy = 0, 15 do + for iy = 0, N - 1 do if loRow[iy] then - local yB, yT = 15 - iy, 16 - iy + local yB, yT = N - 1 - iy, N - iy -- front and back: the drawing per-pixel, columns merged where they -- share a chord plane; a run never crosses the 8px atlas tile seam -- (its u range must interpolate inside one tile) local ix = loRow[iy] while ix <= hiRow[iy] do - local i = iy * 16 + ix + local i = iy * N + ix if z0[i] then local ix2 = ix while ix2 + 1 <= hiRow[iy] do - local j = iy * 16 + ix2 + 1 + local j = iy * N + ix2 + 1 if z0[j] == z0[i] and z1[j] == z1[i] and math.floor((ix2 + 1) / 8) == math.floor(ix / 8) then ix2 = ix2 + 1 @@ -721,8 +766,8 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local ax1 = (texel(ix2, src[i])) local u0, u1 = (ax0 + 0.05) / atlasW, (ax1 + 0.95) / atlasW local v0, v1 = (ay + 0.05) / atlasH, (ay + 0.95) / atlasH - local x0, x1 = ix - 8, ix2 - 7 - local zF, zB = z1[i] - 8, z0[i] - 8 + local x0, x1 = ix - N2, ix2 - N2 + 1 + local zF, zB = z1[i] - N2, z0[i] - N2 quads[#quads + 1] = { { x0, yB, zF }, { x1, yB, zF }, { x1, yT, zF }, { x0, yT, zF }, uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, @@ -742,11 +787,11 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- sides, steps, undersides: constant-texel quads over the z runs a -- neighbour doesn't cover for ix = loRow[iy], hiRow[iy] do - local i = iy * 16 + ix + local i = iy * N + ix if z0[i] then local ax, ay = texel(ix, src[i]) local u, v = (ax + 0.5) / atlasW, (ay + 0.5) / atlasH - local x0, x1 = ix - 8, ix - 7 + local x0, x1 = ix - N2, ix - N2 + 1 -- exposed z pieces against one neighbouring column local function pieces(nx, ny, emit) @@ -757,7 +802,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) while iz2 + 1 < z1[i] and not solidAt(nx, ny, iz2 + 1) do iz2 = iz2 + 1 end - emit(iz - 8, iz2 - 7, iz, iz2) + emit(iz - N2, iz2 - N2 + 1, iz, iz2) iz = iz2 + 1 else iz = iz + 1 @@ -786,7 +831,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) u = tu, v = tv, shade = ROUND_SHADE.top, } end - if izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then + if capTopRow and iy == capTopRow and capZ1 then + -- the CUT FACE (a capped hull's top): project the drawn + -- ellipse across the round cap voxel row by voxel row -- + -- its top arc at the cap's north rim, its bottom arc at + -- the south, the perspective the 2D art already implies + for iz = izA, izB do + local t = capZ1 - 1 > capZ0 + and (iz - capZ0) / (capZ1 - 1 - capZ0) or 0 + local ry = capY0 + math.floor(t * (capY1 - capY0) + 0.5) + local cax, cay = texel(ix, ry) + top(iz - N2, iz - N2 + 1, + (cax + 0.5) / atlasW, (cay + 0.5) / atlasH) + end + elseif izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then -- the dome cap: outline on the rim cells, canopy inside local du, dv = deepTexel(ix, iy) top(zA, zA + 1, u, v) @@ -796,7 +854,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) top(zA, zB, u, v) end end) - if iy < 15 then + if iy < N - 1 then pieces(ix, iy + 1, function(zA, zB) quads[#quads + 1] = { { x0, yB, zB }, { x1, yB, zB }, { x1, yB, zA }, { x0, yB, zA }, @@ -844,23 +902,91 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles) end local tsid = tostring(map.tileset.id or map.tileset.image or "?") + -- the stump class's drawn-ellipse height, hand-authored per tileset + -- (the profile's stump_cap, in art rows) + local stumpCap = 6 + do + local okP, prof = pcall(V.data, "voxel_heights") + local entry = okP and type(prof) == "table" and prof.tilesets + and prof.tilesets[map.tileset.id] + if entry and type(entry.stump_cap) == "number" then + stumpCap = entry.stump_cap + end + end + + -- cells consumed by a 2x2 `canopy` group; the scan runs north to + -- south, west to east, so an anchor always claims its partners + -- before they are visited + local grouped = {} for cy = math.floor(y0 / 2), math.floor(y1 / 2) do for cx = math.floor(x0 / 2), math.floor(x1 / 2) do Budget.tick() + local ckey = cy * 8192 + cx local k = keyOf(cx * 2, cy * 2) - local s = S.shapeAt[k] + local s = (not grouped[ckey]) and S.shapeAt[k] or nil local near = cx * 2 >= -ROUND_RING and cx * 2 < tw + ROUND_RING and cy * 2 >= -ROUND_RING and cy * 2 < th + ROUND_RING - if s and s.art == "cylinder" and near then + if s and s.art == "canopy" and near then + -- ONE 32px hull over the 2x2-cell drawing. The partner cells + -- must be round-pinned too, or the drawing is partial (a map + -- edit, a mod's stray anchor tile) and the anchor is left + -- alone rather than carved into a half-empty giant. + local whole = true + for _, d in ipairs({ { 1, 0 }, { 0, 1 }, { 1, 1 } }) do + local ps = S.shapeAt[keyOf((cx + d[1]) * 2, (cy + d[2]) * 2)] + if not (ps and (ps.art == "cylinder" or ps.art == "canopy")) then + whole = false + end + end + if whole then + local ground = false + if data then + local ids = {} + for dy = 0, 3 do + for dx = 0, 3 do + ids[#ids + 1] = S.tileAt[keyOf(cx * 2 + dx, cy * 2 + dy)] + end + end + local sig = tsid .. "|g32|" .. gsig .. "|" + .. table.concat(ids, ":") + local tpl = roundCache[sig] + if not tpl then + local tq, tbg = roundTemplate(S, map, data, cx, cy, + groundTiles, 32) + tpl = { quads = tq, bg = tbg } + roundCache[sig] = tpl + end + ground = tpl.bg or false + S.roundStamps[#S.roundStamps + 1] = + { quads = tpl.quads, mx = cx * 16 + 16, mz = cy * 16 + 16, + r = 16 } + end + for dy = 0, 3 do + for dx = 0, 3 do + local tk = keyOf(cx * 2 + dx, cy * 2 + dy) + S.skip[tk] = true + S.ground[tk] = ground + end + end + grouped[ckey + 1] = true + grouped[ckey + 8192] = true + grouped[ckey + 8193] = true + end + elseif s and s.art == "cylinder" and near then + -- a `stump`-class cell is the same hull with a cut face: its + -- top capRows of drawing project onto the round top + local cap = s.class == "stump" and stumpCap or nil local ground = false if data then - local sig = tsid .. "|" .. gsig .. "|" .. table.concat({ + local sig = tsid .. (cap and ("|c" .. cap) or "") .. "|" + .. gsig .. "|" .. table.concat({ S.tileAt[k], S.tileAt[keyOf(cx * 2 + 1, cy * 2)], S.tileAt[keyOf(cx * 2, cy * 2 + 1)], S.tileAt[keyOf(cx * 2 + 1, cy * 2 + 1)] }, ":") local tpl = roundCache[sig] if not tpl then - local tq, tbg = roundTemplate(S, map, data, cx, cy, groundTiles) + local tq, tbg = roundTemplate(S, map, data, cx, cy, + groundTiles, 16, cap) tpl = { quads = tq, bg = tbg } roundCache[sig] = tpl end diff --git a/lib/TileShape.lua b/lib/TileShape.lua index 5384105..c9b15fa 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -56,6 +56,15 @@ local FALLBACK_HEIGHTS = { tree = 16, roof = 28, cylinder = 16, + -- big round scenery: a 2x2-CELL drawing carved as ONE 32px voxel hull + -- (Viridian Forest's trees). The class pins only the drawing's + -- top-left corner tile; the other cells stay `cylinder` and are + -- claimed by the group build (see Structures.buildCylinders) + canopy = 32, + -- a cylinder hull whose drawn top is a CUT FACE (tree stumps): the + -- body builds from the bark rows and the drawn ellipse projects onto + -- the hull's round top + stump = 16, billboard = 16, signpost = 16, post = 16, @@ -104,6 +113,8 @@ local ART = { fence = "upright", sign = "upright", cylinder = "cylinder", + canopy = "canopy", + stump = "cylinder", billboard = "billboard", -- signposts share the billboard treatment but as their own pool at a -- 2-voxel depth: a sign is a thin plate on a stick, and the standard From 0ba9109e407669f07b351fc0eb47f42e57ec69c4 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 02:24:05 -0400 Subject: [PATCH 11/16] 1.0.5: furnish every remaining interior Extends the shape profile from ten tilesets to twenty-three -- 1,190 pinned tiles over ~130 maps -- surveyed against the standard the finished interiors set: one 16px wall band carrying its built-ins, half-cell counters, bookcase-collapsed shelving, thin-pool plant standees, objects riding the furniture they are drawn above. The recurring failures were rooms sunk into ponds by the three stale-cache water ids (collision is per cell, so one trapped tile sank its neighbours too), counters and cave edges towering to 32-48px, racks fusing into deep monoliths, and anything in a walkable cell -- stools, tables, staircases, gravestone arches -- vanishing into the floor. Also fixes the door fold overwriting profile pins: it replaced the resolved shape unconditionally, so stair pins on tiles their tileset lists in doorTiles (Celadon Mansion, Pokemon Mansion 3F) silently did nothing. --- CHANGELOG.md | 68 ++ data/voxel_heights.lua | 1402 +++++++++++++++++++++++++++++++++++++++- lib/Structures.lua | 21 +- main.lua | 2 +- manifest.json | 2 +- 5 files changed, 1482 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59541dc..5a50cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,73 @@ # Changelog +## 1.0.5 + +### Added + +- **Every remaining interior is furnished.** The profile covered ten + tilesets; it now covers twenty-three -- 1,190 pinned tiles across + `GATE`, `FOREST_GATE`, `LOBBY`, `MUSEUM`, `LAB`, `MANSION`, + `INTERIOR`, `CLUB`, `SHIP`, `SHIP_PORT`, `FACILITY`, `CEMETERY` and + `UNDERGROUND`, plus full entries for `CAVERN` and `GYM` which had only + stubs. Roughly 130 maps, surveyed against the standard the finished + interiors already set: one 16px wall band carrying whatever is drawn + built into it, half-cell counters so the drawn front folds up and the + top stays on top, `bookcase` collapse for free-standing shelves, + thin-pool standees for plants, and small objects riding the furniture + they are drawn above. + + What the detector was doing before, by way of what changed: + + - **Rooms with no walls.** Three tile ids (`$14`, `$32`, `$48`) are + claimed by the engine's water set in EVERY tileset, and collision is + per CELL, so one of them in a cell's bottom-left corner sank the + whole cell. `$32` draws both the route gates' wall base course and + every counter front, so all 25 gate maps were a checkered floor in a + moat; the same trap put ponds through two thirds of Seafoam B4F, + under every museum vitrine, along the S.S. Anne's wall corners and + across Silph Co 1F's lobby island. The set is wider than three ids + in practice -- `LAB`, `MANSION` and `INTERIOR` each hit six to nine + -- because the test is per cell, so an innocent tile sharing a cell + with a trapped one sinks with it and has to be pinned too. + - **Towers and fused monoliths.** Counters raised to 48px dragging + their wall band with them, merchandise racks fused sideways into + 32px blocks four tiles deep, cave shelf edges standing as 48px fins + beside a 16px band, the Vermilion liner folded upright into a lumpy + 48px slab. + - **Furniture that was not there at all.** Anything whose cell is + walkable resolved to flat ground: 59 department-store stools, every + gate lounge table and pair of binoculars, both museum staircases, + the gym-lounge chairs, and -- via the void rule -- the black + partition walls every gym is divided by, which left their white rim + columns standing as hollow 48px fins. + - **314 gravestones** in Pokemon Tower were 8px stubs, because the + volume path measured only their bottom row and dropped the arch. + + Notable readings: the S.S. Anne's hull is `roof` (a drawing seen from + above, so the art belongs on the top face); the Warden's specimens and + Celadon Gym's shrubs are `cylinder` voxel balls, the first indoor use + of that class; the Fan Club's octagonal boardroom table is `counter` + rather than `table`, because only a counter rides its upper rows onto + the top face in drawn order -- which is what draws the seated chairman + exactly once, the Pokemon Center couch case verbatim. + + Fuchsia Gym's invisible maze is deliberately left flat. Raising it + would read better as a room, and would also hand the player the + solution; a shape is purely presentational, so the drawn answer wins + and the gym plays as the flat game does. + +### Fixed + +- **A profile pin now outranks the door fold.** `Structures.forMap` + folds a door cell into its facade so the doorway does not punch a hole + in the wall -- but it overwrote the resolved shape unconditionally, + including for AUTHORED tiles, which contradicts rule 1 of the + documented resolution order. Any pin on a tile its tileset also lists + in `doorTiles` was dead on arrival: all four Celadon Mansion + staircases and Pokemon Mansion 3F's descent are door tiles, so + `stair_*` pins there silently did nothing and the flights stayed + painted flat on the floor. The fold now skips authored tiles. + ## 1.0.4 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index a8e477e..c16a706 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -166,20 +166,257 @@ return { -- floor tile ($11), which is also Bruno's floor. -- (The round boulder drawn beside some statues, $07/$08/$17/$18, -- is NOT pinned: it also tiles wall-to-wall as Pewter's rock rows, - -- which are scenery for the detector, not a lone prop.) + -- which are scenery for the detector, not a lone prop. Probed: + -- the repeat-aware scenery path already gives every one of them a + -- single 16px course in Pewter's and Bruno's rock walls alike, so + -- a pin would only restate the derived answer.) + -- + -- The rest of the tileset is the ten rooms' own furniture: the six + -- badge gyms it serves (Pewter, Cerulean, Vermilion, Celadon, + -- Fuchsia, Viridian) plus Bruno's and Lorelei's rooms, the + -- Champion's room and the Hall of Fame. (Saffron's and Cinnabar's + -- gyms are FACILITY, Agatha's room is CEMETERY and Lance's is DOJO, + -- so none of them takes a pin from here.) Same failure as every + -- other interior: the detector reads the black interior walls as + -- volumes measured off the whole drawn run, so Vermilion's side + -- walls and Viridian's north-south maze arms came out as 48px fins + -- with their black fill VOIDED flat between them, and the Hall of + -- Fame's recording machine merged into the wall band as a 32px slab + -- whose feet had been flattened away by the walkable-cell rule. GYM = { - wall = { 34, 35, 50, 51 }, - prop = { 2, 56, 18, 19, 11, 12, 27, 28 }, + -- The wall band is ONE 16px face, whatever the artist drew into + -- it. The plinths (34/35/50/51 -- 50 is the $32 water-fallback + -- trap and would recess into a pond lip); the room's own band + -- ($05 white course, $10 trim, $06 the doorway's dark head); the + -- BLACK interior walls every gym is partitioned with -- $0F the + -- fill (near-black, so the void rule flattens it unless it is + -- AUTHORED, which is the whole reason it is listed), $24/$26/$27 + -- the top edge and its corners, $25/$35 the sides, $42/$3E the + -- west and east end caps of a horizontal arm and $10 again as its + -- lit south face. Then the things drawn built INTO those walls, + -- exactly like the Center's healing consoles: Vermilion's target + -- band ($44-$47 with $05 between, over the $54/$56/$57 stripe), + -- the barred gate ($20/$21/$30/$31 -- Lorelei's north door, and + -- the barrier VermilionGymScript paints in until both switches + -- are thrown), and Vermilion's corner switchboard ($29/$2A over + -- $0D/$0E and $1D/$1E), which is drawn 32px tall and so reads as + -- two stacked courses of the same band. + wall = { 34, 35, 50, 51, + 5, 6, 16, + 15, 36, 37, 38, 39, 53, 62, 66, + 68, 69, 70, 71, 84, 86, 87, + 32, 33, 48, 49, + 13, 14, 29, 30, 41, 42 }, + -- Fuchsia's invisible maze walls ($1F) are deliberately NOT here. + -- The artist drew them as the floor with its stripes broken: on + -- the GB they are invisible, and the puzzle IS walking into them. + -- Raising them to a course would read better as a room and give + -- the player something to bump into -- and would also hand them + -- the solution, turning a maze into a corridor. A shape is + -- purely presentational and this file's job is to reproduce what + -- was drawn, so the drawn answer wins: left to the derived path, + -- $1F stays the floor it is painted as and the gym plays exactly + -- as the flat game does. + -- Cerulean's pools are the ONE place the $14 water-fallback trap + -- is right: $14 really is the gym's water, so it is left to fall + -- through to the engine's water set and recess. Its north coping + -- ($04, the grated lip) is drawn in the TOP half of a water cell, + -- so the cell rule dragged it down to the water line with it; + -- pinned flat it stays the poolside at floor level and the water + -- shows a real lip. The pool's south lip ($01) needs nothing -- + -- it is always the top half of a walkable FLOOR cell, so rule 3 + -- already lays it flat. Lorelei's room uses the same pair for + -- the paved plaza that stands out of her water. + ground = { 4 }, + -- Celadon's hedge: a round shrub, one per cell ($2C/$2D over + -- $2E/$2F), 35 of them planted wall to wall. Round drawings get + -- the cylinder archetype -- one voxel ball per 16x16 cell carved + -- from the darkest-pixel outline -- the same treatment the + -- overworld's tree canopies take, and per-CELL, so a hedge row + -- becomes a row of bushes instead of one boxed monolith. + cylinder = { 44, 45, 46, 47 }, + -- The statues, the cans, and Celadon's three little trees + -- ($40/$41 canopy over $50/$51 trunk). A trunk is not round, so + -- the tree cannot be a ball like the shrubs beside it -- it takes + -- the thin standee pool every interior plant takes, which is also + -- a pool apart from the cylinders it touches. + prop = { 2, 56, 18, 19, 11, 12, 27, 28, + 64, 65, 80, 81 }, + -- The Hall of Fame's recording machine, the one piece of real + -- furniture in the tileset. It is drawn 32px wide and THREE tile + -- rows tall against the north band, and the detector made a mess + -- of it twice over: the top two rows merged into the wall band as + -- one 32px slab, and the base row -- whose cell is walkable, + -- because the Hall's floor tile ($19) is its own bottom-left -- + -- was flattened into the floor, so the machine stood on nothing. + -- Split at the drawn seam: the plinth row ($58/$59/$5A) is a + -- half-cell counter, and the console above it ($5B-$5E over + -- $36/$37/$55/$5F) is a standing per-pixel billboard that rides + -- the plinth's top face through the authored-box support rule. + -- 8 + 16 is exactly the 24px the machine is drawn. + counter = { 88, 89, 90 }, + billboard = { 91, 92, 93, 94, 54, 55, 85, 95 }, + -- the ground painted under each pinned prop, by the prop tile's + -- own id: statues and cans stand on the gyms' main floor ($11), + -- Celadon's trees on the garden's light dither ($2B) rather than + -- on whatever their neighbours vote. + -- (Celadon's flowerbed tile $03 is deliberately absent: the + -- tileset ANIMATES it by frame rewrite, so TileShape derives the + -- `flower` pin for it already -- flat ground plus a standing + -- 1px cutout -- and listing it here would take that away. The + -- floors need nothing either. $11/$16/$2B and Viridian's arrow + -- and dot markings $3C/$3D/$3F/$4C/$4D are in the walkable list + -- outright; the paved slabs the Hall of Fame is floored with and + -- Lorelei's plaza is built of ($09/$0A over $19/$1A) get there + -- through the CELL: only $19 is listed, but it is the cell's + -- bottom-left, so rule 3 lays all four of them flat together. + -- Cerulean uses the very same slab as its poolside deck, which is + -- why that gym's perimeter is a deck and not a wall -- the room's + -- only wall band is the one drawn course along its north edge.) prop_ground = { [2] = 17, [56] = 17, [18] = 17, [19] = 17, - [11] = 17, [12] = 17, [27] = 17, [28] = 17 }, + [11] = 17, [12] = 17, [27] = 17, [28] = 17, + [64] = 43, [65] = 43, [80] = 43, [81] = 43 }, }, - -- ledge pairs from data.field.tilePairs + -- The caves: one blockset serves nineteen maps -- Mt Moon's three + -- floors, Rock Tunnel's two, Seafoam Islands' five, Victory Road's + -- three, Cerulean Cave's three, and Diglett's Cave with its two + -- route-side stubs. A Gen 1 cave is drawn as TWO floor levels: a + -- dark lower floor ($20 rough, $21 bright checker, $2A fine checker) + -- and LIT shelves ($05) raised above it. data.field.tilePairs + -- forbids stepping between the two ($20/$21/$2A/$41 <-> $05), which + -- is what the shipped `ledge` pin on $05 records: the lit floor is a + -- step up off the dark floor, reached only by the drawn ramps. + -- Everything else in here is rock, and in a dungeon the rock IS the + -- room -- so every rock tile is pinned to ONE 16px course. + -- + -- What the detector made of it: the rock EDGES towered. A shelf's + -- side is drawn as a cap over a run of face tiles over a corner + -- pair -- $04 / $31 x5 / $28 / $10 down one column -- and the volume + -- builder's repeat scan anchors on the column's FRONT tile, which + -- here is a one-off corner that never recurs above it. Neither the + -- repeat read nor the trim rule (two identical rows directly above + -- the front) fires, so the column keeps its whole extent and caps at + -- MAX_ROWS: a 48px fin out of the 16px band beside it, while the + -- column one tile west -- ending on $28 with two $31 above -- reads + -- 16. Probed before pinning: MT_MOON_B2F tiles (5,4)-(5,11) and + -- (17,8)-(17,15) at 48 with 16 either side; VICTORY_ROAD_2F + -- (25,26)-(25,31); SEAFOAM_ISLANDS_B4F (22,0)-(22,7), + -- (13,12)-(13,19) and (12,13)-(12,19). The same reading turns a + -- column of four DIFFERENT rock tiles into 32px -- VICTORY_ROAD_1F + -- (28,20)-(29,23) and (30,20)-(31,23), $02/$12 over $0C/$1C, next + -- to identical rock at 16. An authored tile never reaches the + -- volume builder, so pinning the rock is the whole fix: there is no + -- run left to misread. CAVERN = { - ledge = { 5 }, + -- The rock, one 16px course. Reading across the blockset: + -- $02/$03 over $12/$13 is the speckled rock mass and $0C/$0D over + -- $1C/$1D the boulder-cluster mass, both solid wall-to-wall + -- fills; $10/$11 is the cobble course facing the south side of + -- every lit shelf, $31 its west face and $17 its east; $04/$07/ + -- $28 are the caps and $25/$26 the inner corners where two faces + -- meet. $0E/$0F over $1E/$1F is the barred rock shelf that + -- stands alone on the dark floor (MT_MOON_1F, ROCK_TUNNEL_1F and + -- twice in SEAFOAM_ISLANDS_B4F): one cell of face-on drawing, so + -- one 16px face -- never a standee, its black surround touches + -- all four rims and would segment into a solid slab. + -- + -- $14 is the bright rubble mass that fills Cerulean Cave and the + -- lower Seafoam floors, and it is the $14 WATER-FALLBACK TRAP: + -- unpinned it hits the engine's stale-cache water set, and every + -- cell it floors RECESSED -- probed `14w-2` over whole blocks of + -- SEAFOAM_ISLANDS_B4F and CERULEAN_CAVE, a cavern of ponds. + wall = { 2, 3, 18, 19, -- $02/$03 over $12/$13, speckled mass + 12, 13, 28, 29, -- $0C/$0D over $1C/$1D, boulder mass + 16, 17, -- $10/$11, the shelf's south cobbles + 49, 23, -- $31 west face, $17 east face + 4, 7, 40, -- $04 NW cap, $07 NE cap, $28 SW cap + 37, 38, -- $25/$26, the inner corners + 20, -- $14 rubble mass -- the water trap + 14, 15, 30, 31 }, -- $0E/$0F over $1E/$1F, barred shelf + -- the lit floor and the shading row along its north rim: one 6px + -- shelf. $05 is the shipped pin (the tilePairs step). $29 is the + -- SAME surface -- the artist's shadow row where the shelf runs up + -- against the rock above it -- but it sits in walkable cells, so + -- unpinned it resolved to flat ground and cut a 6px trench along + -- the north edge of every lit room (`29g00` with `05l06` one tile + -- south, all the way across MT_MOON_B2F). + ledge = { 5, 41 }, + -- the cave ponds: $06/$27 over $24/$01, the tileset's + -- TILEANIM_WATER cell -- Mt Moon B2F's lake, Victory Road's + -- pools, the Seafoam channels you Surf. The engine's water set + -- does not name them (it carries the stale $14/$32/$48 ids + -- instead), so unpinned they fell to the wall fallback and the + -- volume builder read the alternating $06/$24 rows as a drawing + -- eight rows tall: `27w48`/`01w48`, a 48px wall of water standing + -- down the middle of MT_MOON_B2F. + water = { 6, 39, 36, 1 }, + -- Victory Road's boulder switches ($2B/$2C over $2D/$2E, the four + -- plates on 1F, 2F and 3F). Round art, but NOT a round object: + -- the Strength boulders themselves are overworld SPRITES, and + -- this is the plate they get pushed onto -- drawn from straight + -- above as a black ring, a white highlight rim and a grey disc. + -- `cylinder` was tried here first, because a circle is what the + -- overworld's round canopies take, and it came out wrong: the + -- plate's own dithered background carries stray black pixels the + -- darkest-outline flood cannot reach, so they joined the mask, + -- inflated the top and bottom row spans to the full 16, and the + -- hull rendered as a clutch of white shards standing where the + -- plate should be (VICTORY_ROAD_1F cell 17,13). `relief` is the + -- reading anything drawn from above wants: the disc stays flat + -- and the pixels inside its black ring lift a few voxels, so it + -- reads as a plate -- and the cell is walkable, so you step on it. + relief = { 43, 44, 45, 46 }, + -- the flat things, pinned so nothing can raise them: both ladder + -- graphics ($08/$09 over $18/$19, $0A/$0B over $1A/$1B), drawn + -- from ABOVE and carrying the tileset's warp tiles; the ramp + -- treads that walk you off a lit shelf down to the dark floor + -- ($15/$16, four steps seen from above); and the drop holes of + -- Seafoam and Victory Road ($2F rim over $22). All of them + -- already resolved to ground through their walkable cells -- the + -- pins keep that true whatever cell a block puts them in, and + -- keep the hole's lit rim out of the wall fallback. + ground = { 8, 9, 24, 25, -- $08/$09 over $18/$19, ladder + 10, 11, 26, 27, -- $0A/$0B over $1A/$1B, ladder + 21, 22, -- $15/$16, the ramp treads + 47, 34 }, -- $2F over $22, the drop hole + -- Left to the derived defaults on purpose. The dark lower floor + -- ($20, $21, $2A) is in the tileset's walkable list and resolves + -- to flat ground at level 0 -- the level the tilePairs step above + -- is measured FROM -- so pinning it would only restate the + -- default. $3C is solid black and the void rule flattens it + -- (MT_MOON_B1F's unlit north band probes `3Cv00`); pinning it + -- would BREAK that, since the void rule skips authored tiles. }, + -- Viridian Forest. Nearly everything drawn here is ROUND, and the + -- detector was boxing all of it: the big trees came out as ragged + -- mixed-height volumes (their sparse canopy-edge tiles read 0px, + -- their bodies 32px), the stump rows merged into 16px crate walls + -- wearing folded stump art, and the trail sign was a broken pile -- + -- its $32 tile is the water-fallback trap and recessed into a pond + -- lip in the middle of the woods. FOREST = { + -- the hop-down edge, from data.field.tilePairs (CAVERN's entry + -- above carries the same note for its own pair) ledge = { 46 }, + -- the big trees -- the ball ($05/$06/$15/$16/$25/$26), its rim + -- wisps ($04/$07/$17/$23/$24/$27) and its feet ($35/$36) -- and + -- the stumps ($02/$03/$12/$13): one voxel hull per 16x16 cell, + -- cut from the cell's own art, the treatment the overworld border + -- forest already wears. A tree spans 2x2 cells, so its four + -- quarter-hulls tile into one big lumpy canopy. Every tile of + -- the drawing is listed: the cell-rounding is per TILE, and a rim + -- tile left out keeps its detector reading -- which is exactly + -- the ragged 0/8/16/32px box collar the trees wore + cylinder = { 2, 3, 4, 5, 6, 7, 18, 19, 21, 22, 23, + 35, 36, 37, 38, 39, 53, 54 }, + -- the white sparkle filler inside the tree masses: flat, not an + -- invisible zero-height box + ground = { 0 }, + -- the trail signs: the standing thin-slab treatment every town + -- sign gets ($32 pinned is also what lifts it out of the water + -- trap) + signpost = { 33, 34, 49, 50 }, }, -- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's @@ -389,6 +626,1159 @@ return { -- display; 68/69/71 and 84/85/87 the goods rows below bookcase = { 64, 65, 67, 68, 69, 71, 80, 81, 83, 84, 85, 87 }, }, + + -- The route gates and their upstairs lounges, the four Underground + -- Path entrance huts, the Safari Zone gate and its four rest houses + -- -- twenty-five maps on one tileset. (Viridian Forest's two gates + -- draw the very same art from a SEPARATE id, FOREST_GATE below, and + -- pins do not carry between ids.) Before this entry every one of + -- them was a swimming pool: 50/51 -- the dark panel the artist reuses + -- as the wall's base course AND as every counter's front -- is one of + -- the three ids (20/50/72, the $14/$32/$48 of the stale cache) that + -- the engine's water set claims in every tileset, so the whole wall + -- perimeter and every counter front recessed into a moat. The other + -- two are placed here as well: 20 is the exit doormat, 72 the top + -- course of Route 2's wall. What survived the water was towered + -- instead -- the counters raised to a 16px slab, the cabinets boxed, + -- the 2F window read as a 16-to-40px skyline, and the tables and + -- binoculars flattened into the floor because their cells are + -- walkable. + GATE = { + -- The wall band stays ONE 16px face. Three wall drawings share + -- it: the route gates' panelled course (32/33 over 50/51), the + -- Underground Path huts' and Route 22's plain stripes (0), and + -- Route 2's speckled plaster (72 over its dark skirting 74). 0 + -- and 74 need no height correction but are pinned anyway, so the + -- detector cannot swallow the corner plants into the band and + -- tower them with it. The gate 2F's big window is the same band + -- two cells deep (58 glass, 45 its dither, 61 the diagonal glare, + -- 62 the black mullions). The door panes (41/43, the leaf drawn + -- above the walkable threshold) are wall too, so a doorway reads + -- as a recess in the band instead of a picture of a door lying on + -- the floor -- the Center's warp-tile treatment, and pins are + -- look-only so the cell stays walkable. + -- + -- 50/51 is the one compromise in this entry. The same two tiles + -- draw the wall's base course and every counter's front, and a pin + -- cannot tell the two uses apart. Pinned `counter` (8) the + -- counters come out perfect and the wall band castellates: it + -- stacks 32/33 over 50/51 for up to sixteen tile rows down the + -- east and west sides, so a 16px ridge and an 8px trench alternate + -- every 8px of depth and the room ends up built out of crates. + -- Pinned `wall` the band is one clean face everywhere and the + -- price is that a counter's FRONT row stands 16 instead of 8 -- + -- which is cheap, because the mesher's authored-top rule still + -- lays the drawn counter surface (7/8/9) on that block's top face + -- and folds the drawn front panel up its face. It reads as a + -- full-height service counter, not as a slab. The wall is the + -- room's frame and it wins. (FOREST_GATE never places 32/33, so + -- there the counter keeps its half cell.) + wall = { 0, 32, 33, 41, 43, 45, 50, 51, 58, 61, 62, 72, 74 }, + -- the counter, half a cell high: its north rim and end caps (7/8), + -- the long run between them (9), and the top surface a counter + -- drawn running north-south wears (23/24). 8px is one clean band, + -- so the surface stays on top and the guard leans on it rather + -- than standing behind a wall stub. The counters that run wall to + -- wall (Route 12's pair) are pure 8px; the ones that end in the + -- open carry the 16px front row above. + counter = { 7, 8, 9, 23, 24 }, + -- the tall glass-fronted cabinets: three shelf ranks (34/35) + -- folded up a 24px box whose top face adopts the drawn top rim + -- (7/8, pinned `counter` directly above it) through the mesher's + -- authored-fold rule -- the HOUSE / Red's-house bookcase + -- treatment. Deliberately not `bookcase`: that collapses the + -- drawing to one cell of depth, which would leave the wall cell + -- behind each cabinet as bare floor, and its cap rule refuses a + -- row pinned anything but `table`, so the cabinets would lose + -- their tops as well. + desk = { 34, 35 }, + -- standing per-pixel props with body, black-outline segmented: the + -- little tables of the 2F lounges and the Safari rest houses (2/3 + -- the top, 18/19 the legs) and the 2F observation binoculars (36 + -- eyepieces, 52 barrels, 57 pedestal). Both are drawn face-on + -- with air between their legs and a clean white floor margin, so + -- the standee segments whole -- and both were invisible before, + -- flattened by the walkable-cell rule. They stand on the floor: + -- the window band is drawn ABOVE them, and the support rule only + -- lifts a prop drawn above a box. + billboard = { 2, 3, 18, 19, 36, 52, 57 }, + -- the plants, in the THIN pool like every other interior plant: + -- the potted palm that stands in every hut and rest-house corner + -- (5/6 crown, 21/22 fronds, 37/38 over 53/54 the pot), and Route + -- 22's wall plant (14/15 crown, 30/31 pot), which stands on the + -- 16px planter its base course (50/51) makes. + prop = { 5, 6, 14, 15, 21, 22, 30, 31, 37, 38, 53, 54 }, + -- the exit doormat (4 over 20): drawn from straight above, so it + -- has to lie flat -- and 20 is the $14 water-fallback trap, which + -- sank every gate's own doorway into a pond lip. + ground = { 4, 20 }, + -- the flight up to a gate's 2F: real steps climbing east, the way + -- the drawn treads rise to the right. Pixel for pixel the same + -- staircase as Red's house 1F, on the same four tile ids. + stair_e = { 12, 13, 28, 29 }, + -- the flight down -- to 1F from a gate 2F, into the tunnel from an + -- Underground Path hut: a sunken stairwell descending westward, + -- high at the east lip where the player enters. Again the same + -- drawing and the same ids as Red's room upstairs. + stair_down_w = { 10, 11, 26, 27 }, + -- Left to the derived default on purpose: the two floor checkers + -- (1/17) and every threshold -- the door mats (55/56/94) and the + -- tile under the door panels (42/59) -- sit in walkable cells, so + -- the cell rule already lays them flat; and tile 16 is solid + -- black, which the void rule flattens into exactly the darkness + -- that should sit above Route 16's north wall. + }, + + -- Viridian Forest's north and south gates. Tile for tile they are + -- the same drawing as ROUTE_2_GATE, but they are their own tileset + -- id, so they need their own copy of the pins -- and they get the + -- one thing GATE cannot have. These two maps never place the + -- panelled wall course (32/33), so nothing else claims 50/51 and the + -- counter can be the half cell it is drawn as. Before this entry + -- the room was a pond with an island: 72 ($48) sank the north wall's + -- top course, 50 ($32) sank all four counter fronts, and 20 ($14) + -- sank the exit doormat -- the three stale-cache water ids, all + -- three placed in one small room. + FOREST_GATE = { + -- the wall band, one 16px face: the speckled plaster course (72) + -- over its dark skirting (74), plus the door pane (41/43) drawn + -- above the walkable threshold, so the doorway reads as a recess + -- in the band rather than a door lying flat on the floor (pins are + -- look-only; the cell stays walkable). 72 is the $48 + -- water-fallback trap and took the whole north wall down with it. + wall = { 41, 43, 72, 74 }, + -- the four counters, half a cell high: end caps (7/8), the top + -- surface running north-south (23/24), and the front panel (50/51) + -- standing up as one clean 8px band with the surface riding the + -- top face. 50 is the $32 water-fallback trap -- unpinned, every + -- counter's front cell was a pond notch. + counter = { 7, 8, 23, 24, 50, 51 }, + -- the potted palms down both side walls, three to a side, in the + -- THIN standee pool like every other interior plant: 5/6 crown, + -- 21/22 fronds, 37/38 over 53/54 the pot. They stack with no gap + -- between them, and each still segments as its own plant. + prop = { 5, 6, 21, 22, 37, 38, 53, 54 }, + -- the exit doormat (4 over 20), drawn from straight above so it + -- lies flat; 20 is the $14 water-fallback trap. + ground = { 4, 20 }, + -- Left to the derived default on purpose: the two floor checkers + -- (1/17) and the tile under the door panes (42/59) sit in walkable + -- cells, so the cell rule already lays them flat. + }, + + -- Celadon's department store and the rooms that share its blockset: + -- CELADON_MART_1F..5F and the ROOF, the two 2x2 lift cabins + -- (CELADON_MART_ELEVATOR, SILPH_CO_ELEVATOR), ROCKET_HIDEOUT_ELEVATOR, + -- the GAME_CORNER with its PRIZE_ROOM, and CELADON_DINER -- twelve + -- maps off one atlas. Everything the store owns was wrong: the + -- U-shaped sales counters read their whole north-south arm as one + -- drawing and towered to 48px (dragging the wall band they touch up + -- with them), the merchandise racks fused sideways into 32px + -- monoliths FOUR tiles deep -- the Poke Mart's failure exactly -- + -- the diner and roof stools sit in walkable cells and so flattened + -- into the floor entirely, and three separate tiles fell into the + -- engine's stale water set and sank into ponds indoors. + LOBBY = { + -- The two exit cells of every map (the sliding-door threshold, 4 + -- over 20). 20 is $14, which the stale-cache fallback counts as + -- water in EVERY tileset, and rule 2 judges the whole CELL by it: + -- both tiles of the doorway recessed into a pond lip just inside + -- the shop door. It is a flat mat, and the lift cabins use the + -- same pair for their car door. + ground = { 4, 20 }, + -- The wall band stays ONE 16px face. The blank course (1) and the + -- skirting course below it (33) are the band itself; everything + -- else here is drawn BUILT INTO it at 16px, so wall height is the + -- drawn height and each reads as a fitting jutting from the band + -- rather than a tower: + -- 6/22 the framed notice beside the stairwell, every floor + -- 2/3/18/19 the pair of pay phones on 1F + -- 46/47 the floor-directory plaque (it also sits at the east + -- end of 1F's counter top, where 16px reads as a sign + -- board standing on the counter -- the one placement + -- that is not wall, and the least bad of the two) + -- 14/15/30/31 the TV set: on 3F's wall band it stands over the + -- display case below it, and on 3F's goods counter the + -- same 16px box reads as a floor-standing display unit + -- 62/63 the lift call-button panel in both 2x2 cabins + -- 72/73/88/89 the framed picture (72 is $48, a water-set id, so + -- unpinned its top half sank) + -- 68/84 the roof's perimeter safety railing, drawn 16px tall + -- 75/76/77/78/79 the roof parapet and the Rocket lift's cabin + -- walls (79 also rings the roof map as its border block); + -- the Game Corner reuses 75/76/77/78 as the dark south + -- end of its machine banks, where 16px reads as the + -- bank's end cabinet + -- 91 the dithered wall course above the Prize Room counter + -- and the roof stairhead + -- 92/93 the Rocket lift's horizontal cabin frame + wall = { 1, 2, 3, 6, 14, 15, 18, 19, 22, 30, 31, 33, 46, 47, + 62, 63, 68, 72, 73, 75, 76, 77, 78, 79, 84, 88, 89, + 91, 92, 93 }, + -- Every service counter in the group, half a cell high like the + -- Center's and the Mart's. One 8px band means the drawn front row + -- stands up and everything above it rides the TOP face in drawn + -- order -- which is what a counter arm running north-south IS: the + -- rows above the front are its top surface seen from above, not + -- height. Unpinned the arms measured their full 7-row run and + -- towered to 48px, taking the wall band they touch with them. + -- 38/39/41 the top surface and its end caps + -- 21/48/49 the south front panel and its end caps + -- 54/57 the arms' left and right side edges + -- 36/37/52/53 the goods laid out on 3F's counter (a boxed set + -- and a Poke Ball) -- they ride the top face, drawn + -- once, the way the nurse's tray does + -- 9/25/70/71/85/86/87 the round tables of the diner and the + -- roof terrace, drawn from above with a pedestal + -- row at the south: the same half-cell treatment + -- puts the tabletop on top and the pedestal on the + -- front. (Their four INTERIOR tiles are $37, which + -- is also the checkerboard floor and the Prize + -- Room's upper wall -- see the note below.) + counter = { 9, 21, 25, 36, 37, 38, 39, 41, 48, 49, 52, 53, 54, + 57, 70, 71, 85, 86, 87 }, + -- Free-standing merchandise racks and glass cases: TALL drawings, + -- not deep ones. The four-row rack (42-45 / 58-61 / 64-67 / + -- 80-83) stands two racks abreast on floors 2F-5F and along the + -- Game Corner's north wall, and the detector boxed each pair into + -- ONE 32px slab four tiles deep -- the Poke Mart's exact failure. + -- A bookcase rank collapses onto a one-cell-deep box at the full + -- drawn height instead, and the back rows become hidden floor, so + -- racks standing side by side stay separate objects with an aisle + -- behind them. + -- 34/35/50/51 is the glass display case / vending machine, which + -- the same rule fits at BOTH its drawn sizes: 16px where it wears + -- the 1F phones or the 3F TV above it, 24px where a counter cap + -- (38/41) tops it on the roof, the Prize Room and the Game + -- Corner. 50 is $32, a water-set id, so every one of these cases + -- stood in a pond before it was pinned. + bookcase = { 34, 35, 42, 43, 44, 45, 50, 51, 58, 59, 60, 61, + 64, 65, 66, 67, 80, 81, 82, 83 }, + -- The stools: the diner's chairs, the roof terrace's, the six + -- rows in front of the Game Corner's machine banks and the four + -- on 1F. Their cell's bottom-left tile is 23 ($17), which IS on + -- the tileset's walkable list, so rule 3 resolved every one of + -- them to flat ground and they vanished into the floor + -- completely. A pin is the only thing that overrides that. The + -- drawing carries a full black outline with the floor dither + -- showing at all four corners, so the standee segments cleanly -- + -- and `stool` keeps its own pool, apart from anything it touches. + stool = { 7, 8, 23, 24 }, + -- Deliberately NOT pinned: + -- $37 (55) is three different things -- the light half of the + -- checkerboard floor, the interior of the round tables, and + -- the plain upper wall of the Prize Room and the roof + -- stairhead. The cell rules already answer all three + -- correctly (the floor's cells are walkable through their + -- $45 bottom-left tile; the wall's are not), and pinning it + -- to any one class breaks the other two. The cost is the + -- four $37 tiles inside each round table, which stay a + -- detected 16px box in the middle of the 8px top -- read as + -- a centrepiece. Fixing it properly needs the PaletteFX + -- alias ($37 -> $5a on the roof and the diner) carried into + -- the shape lookup, which is engine work. + -- $10 (16) is pure black and the void rule already flattens it + -- to the interior darkness above the Game Corner's wall. + -- The stair openings (10/11/26/27, 40/56) and the lift doors + -- (12/13/28/29) all sit in WALKABLE cells and fold into the + -- facade on their own, which is what the doorway fold is for. + -- $20/$45 (32/69) are the two floors and are walkable. + }, + + -- Pewter's museum, both floors (the tileset shares the gates' atlas + -- image but is its own id). The exhibits were the worst case in the + -- whole building: 50 is $32, a water-set id, and it is the base row + -- of EVERY display case, so rule 2 judged each case's bottom cell + -- water and sank the fossil vitrines, the space shuttle and the + -- glass cabinets into ponds. 72 is $48 and is the wall band's upper + -- course, so the north wall recessed too -- leaving its lower course + -- alone as an 8px stub. On top of that the benches sat in walkable + -- cells and flattened away, the corner plants towered to 32px, and + -- both staircases (also walkable) were flat floor. + MUSEUM = { + -- the two street doors on 1F: the same $14 mat trap as the + -- department store's, flat + ground = { 4, 20 }, + -- the wall band as one 16px face: the dithered upper course (72, + -- the $48 water-set id) over the skirting course (74), and the + -- two framed paintings hung in it (78/79) + wall = { 72, 74, 78, 79 }, + -- The low partitions that divide both floors -- the tileset's own + -- counterTiles name 23 ($17) -- drawn exactly like the department + -- store's counter arms: a light top surface seen from above with + -- dark side edges. Half a cell, so they read as barriers you see + -- over rather than interior walls. Their north caps and their + -- south base rows belong to the case group below, at the same 8px. + counter = { 23, 24 }, + -- Every vitrine in the museum, as tall drawings one cell deep: + -- 7/8/34/35 the run of eight glass cabinets along 1F's north + -- wall (7/8 is the cabinet's top surface, so the + -- rank wears it as its top face -- which is why it + -- is here and not in `counter` with the partitions + -- whose north cap it also draws) + -- 9/50/51 the low case at 1F's centre (9 the top surface, + -- 50/51 the base panels) and the base row under the + -- partition ends + -- 25/39/40/44/46/48/49/58/60/63/70/71/83 the fossil vitrine, + -- four drawn rows inside one black frame: the dark + -- top surface, the ammonite specimens on their + -- light bed, the placard, and the base. Two on 1F, + -- one on 2F. Collapsed to one cell of depth it + -- stands as a 32px case instead of a 16px lid + -- floating over a pond. + -- 64-69/80-91 the space-shuttle vitrine on 2F, the same drawing + -- shape three cells wide. + -- 50/51 is $32/$33 -- the water-set id that sank all of them. + bookcase = { 7, 8, 9, 25, 34, 35, 39, 40, 44, 46, 48, 49, 50, + 51, 58, 60, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }, + -- the visitors' benches on 1F: their cell's bottom-left tile is + -- 18 ($12), which is on the walkable list, so they resolved to + -- flat ground and disappeared. Seat-height standees in their own + -- pool, like the department store's stools + stool = { 2, 3, 18, 19 }, + -- the corner potted plants (crown 5/6/21/22, pot 37/38/53/54): + -- the THIN standee pool, like every other interior plant in this + -- profile. Unpinned they were a 32px two-cell tower. + prop = { 5, 6, 21, 22, 37, 38, 53, 54 }, + -- the Old Amber in its rounded case, drawn face-on with a clean + -- black outline and a light margin at every corner: the standee + -- with body, segmented so the backing floods away and the amber + -- inside the outline stays + billboard = { 76, 77, 92, 93 }, + -- 1F's staircase UP to 2F: a real rising flight climbing east, + -- the way the treads are drawn (low at the west, high at the + -- east). Its cell is walkable, so without the pin it was flat + -- floor with the stair art painted on it. + stair_e = { 12, 13, 28, 29 }, + -- 2F's staircase DOWN to 1F: the same flight from above, so it + -- descends westward -- the player steps on at the east lip. Same + -- tile ids Red's bedroom uses for its stairwell, which is the + -- convention across Gen 1's interior blocksets. + stair_down_w = { 10, 11, 26, 27 }, + -- Deliberately NOT pinned: 1 and 17 ($01/$11), the two halves of + -- the checkerboard floor. Every floor cell's bottom-left tile is + -- $01, which is walkable, so the cell rule already makes the whole + -- cell flat ground. + }, + + -- Cinnabar Lab and its three side rooms (trade, metronome, fossil), + -- the Fuchsia Warden's house, the Fuchsia meeting room and the Safari + -- Zone's secret house -- seven maps on one tileset. Every indoor + -- failure mode shows up here at once: the detector raised the machine + -- banks as 32px monoliths TWO cells deep, boxed the free-standing + -- shelf racks the same way (and CAPPED the metronome room's racks at + -- 16px because their book rows repeat, so a 32px shelf came out a + -- 4-tile-deep 16px slab), cut every lab bench into a patchwork of + -- 8/16/32px stubs, flattened the chairs into floor, merged the + -- meeting room's long partition with the corner plants into a 48px + -- tower -- and dropped six tile ids into the engine's stale-cache + -- water set, so the bench tops ($04/$05/$14/$15), the table's monitor + -- ($48) and the Warden's round specimens ($32) recessed into pond + -- lips inside the building. Oak's Lab (the DOJO entry) is the + -- reference: same rooms, same answers. + LAB = { + -- the wall band stays one 16px face. Blank courses ($22), the + -- pillar/corner that turns Cinnabar Lab's L ($23), the framed + -- Pokeball picture (16/17/32/33), the metronome room's row of + -- wall brackets (48/49 -- drawn INTO a band that is two cells + -- deep there, so both cell rows stay one 16px face), the vent + -- panel over Cinnabar Lab's three doorways (24/25), and the low + -- dark base course with its end caps (42/43) that the meeting + -- room's partition and the Warden's machine both stand on. + -- The Warden's big machine (87/88/89 top, 28/71/29 the dial row, + -- 36/37 the grille row, over 24/25 and the 42-34-34-43 base) is + -- ALSO wall: it is drawn 32px tall and free-standing, so a + -- `bookcase` would be the honest shape -- but its grille tiles + -- 24/25 are the very tiles Cinnabar Lab hangs in its wall band + -- above the doors, and its base row is the shared $22 band, so no + -- per-tile pin can make it 32px without either stubbing the Lab's + -- wall panel or splitting the machine into columns of different + -- heights. One clean 16px console beats both. + wall = { 16, 17, 24, 25, 28, 29, 32, 33, 34, 35, 36, 37, + 42, 43, 48, 49, 71, 87, 88, 89 }, + -- the equipment banks and the shelf racks: TALL drawings, not deep + -- ones. Each rank collapses onto a ONE-CELL-DEEP box at its full + -- drawn height and its back rows become hidden floor -- the + -- Dojo/Mart treatment, and the only thing that stops the fossil + -- room's and the secret house's machine walls from being 32px + -- blocks two cells thick. 69/70 + 85/86 are the cabinet tops, + -- 10/11 the dial faces, 74/75 + 90/91 the narrow filler columns, + -- 26/27 the plinth with its feet, 59 the black end pilaster; + -- 40/41 are the metronome room's and the meeting room's shelf + -- ranks. Where the drawing's own top row is the shared table + -- trim (64/66, pinned `table` below) the bookcase builder adopts + -- it as the rank's CAP -- one more band of height and the art the + -- top face wears -- so those units come out 32px too. + bookcase = { 10, 11, 26, 27, 40, 41, 59, 69, 70, 74, 75, 85, 86, + 90, 91 }, + -- the lab furniture, at table height. The benches in the fossil + -- and metronome rooms (2/3 the specimen tray, 4/5 + 20/21 the + -- microscope, 18/19 the tray's rack row) and the big lab tables + -- (64/65/66 top edge, 80/81/82 body, 83/58/84 the front rail and + -- its legs). 72/73 -- the monitor and the ball lying on the + -- table -- ride the table's authored height rather than standing + -- as separate cutouts, exactly as Oak's starter balls do; pinning + -- them also stops the display frame's black corner brackets from + -- auto-extracting into standing prisms. Four of these ids + -- ($04/$05/$14/$15) plus $48 are the tileset's water-fallback + -- traps: unpinned they sink into a pond lip in the middle of the + -- room. 65 doubles as the meeting room's long partition top, + -- which therefore reads as a 12px shelf behind its 16px base + -- band instead of joining the 48px tower it used to make. + table = { 2, 3, 4, 5, 18, 19, 20, 21, 58, 64, 65, 66, 72, 73, + 80, 81, 82, 83, 84 }, + -- seats, the 8px standee pool, all of them in WALKABLE cells and + -- therefore flat floor until pinned: the four chairs round the + -- trade room's table and the meeting room's spare (14/15/30/31, + -- a whole cell each) and the little stool tucked under every lab + -- bench (6/7 its top edge, 22/23 its seat and legs). 6/7 also + -- carry the bench's apron along their top edge -- the standee + -- wears a thin dark cap for it, which is cheaper than dropping + -- the stool to a half-cell drawing with no floor margin. + stool = { 6, 7, 14, 15, 22, 23, 30, 31 }, + -- the tall corner plants: two cells of drawing (44/45 + 60/61 the + -- frond crown, 46/47 + 62/63 the stem and pot), mostly silhouette, + -- so the THIN standee pool -- as in every other interior. The + -- detector had them at 0/8/16/24/32px depending on which corner + -- they stood in, and in the meeting room it fused them into the + -- partition and towered the pair to 48px. + prop = { 44, 45, 46, 47, 60, 61, 62, 63 }, + -- the five round specimens in the Warden's house: one 16x16 cell + -- each, drawn ROUND with a dithered shell and a lit underside. + -- Neither a box nor a flat cutout reads them; the round archetype + -- carves a voxel ball per cell from the drawing's darkest-pixel + -- outline, which is what they are. 50 is a water-fallback trap, + -- so unpinned the row sat in five pond lips. + cylinder = { 50, 51, 67, 68 }, + -- Deliberately NOT pinned: the checker floor (1/38) and the fossil + -- room's striped carpet (12/13) are walkable cells and already + -- flat; the doormat (39/55) likewise; the three doorways + -- (76/77 over 52/53) are walkable and the engine folds them into + -- the facade; Cinnabar Lab's black surround (54) is authored void + -- by the void rule; and the bench aprons 8/9 stay flat floor, + -- which is where the artist drew them -- inside the walkable cell + -- in front of the bench. + }, + + -- Celadon Mansion's three floors and its roof, plus the Celadon + -- chief's house -- five maps on one tileset. The mansion maps are + -- half interior and half OUTSIDE: the east third of every floor is + -- the open-air stair landing, so the entry has to keep an outdoor + -- strip flat while it furnishes the rooms. The detector towered the + -- display cabinets, boxed the potted palms into 32px 4-tile-deep + -- monoliths, raised the 2F larder cabinet as a 48px tower taller than + -- the walls, gave the same wall band three different heights + -- (8/16/48) on one map -- and sank the whole cabinet bank AND both + -- front-door mats into pond lips, because nine tile ids here + -- ($04/$14/$22/$23/$32/$33/$38/$48/$57) sit in the engine's + -- stale-cache water set. + MANSION = { + -- one 16px face for every wall course, indoors and out: the rooms' + -- north band (80), the interior partition (30) with its white top + -- rim (76/77), the doorway lintel over the front door (22/23), the + -- east exterior wall the landing runs along (74/75) with its + -- junction pieces (72/73 -- 72 is a water trap -- and 88/89), the + -- band's feet and corners (90/91/92/93), and the roof's own rim + -- and corner blocks (78/79). The roof parapet railing (15 over + -- 31, with end caps 42/43 and 33/49) is drawn a full 16px tall, so + -- it stays in the same band rather than dropping to `fence`: on an + -- open roof a knee-high rail reads as a kerb. 48 and 6/7 are the + -- rooftop shed's base course. + wall = { 6, 7, 15, 22, 23, 30, 31, 33, 42, 43, 48, 49, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 88, 89, 90, 91, 92, 93 }, + -- the display cabinets along the north wall of 1F and the chief's + -- house: TALL drawings, not deep ones, so each rank collapses onto + -- a one-cell-deep box at its drawn height. 34/35 are the shelf + -- row of the short cabinets, 40/21 + 56/87 the two rows of the + -- tall ones (the pair with the trophy in the glass), 50/51 the + -- plinth. Their top row is 38/41, pinned `table` below because + -- the mansion's big tables wear the same trim -- the bookcase + -- builder adopts it as the rank's CAP, which lands the short + -- cabinets at 24px and the tall ones at 32px, exactly their drawn + -- heights. 45/46 + 61/62 + 63/47 is 2F's larder cabinet, four + -- drawn rows standing free of any wall: 32px and one cell deep + -- instead of the 48px tower the detector built. + -- Six of these ids are water-fallback traps ($22/$23/$32/$33/$38 + -- /$57) -- the whole cabinet bank was a pond. + bookcase = { 21, 34, 35, 40, 45, 46, 47, 50, 51, 56, 61, 62, 63, + 87 }, + -- table height. The long tables in 1F and the chief's house + -- (38/39/41 top edge, 54/55/57 body, 60/58/59 the front rail with + -- its two legs) and the writing desks on 2F and 3F (36/37/52/53 + -- the back edge with the papers and the ball on it, 64/65/66/67 + -- the body). The desks' aprons (2/3/85/86 over 18/19) are drawn + -- into the WALKABLE cell in front and stay flat floor, which is + -- how the artist placed them. + -- Known compromise: the rooftop shed on CELADON_MANSION_ROOF is + -- drawn with the table's own top and body tiles (38/39/41 over + -- five rows of 54/55/57) with a door punched in its base, so it + -- comes out a 12px block rather than a shed. Nothing towers there + -- any more -- the detector had its east columns at 48px -- and no + -- per-tile pin can give one drawing two heights. + table = { 36, 37, 38, 39, 41, 52, 53, 54, 55, 57, 58, 59, 60, + 64, 65, 66, 67 }, + -- the stool at the 2F/3F desk: a whole cell of drawing (2/3 over + -- 18/19) sitting in a WALKABLE cell, so flat floor until pinned. + -- The 8px standee pool, seat height, as in Red's rooms. + stool = { 2, 3, 18, 19 }, + -- the potted palms: two cells of drawing (68/69 crown, 8/9 fronds, + -- 70/71 stem, 24/25 pot), mostly silhouette, so the THIN standee + -- pool -- the same numbers the generic HOUSE entry uses, and the + -- same answer every interior plant gets. The detector had them as + -- 32px boxes four tiles deep. + prop = { 8, 9, 24, 25, 68, 69, 70, 71 }, + -- the front-door mats of 1F and the chief's house. Their bottom + -- tile is $14, which the engine's stale-cache fallback counts as + -- water in every tileset, so the mat -- and the whole cell with it + -- -- recessed into a pond lip just inside the door. It is a flat + -- rug on the floor. + ground = { 4, 20 }, + -- Deliberately NOT pinned: the interior checker floor (17), the + -- landing's open-air ground (1), the black surround (16, authored + -- void), the back and rooftop doors (81-84, walkable and folded + -- into their facade), the desk aprons (85/86), and the landing + -- edge (5). + -- CANNOT be pinned, engine-side: the four staircases (12/13/28/29 + -- the rising flight, 10/11/26/27 the sunken one) are in this + -- tileset's doorTiles, and Structures' door fold OVERWRITES the + -- resolved shape of any door cell whose north neighbour is upright + -- -- without checking `authored` -- so a stair_e / stair_down_w pin + -- is silently discarded. The indoor flights therefore read as + -- doorways folded into the north wall band, which is at least what + -- they are (you walk into them from the room). The roof's two + -- flights, which stand clear of any wall, come out as a shallow + -- 8px lip -- a stairwell mouth, near enough -- and pinning them + -- `wall` would be worse: it would plug the openings with a 16px + -- block in the middle of the roof. See the report. + }, + + -- Silph Co's 11th floor (the president's office), Bill's house and + -- the Pokemon Fan Club -- three rooms that share one tileset and + -- almost no furniture. The detector raised Silph's side walls as + -- 48px towers (the drawn band is three cell rows deep, so it + -- measured the whole column), split the Fan Club's wall band across + -- 0/8/16px AND a pond lip, boxed Bill's two transporter drums into + -- 48px slabs five tiles deep, and left every chair flat because they + -- sit in walkable cells. Six ids here ($14/$1F/$32/$34/$40/$48) sit + -- in the engine's stale-cache water set -- including $1F, the main + -- FLOOR tile, which sank wherever it shared a cell with $48. + INTERIOR = { + -- the wall band, one 16px face throughout: the rooms' face-on + -- courses (52 upper, 68 lower; 16 in Bill's house and Silph's + -- outer band), the courses seen from above along the south and the + -- partitions (87 top, 88 bottom), the side walls (89/90), the end + -- caps and junction blocks (45/46), the framed Pokemon portraits + -- hung in the band (19/20 over 35/36), and the padded bench built + -- into the Fan Club's north wall (49/50/51 -- its lower half, + -- 65/66/67, is drawn into the walkable cell in front and stays + -- flat, the way the artist drew it). + -- 93/94 are Silph's CARD KEY door: the block only exists while the + -- door is locked (data.field.cardKey swaps it out), so it never + -- appears in maps.lua -- but the probe sees it, and unpinned it + -- came out 0px/8px in the middle of a 16px wall. + -- Bill's pipework belongs here too: the stubs either side of each + -- drum (32/48, 37/53) and the run between them (38/54), all drawn + -- 16px tall against the band. + wall = { 16, 19, 20, 32, 35, 36, 37, 38, 45, 46, 48, 49, 50, 51, + 52, 53, 54, 68, 87, 88, 89, 90, 93, 94 }, + -- Bill's two transporter drums: a TALL drawing (four rows of + -- cylinder plus a base course), not a deep one. The bookcase + -- collapse gives each drum one cell of depth at its full 32px + -- height instead of the 48px, five-tile-deep slab the detector + -- built, and its top face wears the drum's own lid. 7/8/9/10 is + -- the lid, 23/24/25/26 and 39/40/41/42 the barrel, 55/56/57/58 the + -- footing. + bookcase = { 7, 8, 9, 10, 23, 24, 25, 26, 39, 40, 41, 42, + 55, 56, 57, 58 }, + -- the big octagonal table of the Fan Club and Silph's boardroom, + -- half a cell high -- and half a cell ON PURPOSE. A `counter` is + -- ONE band: exactly the drawing's bottom row (79/63/64/81/82, the + -- table's front bevel) stands up as the front, and every row above + -- it rides the top face IN DRAWN ORDER. For a table drawn in plan + -- that is the only class that reproduces the plan: the octagon's + -- bevelled corners (72/73 north, 77/78 the flanks, 63/81 and + -- 79/82 south) land where they are drawn instead of being smeared + -- by a taller box, which wears its NORTH row's art across the + -- whole top face. + -- The chairman's seat is drawn INTO the table -- 17/18 the sitter, + -- 33/34 the chair back, 91/92 the seat -- across three tile rows, + -- and this is the Pokemon Center couch problem exactly: folding + -- two rows upright makes the box two tiles deep and a folded box + -- repeats its north row across the whole top, so every upright + -- arrangement draws the sitter's head two or three times. Riding + -- the counter's top face draws him once, in the right place, at + -- the right depth. Standing him as a cutout is not an option + -- either: the drawing has no floor margin -- it is surrounded on + -- all four sides by the tabletop's own mid grey -- so the mask + -- would drain from every edge. + -- 91/92 double as the wall band's base blocks where a side wall + -- meets the south course in Silph; those six tiles therefore sit + -- 8px instead of 16px. It is a notch at the foot of a wall the + -- player never faces, and the alternative is a 16px stub standing + -- in the middle of the boardroom table. + -- 40 and 48 are water-fallback traps: unpinned, the table's whole + -- north-west cell was a pond, and it dragged the floor tile $1F + -- sharing that cell down with it. + counter = { 17, 18, 33, 34, 63, 64, 72, 73, 74, 77, 78, 79, + 81, 82, 91, 92 }, + -- Bill's desk and the Silph president's, at table height: 11-14 + -- the surface with the terminal and the ball on it, 27-30 the + -- front edge. Their aprons (43/44 + 91/92 on the row below) are + -- drawn into the WALKABLE cell in front; 43/44 belong to the stool + -- there, not to the desk. + table = { 11, 12, 13, 14, 27, 28, 29, 30 }, + -- the seats: Bill's desk stool (43/44 over 59/60) and the Fan + -- Club's four members' chairs (61/62 over 59/60), a whole cell + -- each. All of them sit in WALKABLE cells, so they were flat + -- floor until pinned; the 8px standee pool puts them at seat + -- height, which is also where a character standing on the cell + -- ends up. + stool = { 43, 44, 59, 60, 61, 62 }, + -- flat, and pinned flat on purpose. 31 is the rooms' floor tile + -- and needs no pin for its 980 walkable placements -- but the + -- water test runs per CELL on the engine's own stale set, BEFORE + -- any neighbouring pin can help, so the floor tiles sharing the + -- Fan Club's and Silph's north-west table cell with $48 sank into + -- the pond with it. A pin is the only rule that outranks it. + -- 1/2 are the curve of Bill's drums where they meet the floor, + -- drawn below the pinned barrel and flat where they lie. + ground = { 1, 2, 31 }, + -- Deliberately NOT pinned: the doormats (70/71) and Silph's warp + -- pads, stairwells and elevator mouth (3/4, 5/6/21/22, 83-86) are + -- walkable and already flat; 47 is the black surround outside the + -- building's footprint and the void rule authors it; 69 is the + -- shadow course under Bill's drums and 75/76 the octagon's front + -- apron, both drawn into walkable cells and both correct flat. + }, + + -- The Cable Club rooms and the Bike Shop (one tileset, three maps). + -- COLOSSEUM and TRADE_CENTER are the same 5x4 room with a different + -- machine in the middle and a different board on the wall; BIKE_SHOP + -- reuses the same wall, floor and counter for a showroom full of + -- bicycles. Between them the three maps place every tile. + -- + -- What was wrong: the Bike Shop's bicycles came out at 0 and 8 -- + -- each bike sliced in half at ankle height with its top row missing, + -- and the leftmost rack fused into the wall and towered to 32. The + -- shop's whole L-shaped counter stood at wall height, a 16px slab + -- you could not see over. Both Cable Club rooms lost their stools + -- (a walkable cell, so they flattened to floor) and their machine + -- came out 0/8/16 in the same object. And the Game Boy link poster + -- above the door has $32 in its bottom-left corner, so the poster + -- had a pond in it -- the shore-set trap, and TRADE_CENTER's link + -- table carries the other one, $48. + CLUB = { + -- The wall band stays ONE 16px face. 6 is the striped panel, + -- 52 the fluted pillar that breaks it every three cells (its + -- rounded foot, 53, stands one row lower on the floor and is + -- walkable, so it stays flat ground in front of the band). + -- Drawn INTO the band and therefore wall as well: the two + -- bicycles hung on the Bike Shop's wall (1/2/3 over 17/18/19, + -- exactly two tile rows = exactly the band's height), the Cable + -- Club's link poster (48/49/50/51 -- 50 is $32, the shore trap + -- that had it recessing to -2), and the Trade Center's pair of + -- lit notice boards (63/68/68/69 over 66/67/67/70). + wall = { 1, 2, 3, 6, 17, 18, 19, 48, 49, 50, 51, 52, 63, 66, 67, + 68, 69, 70 }, + -- Counters, half a cell high -- the house rule for anything you + -- lean on. The Bike Shop's L: the till run's top (7/8, the + -- tileset's own counterTiles) with its inside corner (54), east + -- arm (16) and end cap (5), all over the front panel 23/24. 8px + -- is one clean band, so the drawn front stands up and the counter + -- top stays a top; at 12 or 16 it reads as a wall stub. + -- 71/72/73/74 is the TRADE_CENTER link table, which shares that + -- same 23/24 front: its surface is drawn from ABOVE (two Game + -- Boys lying on it, cables between), so it belongs riding the top + -- face, not standing up. 72 is $48, the second shore trap. + counter = { 5, 7, 8, 16, 23, 24, 54, 71, 72, 73, 74 }, + -- The Colosseum's battle machine: a console drawn face-on, four + -- tiles wide and three rows tall, grilled side towers (59/62 and + -- 55/58), a dark screen bay (60/61, 56/57) and the two white + -- horns that crown it (64/65). 24px is its drawn height, so + -- `desk` folds the whole drawing upright as one machine instead + -- of the 0/8/16 rubble the detector left. + desk = { 55, 56, 57, 58, 59, 60, 61, 62, 64, 65 }, + -- The two stools that flank each Cable Club machine: seat 42/43 + -- over base 38/39, in every one of the four placements across + -- both rooms and nowhere else in the tileset. Their cells are + -- walkable, so without a pin they were floor; at seat height a + -- character standing on one sits on it. + stool = { 38, 39, 42, 43 }, + -- The showroom bicycles -- six of them standing on the Bike Shop + -- floor, in two columns, each drawn 3 tiles by 2 side-on. The + -- THIN standee pool, not `bookcase`: a bike is nearly all air + -- (two wheel rings and a frame), and collapsing a rack of them + -- onto a one-cell-deep box at full height would trade six + -- bicycles for one blank 32px panel wearing a bicycle print. As + -- cutouts they segment cleanly -- the drawing has floor margin on + -- three sides and its own black outline seals the wheels -- and + -- the standee builder's connected-component split gives each bike + -- its own feet in its own cell even where a lower bike's + -- handlebar stem touches the wheel of the one drawn above it. + -- 11/12/14 the saddle and bar row, 27/28/9 the frame and wheels. + prop = { 9, 11, 12, 14, 27, 28 }, + -- The crate and the standing pump beside the shop's south wall + -- (29/13 over 21/22), twice. A separate pool from the bicycles + -- on purpose, and the thicker one: a crate is a deliberate object + -- with a body, where a bike is a silhouette. + billboard = { 13, 21, 22, 29 }, + -- The Bike Shop floor's second checker phase. It is NOT in the + -- tileset's walkable list, so the cells it floors resolve by rule + -- 4 and it rose as a wall stub -- or, worse, got swept into the + -- bicycle beside it and dragged to 8px. It is floor; it is flat. + ground = { 30 }, + -- Left to the derived default on purpose: + -- $0F $1F the main floor checker -> walkable, ground + -- $0A $1A the dark slab under the Bike Shop's exit warp -> a + -- threshold mat, walkable, flat + -- $35 the pillar's rounded foot -> its cell is walkable + -- (you walk in front of the pillar), so it lies flat + -- against the 16px band, which is what a plinth does + -- $26-$29 $2C-$2F the octagon painted on both Cable Club + -- floors. Ground markings seen from above, and every + -- cell they sit in is walkable, so rule 3 already + -- keeps them perfectly flat. ($26/$27 are shared + -- with the stool bases above and pinned there; they + -- appear nowhere else in either room.) + }, + + -- The S.S. Anne, inside: both cabin decks and their corridors, the + -- bow, the kitchen, the captain's room -- and two Kanto houses + -- (Cerulean's badge man, Fuchsia's Good Rod fisher) that borrow the + -- liner's furniture set. Twelve maps, and SS_ANNE_CAPTAINS_ROOM plus + -- SS_ANNE_KITCHEN between them place every tile the group uses. + -- + -- Everything was wrong here, in every one of the ways indoors goes + -- wrong. The wall-touching cabin table and the BED were merged into + -- the wall and towered to 32px, so a bunk read as a bank of lockers + -- wearing a porthole on top. The corridor wall band measured 24px + -- instead of 16. In the two houses the band came out at height 0 -- + -- the drawing runs off the top of the map, so the detector had no run + -- to measure and the room had no walls at all. Every stool, chair and + -- staircase sat in a WALKABLE cell and flattened to floor, so the + -- captain's room had no stairs, no chairs and no seat. And the + -- barrels' top-left tile is $48, which the engine's stale-cache shore + -- set counts as water in every tileset but SHIP_PORT: each barrel had + -- a pond dug into its lid. + SHIP = { + -- The wall band stays ONE 16px face. Blank courses (16), the + -- lower trim (18/19), the portholes (2/3), and the band's corner + -- pieces where a corridor meets it (32/33 white, 48/49 trim, + -- 34/50 the black outer corner). $32 (50) is the shore-set trap: + -- unpinned it recesses into a pond lip in the middle of a + -- bulkhead, and it drags the whole 16x16 cell down with it, + -- because rule 2 judges a cell by its bottom-left tile. + -- + -- The black rim is wall too, not void: 17 is the band's cap seen + -- from above (so the fold tops the bulkhead with it), 21/22 the + -- vertical faces that run down both sides of every corridor, 51 + -- the ship's south bulkhead, and 5/6/37/38 the four corners of the + -- notch each cabin doorway is cut into. Left to the detector + -- these came out 8, 16, 24 and 48 in the same room. Only $01 -- + -- pure black, the space OUTSIDE the hull -- is left alone: the + -- void rule flattens it, which is what darkness should do. + -- + -- 14/15/30/31 are the cabin door drawn INTO the band (its cell is + -- walkable, since the player steps on it to warp, so unpinned the + -- door punched a flat hole through the wall). 84/85 are the + -- captain's chair BACK, drawn into the band above his seat -- so + -- the chair reads as a high-backed swivel chair jutting from the + -- wall instead of a separate tower. 45/61 are the bow deck's + -- rope rail and 46/47/62/63 its stepped bulwark: the same 16px + -- course, so the open deck is fenced at one height. + wall = { 2, 3, 5, 6, 14, 15, 16, 17, 18, 19, 21, 22, 30, 31, 32, + 33, 34, 37, 38, 45, 46, 47, 48, 49, 50, 51, 61, 62, 63, + 84, 85 }, + -- Every table in the group, at 12px: the kitchen's three banquet + -- tables (twelve tile rows deep), the counter run along the + -- kitchen's south wall, the captain's desk, and the writing table + -- in the two houses. Corners 9/12, north edge 10, side edges + -- 25/28, the surface 44, the plain apron 11 and the drawer front + -- 59/60. One 8px band folds up, so the drawn apron stands as the + -- front and every row above rides the top face in drawn order -- + -- which is how the tableware stays tableware: the plate (26), the + -- covered platter (64/65/80/81), the kitchen stove's burner grid + -- (53) and the captain's open book (68/69) are all drawn FROM + -- ABOVE, so they belong lying on the top face, not standing up as + -- cutouts. 59/60 doubles as the chest of drawers at the foot of + -- every cabin bunk; 12px puts it a hand above the mattress, which + -- is where a bedside chest belongs. + table = { 9, 10, 11, 12, 25, 26, 28, 44, 53, 59, 60, 64, 65, 68, + 69, 80, 81 }, + -- The cabin bunk: a mattress drawn from above, so its art stays on + -- the TOP face and it lies low. 70/71 the pillow, 86/87 the + -- mattress. (Its drawer end is in `table` above.) + bed = { 70, 71, 86, 87 }, + -- Seats, in the standee pool: the round stool that appears in + -- every cabin, the badge house and the captain's room (7/8 seat + -- over 23/24 legs), and the captain's own chair (66/67 seat over + -- the same 23/24 base, its back pinned into the wall above). + -- Seat height 8 keeps a character standing on the stool's + -- (walkable) cell sitting at seat height rather than floating. + -- The drawings have real floor margin on all four sides, so the + -- black-outline flood eats the checker around them and leaves the + -- seat whole. + stool = { 7, 8, 23, 24, 66, 67 }, + -- The captain's storage rack: three shelf ranks (54 left, 43 + -- right) drawn four tile rows tall against the wall. TALL, not + -- deep -- the bookcase builder collapses it onto one cell of depth + -- at its drawn height, and adopts the row above (9/12, pinned + -- `table` as the tabletop corners they also are) as its cap. + -- Boxed by the detector it was a 16px stub with the wall beside it + -- towered to 24. + bookcase = { 43, 54 }, + -- The galley barrels -- three down the kitchen's east wall, one in + -- the captain's room, one in each house. A per-pixel cutout in + -- the THIN pool, the treatment Lt. Surge's trash cans get: 72/73 + -- the lid, 88/89 the banded body. 72 is $48, the shore-set trap + -- that had each barrel resolving to water at -2. + prop = { 72, 73, 88, 89 }, + -- The stairs, both flights, in every map that has them (1F, 2F, + -- 3F, B1F and the captain's room). Same two drawings the whole + -- game uses, and the warp table says which is which: 41/42/57/58 + -- is the light flight that always leads UP a deck, climbing east + -- the way its treads step; 39/40/55/56 is the dark stairwell that + -- always leads DOWN, sinking westward. Both cells are walkable, + -- so unpinned both were flat floor and the decks did not connect. + stair_e = { 41, 42, 57, 58 }, + stair_down_w = { 39, 40, 55, 56 }, + -- Left to the derived default on purpose: + -- $01 pure black outside the hull -> void, flat darkness + -- $04 $23 deck planking and grating -> walkable, ground + -- $0D $1D cabin floor checker -> walkable, ground + -- $4A cabin door threshold -> walkable, ground + -- $24 $34 the dark slab under the exit warp in the cabins and + -- the two houses: a threshold mat with no wall around + -- it, walkable, and flat is the honest reading + -- $14 the SEA around SS_ANNE_BOW. It is the one tile id + -- the stale-cache water set names in every tileset, + -- and here that guess is simply RIGHT -- the bow deck + -- really does stand in open water, so it is the one + -- trap tile in this group that must NOT be pinned. + }, + + -- Vermilion Dock: the quay, the boarding gangway, and the S.S. Anne + -- herself moored alongside. One map, and it is the only place in + -- the game where a whole VESSEL is drawn as map art rather than as a + -- building sprite. + -- + -- The detector read the liner as a volume and raised her 48px -- + -- three cells, taller than she is long is wide -- with her columns + -- stepping 48/40/24/0 across the hull, so the drawing was folded + -- UPRIGHT and smeared up a lumpy monolith. Both ends of the hull + -- fell to 0 instead, and the port bow ($40/$41/$51) sat in a cell + -- whose bottom-left tile is open water, so rule 2 sank it to -2 and + -- the ship had a hole in her nose. + -- + -- (This tileset is the one place the engine does NOT count $32 and + -- $48 as shore -- Map.lua's NO_SHORE_TILESETS -- because $32 IS the + -- gangway here. So the usual shore trap does not apply, and $14 is + -- honest open water. Nothing in this entry is dodging a water + -- fallback; the whole entry is about the ship.) + SHIP_PORT = { + -- The hull, every tile of her, as a `roof`: a box wearing its art + -- on the TOP face. That is what the drawing IS -- the liner seen + -- from above and slightly astern, funnels and boats and deck rail + -- on the top rows, hull plating on the bottom two -- so folding it + -- upright was always going to smear. 28px of freeboard reads as a + -- liner beside a quay at 0 and water at -2 without towering, and + -- the side bands crop from each edge tile's own art, so the flank + -- the player faces wears the hull plating that is drawn there. + -- Rows north to south: bow rail and boat deck (0/2-7/9/11-15), + -- superstructure (16-31), midships (32-47), waterline strake + -- (48-53 and 61-63 in the stern), hull plating (64-69, 77-79) and + -- the boot topping (81-85, 90-93). + roof = { 0, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 21, 22, 23, 24, 25, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, + 48, 51, 52, 53, 54, 55, 56, 57, 61, 62, + 64, 65, 66, 67, 68, 69, 77, 78, 79, + 81, 82, 83, 85, 90, 91, 93 }, + -- The quayside crates, a 2x2-tile box each (86/87 lid, 1/80 + -- body), stacked two cells deep in all four corners of the dock, + -- and the pallet stacks flanking the gangway head (49). A crate + -- IS a 16px cube: one course, art folded up the face it shows. + -- Unpinned the corner stacks fused into one taller volume. + wall = { 1, 49, 80, 86, 87 }, + -- The delivery van parked on the north quay: drawn face-on, four + -- tiles by two, with clear pavement all round it -- so it takes + -- the standing per-pixel cutout with a body (10 voxels), not a + -- rectangular block that would swallow the air above its cab. + -- 72/73/74/75 the cab and box, 88/89/60/76 the wheels and skirt. + billboard = { 60, 72, 73, 74, 75, 76, 88, 89 }, + -- Left to the derived default on purpose: + -- $0A the quay's paving -> walkable, ground + -- $32 $3B the gangway planks -> walkable, ground, so the walk + -- from the quay to the warp stays flush at 0 and + -- meets the hull's flank rather than climbing it + -- $14 open water -> the tileset's own water, -2 + -- $3A the shaded strip along the quay's foot. Its cell's + -- bottom-left tile is water, so rule 2 puts it at + -- water level -- which is correct: it is the dock's + -- shadow lying ON the water, and the 2px lip between + -- it and the paving is exactly the shoreline the + -- water class exists to draw. + }, + + -- The facilities: twenty-one maps off one 96-tile atlas -- Silph Co's + -- ten office floors, the four Rocket Hideout basements, the four + -- Pokemon Mansion floors, the Power Plant, and the Cinnabar and + -- Saffron gyms. One pin therefore has to read right in a laboratory, + -- an open-plan office, a burnt mansion and a gym at once; where a + -- graphic genuinely means two things the note says which reading won. + -- The failures are the usual indoor ones, louder: the detector raised + -- whole wall COLUMNS to 48px, stood the lab consoles up as 32px + -- cabinets towering over the very band they are drawn INTO, fused + -- vertical runs of computer terminals into 48px monoliths, boxed the + -- potted-plant rows into one slab wearing leaves on its top face, and + -- left both staircases lying flat on the floor. Silph Co's lobby + -- island is tile $14 -- the engine's stale-cache water id -- and sank + -- into a pond in the middle of the foyer. + FACILITY = { + -- The wall band stays ONE 16px face: the dark courses (42/58 + -- horizontal, 43/44 vertical, 45/46 the top corners, 59/60 the + -- bottom caps) and Silph Co's own pale striped band (87/89). A + -- north-south run of 43/44 is one continuous drawing, so unpinned + -- the detector measured the whole column and raised it to 48. + -- + -- Everything DRAWN BUILT INTO that band is `wall` too, the + -- Center's-healing-console rule, so it reads as equipment jutting + -- from the band rather than a separate tower: + -- 74/75 the machine's two dials over 76/77 its footed base -- one + -- graphic serving the Cinnabar Gym's quiz consoles, Silph Co's + -- and the Power Plant's equipment banks and the Mansion's lab + -- rigs. Drawn three rows tall under a one-row wall cap, which + -- is why the detector called it 32 + -- 9/10 screen over 25/26 base: the computer terminal. It is + -- ALSO the free-standing maze wall of Rocket Hideout B2F/B3F + -- and the terminal rows of the Power Plant hall, and 16 -- its + -- drawn height -- is right in every one of them + -- 64/80 the Mansion 2F balustrade seen face-on, 65/81 its + -- north-south run. 65 repeats down a whole column, so it + -- towered exactly like the wall did + -- 86/88 the lift doors of Silph Co: a walkable warp cell, but a + -- pin is look-only, so the doors stand in the wall band + -- instead of lying flat as a mat (the Center does the same + -- with its window/warp tile) + -- 8/24 and 36/37 the shutters the card-key doors and the + -- Cinnabar Gym quiz gates stamp CLOSED at runtime + -- (field.cardKeyDoors' blocks $54/$5F/$2D). They are in no + -- map's block list, so nothing but this line covers them; the + -- detector read them as an 8px stub lying in the doorway + wall = { 8, 9, 10, 24, 25, 26, 36, 37, 42, 43, 44, 45, 46, + 58, 59, 60, 64, 65, 74, 75, 76, 77, 80, 81, 86, + 87, 88, 89 }, + + -- The tall display cabinet of the Mansion and Silph 3F/8F/9F: cap + -- row 40/41 over two ranks of 56/57, one cell wide and drawn FOUR + -- rows tall. A tall drawing, not a deep one, so it takes the + -- Mart's shelf-rack treatment and collapses onto a one-cell-deep + -- box at its full 32px -- as `wall` it flattened into a bench two + -- cells deep, which is the shape of its footprint, not the shape + -- of the thing. Its base course is the computer terminal's own + -- 25/26, listed under `wall` above and therefore shared: that + -- stays a 16px block, and reads as the cabinet's plinth. + bookcase = { 40, 41, 56, 57 }, + + -- The grey slab family -- every desk, bench, worktop and service + -- counter in the tileset is cut from these seven pieces: 13/14 the + -- top corners, 2 the top edge between them, 29/30 the body's side + -- edges, 53 its fill, 68/69 the legs, 18 the counter's front panel + -- (the tileset's own counterTiles). + -- + -- `counter` -- 8px, half a cell -- not `table` or `desk`, because + -- the drawing is a TOP surface seen from above with one row of + -- front elevation at its south end: one clean band folds exactly + -- that bottom row (68/69 legs, or 18) up as the front and rides + -- every row above it on the top face in drawn order, which is what + -- a seven-cell Silph Co bench actually looks like. At 12 or 16 + -- the same drawing reads as a wall stub, the failure the Center's + -- counters are pinned against. + -- + -- 13/14 and 29/30 double as the CAP and body of the wall consoles + -- (blocks $60/$62/$64/$66/$7F). The benches win the tie on count + -- -- 142 block placements across 18 maps against the consoles' 105 + -- across 14 -- and the compromise costs the consoles nothing: the + -- capped rows sit NORTH of the 16px dial face, so an 8px ledge + -- behind a 16px machine is occluded from every southward camera. + counter = { 2, 13, 14, 18, 29, 30, 53, 68, 69 }, + + -- Plants, the thin standee pool, as in every other interior. Two + -- drawings: the tall potted palm of Silph Co, the Hideout and the + -- Mansion (crown 5/6/21/22 over pot 7/15/23/31, a 1x2-cell + -- object), and the gym planter of Cinnabar, Saffron and the + -- Mansion (crown 39/47, bowl 55/63, box 61/62). Unpinned the + -- detector fused a six-plant row into one box wearing the leaf + -- dither as a lid. The crown's checker highlights share the + -- floor's shades so the mask drains some of them -- the same + -- accepted trade as the Center's bush. + prop = { 5, 6, 7, 15, 21, 22, 23, 31, 39, 47, 55, 61, 62, 63 }, + + -- Both flights UP, both climbing EAST. 3/4/19/78 is the one-cell + -- open staircase of the Mansion (1F->2F, 2F->3F) and the Hideout + -- (B1F->Game Corner): white treads whose risers step up toward the + -- upper right. 90/91 over 67/52 is Silph Co's own stairwell, set + -- into the north wall band on every floor -- courses of tread whose + -- east ends step back one notch at a time. BOTH cells are + -- WALKABLE (19 and 67 are the tileset's warp tiles), so unpinned + -- they resolved to flat ground and the steps were painted on the + -- floor. Silph's drawing is the one genuinely ambiguous flight in + -- the tileset -- read as a west rise it builds a ramp that leaves a + -- gap against the east jamb, so east it is, which also makes one + -- consistent story out of every up-stair in the group. + stair_e = { 3, 4, 19, 78, 52, 67, 90, 91 }, + -- The staircase DOWN (11/12/27/28): three treads in a black + -- stairwell, shortening and darkening west to east, so the flight + -- descends EAST into the dark. One graphic again, for every + -- down-stair in the group. (Where the Mansion 3F variant hangs + -- directly under the wall band, block $73, the engine's door fold + -- claims the cell first -- 27 is one of the tileset's doorTiles -- + -- and stands the art up in the band as a doorway. That reads + -- fine, and the fold runs after this profile, so it is not + -- something a pin can or should undo.) + stair_down_e = { 11, 12, 27, 28 }, + + -- The rubble drifts of the Mansion and the Power Plant (83/84 over + -- 54/38): two black-outlined boulders per cell on a plain grey + -- ground, tiling wall-to-wall across whole rooms. Drawn ROUND, so + -- they take the overworld canopy's archetype -- one voxel hull per + -- 16x16 cell carved from the darkest-pixel outline -- which is the + -- only class that keeps a repeating field as separate lumps: a + -- standee pool would cluster a whole drift into one giant flat + -- cutout, and the volume path gave 16px slabs with 48px towers + -- wherever a drift touched a wall. + cylinder = { 38, 54, 83, 84 }, + + -- Silph Co 1F's lobby island (tile $14), the cross-shaped dither + -- the reception terminals ring. $14 is one of the three ids the + -- engine's stale-cache water set claims in EVERY tileset, and it + -- is not on this tileset's walkable list, so it fell straight to + -- `water` and opened a pond in the middle of the foyer. It is a + -- floor-level mat drawn from above: flat. + ground = { 20 }, + + -- Left to the derived defaults on purpose, having been checked: + -- 51, the Hideout's black fill, is all-black art, so the void + -- rule already flattens it -- which is what a room's interior + -- darkness should be + -- 32/33/48/49 -- the four quarter-tiles the Hideout's spinner + -- arrows and the Saffron/Silph warp diamonds are assembled + -- from -- and 94, the Hideout's floor panel, are on the + -- tileset's walkable list and come out flat ground unaided + -- 1 (the checker floor), 17 (the Mansion's upper-floor boards) + -- and 66/82 (the entrance carpet) likewise + -- the other two stale-cache water ids, $32 and $48, are never + -- placed by any of the twenty-one maps, so only $14 above + -- needed the guard + }, + + -- Pokemon Tower's seven floors and Agatha's room (one tileset). + -- Every floor is the same octagonal chamber: a ring of wall panels + -- ($09/$0A over $19/$1A) cut out of a mid-grey mass ($11), with a + -- field of gravestones inside it, and Agatha's room is that chamber + -- shrunk. The ring and the mass the detector already reads as one + -- 16px course (probed) -- they are listed anyway so the answer is + -- authored rather than inferred from a repeat count, and so the + -- panels can never fuse into the gravestones that touch them. + -- What the detector got WRONG was everything else: the headstones + -- came out as 8px stubs with their arched tops dropped, both + -- stairwells were flattened by the walkable-cell rule, and the + -- potted palms lost their crowns to a 16px box. + -- (Nothing here needs pinning against the void rule: the tileset's + -- one all-black tile is $47, and no map in the group places it.) + CEMETERY = { + -- One 16px course: the chamber's wall ring ($09/$0A over + -- $19/$1A), the grey mass beyond it ($11) so the room reads as a + -- chamber cut into solid stock rather than a fence standing on a + -- plain, the roped-off corner of 1F ($02 over $12 running + -- east-west, seen face-on; $1D/$1E running north-south, seen from + -- above -- 8 rows of it, which the volume path would have towered + -- to 64px), and Agatha's north band ($20 over $30). + wall = { 9, 10, 25, 26, + 17, + 2, 18, 29, 30, + 32, 48 }, + -- The gravestones ($05/$06 over $15/$16): a headstone drawn + -- face-on, one per cell, 314 of them across the seven floors and + -- Agatha's room. The volume path measured the drawing as its + -- bottom row alone and left an 8px stub -- the plinth without the + -- stone. `post` is the per-CELL standee pool: each cell is + -- extracted on its own as a per-pixel cutout 6 voxels deep, so + -- the arched top comes back and a 4x2 block of graves stands as + -- eight separate stones instead of one 32px sheet with the back + -- row floating above the front (which is what one shared + -- billboard cluster would have made of them). The stone's white + -- side margins let the background flood in; its own white checker + -- band is sealed by the black arch and survives. + post = { 5, 6, 21, 22 }, + -- The flight UP to the next floor ($03/$04 over $13/$14): real + -- rising steps, climbing east the way the risers are drawn (short + -- at the west, tall at the east -- the same drawing idiom as Red's + -- house). Its collision tile is $13, which IS in the tileset's + -- walkable list, so rule 3 had laid the whole flight flat; and its + -- south-east tile is $14, the water-fallback trap id -- harmless + -- only because the cell's own bottom-left is $13, and pinned here + -- so it can never matter. + stair_e = { 3, 4, 19, 20 }, + -- The flight DOWN ($0B/$0C over $1B/$1C): a sunken stairwell. + -- The drawn step edges are high and lit at the WEST and fall away + -- dark to the east, so the flight descends eastward -- the mirror + -- of Red's bedroom stair. Walkable ($1B) and flattened for the + -- same reason as the flight up. + stair_down_e = { 11, 12, 27, 28 }, + -- The potted palm that flanks Mr. Fuji's shrine on 7F and stands + -- in Agatha's room ($27/$2F crown, $37/$3F fronds, $3D/$3E + -- planter): a plant, so the THIN standee pool, like every other + -- interior plant. It is drawn 24px tall across THREE tile rows, + -- which is why the box path dropped its crown row entirely and + -- kept a 16px planter; a standee takes the whole drawing at its + -- real height. It backs onto the wall ring, and a separate pool + -- keeps it from being absorbed into it. + prop = { 39, 47, 55, 63, 61, 62 }, + -- (Nothing else needs an entry. The tower's floor ($01) and 5F's + -- healing pad ($22) are walkable, so rule 3 lays them flat where + -- they are drawn; the mass, ring, graves, stairs and palm above + -- are every other tile the eight maps place.) + }, + + -- The two Underground Paths (Route 5<->6 north-south, Route 7<->8 + -- west-east): one blockset, two maps, and the plainest geometry in + -- the game -- a tiled corridor cut out of black. Most of it was + -- already right, because the black it is cut from ($10) is solid + -- black and the void rule flattens it (`10v00` over every border + -- block), and the lattice floor ($0B/$0C) with the diagonal shadow + -- strip along its west wall ($15/$18) are walkable and lie flat. + -- Two things were not. + -- + -- The SOUTH rim. The corridor's near wall is drawn as a single 8px + -- light line ($02) with black under it, so the volume builder + -- measured a one-row drawing and raised half a course -- an 8px + -- kerb (`02w08` along the whole bottom row of + -- UNDERGROUND_PATH_WEST_EAST) facing a proper 16px far wall + -- (`06w16`/`09w16`). Pinned, the corridor closes with one band all + -- round. The east and west rim lines ($16/$17) already read 16 -- + -- their columns repeat -- and join the same authored course so the + -- whole enclosure is one decision. + -- + -- The STAIRS. $03/$04 over $13/$14 is a real flight drawn in + -- profile, treads climbing to the upper right, and it is the warp + -- back up to the surface at both ends of both corridors. Its cell + -- is walkable, so it resolved to flat ground and the staircase was + -- a painting (`03g00 04g00 / 13g00 14g00`). stair_e builds the + -- flight the drawing shows, rising east. $14 is also the $14 + -- WATER-FALLBACK id: the walkable cell was hiding the trap here + -- rather than dodging it, and the pin settles it either way. + UNDERGROUND = { + wall = { 6, 9, -- $06 over $09, the far wall band + 2, -- $02, the near wall's light line + 22, 23 }, -- $16/$17, the east and west rims + stair_e = { 3, 4, 19, 20 }, -- $03/$04 over $13/$14 + -- The blockset also carries the wall's unplaced corner variants + -- ($05/$08 north-west, $07/$0A north-east, $11 south-west, $12 + -- south-east). Neither map places them; they belong with the + -- wall band above if one ever does. + }, }, -- Buildings whose whole sprite is voxelized band by band (lib/Buildings.lua, diff --git a/lib/Structures.lua b/lib/Structures.lua index 858f636..4bbc587 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -205,6 +205,14 @@ function Structures.forMap(map) -- the fold then shows the door art standing at ground level in the -- building's front face. Door graphics only (the tileset's doorTiles); -- interior stair/mat warps stay flat. + -- + -- A PROFILE PIN WINS over the fold. The fold is detection, and rule 1 + -- of the resolution order is that an authored tile bypasses detection + -- -- but this used to overwrite shapeAt unconditionally, so a pin on + -- any tile the tileset also lists in doorTiles was dead on arrival. + -- Celadon Mansion is the case that found it: all four of its + -- staircases are door tiles, so `stair_e` / `stair_down_w` pins there + -- silently did nothing and the flights stayed painted on the floor. for cy = math.floor(y0 / 2), math.floor(y1 / 2) do for cx = math.floor(x0 / 2), math.floor(x1 / 2) do if map.doorTiles[map:cellTile(cx, cy)] then @@ -214,11 +222,14 @@ function Structures.forMap(map) for dy = 0, 1 do for dx = 0, 1 do local dk = keyOf(cx * 2 + dx, cy * 2 + dy) - shapeAt[dk] = shapes.classes.wall - -- remembered for buildVolume: a folded doorway column - -- answers to its REGION for height and top, not to its - -- own drawn extent (see the door adoption there) - S.doorFold[dk] = true + local ds = shapeAt[dk] + if not (ds and ds.authored) then + shapeAt[dk] = shapes.classes.wall + -- remembered for buildVolume: a folded doorway column + -- answers to its REGION for height and top, not to its + -- own drawn extent (see the door adoption there) + S.doorFold[dk] = true + end end end end diff --git a/main.lua b/main.lua index 9b01be9..a1f2b8c 100644 --- a/main.lua +++ b/main.lua @@ -376,7 +376,7 @@ mod.events:on("map.reloaded", function(payload) if mapId then ChunkMesher.invalidate(mapId) end end) -mod.exports.version = "1.0.4" +mod.exports.version = "1.0.5" -- exposed so a companion mod can pin its own tiles' shapes or read the -- camera without reaching into this mod's file layout mod.exports.lib = V diff --git a/manifest.json b/manifest.json index fa53bbb..9fa4490 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "DRAMATIC_SHAPE", "name": "Dramatic Shape Voxel Mod", - "version": "1.0.4", + "version": "1.0.5", "api": 2, "entry": "main.lua", "profile": "content", From 7f41f323f553e917048322a69d818c3a191553e2 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 03:06:27 -0400 Subject: [PATCH 12/16] one standing clump per grass tile; ledge pillars at ledge height Grass: each 8x8 tile is one whole clump, so it stands as ONE full-height slab at its own depth. The first cut split every tile again into its top and bottom four art rows and stood those at two different depths, which cut each blade in half -- two 4px stubs 4px apart rather than a tuft. A cell's 2x2 tiles still stand independently, so the player walks between the north and south rows. Ledges: $34 is the cliff slope's foot AND the pillar between hop-down segments. Pinned `wall` with the rest of the slope chain it stood 16px beside a 6px lip, breaking the ledge line with blocks nearly three times its height. At ledge height the run reads continuous, and the Diglett mound is unchanged -- its foot row reads as the talus it is drawn as. --- data/voxel_heights.lua | 30 ++++++++----- lib/Structures.lua | 100 ++++++++++++++++++++++------------------- 2 files changed, 73 insertions(+), 57 deletions(-) diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 6f10a89..77e2b21 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -111,7 +111,15 @@ return { -- the hop-down edges named by data.field.ledges' ledgeTile: their -- art is a ground lip seen from above, which the detector would -- otherwise raise as a wall - ledge = { 13, 29, 39, 54, 55 }, + -- $34 is the cliff slope's FOOT, and it also punctuates the ledge + -- rows: the hop-down runs are set into the cliff face and this + -- tile is the pillar between segments. It was `wall` with the + -- rest of the slope chain, which stood those pillars 16px beside + -- a 6px lip -- the ledge line came out interrupted by blocks + -- nearly three times its height. At ledge height the run reads + -- as one continuous lip, and the mound loses nothing: its foot + -- row simply reads as the talus it is drawn as. + ledge = { 13, 29, 39, 52, 54, 55 }, -- trees are drawn ROUND -- the lone canopy (42/43/58/59) and the -- border tree wall (64/65/80/81, blockset $0F). Boxes and per-pixel -- cutouts both read wrong for them; the cylinder archetype carves @@ -129,15 +137,17 @@ return { -- fence-textured tower. `post` extracts each cell alone, so these -- render as the same thin posts, marching north post = { 14, 85 }, - -- the cliff-mound's dark east slope and its corners ($02 the NE - -- corner, $24 the slope column, $34 the slope-to-rim seat). Their - -- drawn runs span the whole mound drawing, so the detector raised - -- them to 32px towers -- the rock pillar beside Diglett's Cave -- - -- and the doorway column, which adopts its REGION's height, - -- inherited the same 32 and put the cave entrance a block above - -- the mound around it. Pinned to one 16px course they match the - -- plateau body, and the doorway drops with them. - wall = { 2, 36, 52 }, + -- the cliff-mound's dark east slope and its NE corner ($24 the + -- slope column, $02 the corner). Their drawn runs span the whole + -- mound drawing, so the detector raised them to 32px towers -- + -- the rock pillar beside Diglett's Cave -- and the doorway + -- column, which adopts its REGION's height, inherited the same 32 + -- and put the cave entrance a block above the mound around it. + -- Pinned to one 16px course they match the plateau body, and the + -- doorway drops with them. ($34, the slope's foot, is `ledge` + -- instead -- see there.) + wall = { 2, 36 }, + -- the cuttable bush ($2D/$2E/$3D/$3E, the four tiles Cut deletes -- -- across the whole tileset they appear only in the five -- cut-tree blocks): a standing per-pixel cutout 5 voxels deep, diff --git a/lib/Structures.lua b/lib/Structures.lua index 8162040..6ff85f7 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -2091,14 +2091,21 @@ end -- ---- tall grass ---- --- A tall-grass tile draws its four tuft segments in two ROWS: the art's --- top half is one row of grass, the bottom half another. Each row stands --- as a thin per-pixel slab at its own drawn depth inside the tile, over --- the flat grass base the tile already renders -- so the player walks --- BETWEEN rows, and the southern row occludes their feet the way the 2D --- grass overdraw did. Transparency respected: only the tuft strokes stand. --- Runs of adjacent pixels merge into single quads, and one template per --- grass tile id is stamped across the map (grass comes in fields). +-- A tall-grass CELL is four tufts: 2x2 tiles, and each 8x8 tile is one +-- whole clump of grass. Each tile stands as its own thin per-pixel slab +-- at ITS OWN depth -- the cell's north tile row in the north half of the +-- cell, the south row in the south half -- over the flat grass base the +-- tile already renders. So the player walks BETWEEN the two rows, and +-- the southern row occludes their feet the way the 2D grass overdraw +-- did. Transparency respected: only the tuft strokes stand. Runs of +-- adjacent pixels merge into single quads, and one template per grass +-- tile id is stamped across the map (grass comes in fields). +-- +-- One tile is ONE standing piece, full height. The first cut split each +-- tile again into its top and bottom four art rows and stood those at +-- two different depths, which cut every blade that runs down the tile +-- clean in half -- the two halves ended up 4px tall and 4px apart in +-- depth, so a clump read as two stubs rather than one tuft. local GRASS_THICK = 2 local function grassTemplate(map, data, tileId) @@ -2115,49 +2122,48 @@ local function grassTemplate(map, data, tileId) end local quads = {} - for half = 0, 1 do - local rowBase = half * 4 - local zMid = half == 0 and 2.5 or 6.5 - local zB, zF = zMid - GRASS_THICK / 2, zMid + GRASS_THICK / 2 - for iy = 0, 3 do - local yTop = 4 - iy - local yBot = yTop - 1 - local ix = 0 - while ix < 8 do - if opaque(ix, rowBase + iy) then - local ix2 = ix - while ix2 + 1 < 8 and opaque(ix2 + 1, rowBase + iy) do - ix2 = ix2 + 1 - end - local u0 = (ax0 + ix + 0.05) / atlasW - local u1 = (ax0 + ix2 + 0.95) / atlasW - local v0 = (ay0 + rowBase + iy + 0.05) / atlasH - local v1 = (ay0 + rowBase + iy + 0.95) / atlasH - quads[#quads + 1] = { -- front - { ix, yBot, zF }, { ix2 + 1, yBot, zF }, + -- the slab stands across the middle of its own tile, so the two tile + -- rows of a cell are half a cell apart in depth + local zMid = 4 + local zB, zF = zMid - GRASS_THICK / 2, zMid + GRASS_THICK / 2 + for iy = 0, 7 do + local yTop = 8 - iy + local yBot = yTop - 1 + local ix = 0 + while ix < 8 do + if opaque(ix, iy) then + local ix2 = ix + while ix2 + 1 < 8 and opaque(ix2 + 1, iy) do + ix2 = ix2 + 1 + end + local u0 = (ax0 + ix + 0.05) / atlasW + local u1 = (ax0 + ix2 + 0.95) / atlasW + local v0 = (ay0 + iy + 0.05) / atlasH + local v1 = (ay0 + iy + 0.95) / atlasH + quads[#quads + 1] = { -- front + { ix, yBot, zF }, { ix2 + 1, yBot, zF }, + { ix2 + 1, yTop, zF }, { ix, yTop, zF }, + uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, + shade = 1, + } + quads[#quads + 1] = { -- back + { ix2 + 1, yBot, zB }, { ix, yBot, zB }, + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, + uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, + shade = 0.68, + } + -- blade tips: a top strip where the row above is clear + if not opaque(ix, iy - 1) then + quads[#quads + 1] = { + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, { ix2 + 1, yTop, zF }, { ix, yTop, zF }, - uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, + uv = { { u0, v0 }, { u1, v0 }, { u1, v0 }, { u0, v0 } }, shade = 1, } - quads[#quads + 1] = { -- back - { ix2 + 1, yBot, zB }, { ix, yBot, zB }, - { ix, yTop, zB }, { ix2 + 1, yTop, zB }, - uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, - shade = 0.68, - } - -- blade tips: a top strip where the row above is clear - if not opaque(ix, rowBase + iy - 1) then - quads[#quads + 1] = { - { ix, yTop, zB }, { ix2 + 1, yTop, zB }, - { ix2 + 1, yTop, zF }, { ix, yTop, zF }, - uv = { { u0, v0 }, { u1, v0 }, { u1, v0 }, { u0, v0 } }, - shade = 1, - } - end - ix = ix2 + 1 - else - ix = ix + 1 end + ix = ix2 + 1 + else + ix = ix + 1 end end end From 89b6c8513f9416fc422e17e55d5e09e97908f248 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 03:54:33 -0400 Subject: [PATCH 13/16] 1.0.6: conditional pins, Pokemon Tower, plateau statues, cave elevation Adds `when_above` conditional pins, resolved per position in TileShape.at: one graphic can mean two things (the gates' $32 is both wall base course and counter front), and a flat pin has to pick one. Gates get half-height counters and level walls. Pokemon Tower gains a buildings entry -- it is the drawing the map edge cuts off, sealed on the north -- and the Indigo Plateau statues are built like the gym statues. Fixes: grass stands one full-height clump per tile instead of two half-cut stubs at different depths; ledge pillars drop to ledge height; a prop only rides furniture when its own cell is blocked (chairs were being lifted onto tables); and the caves had water and rock pinned backwards, which stood the Seafoam sea up as rock and cut trenches through Mt Moon. No cave tile is below the datum now. --- CHANGELOG.md | 89 ++++ data/voxel_heights.lua | 900 +++++++++++++++++++++++++++++++---------- lib/Structures.lua | 16 +- lib/TileShape.lua | 57 ++- main.lua | 2 +- manifest.json | 2 +- 6 files changed, 849 insertions(+), 217 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b04f9b0..7b081a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,94 @@ # Changelog +## 1.0.6 + +### Added + +- **Conditional pins.** A profile entry may now carry `when_above`: + tile id -> rules keyed on the tile drawn directly north of it, + resolved per POSITION in `TileShape.at`. A pin is per tile id and one + graphic can mean two things -- the route gates' `$32` is both the + wall's dark base course and every service counter's front, and it is + the bottom row of its cell either way. Pinned `wall` the counters + stood a full 16px; pinned `counter` the deep wall banks corrugated + 16/8 for sixteen rows and the room read as crates. What separates the + two uses is what sits on top, so that is what the rule reads. The + gates now have half-height counters AND level walls. + +- **The Pokemon Tower has an exterior.** It is the one catalogued + building drawing the map edge cuts off (no roof band is on the map at + all), and it had no `buildings` entry, so it fell to the volume path + -- which tops a run by repeating its first two rows, laying window + courses flat across the plateau. Sealing the silhouette on the north + alone closes it (88% fill, one piece, against 37% and 126 pieces + unsealed), and one row of roof band spent on the drawing's top margin + costs no window course. It stands as a real tower, panes recessed, + door on the ground. + +- **The Indigo Plateau statues stand up**, built exactly like the gym + statues: plinth a solid 16px block, figure a per-pixel cutout riding + it. On the avenue the statues stack with no gap, so the flood joined + six of them into one 24-row region and the volume builder raised + ridges of boxes with the statue art folded on the front. The same + bird is drawn at the foot of every badge-check pillar, so those are + crowned too. + +### Fixed + +- **Tall grass: one clump per tile.** Each 8x8 tile is a whole clump, + but the template split every tile AGAIN into its top and bottom four + art rows and stood those at two different depths -- so any blade + running down a tile was cut in half, into two 4px stubs 4px apart. + One tile is now one full-height standing slab at its own depth; a + cell's 2x2 tiles still stand independently, so the player walks + between the north and south rows. + +- **Ledge lines are continuous.** `$34` is the cliff slope's foot and + also the pillar between hop-down segments; pinned `wall` with the + rest of the slope chain (the Diglett's Cave fix) it stood those + pillars 16px beside a 6px lip. At ledge height the run reads as one + lip, and the mound is unchanged -- its foot row reads as the talus it + is drawn as. + +- **A prop only stands on furniture when its own cell is blocked.** + "Is something drawn above me" is not "am I standing on it": a chair + drawn against the north side of a table is above the table's trim row + too, and was being lifted onto the tabletop, with its claimed cells + re-tiled as tabletop so the table marched two rows north. Three + chairs in Cinnabar's trade room and Fuchsia's meeting room, and the + Celadon diner's stools. The world already knows the difference: a + thing that sits ON furniture occupies a blocked cell, a seat you walk + up to is in a walkable one. + +- **Caves: nothing below sea level, and the water is water.** Two tiles + were identified backwards in the first pass. `$14` -- the tile the + engine animates, that `Map.WATER_TILES` names and that Surf runs on + -- was pinned `wall`, so Cerulean Cave's lake and the Seafoam sea + stood up as rock slabs. And the pale dithered rock fill was pinned + `water`, cutting 612 tiles of two-cell-wide trench through four maps. + Both corrected; a sweep of all 19 cave maps now reports zero tiles + below the datum. The elevation scheme is documented in the entry and + derived from the game's own `tilePairs`: dark floor, water and drop + holes at 0, the lit shelf a 6px step above, rock at 16. + +- **Cave ladders climb.** Which ladder graphic goes up and which goes + down is unanimous in the warp table -- 37 cells of one always warp + down, 40 of the other always up -- so they are real stepped flights + now, not painted plates. + +- **Poke Mart's register stands on the counter.** The pin was on the + wrong tile: `$08` is the counter's own top band, not the register, so + the standee flood ate everything but two black lines. The register is + the keypad-and-receipt drawing one row up. + +- **Celadon's televisions**, which were solid 16px boxes wearing the TV + art on one face, and the **Pokemon Tower reception desk** and the + **gate counters**, which stood at wall height, are all their drawn + heights now. The **Fan Club and Silph boardroom statues** were read + as seated chairmen and painted onto the tabletop; they are cutouts + standing on their pedestals, and the tables are cut to a true + octagonal footprint. + ## 1.0.5 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 77e2b21..e171f27 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -291,14 +291,39 @@ return { -- The caves: one blockset serves nineteen maps -- Mt Moon's three -- floors, Rock Tunnel's two, Seafoam Islands' five, Victory Road's -- three, Cerulean Cave's three, and Diglett's Cave with its two - -- route-side stubs. A Gen 1 cave is drawn as TWO floor levels: a - -- dark lower floor ($20 rough, $21 bright checker, $2A fine checker) - -- and LIT shelves ($05) raised above it. data.field.tilePairs - -- forbids stepping between the two ($20/$21/$2A/$41 <-> $05), which - -- is what the shipped `ledge` pin on $05 records: the lit floor is a - -- step up off the dark floor, reached only by the drawn ramps. - -- Everything else in here is rock, and in a dungeon the rock IS the - -- room -- so every rock tile is pinned to ONE 16px course. + -- route-side stubs. + -- + -- THE CAVE'S VERTICAL SCHEME. A Gen 1 cave is drawn on two floor + -- levels, and the game's own collision data says which is which. + -- data.field.tilePairs forbids stepping between $20/$21/$2A/$41 and + -- $05 on land, and between $14 and $05 while SURFING -- but nothing + -- forbids $14 <-> $20. Read that back and the elevation falls out: + -- the dark floor and the water are one plane, the lit floor is a + -- step above both, and the rock is the room around them. + -- + -- h what tiles + -- 0 DARK LOWER FLOOR -- the datum $20 $21 $2A + -- 0 CAVE WATER, the surfable pool $14 + -- 0 fall-through drop hole $2F $22 + -- 0 unlit black (the void rule, no pin) $3C + -- 3 Victory Road's boulder switch plate $2B $2C $2D $2E + -- 6 LIT SHELF, one step up (`ledge`) $05 $29 + -- 6 the stair plate down off that shelf $15 $16 + -- 16 ROCK -- one band for the masses, the everything else + -- faces, the caps and the ceiling mass + -- 0 up 16 ladder shaft UP a floor $0A $0B $1A $1B + -- 0 dn 16 ladder shaft DOWN a floor $08 $09 $18 $19 + -- + -- NO CAVE SURFACE SITS BELOW 0. The datum is the dark floor; the + -- water is level with it (you surf off a pool straight onto the dark + -- floor, which is exactly what the missing $14/$20 tile pair says); + -- every other surface is above it. A cave is underground, not + -- underwater, and the only class in this file that goes negative is + -- `water` (-2), which exists to cut the overworld's shoreline lip. + -- It is deliberately NOT used anywhere in this entry. The one thing + -- that does reach below the datum is the DOWN ladder's stairwell, + -- which is a hole on purpose: it is the shaft to the floor below, + -- and no walkable surface is drawn in it. -- -- What the detector made of it: the rock EDGES towered. A shelf's -- side is drawn as a cap over a run of face tiles over a corner @@ -319,84 +344,165 @@ return { -- volume builder, so pinning the rock is the whole fix: there is no -- run left to misread. CAVERN = { - -- The rock, one 16px course. Reading across the blockset: - -- $02/$03 over $12/$13 is the speckled rock mass and $0C/$0D over - -- $1C/$1D the boulder-cluster mass, both solid wall-to-wall - -- fills; $10/$11 is the cobble course facing the south side of - -- every lit shelf, $31 its west face and $17 its east; $04/$07/ - -- $28 are the caps and $25/$26 the inner corners where two faces - -- meet. $0E/$0F over $1E/$1F is the barred rock shelf that - -- stands alone on the dark floor (MT_MOON_1F, ROCK_TUNNEL_1F and - -- twice in SEAFOAM_ISLANDS_B4F): one cell of face-on drawing, so - -- one 16px face -- never a standee, its black surround touches - -- all four rims and would segment into a solid slab. + -- ---- 16: the rock, one band ---- -- - -- $14 is the bright rubble mass that fills Cerulean Cave and the - -- lower Seafoam floors, and it is the $14 WATER-FALLBACK TRAP: - -- unpinned it hits the engine's stale-cache water set, and every - -- cell it floors RECESSED -- probed `14w-2` over whole blocks of - -- SEAFOAM_ISLANDS_B4F and CERULEAN_CAVE, a cavern of ponds. + -- Reading across the blockset: $02/$03 over $12/$13 is the + -- speckled rock mass and $0C/$0D over $1C/$1D the boulder-cluster + -- mass, both solid wall-to-wall fills; $10/$11 is the cobble + -- course facing the south side of every lit shelf, $31 its west + -- face and $17 its east; $04/$07/$28 are the caps and $25/$26 the + -- inner corners where two faces meet. $0E/$0F over $1E/$1F is the + -- barred rock shelf that stands alone on the dark floor + -- (MT_MOON_1F, ROCK_TUNNEL_1F and twice in SEAFOAM_ISLANDS_B4F): + -- one cell of face-on drawing, so one 16px face -- never a + -- standee, its black surround touches all four rims and would + -- segment into a solid slab. + -- + -- $06/$27 over $24/$01 is the CEILING MASS: a pale dithered rock + -- fill with a black rim dithered along its north and south edges, + -- so a run of it tiles into the grid of light blocks that walls + -- off most of MT_MOON_B2F (37 cells) and stands as one lump in + -- each of VICTORY_ROAD_1F/2F/3F. It is blocked in every map that + -- places it ($24 is not on the walkable list) and it is NOT in the + -- engine's water set, so it is rock and belongs in the band with + -- the rest of the rock. The first pass read the checker as a pond + -- and pinned it `water`: that recessed 612 tiles to -2 and cut + -- open trenches down the middle of Mt Moon B2F -- 06w-2 27w-2 / + -- 24w-2 01w-2 for ten cells straight, hard against 31w16 rock and + -- the 05l06 shelf. Its unpinned reading was 48px (eight rows of + -- alternating $06/$24 read as one drawing); 16 is the answer. wall = { 2, 3, 18, 19, -- $02/$03 over $12/$13, speckled mass 12, 13, 28, 29, -- $0C/$0D over $1C/$1D, boulder mass 16, 17, -- $10/$11, the shelf's south cobbles 49, 23, -- $31 west face, $17 east face 4, 7, 40, -- $04 NW cap, $07 NE cap, $28 SW cap 37, 38, -- $25/$26, the inner corners - 20, -- $14 rubble mass -- the water trap - 14, 15, 30, 31 }, -- $0E/$0F over $1E/$1F, barred shelf - -- the lit floor and the shading row along its north rim: one 6px - -- shelf. $05 is the shipped pin (the tilePairs step). $29 is the - -- SAME surface -- the artist's shadow row where the shelf runs up - -- against the rock above it -- but it sits in walkable cells, so + 14, 15, 30, 31, -- $0E/$0F over $1E/$1F, barred shelf + 6, 39, 36, 1 }, -- $06/$27 over $24/$01, ceiling mass + -- ---- 6: the lit shelf, and the stairs off it ---- + -- + -- $05 is the lit floor -- the shipped pin, and the step + -- data.field.tilePairs encodes ($20/$21/$2A/$41 <-> $05). $29 is + -- the SAME surface: the artist's shadow row where the shelf runs + -- up against the rock above it. It sits in walkable cells, so -- unpinned it resolved to flat ground and cut a 6px trench along -- the north edge of every lit room (`29g00` with `05l06` one tile -- south, all the way across MT_MOON_B2F). - ledge = { 5, 41 }, - -- the cave ponds: $06/$27 over $24/$01, the tileset's - -- TILEANIM_WATER cell -- Mt Moon B2F's lake, Victory Road's - -- pools, the Seafoam channels you Surf. The engine's water set - -- does not name them (it carries the stale $14/$32/$48 ids - -- instead), so unpinned they fell to the wall fallback and the - -- volume builder read the alternating $06/$24 rows as a drawing - -- eight rows tall: `27w48`/`01w48`, a 48px wall of water standing - -- down the middle of MT_MOON_B2F. - water = { 6, 39, 36, 1 }, - -- Victory Road's boulder switches ($2B/$2C over $2D/$2E, the four - -- plates on 1F, 2F and 3F). Round art, but NOT a round object: - -- the Strength boulders themselves are overworld SPRITES, and - -- this is the plate they get pushed onto -- drawn from straight - -- above as a black ring, a white highlight rim and a grey disc. - -- `cylinder` was tried here first, because a circle is what the - -- overworld's round canopies take, and it came out wrong: the - -- plate's own dithered background carries stray black pixels the - -- darkest-outline flood cannot reach, so they joined the mask, - -- inflated the top and bottom row spans to the full 16, and the - -- hull rendered as a clutch of white shards standing where the - -- plate should be (VICTORY_ROAD_1F cell 17,13). `relief` is the - -- reading anything drawn from above wants: the disc stays flat - -- and the pixels inside its black ring lift a few voxels, so it - -- reads as a plate -- and the cell is walkable, so you step on it. - relief = { 43, 44, 45, 46 }, - -- the flat things, pinned so nothing can raise them: both ladder - -- graphics ($08/$09 over $18/$19, $0A/$0B over $1A/$1B), drawn - -- from ABOVE and carrying the tileset's warp tiles; the ramp - -- treads that walk you off a lit shelf down to the dark floor - -- ($15/$16, four steps seen from above); and the drop holes of - -- Seafoam and Victory Road ($2F rim over $22). All of them - -- already resolved to ground through their walkable cells -- the - -- pins keep that true whatever cell a block puts them in, and - -- keep the hole's lit rim out of the wall fallback. - ground = { 8, 9, 24, 25, -- $08/$09 over $18/$19, ladder - 10, 11, 26, 27, -- $0A/$0B over $1A/$1B, ladder - 21, 22, -- $15/$16, the ramp treads + -- + -- $15/$16 is the STAIR down off that shelf, and the elevation it + -- spans is 6px, not 16. Its art is four treads seen from above, + -- each a light tread over a black riser line, stacked NORTH-SOUTH: + -- every one of the 54 stair cells in the tileset has the lit floor + -- $05 to its NORTH and the low ground to its SOUTH (dark floor x35, + -- water x14, lit floor x5 where two flights meet), so the flight + -- climbs NORTHWARD, one cell deep, 0 -> 6. + -- + -- It is NOT pinned `stair_e`/`stair_w`, and that is deliberate. + -- Those classes build a flight that marches along X -- 16px tall, + -- rising toward the named side -- so either of them here would + -- throw a 16px staircase sideways across a 6px north-south step, + -- blocking the passage it is supposed to open and climbing at + -- right angles to the drawn risers. A wrong-way flight is worse + -- than a flat one. `ledge` is the honest reading available: the + -- stair cell joins the shelf it belongs to, wears its four treads + -- on the TOP face (which is how they are drawn -- from above), and + -- puts its 6px riser face at the FOOT of the flight where the + -- player actually steps down onto the dark floor. See the report: + -- a real sloped cave stair wants `stair_n`/`stair_s` in + -- Structures.stairCell, which is an engine change, not a pin. + ledge = { 5, 41, -- $05 lit floor, $29 its north shading + 21, 22 }, -- $15/$16, the stair plate off it + -- ---- 0: the floor plane ---- + -- + -- The dark lower floor, pinned rather than left derived so the + -- datum this whole entry measures from is stated in it. $20 is + -- the rough floor, $21 the bright checker, $2A the fine checker; + -- all three are on the tileset's walkable list and all three + -- already resolved here -- the pins restate the level, they do not + -- move it. + -- + -- $14 IS THE CAVE WATER: the tileset animates by TILEANIM_WATER, + -- and TileRenderer's WATER_TILE is $14 -- it is the tile that + -- h-shifts to ripple. Map's water set names it too, so its cells + -- are the ones Surf works on: Cerulean Cave's lake (352 + 272 + -- tiles on 1F/B1F) and the Seafoam channels (304 on B3F, 816 on + -- B4F). The first pass read its blocky white dither as "bright + -- rubble mass" and pinned it `wall`, which stood both lakes up as + -- 16px rock -- two thirds of SEAFOAM_ISLANDS_B4F was a rock slab + -- you were meant to surf across. It is water, and it lies at the + -- floor plane: `ground` keeps the drawn (and animated) water art + -- exactly as it is and puts its surface at 0, level with the dark + -- floor you step off onto, one 6px step below the lit shelf you + -- cannot surf onto. NOT the `water` class -- that is -2, a + -- shoreline lip for the overworld sea, and a cave pool must never + -- read as a trough sunk into the floor you are walking on. + -- + -- $2F over $22 is Seafoam's and Victory Road's fall-through drop + -- hole (Map's warpPadTiles calls $22 a "hole"). Its art is a lit + -- rim over solid black and its cell is walkable -- you step on it + -- and drop a floor -- so it stays flat at the floor plane; the pin + -- keeps the rim out of the wall fallback. + ground = { 32, 33, 42, -- $20/$21/$2A, the dark lower floor + 20, -- $14, the surfable cave water 47, 34 }, -- $2F over $22, the drop hole - -- Left to the derived defaults on purpose. The dark lower floor - -- ($20, $21, $2A) is in the tileset's walkable list and resolves - -- to flat ground at level 0 -- the level the tilePairs step above - -- is measured FROM -- so pinning it would only restate the - -- default. $3C is solid black and the void rule flattens it - -- (MT_MOON_B1F's unlit north band probes `3Cv00`); pinning it - -- would BREAK that, since the void rule skips authored tiles. + -- ---- 0 to 16: the ladders, the caves' real staircases ---- + -- + -- Both ladder graphics are drawn from above as a shaft with two + -- rails and rungs between them, and which one is which is not a + -- guess -- it is in the warp table. Across all nineteen maps, + -- every one of the 37 $08 cells warps to a LOWER floor + -- (MT_MOON_1F->B1F, B1F->B2F, ROCK_TUNNEL_1F->B1F, each + -- SEAFOAM_ISLANDS floor to the next one down, CERULEAN_CAVE_2F->1F + -- and 1F->B1F, the Diglett's route stubs down into the cave) and + -- every one of the 40 $0A cells warps to a HIGHER one (the exact + -- reverse, plus MT_MOON_B1F's and ROCK_TUNNEL_1F's exits back out + -- to daylight). $08 is the ladder DOWN, $0A the ladder UP: 77 + -- cells, no exceptions. + -- + -- So they take the two classes that MOVE between floors. $0A + -- becomes a rising flight -- four real steps climbing 0 -> 16, the + -- height of the rock band it disappears into -- and $08 becomes an + -- excavated stairwell, four steps descending below the floor into + -- a dark opening. East for both: the ladder art is symmetric + -- about its own centre line so the drawing does not choose a side, + -- the cell's east neighbour is the closed (rock) side more often + -- than its west, and the rest of this profile's staircases + -- (REDS_HOUSE_1, UNDERGROUND) climb east too. The flight's treads + -- land one rung apiece, which is as close to a drawn ladder as + -- stepped geometry gets. + -- + -- All four tiles of each cell are listed, the way REDS_HOUSE_1 + -- lists its flight: buildStairs anchors on the cell's TOP-LEFT + -- tile ($08 / $0A) and claims the other three, but pinning them + -- too keeps them out of the region flood and the door fold. + stair_e = { 10, 11, 26, 27 }, -- $0A/$0B over $1A/$1B, UP + stair_down_e = { 8, 9, 24, 25 }, -- $08/$09 over $18/$19, DOWN + -- ---- 3: the boulder switches ---- + -- + -- Victory Road's four plates ($2B/$2C over $2D/$2E, on 1F, 2F and + -- 3F). Round art, but NOT a round object: the Strength boulders + -- themselves are overworld SPRITES, and this is the plate they get + -- pushed onto -- drawn from straight above as a black ring, a + -- white highlight rim and a grey disc. `cylinder` was tried here + -- first, because a circle is what the overworld's round canopies + -- take, and it came out wrong: the plate's own dithered background + -- carries stray black pixels the darkest-outline flood cannot + -- reach, so they joined the mask, inflated the top and bottom row + -- spans to the full 16, and the hull rendered as a clutch of white + -- shards standing where the plate should be (VICTORY_ROAD_1F cell + -- 17,13). `relief` is the reading anything drawn from above + -- wants: the disc stays flat and the pixels inside its black ring + -- lift a few voxels, so it reads as a plate -- and the cell is + -- walkable, so you step on it. + relief = { 43, 44, 45, 46 }, + -- Left to the derived default on purpose: $3C is solid black -- + -- the unlit rock that is most of MT_MOON_B1F -- and the void rule + -- flattens it (`3Cv00`). Pinning it would BREAK that, since the + -- void rule skips authored tiles; a 16px black band there would + -- also wall the corridors in with something the 2D art draws as + -- nothing at all. $30 and $41 are on the walkable list and in + -- tilePairs but no block in this tileset places either, so there + -- is nothing for a pin to catch. }, -- Viridian Forest. Nearly everything drawn here is ROUND, and the -- detector was boxing all of it: the big trees came out as ragged @@ -627,21 +733,46 @@ return { -- healing consoles wall = { 23, 29, 40, 44, 45, 46, 47, 62, 63, 78, 79, 90, 91 }, -- the clerk's counter, half a cell high like every service - -- counter: the south front (24/25), the east-facing display run - -- (16/41/89 panels, 14/15/30/31 the juice-poster case whose art - -- rides the top like the nurse's tray), and the dark section - -- beside the register (56, near-black -- pinned or the void rule - -- flattens it) - counter = { 14, 15, 16, 24, 25, 30, 31, 41, 56, 89 }, - -- the cash register stands ON the pinned counter through the - -- authored-box support rule, black-outline segmented - billboard = { 8 }, + -- counter. It is a C wrapping the alcove the clerk stands in: the + -- south arm's drawn front (24/25) under its top band (8/56), the + -- light work surface the east arm and the rest of the south arm + -- share (16/41), and the north arm's end panel (89). 8 and 56 are + -- the two halves of one top band -- the first pass read 8 as the + -- cash register, pinned it `billboard`, and got a bare black + -- outline standing on the counter with the tile's own art replaced + -- by its neighbour's. + counter = { 8, 16, 24, 25, 41, 56, 89 }, + -- The CASH REGISTER (14/15 over 30/31), a keypad and a curl of + -- receipt paper drawn face-on across two tile rows in the middle of + -- the counter's east arm. Pinned `counter` it was just paint on + -- the work surface. In the `billboard` pool it is a standing + -- per-pixel cutout ten voxels deep -- the flower-and-vase treatment + -- of Red's house, but with a machine's body rather than a stem -- + -- and the support rule lifts it onto the counter automatically, + -- because the tile drawn directly BELOW it (16/41) is a pinned 8px + -- box. Its own pool, so the counter can never absorb it. The art + -- has a clean light margin on three sides and a black outline all + -- round, so the shade flood drains the work surface away and leaves + -- the machine whole (verified pixel by pixel before shooting). + billboard = { 14, 15, 30, 31 }, -- the free-standing shelf racks: TALL drawings, not deep ones -- -- each rank collapses onto a one-cell-deep shelf at its drawn -- height (the Dojo/Red's-house treatment). 64/65/67 and 80/81/83 -- are the bottle rows the clerk's booth also wears as its top -- display; 68/69/71 and 84/85/87 the goods rows below bookcase = { 64, 65, 67, 68, 69, 71, 80, 81, 83, 84, 85, 87 }, + -- Left to the derived default on purpose: the floor checker and + -- its shadowed variants (1/11/17/26/27/54) and the exit mat + -- (12/28) all sit in cells the ROM marks walkable, so the cell + -- rule lays them flat unaided; 76/77 is the SALE case's black + -- interior, which the volume path recesses half a course into the + -- band -- a dark display niche, which is what it is drawn as. + -- NOT covered: INDIGO_PLATEAU_LOBBY, the ninth map on this id and + -- the only one that is not the 4x4 shop. It draws its own hall, + -- lift bank and rope stanchions from tiles no Mart places, three + -- of which fall in the engine's stale water set -- see + -- reports/COUNTERS_pass2.md. Pinning them is a tileset pass of + -- its own and is deliberately not attempted here. }, -- The route gates and their upstairs lounges, the four Underground @@ -662,7 +793,8 @@ return { -- walkable. GATE = { -- The wall band stays ONE 16px face. Three wall drawings share - -- it: the route gates' panelled course (32/33 over 50/51), the + -- it: the route gates' panelled course (32/33 -- its base course + -- 50/51 is `counter`, see below), the -- Underground Path huts' and Route 22's plain stripes (0), and -- Route 2's speckled plaster (72 over its dark skirting 74). 0 -- and 74 need no height correction but are pinned anyway, so the @@ -675,30 +807,54 @@ return { -- the floor -- the Center's warp-tile treatment, and pins are -- look-only so the cell stays walkable. -- - -- 50/51 is the one compromise in this entry. The same two tiles - -- draw the wall's base course and every counter's front, and a pin - -- cannot tell the two uses apart. Pinned `counter` (8) the - -- counters come out perfect and the wall band castellates: it - -- stacks 32/33 over 50/51 for up to sixteen tile rows down the - -- east and west sides, so a 16px ridge and an 8px trench alternate - -- every 8px of depth and the room ends up built out of crates. - -- Pinned `wall` the band is one clean face everywhere and the - -- price is that a counter's FRONT row stands 16 instead of 8 -- - -- which is cheap, because the mesher's authored-top rule still - -- lays the drawn counter surface (7/8/9) on that block's top face - -- and folds the drawn front panel up its face. It reads as a - -- full-height service counter, not as a slab. The wall is the - -- room's frame and it wins. (FOREST_GATE never places 32/33, so - -- there the counter keeps its half cell.) - wall = { 0, 32, 33, 41, 43, 45, 50, 51, 58, 61, 62, 72, 74 }, - -- the counter, half a cell high: its north rim and end caps (7/8), - -- the long run between them (9), and the top surface a counter - -- drawn running north-south wears (23/24). 8px is one clean band, - -- so the surface stays on top and the guard leans on it rather - -- than standing behind a wall stub. The counters that run wall to - -- wall (Route 12's pair) are pure 8px; the ones that end in the - -- open carry the 16px front row above. - counter = { 7, 8, 9, 23, 24 }, + -- 50/51 is NOT in that list, and that is the one deliberate cost in + -- this entry. The artist draws one panel twice: as the wall cell's + -- base course (32/33 over 50/51) and as the front of every counter + -- (7/9/8 or 17/18 over 50/51). A pin is per tile id and a class + -- carries one height, so the two uses cannot be separated here at + -- all -- only in `lib`, by letting a pin be conditional on the tile + -- drawn ABOVE (see reports/COUNTERS_pass2.md). Pinned `wall` (16) + -- the band is one clean face and the counter the guard stands + -- behind is a full 16px slab that hides him to the shoulders. + -- Pinned `counter` (8) every counter is the half cell it is drawn + -- as -- which is what the room is FOR -- and the price is that the + -- wall cell's front 8px of depth drops with it: a shallow wall + -- gains an 8px plinth (reads as a wainscot), and the deep margin + -- masses that run two to eight cells north-south down the east and + -- west sides of the fourteen maps that place both drawings (Routes + -- 5/6/7/8, the five gate 1Fs, the Safari gate and its four rest + -- houses) corrugate, their exposed flank stepping 16/8/16/8 every + -- 8px of depth. The other six -- Route 2, Route 22 and the four + -- Underground Path huts -- place 50/51 and never 32/33, so they + -- take the half cell for nothing. The counters win where it costs: + -- they are the object the player walks up to and talks over, they + -- sit mid-room, and the margin masses are pure border filler seen + -- edge-on. This also puts GATE and FOREST_GATE on the same footing + -- at last -- ROUTE_2_GATE draws the forest gates' room tile for + -- tile and used to be the only one of the three with a 16px front. + wall = { 0, 32, 33, 41, 43, 45, 58, 61, 62, 72, 74 }, + -- the counter, half a cell high everywhere it is drawn: its north + -- rim and end caps (7/8), the long run between them (9), the top + -- surface a counter drawn running north-south wears (23/24), and + -- the drawn front panel (50/51) standing up as one clean 8px band + -- with the surface riding the top face. The guard leans on it + -- instead of standing behind a wall stub, and a counter that runs + -- wall to wall (Route 12's pair) matches one that ends in the open. + counter = { 7, 8, 9, 23, 24, 50, 51 }, + -- 50/51 draws BOTH the counter's front and the wall's base course, + -- and it is the bottom row of its cell either way -- so a flat pin + -- has to pick one and be wrong about the other. Pinned `wall` the + -- counters stood a full 16px; pinned `counter` the deep border + -- banks corrugated 16/8 for sixteen rows and the room read as + -- crates. What tells the two uses apart is what is drawn ABOVE: + -- the wall stacks its panelled course (32/33) over the base, while + -- a counter carries its own top (7/8/9). So the counter pin above + -- is the default and this puts the wall back wherever the panel + -- sits on top of it -- resolved per position, in TileShape.at. + when_above = { + [50] = { { above = { 32 }, class = "wall" } }, + [51] = { { above = { 33 }, class = "wall" } }, + }, -- the tall glass-fronted cabinets: three shelf ranks (34/35) -- folded up a 24px box whose top face adopts the drawn top rim -- (7/8, pinned `counter` directly above it) through the mesher's @@ -723,7 +879,8 @@ return { -- the potted palm that stands in every hut and rest-house corner -- (5/6 crown, 21/22 fronds, 37/38 over 53/54 the pot), and Route -- 22's wall plant (14/15 crown, 30/31 pot), which stands on the - -- 16px planter its base course (50/51) makes. + -- half-cell planter its base course (50/51) makes -- a planter box + -- now rather than the 16px pedestal it used to perch on. prop = { 5, 6, 14, 15, 21, 22, 30, 31, 37, 38, 53, 54 }, -- the exit doormat (4 over 20): drawn from straight above, so it -- has to lie flat -- and 20 is the $14 water-fallback trap, which @@ -748,14 +905,17 @@ return { -- Viridian Forest's north and south gates. Tile for tile they are -- the same drawing as ROUTE_2_GATE, but they are their own tileset - -- id, so they need their own copy of the pins -- and they get the - -- one thing GATE cannot have. These two maps never place the - -- panelled wall course (32/33), so nothing else claims 50/51 and the - -- counter can be the half cell it is drawn as. Before this entry - -- the room was a pond with an island: 72 ($48) sank the north wall's - -- top course, 50 ($32) sank all four counter fronts, and 20 ($14) - -- sank the exit doormat -- the three stale-cache water ids, all - -- three placed in one small room. + -- id, so they need their own copy of the pins. These two maps never + -- place the panelled wall course (32/33), so 50/51 is a counter + -- front here and nothing else -- the half cell it is drawn as costs + -- this id nothing at all, where GATE pays for it in the wall (see + -- the note there). Both ids run 50/51 `counter` now, so the three + -- rooms that share this drawing -- the two forest gates and + -- ROUTE_2_GATE -- finally match. Before this entry the room was a + -- pond with an island: 72 ($48) sank the north wall's top course, 50 + -- ($32) sank all four counter fronts, and 20 ($14) sank the exit + -- doormat -- the three stale-cache water ids, all three placed in + -- one small room. FOREST_GATE = { -- the wall band, one 16px face: the speckled plaster course (72) -- over its dark skirting (74), plus the door pane (41/43) drawn @@ -814,9 +974,6 @@ return { -- end of 1F's counter top, where 16px reads as a sign -- board standing on the counter -- the one placement -- that is not wall, and the least bad of the two) - -- 14/15/30/31 the TV set: on 3F's wall band it stands over the - -- display case below it, and on 3F's goods counter the - -- same 16px box reads as a floor-standing display unit -- 62/63 the lift call-button panel in both 2x2 cabins -- 72/73/88/89 the framed picture (72 is $48, a water-set id, so -- unpinned its top half sank) @@ -829,9 +986,33 @@ return { -- 91 the dithered wall course above the Prize Room counter -- and the roof stairhead -- 92/93 the Rocket lift's horizontal cabin frame - wall = { 1, 2, 3, 6, 14, 15, 18, 19, 22, 30, 31, 33, 46, 47, - 62, 63, 68, 72, 73, 75, 76, 77, 78, 79, 84, 88, 89, - 91, 92, 93 }, + -- 70/71 NOT wall panelling: the round tables' lower-left and + -- lower-right rim ($46/$47). They are here to lift the + -- terrace and diner tabletops to the one height their + -- middle can be built at -- see the long round-table + -- note under `counter`. The tileset places them in the + -- two table blocks (29, 49) and nowhere else, so 16px + -- costs nothing anywhere in the group. + 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 }, + -- 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 + -- thing Red's living room stands up. Six placements: four on the + -- two goods counters, two against the east wall band. As `wall` + -- each was a 16px CUBE with the TV art folded onto one face, so the + -- counters wore grey filing cabinets and the TV shop read as a + -- stockroom. The standee pool cuts the drawing per pixel at 10 + -- voxels of body instead, and the support rule does the rest: the + -- four counter sets are drawn directly above an authored 8px + -- `counter` row, so they STAND ON the counter and the tiles they + -- claim keep rendering as counter top rather than holing it. (The + -- art carries a full black frame with light margin inside it, so + -- the segmentation flood has nothing to drain and the whole set + -- survives.) Its own pool, away from `stool`, so a TV never stacks + -- with a chair drawn beside it. + billboard = { 14, 15, 30, 31 }, -- Every service counter in the group, half a cell high like the -- Center's and the Mart's. One 8px band means the drawn front row -- stands up and everything above it rides the TOP face in drawn @@ -842,18 +1023,53 @@ return { -- 38/39/41 the top surface and its end caps -- 21/48/49 the south front panel and its end caps -- 54/57 the arms' left and right side edges - -- 36/37/52/53 the goods laid out on 3F's counter (a boxed set - -- and a Poke Ball) -- they ride the top face, drawn - -- once, the way the nurse's tray does - -- 9/25/70/71/85/86/87 the round tables of the diner and the - -- roof terrace, drawn from above with a pedestal - -- row at the south: the same half-cell treatment - -- puts the tabletop on top and the pedestal on the - -- front. (Their four INTERIOR tiles are $37, which - -- is also the checkerboard floor and the Prize - -- Room's upper wall -- see the note below.) + -- 36/37/52/53 the games console and its controller laid out on + -- 3F's counter. This is pixel for pixel the drawing + -- Red's house pins `relief` -- a prop seen from + -- ABOVE -- and `relief` is the class it wants. It + -- cannot have it here: Structures.buildRelief anchors + -- its extrusion at y=0 and has no support lookup, + -- while every placement of this drawing in the + -- tileset is on a counter TOP. Pinned `relief` the + -- cell is claimed, dropped to floor level and punched + -- a 16x16 hole clean through the counter (verified + -- in-engine, pass-2 report). `counter` leaves the + -- console exactly where relief would put it -- flat, + -- flush with the surface, art on the top face, drawn + -- once -- and gives up only relief's three voxels of + -- lift. The report carries the one-place engine + -- change that would free it. + -- 9/25/85/86/87 the round tables of the diner and the roof + -- terrace: their north rim (9/25 = $09/$19) and the + -- pedestal course at the south (85/86/87). + -- + -- THE ROUND TABLES in full, because the shape is a compromise. The + -- drawing (block 29, and the same four rows split across blocks 45 + -- and 49 in the diner) is + -- $09 $27 $27 $19 an octagonal top seen from above, with + -- $36 $37 $37 $39 a pedestal drawn below its southern + -- $46 $37 $37 $47 rim + -- $55 $56 $57 $37 + -- Its four INTERIOR tiles are $37, which is ALSO the light half of + -- the checkerboard floor and the plain upper wall of the Prize Room + -- and the roof stairhead, so $37 cannot be pinned at all (see the + -- closing note). Left to the detector those four are a two-row + -- column, which buildVolume reads as a repeat, floors at unit 2 and + -- builds at 16px -- and with the whole rim pinned `counter` at 8px + -- the terrace tables came out as grey CUBES sitting on trays. + -- + -- So stop fighting the 16px and let it BE the tabletop: 70/71 + -- ($46/$47, the lower-left and lower-right rim) move to `wall`, and + -- the table's whole front course then stands at the same 16px its + -- middle already does -- one flat disc, the drawn octagon on its + -- top face and the lower arc folded down its front. What stays at + -- 8px is the FAR rim (9/25, and the shared 39/54/57) and the + -- pedestal (85/86/87): the far rim is occluded by the 16px top in + -- front of it, and the pedestal is meant to sit low. 16px is also + -- the right height against the 8px `stool` chairs drawn around it + -- -- a terrace table you sit at, not a footstool. counter = { 9, 21, 25, 36, 37, 38, 39, 41, 48, 49, 52, 53, 54, - 57, 70, 71, 85, 86, 87 }, + 57, 85, 86, 87 }, -- Free-standing merchandise racks and glass cases: TALL drawings, -- not deep ones. The four-row rack (42-45 / 58-61 / 64-67 / -- 80-83) stands two racks abreast on floors 2F-5F and along the @@ -885,15 +1101,14 @@ return { -- $37 (55) is three different things -- the light half of the -- checkerboard floor, the interior of the round tables, and -- the plain upper wall of the Prize Room and the roof - -- stairhead. The cell rules already answer all three - -- correctly (the floor's cells are walkable through their - -- $45 bottom-left tile; the wall's are not), and pinning it - -- to any one class breaks the other two. The cost is the - -- four $37 tiles inside each round table, which stay a - -- detected 16px box in the middle of the 8px top -- read as - -- a centrepiece. Fixing it properly needs the PaletteFX - -- alias ($37 -> $5a on the roof and the diner) carried into - -- the shape lookup, which is engine work. + -- stairhead -- and every class that suits one ruins the + -- others: `ground` holes the wall and the tabletop, `counter` + -- turns every second floor tile into an 8px lump, `wall` turns + -- it into a 16px pillar. The cell rules already answer the + -- floor (its cells are walkable through their $45 bottom-left + -- tile) and the wall (its cells are not), and the tabletop is + -- answered above by raising 70/71 to meet the 16px the + -- detector builds there. Left unpinned on purpose. -- $10 (16) is pure black and the void rule already flattens it -- to the interior darkness above the Game Corner's wall. -- The stair openings (10/11/26/27, 40/56) and the lift doors @@ -1023,27 +1238,52 @@ return { -- 10/11 the dial faces, 74/75 + 90/91 the narrow filler columns, -- 26/27 the plinth with its feet, 59 the black end pilaster; -- 40/41 are the metronome room's and the meeting room's shelf - -- ranks. Where the drawing's own top row is the shared table - -- trim (64/66, pinned `table` below) the bookcase builder adopts - -- it as the rank's CAP -- one more band of height and the art the - -- top face wears -- so those units come out 32px too. - bookcase = { 10, 11, 26, 27, 40, 41, 59, 69, 70, 74, 75, 85, 86, - 90, 91 }, + -- ranks. + -- + -- 64/65/66 -- the TOP-TRIM row (one black line, one white + -- highlight, then grey) that every flat-topped unit in this + -- tileset wears -- are here too, and this is the load-bearing + -- decision in the entry. They used to be `table`. A `table` is + -- an authored upright BOX, and the support rule stands a pinned + -- standee drawn directly above an authored box ON it: every chair + -- on the FAR side of a table in this tileset is drawn directly + -- above that table's trim row, so the trade room's two north + -- chairs and the meeting room's spare chair were lifted onto the + -- tabletops -- and their cells were re-tiled as tabletop, marching + -- each table two tile rows north to meet them. A `bookcase` is + -- authored but is NOT a box (its art mode is the rank collapse), + -- so the support rule does not fire and those three chairs stand + -- on the floor where they are drawn. + -- Nothing else about the trim changes for the worse. Where it + -- caps a shelf rank (64/66 over 40/41 or 10/11, in the metronome + -- room, the meeting room, the fossil room and the secret house) + -- it simply becomes the rank's fourth row: the same 32px height + -- and the same top-face art the cap-adoption rule used to hand it, + -- minus the 12px stub the trim used to render as BEHIND the shelf. + -- Where it tops a table or the Warden's bench it becomes that + -- unit's own 8px back rim, tucked behind the 12px top. + -- (The one thing it costs is named in the report: the meeting + -- room's two partitions used to hand this trim to the 16px band's + -- TOP face -- the mesher gives a full-height course the authored + -- UPRIGHT row above it -- and now top with their own band art.) + bookcase = { 10, 11, 26, 27, 40, 41, 59, 64, 65, 66, 69, 70, + 74, 75, 85, 86, 90, 91 }, -- the lab furniture, at table height. The benches in the fossil -- and metronome rooms (2/3 the specimen tray, 4/5 + 20/21 the -- microscope, 18/19 the tray's rack row) and the big lab tables - -- (64/65/66 top edge, 80/81/82 body, 83/58/84 the front rail and - -- its legs). 72/73 -- the monitor and the ball lying on the - -- table -- ride the table's authored height rather than standing - -- as separate cutouts, exactly as Oak's starter balls do; pinning - -- them also stops the display frame's black corner brackets from + -- (80/81/82 the top, 83/58/84 the front rail and its legs). + -- 72/73 -- the monitor and the ball lying on the table -- ride the + -- table's authored height rather than standing as separate + -- cutouts, exactly as Oak's starter balls do; pinning them also + -- stops the display frame's black corner brackets from -- auto-extracting into standing prisms. Four of these ids -- ($04/$05/$14/$15) plus $48 are the tileset's water-fallback -- traps: unpinned they sink into a pond lip in the middle of the - -- room. 65 doubles as the meeting room's long partition top, - -- which therefore reads as a 12px shelf behind its 16px base - -- band instead of joining the 48px tower it used to make. - table = { 2, 3, 4, 5, 18, 19, 20, 21, 58, 64, 65, 66, 72, 73, + -- room. With the trim row moved to `bookcase` the top face of + -- each table now wears its own drawn surface instead of the trim + -- -- which is how the Warden's bench finally shows the monitor and + -- the Pokeball the artist drew on it. + table = { 2, 3, 4, 5, 18, 19, 20, 21, 58, 72, 73, 80, 81, 82, 83, 84 }, -- seats, the 8px standee pool, all of them in WALKABLE cells and -- therefore flat floor until pinned: the four chairs round the @@ -1053,6 +1293,9 @@ return { -- carry the bench's apron along their top edge -- the standee -- wears a thin dark cap for it, which is cheaper than dropping -- the stool to a half-cell drawing with no floor margin. + -- The chairs on the far side of a table stay on the FLOOR only + -- because the trim row above them is no longer a box; see the + -- bookcase group. stool = { 6, 7, 14, 15, 22, 23, 30, 31 }, -- the tall corner plants: two cells of drawing (44/45 + 60/61 the -- frond crown, 46/47 + 62/63 the stem and pot), mostly silhouette, @@ -1208,37 +1451,64 @@ return { -- footing. bookcase = { 7, 8, 9, 10, 23, 24, 25, 26, 39, 40, 41, 42, 55, 56, 57, 58 }, - -- the big octagonal table of the Fan Club and Silph's boardroom, - -- half a cell high -- and half a cell ON PURPOSE. A `counter` is - -- ONE band: exactly the drawing's bottom row (79/63/64/81/82, the - -- table's front bevel) stands up as the front, and every row above - -- it rides the top face IN DRAWN ORDER. For a table drawn in plan - -- that is the only class that reproduces the plan: the octagon's - -- bevelled corners (72/73 north, 77/78 the flanks, 63/81 and - -- 79/82 south) land where they are drawn instead of being smeared - -- by a taller box, which wears its NORTH row's art across the - -- whole top face. - -- The chairman's seat is drawn INTO the table -- 17/18 the sitter, - -- 33/34 the chair back, 91/92 the seat -- across three tile rows, - -- and this is the Pokemon Center couch problem exactly: folding - -- two rows upright makes the box two tiles deep and a folded box - -- repeats its north row across the whole top, so every upright - -- arrangement draws the sitter's head two or three times. Riding - -- the counter's top face draws him once, in the right place, at - -- the right depth. Standing him as a cutout is not an option - -- either: the drawing has no floor margin -- it is surrounded on - -- all four sides by the tabletop's own mid grey -- so the mask - -- would drain from every edge. - -- 91/92 double as the wall band's base blocks where a side wall - -- meets the south course in Silph; those six tiles therefore sit - -- 8px instead of 16px. It is a notch at the foot of a wall the - -- player never faces, and the alternative is a 16px stub standing - -- in the middle of the boardroom table. + -- The big OCTAGONAL boardroom table -- the Fan Club's and Silph + -- 11F's, the same drawing in both -- half a cell high, and half a + -- cell on purpose. A `counter` is ONE band: exactly the drawing's + -- bottom row stands up as the front and every row above it rides + -- the top face IN DRAWN ORDER. For a table drawn in PLAN that is + -- the only class that reproduces the plan; a taller box wears its + -- NORTH row's art across the whole top face and smears it. + -- + -- The octagon itself is cut at TILE granularity, which is the + -- finest the mesher has: an authored tile is its own 8x8 column, + -- so the bevel is drawn by choosing which tiles rise. Reading the + -- Fan Club rows (Silph is the identical shape eight rows further + -- down the map): + -- row 4 72 74 74 74 74 73 6 tiles wide + -- rows 5-8 72 <----- 8 tiles -----> 73 8 tiles wide + -- row 9 63 64 64 64 64 81 6 tiles wide + -- row 10 75 76 76 75 4 tiles wide + -- 72/73 are the north chamfer, 63/81 the south one, 77/78 the + -- straight flanks, 74 the north rim and 64 the top itself. 75/76 + -- -- the front apron with its two drawn legs -- is pinned so the + -- octagon's south edge closes and the legs stand on the front face + -- instead of lying painted on the floor; it sits in the walkable + -- cell in front of the table, which is the cell a player stands in + -- to talk across it, and an 8px apron there reads as standing AT + -- the table (the same arrangement as the Center's counter). + -- 79 and 82, the OUTER corner of the south bevel, are `ground` -- + -- see the ground group. + -- + -- 91/92, the statue's pedestal, ride the top face here rather than + -- standing with the statue: those two ids are ALSO the wall band's + -- base blocks where a side wall meets the south course in Silph, + -- so six tiles at the foot of Silph's walls sit 8px instead of + -- 16px. It is a notch at the foot of a wall the player never + -- faces, and the alternatives are worse in both directions -- a + -- 16px stub standing in the middle of the boardroom table, or a + -- paper cutout standing at the foot of every south wall. -- 40 and 48 are water-fallback traps: unpinned, the table's whole -- north-west cell was a pond, and it dragged the floor tile $1F -- sharing that cell down with it. - counter = { 17, 18, 33, 34, 63, 64, 72, 73, 74, 77, 78, 79, - 81, 82, 91, 92 }, + counter = { 63, 64, 72, 73, 74, 75, 76, 77, 78, 81, 91, 92 }, + -- The Pokemon statue standing in the middle of both boardroom + -- tables: a per-pixel cutout, ONE voxel deep, standing ON the + -- table through the authored-box support rule -- a standee drawn + -- directly above an authored box rises from that box's top, which + -- is how the gym's bird statues stand on their plinths and the + -- potted plant stands on Red's table. 17/18 is the crown and the + -- head, 33/34 the face and the top rim of the pedestal it stands + -- in; the pedestal proper (91/92) stays `counter` under its feet. + -- The four claimed tiles keep rendering as the tabletop they were + -- cut out of, so the table is whole underneath. + -- `cutout` and not `prop`: the drawing has NO floor margin -- it + -- is surrounded on all four sides by the tabletop's own mid grey + -- -- and the cutout pool's stricter contract makes mid shades + -- background unconditionally, so the tabletop drains away cleanly + -- while the black outline, the paint whites and everything they + -- enclose survive. Pinned `counter` with the rest of the table + -- (what it was) the statue was painted FLAT on the tabletop. + cutout = { 17, 18, 33, 34 }, -- Bill's desk and the Silph president's, at table height: 11-14 -- the surface with the terminal and the ball on it, 27-30 the -- front edge. Their aprons (43/44 + 91/92 on the row below) are @@ -1260,13 +1530,20 @@ return { -- the pond with it. A pin is the only rule that outranks it. -- 1/2 are the curve of Bill's drums where they meet the floor, -- drawn below the pinned barrel and flat where they lie. - ground = { 1, 2, 31 }, + -- 79/82 are the OUTER corner of the octagon's south bevel, one + -- tile beyond 63/81 on each side. Raised with the rest of the + -- table they squared both bottom corners off and left an 8px stub + -- standing in the floor a tile away from the table on each side; + -- flat they are what they are drawn as -- the table's cast shadow + -- where the bevel meets the floor -- and the octagon closes with a + -- clean 45 degree step on all four corners. + ground = { 1, 2, 31, 79, 82 }, -- Deliberately NOT pinned: the doormats (70/71) and Silph's warp -- pads, stairwells and elevator mouth (3/4, 5/6/21/22, 83-86) are -- walkable and already flat; 47 is the black surround outside the -- building's footprint and the void rule authors it; 69 is the - -- shadow course under Bill's drums and 75/76 the octagon's front - -- apron, both drawn into walkable cells and both correct flat. + -- shadow course under Bill's drums, drawn into a walkable cell and + -- correct flat. }, -- The Cable Club rooms and the Bike Shop (one tileset, three maps). @@ -1708,14 +1985,25 @@ return { -- One 16px course: the chamber's wall ring ($09/$0A over -- $19/$1A), the grey mass beyond it ($11) so the room reads as a -- chamber cut into solid stock rather than a fence standing on a - -- plain, the roped-off corner of 1F ($02 over $12 running - -- east-west, seen face-on; $1D/$1E running north-south, seen from - -- above -- 8 rows of it, which the volume path would have towered - -- to 64px), and Agatha's north band ($20 over $30). + -- plain, and Agatha's north band ($20 over $30). wall = { 9, 10, 25, 26, 17, - 2, 18, 29, 30, 32, 48 }, + -- The reception counter of POKEMON_TOWER_1F -- the only counter in + -- the group, and the ROM's own counterTiles for this tileset name + -- $12 as exactly that. It is an L that fences the receptionist + -- into the south-east corner: the east-west arm drawn face-on + -- ($02 the surface over $12 the front panel, eight cells of it) + -- meeting the north-south arm drawn from above ($1D/$1E, eight + -- tile rows, which the volume path would have towered to 64px). + -- The first pass put all four in `wall` and the barrier came out + -- a full-height partition with the receptionist hidden behind it + -- to the shoulders. Half a cell is what a counter is: she leans + -- on it, the drawn surface ($02) rides the top face and the drawn + -- panel ($12) folds up the front, and both arms come out the same + -- height so the corner turns cleanly instead of stepping. The + -- cells stay blocked either way -- pins are look-only. + counter = { 2, 18, 29, 30 }, -- The gravestones ($05/$06 over $15/$16): a headstone drawn -- face-on, one per cell, 314 of them across the seven floors and -- Agatha's room. The volume path measured the drawing as its @@ -2443,6 +2731,72 @@ return { roofRows = 64, roofBack = 8, roofFront = 8, roofCycle = { 8, 31 }, slab = 4, frontEave = 4, ledge = nil, seal = "s", }, + -- assets/docs/buildings/B30: the POKEMON TOWER, and the one drawing + -- in the catalogue the map edge cuts off. It stands at + -- LAVENDER_TOWN (12,0), tile row 0, so the four rows of roof band + -- every other block of this family wears are simply not on the map: + -- rows 0..55 are three window courses over brick, 56..62 the base + -- course, and that is the whole drawing. + -- + -- REMAINING.md files it unvoxelizable on two counts. The first is + -- real and `seal` answers it; the second is a misreading of it. + -- + -- Unbounded silhouette. Every sibling is closed on the north by + -- the black outline along the top of its roof band. Here the top + -- row is the 1px light margin over the first window course, so the + -- flood seeds the whole of y=0, drops down the 2px light channel + -- between the side outline (x=5, x=90) and the window band (x=8), + -- and from the first brick course eats the wall out: 37% fill in + -- 126 pieces. North is the ONLY way in -- the base course seals + -- the south, the side outlines seal east and west -- so `seal = + -- "n"` alone restores it: 88% fill, ONE piece, largest 100%, in + -- the shipped band. The 728px left outside are exactly the 5px + -- grass margin down each flank plus the terrain row under the base + -- course, the same margins every sibling keeps. (REMAINING.md + -- tests "s" and "nswe" and concludes the box cannot be sealed + -- because a quarter of it is ground -- but that quarter is the + -- unsealed flood's own damage measured back: its 1552px is + -- precisely what the flood ate. North alone is never tried.) + -- + -- No roof drawn. True, and not fixable: `tiles` is the matcher as + -- well as the art, so the roof band cannot be borrowed from B14 + -- without asking the map for tiles at ty=-4. One row of band is + -- the least the pipeline can be given, and roofRows = 1 spends it + -- on that light top margin alone -- no window course is eaten, the + -- whole 63px of drawn facade stays standing, and the 4px slab caps + -- it flat. 67 voxels: nearly twice the Center and the Mart beside + -- it, and one voxel under its own twin B14 -- which is right, + -- because B14's facade IS this drawing. + -- + -- The last row carries a THIRTEENTH tile id. `matches` walks each + -- row to its own length while `read` composites only the first + -- #tiles[1] columns, so that id constrains where the template may + -- land without entering the drawing -- and it must not enter it, + -- because the only tile beside the tower is the $11 filler, whose + -- hatch carries isolated dark pixels the flood cannot reach: drawn, + -- each becomes a 64-deep rod of speckle hanging beside the tower. + -- The constraint is needed because these 8 rows ARE rows 5..12 of + -- flat_block_6x6: the bare grid matches four times, the tower plus + -- the lower two thirds of B14 at CELADON_CITY (14,4), ROUTE_16 + -- (18,6) and ROUTE_18 (34,4), and nothing stops a second model being + -- stamped inside the first. $11 sits east of the tower's south-east + -- corner; the three B14 plots carry grass, path and a plant there + -- ($23, $30, $55), so with the extra id the grid resolves once. + { + id = "pokemon_tower", + tiles = { + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79, 17 }, + }, + roofRows = 1, roofBack = 1, roofFront = 0, roofCycle = { 0, 0 }, + slab = 4, frontEave = 4, ledge = nil, seal = "n", + }, }, FOREST = { @@ -2463,25 +2817,145 @@ return { }, }, + -- The approach to the Pokemon League: two maps, INDIGO_PLATEAU (the + -- forecourt the League itself stands on) and ROUTE_23 (the long + -- climb up to it, with its badge-check gates). Everything this + -- blockset draws is one piece of architecture -- striated rock + -- walls, white pillars, and the bird STATUES that line both the + -- avenue and the plaza. 29 of its 73 blocks are never placed, so + -- every tile named below really is one of these two maps'. + -- + -- A statue is built exactly like the badge gyms' (see GYM above): + -- one cell of FIGURE ($10/$12 over $28/$29) standing on one cell of + -- PLINTH ($15/$16 the cap over $30/$31 the plaque). 47 of them -- + -- 12 on INDIGO_PLATEAU, six a side down the avenue, and 35 more + -- across ROUTE_23's plaza (blocks $42/$43; the $25/$26 twins that + -- stand the same statue on grass are never placed). + -- + -- What the detector made of them is the bug this entry exists for. + -- On the avenue the statues stack with NO gap: the plinth's plaque + -- row is drawn directly above the next figure's head, so the + -- flood-fill joined all six of a column into ONE region 24 tile rows + -- tall, and the volume builder's repeat scan read 32px down one half + -- of the drawing and 24px down the other. Each row came out as a + -- continuous stepped RIDGE of boxes wearing the statue art folded + -- onto its south face -- probed INDIGO_PLATEAU tiles (16,12)-(17,35) + -- at 32/24 and (22,12)-(23,35) mirrored. ROUTE_23's plaza statues + -- stand alone and fared no better: 24px boxes with the figure's top + -- row skipped outright. + -- + -- Pinned the gyms' way the ridge becomes statues. The plinth is a + -- SOLID 16px `wall` block; the figure is a per-pixel cutout 5 voxels + -- deep (the thin `prop` pool) that rides the plinth's top face + -- through the authored-box support rule and collapses to the + -- plinth's SINGLE cell of footprint -- Structures' wall-support case, + -- so the base never marches backwards. + -- + -- The one thing the gyms did not have to deal with: $28/$29 is + -- SHARED. The same bird is drawn again at the foot of every white + -- pillar, framed there by the pillar's black edge ($25/$26 over that + -- same $28/$29, blocks $18/$1B) -- 80 of them, 76 down ROUTE_23 and + -- four on INDIGO_PLATEAU (its two outer corners and the pair + -- flanking the League's recess). So $25/$26 joins the same pool: + -- pinning half a cell would have stood a half-height bird under a + -- wall. That in turn is why the pillar itself is pinned -- see the + -- last paragraph of `wall`. PLATEAU = { - -- assets/docs/buildings/B23: the Victory Road entrance on - -- Route 23: a long rock face with two barred doors cut into - -- it. Not a house -- what the roof band reads is the pale top - -- of the cliff seen from above, and the facade is the - -- striated rock below it. - { - id = "victory_road_gate", - tiles = { - { 37, 38, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 37, 38 }, - { 40, 41, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 40, 41 }, - { 21, 22, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 15, 15, 15, 15, 21, 22 }, - { 5, 6, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 15, 15, 15, 15, 5, 6 }, - { 5, 6, 15, 15, 15, 15, 15, 15, 11, 12, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 11, 12, 15, 15, 15, 15, 5, 6 }, - { 21, 22, 14, 14, 14, 14, 14, 14, 27, 28, 14, 14, 14, 14, 14, 14, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 27, 28, 14, 14, 14, 14, 21, 22 }, - }, - roofRows = 16, roofBack = 7, roofFront = 8, roofCycle = { 9, 12 }, - slab = 4, frontEave = 4, ledge = nil, - }, + -- ONE 16px course for every piece of masonry here. + -- + -- $03 is the striated rock face, 2436 placements and the bulk of + -- both maps. It already read 16 nearly everywhere, but in the + -- columns of INDIGO_PLATEAU's rim that stand over a corner pillar + -- the repeat scan came out 24 -- a stagger in the plateau's + -- skyline (probed `03w24` at tiles (8,0)-(9,2), (12,0)-(13,2) and + -- their two mirrors). Authored, the rim is one course. + -- + -- $0D/$0F/$0E are the League's outer wall -- top band, face and + -- base. The same three tiles draw the Pokemon League's own + -- facade, the long walls flanking the avenue, and every + -- badge-check gate down ROUTE_23. + -- + -- $15/$16 + $05/$06 + $30/$31 are the pilaster: cap, shaft, and + -- the plaque base. $15/$16 over $30/$31 IS the statue's plinth -- + -- the artist drew the same stone twice -- which is why one pin + -- serves the gate corners and the statues alike. + -- + -- $2E/$2F the white pillar shaft and $20/$21 its cap change no + -- HEIGHT: they derive 16 already. What the pin buys is + -- `authored`, which is exactly what the prop support rule tests. + -- Without it a pillar-foot bird finds no support, drops to ground + -- level, and leaves a hole punched clean through the pillar + -- (probed, shot, then fixed). + wall = { 3, + 13, 14, 15, + 21, 22, 48, 49, 5, 6, + 32, 33, 46, 47 }, + -- 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. + prop = { 16, 18, 40, 41, 37, 38 }, + -- Round drawings, one voxel ball per 16x16 cell -- the treatment + -- the overworld's canopies and Celadon's hedge take. + -- + -- $07/$08 over $17/$18: the seven canopies planted on pillar tops + -- along ROUTE_23 (blocks $44/$45; the $0F block that tiles four of + -- them together is never placed). Boxed, the canopy art smeared + -- down the whole pillar column beneath it. + -- + -- $2A/$2B over $22/$1D: the boulders strewn across ROUTE_23's + -- middle terrace (blocks $02/$13/$16), one per cell and NOT + -- walkable. As boxes they sat flat enough to look painted onto + -- the path; as balls they read as the obstacles they are. + cylinder = { 7, 8, 23, 24, + 29, 34, 42, 43 }, + -- The Route 23 sign, one cell, block $48's only placement. + -- Unpinned it probed `09w00 0Aw00 / 19w08 1Aw08` -- an 8px stub + -- with its board skipped. Same thin plate on a stick every other + -- outdoor sign gets. + signpost = { 9, 10, 25, 26 }, + -- The Route 22 gate's roof, drawn from ABOVE and filling + -- ROUTE_23's last two block rows ($3D the tiling, $3E/$44 its + -- edges, $40/$41 the corners). Art on the TOP face -- the way + -- SHIP_PORT's hull is pinned -- rather than folded up a 16px kerb. + -- It stands past the map's last walkable row (you warp to + -- ROUTE_22_GATE before you reach it), so this is a tidy-up rather + -- than a fix. + roof = { 61, 62, 64, 65, 68 }, + -- The ground painted where a pinned figure's cell used to be: + -- $23, the pale paving both maps are floored with. It is also the + -- white the pillars are drawn in, so the cell a pillar-foot bird + -- vacates reads as more pillar -- where the neighbour vote left a + -- BLACK hole, having nothing to elect (all four neighbours of that + -- cell are wall). On the avenue and the plaza the statues' own + -- cells simply keep the paving they stand on. + prop_ground = { [16] = 35, [18] = 35, [40] = 35, [41] = 35, + [37] = 35, [38] = 35 }, + -- Deliberately NOT pinned: + -- + -- $14, the water -- 1446 placements, the pond on ROUTE_23's middle + -- terrace -- and its bank shading $32/$33/$1F. $14 and $32 are + -- two of the three stale-cache water ids the trap is named for, + -- but here they are honestly water: TILEANIM_WATER animates $14, + -- the pond is real, and $32/$33/$1F are drawn in the TOP half of + -- water cells as the waterline itself, so the cell rule dropping + -- them to -2 is what the art means. Left to fall through to the + -- engine's water set. ($48, the third trap id, is not in this + -- atlas at all -- it stops at $45.) + -- + -- $0B/$0C over $1B/$1C, the barred doors: the Pokemon League's two + -- and Victory Road's two. They are the tileset's own doorTiles, + -- so the door fold already stands them upright in the facade; + -- probed 16px identically before and after this entry. + -- + -- $23/$2C/$2D are in the walkable list outright, and $45 is the + -- tileset's grassTile, which derives its own standing-tuft pin. + -- + -- The Victory Road entrance is a `buildings` template + -- (victory_road_gate, at the bottom of this file): 36x6 tiles at + -- ROUTE_23 (0,58), and its ends are built out of $25/$26/$28/$29 + -- and $15/$16/$05/$06. Buildings claim their tiles before any of + -- the above can reach them -- probed `b` class over all 216 of + -- them, unchanged by this entry. }, }, } diff --git a/lib/Structures.lua b/lib/Structures.lua index 6ff85f7..3baf693 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -1919,10 +1919,24 @@ function Structures.buildObject(S, map, region, cluster, -- the box's top with its feet on the box's north row, and the claimed -- tiles keep rendering as that box (wearing its plain art) instead of -- punching a floor-level hole through it. + -- + -- Only when the prop's OWN CELL IS BLOCKED, though. "Is something + -- drawn above me?" is not the same question as "am I standing on it": + -- a chair drawn against the north side of a table is above the table's + -- trim row too, and it was being lifted onto the tabletop -- three + -- chairs standing on the furniture in Cinnabar's trade room and + -- Fuchsia's meeting room, with the claimed cells re-tiled as tabletop + -- so the table marched two rows north with them. The world already + -- knows which is which: a thing that sits ON furniture occupies a + -- blocked cell (you cannot walk through the gym statue, Red's plant, + -- the PC), while a seat you walk up to is in a walkable one. local baseY, support = 0, nil if force then local bs = S.shapeAt[keyOf(cluster.minX, cluster.maxY + 1)] - if bs and bs.authored and bs.art == "upright" and (bs.h or 0) > 0 then + 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 baseY, support = bs.h, bs end end diff --git a/lib/TileShape.lua b/lib/TileShape.lua index c9b15fa..1e11c59 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -202,6 +202,48 @@ local function authoredGroups(tilesetId, heights) return out end +-- Conditional pins: tile id -> list of { above = {tile ids}, class }. +-- +-- A pin is per TILE ID, and one graphic can mean two things. The route +-- gates' $32/$33 is the case that forced this: the artist reuses it for +-- the wall's dark base course AND for every service counter's front, and +-- it is the bottom row of its cell either way. Pinned `wall` the counter +-- stands a full 16px; pinned `counter` the wall bank corrugates 16/8 for +-- sixteen rows. Neither is right, and no per-tile pin can be, because +-- forMap resolves an id to ONE shape. +-- +-- What separates the two uses is what is drawn ABOVE: the wall's upper +-- course over a wall base, the counter's top over a counter front. So a +-- profile entry may carry `when_above = { [tile] = { { above = {...}, +-- 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. +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 + 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 } + end + end + if #list > 0 then + out[tile] = list + any = true + end + end + end + return any and out or nil +end + local function shapeFor(class, heights, authored) return { class = class, h = heights[class] or 0, art = ART[class] or "upright", @@ -251,7 +293,7 @@ function TileShape.forMap(map) end end - local shapes = { classes = {} } + local shapes = { classes = {}, cond = authoredConditions(id, heights) } for class in pairs(FALLBACK_HEIGHTS) do shapes.classes[class] = shapeFor(class, heights) end @@ -284,6 +326,19 @@ end -- map:tileAt(tx, ty), passed in because every caller already has it. function TileShape.at(map, shapes, tile, tx, ty) local s = shapes[tile] + -- conditional pins first: they are authored answers that need the + -- POSITION to resolve, so they outrank both the flat pin on the same + -- 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 + shapes.classes[rule.class].authored = true + return shapes.classes[rule.class] + end + end + end if not s or s.authored then return s end local cx = math.floor(tx / 2) local cy = math.floor(ty / 2) diff --git a/main.lua b/main.lua index a1f2b8c..fd9a5a7 100644 --- a/main.lua +++ b/main.lua @@ -376,7 +376,7 @@ mod.events:on("map.reloaded", function(payload) if mapId then ChunkMesher.invalidate(mapId) end end) -mod.exports.version = "1.0.5" +mod.exports.version = "1.0.6" -- exposed so a companion mod can pin its own tiles' shapes or read the -- camera without reaching into this mod's file layout mod.exports.lib = V diff --git a/manifest.json b/manifest.json index 9fa4490..89de570 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "DRAMATIC_SHAPE", "name": "Dramatic Shape Voxel Mod", - "version": "1.0.5", + "version": "1.0.6", "api": 2, "entry": "main.lua", "profile": "content", From cf25f54352f0a24d654c76092f8403b96af083b2 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 04:21:19 -0400 Subject: [PATCH 14/16] add a building-placement probe, with its own caveat Prints the building models built and the tiles objects claimed. The comment carries the trap it cost me: Buildings.stats() is the module's memoisation cache, global and cumulative for the session, so it reads as "a foreign building stamped here" when nothing of the sort happened. Match template grids offline against maps.lua instead -- and remember a drawing can straddle a map boundary, as the Pokemon Tower's does. --- tests/voxel_building_probe.lua | 67 ++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/voxel_building_probe.lua diff --git a/tests/voxel_building_probe.lua b/tests/voxel_building_probe.lua new file mode 100644 index 0000000..848a134 --- /dev/null +++ b/tests/voxel_building_probe.lua @@ -0,0 +1,67 @@ +-- Driver: which building models exist, and how much of this map is +-- claimed by objects? +-- +-- Buildings.build matches every profiled template against the whole map +-- and stamps each hit, so two templates whose tile grids both match one +-- drawing stamp TWO models into the same cells. +-- +-- READ THE CAVEAT BEFORE TRUSTING THIS. Buildings.stats() reports the +-- module's `models` cache, which is keyed ":" and is +-- GLOBAL AND CUMULATIVE for the session -- it lists every model built +-- since boot, including ones from maps visited earlier, and says nothing +-- about where they were placed. It is a memoisation table, not a +-- placement log. Reading it as "what is on this map" will convince you a +-- foreign building has stamped here when it has not. +-- +-- To answer "does template X place on map Y", match its `tiles` grid +-- against the map's tile grid OFFLINE (data/generated/maps.lua plus +-- tilesets.lua) -- no engine needed, and it gives you the coordinates. +-- And note a drawing can STRADDLE A MAP BOUNDARY (the Pokemon Tower's +-- roof is on ROUTE_10, its facade on LAVENDER_TOWN), in which case no +-- single map's grid holds the whole thing and each half meshes alone. +-- +-- BUILD_MAP=LAVENDER_TOWN POKEPORT_DRIVER=mods/DRAMATIC_SHAPE/tests/voxel_building_probe.lua lovec . +return function(game) + local U = dofile("tests/drivers/util.lua") + local mapId = os.getenv("BUILD_MAP") or "LAVENDER_TOWN" + U.teleport(game, mapId, tonumber(os.getenv("BUILD_X") or "3"), + tonumber(os.getenv("BUILD_Y") or "5"), "up") + U.wait(10) + + local V = game.mods.exports["DRAMATIC_SHAPE"] + V = V and V.lib + local Structures = V and V.require("Structures") + local Buildings = V and V.require("Buildings") + local ow = game.overworld + if not (Structures and Buildings and ow and ow.map) then + print("[bld] mod or map unavailable") + return + end + + -- placements are recorded as the templates are stamped, so force the + -- analysis first + Structures.forMap(ow.map) + + print(("[bld] %s"):format(mapId)) + local stats = Buildings.stats and Buildings.stats() or {} + local keys = {} + for k in pairs(stats) do keys[#keys + 1] = k end + table.sort(keys) + for _, k in ipairs(keys) do + local s = stats[k] + print(("[bld] %-24s voxels=%s shell=%s quads=%s"):format( + k, tostring(s.voxels), tostring(s.shell), tostring(s.quads))) + end + + -- and the per-cell truth: which cells a building claimed + local S = Structures.forMap(ow.map) + local def = ow.map.def + local claimed = 0 + for ty = 0, def.height * 4 - 1 do + for tx = 0, def.width * 4 - 1 do + if S.skip[(ty + 64) * 4096 + (tx + 64)] then claimed = claimed + 1 end + end + end + print(("[bld] tiles claimed by objects/buildings: %d"):format(claimed)) + print("[bld] done") +end From 0b8730fbf6a90b98f592a8ef55a40a1c051c719d Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 04:39:27 -0400 Subject: [PATCH 15/16] rejoin the Pokemon Tower across the LAVENDER_TOWN / ROUTE_10 seam The tower's drawing straddles the map boundary -- roof band and top window courses in Route 10's last rows, the rest in Lavender -- so each map folded its half upright alone: two half-buildings, Lavender's under a fabricated yellow cap. Buildings templates gain `topRows`: drawing rows composited ABOVE the matched grid, for a drawing no single map holds. The model is built from the complete 16-row artwork (96px facade under the real 32px purple band) while placement still matches only the on-map rows; depth now derives from the matched footprint rather than sprite height, identical for every whole-drawing template. A `claimOnly` twin template claims Route 10's roof rows and stamps nothing, so the top half no longer also stands as its own building. Both templates verified to place exactly once, world-wide, at the intended coordinates. --- data/voxel_heights.lua | 105 +++++++++++++++++++++-------------------- lib/Buildings.lua | 40 ++++++++++++++-- 2 files changed, 89 insertions(+), 56 deletions(-) diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index e171f27..55bec61 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -2731,59 +2731,45 @@ return { roofRows = 64, roofBack = 8, roofFront = 8, roofCycle = { 8, 31 }, slab = 4, frontEave = 4, ledge = nil, seal = "s", }, - -- assets/docs/buildings/B30: the POKEMON TOWER, and the one drawing - -- in the catalogue the map edge cuts off. It stands at - -- LAVENDER_TOWN (12,0), tile row 0, so the four rows of roof band - -- every other block of this family wears are simply not on the map: - -- rows 0..55 are three window courses over brick, 56..62 the base - -- course, and that is the whole drawing. + -- assets/docs/buildings/B30: the POKEMON TOWER. The one drawing + -- in the catalogue that STRADDLES A MAP BOUNDARY: its purple roof + -- band and top two window courses stand in ROUTE_10's last eight + -- tile rows, and only the lower eight rows -- six more courses and + -- the base with the door -- are on LAVENDER_TOWN, where the + -- building begins at tile row 0. Modelled per map it came out as + -- two half-buildings: Lavender's half wearing a fabricated flat + -- cap (art row 0 stretched into a yellow slab), Route 10's half a + -- low volume of roof drawing folded at ground level behind it. -- - -- REMAINING.md files it unvoxelizable on two counts. The first is - -- real and `seal` answers it; the second is a misreading of it. + -- `topRows` is the fix, built for this template: the eight + -- ROUTE_10 rows are composited ABOVE the matched grid, so the + -- model is the COMPLETE 16-row drawing -- 96px of facade under a + -- real 32px roof band -- while placement still matches only the + -- Lavender rows. Its twin `pokemon_tower_top` below claims the + -- Route 10 rows and stamps nothing, so the roof half no longer + -- also stands as its own building behind the tower. -- - -- Unbounded silhouette. Every sibling is closed on the north by - -- the black outline along the top of its roof band. Here the top - -- row is the 1px light margin over the first window course, so the - -- flood seeds the whole of y=0, drops down the 2px light channel - -- between the side outline (x=5, x=90) and the window band (x=8), - -- and from the first brick course eats the wall out: 37% fill in - -- 126 pieces. North is the ONLY way in -- the base course seals - -- the south, the side outlines seal east and west -- so `seal = - -- "n"` alone restores it: 88% fill, ONE piece, largest 100%, in - -- the shipped band. The 728px left outside are exactly the 5px - -- grass margin down each flank plus the terrain row under the base - -- course, the same margins every sibling keeps. (REMAINING.md - -- tests "s" and "nswe" and concludes the box cannot be sealed - -- because a quarter of it is ground -- but that quarter is the - -- unsealed flood's own damage measured back: its 1552px is - -- precisely what the flood ate. North alone is never tried.) - -- - -- No roof drawn. True, and not fixable: `tiles` is the matcher as - -- well as the art, so the roof band cannot be borrowed from B14 - -- without asking the map for tiles at ty=-4. One row of band is - -- the least the pipeline can be given, and roofRows = 1 spends it - -- on that light top margin alone -- no window course is eaten, the - -- whole 63px of drawn facade stays standing, and the 4px slab caps - -- it flat. 67 voxels: nearly twice the Center and the Mart beside - -- it, and one voxel under its own twin B14 -- which is right, - -- because B14's facade IS this drawing. - -- - -- The last row carries a THIRTEENTH tile id. `matches` walks each - -- row to its own length while `read` composites only the first - -- #tiles[1] columns, so that id constrains where the template may - -- land without entering the drawing -- and it must not enter it, - -- because the only tile beside the tower is the $11 filler, whose - -- hatch carries isolated dark pixels the flood cannot reach: drawn, - -- each becomes a 64-deep rod of speckle hanging beside the tower. - -- The constraint is needed because these 8 rows ARE rows 5..12 of - -- flat_block_6x6: the bare grid matches four times, the tower plus - -- the lower two thirds of B14 at CELADON_CITY (14,4), ROUTE_16 - -- (18,6) and ROUTE_18 (34,4), and nothing stops a second model being - -- stamped inside the first. $11 sits east of the tower's south-east - -- corner; the three B14 plots carry grass, path and a plant there - -- ($23, $30, $55), so with the extra id the grid resolves once. + -- The old silhouette problem is gone with the roof back: the band + -- closes the drawing's north outline the way it does on every + -- sibling, so no `seal` is needed. The 13-id last row survives + -- from the first attempt: the bare 12-wide grid is B14's lower + -- two-thirds tile for tile, and the extra id keeps this template + -- from stamping inside flat_block_6x6's three placements + -- (`matches` walks a row to its own length; `read` composites + -- only #tiles[1] columns, so the 13th id constrains placement + -- without entering the drawing). { id = "pokemon_tower", + topRows = { + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + }, tiles = { { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, @@ -2794,8 +2780,25 @@ return { { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79, 17 }, }, - roofRows = 1, roofBack = 1, roofFront = 0, roofCycle = { 0, 0 }, - slab = 4, frontEave = 4, ledge = nil, seal = "n", + roofRows = 32, roofBack = 7, roofFront = 8, roofCycle = { 5, 12 }, + slab = 4, frontEave = 4, ledge = nil, + }, + -- the tower's roof half, where it actually stands on ROUTE_10: + -- claimed flat so the drawing does not ALSO fold up as a second + -- building behind the modelled tower (see topRows above) + { + id = "pokemon_tower_top", + claimOnly = true, + tiles = { + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + }, }, }, diff --git a/lib/Buildings.lua b/lib/Buildings.lua index 050a312..244943a 100644 --- a/lib/Buildings.lua +++ b/lib/Buildings.lua @@ -98,9 +98,24 @@ local models = {} -- ":" -> prebuilt local quads -- Composite the template out of the atlas and flood the silhouette in from -- the border. Returns flat arrays indexed y * W + x. +-- +-- `topRows`, when a template carries it, is extra drawing rows composited +-- ABOVE the matched grid: rows of the same drawing that are not on the +-- map this template places on. The Pokemon Tower is the case that needs +-- it -- the drawing straddles the LAVENDER_TOWN / ROUTE_10 boundary, its +-- roof band and top window courses standing in the route's last rows, so +-- no single map's grid holds the whole building. The matcher never sees +-- topRows (placement is still by `tiles` alone); they exist so the MODEL +-- is built from the complete drawing and the tower rises to its real +-- height instead of folding as two half-buildings. local function read(t, data, perRow) local tiles = t.tiles - local bh, bw = #tiles, #tiles[1] + if t.topRows then + tiles = {} + for _, row in ipairs(t.topRows) do tiles[#tiles + 1] = row end + for _, row in ipairs(t.tiles) do tiles[#tiles + 1] = row end + end + local bh, bw = #tiles, #t.tiles[1] local W, H = bw * 8, bh * 8 local col, ax, ay = {}, {}, {} for sy = 0, H - 1 do @@ -258,7 +273,13 @@ local function measure(sp, t) shadeTexel[s] = shadeTexel[s] or shadeTexel[BLACK] or 0 end - return { top = top, ytop = ytop, D = H, + -- Depth is the MATCHED footprint, not the sprite height. The two are + -- the same number for every whole-drawing template (the sprite is + -- built from `tiles` alone), but a template with `topRows` has a + -- sprite taller than its footprint -- the tower's 16-row drawing + -- stands on the 8 rows of it that are actually on the map, and D = H + -- would have pushed its body 64px south into the town plaza. + return { top = top, ytop = ytop, D = #t.tiles * 8, recess = recess, interior = interior, shadeTexel = shadeTexel } end @@ -610,9 +631,18 @@ function Buildings.build(S, map, data, perRow) if not built then local key = tileset.id .. ":" .. index if not models[key] then - local sp = read(t, data, perRow) - local pr = measure(sp, t) - models[key] = emit(model(sp, pr, t), sp, atlasW, atlasH) + if t.claimOnly then + -- claim the cells, stamp nothing: the drawing here is + -- the off-map half of a building another map models in + -- full (the tower's roof rows on ROUTE_10 -- Lavender's + -- placement composites them via topRows). Left to the + -- detector they stood as a second half-building. + models[key] = {} + else + local sp = read(t, data, perRow) + local pr = measure(sp, t) + models[key] = emit(model(sp, pr, t), sp, atlasW, atlasH) + end end built = models[key] end From 2ff518a6d1671d4d785e78805f89507c5122f744 Mon Sep 17 00:00:00 2001 From: DramaticShape Date: Mon, 27 Jul 2026 04:52:52 -0400 Subject: [PATCH 16/16] stop a second tower stamping behind the Pokemon Tower The tower's twelve ROUTE_10 rows are `gabled_block_6x6` tile for tile -- the artist drew the top of the tower as an ordinary six-cell block -- so that template matched at (24,132) and stood a whole second building behind it. Templates were matched and stamped independently, with nothing stopping two grids claiming one drawing. Placement is now first-claim-wins: a template never stamps into cells another already claimed, which makes the list order the priority order. The tower's own two templates move to the front so they take those cells first. Also corrects the split: the Route 10 half is TWELVE rows, not eight -- the 64px purple roof band plus two window courses -- so topRows and the claimOnly twin now carry all of it, and the model is the complete twenty-row drawing (96px facade under a real 64px roof). Both templates verified to place exactly once world-wide. The mound probe gains a MOUND_LEVEL knob; the low rungs are where this was visible. --- data/voxel_heights.lua | 148 +++++++++++++++++++----------------- lib/Buildings.lua | 22 +++++- tests/voxel_mound_probe.lua | 3 +- 3 files changed, 102 insertions(+), 71 deletions(-) diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 55bec61..a3f6975 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -2140,6 +2140,85 @@ return { -- placement. buildings = { OVERWORLD = { + -- assets/docs/buildings/B30: the POKEMON TOWER -- the one drawing + -- in the catalogue that STRADDLES A MAP BOUNDARY. Twelve of its + -- twenty rows stand in ROUTE_10's last rows (the 64px purple roof + -- band plus two window courses); the other eight -- five more + -- courses and the base with the door -- are on LAVENDER_TOWN, + -- where the tower begins at tile row 0. Modelled per map it came + -- out as two buildings, one per half. + -- + -- `topRows` composites ROUTE_10's twelve rows ABOVE the matched + -- grid, so the model is built from the COMPLETE twenty-row drawing + -- -- 96px of facade under the real 64px roof -- while placement + -- still matches only the eight Lavender rows. Its `claimOnly` + -- twin below claims the ROUTE_10 rows and stamps nothing, so the + -- roof half does not also stand as its own building. + -- + -- BOTH COME FIRST IN THIS LIST ON PURPOSE. The tower's upper + -- twelve rows are `gabled_block_6x6` tile for tile -- the artist + -- drew the top of the tower as an ordinary six-cell block -- so + -- that template matches at ROUTE_10 (24,132) too. Placement is + -- first-claim-wins (see Buildings.build), so claiming here before + -- the generic block reaches it is what keeps the second tower from + -- being stamped behind this one. + -- + -- The 13-id last row is kept from the first attempt: the bare + -- 12-wide body grid is B14's lower two-thirds tile for tile, and + -- the extra id stops this template stamping inside + -- `flat_block_6x6`'s three placements (`matches` walks a row to + -- its own length, while `read` composites only #tiles[1] columns, + -- so a 13th id constrains placement without entering the drawing). + { + id = "pokemon_tower", + topRows = { + { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + }, + tiles = { + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79, 17 }, + }, + roofRows = 64, roofBack = 8, roofFront = 8, roofCycle = { 8, 31 }, + slab = 4, frontEave = 4, ledge = nil, + }, + -- the tower's roof half, where it actually stands on ROUTE_10: + -- claimed flat so the drawing does not ALSO fold up as a building + -- behind the modelled tower (see topRows above) + { + id = "pokemon_tower_top", + claimOnly = true, + tiles = { + { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + }, + }, -- assets/docs/buildings/B07: the two-storey gabled house. Coursed cap over -- a gable wall, an awning with its double black underline, then the -- ground floor with the door. 7 placements, Red's and Blue's among @@ -2731,75 +2810,6 @@ return { roofRows = 64, roofBack = 8, roofFront = 8, roofCycle = { 8, 31 }, slab = 4, frontEave = 4, ledge = nil, seal = "s", }, - -- assets/docs/buildings/B30: the POKEMON TOWER. The one drawing - -- in the catalogue that STRADDLES A MAP BOUNDARY: its purple roof - -- band and top two window courses stand in ROUTE_10's last eight - -- tile rows, and only the lower eight rows -- six more courses and - -- the base with the door -- are on LAVENDER_TOWN, where the - -- building begins at tile row 0. Modelled per map it came out as - -- two half-buildings: Lavender's half wearing a fabricated flat - -- cap (art row 0 stretched into a yellow slab), Route 10's half a - -- low volume of roof drawing folded at ground level behind it. - -- - -- `topRows` is the fix, built for this template: the eight - -- ROUTE_10 rows are composited ABOVE the matched grid, so the - -- model is the COMPLETE 16-row drawing -- 96px of facade under a - -- real 32px roof band -- while placement still matches only the - -- Lavender rows. Its twin `pokemon_tower_top` below claims the - -- Route 10 rows and stamps nothing, so the roof half no longer - -- also stands as its own building behind the tower. - -- - -- The old silhouette problem is gone with the roof back: the band - -- closes the drawing's north outline the way it does on every - -- sibling, so no `seal` is needed. The 13-id last row survives - -- from the first attempt: the bare 12-wide grid is B14's lower - -- two-thirds tile for tile, and the extra id keeps this template - -- from stamping inside flat_block_6x6's three placements - -- (`matches` walks a row to its own length; `read` composites - -- only #tiles[1] columns, so the 13th id constrains placement - -- without entering the drawing). - { - id = "pokemon_tower", - topRows = { - { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, - { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, - { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, - { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - }, - tiles = { - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79, 17 }, - }, - roofRows = 32, roofBack = 7, roofFront = 8, roofCycle = { 5, 12 }, - slab = 4, frontEave = 4, ledge = nil, - }, - -- the tower's roof half, where it actually stands on ROUTE_10: - -- claimed flat so the drawing does not ALSO fold up as a second - -- building behind the modelled tower (see topRows above) - { - id = "pokemon_tower_top", - claimOnly = true, - tiles = { - { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, - { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, - { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, - { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, - { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, - }, - }, }, FOREST = { diff --git a/lib/Buildings.lua b/lib/Buildings.lua index 244943a..7bfc6e0 100644 --- a/lib/Buildings.lua +++ b/lib/Buildings.lua @@ -627,7 +627,27 @@ function Buildings.build(S, map, data, perRow) for ty = 0, th - bh do Budget.tick() for tx = 0, tw - bw do - if S.tileAt[keyOf(tx, ty)] == first and matches(S, t, tx, ty) then + -- A placement never stamps into cells another template already + -- claimed. Templates are matched independently, and one + -- drawing can satisfy two grids: the Pokemon Tower's upper + -- twelve rows on ROUTE_10 are a standard 6-cell block tile for + -- tile, so `gabled_block_6x6` matched there and stood a whole + -- second building behind the tower. First claim wins, so the + -- list order below is the priority order -- the tower's own + -- templates come first precisely so they take those cells. + local free = S.tileAt[keyOf(tx, ty)] == first + if free then + for r = 0, bh - 1 do + for c = 0, bw - 1 do + if S.skip[keyOf(tx + c, ty + r)] then + free = false + break + end + end + if not free then break end + end + end + if free and matches(S, t, tx, ty) then if not built then local key = tileset.id .. ":" .. index if not models[key] then diff --git a/tests/voxel_mound_probe.lua b/tests/voxel_mound_probe.lua index 3bbcb2a..11ad540 100644 --- a/tests/voxel_mound_probe.lua +++ b/tests/voxel_mound_probe.lua @@ -30,7 +30,8 @@ return function(game) Pipelines.setLevel("tiltshift", 0) U.teleport(game, mapId, tonumber(sx), tonumber(sy), facing) U.wait(20) - Pipelines.setLevel("voxel", 3) + Pipelines.setLevel("voxel", + math.floor(tonumber(os.getenv("MOUND_LEVEL")) or 3)) U.wait(30) local V = game.mods.exports["DRAMATIC_SHAPE"]