fix victory road statues

This commit is contained in:
DramaticShape
2026-08-09 00:01:47 -04:00
parent 47369c24fe
commit 2af88d07cf
7 changed files with 501 additions and 44 deletions
+49
View File
@@ -167,6 +167,55 @@
measurement of the geometry can recover. Only the top face is measurement of the geometry can recover. Only the top face is
redirected; the poster still faces the room. redirected; the poster still faces the room.
Five rooms take it. `HOUSE`, `POKECENTER`, `REDS_HOUSE_1` and
`REDS_HOUSE_2` name one course for every wall in the atlas -- each of
those dresses one kind of room, and a list keyed by the decorated tiles
would need extending every time a map hung something new on the same
wall. `LOBBY` names the tiles instead (`{ [40] = 93, [56] = 93 }`): the
Rocket lift's car doors cap with the cabin frame, and the department
store, the Game Corner, Silph's floors and the roof -- all on that one
atlas -- keep exactly the tops they had. The doors are also the reason
the cap is applied in the mesher's DETECTED-run branch as well as its
pinned one; Structures finds them rather than a pin naming them.
- **Lance's room is furnished with the badge gyms' bird statue.** It is
the gyms' statue tile for tile on the DOJO atlas -- one cell of figure
($02/$38/$12/$13) over one cell of plinth ($22/$23/$32/$33) -- and left
derived the pair merged into one 32px volume wearing the statue folded
onto its face. The extruded picture, the same failure the gyms' statues
and the Plateau's avenue had, and it takes the same answer: the plinth a
solid 16px block, the bird a per-pixel cutout 5 voxels deep riding its
top face. Every placement of those eight tiles in the game is a statue
-- 18 in LANCES_ROOM and 2 in FIGHTING_DOJO -- and Oak's Lab, the third
map on the atlas, places none of them.
- **A wall cut into a terrace inherits terrace, never the statue standing
on it.** `bookcase_backfill = "above"` hands a collapsed rank's vacated
rows the cell above the run, so the League's gate walls have more
hillside behind them rather than a trench. Indigo Plateau's avenue
statues stand directly on the pilasters that collapse that way, so what
every one of them inherited was the BIRD: the figure's shape and art
copied onto two more rows down the shaft, and each statue came out two
deep behind itself. Only bodies backfill now -- flat, top and upright.
A per-pixel standee above (a statue, a sign, a bush) is an object
standing ON the terrace rather than terrace, so the row has nothing to
inherit and takes the default synthesized ground.
- **...and a statue on a collapsed pilaster stands ON it.** The duplicate
above was masking a second fault. A standee finds its support by reading
the cell below its own drawing, and the bookcase collapse MOVES the box
it finds: the whole four-row pilaster walks onto its southmost cell,
which on the Plateau is a full cell south of where the test looked. So
the bird was lifted to the right HEIGHT and left standing over open
ground with its pillar behind it -- invisible while the vacated rows
were being filled with copies of the bird itself, obvious the moment
they were not. Every row of a collapsed rank now records the row its box
actually stands on (`S.bookcaseBox`), and a standee supported by one is
placed there instead of at its drawn position. Supports that do not move
-- the gyms' plinths, furniture, `building` claims -- are unaffected.
The plinth keeps its elevation and the statue extends exactly one cell
above it.
## 1.8.0 ## 1.8.0
### Added ### Added
+63 -13
View File
@@ -132,15 +132,23 @@
-- measured rather than drawn, by flooding the panel tile's own stripe out -- measured rather than drawn, by flooding the panel tile's own stripe out
-- from behind them; see CLUB below. -- from behind them; see CLUB below.
-- --
-- And `wall_top` (not a class either): the tile every `wall` cell's TOP -- And `wall_top` (not a class either): what a `wall` cell's TOP face
-- face wears, whatever the cell itself draws. An interior wall band is -- wears, whatever the cell itself draws. An interior wall band is 16px of
-- 16px of art folded upright, and a fully folded run has no drawn row left -- art folded upright, and a fully folded run has no drawn row left over to
-- over to lay flat on top -- so the top repeated the FACE, and the town-map -- lay flat on top -- so the top repeated the FACE, and the town-map poster
-- poster and the window of a house came out lying across the top of the -- and the window of a house came out lying across the top of the wall as
-- wall as well as hanging on it. What is really up there is the wall's own -- 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 -- 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 -- neighbours draw. The face is untouched -- the poster still faces the
-- it. The face is untouched -- the poster still faces the room. -- room. Two forms:
--
-- wall_top = <id> every wall in the tileset caps with this,
-- for an atlas that dresses one kind of room
-- (the houses, the Centers, Red's floors)
-- wall_top = { [tile] = id } only the named tiles are redirected, for
-- an atlas that dresses several (LOBBY is
-- the department store, the Game Corner,
-- Silph, the roof AND the Rocket lift)
-- --
-- Whole BUILDINGS are not tile pins -- one drawing packs a roof seen from -- 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, -- above, a facade seen face-on and sloped ends as diagonal silhouettes,
@@ -771,12 +779,38 @@ return {
signpost = { 33, 34, 49, 50 }, signpost = { 33, 34, 49, 50 },
}, },
-- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's -- Oak's Lab, the Fighting Dojo and Lance's room (one tileset). The
-- room, which use none of these tiles). The free-standing shelf -- lab's free-standing shelf ranks: book rows and base pinned; the
-- ranks: book rows and base pinned; the shared trim tiles above -- shared trim tiles above (41/42, also the lab tables' corners) are
-- (41/42, also the lab tables' corners) are adopted as caps by the -- adopted as caps by the bookcase builder rather than pinned. The
-- bookcase builder rather than pinned. -- other two rooms are furnished with one thing between them -- the
-- BIRD STATUE, and it is the badge gyms' statue exactly.
DOJO = { DOJO = {
-- THE STATUES. The same drawing as GYM's, tile for tile on this
-- atlas: one cell of figure ($02/$38/$12/$13) over one cell of
-- plinth ($22/$23/$32/$33), so it takes the same treatment -- the
-- plinth a SOLID 16px block, the figure a per-pixel cutout 5 voxels
-- deep (the thin `prop` pool) riding the plinth's top face through
-- the authored-box support rule and collapsing to the plinth's
-- single cell of footprint.
--
-- Left derived the pair merged into ONE 32px volume wearing the
-- statue art folded onto its face -- the extruded picture, the same
-- failure the gyms' statues and the Plateau's avenue had.
--
-- Every placement of these eight tiles in the game is a statue:
-- blocks 49/50/114/115 pack figure over plinth in one 2x2-cell
-- block, and they are placed 18 times in LANCES_ROOM (the pairs
-- lining his aisle, and the two flanking his dais at cells (6,12)
-- and (7,12) over (6,13)/(7,13)) and twice in FIGHTING_DOJO. Oak's
-- Lab, the third map on this atlas, places none of them -- and the
-- four figure-only and plinth-only blocks are never placed at all.
wall = { 34, 35, 50, 51 },
prop = { 2, 18, 19, 56 },
-- and each stands on the room's main floor ($11) rather than on
-- whatever its neighbours vote -- the gyms' rule, for the gyms'
-- reason: a statue against a wall would otherwise take the wall.
prop_ground = { [2] = 17, [18] = 17, [19] = 17, [56] = 17 },
bookcase = { 13, 14, 29, 30 }, bookcase = { 13, 14, 29, 30 },
-- the lab tables (the starter-ball display and the north tables): -- the lab tables (the starter-ball display and the north tables):
-- 41/42 are also the shelf trim the bookcase builder adopts as -- 41/42 are also the shelf trim the bookcase builder adopts as
@@ -800,6 +834,11 @@ return {
REDS_HOUSE_2 = { REDS_HOUSE_2 = {
-- the wall band with its windows stays one 16px face -- the wall band with its windows stays one 16px face
wall = { 0, 36, 37, 52, 53 }, wall = { 0, 36, 37, 52, 53 },
-- and caps with the blank course. The windows of Red's 2F sit at
-- cells (5,0) and (7,0); without this the panes came out lying
-- across the top of the wall as well as glazing its face, where
-- (6,0) between them draws the plain panel that belongs up there
wall_top = 0,
-- the bed: a mattress drawn from above, half a block high -- the bed: a mattress drawn from above, half a block high
bed = { 45, 46, 47, 61, 62, 63 }, bed = { 45, 46, 47, 61, 62, 63 },
-- stools: a seat-high box, seat art on top, legs on the front -- stools: a seat-high box, seat art on top, legs on the front
@@ -838,6 +877,9 @@ return {
-- table rides these heights, not the 8/12px class defaults -- table rides these heights, not the 8/12px class defaults
heights = { stool = 5, table = 6 }, heights = { stool = 5, table = 6 },
wall = { 0, 36, 37, 52, 53 }, wall = { 0, 36, 37, 52, 53 },
-- the same blank course caps 1F, whose windows are cells (3,0),
-- (5,0) and (7,0) -- (6,0) between the last two is the panel
wall_top = 0,
stool = { 2, 3, 18, 19 }, stool = { 2, 3, 18, 19 },
-- the dining table (38-44/58-60); its top row also caps the -- the dining table (38-44/58-60); its top row also caps the
-- bookcases below -- bookcases below
@@ -1615,6 +1657,14 @@ return {
wall = { 1, 2, 3, 6, 18, 19, 22, 33, 46, 47, wall = { 1, 2, 3, 6, 18, 19, 22, 33, 46, 47,
62, 63, 68, 70, 71, 72, 73, 75, 76, 77, 78, 79, 84, 62, 63, 68, 70, 71, 72, 73, 75, 76, 77, 78, 79, 84,
88, 89, 91, 92, 93 }, 88, 89, 91, 92, 93 },
-- The Rocket lift's CAR DOORS (40 over 56, cells (2,1) and (3,1) of
-- ROCKET_HIDEOUT_ELEVATOR) cap with the cabin frame's lower course
-- -- the tile cell (2,0) draws beneath its own top band, and what
-- every other column of that wall already caps with. Keyed by tile
-- rather than blanket, unlike the houses: this one atlas dresses the
-- department store, the Game Corner, Silph's floors and the roof
-- too, and none of those wall tops is a lift frame.
wall_top = { [40] = 93, [56] = 93 },
-- 3F's television sets ($0E/$0F/$1E/$1F): the one drawing in this -- 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 -- 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 -- cabinet, a bezel and a lit screen, drawn face-on -- and the same
+14 -7
View File
@@ -304,10 +304,15 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
-- The capping course an interior wall wears on its TOP face (see -- The capping course an interior wall wears on its TOP face (see
-- TileShape.wallTop). A wall band folds entirely onto its own face, so -- 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 -- 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 -- town-map poster and window, a Center's pokeball poster and the Rocket
-- lying across the top of the wall as well as hanging on it. Only the top -- lift's doors came out lying across the top of the wall as well as
-- is redirected: the face still draws what the map draws. -- standing in it. Only the top is redirected: the face still draws what
-- the map draws.
local wallTop = TileShape.wallTop(tileset.id) local wallTop = TileShape.wallTop(tileset.id)
local function capOf(s, tile)
if not (wallTop and s.class == "wall") then return nil end
return wallTop(tile)
end
-- one atlas-rect UV, optionally cropped to art rows [vTop, vBot] of 8 -- one atlas-rect UV, optionally cropped to art rows [vTop, vBot] of 8
local function uvRect(tile, vTop, vBot) local function uvRect(tile, vTop, vBot)
@@ -594,6 +599,11 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
{ { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, 0.95) { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, 0.95)
elseif run then elseif run then
local topTile = map:tileAt(tx, ChunkMesher.flatTopRow(run, ty)) local topTile = map:tileAt(tx, ChunkMesher.flatTopRow(run, ty))
-- a DETECTED wall volume caps the same way a pinned one does:
-- the Rocket lift's cabin doors are found rather than pinned,
-- and their drawing lay across the top of the wall they are set
-- into (see wallTop)
topTile = capOf(s, tile) or topTile
topQuad(x0, z0, h, topTile, VOLUME_TOP_SHADE) topQuad(x0, z0, h, topTile, VOLUME_TOP_SHADE)
else else
local topTile = tile local topTile = tile
@@ -657,10 +667,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
row = (above and above.authored and above.art == "upright") row = (above and above.authored and above.art == "upright")
and (north - 1) or north and (north - 1) or north
end end
topTile = S.tileAt[keyOf(tx, row)] topTile = capOf(s, tile) or 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 end
-- water's surface, and only water's: the recessed sheet itself, -- water's surface, and only water's: the recessed sheet itself,
-- never the ground's shoreline bands around it. A cell an object -- never the ground's shoreline bands around it. A cell an object
+45 -5
View File
@@ -227,7 +227,11 @@ function Structures.forMap(map)
S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def), S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def),
hideBareRing = hullRingOnly or nil, hideBareRing = hullRingOnly or nil,
runs = {}, skip = {}, ground = {}, doorFold = {}, objectQuads = {}, runs = {}, skip = {}, ground = {}, doorFold = {}, objectQuads = {},
grassQuads = {}, flowerQuads = {}, roundStamps = {}, figures = {} } grassQuads = {}, flowerQuads = {}, roundStamps = {}, figures = {},
-- tile key -> the row a collapsed bookcase rank's box actually
-- stands on, so a standee supported by one lands on it rather than
-- where the drawing put it (see buildBookcases)
bookcaseBox = {} }
Buildings.build(S, map, pixels(tileset), perRow) Buildings.build(S, map, pixels(tileset), perRow)
-- Fold doors into their buildings. A door cell is WALKABLE (the player -- Fold doors into their buildings. A door cell is WALKABLE (the player
@@ -1882,6 +1886,12 @@ local function bookcaseRank(S, map, perRow, run, i, j, k, pane, srcU, srcV,
end end
end end
-- The arts a `bookcase_backfill = "above"` row may inherit: terrain and
-- solid bodies only (see the note at the backfill itself). Everything
-- absent here -- billboard, post, cylinder, grass, flower -- is a per-pixel
-- object STANDING on terrain rather than terrain.
local BACKFILL_ART = { flat = true, top = true, upright = true }
function Structures.buildBookcases(S, map, x0, x1, y0, y1, data, perRow) function Structures.buildBookcases(S, map, x0, x1, y0, y1, data, perRow)
perRow = perRow or map.tileset.tilesPerRow or 16 perRow = perRow or map.tileset.tilesPerRow or 16
-- What to do with the rows a rank VACATES (see TileShape.bookcaseBackfill). -- What to do with the rows a rank VACATES (see TileShape.bookcaseBackfill).
@@ -1928,11 +1938,32 @@ function Structures.buildBookcases(S, map, x0, x1, y0, y1, data, perRow)
-- shelf standing in a room. `bookcase_backfill = "above"` hands it -- shelf standing in a room. `bookcase_backfill = "above"` hands it
-- the cell above the run instead, shape and art, so a wall cut into -- the cell above the run instead, shape and art, so a wall cut into
-- a terrace has more terrace behind it rather than a trench. -- a terrace has more terrace behind it rather than a trench.
--
-- Only BODY above backfills: a vacated row wants more of the
-- terrace the wall is cut into, and the terrace is whatever lies
-- flat, tops out or stands as a solid face. A per-pixel STANDEE
-- above -- a statue, a sign, a bush -- is an object standing ON
-- that terrace, and copying it northward builds a second and a
-- third of it: Indigo Plateau's avenue statues sit directly on
-- the pilasters that collapse here, so every bird came out
-- duplicated twice down the shaft behind itself. A standee
-- above means the row has no terrace to inherit, so it takes the
-- default and is painted with synthesized ground.
local covered = math.min(2, front - top + 1) local covered = math.min(2, front - top + 1)
local srcK = keyOf(tx, top - 1) local srcK = keyOf(tx, top - 1)
local src = backfill == "above" and S.shapeAt[srcK] or nil local src = backfill == "above" and S.shapeAt[srcK] or nil
if src and not BACKFILL_ART[src.art] then src = nil end
-- Where the box ACTUALLY ends up, remembered for every row of the
-- rank: the collapse walks the whole drawn run onto its southmost
-- cell, so anything that has to stand ON the box has to be told
-- where the box went. A statue keys off the cell below its own
-- drawing, which is the run's NORTH end -- two rows away from the
-- box on a two-cell pilaster, which is exactly the distance the
-- Plateau's birds floated by.
local boxTop = front - covered + 1
for cy = top, front do for cy = top, front do
local tk = keyOf(tx, cy) local tk = keyOf(tx, cy)
S.bookcaseBox[tk] = boxTop
if src and cy <= front - covered then if src and cy <= front - covered then
S.shapeAt[tk] = src S.shapeAt[tk] = src
S.tileAt[tk] = S.tileAt[srcK] S.tileAt[tk] = S.tileAt[srcK]
@@ -2840,9 +2871,10 @@ function Structures.buildObject(S, map, region, cluster,
-- Town is where it showed: pinning the cliff's slope chain gave the -- Town is where it showed: pinning the cliff's slope chain gave the
-- posts along the cliff edge an authored 16px box to their south, and -- posts along the cliff edge an authored 16px box to their south, and
-- they were hoisted to stand on the clifftop instead of the path. -- they were hoisted to stand on the clifftop instead of the path.
local baseY, support = 0, nil local baseY, support, supportRow = 0, nil, nil
if force and force ~= "opaque" then if force and force ~= "opaque" then
local bs = S.shapeAt[keyOf(cluster.minX, cluster.maxY + 1)] local belowK = keyOf(cluster.minX, cluster.maxY + 1)
local bs = S.shapeAt[belowK]
local blocked = not map:isWalkableCell(math.floor(cluster.minX / 2), local blocked = not map:isWalkableCell(math.floor(cluster.minX / 2),
math.floor(cluster.maxY / 2)) math.floor(cluster.maxY / 2))
-- `bookcase` supports as well as `upright`. A prop drawn above an -- `bookcase` supports as well as `upright`. A prop drawn above an
@@ -2859,6 +2891,13 @@ function Structures.buildObject(S, map, region, cluster,
and (bs.art == "upright" or bs.art == "bookcase" and (bs.art == "upright" or bs.art == "bookcase"
or bs.class == "building") then or bs.class == "building") then
baseY, support = bs.h, bs baseY, support = bs.h, bs
-- A bookcase support has MOVED: the collapse walks the whole drawn
-- run onto its southmost cell, and the cell tested above is the run's
-- north end. On the Plateau's two-cell pilasters that is a full cell
-- away, and the bird stood at the right HEIGHT over open ground with
-- its pillar behind it -- floating. Stand it on the box's own north
-- row instead of one row south of its drawing.
supportRow = S.bookcaseBox[belowK]
end end
end end
local atlasW = map.tileset.imageWidth or 128 local atlasW = map.tileset.imageWidth or 128
@@ -2910,8 +2949,9 @@ function Structures.buildObject(S, map, region, cluster,
end end
end end
for _, c in ipairs(comps) do for _, c in ipairs(comps) do
c.z0 = cluster.minY * 8 + math.floor(c.lowY / 8) * 8 c.z0 = supportRow and (supportRow * 8 + (8 - depth) / 2)
+ (support and 8 or 0) + (8 - depth) / 2 or (cluster.minY * 8 + math.floor(c.lowY / 8) * 8
+ (support and 8 or 0) + (8 - depth) / 2)
c.z1 = c.z0 + depth c.z1 = c.z0 + depth
end end
+31 -9
View File
@@ -764,8 +764,9 @@ function TileShape.bookcaseRelief(tilesetId)
return not (entry and entry.bookcase_relief == false) return not (entry and entry.bookcase_relief == false)
end end
--- What every `wall` cell's TOP face wears in this tileset (a tileset --- What a `wall` cell's TOP face wears in this tileset (a tileset entry's
--- entry's wall_top). Returns a tile id, or nil to leave the top alone. --- wall_top). Returns a function tile -> cap tile id (nil for "leave it
--- alone"), or nil when the tileset says nothing at all.
--- ---
--- A wall band is 16px of art folded upright over a run two drawn rows --- 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 --- deep, so it folds ENTIRELY onto its face and has no row left to lay
@@ -773,17 +774,38 @@ end
--- poster and window came out lying across the top of the wall as well as --- 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 --- 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 --- 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 --- is the whole fix, because "plain" is a fact about the drawing that
--- drawing that nothing in the geometry can measure. --- nothing in the geometry can measure.
--- ---
--- Per tileset rather than per tile: one room caps with one course, and a --- Two forms, because tilesets differ in how far one answer reaches:
--- list keyed by the decorated tiles would have to be extended every time ---
--- a map hung something new on a wall already covered. --- wall_top = <id> EVERY wall cell caps with this course.
--- Right where one atlas dresses one kind of
--- room -- the town house, the Centers, Red's
--- two floors all cap with their own blank
--- panel, and a list keyed by the decorated
--- tiles would need extending every time a
--- map hung something new on the same wall.
--- wall_top = { [tile] = id } only these tiles are redirected. Right
--- where one atlas dresses several rooms:
--- LOBBY is the department store, the Game
--- Corner, Silph's floors, the roof AND the
--- Rocket lift, and the lift's cabin frame is
--- not what a shop wall caps with.
function TileShape.wallTop(tilesetId) function TileShape.wallTop(tilesetId)
local s = load() local s = load()
local entry = s and s.tilesets and s.tilesets[tilesetId] local entry = s and s.tilesets and s.tilesets[tilesetId]
local tile = entry and entry.wall_top local spec = entry and entry.wall_top
return type(tile) == "number" and tile or nil if type(spec) == "number" then
return function() return spec end
end
if type(spec) == "table" then
return function(tile)
local cap = spec[tile]
return type(cap) == "number" and cap or nil
end
end
return nil
end end
-- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset -- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
+162 -10
View File
@@ -7020,10 +7020,21 @@ end)()
local Struct = run.loader.exports.DRAMATIC_SHAPE.lib.require("Structures") local Struct = run.loader.exports.DRAMATIC_SHAPE.lib.require("Structures")
local Shape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape") local Shape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape")
T.eq(Shape.wallTop("HOUSE"), 0, -- the blanket form: whatever a wall cell draws, it caps with the one
-- course the atlas's rooms are panelled in
T.eq(Shape.wallTop("HOUSE")(45), 0,
"the town house caps its walls with the blank course") "the town house caps its walls with the blank course")
T.eq(Shape.wallTop("POKECENTER"), 40, T.eq(Shape.wallTop("POKECENTER")(2), 40,
"and a Center with its striped panel -- the tile cell (9,0) draws") "and a Center with its striped panel -- the tile cell (9,0) draws")
T.eq(Shape.wallTop("REDS_HOUSE_2")(36), 0,
"Red's bedroom with its own blank panel, over the window")
T.eq(Shape.wallTop("REDS_HOUSE_1")(36), 0, "and so does the floor below")
-- the keyed form, for an atlas that dresses more than one kind of room
T.eq(Shape.wallTop("LOBBY")(40), 93,
"the Rocket lift's car door caps with the cabin frame")
T.eq(Shape.wallTop("LOBBY")(1), nil,
"but the department store's own panel is left exactly as it was -- "
.. "one atlas, several rooms, and only the lift is a lift")
T.eq(Shape.wallTop("DS_NO_SUCH_TILESET"), nil, T.eq(Shape.wallTop("DS_NO_SUCH_TILESET"), nil,
"a tileset that says nothing keeps the top it always had") "a tileset that says nothing keeps the top it always had")
@@ -7061,7 +7072,11 @@ end)()
local ax = math.floor(math.min(a[4], b[4], c[4], d[4]) * 128 + 0.5) 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 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) 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)) local ty = math.floor(math.min(a[3], b[3], c[3], d[3]) / 8)
-- keyed by the TILE ROW too: a wall band is two drawn rows deep
-- and they can wear different art
out[ty * 100 + tx] = out[ty * 100 + tx]
or (math.floor(ay / 8) * 16 + math.floor(ax / 8))
end end
end end
return out, verts return out, verts
@@ -7073,9 +7088,12 @@ end)()
{ 0, 0, 45, 46, 36, 36, 0, 0 }, { 0, 0, 45, 46, 36, 36, 0, 0 },
{ 0, 0, 61, 62, 52, 52, 0, 0 }, { 0, 0, 61, 62, 52, 52, 0, 0 },
}) })
for tx = 0, 7 do for ty = 0, 1 do
T.eq(houseTops[tx], 0, for tx = 0, 7 do
("column %d of the house's back wall caps with the blank course"):format(tx)) T.eq(houseTops[ty * 100 + tx], 0,
("(%d,%d) of the house's back wall caps with the blank course")
:format(tx, ty))
end
end end
-- and the FACE is untouched: the poster still hangs in the room, which is -- and the FACE is untouched: the poster still hangs in the room, which is
@@ -7095,11 +7113,145 @@ end)()
{ 40, 40, 40, 2, 3, 40, 40, 40 }, { 40, 40, 40, 2, 3, 40, 40, 40 },
{ 40, 40, 40, 18, 19, 40, 40, 40 }, { 40, 40, 40, 18, 19, 40, 40, 40 },
}) })
for tx = 0, 7 do for ty = 0, 1 do
T.eq(pcTops[tx], 40, for tx = 0, 7 do
("column %d of the Center's back wall caps with the striped panel") T.eq(pcTops[ty * 100 + tx], 40,
:format(tx)) ("(%d,%d) of the Center's back wall caps with the striped panel")
:format(tx, ty))
end
end end
-- ------- the Rocket lift's car doors
--
-- Detected rather than pinned -- the doors are a volume Structures finds
-- -- so the cap has to reach the RUN branch too, not only the pinned one.
local liftTops = topTiles("LOBBY", {
{ 92, 92, 92, 92, 92, 92, 92, 92 },
{ 93, 93, 93, 93, 93, 93, 93, 93 },
{ 1, 1, 40, 40, 40, 40, 1, 1 },
{ 33, 33, 56, 56, 56, 56, 33, 33 },
})
-- rows 2 and 3 are the doors' own band; the doors span columns 2-5
for ty = 2, 3 do
for tx = 2, 5 do
T.eq(liftTops[ty * 100 + tx], 93,
("(%d,%d) of the lift's door caps with the cabin frame"):format(tx, ty))
end
end
-- and the panelling either side of them is left alone, which is what the
-- keyed form buys: the same atlas panels the department store
T.eq(liftTops[2 * 100], 1, "the blank course beside the door is untouched")
T.eq(liftTops[3 * 100], 33, "and so is its skirting")
end)()
-- ------- Lance's room is furnished with the badge gyms' statue
--
-- The same drawing on a different atlas: one cell of bird over one cell of
-- plinth. Left derived the pair merged into one 32px volume wearing the
-- statue folded onto its face -- the extruded picture. The gyms' reading
-- (solid plinth, per-pixel standee on top) is the right one here too, and
-- these eight tiles draw nothing else anywhere in the game.
;(function()
local Shape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape")
local shapes = Shape.forMap({
tileset = { id = "DOJO", tilesPerRow = 16,
imageWidth = 128, imageHeight = 48, grassTile = -1 },
walkable = { [17] = true },
})
for _, t in ipairs({ 2, 18, 19, 56 }) do
T.eq(shapes[t].class, "prop",
("the statue's figure tile %d stands as a per-pixel cutout"):format(t))
T.eq(shapes[t].authored, true, "and it is an authored answer, not derived")
end
for _, t in ipairs({ 34, 35, 50, 51 }) do
T.eq(shapes[t].class, "wall",
("the plinth tile %d stays a solid 16px block"):format(t))
T.eq(shapes[t].authored, true,
"authored, which is what the standee's support rule tests -- and "
.. "what keeps tile 50 ($32) out of the water-fallback trap")
end
T.eq(shapes[13].class, "bookcase",
"and Oak's shelf ranks on the same atlas are untouched")
end)()
-- ------- a wall cut into a terrace inherits TERRACE, never a statue
--
-- `bookcase_backfill = "above"` hands a vacated row the cell above the run,
-- so masonry set into a hillside has more hillside behind it rather than a
-- trench. Indigo Plateau's avenue statues stand directly on the pilasters
-- that collapse this way, so what every one of them inherited was the BIRD:
-- the figure's shape and art copied onto two more rows down the shaft, and
-- the statue came out two deep behind itself. A standee above is an object
-- standing ON the terrace, not terrace -- so there is nothing to inherit.
;(function()
local Struct = run.loader.exports.DRAMATIC_SHAPE.lib.require("Structures")
-- the real west-edge statue of INDIGO_PLATEAU, tile for tile: the bird
-- (37/38 over 40/41) on the pilaster (cap 21/22, shaft 5/6 twice, foot
-- 21/22), standing on the plateau's paving (35)
local rows = {
{ 46, 47 }, { 46, 47 },
{ 37, 38 }, { 40, 41 },
{ 21, 22 }, { 5, 6 },
{ 5, 6 }, { 21, 22 },
{ 35, 35 }, { 35, 35 },
{ 35, 35 }, { 35, 35 },
}
local map = {
id = "DS_TEST_PLATEAU_STATUE",
tileset = { id = "PLATEAU", image = "gfx/tilesets/ds_test.png",
tilesPerRow = 16, imageWidth = 128, imageHeight = 40,
blocks = {}, grassTile = -1 },
def = { width = 1, height = 3, tileset = "PLATEAU" },
walkable = { [35] = true },
waterTiles = {},
doorTiles = {},
tileAt = function(_, tx, ty)
local r = rows[math.max(1, math.min(#rows, ty + 1))]
return r[(tx % 2) + 1]
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) == 35
end,
isDoorTileCell = function() return false end,
isGrassCell = function() return false end,
inBounds = function(_, cx, cy)
return cx >= 0 and cy >= 0 and cx < 2 and cy < 6
end,
}
Struct.invalidate(map.id)
local S = Struct.forMap(map)
local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end
-- the bird stands, once, where it is drawn
for _, ty in ipairs({ 2, 3 }) do
T.eq(S.shapeAt[keyOf(0, ty)].art, "billboard",
"the statue's own rows carry the standee")
end
-- and the pilaster rows it vacates carry NOTHING -- not a second bird
for _, ty in ipairs({ 4, 5, 6, 7 }) do
local s = S.shapeAt[keyOf(0, ty)]
T.check(s == nil or s.art ~= "billboard",
("pilaster row %d did not inherit the statue standing on it"):format(ty))
end
-- ...and the bird stands on the box rather than where it is DRAWN. The
-- collapse walks the whole four-row pilaster onto its southmost cell, so
-- a standee that trusted its own drawn position ended up a full cell
-- north of the pillar holding it up -- at the right height, over open
-- ground. Every row of the rank records where the box actually went.
for _, ty in ipairs({ 4, 5, 6, 7 }) do
T.eq(S.bookcaseBox[keyOf(0, ty)], 6,
("rank row %d knows the box stands on row 6"):format(ty))
end
local zlo = math.huge
for _, q in ipairs(S.objectQuads) do
for i = 1, 4 do zlo = math.min(zlo, q[i][3]) end
end
T.eq(math.floor(zlo / 8), 6,
"and the statue's own geometry starts on that row -- standing on the "
.. "pillar, not hanging in the air two cells in front of it")
end)() end)()
Pipelines.reset() Pipelines.reset()
+137
View File
@@ -0,0 +1,137 @@
-- Scratch driver: one shot of each fixture the wall-top / statue pass
-- touched, so the change can be looked at rather than reasoned about.
--
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/voxel_fix_shots.lua \
-- SHOT_DIR=mods/DramaticShapeVoxelMod/.claude/voxel_fix AB_TAG=. lovec.exe .
--
-- SHOT_DIR is relative to the PROJECT ROOT, which is where lovec runs from --
-- the mod's own .claude/ is where these belong, so it has to be spelt out.
--
-- Every scene stands the player on the nearest WALKABLE cell to the vantage
-- named below (teleporting into a wall puts the close cameras inside the
-- geometry), faces the fixture, and shoots at voxel rung 3 -- the higher
-- top-down camera, which is the one that shows what a wall wears on TOP.
return function(game)
local U = dofile("tests/drivers/util.lua")
local Pipelines = require("src.render.Pipelines")
local ROOT = (os.getenv("SHOT_DIR")
or "mods/DramaticShapeVoxelMod/.claude/voxel_fix")
.. "/" .. (os.getenv("AB_TAG") or ".")
local handle = game.mods.exports["DRAMATIC_SHAPE"]
if not (handle and handle.lib) then
print("[voxfix] DRAMATIC_SHAPE mod not loaded")
return
end
local V = handle.lib
local DayNight = V.require("DayNight")
local ChunkMesher = V.require("ChunkMesher")
local Voxel = V.require("VoxelState")
local TileShape = V.require("TileShape")
-- prove the game is reading THIS tree, not a stale installed copy
local ht = TileShape.wallTop and TileShape.wallTop("HOUSE")
print(("[voxfix] wallTop present=%s HOUSE(45)=%s")
:format(tostring(TileShape.wallTop ~= nil),
tostring(ht and ht(45))))
require("src.world.OverworldController").rollEncounter = function() return nil end
local TileRenderer = require("src.render.TileRenderer")
TileRenderer.tick = function() end
TileRenderer.animFrame = function() return 0 end
DayNight.setting:sync("day")
pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
local Zoom = require("src.render.Zoom")
pcall(function()
game.save.options.zoom = 1
Zoom.applyOptions(game.save.options)
end)
local function settle()
for _ = 1, 900 do
if ChunkMesher.pending() == 0 then break end
U.wait(1)
end
for _ = 1, 300 do
if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
U.wait(1)
end
U.wait(40)
end
-- the nearest walkable cell to the vantage, searched in rings -- a
-- vantage picked off a map dump is often the fixture itself
local function place(x, y, face)
local m = game.overworld.map
for r = 0, 8 do
for dy = -r, r do
for dx = -r, r do
if math.max(math.abs(dx), math.abs(dy)) == r then
local px, py = x + dx, y + dy
if m:inBounds(px, py) and m:isWalkableCell(px, py) then
game.overworld.player.cellX = px
game.overworld.player.cellY = py
game.overworld.player.facing = face
return px, py
end
end
end
end
end
return x, y
end
local SCENES = {
{ map = "BLUES_HOUSE", x = 4, y = 3, face = "up",
label = "house_wall_top" },
{ map = "VIRIDIAN_POKECENTER", x = 4, y = 3, face = "up",
label = "pokecenter_wall_top" },
{ map = "ROCKET_HIDEOUT_ELEVATOR", x = 2, y = 3, face = "up",
label = "rocket_lift_wall_top" },
{ map = "REDS_HOUSE_2F", x = 6, y = 3, face = "up",
label = "reds_2f_wall_top" },
{ map = "REDS_HOUSE_1F", x = 5, y = 3, face = "up",
label = "reds_1f_wall_top" },
{ map = "LANCES_ROOM", x = 6, y = 15, face = "up",
label = "lances_room_statues" },
{ map = "INDIGO_PLATEAU", x = 2, y = 5, face = "up",
label = "indigo_plateau_statue" },
-- the west-edge statue itself, close enough to count the birds on it
{ map = "INDIGO_PLATEAU", x = 1, y = 4, face = "up",
label = "indigo_plateau_statue_close", rung = 5 },
{ map = "INDIGO_PLATEAU", x = 2, y = 4, face = "up",
label = "indigo_plateau_statue_near" },
}
local only = os.getenv("VF_ONLY")
local shots = 0
for _, s in ipairs(SCENES) do
if not (only and only ~= "" and not only:find(s.label, 1, true)) then
local ok, err = pcall(function()
U.teleport(game, s.map, s.x, s.y, s.face)
local px, py = place(s.x, s.y, s.face)
Pipelines.setLevel("voxel", s.rung or 3)
Pipelines.setLevel("tiltshift", 0)
settle()
local path = ("%s/%s.png"):format(ROOT, s.label)
game.capturePath = path
U.wait(8)
local f = io.open(path, "rb")
if f then
f:close()
shots = shots + 1
print(("[voxfix] %s -> %s (stood %d,%d)"):format(s.map, path, px, py))
else
print("[voxfix] capture missed: " .. path)
end
end)
if not ok then print("[voxfix] " .. s.map .. " failed: " .. tostring(err)) end
end
end
print(("[voxfix] %d/%d shots into %s"):format(shots, #SCENES, ROOT))
love.event.quit()
end