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)