diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3e552..bca1656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,50 @@ on the pattern by luck -- without that, every setting would be itself and 1/8192 in parallel, and no setting could ever be rarer than 8192. +- **DOORS ON A GATE HOUSE'S OTHER SIDES.** A route gate is walked + through, so it opens on two opposite faces -- and the overworld drawing + can only show one. The sprite is a facade seen face-on under a roof + seen from above, so a SOUTH entrance is drawn (a doorway block in the + facade's last rows, which `Structures` folds up into the front face) + and a north, east or west one is drawn as nothing at all: the warp + sits on the ground cell outside, the art beside it is plain wall. + Top-down that reads fine, because the wall is never seen. In 3D you + walked into a blank slab -- 33 of them, on eleven gates. + + Each is now a real doorway, one cell of the tileset's own door art + standing on the ground of the face you walk into, hung by the SAME + rule the drawn facade hangs its own door by: the black frame stays + flush with the wall and what it seals sinks a voxel behind it, so a + side door and a front door are the same opening at the same depth, and + the jambs the recess exposes fall out of the mesher wearing the + frame's own texels. The art's outer ring is left alone -- a doorway + cell is a cell OF a facade, its border is the wall beside and above + the frame, and painting all 16x16 would stamp a one-pixel strip of + front-wall art around every door. The same flood the facade tells wall + from pane with, bounded to the cell, tells them apart here. + + **Nothing is authored but the art.** `data/voxel_heights.lua` names + one door cell per tileset and no coordinates: where the doors go is + read off the map, from the warps that land in a gate house and the + building standing against them. A warp is an entrance when it leads + into a GATE-tileset map, is not already ON a door tile (a drawn south + door would be fought over), and stands on a WALKABLE cell -- the ROM + gives several gates an unreachable twin warp on the fence or tree + beside the real opening, and a door behind a fence is a door into + nothing. What is left is the entrance, and the two cells side by side + that most gates do have come out as the double door they always were. + So no hand list to drift out of step with a map edit, and every other + building in the game is untouched: exactly the eleven gate placements + get doors. + + Doors belong to the PLACEMENT, not the drawing -- the same 6x4 block + is the gate on four routes and the warps sit at different rows of it + on each -- so the model cache is keyed by the openings as well, and + only placements that agree share a model. It costs about 290 quads a + door (a flank quad carries one texel, so the art cannot merge into + strips the way a facade's does) and no voxels: the recess removes as + many faces as it exposes. + ## 1.8.0 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index a963ab3..2bb11c5 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -890,9 +890,8 @@ return { -- which has a PERSON drawn into the tile art -- becomes a monolith -- wearing his face. POKECENTER = { - -- the wall band stays one 16px face: striped panels (40), the high - -- windows (92-95; 94 doubles as the map's warp tile, and pins are - -- look-only), the pokeball poster (2/3/18/19), and the pillars + -- the wall band stays one 16px face: striped panels (40), the + -- pokeball poster (2/3/18/19), and the pillars -- (16/41) with their bases (4/5/20/21; 20 is the $14 water-fallback -- trap and would recess into a pond lip). The healing machines' -- console face (76/77) and button panel (6/22) are ALSO wall: @@ -905,7 +904,30 @@ return { -- void rule flattens them. What is NOT wall is the machines' -- two flanks -- see `prop` below. wall = { 2, 3, 4, 5, 6, 16, 18, 19, 20, 21, 22, 40, 41, - 76, 77, 92, 93, 94, 95 }, + 76, 77 }, + -- 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. + -- + -- 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. + -- + -- 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 + -- else: 22 placements, exactly the two cells in each of the eleven + -- 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 }, -- 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 @@ -3072,6 +3094,36 @@ return { FOREST = { 42, 43, 58, 59 }, }, + -- THE DOOR A GATE HOUSE IS ENTERED BY FROM ANY SIDE BUT THE SOUTH. + -- + -- A route gate is walked THROUGH, so it has an opening on two opposite + -- sides -- and the drawing can only show one of them. The overworld + -- sprite is a facade seen face-on with a roof laid over it, so a south + -- entrance is drawn (a doorway block in the facade's last rows, folded up + -- by lib/Structures.lua) and a north, east or west one is drawn as + -- NOTHING: the warp sits on the ground cell outside, the art beside it is + -- plain wall, and top-down that reads fine because you never see the + -- wall. In 3D you walk straight into a blank slab. + -- + -- So the door is put back, on the face the player walks into. This names + -- only the ART -- one 16x16 cell of the tileset's own doorway block, rows + -- north-first, the same ids `frontOnly` above lists as facade-only. + -- WHERE it goes is not authored at all: lib/Buildings.lua reads it off + -- the map, from the warps that land in a gate and the building standing + -- against them (see `sideDoors` there), because the map already states it + -- and a hand list of thirty-odd coordinates would only be a chance to get + -- one wrong. + -- + -- OVERWORLD is the whole table because every such entrance in the game + -- stands against an OVERWORLD building: the Safari Zone's north gate is a + -- gap between two fence stubs with no drawing to carve, and the Route 22 + -- league gate on ROUTE_23 puts its warps on the road THROUGH the arch + -- rather than against a wall. Both come out with no door, which is what + -- they always had. + sideDoors = { + OVERWORLD = { { 11, 12 }, { 27, 28 } }, + }, + -- Buildings whose whole sprite is voxelized band by band (lib/Buildings.lua, -- the pipeline in assets/docs/buidling_to_voxel/). A building is matched by its -- exact tile grid -- the drawings are catalogued in assets/docs/buildings/ -- so diff --git a/lib/Buildings.lua b/lib/Buildings.lua index f11d1ca..46f89a8 100644 --- a/lib/Buildings.lua +++ b/lib/Buildings.lua @@ -136,6 +136,13 @@ end local models = {} -- ":" -> prebuilt local quads local frontSets = {} -- tileset id -> { [tile] = true } or false +-- The tileset's side-door art: the 2x2 tile grid of one doorway cell +-- (data/voxel_heights.lua `sideDoors`), or nil when the tileset names none. +function Buildings.sideDoorCell(tilesetId) + local s = profile() + return s and s.sideDoors and s.sideDoors[tilesetId] or nil +end + -- The tileset's front-only tiles as a set (data/voxel_heights.lua -- `frontOnly`): the doorways, shop signs and painted lettering that belong -- on a facade and on no other face of the same building. nil when the @@ -252,6 +259,82 @@ end -- 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. +-- Composite one doorway cell PAST the end of the sprite, at indices +-- W*H .. W*H+255, and hand back its base. The side-door pass paints with +-- sprite indices like everything else -- `emit` resolves a voxel's colour +-- through sp.ax/sp.ay and knows nothing about where the index came from -- +-- so the door only has to BE in the sprite arrays to travel the rest of +-- the pipeline untouched. Appending rather than drawing into the grid is +-- the point: the drawing itself must not change, or the silhouette flood, +-- the taper and every measured band would be read off art the tileset +-- never placed here. +-- +-- Nothing else walks past W*H (measure's shadeTexel scan and the pane +-- flood both stop there), so the block is invisible to measurement and +-- visible only to the code that asks for it by index. +-- +-- WHICH OF THE 256 TEXELS ARE THE DOOR. A doorway cell is not a doorway +-- edge to edge: the tileset draws it as a cell OF A FACADE, so its outer +-- ring is the wall beside and above the frame, and its last row is the +-- black threshold the building stands on with the door's own step cut into +-- it. Painting all 16x16 onto a flank would stamp a one-pixel border of +-- front-wall art around every door. +-- +-- The front facade tells the ring from the door by flooding: the wall +-- around the door is one region with the whole facade, far too big to be a +-- pane, and only what the black frame SEALS sinks. The same test, bounded +-- to the block: flood the left, right and top edges through their own +-- shade class, and what the flood reaches is context -- left unpainted, so +-- the flank keeps the texel it already had. Not the bottom edge, because +-- the bottom edge is the ground: the step under the door is sealed there +-- on the drawn facade too, and it recesses with the rest of the doorway. +-- +-- What is painted then splits the way a facade's does: black is frame and +-- stays flush with the wall, everything it seals sinks a voxel behind it. +local function readDoor(sp, data, perRow, cell) + local base = sp.W * sp.H + local black = {} + for dy = 0, 15 do + local row = cell[math.floor(dy / 8) + 1] + for dx = 0, 15 do + local tile = row[math.floor(dx / 8) + 1] + local px = (tile % perRow) * 8 + dx % 8 + local py = math.floor(tile / perRow) * 8 + dy % 8 + local k = dy * 16 + dx + local i = base + k + sp.ax[i], sp.ay[i] = px, py + local r, g, b, a = data:getPixel(px, py) + sp.col[i] = shadeOf(r, g, b, a) + sp.inside[i] = true + black[k] = sp.col[i] == BLACK + end + end + + local context, stack = {}, {} + local function seed(dx, dy, cls) + if dx < 0 or dx > 15 or dy < 0 or dy > 15 then return end + local k = dy * 16 + dx + if context[k] or black[k] ~= cls then return end + context[k] = true + stack[#stack + 1] = k + end + for dy = 0, 15 do + seed(0, dy, black[dy * 16]) + seed(15, dy, black[dy * 16 + 15]) + end + for dx = 0, 15 do seed(dx, 0, black[dx]) end + while #stack > 0 do + local k = table.remove(stack) + local dx, dy, cls = k % 16, math.floor(k / 16), black[k] + seed(dx + 1, dy, cls) + seed(dx - 1, dy, cls) + seed(dx, dy + 1, cls) + seed(dx, dy - 1, cls) + end + + sp.door = { base = base, black = black, context = context } +end + local function read(t, data, perRow, frontOnly) local tiles = t.tiles if t.topRows then @@ -1087,11 +1170,113 @@ local function deskSetModel(sp, pr, t) W = W, ytop = ytop, zmin = 0, zmax = D - 1 } end +-- ------- the doorway a gate house is entered by from a side the drawing +-- never shows it on (data/voxel_heights.lua `sideDoors`, and `sideDoorsAt` +-- below for how the placements are found). +-- +-- One cell of the tileset's own doorway art, standing on the ground of the +-- face the player walks into, and hung by the SAME rule the drawn facade +-- hangs its own door by: the art's black frame stays flush with the wall +-- and everything it seals sinks a voxel behind it (`measure`'s pane pass, +-- applied here by hand because the art is not in the drawing to be flooded +-- with it). So a side door and a front door are the same depth of the same +-- opening, and the jamb faces the recess exposes come out of the mesher for +-- free, wearing the frame's own texels. +-- +-- ORIENTATION IS NOT FREE. A flank quad carries one texel and the mesher +-- picks it per voxel, so which art column lands at which world coordinate +-- is decided HERE and nowhere else -- and a face is read from outside, so +-- the art's own left-to-right runs with the viewer's, not with the world's: +-- facing east at a west wall, south is to your right (+z); facing west at +-- an east wall, north is (-z); facing south at a north wall, west is (-x). +-- Two of the three are mirrored against the axis, which is the same reason +-- `backMap` exists -- a wall seen from behind IS the drawing mirrored. +local DOOR = 16 + +local function sideDoors(at, sp, doors, W) + if not (doors and #doors > 0 and sp.door) then return at end + local art = sp.door + + -- The face's OUTER surface, walked in from the box edge until the wall + -- answers. A drawing inset from its own grid (B03's outer columns are + -- terrain, not building) stands its flank a column or two in, and a door + -- pinned to the box edge would hang in the air beside it. + local function faceX(from, step, off) + for k = 0, W - 1 do + local x = from + step * k + for y = 0, DOOR - 1 do + for z = off, off + DOOR - 1 do + if at(x, y, z) then return x end + end + end + end + return nil + end + + local list = {} + for _, d in ipairs(doors) do + local face = 0 -- north: the facade's own z origin + if d.side == "w" then face = faceX(0, 1, d.at) + elseif d.side == "e" then face = faceX(W - 1, -1, d.at) end + if face then + list[#list + 1] = { side = d.side, off = d.at, face = face } + end + end + if #list == 0 then return at end + + -- art column at (x, z) for door `e`, or nil when the voxel is not in it + local function column(e, x, z) + if e.side == "n" then + if (z == 0 or z == 1) and x >= e.off and x < e.off + DOOR then + return e.off + DOOR - 1 - x, z == 0 + end + elseif z >= e.off and z < e.off + DOOR then + if e.side == "w" then + if x == e.face then return z - e.off, true end + if x == e.face + 1 then return z - e.off, false end + else + if x == e.face then return e.off + DOOR - 1 - z, true end + if x == e.face - 1 then return e.off + DOOR - 1 - z, false end + end + end + return nil + end + + return function(x, y, z) + local v = at(x, y, z) + -- no wall here is the end of it: a door is hung ON the building, and + -- nothing about it may add geometry the drawing does not stand up + if v == nil or y >= DOOR then return v end + for _, e in ipairs(list) do + local c, outer = column(e, x, z) + if c then + -- art row 0 is the door's head, so the ground row is its last + local k = (DOOR - 1 - y) * DOOR + c + -- the art's own ring: wall, not door. The flank keeps its texel. + if art.context[k] then return v end + if art.black[k] then + -- the frame, flush with the wall; behind it the wall stands on + if outer then return art.base + k end + return v + end + -- and what the frame seals sinks: the face voxel goes, the one + -- behind it wears the art. (Written long: `outer and nil or i` + -- returns i for BOTH, nil being false to `and`.) + if outer then return nil end + return art.base + k + end + end + return v + end +end + -- The voxel model as a lookup: `at(x, y, z)` is the index of the sprite -- pixel that voxel wears, or nil. Build ORDER is expressed as lookup -- order -- roof first, so it overwrites the walls it intersects, and walls --- are trimmed to its underside so nothing pokes through the surface. -local function model(sp, pr, t) +-- are trimmed to its underside so nothing pokes through the surface. A +-- gate's side doors are hung on the finished lookup, last of all, because +-- they answer to the FACE rather than to any band of the drawing. +local function model(sp, pr, t, doors) if t.parts then return deskSetModel(sp, pr, t) end local W, H, D = sp.W, sp.H, pr.D local slab, roofRows = t.slab, t.roofRows @@ -1209,6 +1394,8 @@ local function model(sp, pr, t) return pr.interior[i] end + at = sideDoors(at, sp, doors, W) + return { at = at, W = W, ytop = ytop, zmin = ledge0 and -2 or 0, zmax = math.max(rz1, ledge0 and (D + 1) or 0) } @@ -1420,6 +1607,76 @@ local function matches(S, t, tx, ty) return true end +-- ------- which of a placement's faces a gate is entered by +-- +-- Read off the MAP, not authored: a gate entrance is a warp that lands in a +-- gate house, and the face is whichever one of this placement the warp cell +-- stands against. Thirty-odd doors fall out of two lines of geometry, and +-- none of them can drift out of step with a map edit the way a hand list +-- would. Three tests, each of them load-bearing: +-- +-- the destination is a GATE tileset -- what makes a building a gate house +-- rather than a house with a back door. GATE and FOREST_GATE both, so +-- the Viridian Forest pair count; the Safari rest houses are on GATE +-- too and are excluded by the next test, their warps being drawn doors +-- already. +-- the cell is not already a door tile -- a south entrance IS drawn, as a +-- doorway block in the facade, and lib/Structures.lua folds it up into +-- the front face. Adding a second one there would fight it. +-- the cell is WALKABLE -- the ROM gives an unreachable twin warp to +-- several gates (a fence cell beside the real opening on Route 7 west +-- and Route 16 east, a tree beside Route 6's), and a door on the wall +-- behind a fence is a door into nothing. The reachable cells are the +-- entrance, and two of them side by side are the gate's real two-cell +-- opening, which comes out as the double door it always was. +-- +-- The south face is skipped whether or not it is drawn: it is the one face +-- the drawing states in full, so anything it needs it already has. +local function sideDoorsAt(map, tileset, tx, ty, bw, bh) + local defs = _G.Game and Game.data and Game.data.maps + local warps = map.def and map.def.warps + if not (defs and warps and warps[1]) then return nil end + if not Buildings.sideDoorCell(tileset.id) then return nil end + + local out = nil + for _, w in ipairs(warps) do + local dest = defs[w.destMap] + if dest and dest.tileset and dest.tileset:find("GATE", 1, true) + and map:isWalkableCell(w.x, w.y) + and not map:isDoorTileCell(w.x, w.y) then + -- the cell in the model's own pixels: a cell is two tiles, a tile + -- eight pixels, and the placement's origin is (tx, ty) in tiles + local lx, lz = w.x * 16 - tx * 8, w.y * 16 - ty * 8 + local side = nil + if lz == -16 and lx >= 0 and lx < bw * 8 then side = "n" + elseif lx == -16 and lz >= 0 and lz < bh * 8 then side = "w" + elseif lx == bw * 8 and lz >= 0 and lz < bh * 8 then side = "e" end + if side then + out = out or {} + out[#out + 1] = { side = side, at = side == "n" and lx or lz } + end + end + end + if out then + table.sort(out, function(a, b) + if a.side ~= b.side then return a.side < b.side end + return a.at < b.at + end) + end + return out +end + +-- The model cache key's door half. A template's doors belong to the +-- PLACEMENT -- the same 6x4 block is the gate on four routes and the warps +-- sit at different rows of it on each -- so two placements of one drawing +-- are two models, and only placements that agree share one. +local function doorKey(doors) + if not doors then return "" end + local parts = {} + for i, d in ipairs(doors) do parts[i] = d.side .. d.at end + return "#" .. table.concat(parts, ",") +end + -- Find every placement of every template for this map's tileset, build one -- model per template, and stamp it. Returns nothing; the quads land in -- S.objectQuads and the tiles are claimed so the volume path never boxes a @@ -1440,6 +1697,9 @@ function Buildings.build(S, map, data, perRow) if type(t.tiles) == "table" and #t.tiles > 0 then local bh, bw = #t.tiles, #t.tiles[1] local first = t.tiles[1][1] + -- the model this placement stamps. Not hoisted out of the loops any + -- more: a template's doors belong to the placement, so two hits of + -- one drawing on the same map can be two models (see doorKey). local built = nil for ty = 0, th - bh do Budget.tick() @@ -1465,8 +1725,13 @@ function Buildings.build(S, map, data, perRow) end end if free and matches(S, t, tx, ty) then - if not built then - local key = tileset.id .. ":" .. index + do + -- keyed per PLACEMENT once a gate's doors are in play (see + -- doorKey): the drawing is shared, the openings are not + local doors = not t.claimOnly + and sideDoorsAt(map, tileset, tx, ty, bw, bh) + or nil + local key = tileset.id .. ":" .. index .. doorKey(doors) if not models[key] then if t.claimOnly then -- claim the cells, stamp nothing: the drawing here is @@ -1478,8 +1743,13 @@ function Buildings.build(S, map, data, perRow) else local sp = read(t, data, perRow, Buildings.frontOnly(tileset.id)) + if doors then + readDoor(sp, data, perRow, + Buildings.sideDoorCell(tileset.id)) + end local pr = measure(sp, t) - models[key] = emit(model(sp, pr, t), sp, atlasW, atlasH) + models[key] = emit(model(sp, pr, t, doors), sp, + atlasW, atlasH) end end built = models[key] diff --git a/lib/Structures.lua b/lib/Structures.lua index 2f6ee1b..814364d 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -2004,7 +2004,9 @@ 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 down = s.class == "stair_down_e" or s.class == "stair_down_w" + local north = s.class == "stair_down_n" + local down = north 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 local h = s.h or 16 @@ -2053,6 +2055,90 @@ local function stairCell(S, map, data, cx, cy, s) end end + -- A flight running INTO the map instead of across it. The drawing is + -- the same staircase seen head-on rather than from the side, and that + -- changes which axis of the art means what: a drawn ROW is a step here, + -- and -- because looking down a well is looking along its depth -- drawn + -- row IS depth row, 1:1 across the cell's 16. + -- + -- The Centers' steps state their own band table and it lands exactly: + -- 4 white rows, 1 black, 3 grey, 1 black, 3 checker, 4 black = 16. So + -- an even four-step division puts a black NOSING on the southmost row of + -- every band (15, 11, 7, 3) and leaves the rows behind it as that step's + -- tread. Nothing is authored but the RISE, which no head-on drawing can + -- state; the depths, the treads and the nosings are all measured. + -- + -- A nosing is drawn as one row because it is seen nearly edge-on, so + -- un-projected it has real height and no depth: its row lies flat as the + -- tread's front lip AND stands as the riser under it. That is the one + -- texel in the flight used twice, and using it twice is what a nosing is. + -- + -- The well's own walls come free as well: the drawing's first and last + -- 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. + -- + -- 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 + -- between two unrelated corners of the sheet. + if north then + local runD = 16 / STAIR_STEPS + local HALVES = { { 0.2, 7.9, 0, 8 }, { 8.1, 15.8, 8, 16 } } + 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 z0b, z1b = mz + a0, mz + a1 + + for _, H in ipairs(HALVES) do + local ax0, ax1, wx0, wx1 = H[1], H[2], mx + H[3], mx + H[4] + + -- the tread: the whole band, drawn row = depth row, so the nosing + -- 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) + + -- 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 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 + 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 + 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 } } + else -- east wall, faces W + c = { { px, yTop, z0b }, { px, yTop, z1b }, + { px, 0, z1b }, { px, 0, 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) + end + return + end + for i = 0, STAIR_STEPS - 1 do local sx0 = east and (i * runW) or (16 - (i + 1) * runW) local sx1 = sx0 + runW @@ -2152,6 +2238,7 @@ function Structures.buildStairs(S, map, x0, x1, y0, y1) -- box or floor it. A rising flight stands on the map's common -- floor; a stairwell IS the hole, so nothing is painted under it local down = s.class == "stair_down_e" or s.class == "stair_down_w" + or s.class == "stair_down_n" for dy = 0, 1 do for dx = 0, 1 do local tk = keyOf(cx * 2 + dx, cy * 2 + dy) diff --git a/lib/TileShape.lua b/lib/TileShape.lua index c4c372f..c0c19ce 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -125,6 +125,12 @@ 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 + stair_down_n = 16, } -- class -> how the mesher draws it (see the header). The last three are @@ -215,6 +221,7 @@ local ART = { stair_w = "stair", stair_down_e = "stair", stair_down_w = "stair", + stair_down_n = "stair", } local spec = nil -- the loaded data file, or false when absent diff --git a/lib/Voxel3D.lua b/lib/Voxel3D.lua index 5b3d7f9..3e690cd 100644 --- a/lib/Voxel3D.lua +++ b/lib/Voxel3D.lua @@ -503,12 +503,22 @@ local active = false -- which is exactly the old behaviour minus the reflections. local DEPTH_FORMATS = { "depth24", "depth24stencil8", "depth32f", "depth16" } +-- dpiscale = 1, for the same reason PixelCanvas pins it and for one more: +-- newCanvas otherwise takes the WINDOW's scale, and every canvas bound +-- together must agree on PIXEL dimensions. The colour canvas beside this one +-- comes from PixelCanvas at scale 1, so on any surface whose scale is not 1 +-- -- Android's density is routinely 2.625, and a retina Mac's is 2 -- this +-- one came back 2.625x larger and the pair would not bind. beginScene then +-- dropped the readable depth for the session (see below), depthReadable() +-- went false, and the water pass never ran at all: the reflections were +-- missing on every high-density display, with nothing in the log to say so, +-- because a canvas that will not BIND is not a canvas the driver refused. local function newDepth(w, h) if not (love.graphics and love.graphics.newCanvas) then return nil end local c = nil for _, format in ipairs(DEPTH_FORMATS) do local ok, made = pcall(love.graphics.newCanvas, w, h, - { format = format, readable = true }) + { format = format, readable = true, dpiscale = 1 }) if ok and made then c = made break end end if not c then return nil end @@ -1344,7 +1354,10 @@ end function Voxel3D.beginWater(paint) if not (active and canvas and held and held.depth) then return nil end if not held.mirror then - local ok, c = pcall(love.graphics.newCanvas, held.w, held.h) + -- through PixelCanvas, because this one is bound WITH held.depth a few + -- lines down and the two must agree on pixel dimensions -- the same + -- scale trap newDepth documents + local ok, c = PixelCanvas.new(held.w, held.h) if not (ok and c) then return nil end pcall(c.setFilter, c, "nearest", "nearest") pcall(c.setWrap, c, "clamp", "clamp")