pokecenter machines, water occlusion
DramaticShape · 2026-08-02 ·
parent 91cc2d6f51
pokecenter machines, water occlusion
Changed files
data/voxel_heights.lua | 337 +++++++++++++++++++++++++++++++++++++++++++++++---------------
lib/Buildings.lua | 86 +++++++++++++---
lib/VoxelScene.lua | 45 +++++++--
lib/Water.lua | 9 +-
tests/dramatic_shape_test.lua | 13 +++
tools/building_voxels.py | 107 +++++++++++++-------
6 files changed, 454 insertions(+), 143 deletions(-)
Diff
diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua
index 2c5ae61..a98c059 100644
--- a/data/voxel_heights.lua
+++ b/data/voxel_heights.lua
@@ -842,12 +842,12 @@ return {
-- console face (76/77) and button panel (6/22) are ALSO wall:
-- drawn 16px tall against the band, so wall height is their drawn
-- height and they read as equipment jutting from it. (The
- -- `center_heal_machine` template below claims those four tiles at
- -- every placement and models the console properly; these pins are
- -- the degradation path when the profile is absent.) The
- -- near-black screen tiles must be pinned or the void rule flattens
- -- them. What is NOT wall is the machines' two flanks -- see
- -- `prop` below.
+ -- `center_heal_machine_w`/`_e` templates below claim the full
+ -- 4x4 machine grids at every placement and model the console
+ -- properly; these pins are the degradation path when the profile
+ -- is absent.) The near-black screen tiles must be pinned or the
+ -- void rule flattens them. What is NOT wall is the machines'
+ -- two flanks -- see `prop` below.
wall = { 2, 3, 4, 5, 6, 16, 18, 19, 20, 21, 22, 40, 41,
76, 77, 92, 93, 94, 95 },
-- the counters, half a cell high: top band (8) with the nurse's
@@ -899,7 +899,10 @@ return {
-- game -- 12 maps, the same two cells in each -- so this pin cannot
-- reach anything but the machines. 72 is still the $48
-- water-fallback trap and still has to carry SOME pin; it just
- -- wanted the thin one, not the wall.
+ -- wanted the thin one, not the wall. (The machine templates now
+ -- claim the flank tiles too and model the hoses and keyboard as
+ -- attached 3D equipment; these standee pins are the degradation
+ -- path when the profile is absent.)
prop = { 7, 13, 72, 73,
-- ...and the potted plants -- see THE POTTED PLANTS below
32, 33, 34, 35, 48, 49, 50, 51 },
@@ -1968,13 +1971,24 @@ return {
-- on (see `buildings` below). The pin is only the degradation
-- path here -- the template claims both placements -- but the two
-- must not disagree about where the desk's top is.
- heights = { table = 8 },
+ --
+ -- `stool` is 5 for the same reason: that is the drawn elevation of
+ -- BOTH seats in this tileset -- rows 11-15, the seat's front edge
+ -- over its legs -- and the height the `club_stool` template stands
+ -- them. Bill's chair and the Fan Club's are different drawings
+ -- above the seat but share tiles 59/60 below it, so they are drawn
+ -- at the same height pixel for pixel; the class default of 8 was
+ -- three voxels over both. Every one of these cells is WALKABLE,
+ -- so this is also where a character sitting on one ends up.
+ heights = { table = 8, stool = 5 },
-- the seats: Bill's desk stool (43/44 over 59/60) and the Fan
-- Club's four members' chairs (61/62 over 59/60), a whole cell
-- each. All of them sit in WALKABLE cells, so they were flat
- -- floor until pinned; the 8px standee pool puts them at seat
- -- height, which is also where a character standing on the cell
- -- ends up.
+ -- floor until pinned; the standee pool puts them at seat height,
+ -- which is also where a character standing on the cell ends up.
+ -- The Fan Club's four are modelled in full by `club_stool` below
+ -- and these pins are their degradation path; Bill's chair stays a
+ -- standee, stood up as a part of `bills_desk`.
stool = { 43, 44, 59, 60, 61, 62 },
-- flat, and pinned flat on purpose. 31 is the rooms' floor tile
-- and needs no pin for its 980 walkable placements -- but the
@@ -3737,82 +3751,154 @@ return {
z = 13 }, -- keyboard
},
},
- -- F05: the healing machine behind the counter -- the pair in every
- -- Center (cells (1,0):(2,1) and (6,0):(7,1)), 22 placements, plus
- -- the Indigo Plateau lobby's pair under MART below (scan
- -- "58,59;74,75;76,77;6,22").
+ -- F05: the healing machine behind the counter -- two 4x4-tile
+ -- variants, 24 placements between them (a pair in every Center,
+ -- cells (1,0):(2,1) and (6,0):(7,1), plus the Indigo Plateau
+ -- lobby's pair under MART below). The variants differ only in
+ -- the east flank: the west machine has the control keyboard
+ -- there (7/13, scan "40,58,59,40;40,74,75,40;72,76,77,7;
+ -- 72,6,22,13" -- 12 placements), the east machine mirrored
+ -- hoses (73, scan "...;72,76,77,73;72,6,22,73" -- 12).
--
- -- The drawing is 4x4 tiles but only its middle COLUMN is this
- -- template's. The corners are the striped wall band (40), and
- -- the outer tile columns are the flanking EQUIPMENT -- the hoses
- -- (72 west, 73 east) and the control keyboard (7/13) -- which are
- -- separate thin objects, not part of this console: they are
- -- pinned into the THIN standee pool above and stay out of this
- -- grid. What no class can split is the middle -- a wall-height
+ -- The FULL drawing is claimed now: the corners are the striped
+ -- wall band (40), the flanks the machine's own equipment --
+ -- hoses and a keyboard ATTACHED to the cabinet, not furniture
+ -- standing free (their standee pins above stay as the
+ -- degradation path). What no class can split is a wall-height
-- cabinet with a monitor perched on its front top edge, drawn
-- across two map rows because it towers over the 16px band
-- behind it, which the volume path can only read as more wall.
--
- -- Read off the pixels (local x, the whole drawing's x8..x23):
- -- rows 15-31 the CABINET's front face, 16 wide, 17 tall:
- -- black front-top edge with vent notches (15),
- -- white top rail (16), black panel in a white
- -- frame (17-28), white rail / light plinth / black
- -- ground line (29-31).
- -- rows 6-14 the cabinet's TOP FACE seen from above -- not a
- -- second-story facade: a white expanse with a lit
- -- west strip (x1 white) and a shaded east strip
- -- (x14 #555) wrapping the monitor, full-width in
- -- front of it (13-14), strips beside it (7-12).
- -- Its 9 rows plus the front edge row 15 ARE the
- -- cabinet's depth: 10. `desk.top` lays the band
- -- flat as the lid; where the monitor's drawing
- -- occludes it, the lid continues the nearest strip
- -- (the drawing's own pixels).
- -- rows 1-13 the MONITOR, x2..x13: black back rim (1) and
- -- white cap (2-3) seen from above -- 3 rows plus
- -- the front edge, depth 4 -- then bezel, screen
- -- and control strip (4-13) face-on, 10 tall,
- -- standing on the cabinet top at its front: the
- -- drawn base edge (13) is one band row up from the
- -- front strip (14), so its front plane is one
- -- voxel back from the cabinet's -- z 20..23. The
- -- screen interior (x5..x10, rows 6-8) is sealed
- -- behind its own black frame and sinks one voxel
+ -- Read off the pixels (absolute grid x0..x31):
+ -- rows 15-31, x8..x23 the CABINET's front face, 16 wide, 17
+ -- tall: black front-top edge with vent notches
+ -- (15), white top rail (16), black panel in a
+ -- white frame (17-28), white rail / light plinth /
+ -- black ground line (29-31). `desk.x` bounds the
+ -- desk to the middle columns.
+ -- rows 6-14, x8..x23 the cabinet's TOP FACE seen from above
+ -- -- not a second-story facade: a white expanse
+ -- with a lit west strip (x9) and a shaded east
+ -- strip (x22) wrapping the monitor. Its 9 rows
+ -- plus the front edge row 15 ARE the cabinet's
+ -- depth: 10, z 16..25 against the wall.
+ -- `desk.top` lays the band flat as the lid; where
+ -- the monitor's drawing occludes it, the lid
+ -- continues the nearest strip (the drawing's own
+ -- pixels).
+ -- rows 1-13, x10..x21 the MONITOR: black back rim (1) and
+ -- white cap (2-3) seen from above -- depth 4 --
+ -- then bezel, screen and control strip (4-13)
+ -- face-on, 10 tall, at the cabinet top's front
+ -- (the drawn base edge 13 is one band row up from
+ -- the front strip 14): z 20..23. The screen
+ -- interior (x13..x18, rows 6-8) sinks one voxel
-- via `inset` -- the pane rule applied by hand,
-- because `panes = false` blocks the global pass.
- -- rows 0-5 at the flanks: wall stripes and the machine's
- -- cast shadow on the wall east of it -- BACKGROUND,
- -- the same standing as the potted plants' floor.
- -- The `wall` element keeps the band solid over the
- -- back map row: a 16-tall, 16-deep block cycling
- -- the drawing's own stripe unit (x0, rows 0-3),
- -- what the neighbouring cells' wall pins render.
+ -- rows 16-31, x0..x7 the HOSES (tile 72 stacked): one 8-row
+ -- motif per map row, and the two stacked motifs
+ -- are two hoses in DEPTH -- the drawn row band is
+ -- the depth band, and each motif's own rows are
+ -- its elevation WITHIN that band, exactly the
+ -- potted plants' stacked-cell rule. So both
+ -- hoses hug the floor: a horizontal run off the
+ -- cabinet's side (rows 24-26, x3..x7 -> heights
+ -- 5..7) over a shaded drop landing ON the floor
+ -- (rows 27-31, x2..x7 -> heights 0..4) -- all
+ -- measured, the drawn extent maps 1:1 with no
+ -- continuation. `box` parts at z 18 and z 24, 3
+ -- deep; both wear the LOWER motif's rows (the one
+ -- drawn at its true elevation; the upper motif is
+ -- the same atlas tile, pixel-identical).
+ -- rows 16-31, x24..x31 west variant: the KEYBOARD (7/13), a
+ -- key grid with its cable at the north end and a
+ -- bar down the east edge -- TOP-VIEW art, 16 rows
+ -- = 16 depth rows. A `flat` part mounted on the
+ -- cabinet's side at COUNTER level (`at` 8, the
+ -- counters' own 8px band -- the one number
+ -- top-view art cannot state, pinned to the room's
+ -- work surface), 3 thick, top face wearing the
+ -- drawn art. The dark dashes east of it (x30-31)
+ -- are its cast shadow on the floor: background.
+ -- East variant: mirrored hoses (runs x24..x28,
+ -- drops x24..x29), same rows and z slots.
+ -- rows 0-15 elsewhere wall stripes and the machine's cast
+ -- shadow -- BACKGROUND, the same standing as the
+ -- potted plants' floor. The `wall` element keeps
+ -- the band solid over the back map row, full grid
+ -- width: a 16-tall, 16-deep block cycling the
+ -- drawing's own stripe unit (x0, rows 0-3), what
+ -- the neighbouring cells' wall pins render.
--
-- `depth = 4` is BOTH map rows: the back row is the wall's plot,
- -- the front row the machine's own, and the cabinet leans against
- -- the wall at z 16..25. `panes = false`: the front panel seals
- -- DARK behind LIGHT (a black panel in a white frame), the
- -- opposite polarity to the pane rule, so the global recess pass
- -- would sink the frame and leave the panel proud.
+ -- the front row the machine's own. `panes = false`: the front
+ -- panel seals DARK behind LIGHT (a black panel in a white
+ -- frame), the opposite polarity to the pane rule, so the global
+ -- recess pass would sink the frame and leave the panel proud.
{
- id = "center_heal_machine",
+ id = "center_heal_machine_w",
tiles = {
- { 58, 59 },
- { 74, 75 },
- { 76, 77 },
- { 6, 22 },
+ { 40, 58, 59, 40 },
+ { 40, 74, 75, 40 },
+ { 72, 76, 77, 7 },
+ { 72, 6, 22, 13 },
},
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
slab = 0, frontEave = 0, ledge = nil, depth = 4,
panes = false,
wall = { h = 16, depthPx = 16, x = 0, cycle = { 0, 3 } },
- desk = { fascia = { 15, 16 }, base = { 17, 31 }, z = 16,
- depthPx = 10, top = { 6, 14 } },
+ desk = { x = { 8, 23 }, fascia = { 15, 16 }, base = { 17, 31 },
+ z = 16, depthPx = 10, top = { 6, 14 } },
parts = {
- { kind = "upright", x = { 2, 13 }, top = { 1, 3 },
+ { kind = "upright", x = { 10, 21 }, top = { 1, 3 },
facade = { 4, 13 }, z = 20, depth = 4,
- inset = { x = { 5, 10 }, rows = { 6, 8 } } }, -- the monitor
+ inset = { x = { 13, 18 }, rows = { 6, 8 } } }, -- the monitor
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 18, depth = 3 }, -- N hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 18, depth = 3 }, -- N hose drop
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 24, depth = 3 }, -- S hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 24, depth = 3 }, -- S hose drop
+ { kind = "flat", x = { 24, 29 }, rows = { 16, 31 },
+ z = 16, at = 8, thick = 3 }, -- keyboard shelf
+ },
+ },
+ {
+ id = "center_heal_machine_e",
+ tiles = {
+ { 40, 58, 59, 40 },
+ { 40, 74, 75, 40 },
+ { 72, 76, 77, 73 },
+ { 72, 6, 22, 73 },
+ },
+ roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+ slab = 0, frontEave = 0, ledge = nil, depth = 4,
+ panes = false,
+ wall = { h = 16, depthPx = 16, x = 0, cycle = { 0, 3 } },
+ desk = { x = { 8, 23 }, fascia = { 15, 16 }, base = { 17, 31 },
+ z = 16, depthPx = 10, top = { 6, 14 } },
+ parts = {
+ { kind = "upright", x = { 10, 21 }, top = { 1, 3 },
+ facade = { 4, 13 }, z = 20, depth = 4,
+ inset = { x = { 13, 18 }, rows = { 6, 8 } } }, -- the monitor
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 18, depth = 3 }, -- NW hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 18, depth = 3 }, -- NW hose drop
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 24, depth = 3 }, -- SW hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 24, depth = 3 }, -- SW hose drop
+ { kind = "box", x = { 24, 28 }, rows = { 24, 26 },
+ z = 18, depth = 3 }, -- NE hose run
+ { kind = "box", x = { 24, 29 }, rows = { 27, 31 },
+ z = 18, depth = 3 }, -- NE hose drop
+ { kind = "box", x = { 24, 28 }, rows = { 24, 26 },
+ z = 24, depth = 3 }, -- SE hose run
+ { kind = "box", x = { 24, 29 }, rows = { 27, 31 },
+ z = 24, depth = 3 }, -- SE hose drop
},
},
},
@@ -3840,28 +3926,75 @@ return {
},
},
-- F05 again: the Indigo Plateau lobby has the Centers' healing
- -- machine pair too (cells (5,4):(6,5) and (10,4):(11,5)), and its
- -- MART tileset shares the POKECENTER atlas, so this is the same
- -- drawing tile for tile. Same part table as the POKECENTER entry
- -- above.
+ -- machine pair too -- the west-variant grid at (5,4):(6,5) and
+ -- the east at (10,4):(11,5), verified by region dump -- and its
+ -- MART tileset shares the POKECENTER atlas, so these are the
+ -- same drawings tile for tile. Same part tables as the
+ -- POKECENTER entries above.
{
- id = "center_heal_machine",
+ id = "center_heal_machine_w",
tiles = {
- { 58, 59 },
- { 74, 75 },
- { 76, 77 },
- { 6, 22 },
+ { 40, 58, 59, 40 },
+ { 40, 74, 75, 40 },
+ { 72, 76, 77, 7 },
+ { 72, 6, 22, 13 },
},
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
slab = 0, frontEave = 0, ledge = nil, depth = 4,
panes = false,
wall = { h = 16, depthPx = 16, x = 0, cycle = { 0, 3 } },
- desk = { fascia = { 15, 16 }, base = { 17, 31 }, z = 16,
- depthPx = 10, top = { 6, 14 } },
+ desk = { x = { 8, 23 }, fascia = { 15, 16 }, base = { 17, 31 },
+ z = 16, depthPx = 10, top = { 6, 14 } },
parts = {
- { kind = "upright", x = { 2, 13 }, top = { 1, 3 },
+ { kind = "upright", x = { 10, 21 }, top = { 1, 3 },
facade = { 4, 13 }, z = 20, depth = 4,
- inset = { x = { 5, 10 }, rows = { 6, 8 } } }, -- the monitor
+ inset = { x = { 13, 18 }, rows = { 6, 8 } } }, -- the monitor
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 18, depth = 3 }, -- N hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 18, depth = 3 }, -- N hose drop
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 24, depth = 3 }, -- S hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 24, depth = 3 }, -- S hose drop
+ { kind = "flat", x = { 24, 29 }, rows = { 16, 31 },
+ z = 16, at = 8, thick = 3 }, -- keyboard shelf
+ },
+ },
+ {
+ id = "center_heal_machine_e",
+ tiles = {
+ { 40, 58, 59, 40 },
+ { 40, 74, 75, 40 },
+ { 72, 76, 77, 73 },
+ { 72, 6, 22, 73 },
+ },
+ roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+ slab = 0, frontEave = 0, ledge = nil, depth = 4,
+ panes = false,
+ wall = { h = 16, depthPx = 16, x = 0, cycle = { 0, 3 } },
+ desk = { x = { 8, 23 }, fascia = { 15, 16 }, base = { 17, 31 },
+ z = 16, depthPx = 10, top = { 6, 14 } },
+ parts = {
+ { kind = "upright", x = { 10, 21 }, top = { 1, 3 },
+ facade = { 4, 13 }, z = 20, depth = 4,
+ inset = { x = { 13, 18 }, rows = { 6, 8 } } }, -- the monitor
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 18, depth = 3 }, -- NW hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 18, depth = 3 }, -- NW hose drop
+ { kind = "box", x = { 3, 7 }, rows = { 24, 26 },
+ z = 24, depth = 3 }, -- SW hose run
+ { kind = "box", x = { 2, 7 }, rows = { 27, 31 },
+ z = 24, depth = 3 }, -- SW hose drop
+ { kind = "box", x = { 24, 28 }, rows = { 24, 26 },
+ z = 18, depth = 3 }, -- NE hose run
+ { kind = "box", x = { 24, 29 }, rows = { 27, 31 },
+ z = 18, depth = 3 }, -- NE hose drop
+ { kind = "box", x = { 24, 28 }, rows = { 24, 26 },
+ z = 24, depth = 3 }, -- SE hose run
+ { kind = "box", x = { 24, 29 }, rows = { 27, 31 },
+ z = 24, depth = 3 }, -- SE hose drop
},
},
},
@@ -4236,6 +4369,46 @@ return {
facade = { 22, 31 }, rise = -8, z = 22, depth = 10 },
},
},
+ -- F09 once more: the Pokemon Fan Club's four members' chairs,
+ -- drawn round the boardroom table -- POKEMON_FAN_CLUB cells (1,3),
+ -- (1,4), (6,3) and (6,4), and nowhere else in the game.
+ --
+ -- A DIFFERENT drawing from the house stool -- its seat field
+ -- carries a one-pixel white margin where the house's carries two
+ -- -- but the same object band for band: rows 5-10 the seat seen
+ -- from above (its lid), row 11 the seat's own front edge, 12-15
+ -- the legs with the floor showing between them. Its elevation
+ -- rows are PIXEL-IDENTICAL to the house drawing's, so it takes
+ -- that part table unchanged, growth and all: z 3..13, `stretch`
+ -- mapping the seat band over the deeper lid, `panes = false` so
+ -- the pane rule does not sink the legs' lit faces.
+ --
+ -- Rows 11-15 come from tiles 59/60, which this drawing SHARES
+ -- with Bill's chair (43/44 over 59/60, modelled as a part of
+ -- `bills_desk` above). That is why the tileset's `stool = 5`
+ -- height override is right for both seats rather than a special
+ -- case for this one: they are drawn at the same height because
+ -- they are drawn with the same pixels.
+ --
+ -- Listed after `bills_desk` although neither grid is a subgrid of
+ -- the other (that grid has 43/44 over 59/60, this one 61/62), so
+ -- the order is free and the bigger, more specific grid keeps
+ -- first claim.
+ {
+ id = "club_stool",
+ tiles = {
+ { 61, 62 },
+ { 59, 60 },
+ },
+ roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
+ slab = 0, frontEave = 0, ledge = nil,
+ panes = false,
+ parts = {
+ { kind = "upright", x = { 2, 13 }, top = { 5, 10 },
+ facade = { 11, 15 }, z = 3, depth = 11,
+ stretch = true }, -- the stool
+ },
+ },
},
},
}
diff --git a/lib/Buildings.lua b/lib/Buildings.lua
index 04a55df..def6c81 100644
--- a/lib/Buildings.lua
+++ b/lib/Buildings.lua
@@ -440,14 +440,68 @@ local function deskSetModel(sp, pr, t)
if p.kind == "flat" then
-- drawn row = depth row by default; `z` renames the origin when
-- the flat sits below the desk's own drawn top span (the Center
- -- PC's keyboard)
+ -- PC's keyboard). `at` names the sheet's own height when it does
+ -- not lie on the desk plane (the healing machine's keyboard is a
+ -- shelf mounted on the cabinet's side); `thick` gives it a body
+ -- -- layers below the sheet repeating each column's own texel,
+ -- the same continuation rule every synthesized surface follows.
local r0 = p.rows[1]
local z0 = p.z or r0
+ local atY = p.at or plane
+ local thick = p.thick or 1
+ if atY > ytop then ytop = atY end
for sy = r0, p.rows[2] do
local z = z0 + (sy - r0)
if z >= 0 and z < D then
for sx = x0, x1 do
- if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
+ if inside[sy * W + sx] then
+ for y = math.max(0, atY - thick + 1), atY do
+ put(sx, y, z, sy * W + sx)
+ end
+ end
+ end
+ end
+ end
+ elseif p.kind == "box" then
+ -- A BOX part is a drawn rect standing at its own drawn
+ -- elevation -- equipment attached to the machine rather than an
+ -- object on the desk plane. The rows are face-on art: the top
+ -- row's drawn height IS the box's top (ground - 1 - r0,
+ -- measured), and the box runs down to `base` (default the drawn
+ -- extent; 0 continues it to the floor, the legs-continue rule).
+ -- Height beyond the drawn rows fills the way a roof band does:
+ -- rows before `cycle` map 1:1 from the top, rows after it 1:1
+ -- from the bottom -- the healing machine hoses' foot lands ON
+ -- the floor -- and the cycle window repeats between.
+ local r0, r1 = p.rows[1], p.rows[2]
+ local c0 = p.cycle and p.cycle[1] or r1
+ local c1 = p.cycle and p.cycle[2] or r1
+ local pz = p.z or 0
+ local pd = p.depth
+ local top = pr.ground - 1 - r0
+ local bot = p.base or (pr.ground - 1 - r1)
+ local nTop, nBot = c0 - r0, r1 - c1
+ if top > ytop then ytop = top end
+ for y = bot, top do
+ local k, j = top - y, y - bot
+ local sy
+ if k < nTop then
+ sy = r0 + k
+ elseif j < nBot then
+ sy = r1 - j
+ else
+ sy = c0 + (k - nTop) % (c1 - c0 + 1)
+ end
+ for sx = x0, x1 do
+ local i = sy * W + sx
+ if inside[i] then
+ local ix = interiorAt(sx, sy, x0, x1)
+ for z = pz, pz + pd - 1 do
+ if z >= 0 and z < D then
+ local px = (z == pz or z == pz + pd - 1) and sx or ix
+ put(sx, y, z, sy * W + px)
+ end
+ end
end
end
end
@@ -700,6 +754,11 @@ local function deskSetModel(sp, pr, t)
local dz0 = t.desk.z or 0
local dz1 = dz0 + deskD - 1
local deskG = b1 + 1
+ -- ...and the desk's COLUMNS (`x`), when the grid is wider than the
+ -- desk: the healing machine's grid carries its flanking hoses and
+ -- keyboard, and the cabinet is only the middle 16 columns.
+ local dx0 = t.desk.x and t.desk.x[1] or 0
+ local dx1 = t.desk.x and t.desk.x[2] or W - 1
-- The WALL element: the band the machine backs onto, whose tiles this
-- grid claims. The drawing shows it only as the stripe background
@@ -727,9 +786,9 @@ local function deskSetModel(sp, pr, t)
for sy = b0, b1 do
Budget.tick()
local y = deskG - 1 - sy
- for sx = 0, W - 1 do
+ for sx = dx0, dx1 do
if inside[sy * W + sx] then
- local ix = interiorAt(sx, sy, 0, W - 1)
+ local ix = interiorAt(sx, sy, dx0, dx1)
for z = dz0, dz1 do
local px = (z == dz0 or z == dz1) and sx or ix
put(sx, y, z, sy * W + px)
@@ -739,8 +798,9 @@ local function deskSetModel(sp, pr, t)
end
for i in pairs(pr.recess) do
local sy = math.floor(i / W)
- if sy >= b0 and sy <= b1 then
- vox[key(i % W, deskG - 1 - sy, dz1)] = nil
+ local sx = i % W
+ if sy >= b0 and sy <= b1 and sx >= dx0 and sx <= dx1 then
+ vox[key(sx, deskG - 1 - sy, dz1)] = nil
end
end
@@ -748,7 +808,7 @@ local function deskSetModel(sp, pr, t)
for sy = f0, f1 do
Budget.tick()
local y = plane - 1 - (sy - f0)
- for sx = 0, W - 1 do
+ for sx = dx0, dx1 do
for z = dz0, dz1 do put(sx, y, z, sy * W + sx) end
end
end
@@ -767,19 +827,19 @@ local function deskSetModel(sp, pr, t)
for z = dz0, dz1 do
Budget.tick()
local sy = z == dz1 and f0 or math.min(tr0 + (z - dz0), tr1)
- for sx = 0, W - 1 do
+ for sx = dx0, dx1 do
local px = sx
for _, p in ipairs(t.parts) do
local px0, px1 = p.x[1], p.x[2]
local r0, r1
- if p.kind == "flat" or p.kind == "iso" then
+ if p.kind == "flat" or p.kind == "iso" or p.kind == "box" then
r0, r1 = p.rows[1], p.rows[2]
else
r0, r1 = p.top[1], p.facade[2]
end
if sx >= px0 and sx <= px1 and sy >= r0 and sy <= r1 then
px = (sx - px0 < px1 - sx) and (px0 - 1) or (px1 + 1)
- px = math.max(0, math.min(W - 1, px))
+ px = math.max(dx0, math.min(dx1, px))
break
end
end
@@ -790,12 +850,12 @@ local function deskSetModel(sp, pr, t)
-- the lid continues the sibling tables' top -- black rim, white
-- highlight courses along the north and west, grey field
local field = t.desk.lid == "white" and WHITE or GREY
- for sx = 0, W - 1 do
+ for sx = dx0, dx1 do
for z = dz0, dz1 do
local shade = field
- if sx == 0 or sx == W - 1 or z == dz0 or z == dz1 then
+ if sx == dx0 or sx == dx1 or z == dz0 or z == dz1 then
shade = BLACK
- elseif sx == 1 or z == dz0 + 1 then
+ elseif sx == dx0 + 1 or z == dz0 + 1 then
shade = WHITE
end
put(sx, plane - 1, z, pr.shadeTexel[shade])
diff --git a/lib/VoxelScene.lua b/lib/VoxelScene.lua
index daa4131..e06036e 100644
--- a/lib/VoxelScene.lua
+++ b/lib/VoxelScene.lua
@@ -583,8 +583,40 @@ end
-- The overworld's alone: the staged battle draws its water plain, always --
-- its placed camera reads this pass wrong, and a stage set wants painted
-- water anyway (see BattleScene, where the choice is argued).
+-- ------- and why the flat draw happens FIRST, every time
+--
+-- The reflective pass writes no depth -- it cannot, the depth canvas is
+-- detached for the length of it so the shader can READ it -- and it does its
+-- own depth test against that texture instead. That test asks whether
+-- something opaque is in front, and it answers correctly for every case but
+-- one: WATER IN FRONT OF WATER. Nothing puts water in the depth buffer, so
+-- no lake can hide another, and the pass simply paints them in mesh order.
+--
+-- On flat water that never mattered, because every surface lies in the one
+-- plane at its own recessed height and a farther sheet always lands farther
+-- down the screen. THE WORLD CURVE ENDS THAT. The bend drops the world by
+-- the square of its distance, so the far side of the map swings down and
+-- back up into the near field of view -- and a sheet of sea a hundred and
+-- fifty tiles away, drawn later in the same mesh, paints straight over the
+-- pond at the player's feet. That is the tall grass and the water "from the
+-- other side of the map": not a reflection of them, THEM, rasterised on top
+-- of the water in front of you.
+--
+-- So the meshes go down flat first, through the ordinary scene shader with
+-- depth writes on, and the reflective pass draws over the top of what
+-- survived. Three things fall out of it and all three are wanted: the depth
+-- buffer now holds the water surface, so the pass's own test throws the far
+-- sheet away; the reflection COPY holds it too, so a ray grazing another
+-- part of the lake reads water rather than the void behind it; and the flat
+-- draw is the same fallback this function already ended with, so a frame
+-- that cannot run the reflective pass is unchanged.
+--
+-- It costs one more rasterisation of the water meshes. Water is a small
+-- share of a frame's triangles and this is the cheapest shader in the mode.
function VoxelScene.drawWater(draws, cast)
- local plain = true
+ for _, d in ipairs(draws) do
+ Voxel3D.draw(d[1], d[2], d[3])
+ end
if Water.enabled() and Voxel3D.depthReadable() then
local mirror, depth = Voxel3D.beginWater(cast)
local w, h = Voxel3D.size()
@@ -602,20 +634,15 @@ function VoxelScene.drawWater(draws, cast)
Water.draw(d[1], d[2], d[3])
end
Water.finish()
- plain = false
end
-- Unconditionally, and OUTSIDE the success branch: beginWater unbinds
-- the shader and the depth mode BEFORE it can discover it cannot go on,
-- so a frame that bails halfway through has to be put back together
- -- exactly like one that succeeded -- otherwise the plain draw below (and
- -- every pass after it) runs with no shader and no depth test.
+ -- exactly like one that succeeded -- otherwise every pass after it runs
+ -- with no shader and no depth test. (What a bail leaves on screen is the
+ -- flat draw above, which is where this function used to end anyway.)
Voxel3D.endWater()
end
- if plain then
- for _, d in ipairs(draws) do
- Voxel3D.draw(d[1], d[2], d[3])
- end
- end
end
-- A stamp of everything the sun pass depends on. Nothing in it moving
diff --git a/lib/Water.lua b/lib/Water.lua
index 4ab4022..3bfcf4e 100644
--- a/lib/Water.lua
+++ b/lib/Water.lua
@@ -956,8 +956,15 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
// the bound canvas's own pixel size, the same units sc is measured in, on
// every display. (`screen` stays in units: skyPos reads it against cell
// and skyEdge, which are unit-measured with it.)
+ //
+ // The buffer now holds THIS SURFACE too (VoxelScene draws the water flat
+ // before the pass that reflects it), which is what makes one lake able to
+ // hide another -- and it means every fragment here is testing against its
+ // own depth. The two draws reach it through different shader programs, so
+ // ask for a hair of slack rather than bit-equality: far less than the gap
+ // to anything genuinely in front, far more than two compilers disagree by.
vec2 uv = sc / love_ScreenSize.xy;
- if (gl_FragCoord.z > Texel(depthTex, uv).r) discard;
+ if (gl_FragCoord.z > Texel(depthTex, uv).r + 1e-5) discard;
// THE COLUMN THIS FRAGMENT IS LOOKING AT. Every water pixel is a bar of
// its own standing a whole number of pixels tall, and the ray decides
diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua
index 2fb9094..a832e15 100644
--- a/tests/dramatic_shape_test.lua
+++ b/tests/dramatic_shape_test.lua
@@ -1915,6 +1915,19 @@ T.check(plain:find("sc / love_ScreenSize.xy", 1, true) ~= nil,
.. "size -- `screen` counts canvas UNITS, and on a highdpi phone the two "
.. "differ by the density, which clamped the lookup and cut the water "
.. "into blocks")
+-- ...and it tests against a buffer that now holds the water surface too,
+-- because VoxelScene lays the meshes down flat before the reflective pass
+-- runs over them. Nothing else can make one lake hide another: the pass
+-- writes no depth (the buffer is detached so it can be READ), so before this
+-- the sheets were simply painted in mesh order. Flat water never showed it --
+-- one plane, and a farther sheet always lands farther down the screen -- but
+-- the world curve drops the far side of the map into the near field of view,
+-- and a sea a hundred and fifty tiles away came out rasterised on top of the
+-- pond at the player's feet, tall grass and all.
+T.check(plain:find("Texel(depthTex, uv).r + 1e-5", 1, true) ~= nil,
+ "with a hair of slack, because every water fragment is now testing "
+ .. "against its own depth through a second shader program")
+T.check(VoxelScene.drawWater ~= nil, "and the flat draw that puts it there")
-- ------- the lift itself
--
diff --git a/tools/building_voxels.py b/tools/building_voxels.py
index a73fa9d..bcc9078 100644
--- a/tools/building_voxels.py
+++ b/tools/building_voxels.py
@@ -765,6 +765,35 @@ TEMPLATES = {
z=3, depth=11, stretch=True), # the stool
],
),
+ # F09 once more, on the interior atlas: the Pokemon Fan Club's four
+ # members' chairs, drawn round the boardroom table -- 4 placements
+ # (cells (1,3), (1,4), (6,3), (6,4)) and nowhere else in the game.
+ # A DIFFERENT drawing from the house stool -- its seat field carries
+ # a one-pixel white margin where the house's carries two -- but the
+ # same object band for band, and its elevation rows (11-15: the
+ # seat's front edge, then the legs) are pixel-identical to the house
+ # drawing's. So it takes the same part table unchanged, including
+ # the two-voxel growth north and south.
+ #
+ # Those elevation rows come from tiles 59/60, which this drawing
+ # SHARES with Bill's chair (43/44 over 59/60, modelled as a part of
+ # `bills_desk`) -- which is why one `stool = 5` height override in
+ # the tileset entry is right for both seats rather than a special
+ # case: they are drawn at the same height because they are drawn
+ # with the same pixels.
+ "club_stool": dict(
+ tiles=[
+ [61, 62],
+ [59, 60],
+ ],
+ roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
+ slab=0, front_eave=0, ledge=None, tileset="interior",
+ panes=False,
+ parts=[
+ dict(kind="upright", x=(2, 13), top=(5, 10), facade=(11, 15),
+ z=3, depth=11, stretch=True), # the stool
+ ],
+ ),
# F04: the Pokemon Center's PC -- every Center's northeast corner
# (11 placements) plus the Indigo Plateau lobby, whose MART tileset
# shares this atlas. The lab desk-set read again: a
@@ -829,29 +858,31 @@ TEMPLATES = {
# voxel via `inset` -- the pane rule by hand, since
# `panes=False` blocks the global pass.
# rows 16-31, x0..x7 the HOSES (tile 72 twice): one 8-row
- # motif per map row -- a horizontal run high on the
- # cabinet's side (16-18, x3..x7 -> heights 13..15,
- # measured off the drawn rows) and a shaded drop
- # (19-23, x2..x7) that bends down INTO THE FLOOR.
- # The two stacked motifs are two hoses in DEPTH (the
- # drawn row band is the depth band): `box` parts at
- # z 18 and z 24, 3 deep, the drop cycling its two
- # shading rows (19-20) down to the floor with the
- # drawn foot rows (21-23) landing at the bottom --
- # the roof-rim rule turned vertical. The second
- # hose re-wears the first motif's rows: the atlas
- # tile is the same, so the pixels are identical.
+ # motif per map row, and the two stacked motifs are
+ # two hoses in DEPTH -- the drawn row band is the
+ # depth band, and each motif's own rows are its
+ # elevation WITHIN that band, exactly the potted
+ # plants' stacked-cell rule. So both hoses hug the
+ # floor: a horizontal run off the cabinet's side
+ # (rows 24-26, x3..x7 -> heights 5..7) over a shaded
+ # drop landing ON the floor (rows 27-31, x2..x7 ->
+ # heights 0..4) -- all measured, the drawn extent
+ # maps 1:1 with no continuation. `box` parts at z 18
+ # and z 24, 3 deep; both wear the LOWER motif's rows
+ # (the one drawn at its true elevation; the upper
+ # motif is the same atlas tile, pixel-identical).
# rows 16-31, x24..x31 west variant: the KEYBOARD (7/13), a
# key grid with its cable at the north end and a bar
# down the east edge -- TOP-VIEW art, 16 rows = 16
# depth rows. A `flat` part mounted on the cabinet's
- # side at working height (`at` 11, authored -- the
- # one number top-view art cannot state), 3 thick,
- # top face wearing the drawn art. The stray dark
- # dashes east of it (x30-31) are its cast shadow on
- # the floor: background. East variant: mirrored
- # hoses (runs 16-18 x24..x28, drops 19-23 x24..x29),
- # same z slots.
+ # side at COUNTER level (`at` 8, the counters' own
+ # 8px band -- the one number top-view art cannot
+ # state, pinned to the room's work surface), 3
+ # thick, top face wearing the drawn art. The stray
+ # dark dashes east of it (x30-31) are its cast
+ # shadow on the floor: background. East variant:
+ # mirrored hoses (runs x24..x28, drops x24..x29),
+ # same rows and z slots.
# rows 0-15 elsewhere wall stripes and cast shadow: BACKGROUND.
# The `wall` element keeps the band solid over the
# back map row, full grid width -- a 16-tall,
@@ -878,16 +909,16 @@ TEMPLATES = {
dict(kind="upright", x=(10, 21), top=(1, 3), facade=(4, 13),
z=20, depth=4,
inset=dict(x=(13, 18), rows=(6, 8))), # the monitor
- dict(kind="box", x=(3, 7), rows=(16, 18),
+ dict(kind="box", x=(3, 7), rows=(24, 26),
z=18, depth=3), # north hose run
- dict(kind="box", x=(2, 7), rows=(19, 23), cycle=(19, 20),
- base=0, z=18, depth=3), # north hose drop
- dict(kind="box", x=(3, 7), rows=(16, 18),
+ dict(kind="box", x=(2, 7), rows=(27, 31),
+ z=18, depth=3), # north hose drop
+ dict(kind="box", x=(3, 7), rows=(24, 26),
z=24, depth=3), # south hose run
- dict(kind="box", x=(2, 7), rows=(19, 23), cycle=(19, 20),
- base=0, z=24, depth=3), # south hose drop
+ dict(kind="box", x=(2, 7), rows=(27, 31),
+ z=24, depth=3), # south hose drop
dict(kind="flat", x=(24, 29), rows=(16, 31),
- z=16, at=11, thick=3), # keyboard shelf
+ z=16, at=8, thick=3), # keyboard shelf
],
),
"center_heal_machine_e": dict(
@@ -907,22 +938,22 @@ TEMPLATES = {
dict(kind="upright", x=(10, 21), top=(1, 3), facade=(4, 13),
z=20, depth=4,
inset=dict(x=(13, 18), rows=(6, 8))), # the monitor
- dict(kind="box", x=(3, 7), rows=(16, 18),
+ dict(kind="box", x=(3, 7), rows=(24, 26),
z=18, depth=3), # NW hose run
- dict(kind="box", x=(2, 7), rows=(19, 23), cycle=(19, 20),
- base=0, z=18, depth=3), # NW hose drop
- dict(kind="box", x=(3, 7), rows=(16, 18),
+ dict(kind="box", x=(2, 7), rows=(27, 31),
+ z=18, depth=3), # NW hose drop
+ dict(kind="box", x=(3, 7), rows=(24, 26),
z=24, depth=3), # SW hose run
- dict(kind="box", x=(2, 7), rows=(19, 23), cycle=(19, 20),
- base=0, z=24, depth=3), # SW hose drop
- dict(kind="box", x=(24, 28), rows=(16, 18),
+ dict(kind="box", x=(2, 7), rows=(27, 31),
+ z=24, depth=3), # SW hose drop
+ dict(kind="box", x=(24, 28), rows=(24, 26),
z=18, depth=3), # NE hose run
- dict(kind="box", x=(24, 29), rows=(19, 23), cycle=(19, 20),
- base=0, z=18, depth=3), # NE hose drop
- dict(kind="box", x=(24, 28), rows=(16, 18),
+ dict(kind="box", x=(24, 29), rows=(27, 31),
+ z=18, depth=3), # NE hose drop
+ dict(kind="box", x=(24, 28), rows=(24, 26),
z=24, depth=3), # SE hose run
- dict(kind="box", x=(24, 29), rows=(19, 23), cycle=(19, 20),
- base=0, z=24, depth=3), # SE hose drop
+ dict(kind="box", x=(24, 29), rows=(27, 31),
+ z=24, depth=3), # SE hose drop
],
),
# F06: the Bike Shop's toolbox -- 2 placements, cells (6,6) and (7,7)