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.
This commit is contained in:
DramaticShape
2026-07-27 00:37:56 -04:00
parent 5a94dfe85a
commit da0fc49176
4 changed files with 129 additions and 5 deletions
+19
View File
@@ -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
+9
View File
@@ -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,
+36 -5
View File
@@ -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
+65
View File
@@ -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