diff --git a/CHANGELOG.md b/CHANGELOG.md index bca1656..c9a9a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,91 @@ 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. + + 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 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 2bb11c5..12d9beb 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -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,24 @@ -- 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): what a `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. The face is untouched -- the poster still faces the +-- room. Two forms: +-- +-- wall_top = 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 -- 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 @@ -758,12 +779,38 @@ return { signpost = { 33, 34, 49, 50 }, }, - -- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's - -- room, which use none of these tiles). The free-standing shelf - -- ranks: book rows and base pinned; the shared trim tiles above - -- (41/42, also the lab tables' corners) are adopted as caps by the - -- bookcase builder rather than pinned. + -- Oak's Lab, the Fighting Dojo and Lance's room (one tileset). The + -- lab's free-standing shelf ranks: book rows and base pinned; the + -- shared trim tiles above (41/42, also the lab tables' corners) are + -- adopted as caps by the bookcase builder rather than pinned. The + -- other two rooms are furnished with one thing between them -- the + -- BIRD STATUE, and it is the badge gyms' statue exactly. 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 }, -- the lab tables (the starter-ball display and the north tables): -- 41/42 are also the shelf trim the bookcase builder adopts as @@ -787,6 +834,11 @@ return { REDS_HOUSE_2 = { -- the wall band with its windows stays one 16px face 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 bed = { 45, 46, 47, 61, 62, 63 }, -- stools: a seat-high box, seat art on top, legs on the front @@ -825,6 +877,9 @@ return { -- table rides these heights, not the 8/12px class defaults heights = { stool = 5, table = 6 }, 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 }, -- the dining table (38-44/58-60); its top row also caps the -- bookcases below @@ -860,6 +915,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 +966,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 +995,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 @@ -1589,6 +1657,14 @@ return { wall = { 1, 2, 3, 6, 18, 19, 22, 33, 46, 47, 62, 63, 68, 70, 71, 72, 73, 75, 76, 77, 78, 79, 84, 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 -- 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 diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index 4167656..f19a96d 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -301,6 +301,19 @@ 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, a Center's pokeball poster and the Rocket + -- lift's doors came out lying across the top of the wall as well as + -- standing in it. Only the top is redirected: the face still draws what + -- the map draws. + 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 local function uvRect(tile, vTop, vBot) local ax = (tile % perRow) * 8 @@ -586,6 +599,11 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink) { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, 0.95) elseif run then 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) else local topTile = tile @@ -649,7 +667,7 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink) row = (above and above.authored and above.art == "upright") and (north - 1) or north end - topTile = S.tileAt[keyOf(tx, row)] + topTile = capOf(s, tile) or S.tileAt[keyOf(tx, row)] end -- water's surface, and only water's: the recessed sheet itself, -- never the ground's shoreline bands around it. A cell an object diff --git a/lib/Structures.lua b/lib/Structures.lua index 814364d..ede84d3 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -227,7 +227,11 @@ function Structures.forMap(map) S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def), hideBareRing = hullRingOnly or nil, 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) -- 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 +-- 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) perRow = perRow or map.tileset.tilesPerRow or 16 -- 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 -- 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. + -- + -- 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 srcK = keyOf(tx, top - 1) 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 local tk = keyOf(tx, cy) + S.bookcaseBox[tk] = boxTop if src and cy <= front - covered then S.shapeAt[tk] = src S.tileAt[tk] = S.tileAt[srcK] @@ -1992,6 +2023,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 +2039,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 +2112,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 +2133,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 +2144,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 @@ -2812,9 +2871,10 @@ function Structures.buildObject(S, map, region, cluster, -- 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 -- 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 - 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), math.floor(cluster.maxY / 2)) -- `bookcase` supports as well as `upright`. A prop drawn above an @@ -2831,6 +2891,13 @@ function Structures.buildObject(S, map, region, cluster, and (bs.art == "upright" or bs.art == "bookcase" or bs.class == "building") then 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 local atlasW = map.tileset.imageWidth or 128 @@ -2882,8 +2949,9 @@ function Structures.buildObject(S, map, region, cluster, end end for _, c in ipairs(comps) do - c.z0 = cluster.minY * 8 + math.floor(c.lowY / 8) * 8 - + (support and 8 or 0) + (8 - depth) / 2 + c.z0 = supportRow and (supportRow * 8 + (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 end diff --git a/lib/TileShape.lua b/lib/TileShape.lua index c0c19ce..bbfffa3 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -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,50 @@ function TileShape.bookcaseRelief(tilesetId) return not (entry and entry.bookcase_relief == false) end +--- What a `wall` cell's TOP face wears in this tileset (a tileset entry's +--- 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 +--- 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 +--- is the whole fix, because "plain" is a fact about the drawing that +--- nothing in the geometry can measure. +--- +--- Two forms, because tilesets differ in how far one answer reaches: +--- +--- wall_top = 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) + local s = load() + local entry = s and s.tilesets and s.tilesets[tilesetId] + local spec = entry and entry.wall_top + 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 + -- 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() diff --git a/lib/Voxel3D.lua b/lib/Voxel3D.lua index 3e690cd..1532c2a 100644 --- a/lib/Voxel3D.lua +++ b/lib/Voxel3D.lua @@ -89,7 +89,14 @@ local SHADER = [[ varying vec3 vSun; // this fragment's place in the sun's view varying float vFog; // how deep into the map's haze it stands varying float vFirefly; // zero normally, night glow on firefly cards - uniform float fireflyNight; // shared safely by vertex and pixel stages + // Explicitly qualified, and it has to be: an unqualified float takes each + // STAGE's default precision, and those do not agree -- highp in the vertex + // stage, mediump in the fragment one. LOVE 11 linked the pair anyway; + // LOVE 12 holds both declarations to the same qualifier and refuses the + // whole shader over it, which costs the entire 3D pass -- Voxel3D.available() + // is a shader that compiled, and the overworld falls back to flat 2D with + // nothing said anywhere. Same macro the varyings below already use. + uniform LOVE_HIGHP_OR_MEDIUMP float fireflyNight; #ifdef VOXEL_CULL // where this fragment stands in the FLAT world, for the diorama's // viewport to measure. Same precision reasoning as vGrid below: a diff --git a/manifest.json b/manifest.json index 12ae7a3..b16f350 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "DRAMATIC_SHAPE", "name": "Dramatic Shape Voxel Mod", - "version": "1.8.1", + "version": "1.8.2", "api": 2, "entry": "main.lua", "profile": "content", diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua index 2f3ed88..3e0d84a 100644 --- a/tests/dramatic_shape_test.lua +++ b/tests/dramatic_shape_test.lua @@ -7002,6 +7002,258 @@ 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") + + -- 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") + T.eq(Shape.wallTop("POKECENTER")(2), 40, + "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, + "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) + 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 + 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 ty = 0, 1 do + for tx = 0, 7 do + 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 + + -- 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 ty = 0, 1 do + for tx = 0, 7 do + T.eq(pcTops[ty * 100 + tx], 40, + ("(%d,%d) of the Center's back wall caps with the striped panel") + :format(tx, ty)) + 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)() + Pipelines.reset() run.release() diff --git a/tests/voxel_fix_shots.lua b/tests/voxel_fix_shots.lua new file mode 100644 index 0000000..ab408cb --- /dev/null +++ b/tests/voxel_fix_shots.lua @@ -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