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