Compare commits

..

7 Commits

Author SHA1 Message Date
DramaticShape c4d997cb75 Merge pull request #150 from DramaticShape/dev
1.8.2
2026-08-09 00:12:36 -04:00
DramaticShape a7cbfc8b70 Merge pull request #149 from DramaticShape/1.8.1
1.8.1
2026-08-09 00:08:10 -04:00
DramaticShape 12a1ab0266 iterate version 2026-08-09 00:02:21 -04:00
DramaticShape 2af88d07cf fix victory road statues 2026-08-09 00:01:47 -04:00
DramaticShape 47369c24fe fixed tops of walls in houses 2026-08-08 22:12:07 -04:00
DramaticShape 0a48f764f6 Merge pull request #145 from luisgonzaleznf/fix/scene-shader-precision-love12
fix: scene shader under LÖVE 12 — qualify fireflyNight's precision
2026-08-08 21:54:13 -04:00
luisgonzaleznf 2a924ea8a3 fix scene shader under LOVE 12: qualify fireflyNight's precision
An unqualified float uniform takes each stage's default precision, and
those differ -- highp in the vertex stage, mediump in the fragment one.
LOVE 11 linked the pair regardless; LOVE 12 requires both declarations to
carry the same qualifier and refuses the shader:

  Cannot compile shader: ERROR: Linking fragment stage: Precision
  qualifiers must match: vertex stage: fireflyNight "" fragment stage:
  fireflyNight ""

Voxel3D.available() is 'the scene shader compiled', so the refusal takes
the whole 3D pass with it and the overworld silently falls back to flat
2D.  That is the current state on iOS, whose shell ships LOVE 12, and it
is latent for every other platform that moves to 12.

Uses LOVE_HIGHP_OR_MEDIUMP, the same macro vWorld and vGrid already carry.
2026-08-09 01:48:05 +02:00
9 changed files with 743 additions and 53 deletions
+85
View File
@@ -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
+94 -18
View File
@@ -50,6 +50,9 @@
-- stair_down_e / _down_w a sunken stairwell: the cell opens into a
-- hole with steps descending toward the named
-- side -- stairs that lead DOWN a floor
-- stair_n / stair_down_n the same two flights running into the map
-- rather than across it, for a staircase drawn
-- HEAD-ON: a drawn row is a step, not a column
-- relief a prop drawn from above (a console on the
-- floor): the drawing stays flat and the
-- pixels inside its black outline extrude a
@@ -129,6 +132,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 = <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
-- 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
+19 -1
View File
@@ -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
+95 -27
View File
@@ -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
+52 -5
View File
@@ -125,11 +125,13 @@ local FALLBACK_HEIGHTS = {
stair_w = 16,
stair_down_e = 16,
stair_down_w = 16,
-- a stairwell descending toward the BACK of the map, drawn head-on
-- instead of from the side (the Centers' Cable Club steps). Its own
-- class because the art reading is not the east/west one turned: there
-- a drawn COLUMN is a step and a drawn row is height, here a drawn ROW
-- is a step and drawn row = depth row, 1:1 down the well
-- a flight running toward the BACK of the map, drawn head-on instead of
-- from the side (the Centers' Cable Club steps). Its own class because
-- the art reading is not the east/west one turned: there a drawn COLUMN
-- is a step and a drawn row is height, here a drawn ROW is a step and
-- drawn row = depth row, 1:1 into the opening. `stair_n` climbs away
-- from the room, `stair_down_n` descends into a well
stair_n = 16,
stair_down_n = 16,
}
@@ -221,6 +223,7 @@ local ART = {
stair_w = "stair",
stair_down_e = "stair",
stair_down_w = "stair",
stair_n = "stair",
stair_down_n = "stair",
}
@@ -761,6 +764,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 = <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)
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()
+8 -1
View File
@@ -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
+1 -1
View File
@@ -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",
+252
View File
@@ -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()
+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