fixed tops of walls in houses

This commit is contained in:
DramaticShape
2026-08-08 22:12:07 -04:00
parent 787d93a0e4
commit 47369c24fe
6 changed files with 266 additions and 40 deletions
+36
View File
@@ -131,6 +131,42 @@
strips the way a facade's does) and no voxels: the recess removes as
many faces as it exposes.
### Fixed
- **The Pokemon Center's steps climb.** The Cable Club stairs, cut into
the back wall of all eleven Centers, were built as a stairwell sunk
into the floor -- they lead UP to the Center's second floor. The
head-on stair reading was right and stays: a drawn ROW is a step and
drawn row is depth row, 1:1 into the opening, the drawing's own band
table landing exactly on four steps and its black edge columns walling
the opening. Only the sign of the rise was wrong, and two things follow
from it. The risers turn around -- a flight descending away from you
closes its steps from below and shows you their backs, one climbing
away shows you their fronts -- and the black edge columns become the
walls of the opening the flight climbs into, running from each tread up
to the top of the wall band rather than down from the floor to it. The
top step lands level with that band, in the dark rows the artist drew
there, so the flight fills the opening it leaves by. The descending
class (`stair_down_n`) is unchanged and still available; the Centers
now pin `stair_n`.
- **Nothing is hung on the TOP of an interior wall.** A wall band is 16px
of art folded upright over a run two drawn rows deep, so it folds
entirely onto its south face and has no drawn row left to lay flat on
top -- and the top then repeated the face. The town house's town-map
poster (cell (3,0)) and its window ((5,0)), and the Pokemon Center's
pokeball poster ((3,0) and (4,0)), each came out lying across the top
of the wall as well as hanging on it: a picture you look DOWN on.
What is really up there is the wall's own capping course, which is
exactly the plain panel the decorated column's neighbours draw --
`wall_top` in `data/voxel_heights.lua` names it per tileset (HOUSE caps
with the blank course, POKECENTER with the striped panel cell (9,0)
draws). Per tileset rather than per tile because one room caps with one
course, and because "plain" is a fact about the drawing that no
measurement of the geometry can recover. Only the top face is
redirected; the poster still faces the room.
## 1.8.0
### Added
+39 -13
View File
@@ -50,6 +50,9 @@
-- stair_down_e / _down_w a sunken stairwell: the cell opens into a
-- hole with steps descending toward the named
-- side -- stairs that lead DOWN a floor
-- stair_n / stair_down_n the same two flights running into the map
-- rather than across it, for a staircase drawn
-- HEAD-ON: a drawn row is a step, not a column
-- relief a prop drawn from above (a console on the
-- floor): the drawing stays flat and the
-- pixels inside its black outline extrude a
@@ -129,6 +132,16 @@
-- measured rather than drawn, by flooding the panel tile's own stripe out
-- from behind them; see CLUB below.
--
-- And `wall_top` (not a class either): the tile every `wall` cell's TOP
-- face wears, whatever the cell itself draws. An interior wall band is
-- 16px of art folded upright, and a fully folded run has no drawn row left
-- over to lay flat on top -- so the top repeated the FACE, and the town-map
-- poster and the window of a house came out lying across the top of the
-- wall as well as hanging on it. What is really up there is the wall's own
-- capping course, which is exactly the plain panel the decorated column's
-- neighbours draw: name that tile and every wall in the tileset caps with
-- it. The face is untouched -- the poster still faces the room.
--
-- 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
@@ -860,6 +873,12 @@ return {
-- the wall band stays one 16px face: blank courses, the window,
-- the framed picture, and the schoolhouse blackboard (72-75/88-91)
wall = { 0, 36, 45, 46, 52, 61, 62, 72, 73, 75, 88, 89, 90, 91 },
-- and it caps with the blank course. Cells (3,0) and (5,0) of the
-- town house are the town-map poster (45/46 over 61/62) and the
-- window (36 over 52); without this the top of the wall wore the
-- poster and the glass, laid flat, instead of the plain panel their
-- own left-hand neighbour draws
wall_top = 0,
-- stools: a seat-high box that also seats Daisy
stool = { 2, 3, 18, 19 },
-- the dining table (top edge 38/41 also caps the bookcases);
@@ -905,21 +924,28 @@ return {
-- two flanks -- see `prop` below.
wall = { 2, 3, 4, 5, 6, 16, 18, 19, 20, 21, 22, 40, 41,
76, 77 },
-- and it caps with the striped panel, the tile cell (9,0) draws.
-- The pokeball poster spans cells (3,0) and (4,0) (2/3 over 18/19),
-- and without this the top of the wall wore the poster lying flat as
-- well as hanging it on the face
wall_top = 40,
-- THE CABLE CLUB STEPS, cut into the back wall at cells (10,0) and
-- (12,0) of every Center -- a flight going DOWN, away from the room,
-- and the reason `stair_down_n` exists at all: the profile's other
-- stairs run east or west and are drawn from the SIDE, where a drawn
-- column is a step; these are drawn HEAD-ON, where a drawn row is,
-- and no rotation of the east/west reading produces that.
-- (12,0) of every Center -- a flight going UP, away from the room, to
-- the Center's second floor, and the reason the head-on stair classes
-- exist at all: the profile's other stairs run east or west and are
-- drawn from the SIDE, where a drawn column is a step; these are drawn
-- HEAD-ON, where a drawn row is, and no rotation of the east/west
-- reading produces that.
--
-- The drawing is its own band table, and it lands exactly on an even
-- four-step division of the cell: 4 white rows (the near tread), a
-- black nosing, 3 grey, a nosing, 3 checker, then 4 black rows -- the
-- dark the flight leaves into, which is also what the far end wall
-- wears. Its first and last COLUMNS are the well's black side walls.
-- Drawn row = depth row throughout; the rise is the only number the
-- head-on view cannot state, and it takes the class height over the
-- four steps like every other flight here.
-- four-step division of the cell: 4 white rows (the near tread, the
-- one at floor level), a black nosing, 3 grey, a nosing, 3 checker,
-- then 4 black rows -- the dark the flight climbs into, which is the
-- top step, level with the wall band it is cut through. Its first and
-- last COLUMNS are the opening's black side walls. Drawn row = depth
-- row throughout; the rise is the only number the head-on view cannot
-- state, and it takes the class height over the four steps like every
-- other flight here.
--
-- Pinned as one cell (the class resolves off the top-left tile) but
-- all four ids carry it, and the scan says they cannot reach anything
@@ -927,7 +953,7 @@ return {
-- Centers, and the Celadon Hotel on the same id places none of them.
-- 94 is also the map's warp tile; pins are look-only, so the warp is
-- untouched and the steps stay walk-through.
stair_down_n = { 92, 93, 94, 95 },
stair_n = { 92, 93, 94, 95 },
-- the counters, half a cell high: top band (8) and the one cell of
-- it that carries the push bell (10, lifted off as a figure below --
-- the pin stays as the degradation path), front face (24/25, the
+11
View File
@@ -301,6 +301,14 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
return tile
end
-- The capping course an interior wall wears on its TOP face (see
-- TileShape.wallTop). A wall band folds entirely onto its own face, so
-- the top had nothing left to lay flat and repeated the face -- a house's
-- town-map poster and window, and a Center's pokeball poster, came out
-- lying across the top of the wall as well as hanging on it. Only the top
-- is redirected: the face still draws what the map draws.
local wallTop = TileShape.wallTop(tileset.id)
-- one atlas-rect UV, optionally cropped to art rows [vTop, vBot] of 8
local function uvRect(tile, vTop, vBot)
local ax = (tile % perRow) * 8
@@ -650,6 +658,9 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
and (north - 1) or north
end
topTile = S.tileAt[keyOf(tx, row)]
-- ...unless the tileset names the course its walls cap with,
-- which every wall cell wears whatever it draws on its face
if wallTop and s.class == "wall" then topTile = wallTop end
end
-- water's surface, and only water's: the recessed sheet itself,
-- never the ground's shoreline bands around it. A cell an object
+50 -22
View File
@@ -1992,6 +1992,10 @@ end
-- walls) wear the matching slice of that drawing -- the railing's
-- diagonal lands along the stepped silhouette -- while treads sample the
-- art band drawn at their own height.
--
-- stair_n / stair_down_n are the same pair of flights running INTO the
-- map rather than across it, for a staircase drawn head-on; that changes
-- the art reading enough to need its own branch below.
local STAIR_STEPS = 4
local STAIR_SHADE = { south = 1.0, north = 0.68, tread = 1.0,
@@ -2004,8 +2008,8 @@ local function stairCell(S, map, data, cx, cy, s)
local atlasW = map.tileset.imageWidth or 128
local atlasH = map.tileset.imageHeight or 48
local quads = S.objectQuads
local north = s.class == "stair_down_n"
local down = north or s.class == "stair_down_e"
local north = s.class == "stair_n" or s.class == "stair_down_n"
local down = s.class == "stair_down_n" or s.class == "stair_down_e"
or s.class == "stair_down_w"
local east = s.class == "stair_e" or s.class == "stair_down_e"
local mx, mz = cx * 16, cy * 16
@@ -2077,6 +2081,17 @@ local function stairCell(S, map, data, cx, cy, s)
-- COLUMNS are its black side walls, and its top band is the darkness the
-- flight leaves by, which is what the far end wants to wear.
--
-- A flight CLIMBING away (`stair_n`) is the same reading with the sign of
-- the rise flipped -- bands still run south to north, drawn row is still
-- depth row, the nosing still serves twice. Two things follow from the
-- sign. The risers turn around: a flight descending away from you closes
-- its steps from below and shows you their backs, one climbing away shows
-- you their FRONTS, so they face south. And the drawing's black side
-- columns stop being a well's walls and become the walls of the opening
-- the flight climbs into: they run from each tread UP to the top of the
-- wall band rather than down from the floor. At the last step the flight
-- has reached that top and there is no opening left to wall.
--
-- Every quad here is split at the cell's own 8px seam, in x and in rows
-- both: `uv` resolves ONE tile per corner, and these four tiles are not
-- neighbours in the atlas, so a quad that spans a seam interpolates
@@ -2087,7 +2102,8 @@ local function stairCell(S, map, data, cx, cy, s)
for i = 0, STAIR_STEPS - 1 do
local a0 = 16 - (i + 1) * runD -- band i, in art rows
local a1 = a0 + runD
local yTop = -(i + 1) * rise
local yTop = (down and -1 or 1) * (i + 1) * rise
local ry = (down and -1 or 1) * i * rise -- the step behind it
local z0b, z1b = mz + a0, mz + a1
for _, H in ipairs(HALVES) do
@@ -2097,44 +2113,56 @@ local function stairCell(S, map, data, cx, cy, s)
-- lies on its front lip exactly where the artist drew it
face({ wx0, yTop, z0b }, { wx1, yTop, z0b },
{ wx1, yTop, z1b }, { wx0, yTop, z1b },
ax0, a1, ax1, a0, STAIR_SHADE.wellTread)
ax0, a1, ax1, a0,
down and STAIR_SHADE.wellTread or STAIR_SHADE.tread)
-- the riser under that lip. It faces NORTH -- a flight descending
-- away from you turns its risers away with it, and they close the
-- steps from below rather than being looked at. One art row tall,
-- so it needs none of `banded`'s row splitting; written straight
-- keeps the geometry flush at the seam while the art stays inside
-- its tile
local ry = -i * rise
face({ wx1, yTop, z1b }, { wx0, yTop, z1b },
{ wx0, ry, z1b }, { wx1, ry, z1b },
ax1, a1 - 1, ax0, a1, STAIR_SHADE.riser)
-- the riser at that lip, one art row tall -- so it needs none of
-- `banded`'s row splitting, and written straight keeps the geometry
-- flush at the seam while the art stays inside its tile. Facing
-- north when the flight descends (the steps are closed from below,
-- not looked at) and south when it climbs
if down then
face({ wx1, yTop, z1b }, { wx0, yTop, z1b },
{ wx0, ry, z1b }, { wx1, ry, z1b },
ax1, a1 - 1, ax0, a1, STAIR_SHADE.riser)
else
face({ wx0, ry, z1b }, { wx1, ry, z1b },
{ wx1, yTop, z1b }, { wx0, yTop, z1b },
ax0, a1 - 1, ax1, a1, STAIR_SHADE.riser)
end
-- the deep end, closing the opening this flight is cut into: from
-- the floor of the well up to the top of the wall band beside it,
-- in the drawing's own black top rows
if i == STAIR_STEPS - 1 then
-- in the drawing's own black top rows. A climbing flight has no
-- such end -- its top tread stands at the wall's own height and
-- fills the opening
if down and i == STAIR_STEPS - 1 then
face({ wx1, -h, mz }, { wx0, -h, mz },
{ wx0, h, mz }, { wx1, h, mz },
ax1, 3.9, ax0, 0.1, STAIR_SHADE.wellEnd)
end
end
-- the well's side walls above this tread, wearing the drawing's own
-- black edge columns -- the excavation is walled in its own texels
-- the opening's side walls beside this tread, wearing the drawing's
-- own black edge columns -- excavation or recess, it is walled in its
-- own texels. Descending they run from the tread up to the floor,
-- climbing from the tread up to the top of the wall band
local wallTop = down and 0 or h
local function sideWall(px, sx0, sx1, inward)
local c
if inward then -- west wall, faces E
c = { { px, yTop, z1b }, { px, yTop, z0b },
{ px, 0, z0b }, { px, 0, z1b } }
{ px, wallTop, z0b }, { px, wallTop, z1b } }
else -- east wall, faces W
c = { { px, yTop, z0b }, { px, yTop, z1b },
{ px, 0, z1b }, { px, 0, z0b } }
{ px, wallTop, z1b }, { px, wallTop, z0b } }
end
face(c[1], c[2], c[3], c[4], sx0, a1, sx1, a0, STAIR_SHADE.wellN)
end
sideWall(mx, 0.1, 1.3, true)
sideWall(mx + 16, 14.7, 15.9, false)
if wallTop > yTop then
sideWall(mx, 0.1, 1.3, true)
sideWall(mx + 16, 14.7, 15.9, false)
end
end
return
end
+30 -5
View File
@@ -125,11 +125,13 @@ local FALLBACK_HEIGHTS = {
stair_w = 16,
stair_down_e = 16,
stair_down_w = 16,
-- a stairwell descending toward the BACK of the map, drawn head-on
-- instead of from the side (the Centers' Cable Club steps). Its own
-- class because the art reading is not the east/west one turned: there
-- a drawn COLUMN is a step and a drawn row is height, here a drawn ROW
-- is a step and drawn row = depth row, 1:1 down the well
-- a flight running toward the BACK of the map, drawn head-on instead of
-- from the side (the Centers' Cable Club steps). Its own class because
-- the art reading is not the east/west one turned: there a drawn COLUMN
-- is a step and a drawn row is height, here a drawn ROW is a step and
-- drawn row = depth row, 1:1 into the opening. `stair_n` climbs away
-- from the room, `stair_down_n` descends into a well
stair_n = 16,
stair_down_n = 16,
}
@@ -221,6 +223,7 @@ local ART = {
stair_w = "stair",
stair_down_e = "stair",
stair_down_w = "stair",
stair_n = "stair",
stair_down_n = "stair",
}
@@ -761,6 +764,28 @@ function TileShape.bookcaseRelief(tilesetId)
return not (entry and entry.bookcase_relief == false)
end
--- What every `wall` cell's TOP face wears in this tileset (a tileset
--- entry's wall_top). Returns a tile id, or nil to leave the top alone.
---
--- A wall band is 16px of art folded upright over a run two drawn rows
--- deep, so it folds ENTIRELY onto its face and has no row left to lay
--- flat on top -- the top then repeats the face, and a house's town-map
--- poster and window came out lying across the top of the wall as well as
--- hanging on it. What is up there is the wall's capping course, which is
--- the plain panel the decorated column's own neighbours draw; naming it
--- per tileset is the whole fix, because "plain" is a fact about the
--- drawing that nothing in the geometry can measure.
---
--- Per tileset rather than per tile: one room caps with one course, and a
--- list keyed by the decorated tiles would have to be extended every time
--- a map hung something new on a wall already covered.
function TileShape.wallTop(tilesetId)
local s = load()
local entry = s and s.tilesets and s.tilesets[tilesetId]
local tile = entry and entry.wall_top
return type(tile) == "number" and tile or nil
end
-- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
-- record needs the next lookup to re-resolve (hot reload, mod toggle).
function TileShape.invalidate()
+100
View File
@@ -7002,6 +7002,106 @@ end)()
T.check(ball.stars == nil, "and the stars burn out on their own")
end)()
-- ------- an interior wall caps with its own plain course
--
-- A wall band is 16px of art over a run two drawn rows deep, so it folds
-- ENTIRELY onto its south face and has no drawn row left to lay flat on
-- top. The top therefore repeated the face: the town house's town-map
-- poster and window, and the Pokemon Center's pokeball poster, came out
-- lying across the top of the wall as well as hanging on it -- a picture
-- you look DOWN on from the voxel camera.
--
-- `wall_top` names the capping course per tileset (the plain panel the
-- decorated column's own neighbours draw), and it is a fact about the
-- drawing that no measurement of the geometry can recover -- nothing
-- distinguishes "poster" from "panel" but knowing which is which.
;(function()
local Mesher = run.loader.exports.DRAMATIC_SHAPE.lib.require("ChunkMesher")
local Struct = run.loader.exports.DRAMATIC_SHAPE.lib.require("Structures")
local Shape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape")
T.eq(Shape.wallTop("HOUSE"), 0,
"the town house caps its walls with the blank course")
T.eq(Shape.wallTop("POKECENTER"), 40,
"and a Center with its striped panel -- the tile cell (9,0) draws")
T.eq(Shape.wallTop("DS_NO_SUCH_TILESET"), nil,
"a tileset that says nothing keeps the top it always had")
local FLOOR = 1
local function topTiles(tilesetId, rows)
local map = {
id = "DS_TEST_WALLTOP_" .. tilesetId,
tileset = { id = tilesetId, image = "gfx/tilesets/ds_test.png",
tilesPerRow = 16, imageWidth = 128, imageHeight = 48,
blocks = {}, grassTile = -1 },
def = { width = 4, height = 2, tileset = tilesetId },
walkable = { [FLOOR] = true },
waterTiles = {},
doorTiles = {},
tileAt = function(_, tx, ty)
local r = rows[ty + 1]
return (r and r[tx + 1]) or FLOOR
end,
cellTile = function(self, cx, cy) return self:tileAt(cx * 2, cy * 2 + 1) end,
isWaterCell = function() return false end,
isWalkableCell = function(self, cx, cy)
return self:cellTile(cx, cy) == FLOOR
end,
inBounds = function(_, cx, cy)
return cx >= 0 and cy >= 0 and cx < 4 and cy < 2
end,
}
Struct.invalidate(map.id)
local verts = Mesher.geometry(map, true, nil)
-- the flat quads standing at wall height, keyed by the column they cap
local out = {}
for i = 1, #verts, 4 do
local a, b, c, d = verts[i], verts[i + 1], verts[i + 2], verts[i + 3]
if a[2] == b[2] and b[2] == c[2] and c[2] == d[2] and a[2] == 16 then
local ax = math.floor(math.min(a[4], b[4], c[4], d[4]) * 128 + 0.5)
local ay = math.floor(math.min(a[5], b[5], c[5], d[5]) * 48 + 0.5)
local tx = math.floor(math.min(a[1], b[1], c[1], d[1]) / 8)
out[tx] = out[tx] or (math.floor(ay / 8) * 16 + math.floor(ax / 8))
end
end
return out, verts
end
-- BLUES_HOUSE's back wall: blank panel, the town-map poster (45/46 over
-- 61/62) at cell (3,0)'s block, the window (36 over 52) at (5,0)'s
local houseTops, houseVerts = topTiles("HOUSE", {
{ 0, 0, 45, 46, 36, 36, 0, 0 },
{ 0, 0, 61, 62, 52, 52, 0, 0 },
})
for tx = 0, 7 do
T.eq(houseTops[tx], 0,
("column %d of the house's back wall caps with the blank course"):format(tx))
end
-- and the FACE is untouched: the poster still hangs in the room, which is
-- the half of this the fix must not take with it
local hung = {}
for _, v in ipairs(houseVerts) do
hung[math.floor(math.floor(v[5] * 48 + 0.5) / 8) * 16
+ math.floor(math.floor(v[4] * 128 + 0.5) / 8)] = true
end
T.check(hung[45] and hung[46] and hung[61] and hung[62],
"the town-map poster is still drawn on the wall it hangs on")
T.check(hung[36] and hung[52], "and so is the window")
-- VIRIDIAN_POKECENTER's back wall: the striped panel with the pokeball
-- poster (2/3 over 18/19) spanning cells (3,0) and (4,0)
local pcTops = topTiles("POKECENTER", {
{ 40, 40, 40, 2, 3, 40, 40, 40 },
{ 40, 40, 40, 18, 19, 40, 40, 40 },
})
for tx = 0, 7 do
T.eq(pcTops[tx], 40,
("column %d of the Center's back wall caps with the striped panel")
:format(tx))
end
end)()
Pipelines.reset()
run.release()