diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b4294..7b081a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,374 @@ # Changelog +## 1.0.6 + +### Added + +- **Conditional pins.** A profile entry may now carry `when_above`: + tile id -> rules keyed on the tile drawn directly north of it, + resolved per POSITION in `TileShape.at`. A pin is per tile id and one + graphic can mean two things -- the route gates' `$32` is both the + wall's dark base course and every service counter's front, and it is + the bottom row of its cell either way. Pinned `wall` the counters + stood a full 16px; pinned `counter` the deep wall banks corrugated + 16/8 for sixteen rows and the room read as crates. What separates the + two uses is what sits on top, so that is what the rule reads. The + gates now have half-height counters AND level walls. + +- **The Pokemon Tower has an exterior.** It is the one catalogued + building drawing the map edge cuts off (no roof band is on the map at + all), and it had no `buildings` entry, so it fell to the volume path + -- which tops a run by repeating its first two rows, laying window + courses flat across the plateau. Sealing the silhouette on the north + alone closes it (88% fill, one piece, against 37% and 126 pieces + unsealed), and one row of roof band spent on the drawing's top margin + costs no window course. It stands as a real tower, panes recessed, + door on the ground. + +- **The Indigo Plateau statues stand up**, built exactly like the gym + statues: plinth a solid 16px block, figure a per-pixel cutout riding + it. On the avenue the statues stack with no gap, so the flood joined + six of them into one 24-row region and the volume builder raised + ridges of boxes with the statue art folded on the front. The same + bird is drawn at the foot of every badge-check pillar, so those are + crowned too. + +### Fixed + +- **Tall grass: one clump per tile.** Each 8x8 tile is a whole clump, + but the template split every tile AGAIN into its top and bottom four + art rows and stood those at two different depths -- so any blade + running down a tile was cut in half, into two 4px stubs 4px apart. + One tile is now one full-height standing slab at its own depth; a + cell's 2x2 tiles still stand independently, so the player walks + between the north and south rows. + +- **Ledge lines are continuous.** `$34` is the cliff slope's foot and + also the pillar between hop-down segments; pinned `wall` with the + rest of the slope chain (the Diglett's Cave fix) it stood those + pillars 16px beside a 6px lip. At ledge height the run reads as one + lip, and the mound is unchanged -- its foot row reads as the talus it + is drawn as. + +- **A prop only stands on furniture when its own cell is blocked.** + "Is something drawn above me" is not "am I standing on it": a chair + drawn against the north side of a table is above the table's trim row + too, and was being lifted onto the tabletop, with its claimed cells + re-tiled as tabletop so the table marched two rows north. Three + chairs in Cinnabar's trade room and Fuchsia's meeting room, and the + Celadon diner's stools. The world already knows the difference: a + thing that sits ON furniture occupies a blocked cell, a seat you walk + up to is in a walkable one. + +- **Caves: nothing below sea level, and the water is water.** Two tiles + were identified backwards in the first pass. `$14` -- the tile the + engine animates, that `Map.WATER_TILES` names and that Surf runs on + -- was pinned `wall`, so Cerulean Cave's lake and the Seafoam sea + stood up as rock slabs. And the pale dithered rock fill was pinned + `water`, cutting 612 tiles of two-cell-wide trench through four maps. + Both corrected; a sweep of all 19 cave maps now reports zero tiles + below the datum. The elevation scheme is documented in the entry and + derived from the game's own `tilePairs`: dark floor, water and drop + holes at 0, the lit shelf a 6px step above, rock at 16. + +- **Cave ladders climb.** Which ladder graphic goes up and which goes + down is unanimous in the warp table -- 37 cells of one always warp + down, 40 of the other always up -- so they are real stepped flights + now, not painted plates. + +- **Poke Mart's register stands on the counter.** The pin was on the + wrong tile: `$08` is the counter's own top band, not the register, so + the standee flood ate everything but two black lines. The register is + the keypad-and-receipt drawing one row up. + +- **Celadon's televisions**, which were solid 16px boxes wearing the TV + art on one face, and the **Pokemon Tower reception desk** and the + **gate counters**, which stood at wall height, are all their drawn + heights now. The **Fan Club and Silph boardroom statues** were read + as seated chairmen and painted onto the tabletop; they are cutouts + standing on their pedestals, and the tables are cut to a true + octagonal footprint. + +## 1.0.5 + +### Added + +- **Every remaining interior is furnished.** The profile covered ten + tilesets; it now covers twenty-three -- 1,190 pinned tiles across + `GATE`, `FOREST_GATE`, `LOBBY`, `MUSEUM`, `LAB`, `MANSION`, + `INTERIOR`, `CLUB`, `SHIP`, `SHIP_PORT`, `FACILITY`, `CEMETERY` and + `UNDERGROUND`, plus full entries for `CAVERN` and `GYM` which had only + stubs. Roughly 130 maps, surveyed against the standard the finished + interiors already set: one 16px wall band carrying whatever is drawn + built into it, half-cell counters so the drawn front folds up and the + top stays on top, `bookcase` collapse for free-standing shelves, + thin-pool standees for plants, and small objects riding the furniture + they are drawn above. + + What the detector was doing before, by way of what changed: + + - **Rooms with no walls.** Three tile ids (`$14`, `$32`, `$48`) are + claimed by the engine's water set in EVERY tileset, and collision is + per CELL, so one of them in a cell's bottom-left corner sank the + whole cell. `$32` draws both the route gates' wall base course and + every counter front, so all 25 gate maps were a checkered floor in a + moat; the same trap put ponds through two thirds of Seafoam B4F, + under every museum vitrine, along the S.S. Anne's wall corners and + across Silph Co 1F's lobby island. The set is wider than three ids + in practice -- `LAB`, `MANSION` and `INTERIOR` each hit six to nine + -- because the test is per cell, so an innocent tile sharing a cell + with a trapped one sinks with it and has to be pinned too. + - **Towers and fused monoliths.** Counters raised to 48px dragging + their wall band with them, merchandise racks fused sideways into + 32px blocks four tiles deep, cave shelf edges standing as 48px fins + beside a 16px band, the Vermilion liner folded upright into a lumpy + 48px slab. + - **Furniture that was not there at all.** Anything whose cell is + walkable resolved to flat ground: 59 department-store stools, every + gate lounge table and pair of binoculars, both museum staircases, + the gym-lounge chairs, and -- via the void rule -- the black + partition walls every gym is divided by, which left their white rim + columns standing as hollow 48px fins. + - **314 gravestones** in Pokemon Tower were 8px stubs, because the + volume path measured only their bottom row and dropped the arch. + + Notable readings: the S.S. Anne's hull is `roof` (a drawing seen from + above, so the art belongs on the top face); the Warden's specimens and + Celadon Gym's shrubs are `cylinder` voxel balls, the first indoor use + of that class; the Fan Club's octagonal boardroom table is `counter` + rather than `table`, because only a counter rides its upper rows onto + the top face in drawn order -- which is what draws the seated chairman + exactly once, the Pokemon Center couch case verbatim. + + Fuchsia Gym's invisible maze is deliberately left flat. Raising it + would read better as a room, and would also hand the player the + solution; a shape is purely presentational, so the drawn answer wins + and the gym plays as the flat game does. + +### Fixed + +- **A profile pin now outranks the door fold.** `Structures.forMap` + folds a door cell into its facade so the doorway does not punch a hole + in the wall -- but it overwrote the resolved shape unconditionally, + including for AUTHORED tiles, which contradicts rule 1 of the + documented resolution order. Any pin on a tile its tileset also lists + in `doorTiles` was dead on arrival: all four Celadon Mansion + staircases and Pokemon Mansion 3F's descent are door tiles, so + `stair_*` pins there silently did nothing and the flights stayed + painted flat on the floor. The fold now skips authored tiles. + +## 1.0.4 + +### Added + +- **Viridian Forest grows real trees.** Nearly everything drawn in the + forest is ROUND, and the detector was boxing all of it: the big trees + came out as ragged mixed-height volumes (their sparse canopy-rim + tiles read 0px against 32px bodies, leaving gap-toothed hedge walls), + the stump rows merged into 16px crate walls wearing folded stump art, + and the trail signs were broken piles -- their $32 tile is the + water-fallback trap and recessed into a pond lip in the middle of the + woods. All of it is now profile-pinned to the treatments the rest of + the world already uses: every tile of the tree drawing (ball, rim + wisps, feet) and the stumps take the per-cell voxel HULL the overworld + border forest wears -- a tree spans 2x2 cells, so its four + quarter-hulls tile into one big lumpy canopy, and each stump becomes a + round bollard; the signs take the standing thin-slab `signpost` + treatment every town sign gets; and the white sparkle filler inside + the tree masses is flat ground instead of an invisible zero-height + box. The whole map now resolves to hulls, signs, ledges and ground -- + a detector sweep finds no stray boxed column anywhere. + + Two refinements over the first cut, both new hull-builder abilities. + A tree's drawing spans 2x2 CELLS, and per-cell hulls unfolded it onto + the ground -- the ball's top half sat one cell north of its bottom + half at the same elevation, reading as a tree cut in half. The new + `canopy` class pins the drawing's corner tile as a group anchor and + the whole 2x2-cell drawing carves as ONE 32px hull, so every tree is + a single tall round canopy (the carver is now parametric over its + canvas size, and hull stamps carry their footprint radius). And the + stumps' drawn tops are a CUT FACE -- an ellipse of growth rings seen + at an angle, not body: the new `stump` class builds the hull from the + bark rows alone and projects the ellipse across the round flat top, + near arc to the south (`stump_cap` names the ellipse's drawn height), + so the rings ride the round part in perspective. + +- **Flowers stand up, and keep swaying.** The animated meadow tile + ($03, the one tile the overworld animates by frame rewrite) now + renders as a billboard one voxel deep: the drawing's darkest tones + plus everything they enclose are cut out per pixel, and the ground + beneath is synthesized from the commonest flat neighbour, exactly + like the ground under a detected prop. + + The interesting part is that the cutout still animates. A mesh is + static, so the geometry spans the UNION of the mask over the base art + and all three animation frames, and the animation lives entirely in + the texture: TerrainAtlas already rewrites the flower's slot in the + private animated atlas each step, and for this tile it now writes + only the current frame's mask opaque with everything else keyed to + alpha 0 -- which the voxel shader discards, and the shadow pass with + it. The standing silhouette trims itself frame by frame in texture + space, off the same engine clock as the flat path, without a vertex + moving. The class is derived, not authored: any frames-animated tile + resolves to the new `flower` class with no profile entry, the same + way tall grass derives from `grassTile` (hand-authoring still wins). + +- **The cuttable bush is a standing cutout.** The four tiles Cut + deletes ($2D/$2E/$3D/$3E -- across the whole tileset they appear only + in the five cut-tree blocks) are pinned to the thin `prop` pool: a + per-pixel standee 5 voxels deep, black-outline segmented with its + enclosed pixels kept, the drawn grass dither flooding away. It + stands on plain grass ($2C) -- the very tile Cut leaves behind per + field.cutTreeSwaps -- via the profile's new `prop_ground` key, which + names the tile painted under a pinned prop instead of whatever flat + tile its neighbours vote in. + +- **Gym statues: a solid plinth, a standing bird.** The statue pair + flanking every badge gym's aisle (and Bruno's room) is one cell of + figure over one cell of plinth. The plinth ($22/$23/$32/$33) is + pinned `wall`: a solid 16px block. The figure ($02/$38/$12/$13) is + pinned `prop`: a 5-voxel cutout that stands ON the plinth through the + authored-box support rule, its checkered background flooded away and + the pixels its outline encloses kept. + + The whole statue keeps ONE cell of footprint. The support rule used + to extend the box under the claimed cell (the monitor-on-desk path), + which marched the plinth a second block backwards; a figure whose + support is a FULL-HEIGHT block now collapses instead -- the drawn + figure cell becomes synthesized floor, since the block below already + carries the whole base. Furniture supports keep the extension: their + drawn cell is the furniture's own upper rows, and floor there would + amputate the desk. The round boulder drawn beside some statues is + deliberately NOT pinned -- it also tiles wall-to-wall as Pewter's + rock rows, which are scenery for the detector. + +- **Lt. Surge's trash cans stand up.** The can ($0B/$0C/$1B/$1C, the + lone graphic of blocks 38/39) takes the same treatment as the + cuttable bush: a 5-voxel `prop` cutout, black-outline segmented with + its enclosed pixels kept, standing on the gyms' main floor tile + ($11) via `prop_ground`. + +- **The Poke Marts furnished to the Center's standard.** The MART + tileset shares the Center's atlas image but is its own id, so none + of the Center's pins applied, and every mart was raw detector + output: a 32px double-height display band for a back wall, the two + shelf racks fused into one four-tile-deep monolith, and the clerk's + booth towered into a 48px slab wearing the juice poster. Pinned the + way the finished interiors are -- the back wall's SALE cases and + drink fridges one 16px face like the Center's healing consoles, the + racks collapsed to one-cell-deep shelves at drawn height like Red's + bookcases, the counter half a cell with the poster riding its top + like the nurse's tray, and the cash register standing ON the counter + through the authored-box support rule. One 4x4 layout serves every + city, so this covers all eight marts. + +### Fixed + +- **Cut trees now vanish in voxel mode -- and grow back.** The + engine's Cut path swapped the block with a raw `setBlock` + renderer + rebuild, never emitting `world.block_replaced` -- so this mod's + listener (which rebuilds the map's mesh exactly for this) never + heard about it, and the diorama kept showing the tree. The engine + now routes Cut through `replaceBlock` + (src/world/OverworldController.lua), whose whole purpose -- per its + own comment, "Victory Road barriers, Cut trees" -- is that same swap + plus the event. The regrowth path had the same hole one door away: + cut trees are restored block by block when the map is re-entered, + and the card-key doors are stamped closed on floor load, both + through the same silent `setBlock` -- with the mesh cache staying + warm across a round trip (that is what prevLive is for), the world + kept showing the stump you left. Both paths now announce each block. + +- **A block edit no longer blinks the world down to 2D.** The + listener used to drop the edited map's mesh outright, and mesh + builds are asynchronous -- so cutting a tree (or stepping out of a + door onto a map whose trees just regrew) flashed the flat 2D world + for the frames the rebuild took. `ChunkMesher.refresh` rebuilds in + place instead: the stale mesh keeps drawing, the replacement cooks + in the background, and each slot swaps as its build lands -- the + tree pops out (or back in) with the scene never leaving 3D. + +- **Flowers cull the player correctly from every angle.** The flower + billboards were baked into the terrain mesh, which draws without + the characters' camera-ward pull -- so a walker standing among + flowers won the depth test against ALL of them, including the + flower south of their feet that should overdraw them. The flower + quads now ride their own mesh, drawn after the characters with + exactly the characters' pull (the tall-grass trick): the flower in + front of a walker occludes their feet, the one behind them hides, + at every camera angle. Unlike grass the flower mesh still casts + shadows -- it is a handful of cutouts per meadow, not thousands of + tufts. + +- The `voxel_anim_probe` driver crashed on engine builds without the + optional `TileRenderer.animFrame` seam, and again on tilesets whose + animation list carries a "toggle" entry (spinner rooms), which claims + a tile LIST rather than one slot. It now reads the clock through the + mod's own fallback chain and skips toggle entries in the placement + census. + +- **The shoreline no longer opens into the sky beside buildings and + signs.** Water recesses 2px below the ground, and the ground tile beside + it closes the step with a small below-ground side band -- but a tile + CLAIMED by a standing object (a building footprint, a sign standee, the + bushes ringing Fuchsia's ponds) only painted its synthesized flat ground + and never emitted sides. Along every stretch where such a tile met + water, the two-pixel step was an open slit straight through to the sky + behind the mesh. The skip branch now emits the same below-ground bands + ordinary ground does, cut from the synthesized ground's own art, so the + shoreline lip is continuous whatever stands on the bank. + +- **Edge-row buildings keep their facades.** The south wall of Saffron's + row houses -- profiled buildings whose front row is the map's last tile + row -- lies exactly on the boundary plane shared with Route 6, and the + prebuilt-quad keep rules dropped it: the strict body test excludes the + plane, and the closed neighbour mask (which exists to kill ring scraps + whose rects sit exactly on that line) swallowed what was left, so from + Route 6 the houses stood hollow. The two cases are geometrically + identical degenerate rects, but they FACE opposite ways: a face pointing + away from the body is this map's own facade and nothing in the + neighbour will ever draw that plane, while a face pointing into the + body is the scrap the mask is for. The mesher now reads the winding and + keeps outward faces on the body's boundary planes, on all four edges. + + The roof RIM had the same problem one step further out: an edge-row + house's eave overhangs `frontEave` voxels PAST the boundary plane into + the neighbour's airspace, and those quads are neither on the plane + (the winding rescue) nor over the body -- the neighbour-body mask ate + them as ring scraps, so from across the seam the roof edge was open + sky at low camera angles. Building placements only ever scan the map + BODY, so every building quad is this map's own structure by + construction: they now carry an `own` flag the edge keep-rules never + touch. + +- **Diglett's Cave mounds (and cliffs everywhere) stop sprouting + towers.** Two detector misreadings stacked up on the cave-entrance + mound. The dark east slope of the cliff drawing ($02/$24/$34) is one + texture repeated over the mound's whole height, but its corner tiles + break the repeat scan, so those columns rose to 32px -- the rock + pillar beside the entrance. And a folded doorway column reads its own + drawn extent (the door plus everything above it), which is a house's + real height when the door is a house's, but a 32px tower over a 16px + plateau when the door is a cave mouth -- the entrance jumped a block + above the mound around it. The slope chain is now profile-pinned to + one 16px course, and a doorway column answers to its REGION entirely: + height from the region's dominant column, top flat when those columns + are flat repeats (the mound) and roofed when they are drawn facades + (a house). Both cave entrances -- and every cliff built from the same + slope tiles -- now read as one level mesa with the cave mouth at + ground level. A new `voxel_mound_probe` driver prints the detector's + per-column class and height over any rectangle, which is how this was + diagnosed. + + Routes 3 and 4 had a third variant of the same misreading: the repeat + scan anchors at a column's FRONT tile, and a plateau column that ends + in a one-off rounded corner tile ($13/$35) never matched -- it read + its whole capped extent and shot up as a 48px fin (several together + made a tent). When the two rows directly above the front are + identical, the column is now read as that repeat wearing a trim foot: + its unit is one course plus the trim. Doorway columns still answer to + their region first, so houses are untouched. + ## 1.0.3 ### Added diff --git a/data/voxel_heights.lua b/data/voxel_heights.lua index 2757fa9..a3f6975 100644 --- a/data/voxel_heights.lua +++ b/data/voxel_heights.lua @@ -65,6 +65,11 @@ -- monitor on its desk). prop is a second pool -- so two touching cutouts stay separate -- +-- A tileset entry may also carry `prop_ground` (not a class): a map of +-- prop tile id -> ground tile id naming the tile painted under that +-- pinned prop, overriding the flat-neighbour vote -- the cuttable bush +-- stands on the grass Cut leaves behind, whatever borders it. +-- -- 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 @@ -106,7 +111,15 @@ return { -- the hop-down edges named by data.field.ledges' ledgeTile: their -- art is a ground lip seen from above, which the detector would -- otherwise raise as a wall - ledge = { 13, 29, 39, 54, 55 }, + -- $34 is the cliff slope's FOOT, and it also punctuates the ledge + -- rows: the hop-down runs are set into the cliff face and this + -- tile is the pillar between segments. It was `wall` with the + -- rest of the slope chain, which stood those pillars 16px beside + -- a 6px lip -- the ledge line came out interrupted by blocks + -- nearly three times its height. At ledge height the run reads + -- as one continuous lip, and the mound loses nothing: its foot + -- row simply reads as the talus it is drawn as. + ledge = { 13, 29, 39, 52, 54, 55 }, -- trees are drawn ROUND -- the lone canopy (42/43/58/59) and the -- border tree wall (64/65/80/81, blockset $0F). Boxes and per-pixel -- cutouts both read wrong for them; the cylinder archetype carves @@ -124,14 +137,409 @@ return { -- fence-textured tower. `post` extracts each cell alone, so these -- render as the same thin posts, marching north post = { 14, 85 }, + -- the cliff-mound's dark east slope and its NE corner ($24 the + -- slope column, $02 the corner). Their drawn runs span the whole + -- mound drawing, so the detector raised them to 32px towers -- + -- the rock pillar beside Diglett's Cave -- and the doorway + -- column, which adopts its REGION's height, inherited the same 32 + -- and put the cave entrance a block above the mound around it. + -- Pinned to one 16px course they match the plateau body, and the + -- doorway drops with them. ($34, the slope's foot, is `ledge` + -- instead -- see there.) + wall = { 2, 36 }, + + -- the cuttable bush ($2D/$2E/$3D/$3E, the four tiles Cut deletes + -- -- across the whole tileset they appear only in the five + -- cut-tree blocks): a standing per-pixel cutout 5 voxels deep, + -- black-outline segmented with the pixels the outline encloses + -- kept, its drawn grass dither flooding away as background + prop = { 45, 46, 61, 62 }, + -- the ground painted under those pinned props, by the prop tile's + -- own id: the bush stands on plain grass ($2C) -- the very tile + -- Cut leaves behind (field.cutTreeSwaps' after-blocks) -- rather + -- than whatever flat tile its neighbours vote + prop_ground = { [45] = 44, [46] = 44, [61] = 44, [62] = 44 }, }, - -- ledge pairs from data.field.tilePairs - CAVERN = { - ledge = { 5 }, + -- The badge gyms and Bruno's room (one tileset): the bird statues + -- that flank every gym's aisles, and Lt. Surge's trash cans. A + -- statue is one cell of figure ($02/$38/$12/$13) drawn over one + -- cell of plinth ($22/$23/$32/$33); the trash can is one cell + -- ($0B/$0C/$1B/$1C, blocks 38/39 only). Across the whole blockset + -- none of these tiles appears anywhere else. The plinth stays a + -- SOLID 16px block; the figure stands ON it as a per-pixel cutout + -- 5 voxels deep (the thin `prop` pool), collapsing to the plinth's + -- single cell of footprint (Structures' wall-support rule); the + -- can is the same 5-voxel cutout at floor level. Both are + -- black-outline segmented -- backgrounds flood away, the pixels + -- the outline encloses stay. Everything stands on the gyms' main + -- floor tile ($11), which is also Bruno's floor. + -- (The round boulder drawn beside some statues, $07/$08/$17/$18, + -- is NOT pinned: it also tiles wall-to-wall as Pewter's rock rows, + -- which are scenery for the detector, not a lone prop. Probed: + -- the repeat-aware scenery path already gives every one of them a + -- single 16px course in Pewter's and Bruno's rock walls alike, so + -- a pin would only restate the derived answer.) + -- + -- The rest of the tileset is the ten rooms' own furniture: the six + -- badge gyms it serves (Pewter, Cerulean, Vermilion, Celadon, + -- Fuchsia, Viridian) plus Bruno's and Lorelei's rooms, the + -- Champion's room and the Hall of Fame. (Saffron's and Cinnabar's + -- gyms are FACILITY, Agatha's room is CEMETERY and Lance's is DOJO, + -- so none of them takes a pin from here.) Same failure as every + -- other interior: the detector reads the black interior walls as + -- volumes measured off the whole drawn run, so Vermilion's side + -- walls and Viridian's north-south maze arms came out as 48px fins + -- with their black fill VOIDED flat between them, and the Hall of + -- Fame's recording machine merged into the wall band as a 32px slab + -- whose feet had been flattened away by the walkable-cell rule. + GYM = { + -- The wall band is ONE 16px face, whatever the artist drew into + -- it. The plinths (34/35/50/51 -- 50 is the $32 water-fallback + -- trap and would recess into a pond lip); the room's own band + -- ($05 white course, $10 trim, $06 the doorway's dark head); the + -- BLACK interior walls every gym is partitioned with -- $0F the + -- fill (near-black, so the void rule flattens it unless it is + -- AUTHORED, which is the whole reason it is listed), $24/$26/$27 + -- the top edge and its corners, $25/$35 the sides, $42/$3E the + -- west and east end caps of a horizontal arm and $10 again as its + -- lit south face. Then the things drawn built INTO those walls, + -- exactly like the Center's healing consoles: Vermilion's target + -- band ($44-$47 with $05 between, over the $54/$56/$57 stripe), + -- the barred gate ($20/$21/$30/$31 -- Lorelei's north door, and + -- the barrier VermilionGymScript paints in until both switches + -- are thrown), and Vermilion's corner switchboard ($29/$2A over + -- $0D/$0E and $1D/$1E), which is drawn 32px tall and so reads as + -- two stacked courses of the same band. + wall = { 34, 35, 50, 51, + 5, 6, 16, + 15, 36, 37, 38, 39, 53, 62, 66, + 68, 69, 70, 71, 84, 86, 87, + 32, 33, 48, 49, + 13, 14, 29, 30, 41, 42 }, + -- Fuchsia's invisible maze walls ($1F) are deliberately NOT here. + -- The artist drew them as the floor with its stripes broken: on + -- the GB they are invisible, and the puzzle IS walking into them. + -- Raising them to a course would read better as a room and give + -- the player something to bump into -- and would also hand them + -- the solution, turning a maze into a corridor. A shape is + -- purely presentational and this file's job is to reproduce what + -- was drawn, so the drawn answer wins: left to the derived path, + -- $1F stays the floor it is painted as and the gym plays exactly + -- as the flat game does. + -- Cerulean's pools are the ONE place the $14 water-fallback trap + -- is right: $14 really is the gym's water, so it is left to fall + -- through to the engine's water set and recess. Its north coping + -- ($04, the grated lip) is drawn in the TOP half of a water cell, + -- so the cell rule dragged it down to the water line with it; + -- pinned flat it stays the poolside at floor level and the water + -- shows a real lip. The pool's south lip ($01) needs nothing -- + -- it is always the top half of a walkable FLOOR cell, so rule 3 + -- already lays it flat. Lorelei's room uses the same pair for + -- the paved plaza that stands out of her water. + ground = { 4 }, + -- Celadon's hedge: a round shrub, one per cell ($2C/$2D over + -- $2E/$2F), 35 of them planted wall to wall. Round drawings get + -- the cylinder archetype -- one voxel ball per 16x16 cell carved + -- from the darkest-pixel outline -- the same treatment the + -- overworld's tree canopies take, and per-CELL, so a hedge row + -- becomes a row of bushes instead of one boxed monolith. + cylinder = { 44, 45, 46, 47 }, + -- The statues, the cans, and Celadon's three little trees + -- ($40/$41 canopy over $50/$51 trunk). A trunk is not round, so + -- the tree cannot be a ball like the shrubs beside it -- it takes + -- the thin standee pool every interior plant takes, which is also + -- a pool apart from the cylinders it touches. + prop = { 2, 56, 18, 19, 11, 12, 27, 28, + 64, 65, 80, 81 }, + -- The Hall of Fame's recording machine, the one piece of real + -- furniture in the tileset. It is drawn 32px wide and THREE tile + -- rows tall against the north band, and the detector made a mess + -- of it twice over: the top two rows merged into the wall band as + -- one 32px slab, and the base row -- whose cell is walkable, + -- because the Hall's floor tile ($19) is its own bottom-left -- + -- was flattened into the floor, so the machine stood on nothing. + -- Split at the drawn seam: the plinth row ($58/$59/$5A) is a + -- half-cell counter, and the console above it ($5B-$5E over + -- $36/$37/$55/$5F) is a standing per-pixel billboard that rides + -- the plinth's top face through the authored-box support rule. + -- 8 + 16 is exactly the 24px the machine is drawn. + counter = { 88, 89, 90 }, + billboard = { 91, 92, 93, 94, 54, 55, 85, 95 }, + -- the ground painted under each pinned prop, by the prop tile's + -- own id: statues and cans stand on the gyms' main floor ($11), + -- Celadon's trees on the garden's light dither ($2B) rather than + -- on whatever their neighbours vote. + -- (Celadon's flowerbed tile $03 is deliberately absent: the + -- tileset ANIMATES it by frame rewrite, so TileShape derives the + -- `flower` pin for it already -- flat ground plus a standing + -- 1px cutout -- and listing it here would take that away. The + -- floors need nothing either. $11/$16/$2B and Viridian's arrow + -- and dot markings $3C/$3D/$3F/$4C/$4D are in the walkable list + -- outright; the paved slabs the Hall of Fame is floored with and + -- Lorelei's plaza is built of ($09/$0A over $19/$1A) get there + -- through the CELL: only $19 is listed, but it is the cell's + -- bottom-left, so rule 3 lays all four of them flat together. + -- Cerulean uses the very same slab as its poolside deck, which is + -- why that gym's perimeter is a deck and not a wall -- the room's + -- only wall band is the one drawn course along its north edge.) + prop_ground = { [2] = 17, [56] = 17, [18] = 17, [19] = 17, + [11] = 17, [12] = 17, [27] = 17, [28] = 17, + [64] = 43, [65] = 43, [80] = 43, [81] = 43 }, }, + + -- The caves: one blockset serves nineteen maps -- Mt Moon's three + -- floors, Rock Tunnel's two, Seafoam Islands' five, Victory Road's + -- three, Cerulean Cave's three, and Diglett's Cave with its two + -- route-side stubs. + -- + -- THE CAVE'S VERTICAL SCHEME. A Gen 1 cave is drawn on two floor + -- levels, and the game's own collision data says which is which. + -- data.field.tilePairs forbids stepping between $20/$21/$2A/$41 and + -- $05 on land, and between $14 and $05 while SURFING -- but nothing + -- forbids $14 <-> $20. Read that back and the elevation falls out: + -- the dark floor and the water are one plane, the lit floor is a + -- step above both, and the rock is the room around them. + -- + -- h what tiles + -- 0 DARK LOWER FLOOR -- the datum $20 $21 $2A + -- 0 CAVE WATER, the surfable pool $14 + -- 0 fall-through drop hole $2F $22 + -- 0 unlit black (the void rule, no pin) $3C + -- 3 Victory Road's boulder switch plate $2B $2C $2D $2E + -- 6 LIT SHELF, one step up (`ledge`) $05 $29 + -- 6 the stair plate down off that shelf $15 $16 + -- 16 ROCK -- one band for the masses, the everything else + -- faces, the caps and the ceiling mass + -- 0 up 16 ladder shaft UP a floor $0A $0B $1A $1B + -- 0 dn 16 ladder shaft DOWN a floor $08 $09 $18 $19 + -- + -- NO CAVE SURFACE SITS BELOW 0. The datum is the dark floor; the + -- water is level with it (you surf off a pool straight onto the dark + -- floor, which is exactly what the missing $14/$20 tile pair says); + -- every other surface is above it. A cave is underground, not + -- underwater, and the only class in this file that goes negative is + -- `water` (-2), which exists to cut the overworld's shoreline lip. + -- It is deliberately NOT used anywhere in this entry. The one thing + -- that does reach below the datum is the DOWN ladder's stairwell, + -- which is a hole on purpose: it is the shaft to the floor below, + -- and no walkable surface is drawn in it. + -- + -- What the detector made of it: the rock EDGES towered. A shelf's + -- side is drawn as a cap over a run of face tiles over a corner + -- pair -- $04 / $31 x5 / $28 / $10 down one column -- and the volume + -- builder's repeat scan anchors on the column's FRONT tile, which + -- here is a one-off corner that never recurs above it. Neither the + -- repeat read nor the trim rule (two identical rows directly above + -- the front) fires, so the column keeps its whole extent and caps at + -- MAX_ROWS: a 48px fin out of the 16px band beside it, while the + -- column one tile west -- ending on $28 with two $31 above -- reads + -- 16. Probed before pinning: MT_MOON_B2F tiles (5,4)-(5,11) and + -- (17,8)-(17,15) at 48 with 16 either side; VICTORY_ROAD_2F + -- (25,26)-(25,31); SEAFOAM_ISLANDS_B4F (22,0)-(22,7), + -- (13,12)-(13,19) and (12,13)-(12,19). The same reading turns a + -- column of four DIFFERENT rock tiles into 32px -- VICTORY_ROAD_1F + -- (28,20)-(29,23) and (30,20)-(31,23), $02/$12 over $0C/$1C, next + -- to identical rock at 16. An authored tile never reaches the + -- volume builder, so pinning the rock is the whole fix: there is no + -- run left to misread. + CAVERN = { + -- ---- 16: the rock, one band ---- + -- + -- Reading across the blockset: $02/$03 over $12/$13 is the + -- speckled rock mass and $0C/$0D over $1C/$1D the boulder-cluster + -- mass, both solid wall-to-wall fills; $10/$11 is the cobble + -- course facing the south side of every lit shelf, $31 its west + -- face and $17 its east; $04/$07/$28 are the caps and $25/$26 the + -- inner corners where two faces meet. $0E/$0F over $1E/$1F is the + -- barred rock shelf that stands alone on the dark floor + -- (MT_MOON_1F, ROCK_TUNNEL_1F and twice in SEAFOAM_ISLANDS_B4F): + -- one cell of face-on drawing, so one 16px face -- never a + -- standee, its black surround touches all four rims and would + -- segment into a solid slab. + -- + -- $06/$27 over $24/$01 is the CEILING MASS: a pale dithered rock + -- fill with a black rim dithered along its north and south edges, + -- so a run of it tiles into the grid of light blocks that walls + -- off most of MT_MOON_B2F (37 cells) and stands as one lump in + -- each of VICTORY_ROAD_1F/2F/3F. It is blocked in every map that + -- places it ($24 is not on the walkable list) and it is NOT in the + -- engine's water set, so it is rock and belongs in the band with + -- the rest of the rock. The first pass read the checker as a pond + -- and pinned it `water`: that recessed 612 tiles to -2 and cut + -- open trenches down the middle of Mt Moon B2F -- 06w-2 27w-2 / + -- 24w-2 01w-2 for ten cells straight, hard against 31w16 rock and + -- the 05l06 shelf. Its unpinned reading was 48px (eight rows of + -- alternating $06/$24 read as one drawing); 16 is the answer. + wall = { 2, 3, 18, 19, -- $02/$03 over $12/$13, speckled mass + 12, 13, 28, 29, -- $0C/$0D over $1C/$1D, boulder mass + 16, 17, -- $10/$11, the shelf's south cobbles + 49, 23, -- $31 west face, $17 east face + 4, 7, 40, -- $04 NW cap, $07 NE cap, $28 SW cap + 37, 38, -- $25/$26, the inner corners + 14, 15, 30, 31, -- $0E/$0F over $1E/$1F, barred shelf + 6, 39, 36, 1 }, -- $06/$27 over $24/$01, ceiling mass + -- ---- 6: the lit shelf, and the stairs off it ---- + -- + -- $05 is the lit floor -- the shipped pin, and the step + -- data.field.tilePairs encodes ($20/$21/$2A/$41 <-> $05). $29 is + -- the SAME surface: the artist's shadow row where the shelf runs + -- up against the rock above it. It sits in walkable cells, so + -- unpinned it resolved to flat ground and cut a 6px trench along + -- the north edge of every lit room (`29g00` with `05l06` one tile + -- south, all the way across MT_MOON_B2F). + -- + -- $15/$16 is the STAIR down off that shelf, and the elevation it + -- spans is 6px, not 16. Its art is four treads seen from above, + -- each a light tread over a black riser line, stacked NORTH-SOUTH: + -- every one of the 54 stair cells in the tileset has the lit floor + -- $05 to its NORTH and the low ground to its SOUTH (dark floor x35, + -- water x14, lit floor x5 where two flights meet), so the flight + -- climbs NORTHWARD, one cell deep, 0 -> 6. + -- + -- It is NOT pinned `stair_e`/`stair_w`, and that is deliberate. + -- Those classes build a flight that marches along X -- 16px tall, + -- rising toward the named side -- so either of them here would + -- throw a 16px staircase sideways across a 6px north-south step, + -- blocking the passage it is supposed to open and climbing at + -- right angles to the drawn risers. A wrong-way flight is worse + -- than a flat one. `ledge` is the honest reading available: the + -- stair cell joins the shelf it belongs to, wears its four treads + -- on the TOP face (which is how they are drawn -- from above), and + -- puts its 6px riser face at the FOOT of the flight where the + -- player actually steps down onto the dark floor. See the report: + -- a real sloped cave stair wants `stair_n`/`stair_s` in + -- Structures.stairCell, which is an engine change, not a pin. + ledge = { 5, 41, -- $05 lit floor, $29 its north shading + 21, 22 }, -- $15/$16, the stair plate off it + -- ---- 0: the floor plane ---- + -- + -- The dark lower floor, pinned rather than left derived so the + -- datum this whole entry measures from is stated in it. $20 is + -- the rough floor, $21 the bright checker, $2A the fine checker; + -- all three are on the tileset's walkable list and all three + -- already resolved here -- the pins restate the level, they do not + -- move it. + -- + -- $14 IS THE CAVE WATER: the tileset animates by TILEANIM_WATER, + -- and TileRenderer's WATER_TILE is $14 -- it is the tile that + -- h-shifts to ripple. Map's water set names it too, so its cells + -- are the ones Surf works on: Cerulean Cave's lake (352 + 272 + -- tiles on 1F/B1F) and the Seafoam channels (304 on B3F, 816 on + -- B4F). The first pass read its blocky white dither as "bright + -- rubble mass" and pinned it `wall`, which stood both lakes up as + -- 16px rock -- two thirds of SEAFOAM_ISLANDS_B4F was a rock slab + -- you were meant to surf across. It is water, and it lies at the + -- floor plane: `ground` keeps the drawn (and animated) water art + -- exactly as it is and puts its surface at 0, level with the dark + -- floor you step off onto, one 6px step below the lit shelf you + -- cannot surf onto. NOT the `water` class -- that is -2, a + -- shoreline lip for the overworld sea, and a cave pool must never + -- read as a trough sunk into the floor you are walking on. + -- + -- $2F over $22 is Seafoam's and Victory Road's fall-through drop + -- hole (Map's warpPadTiles calls $22 a "hole"). Its art is a lit + -- rim over solid black and its cell is walkable -- you step on it + -- and drop a floor -- so it stays flat at the floor plane; the pin + -- keeps the rim out of the wall fallback. + ground = { 32, 33, 42, -- $20/$21/$2A, the dark lower floor + 20, -- $14, the surfable cave water + 47, 34 }, -- $2F over $22, the drop hole + -- ---- 0 to 16: the ladders, the caves' real staircases ---- + -- + -- Both ladder graphics are drawn from above as a shaft with two + -- rails and rungs between them, and which one is which is not a + -- guess -- it is in the warp table. Across all nineteen maps, + -- every one of the 37 $08 cells warps to a LOWER floor + -- (MT_MOON_1F->B1F, B1F->B2F, ROCK_TUNNEL_1F->B1F, each + -- SEAFOAM_ISLANDS floor to the next one down, CERULEAN_CAVE_2F->1F + -- and 1F->B1F, the Diglett's route stubs down into the cave) and + -- every one of the 40 $0A cells warps to a HIGHER one (the exact + -- reverse, plus MT_MOON_B1F's and ROCK_TUNNEL_1F's exits back out + -- to daylight). $08 is the ladder DOWN, $0A the ladder UP: 77 + -- cells, no exceptions. + -- + -- So they take the two classes that MOVE between floors. $0A + -- becomes a rising flight -- four real steps climbing 0 -> 16, the + -- height of the rock band it disappears into -- and $08 becomes an + -- excavated stairwell, four steps descending below the floor into + -- a dark opening. East for both: the ladder art is symmetric + -- about its own centre line so the drawing does not choose a side, + -- the cell's east neighbour is the closed (rock) side more often + -- than its west, and the rest of this profile's staircases + -- (REDS_HOUSE_1, UNDERGROUND) climb east too. The flight's treads + -- land one rung apiece, which is as close to a drawn ladder as + -- stepped geometry gets. + -- + -- All four tiles of each cell are listed, the way REDS_HOUSE_1 + -- lists its flight: buildStairs anchors on the cell's TOP-LEFT + -- tile ($08 / $0A) and claims the other three, but pinning them + -- too keeps them out of the region flood and the door fold. + stair_e = { 10, 11, 26, 27 }, -- $0A/$0B over $1A/$1B, UP + stair_down_e = { 8, 9, 24, 25 }, -- $08/$09 over $18/$19, DOWN + -- ---- 3: the boulder switches ---- + -- + -- Victory Road's four plates ($2B/$2C over $2D/$2E, on 1F, 2F and + -- 3F). Round art, but NOT a round object: the Strength boulders + -- themselves are overworld SPRITES, and this is the plate they get + -- pushed onto -- drawn from straight above as a black ring, a + -- white highlight rim and a grey disc. `cylinder` was tried here + -- first, because a circle is what the overworld's round canopies + -- take, and it came out wrong: the plate's own dithered background + -- carries stray black pixels the darkest-outline flood cannot + -- reach, so they joined the mask, inflated the top and bottom row + -- spans to the full 16, and the hull rendered as a clutch of white + -- shards standing where the plate should be (VICTORY_ROAD_1F cell + -- 17,13). `relief` is the reading anything drawn from above + -- wants: the disc stays flat and the pixels inside its black ring + -- lift a few voxels, so it reads as a plate -- and the cell is + -- walkable, so you step on it. + relief = { 43, 44, 45, 46 }, + -- Left to the derived default on purpose: $3C is solid black -- + -- the unlit rock that is most of MT_MOON_B1F -- and the void rule + -- flattens it (`3Cv00`). Pinning it would BREAK that, since the + -- void rule skips authored tiles; a 16px black band there would + -- also wall the corridors in with something the 2D art draws as + -- nothing at all. $30 and $41 are on the walkable list and in + -- tilePairs but no block in this tileset places either, so there + -- is nothing for a pin to catch. + }, + -- Viridian Forest. Nearly everything drawn here is ROUND, and the + -- detector was boxing all of it: the big trees came out as ragged + -- mixed-height volumes (their sparse canopy-edge tiles read 0px, + -- their bodies 32px), the stump rows merged into 16px crate walls + -- wearing folded stump art, and the trail sign was a broken pile -- + -- its $32 tile is the water-fallback trap and recessed into a pond + -- lip in the middle of the woods. FOREST = { + -- the hop-down edge, from data.field.tilePairs (CAVERN's entry + -- above carries the same note for its own pair) ledge = { 46 }, + -- the big trees: the whole 2x2-CELL drawing carves as ONE 32px + -- voxel hull, so a tree is a single tall canopy instead of four + -- ground-level quarter-pancakes ("cut in half", as the first + -- attempt read). `canopy` pins the drawing's top-left corner + -- tile ($04) as the group anchor; every other tile of the + -- drawing stays `cylinder` so the group build can verify and + -- claim its cells -- and so a stray partial drawing degrades to + -- per-cell hulls rather than boxes + canopy = { 4 }, + cylinder = { 5, 6, 7, 21, 22, 23, + 35, 36, 37, 38, 39, 53, 54 }, + -- the stumps ($02/$03/$12/$13): a hull whose drawn top is a CUT + -- FACE. The body builds from the bark rows alone, and the drawn + -- ellipse of growth rings projects onto the hull's round flat + -- top -- its bottom arc to the south, the way the 2D art means + -- it. stump_cap is the ellipse's height in art rows + stump = { 2, 3, 18, 19 }, + stump_cap = 7, + -- the white sparkle filler inside the tree masses: flat, not an + -- invisible zero-height box + ground = { 0 }, + -- the trail signs: the standing thin-slab treatment every town + -- sign gets ($32 pinned is also what lifts it out of the water + -- trap) + signpost = { 33, 34, 49, 50 }, }, -- Oak's Lab (the tileset also serves the Fighting Dojo and Lance's @@ -309,6 +717,1373 @@ return { -- accepted trade for a real plant silhouette prop = { 32, 33, 34, 35, 48, 49, 50, 51 }, }, + + -- The Poke Marts (one 4x4 layout serves every city; the tileset + -- shares the Center's atlas image but is its own id, so the pins + -- above do not carry over). Same failures as everywhere indoors: + -- the detector towered the whole north display band to 32px, merged + -- the clerk's booth into a 48px slab wearing the juice poster, and + -- boxed the two free-standing shelf racks into one 4-tile-deep + -- monolith. + MART = { + -- the wall band stays one 16px face: the trim and case tops + -- (40/90/91), the SALE cases (78/79) with their feet (23/29), and + -- the glass drink fridges (44-47) with their feet (62/63) -- all + -- drawn built INTO the back wall, exactly like the Center's + -- healing consoles + wall = { 23, 29, 40, 44, 45, 46, 47, 62, 63, 78, 79, 90, 91 }, + -- the clerk's counter, half a cell high like every service + -- counter. It is a C wrapping the alcove the clerk stands in: the + -- south arm's drawn front (24/25) under its top band (8/56), the + -- light work surface the east arm and the rest of the south arm + -- share (16/41), and the north arm's end panel (89). 8 and 56 are + -- the two halves of one top band -- the first pass read 8 as the + -- cash register, pinned it `billboard`, and got a bare black + -- outline standing on the counter with the tile's own art replaced + -- by its neighbour's. + counter = { 8, 16, 24, 25, 41, 56, 89 }, + -- The CASH REGISTER (14/15 over 30/31), a keypad and a curl of + -- receipt paper drawn face-on across two tile rows in the middle of + -- the counter's east arm. Pinned `counter` it was just paint on + -- the work surface. In the `billboard` pool it is a standing + -- per-pixel cutout ten voxels deep -- the flower-and-vase treatment + -- of Red's house, but with a machine's body rather than a stem -- + -- and the support rule lifts it onto the counter automatically, + -- because the tile drawn directly BELOW it (16/41) is a pinned 8px + -- box. Its own pool, so the counter can never absorb it. The art + -- has a clean light margin on three sides and a black outline all + -- round, so the shade flood drains the work surface away and leaves + -- the machine whole (verified pixel by pixel before shooting). + billboard = { 14, 15, 30, 31 }, + -- the free-standing shelf racks: TALL drawings, not deep ones -- + -- each rank collapses onto a one-cell-deep shelf at its drawn + -- height (the Dojo/Red's-house treatment). 64/65/67 and 80/81/83 + -- are the bottle rows the clerk's booth also wears as its top + -- display; 68/69/71 and 84/85/87 the goods rows below + bookcase = { 64, 65, 67, 68, 69, 71, 80, 81, 83, 84, 85, 87 }, + -- Left to the derived default on purpose: the floor checker and + -- its shadowed variants (1/11/17/26/27/54) and the exit mat + -- (12/28) all sit in cells the ROM marks walkable, so the cell + -- rule lays them flat unaided; 76/77 is the SALE case's black + -- interior, which the volume path recesses half a course into the + -- band -- a dark display niche, which is what it is drawn as. + -- NOT covered: INDIGO_PLATEAU_LOBBY, the ninth map on this id and + -- the only one that is not the 4x4 shop. It draws its own hall, + -- lift bank and rope stanchions from tiles no Mart places, three + -- of which fall in the engine's stale water set -- see + -- reports/COUNTERS_pass2.md. Pinning them is a tileset pass of + -- its own and is deliberately not attempted here. + }, + + -- The route gates and their upstairs lounges, the four Underground + -- Path entrance huts, the Safari Zone gate and its four rest houses + -- -- twenty-five maps on one tileset. (Viridian Forest's two gates + -- draw the very same art from a SEPARATE id, FOREST_GATE below, and + -- pins do not carry between ids.) Before this entry every one of + -- them was a swimming pool: 50/51 -- the dark panel the artist reuses + -- as the wall's base course AND as every counter's front -- is one of + -- the three ids (20/50/72, the $14/$32/$48 of the stale cache) that + -- the engine's water set claims in every tileset, so the whole wall + -- perimeter and every counter front recessed into a moat. The other + -- two are placed here as well: 20 is the exit doormat, 72 the top + -- course of Route 2's wall. What survived the water was towered + -- instead -- the counters raised to a 16px slab, the cabinets boxed, + -- the 2F window read as a 16-to-40px skyline, and the tables and + -- binoculars flattened into the floor because their cells are + -- walkable. + GATE = { + -- The wall band stays ONE 16px face. Three wall drawings share + -- it: the route gates' panelled course (32/33 -- its base course + -- 50/51 is `counter`, see below), the + -- Underground Path huts' and Route 22's plain stripes (0), and + -- Route 2's speckled plaster (72 over its dark skirting 74). 0 + -- and 74 need no height correction but are pinned anyway, so the + -- detector cannot swallow the corner plants into the band and + -- tower them with it. The gate 2F's big window is the same band + -- two cells deep (58 glass, 45 its dither, 61 the diagonal glare, + -- 62 the black mullions). The door panes (41/43, the leaf drawn + -- above the walkable threshold) are wall too, so a doorway reads + -- as a recess in the band instead of a picture of a door lying on + -- the floor -- the Center's warp-tile treatment, and pins are + -- look-only so the cell stays walkable. + -- + -- 50/51 is NOT in that list, and that is the one deliberate cost in + -- this entry. The artist draws one panel twice: as the wall cell's + -- base course (32/33 over 50/51) and as the front of every counter + -- (7/9/8 or 17/18 over 50/51). A pin is per tile id and a class + -- carries one height, so the two uses cannot be separated here at + -- all -- only in `lib`, by letting a pin be conditional on the tile + -- drawn ABOVE (see reports/COUNTERS_pass2.md). Pinned `wall` (16) + -- the band is one clean face and the counter the guard stands + -- behind is a full 16px slab that hides him to the shoulders. + -- Pinned `counter` (8) every counter is the half cell it is drawn + -- as -- which is what the room is FOR -- and the price is that the + -- wall cell's front 8px of depth drops with it: a shallow wall + -- gains an 8px plinth (reads as a wainscot), and the deep margin + -- masses that run two to eight cells north-south down the east and + -- west sides of the fourteen maps that place both drawings (Routes + -- 5/6/7/8, the five gate 1Fs, the Safari gate and its four rest + -- houses) corrugate, their exposed flank stepping 16/8/16/8 every + -- 8px of depth. The other six -- Route 2, Route 22 and the four + -- Underground Path huts -- place 50/51 and never 32/33, so they + -- take the half cell for nothing. The counters win where it costs: + -- they are the object the player walks up to and talks over, they + -- sit mid-room, and the margin masses are pure border filler seen + -- edge-on. This also puts GATE and FOREST_GATE on the same footing + -- at last -- ROUTE_2_GATE draws the forest gates' room tile for + -- tile and used to be the only one of the three with a 16px front. + wall = { 0, 32, 33, 41, 43, 45, 58, 61, 62, 72, 74 }, + -- the counter, half a cell high everywhere it is drawn: its north + -- rim and end caps (7/8), the long run between them (9), the top + -- surface a counter drawn running north-south wears (23/24), and + -- the drawn front panel (50/51) standing up as one clean 8px band + -- with the surface riding the top face. The guard leans on it + -- instead of standing behind a wall stub, and a counter that runs + -- wall to wall (Route 12's pair) matches one that ends in the open. + counter = { 7, 8, 9, 23, 24, 50, 51 }, + -- 50/51 draws BOTH the counter's front and the wall's base course, + -- and it is the bottom row of its cell either way -- so a flat pin + -- has to pick one and be wrong about the other. Pinned `wall` the + -- counters stood a full 16px; pinned `counter` the deep border + -- banks corrugated 16/8 for sixteen rows and the room read as + -- crates. What tells the two uses apart is what is drawn ABOVE: + -- the wall stacks its panelled course (32/33) over the base, while + -- a counter carries its own top (7/8/9). So the counter pin above + -- is the default and this puts the wall back wherever the panel + -- sits on top of it -- resolved per position, in TileShape.at. + when_above = { + [50] = { { above = { 32 }, class = "wall" } }, + [51] = { { above = { 33 }, class = "wall" } }, + }, + -- the tall glass-fronted cabinets: three shelf ranks (34/35) + -- folded up a 24px box whose top face adopts the drawn top rim + -- (7/8, pinned `counter` directly above it) through the mesher's + -- authored-fold rule -- the HOUSE / Red's-house bookcase + -- treatment. Deliberately not `bookcase`: that collapses the + -- drawing to one cell of depth, which would leave the wall cell + -- behind each cabinet as bare floor, and its cap rule refuses a + -- row pinned anything but `table`, so the cabinets would lose + -- their tops as well. + desk = { 34, 35 }, + -- standing per-pixel props with body, black-outline segmented: the + -- little tables of the 2F lounges and the Safari rest houses (2/3 + -- the top, 18/19 the legs) and the 2F observation binoculars (36 + -- eyepieces, 52 barrels, 57 pedestal). Both are drawn face-on + -- with air between their legs and a clean white floor margin, so + -- the standee segments whole -- and both were invisible before, + -- flattened by the walkable-cell rule. They stand on the floor: + -- the window band is drawn ABOVE them, and the support rule only + -- lifts a prop drawn above a box. + billboard = { 2, 3, 18, 19, 36, 52, 57 }, + -- the plants, in the THIN pool like every other interior plant: + -- the potted palm that stands in every hut and rest-house corner + -- (5/6 crown, 21/22 fronds, 37/38 over 53/54 the pot), and Route + -- 22's wall plant (14/15 crown, 30/31 pot), which stands on the + -- half-cell planter its base course (50/51) makes -- a planter box + -- now rather than the 16px pedestal it used to perch on. + prop = { 5, 6, 14, 15, 21, 22, 30, 31, 37, 38, 53, 54 }, + -- the exit doormat (4 over 20): drawn from straight above, so it + -- has to lie flat -- and 20 is the $14 water-fallback trap, which + -- sank every gate's own doorway into a pond lip. + ground = { 4, 20 }, + -- the flight up to a gate's 2F: real steps climbing east, the way + -- the drawn treads rise to the right. Pixel for pixel the same + -- staircase as Red's house 1F, on the same four tile ids. + stair_e = { 12, 13, 28, 29 }, + -- the flight down -- to 1F from a gate 2F, into the tunnel from an + -- Underground Path hut: a sunken stairwell descending westward, + -- high at the east lip where the player enters. Again the same + -- drawing and the same ids as Red's room upstairs. + stair_down_w = { 10, 11, 26, 27 }, + -- Left to the derived default on purpose: the two floor checkers + -- (1/17) and every threshold -- the door mats (55/56/94) and the + -- tile under the door panels (42/59) -- sit in walkable cells, so + -- the cell rule already lays them flat; and tile 16 is solid + -- black, which the void rule flattens into exactly the darkness + -- that should sit above Route 16's north wall. + }, + + -- Viridian Forest's north and south gates. Tile for tile they are + -- the same drawing as ROUTE_2_GATE, but they are their own tileset + -- id, so they need their own copy of the pins. These two maps never + -- place the panelled wall course (32/33), so 50/51 is a counter + -- front here and nothing else -- the half cell it is drawn as costs + -- this id nothing at all, where GATE pays for it in the wall (see + -- the note there). Both ids run 50/51 `counter` now, so the three + -- rooms that share this drawing -- the two forest gates and + -- ROUTE_2_GATE -- finally match. Before this entry the room was a + -- pond with an island: 72 ($48) sank the north wall's top course, 50 + -- ($32) sank all four counter fronts, and 20 ($14) sank the exit + -- doormat -- the three stale-cache water ids, all three placed in + -- one small room. + FOREST_GATE = { + -- the wall band, one 16px face: the speckled plaster course (72) + -- over its dark skirting (74), plus the door pane (41/43) drawn + -- above the walkable threshold, so the doorway reads as a recess + -- in the band rather than a door lying flat on the floor (pins are + -- look-only; the cell stays walkable). 72 is the $48 + -- water-fallback trap and took the whole north wall down with it. + wall = { 41, 43, 72, 74 }, + -- the four counters, half a cell high: end caps (7/8), the top + -- surface running north-south (23/24), and the front panel (50/51) + -- standing up as one clean 8px band with the surface riding the + -- top face. 50 is the $32 water-fallback trap -- unpinned, every + -- counter's front cell was a pond notch. + counter = { 7, 8, 23, 24, 50, 51 }, + -- the potted palms down both side walls, three to a side, in the + -- THIN standee pool like every other interior plant: 5/6 crown, + -- 21/22 fronds, 37/38 over 53/54 the pot. They stack with no gap + -- between them, and each still segments as its own plant. + prop = { 5, 6, 21, 22, 37, 38, 53, 54 }, + -- the exit doormat (4 over 20), drawn from straight above so it + -- lies flat; 20 is the $14 water-fallback trap. + ground = { 4, 20 }, + -- Left to the derived default on purpose: the two floor checkers + -- (1/17) and the tile under the door panes (42/59) sit in walkable + -- cells, so the cell rule already lays them flat. + }, + + -- Celadon's department store and the rooms that share its blockset: + -- CELADON_MART_1F..5F and the ROOF, the two 2x2 lift cabins + -- (CELADON_MART_ELEVATOR, SILPH_CO_ELEVATOR), ROCKET_HIDEOUT_ELEVATOR, + -- the GAME_CORNER with its PRIZE_ROOM, and CELADON_DINER -- twelve + -- maps off one atlas. Everything the store owns was wrong: the + -- U-shaped sales counters read their whole north-south arm as one + -- drawing and towered to 48px (dragging the wall band they touch up + -- with them), the merchandise racks fused sideways into 32px + -- monoliths FOUR tiles deep -- the Poke Mart's failure exactly -- + -- the diner and roof stools sit in walkable cells and so flattened + -- into the floor entirely, and three separate tiles fell into the + -- engine's stale water set and sank into ponds indoors. + LOBBY = { + -- The two exit cells of every map (the sliding-door threshold, 4 + -- over 20). 20 is $14, which the stale-cache fallback counts as + -- water in EVERY tileset, and rule 2 judges the whole CELL by it: + -- both tiles of the doorway recessed into a pond lip just inside + -- the shop door. It is a flat mat, and the lift cabins use the + -- same pair for their car door. + ground = { 4, 20 }, + -- The wall band stays ONE 16px face. The blank course (1) and the + -- skirting course below it (33) are the band itself; everything + -- else here is drawn BUILT INTO it at 16px, so wall height is the + -- drawn height and each reads as a fitting jutting from the band + -- rather than a tower: + -- 6/22 the framed notice beside the stairwell, every floor + -- 2/3/18/19 the pair of pay phones on 1F + -- 46/47 the floor-directory plaque (it also sits at the east + -- end of 1F's counter top, where 16px reads as a sign + -- board standing on the counter -- the one placement + -- that is not wall, and the least bad of the two) + -- 62/63 the lift call-button panel in both 2x2 cabins + -- 72/73/88/89 the framed picture (72 is $48, a water-set id, so + -- unpinned its top half sank) + -- 68/84 the roof's perimeter safety railing, drawn 16px tall + -- 75/76/77/78/79 the roof parapet and the Rocket lift's cabin + -- walls (79 also rings the roof map as its border block); + -- the Game Corner reuses 75/76/77/78 as the dark south + -- end of its machine banks, where 16px reads as the + -- bank's end cabinet + -- 91 the dithered wall course above the Prize Room counter + -- and the roof stairhead + -- 92/93 the Rocket lift's horizontal cabin frame + -- 70/71 NOT wall panelling: the round tables' lower-left and + -- lower-right rim ($46/$47). They are here to lift the + -- terrace and diner tabletops to the one height their + -- middle can be built at -- see the long round-table + -- note under `counter`. The tileset places them in the + -- two table blocks (29, 49) and nowhere else, so 16px + -- costs nothing anywhere in the group. + 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 }, + -- 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 + -- thing Red's living room stands up. Six placements: four on the + -- two goods counters, two against the east wall band. As `wall` + -- each was a 16px CUBE with the TV art folded onto one face, so the + -- counters wore grey filing cabinets and the TV shop read as a + -- stockroom. The standee pool cuts the drawing per pixel at 10 + -- voxels of body instead, and the support rule does the rest: the + -- four counter sets are drawn directly above an authored 8px + -- `counter` row, so they STAND ON the counter and the tiles they + -- claim keep rendering as counter top rather than holing it. (The + -- art carries a full black frame with light margin inside it, so + -- the segmentation flood has nothing to drain and the whole set + -- survives.) Its own pool, away from `stool`, so a TV never stacks + -- with a chair drawn beside it. + billboard = { 14, 15, 30, 31 }, + -- Every service counter in the group, half a cell high like the + -- Center's and the Mart's. One 8px band means the drawn front row + -- stands up and everything above it rides the TOP face in drawn + -- order -- which is what a counter arm running north-south IS: the + -- rows above the front are its top surface seen from above, not + -- height. Unpinned the arms measured their full 7-row run and + -- towered to 48px, taking the wall band they touch with them. + -- 38/39/41 the top surface and its end caps + -- 21/48/49 the south front panel and its end caps + -- 54/57 the arms' left and right side edges + -- 36/37/52/53 the games console and its controller laid out on + -- 3F's counter. This is pixel for pixel the drawing + -- Red's house pins `relief` -- a prop seen from + -- ABOVE -- and `relief` is the class it wants. It + -- cannot have it here: Structures.buildRelief anchors + -- its extrusion at y=0 and has no support lookup, + -- while every placement of this drawing in the + -- tileset is on a counter TOP. Pinned `relief` the + -- cell is claimed, dropped to floor level and punched + -- a 16x16 hole clean through the counter (verified + -- in-engine, pass-2 report). `counter` leaves the + -- console exactly where relief would put it -- flat, + -- flush with the surface, art on the top face, drawn + -- once -- and gives up only relief's three voxels of + -- lift. The report carries the one-place engine + -- change that would free it. + -- 9/25/85/86/87 the round tables of the diner and the roof + -- terrace: their north rim (9/25 = $09/$19) and the + -- pedestal course at the south (85/86/87). + -- + -- THE ROUND TABLES in full, because the shape is a compromise. The + -- drawing (block 29, and the same four rows split across blocks 45 + -- and 49 in the diner) is + -- $09 $27 $27 $19 an octagonal top seen from above, with + -- $36 $37 $37 $39 a pedestal drawn below its southern + -- $46 $37 $37 $47 rim + -- $55 $56 $57 $37 + -- Its four INTERIOR tiles are $37, which is ALSO the light half of + -- the checkerboard floor and the plain upper wall of the Prize Room + -- and the roof stairhead, so $37 cannot be pinned at all (see the + -- closing note). Left to the detector those four are a two-row + -- column, which buildVolume reads as a repeat, floors at unit 2 and + -- builds at 16px -- and with the whole rim pinned `counter` at 8px + -- the terrace tables came out as grey CUBES sitting on trays. + -- + -- So stop fighting the 16px and let it BE the tabletop: 70/71 + -- ($46/$47, the lower-left and lower-right rim) move to `wall`, and + -- the table's whole front course then stands at the same 16px its + -- middle already does -- one flat disc, the drawn octagon on its + -- top face and the lower arc folded down its front. What stays at + -- 8px is the FAR rim (9/25, and the shared 39/54/57) and the + -- pedestal (85/86/87): the far rim is occluded by the 16px top in + -- front of it, and the pedestal is meant to sit low. 16px is also + -- the right height against the 8px `stool` chairs drawn around it + -- -- a terrace table you sit at, not a footstool. + counter = { 9, 21, 25, 36, 37, 38, 39, 41, 48, 49, 52, 53, 54, + 57, 85, 86, 87 }, + -- Free-standing merchandise racks and glass cases: TALL drawings, + -- not deep ones. The four-row rack (42-45 / 58-61 / 64-67 / + -- 80-83) stands two racks abreast on floors 2F-5F and along the + -- Game Corner's north wall, and the detector boxed each pair into + -- ONE 32px slab four tiles deep -- the Poke Mart's exact failure. + -- A bookcase rank collapses onto a one-cell-deep box at the full + -- drawn height instead, and the back rows become hidden floor, so + -- racks standing side by side stay separate objects with an aisle + -- behind them. + -- 34/35/50/51 is the glass display case / vending machine, which + -- the same rule fits at BOTH its drawn sizes: 16px where it wears + -- the 1F phones or the 3F TV above it, 24px where a counter cap + -- (38/41) tops it on the roof, the Prize Room and the Game + -- Corner. 50 is $32, a water-set id, so every one of these cases + -- stood in a pond before it was pinned. + bookcase = { 34, 35, 42, 43, 44, 45, 50, 51, 58, 59, 60, 61, + 64, 65, 66, 67, 80, 81, 82, 83 }, + -- The stools: the diner's chairs, the roof terrace's, the six + -- rows in front of the Game Corner's machine banks and the four + -- on 1F. Their cell's bottom-left tile is 23 ($17), which IS on + -- the tileset's walkable list, so rule 3 resolved every one of + -- them to flat ground and they vanished into the floor + -- completely. A pin is the only thing that overrides that. The + -- drawing carries a full black outline with the floor dither + -- showing at all four corners, so the standee segments cleanly -- + -- and `stool` keeps its own pool, apart from anything it touches. + stool = { 7, 8, 23, 24 }, + -- Deliberately NOT pinned: + -- $37 (55) is three different things -- the light half of the + -- checkerboard floor, the interior of the round tables, and + -- the plain upper wall of the Prize Room and the roof + -- stairhead -- and every class that suits one ruins the + -- others: `ground` holes the wall and the tabletop, `counter` + -- turns every second floor tile into an 8px lump, `wall` turns + -- it into a 16px pillar. The cell rules already answer the + -- floor (its cells are walkable through their $45 bottom-left + -- tile) and the wall (its cells are not), and the tabletop is + -- answered above by raising 70/71 to meet the 16px the + -- detector builds there. Left unpinned on purpose. + -- $10 (16) is pure black and the void rule already flattens it + -- to the interior darkness above the Game Corner's wall. + -- The stair openings (10/11/26/27, 40/56) and the lift doors + -- (12/13/28/29) all sit in WALKABLE cells and fold into the + -- facade on their own, which is what the doorway fold is for. + -- $20/$45 (32/69) are the two floors and are walkable. + }, + + -- Pewter's museum, both floors (the tileset shares the gates' atlas + -- image but is its own id). The exhibits were the worst case in the + -- whole building: 50 is $32, a water-set id, and it is the base row + -- of EVERY display case, so rule 2 judged each case's bottom cell + -- water and sank the fossil vitrines, the space shuttle and the + -- glass cabinets into ponds. 72 is $48 and is the wall band's upper + -- course, so the north wall recessed too -- leaving its lower course + -- alone as an 8px stub. On top of that the benches sat in walkable + -- cells and flattened away, the corner plants towered to 32px, and + -- both staircases (also walkable) were flat floor. + MUSEUM = { + -- the two street doors on 1F: the same $14 mat trap as the + -- department store's, flat + ground = { 4, 20 }, + -- the wall band as one 16px face: the dithered upper course (72, + -- the $48 water-set id) over the skirting course (74), and the + -- two framed paintings hung in it (78/79) + wall = { 72, 74, 78, 79 }, + -- The low partitions that divide both floors -- the tileset's own + -- counterTiles name 23 ($17) -- drawn exactly like the department + -- store's counter arms: a light top surface seen from above with + -- dark side edges. Half a cell, so they read as barriers you see + -- over rather than interior walls. Their north caps and their + -- south base rows belong to the case group below, at the same 8px. + counter = { 23, 24 }, + -- Every vitrine in the museum, as tall drawings one cell deep: + -- 7/8/34/35 the run of eight glass cabinets along 1F's north + -- wall (7/8 is the cabinet's top surface, so the + -- rank wears it as its top face -- which is why it + -- is here and not in `counter` with the partitions + -- whose north cap it also draws) + -- 9/50/51 the low case at 1F's centre (9 the top surface, + -- 50/51 the base panels) and the base row under the + -- partition ends + -- 25/39/40/44/46/48/49/58/60/63/70/71/83 the fossil vitrine, + -- four drawn rows inside one black frame: the dark + -- top surface, the ammonite specimens on their + -- light bed, the placard, and the base. Two on 1F, + -- one on 2F. Collapsed to one cell of depth it + -- stands as a 32px case instead of a 16px lid + -- floating over a pond. + -- 64-69/80-91 the space-shuttle vitrine on 2F, the same drawing + -- shape three cells wide. + -- 50/51 is $32/$33 -- the water-set id that sank all of them. + bookcase = { 7, 8, 9, 25, 34, 35, 39, 40, 44, 46, 48, 49, 50, + 51, 58, 60, 63, 64, 65, 66, 67, 68, 69, 70, 71, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }, + -- the visitors' benches on 1F: their cell's bottom-left tile is + -- 18 ($12), which is on the walkable list, so they resolved to + -- flat ground and disappeared. Seat-height standees in their own + -- pool, like the department store's stools + stool = { 2, 3, 18, 19 }, + -- the corner potted plants (crown 5/6/21/22, pot 37/38/53/54): + -- the THIN standee pool, like every other interior plant in this + -- profile. Unpinned they were a 32px two-cell tower. + prop = { 5, 6, 21, 22, 37, 38, 53, 54 }, + -- the Old Amber in its rounded case, drawn face-on with a clean + -- black outline and a light margin at every corner: the standee + -- with body, segmented so the backing floods away and the amber + -- inside the outline stays + billboard = { 76, 77, 92, 93 }, + -- 1F's staircase UP to 2F: a real rising flight climbing east, + -- the way the treads are drawn (low at the west, high at the + -- east). Its cell is walkable, so without the pin it was flat + -- floor with the stair art painted on it. + stair_e = { 12, 13, 28, 29 }, + -- 2F's staircase DOWN to 1F: the same flight from above, so it + -- descends westward -- the player steps on at the east lip. Same + -- tile ids Red's bedroom uses for its stairwell, which is the + -- convention across Gen 1's interior blocksets. + stair_down_w = { 10, 11, 26, 27 }, + -- Deliberately NOT pinned: 1 and 17 ($01/$11), the two halves of + -- the checkerboard floor. Every floor cell's bottom-left tile is + -- $01, which is walkable, so the cell rule already makes the whole + -- cell flat ground. + }, + + -- Cinnabar Lab and its three side rooms (trade, metronome, fossil), + -- the Fuchsia Warden's house, the Fuchsia meeting room and the Safari + -- Zone's secret house -- seven maps on one tileset. Every indoor + -- failure mode shows up here at once: the detector raised the machine + -- banks as 32px monoliths TWO cells deep, boxed the free-standing + -- shelf racks the same way (and CAPPED the metronome room's racks at + -- 16px because their book rows repeat, so a 32px shelf came out a + -- 4-tile-deep 16px slab), cut every lab bench into a patchwork of + -- 8/16/32px stubs, flattened the chairs into floor, merged the + -- meeting room's long partition with the corner plants into a 48px + -- tower -- and dropped six tile ids into the engine's stale-cache + -- water set, so the bench tops ($04/$05/$14/$15), the table's monitor + -- ($48) and the Warden's round specimens ($32) recessed into pond + -- lips inside the building. Oak's Lab (the DOJO entry) is the + -- reference: same rooms, same answers. + LAB = { + -- the wall band stays one 16px face. Blank courses ($22), the + -- pillar/corner that turns Cinnabar Lab's L ($23), the framed + -- Pokeball picture (16/17/32/33), the metronome room's row of + -- wall brackets (48/49 -- drawn INTO a band that is two cells + -- deep there, so both cell rows stay one 16px face), the vent + -- panel over Cinnabar Lab's three doorways (24/25), and the low + -- dark base course with its end caps (42/43) that the meeting + -- room's partition and the Warden's machine both stand on. + -- The Warden's big machine (87/88/89 top, 28/71/29 the dial row, + -- 36/37 the grille row, over 24/25 and the 42-34-34-43 base) is + -- ALSO wall: it is drawn 32px tall and free-standing, so a + -- `bookcase` would be the honest shape -- but its grille tiles + -- 24/25 are the very tiles Cinnabar Lab hangs in its wall band + -- above the doors, and its base row is the shared $22 band, so no + -- per-tile pin can make it 32px without either stubbing the Lab's + -- wall panel or splitting the machine into columns of different + -- heights. One clean 16px console beats both. + wall = { 16, 17, 24, 25, 28, 29, 32, 33, 34, 35, 36, 37, + 42, 43, 48, 49, 71, 87, 88, 89 }, + -- the equipment banks and the shelf racks: TALL drawings, not deep + -- ones. Each rank collapses onto a ONE-CELL-DEEP box at its full + -- drawn height and its back rows become hidden floor -- the + -- Dojo/Mart treatment, and the only thing that stops the fossil + -- room's and the secret house's machine walls from being 32px + -- blocks two cells thick. 69/70 + 85/86 are the cabinet tops, + -- 10/11 the dial faces, 74/75 + 90/91 the narrow filler columns, + -- 26/27 the plinth with its feet, 59 the black end pilaster; + -- 40/41 are the metronome room's and the meeting room's shelf + -- ranks. + -- + -- 64/65/66 -- the TOP-TRIM row (one black line, one white + -- highlight, then grey) that every flat-topped unit in this + -- tileset wears -- are here too, and this is the load-bearing + -- decision in the entry. They used to be `table`. A `table` is + -- an authored upright BOX, and the support rule stands a pinned + -- standee drawn directly above an authored box ON it: every chair + -- on the FAR side of a table in this tileset is drawn directly + -- above that table's trim row, so the trade room's two north + -- chairs and the meeting room's spare chair were lifted onto the + -- tabletops -- and their cells were re-tiled as tabletop, marching + -- each table two tile rows north to meet them. A `bookcase` is + -- authored but is NOT a box (its art mode is the rank collapse), + -- so the support rule does not fire and those three chairs stand + -- on the floor where they are drawn. + -- Nothing else about the trim changes for the worse. Where it + -- caps a shelf rank (64/66 over 40/41 or 10/11, in the metronome + -- room, the meeting room, the fossil room and the secret house) + -- it simply becomes the rank's fourth row: the same 32px height + -- and the same top-face art the cap-adoption rule used to hand it, + -- minus the 12px stub the trim used to render as BEHIND the shelf. + -- Where it tops a table or the Warden's bench it becomes that + -- unit's own 8px back rim, tucked behind the 12px top. + -- (The one thing it costs is named in the report: the meeting + -- room's two partitions used to hand this trim to the 16px band's + -- TOP face -- the mesher gives a full-height course the authored + -- UPRIGHT row above it -- and now top with their own band art.) + bookcase = { 10, 11, 26, 27, 40, 41, 59, 64, 65, 66, 69, 70, + 74, 75, 85, 86, 90, 91 }, + -- the lab furniture, at table height. The benches in the fossil + -- and metronome rooms (2/3 the specimen tray, 4/5 + 20/21 the + -- microscope, 18/19 the tray's rack row) and the big lab tables + -- (80/81/82 the top, 83/58/84 the front rail and its legs). + -- 72/73 -- the monitor and the ball lying on the table -- ride the + -- table's authored height rather than standing as separate + -- cutouts, exactly as Oak's starter balls do; pinning them also + -- stops the display frame's black corner brackets from + -- auto-extracting into standing prisms. Four of these ids + -- ($04/$05/$14/$15) plus $48 are the tileset's water-fallback + -- traps: unpinned they sink into a pond lip in the middle of the + -- room. With the trim row moved to `bookcase` the top face of + -- each table now wears its own drawn surface instead of the trim + -- -- which is how the Warden's bench finally shows the monitor and + -- the Pokeball the artist drew on it. + table = { 2, 3, 4, 5, 18, 19, 20, 21, 58, 72, 73, + 80, 81, 82, 83, 84 }, + -- seats, the 8px standee pool, all of them in WALKABLE cells and + -- therefore flat floor until pinned: the four chairs round the + -- trade room's table and the meeting room's spare (14/15/30/31, + -- a whole cell each) and the little stool tucked under every lab + -- bench (6/7 its top edge, 22/23 its seat and legs). 6/7 also + -- carry the bench's apron along their top edge -- the standee + -- wears a thin dark cap for it, which is cheaper than dropping + -- the stool to a half-cell drawing with no floor margin. + -- The chairs on the far side of a table stay on the FLOOR only + -- because the trim row above them is no longer a box; see the + -- bookcase group. + stool = { 6, 7, 14, 15, 22, 23, 30, 31 }, + -- the tall corner plants: two cells of drawing (44/45 + 60/61 the + -- frond crown, 46/47 + 62/63 the stem and pot), mostly silhouette, + -- so the THIN standee pool -- as in every other interior. The + -- detector had them at 0/8/16/24/32px depending on which corner + -- they stood in, and in the meeting room it fused them into the + -- partition and towered the pair to 48px. + prop = { 44, 45, 46, 47, 60, 61, 62, 63 }, + -- the five round specimens in the Warden's house: one 16x16 cell + -- each, drawn ROUND with a dithered shell and a lit underside. + -- Neither a box nor a flat cutout reads them; the round archetype + -- carves a voxel ball per cell from the drawing's darkest-pixel + -- outline, which is what they are. 50 is a water-fallback trap, + -- so unpinned the row sat in five pond lips. + cylinder = { 50, 51, 67, 68 }, + -- Deliberately NOT pinned: the checker floor (1/38) and the fossil + -- room's striped carpet (12/13) are walkable cells and already + -- flat; the doormat (39/55) likewise; the three doorways + -- (76/77 over 52/53) are walkable and the engine folds them into + -- the facade; Cinnabar Lab's black surround (54) is authored void + -- by the void rule; and the bench aprons 8/9 stay flat floor, + -- which is where the artist drew them -- inside the walkable cell + -- in front of the bench. + }, + + -- Celadon Mansion's three floors and its roof, plus the Celadon + -- chief's house -- five maps on one tileset. The mansion maps are + -- half interior and half OUTSIDE: the east third of every floor is + -- the open-air stair landing, so the entry has to keep an outdoor + -- strip flat while it furnishes the rooms. The detector towered the + -- display cabinets, boxed the potted palms into 32px 4-tile-deep + -- monoliths, raised the 2F larder cabinet as a 48px tower taller than + -- the walls, gave the same wall band three different heights + -- (8/16/48) on one map -- and sank the whole cabinet bank AND both + -- front-door mats into pond lips, because nine tile ids here + -- ($04/$14/$22/$23/$32/$33/$38/$48/$57) sit in the engine's + -- stale-cache water set. + MANSION = { + -- one 16px face for every wall course, indoors and out: the rooms' + -- north band (80), the interior partition (30) with its white top + -- rim (76/77), the doorway lintel over the front door (22/23), the + -- east exterior wall the landing runs along (74/75) with its + -- junction pieces (72/73 -- 72 is a water trap -- and 88/89), the + -- band's feet and corners (90/91/92/93), and the roof's own rim + -- and corner blocks (78/79). The roof parapet railing (15 over + -- 31, with end caps 42/43 and 33/49) is drawn a full 16px tall, so + -- it stays in the same band rather than dropping to `fence`: on an + -- open roof a knee-high rail reads as a kerb. 48 and 6/7 are the + -- rooftop shed's base course. + wall = { 6, 7, 15, 22, 23, 30, 31, 33, 42, 43, 48, 49, 72, 73, + 74, 75, 76, 77, 78, 79, 80, 88, 89, 90, 91, 92, 93 }, + -- the display cabinets along the north wall of 1F and the chief's + -- house: TALL drawings, not deep ones, so each rank collapses onto + -- a one-cell-deep box at its drawn height. 34/35 are the shelf + -- row of the short cabinets, 40/21 + 56/87 the two rows of the + -- tall ones (the pair with the trophy in the glass), 50/51 the + -- plinth. Their top row is 38/41, pinned `table` below because + -- the mansion's big tables wear the same trim -- the bookcase + -- builder adopts it as the rank's CAP, which lands the short + -- cabinets at 24px and the tall ones at 32px, exactly their drawn + -- heights. 45/46 + 61/62 + 63/47 is 2F's larder cabinet, four + -- drawn rows standing free of any wall: 32px and one cell deep + -- instead of the 48px tower the detector built. + -- Six of these ids are water-fallback traps ($22/$23/$32/$33/$38 + -- /$57) -- the whole cabinet bank was a pond. + bookcase = { 21, 34, 35, 40, 45, 46, 47, 50, 51, 56, 61, 62, 63, + 87 }, + -- table height. The long tables in 1F and the chief's house + -- (38/39/41 top edge, 54/55/57 body, 60/58/59 the front rail with + -- its two legs) and the writing desks on 2F and 3F (36/37/52/53 + -- the back edge with the papers and the ball on it, 64/65/66/67 + -- the body). The desks' aprons (2/3/85/86 over 18/19) are drawn + -- into the WALKABLE cell in front and stay flat floor, which is + -- how the artist placed them. + -- Known compromise: the rooftop shed on CELADON_MANSION_ROOF is + -- drawn with the table's own top and body tiles (38/39/41 over + -- five rows of 54/55/57) with a door punched in its base, so it + -- comes out a 12px block rather than a shed. Nothing towers there + -- any more -- the detector had its east columns at 48px -- and no + -- per-tile pin can give one drawing two heights. + table = { 36, 37, 38, 39, 41, 52, 53, 54, 55, 57, 58, 59, 60, + 64, 65, 66, 67 }, + -- the stool at the 2F/3F desk: a whole cell of drawing (2/3 over + -- 18/19) sitting in a WALKABLE cell, so flat floor until pinned. + -- The 8px standee pool, seat height, as in Red's rooms. + stool = { 2, 3, 18, 19 }, + -- the potted palms: two cells of drawing (68/69 crown, 8/9 fronds, + -- 70/71 stem, 24/25 pot), mostly silhouette, so the THIN standee + -- pool -- the same numbers the generic HOUSE entry uses, and the + -- same answer every interior plant gets. The detector had them as + -- 32px boxes four tiles deep. + prop = { 8, 9, 24, 25, 68, 69, 70, 71 }, + -- the front-door mats of 1F and the chief's house. Their bottom + -- tile is $14, which the engine's stale-cache fallback counts as + -- water in every tileset, so the mat -- and the whole cell with it + -- -- recessed into a pond lip just inside the door. It is a flat + -- rug on the floor. + ground = { 4, 20 }, + -- Deliberately NOT pinned: the interior checker floor (17), the + -- landing's open-air ground (1), the black surround (16, authored + -- void), the back and rooftop doors (81-84, walkable and folded + -- into their facade), the desk aprons (85/86), and the landing + -- edge (5). + -- CANNOT be pinned, engine-side: the four staircases (12/13/28/29 + -- the rising flight, 10/11/26/27 the sunken one) are in this + -- tileset's doorTiles, and Structures' door fold OVERWRITES the + -- resolved shape of any door cell whose north neighbour is upright + -- -- without checking `authored` -- so a stair_e / stair_down_w pin + -- is silently discarded. The indoor flights therefore read as + -- doorways folded into the north wall band, which is at least what + -- they are (you walk into them from the room). The roof's two + -- flights, which stand clear of any wall, come out as a shallow + -- 8px lip -- a stairwell mouth, near enough -- and pinning them + -- `wall` would be worse: it would plug the openings with a 16px + -- block in the middle of the roof. See the report. + }, + + -- Silph Co's 11th floor (the president's office), Bill's house and + -- the Pokemon Fan Club -- three rooms that share one tileset and + -- almost no furniture. The detector raised Silph's side walls as + -- 48px towers (the drawn band is three cell rows deep, so it + -- measured the whole column), split the Fan Club's wall band across + -- 0/8/16px AND a pond lip, boxed Bill's two transporter drums into + -- 48px slabs five tiles deep, and left every chair flat because they + -- sit in walkable cells. Six ids here ($14/$1F/$32/$34/$40/$48) sit + -- in the engine's stale-cache water set -- including $1F, the main + -- FLOOR tile, which sank wherever it shared a cell with $48. + INTERIOR = { + -- the wall band, one 16px face throughout: the rooms' face-on + -- courses (52 upper, 68 lower; 16 in Bill's house and Silph's + -- outer band), the courses seen from above along the south and the + -- partitions (87 top, 88 bottom), the side walls (89/90), the end + -- caps and junction blocks (45/46), the framed Pokemon portraits + -- hung in the band (19/20 over 35/36), and the padded bench built + -- into the Fan Club's north wall (49/50/51 -- its lower half, + -- 65/66/67, is drawn into the walkable cell in front and stays + -- flat, the way the artist drew it). + -- 93/94 are Silph's CARD KEY door: the block only exists while the + -- door is locked (data.field.cardKey swaps it out), so it never + -- appears in maps.lua -- but the probe sees it, and unpinned it + -- came out 0px/8px in the middle of a 16px wall. + -- Bill's pipework belongs here too: the stubs either side of each + -- drum (32/48, 37/53) and the run between them (38/54), all drawn + -- 16px tall against the band. + wall = { 16, 19, 20, 32, 35, 36, 37, 38, 45, 46, 48, 49, 50, 51, + 52, 53, 54, 68, 87, 88, 89, 90, 93, 94 }, + -- Bill's two transporter drums: a TALL drawing (four rows of + -- cylinder plus a base course), not a deep one. The bookcase + -- collapse gives each drum one cell of depth at its full 32px + -- height instead of the 48px, five-tile-deep slab the detector + -- built, and its top face wears the drum's own lid. 7/8/9/10 is + -- the lid, 23/24/25/26 and 39/40/41/42 the barrel, 55/56/57/58 the + -- footing. + bookcase = { 7, 8, 9, 10, 23, 24, 25, 26, 39, 40, 41, 42, + 55, 56, 57, 58 }, + -- The big OCTAGONAL boardroom table -- the Fan Club's and Silph + -- 11F's, the same drawing in both -- half a cell high, and half a + -- cell on purpose. A `counter` is ONE band: exactly the drawing's + -- bottom row stands up as the front and every row above it rides + -- the top face IN DRAWN ORDER. For a table drawn in PLAN that is + -- the only class that reproduces the plan; a taller box wears its + -- NORTH row's art across the whole top face and smears it. + -- + -- The octagon itself is cut at TILE granularity, which is the + -- finest the mesher has: an authored tile is its own 8x8 column, + -- so the bevel is drawn by choosing which tiles rise. Reading the + -- Fan Club rows (Silph is the identical shape eight rows further + -- down the map): + -- row 4 72 74 74 74 74 73 6 tiles wide + -- rows 5-8 72 <----- 8 tiles -----> 73 8 tiles wide + -- row 9 63 64 64 64 64 81 6 tiles wide + -- row 10 75 76 76 75 4 tiles wide + -- 72/73 are the north chamfer, 63/81 the south one, 77/78 the + -- straight flanks, 74 the north rim and 64 the top itself. 75/76 + -- -- the front apron with its two drawn legs -- is pinned so the + -- octagon's south edge closes and the legs stand on the front face + -- instead of lying painted on the floor; it sits in the walkable + -- cell in front of the table, which is the cell a player stands in + -- to talk across it, and an 8px apron there reads as standing AT + -- the table (the same arrangement as the Center's counter). + -- 79 and 82, the OUTER corner of the south bevel, are `ground` -- + -- see the ground group. + -- + -- 91/92, the statue's pedestal, ride the top face here rather than + -- standing with the statue: those two ids are ALSO the wall band's + -- base blocks where a side wall meets the south course in Silph, + -- so six tiles at the foot of Silph's walls sit 8px instead of + -- 16px. It is a notch at the foot of a wall the player never + -- faces, and the alternatives are worse in both directions -- a + -- 16px stub standing in the middle of the boardroom table, or a + -- paper cutout standing at the foot of every south wall. + -- 40 and 48 are water-fallback traps: unpinned, the table's whole + -- north-west cell was a pond, and it dragged the floor tile $1F + -- sharing that cell down with it. + counter = { 63, 64, 72, 73, 74, 75, 76, 77, 78, 81, 91, 92 }, + -- The Pokemon statue standing in the middle of both boardroom + -- tables: a per-pixel cutout, ONE voxel deep, standing ON the + -- table through the authored-box support rule -- a standee drawn + -- directly above an authored box rises from that box's top, which + -- is how the gym's bird statues stand on their plinths and the + -- potted plant stands on Red's table. 17/18 is the crown and the + -- head, 33/34 the face and the top rim of the pedestal it stands + -- in; the pedestal proper (91/92) stays `counter` under its feet. + -- The four claimed tiles keep rendering as the tabletop they were + -- cut out of, so the table is whole underneath. + -- `cutout` and not `prop`: the drawing has NO floor margin -- it + -- is surrounded on all four sides by the tabletop's own mid grey + -- -- and the cutout pool's stricter contract makes mid shades + -- background unconditionally, so the tabletop drains away cleanly + -- while the black outline, the paint whites and everything they + -- enclose survive. Pinned `counter` with the rest of the table + -- (what it was) the statue was painted FLAT on the tabletop. + cutout = { 17, 18, 33, 34 }, + -- Bill's desk and the Silph president's, at table height: 11-14 + -- the surface with the terminal and the ball on it, 27-30 the + -- front edge. Their aprons (43/44 + 91/92 on the row below) are + -- drawn into the WALKABLE cell in front; 43/44 belong to the stool + -- there, not to the desk. + table = { 11, 12, 13, 14, 27, 28, 29, 30 }, + -- 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. + 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 + -- water test runs per CELL on the engine's own stale set, BEFORE + -- any neighbouring pin can help, so the floor tiles sharing the + -- Fan Club's and Silph's north-west table cell with $48 sank into + -- the pond with it. A pin is the only rule that outranks it. + -- 1/2 are the curve of Bill's drums where they meet the floor, + -- drawn below the pinned barrel and flat where they lie. + -- 79/82 are the OUTER corner of the octagon's south bevel, one + -- tile beyond 63/81 on each side. Raised with the rest of the + -- table they squared both bottom corners off and left an 8px stub + -- standing in the floor a tile away from the table on each side; + -- flat they are what they are drawn as -- the table's cast shadow + -- where the bevel meets the floor -- and the octagon closes with a + -- clean 45 degree step on all four corners. + ground = { 1, 2, 31, 79, 82 }, + -- Deliberately NOT pinned: the doormats (70/71) and Silph's warp + -- pads, stairwells and elevator mouth (3/4, 5/6/21/22, 83-86) are + -- walkable and already flat; 47 is the black surround outside the + -- building's footprint and the void rule authors it; 69 is the + -- shadow course under Bill's drums, drawn into a walkable cell and + -- correct flat. + }, + + -- The Cable Club rooms and the Bike Shop (one tileset, three maps). + -- COLOSSEUM and TRADE_CENTER are the same 5x4 room with a different + -- machine in the middle and a different board on the wall; BIKE_SHOP + -- reuses the same wall, floor and counter for a showroom full of + -- bicycles. Between them the three maps place every tile. + -- + -- What was wrong: the Bike Shop's bicycles came out at 0 and 8 -- + -- each bike sliced in half at ankle height with its top row missing, + -- and the leftmost rack fused into the wall and towered to 32. The + -- shop's whole L-shaped counter stood at wall height, a 16px slab + -- you could not see over. Both Cable Club rooms lost their stools + -- (a walkable cell, so they flattened to floor) and their machine + -- came out 0/8/16 in the same object. And the Game Boy link poster + -- above the door has $32 in its bottom-left corner, so the poster + -- had a pond in it -- the shore-set trap, and TRADE_CENTER's link + -- table carries the other one, $48. + CLUB = { + -- The wall band stays ONE 16px face. 6 is the striped panel, + -- 52 the fluted pillar that breaks it every three cells (its + -- rounded foot, 53, stands one row lower on the floor and is + -- walkable, so it stays flat ground in front of the band). + -- Drawn INTO the band and therefore wall as well: the two + -- bicycles hung on the Bike Shop's wall (1/2/3 over 17/18/19, + -- exactly two tile rows = exactly the band's height), the Cable + -- Club's link poster (48/49/50/51 -- 50 is $32, the shore trap + -- that had it recessing to -2), and the Trade Center's pair of + -- lit notice boards (63/68/68/69 over 66/67/67/70). + wall = { 1, 2, 3, 6, 17, 18, 19, 48, 49, 50, 51, 52, 63, 66, 67, + 68, 69, 70 }, + -- Counters, half a cell high -- the house rule for anything you + -- lean on. The Bike Shop's L: the till run's top (7/8, the + -- tileset's own counterTiles) with its inside corner (54), east + -- arm (16) and end cap (5), all over the front panel 23/24. 8px + -- is one clean band, so the drawn front stands up and the counter + -- top stays a top; at 12 or 16 it reads as a wall stub. + -- 71/72/73/74 is the TRADE_CENTER link table, which shares that + -- same 23/24 front: its surface is drawn from ABOVE (two Game + -- Boys lying on it, cables between), so it belongs riding the top + -- face, not standing up. 72 is $48, the second shore trap. + counter = { 5, 7, 8, 16, 23, 24, 54, 71, 72, 73, 74 }, + -- The Colosseum's battle machine: a console drawn face-on, four + -- tiles wide and three rows tall, grilled side towers (59/62 and + -- 55/58), a dark screen bay (60/61, 56/57) and the two white + -- horns that crown it (64/65). 24px is its drawn height, so + -- `desk` folds the whole drawing upright as one machine instead + -- of the 0/8/16 rubble the detector left. + desk = { 55, 56, 57, 58, 59, 60, 61, 62, 64, 65 }, + -- The two stools that flank each Cable Club machine: seat 42/43 + -- over base 38/39, in every one of the four placements across + -- both rooms and nowhere else in the tileset. Their cells are + -- walkable, so without a pin they were floor; at seat height a + -- character standing on one sits on it. + stool = { 38, 39, 42, 43 }, + -- The showroom bicycles -- six of them standing on the Bike Shop + -- floor, in two columns, each drawn 3 tiles by 2 side-on. The + -- THIN standee pool, not `bookcase`: a bike is nearly all air + -- (two wheel rings and a frame), and collapsing a rack of them + -- onto a one-cell-deep box at full height would trade six + -- bicycles for one blank 32px panel wearing a bicycle print. As + -- cutouts they segment cleanly -- the drawing has floor margin on + -- three sides and its own black outline seals the wheels -- and + -- the standee builder's connected-component split gives each bike + -- its own feet in its own cell even where a lower bike's + -- handlebar stem touches the wheel of the one drawn above it. + -- 11/12/14 the saddle and bar row, 27/28/9 the frame and wheels. + prop = { 9, 11, 12, 14, 27, 28 }, + -- The crate and the standing pump beside the shop's south wall + -- (29/13 over 21/22), twice. A separate pool from the bicycles + -- on purpose, and the thicker one: a crate is a deliberate object + -- with a body, where a bike is a silhouette. + billboard = { 13, 21, 22, 29 }, + -- The Bike Shop floor's second checker phase. It is NOT in the + -- tileset's walkable list, so the cells it floors resolve by rule + -- 4 and it rose as a wall stub -- or, worse, got swept into the + -- bicycle beside it and dragged to 8px. It is floor; it is flat. + ground = { 30 }, + -- Left to the derived default on purpose: + -- $0F $1F the main floor checker -> walkable, ground + -- $0A $1A the dark slab under the Bike Shop's exit warp -> a + -- threshold mat, walkable, flat + -- $35 the pillar's rounded foot -> its cell is walkable + -- (you walk in front of the pillar), so it lies flat + -- against the 16px band, which is what a plinth does + -- $26-$29 $2C-$2F the octagon painted on both Cable Club + -- floors. Ground markings seen from above, and every + -- cell they sit in is walkable, so rule 3 already + -- keeps them perfectly flat. ($26/$27 are shared + -- with the stool bases above and pinned there; they + -- appear nowhere else in either room.) + }, + + -- The S.S. Anne, inside: both cabin decks and their corridors, the + -- bow, the kitchen, the captain's room -- and two Kanto houses + -- (Cerulean's badge man, Fuchsia's Good Rod fisher) that borrow the + -- liner's furniture set. Twelve maps, and SS_ANNE_CAPTAINS_ROOM plus + -- SS_ANNE_KITCHEN between them place every tile the group uses. + -- + -- Everything was wrong here, in every one of the ways indoors goes + -- wrong. The wall-touching cabin table and the BED were merged into + -- the wall and towered to 32px, so a bunk read as a bank of lockers + -- wearing a porthole on top. The corridor wall band measured 24px + -- instead of 16. In the two houses the band came out at height 0 -- + -- the drawing runs off the top of the map, so the detector had no run + -- to measure and the room had no walls at all. Every stool, chair and + -- staircase sat in a WALKABLE cell and flattened to floor, so the + -- captain's room had no stairs, no chairs and no seat. And the + -- barrels' top-left tile is $48, which the engine's stale-cache shore + -- set counts as water in every tileset but SHIP_PORT: each barrel had + -- a pond dug into its lid. + SHIP = { + -- The wall band stays ONE 16px face. Blank courses (16), the + -- lower trim (18/19), the portholes (2/3), and the band's corner + -- pieces where a corridor meets it (32/33 white, 48/49 trim, + -- 34/50 the black outer corner). $32 (50) is the shore-set trap: + -- unpinned it recesses into a pond lip in the middle of a + -- bulkhead, and it drags the whole 16x16 cell down with it, + -- because rule 2 judges a cell by its bottom-left tile. + -- + -- The black rim is wall too, not void: 17 is the band's cap seen + -- from above (so the fold tops the bulkhead with it), 21/22 the + -- vertical faces that run down both sides of every corridor, 51 + -- the ship's south bulkhead, and 5/6/37/38 the four corners of the + -- notch each cabin doorway is cut into. Left to the detector + -- these came out 8, 16, 24 and 48 in the same room. Only $01 -- + -- pure black, the space OUTSIDE the hull -- is left alone: the + -- void rule flattens it, which is what darkness should do. + -- + -- 14/15/30/31 are the cabin door drawn INTO the band (its cell is + -- walkable, since the player steps on it to warp, so unpinned the + -- door punched a flat hole through the wall). 84/85 are the + -- captain's chair BACK, drawn into the band above his seat -- so + -- the chair reads as a high-backed swivel chair jutting from the + -- wall instead of a separate tower. 45/61 are the bow deck's + -- rope rail and 46/47/62/63 its stepped bulwark: the same 16px + -- course, so the open deck is fenced at one height. + wall = { 2, 3, 5, 6, 14, 15, 16, 17, 18, 19, 21, 22, 30, 31, 32, + 33, 34, 37, 38, 45, 46, 47, 48, 49, 50, 51, 61, 62, 63, + 84, 85 }, + -- Every table in the group, at 12px: the kitchen's three banquet + -- tables (twelve tile rows deep), the counter run along the + -- kitchen's south wall, the captain's desk, and the writing table + -- in the two houses. Corners 9/12, north edge 10, side edges + -- 25/28, the surface 44, the plain apron 11 and the drawer front + -- 59/60. One 8px band folds up, so the drawn apron stands as the + -- front and every row above rides the top face in drawn order -- + -- which is how the tableware stays tableware: the plate (26), the + -- covered platter (64/65/80/81), the kitchen stove's burner grid + -- (53) and the captain's open book (68/69) are all drawn FROM + -- ABOVE, so they belong lying on the top face, not standing up as + -- cutouts. 59/60 doubles as the chest of drawers at the foot of + -- every cabin bunk; 12px puts it a hand above the mattress, which + -- is where a bedside chest belongs. + table = { 9, 10, 11, 12, 25, 26, 28, 44, 53, 59, 60, 64, 65, 68, + 69, 80, 81 }, + -- The cabin bunk: a mattress drawn from above, so its art stays on + -- the TOP face and it lies low. 70/71 the pillow, 86/87 the + -- mattress. (Its drawer end is in `table` above.) + bed = { 70, 71, 86, 87 }, + -- Seats, in the standee pool: the round stool that appears in + -- every cabin, the badge house and the captain's room (7/8 seat + -- over 23/24 legs), and the captain's own chair (66/67 seat over + -- the same 23/24 base, its back pinned into the wall above). + -- Seat height 8 keeps a character standing on the stool's + -- (walkable) cell sitting at seat height rather than floating. + -- The drawings have real floor margin on all four sides, so the + -- black-outline flood eats the checker around them and leaves the + -- seat whole. + stool = { 7, 8, 23, 24, 66, 67 }, + -- The captain's storage rack: three shelf ranks (54 left, 43 + -- right) drawn four tile rows tall against the wall. TALL, not + -- deep -- the bookcase builder collapses it onto one cell of depth + -- at its drawn height, and adopts the row above (9/12, pinned + -- `table` as the tabletop corners they also are) as its cap. + -- Boxed by the detector it was a 16px stub with the wall beside it + -- towered to 24. + bookcase = { 43, 54 }, + -- The galley barrels -- three down the kitchen's east wall, one in + -- the captain's room, one in each house. A per-pixel cutout in + -- the THIN pool, the treatment Lt. Surge's trash cans get: 72/73 + -- the lid, 88/89 the banded body. 72 is $48, the shore-set trap + -- that had each barrel resolving to water at -2. + prop = { 72, 73, 88, 89 }, + -- The stairs, both flights, in every map that has them (1F, 2F, + -- 3F, B1F and the captain's room). Same two drawings the whole + -- game uses, and the warp table says which is which: 41/42/57/58 + -- is the light flight that always leads UP a deck, climbing east + -- the way its treads step; 39/40/55/56 is the dark stairwell that + -- always leads DOWN, sinking westward. Both cells are walkable, + -- so unpinned both were flat floor and the decks did not connect. + stair_e = { 41, 42, 57, 58 }, + stair_down_w = { 39, 40, 55, 56 }, + -- Left to the derived default on purpose: + -- $01 pure black outside the hull -> void, flat darkness + -- $04 $23 deck planking and grating -> walkable, ground + -- $0D $1D cabin floor checker -> walkable, ground + -- $4A cabin door threshold -> walkable, ground + -- $24 $34 the dark slab under the exit warp in the cabins and + -- the two houses: a threshold mat with no wall around + -- it, walkable, and flat is the honest reading + -- $14 the SEA around SS_ANNE_BOW. It is the one tile id + -- the stale-cache water set names in every tileset, + -- and here that guess is simply RIGHT -- the bow deck + -- really does stand in open water, so it is the one + -- trap tile in this group that must NOT be pinned. + }, + + -- Vermilion Dock: the quay, the boarding gangway, and the S.S. Anne + -- herself moored alongside. One map, and it is the only place in + -- the game where a whole VESSEL is drawn as map art rather than as a + -- building sprite. + -- + -- The detector read the liner as a volume and raised her 48px -- + -- three cells, taller than she is long is wide -- with her columns + -- stepping 48/40/24/0 across the hull, so the drawing was folded + -- UPRIGHT and smeared up a lumpy monolith. Both ends of the hull + -- fell to 0 instead, and the port bow ($40/$41/$51) sat in a cell + -- whose bottom-left tile is open water, so rule 2 sank it to -2 and + -- the ship had a hole in her nose. + -- + -- (This tileset is the one place the engine does NOT count $32 and + -- $48 as shore -- Map.lua's NO_SHORE_TILESETS -- because $32 IS the + -- gangway here. So the usual shore trap does not apply, and $14 is + -- honest open water. Nothing in this entry is dodging a water + -- fallback; the whole entry is about the ship.) + SHIP_PORT = { + -- The hull, every tile of her, as a `roof`: a box wearing its art + -- on the TOP face. That is what the drawing IS -- the liner seen + -- from above and slightly astern, funnels and boats and deck rail + -- on the top rows, hull plating on the bottom two -- so folding it + -- upright was always going to smear. 28px of freeboard reads as a + -- liner beside a quay at 0 and water at -2 without towering, and + -- the side bands crop from each edge tile's own art, so the flank + -- the player faces wears the hull plating that is drawn there. + -- Rows north to south: bow rail and boat deck (0/2-7/9/11-15), + -- superstructure (16-31), midships (32-47), waterline strake + -- (48-53 and 61-63 in the stern), hull plating (64-69, 77-79) and + -- the boot topping (81-85, 90-93). + roof = { 0, 2, 3, 4, 5, 6, 7, 9, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 21, 22, 23, 24, 25, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, + 48, 51, 52, 53, 54, 55, 56, 57, 61, 62, + 64, 65, 66, 67, 68, 69, 77, 78, 79, + 81, 82, 83, 85, 90, 91, 93 }, + -- The quayside crates, a 2x2-tile box each (86/87 lid, 1/80 + -- body), stacked two cells deep in all four corners of the dock, + -- and the pallet stacks flanking the gangway head (49). A crate + -- IS a 16px cube: one course, art folded up the face it shows. + -- Unpinned the corner stacks fused into one taller volume. + wall = { 1, 49, 80, 86, 87 }, + -- The delivery van parked on the north quay: drawn face-on, four + -- tiles by two, with clear pavement all round it -- so it takes + -- the standing per-pixel cutout with a body (10 voxels), not a + -- rectangular block that would swallow the air above its cab. + -- 72/73/74/75 the cab and box, 88/89/60/76 the wheels and skirt. + billboard = { 60, 72, 73, 74, 75, 76, 88, 89 }, + -- Left to the derived default on purpose: + -- $0A the quay's paving -> walkable, ground + -- $32 $3B the gangway planks -> walkable, ground, so the walk + -- from the quay to the warp stays flush at 0 and + -- meets the hull's flank rather than climbing it + -- $14 open water -> the tileset's own water, -2 + -- $3A the shaded strip along the quay's foot. Its cell's + -- bottom-left tile is water, so rule 2 puts it at + -- water level -- which is correct: it is the dock's + -- shadow lying ON the water, and the 2px lip between + -- it and the paving is exactly the shoreline the + -- water class exists to draw. + }, + + -- The facilities: twenty-one maps off one 96-tile atlas -- Silph Co's + -- ten office floors, the four Rocket Hideout basements, the four + -- Pokemon Mansion floors, the Power Plant, and the Cinnabar and + -- Saffron gyms. One pin therefore has to read right in a laboratory, + -- an open-plan office, a burnt mansion and a gym at once; where a + -- graphic genuinely means two things the note says which reading won. + -- The failures are the usual indoor ones, louder: the detector raised + -- whole wall COLUMNS to 48px, stood the lab consoles up as 32px + -- cabinets towering over the very band they are drawn INTO, fused + -- vertical runs of computer terminals into 48px monoliths, boxed the + -- potted-plant rows into one slab wearing leaves on its top face, and + -- left both staircases lying flat on the floor. Silph Co's lobby + -- island is tile $14 -- the engine's stale-cache water id -- and sank + -- into a pond in the middle of the foyer. + FACILITY = { + -- The wall band stays ONE 16px face: the dark courses (42/58 + -- horizontal, 43/44 vertical, 45/46 the top corners, 59/60 the + -- bottom caps) and Silph Co's own pale striped band (87/89). A + -- north-south run of 43/44 is one continuous drawing, so unpinned + -- the detector measured the whole column and raised it to 48. + -- + -- Everything DRAWN BUILT INTO that band is `wall` too, the + -- Center's-healing-console rule, so it reads as equipment jutting + -- from the band rather than a separate tower: + -- 74/75 the machine's two dials over 76/77 its footed base -- one + -- graphic serving the Cinnabar Gym's quiz consoles, Silph Co's + -- and the Power Plant's equipment banks and the Mansion's lab + -- rigs. Drawn three rows tall under a one-row wall cap, which + -- is why the detector called it 32 + -- 9/10 screen over 25/26 base: the computer terminal. It is + -- ALSO the free-standing maze wall of Rocket Hideout B2F/B3F + -- and the terminal rows of the Power Plant hall, and 16 -- its + -- drawn height -- is right in every one of them + -- 64/80 the Mansion 2F balustrade seen face-on, 65/81 its + -- north-south run. 65 repeats down a whole column, so it + -- towered exactly like the wall did + -- 86/88 the lift doors of Silph Co: a walkable warp cell, but a + -- pin is look-only, so the doors stand in the wall band + -- instead of lying flat as a mat (the Center does the same + -- with its window/warp tile) + -- 8/24 and 36/37 the shutters the card-key doors and the + -- Cinnabar Gym quiz gates stamp CLOSED at runtime + -- (field.cardKeyDoors' blocks $54/$5F/$2D). They are in no + -- map's block list, so nothing but this line covers them; the + -- detector read them as an 8px stub lying in the doorway + wall = { 8, 9, 10, 24, 25, 26, 36, 37, 42, 43, 44, 45, 46, + 58, 59, 60, 64, 65, 74, 75, 76, 77, 80, 81, 86, + 87, 88, 89 }, + + -- The tall display cabinet of the Mansion and Silph 3F/8F/9F: cap + -- row 40/41 over two ranks of 56/57, one cell wide and drawn FOUR + -- rows tall. A tall drawing, not a deep one, so it takes the + -- Mart's shelf-rack treatment and collapses onto a one-cell-deep + -- box at its full 32px -- as `wall` it flattened into a bench two + -- cells deep, which is the shape of its footprint, not the shape + -- of the thing. Its base course is the computer terminal's own + -- 25/26, listed under `wall` above and therefore shared: that + -- stays a 16px block, and reads as the cabinet's plinth. + bookcase = { 40, 41, 56, 57 }, + + -- The grey slab family -- every desk, bench, worktop and service + -- counter in the tileset is cut from these seven pieces: 13/14 the + -- top corners, 2 the top edge between them, 29/30 the body's side + -- edges, 53 its fill, 68/69 the legs, 18 the counter's front panel + -- (the tileset's own counterTiles). + -- + -- `counter` -- 8px, half a cell -- not `table` or `desk`, because + -- the drawing is a TOP surface seen from above with one row of + -- front elevation at its south end: one clean band folds exactly + -- that bottom row (68/69 legs, or 18) up as the front and rides + -- every row above it on the top face in drawn order, which is what + -- a seven-cell Silph Co bench actually looks like. At 12 or 16 + -- the same drawing reads as a wall stub, the failure the Center's + -- counters are pinned against. + -- + -- 13/14 and 29/30 double as the CAP and body of the wall consoles + -- (blocks $60/$62/$64/$66/$7F). The benches win the tie on count + -- -- 142 block placements across 18 maps against the consoles' 105 + -- across 14 -- and the compromise costs the consoles nothing: the + -- capped rows sit NORTH of the 16px dial face, so an 8px ledge + -- behind a 16px machine is occluded from every southward camera. + counter = { 2, 13, 14, 18, 29, 30, 53, 68, 69 }, + + -- Plants, the thin standee pool, as in every other interior. Two + -- drawings: the tall potted palm of Silph Co, the Hideout and the + -- Mansion (crown 5/6/21/22 over pot 7/15/23/31, a 1x2-cell + -- object), and the gym planter of Cinnabar, Saffron and the + -- Mansion (crown 39/47, bowl 55/63, box 61/62). Unpinned the + -- detector fused a six-plant row into one box wearing the leaf + -- dither as a lid. The crown's checker highlights share the + -- floor's shades so the mask drains some of them -- the same + -- accepted trade as the Center's bush. + prop = { 5, 6, 7, 15, 21, 22, 23, 31, 39, 47, 55, 61, 62, 63 }, + + -- Both flights UP, both climbing EAST. 3/4/19/78 is the one-cell + -- open staircase of the Mansion (1F->2F, 2F->3F) and the Hideout + -- (B1F->Game Corner): white treads whose risers step up toward the + -- upper right. 90/91 over 67/52 is Silph Co's own stairwell, set + -- into the north wall band on every floor -- courses of tread whose + -- east ends step back one notch at a time. BOTH cells are + -- WALKABLE (19 and 67 are the tileset's warp tiles), so unpinned + -- they resolved to flat ground and the steps were painted on the + -- floor. Silph's drawing is the one genuinely ambiguous flight in + -- the tileset -- read as a west rise it builds a ramp that leaves a + -- gap against the east jamb, so east it is, which also makes one + -- consistent story out of every up-stair in the group. + stair_e = { 3, 4, 19, 78, 52, 67, 90, 91 }, + -- The staircase DOWN (11/12/27/28): three treads in a black + -- stairwell, shortening and darkening west to east, so the flight + -- descends EAST into the dark. One graphic again, for every + -- down-stair in the group. (Where the Mansion 3F variant hangs + -- directly under the wall band, block $73, the engine's door fold + -- claims the cell first -- 27 is one of the tileset's doorTiles -- + -- and stands the art up in the band as a doorway. That reads + -- fine, and the fold runs after this profile, so it is not + -- something a pin can or should undo.) + stair_down_e = { 11, 12, 27, 28 }, + + -- The rubble drifts of the Mansion and the Power Plant (83/84 over + -- 54/38): two black-outlined boulders per cell on a plain grey + -- ground, tiling wall-to-wall across whole rooms. Drawn ROUND, so + -- they take the overworld canopy's archetype -- one voxel hull per + -- 16x16 cell carved from the darkest-pixel outline -- which is the + -- only class that keeps a repeating field as separate lumps: a + -- standee pool would cluster a whole drift into one giant flat + -- cutout, and the volume path gave 16px slabs with 48px towers + -- wherever a drift touched a wall. + cylinder = { 38, 54, 83, 84 }, + + -- Silph Co 1F's lobby island (tile $14), the cross-shaped dither + -- the reception terminals ring. $14 is one of the three ids the + -- engine's stale-cache water set claims in EVERY tileset, and it + -- is not on this tileset's walkable list, so it fell straight to + -- `water` and opened a pond in the middle of the foyer. It is a + -- floor-level mat drawn from above: flat. + ground = { 20 }, + + -- Left to the derived defaults on purpose, having been checked: + -- 51, the Hideout's black fill, is all-black art, so the void + -- rule already flattens it -- which is what a room's interior + -- darkness should be + -- 32/33/48/49 -- the four quarter-tiles the Hideout's spinner + -- arrows and the Saffron/Silph warp diamonds are assembled + -- from -- and 94, the Hideout's floor panel, are on the + -- tileset's walkable list and come out flat ground unaided + -- 1 (the checker floor), 17 (the Mansion's upper-floor boards) + -- and 66/82 (the entrance carpet) likewise + -- the other two stale-cache water ids, $32 and $48, are never + -- placed by any of the twenty-one maps, so only $14 above + -- needed the guard + }, + + -- Pokemon Tower's seven floors and Agatha's room (one tileset). + -- Every floor is the same octagonal chamber: a ring of wall panels + -- ($09/$0A over $19/$1A) cut out of a mid-grey mass ($11), with a + -- field of gravestones inside it, and Agatha's room is that chamber + -- shrunk. The ring and the mass the detector already reads as one + -- 16px course (probed) -- they are listed anyway so the answer is + -- authored rather than inferred from a repeat count, and so the + -- panels can never fuse into the gravestones that touch them. + -- What the detector got WRONG was everything else: the headstones + -- came out as 8px stubs with their arched tops dropped, both + -- stairwells were flattened by the walkable-cell rule, and the + -- potted palms lost their crowns to a 16px box. + -- (Nothing here needs pinning against the void rule: the tileset's + -- one all-black tile is $47, and no map in the group places it.) + CEMETERY = { + -- One 16px course: the chamber's wall ring ($09/$0A over + -- $19/$1A), the grey mass beyond it ($11) so the room reads as a + -- chamber cut into solid stock rather than a fence standing on a + -- plain, and Agatha's north band ($20 over $30). + wall = { 9, 10, 25, 26, + 17, + 32, 48 }, + -- The reception counter of POKEMON_TOWER_1F -- the only counter in + -- the group, and the ROM's own counterTiles for this tileset name + -- $12 as exactly that. It is an L that fences the receptionist + -- into the south-east corner: the east-west arm drawn face-on + -- ($02 the surface over $12 the front panel, eight cells of it) + -- meeting the north-south arm drawn from above ($1D/$1E, eight + -- tile rows, which the volume path would have towered to 64px). + -- The first pass put all four in `wall` and the barrier came out + -- a full-height partition with the receptionist hidden behind it + -- to the shoulders. Half a cell is what a counter is: she leans + -- on it, the drawn surface ($02) rides the top face and the drawn + -- panel ($12) folds up the front, and both arms come out the same + -- height so the corner turns cleanly instead of stepping. The + -- cells stay blocked either way -- pins are look-only. + counter = { 2, 18, 29, 30 }, + -- The gravestones ($05/$06 over $15/$16): a headstone drawn + -- face-on, one per cell, 314 of them across the seven floors and + -- Agatha's room. The volume path measured the drawing as its + -- bottom row alone and left an 8px stub -- the plinth without the + -- stone. `post` is the per-CELL standee pool: each cell is + -- extracted on its own as a per-pixel cutout 6 voxels deep, so + -- the arched top comes back and a 4x2 block of graves stands as + -- eight separate stones instead of one 32px sheet with the back + -- row floating above the front (which is what one shared + -- billboard cluster would have made of them). The stone's white + -- side margins let the background flood in; its own white checker + -- band is sealed by the black arch and survives. + post = { 5, 6, 21, 22 }, + -- The flight UP to the next floor ($03/$04 over $13/$14): real + -- rising steps, climbing east the way the risers are drawn (short + -- at the west, tall at the east -- the same drawing idiom as Red's + -- house). Its collision tile is $13, which IS in the tileset's + -- walkable list, so rule 3 had laid the whole flight flat; and its + -- south-east tile is $14, the water-fallback trap id -- harmless + -- only because the cell's own bottom-left is $13, and pinned here + -- so it can never matter. + stair_e = { 3, 4, 19, 20 }, + -- The flight DOWN ($0B/$0C over $1B/$1C): a sunken stairwell. + -- The drawn step edges are high and lit at the WEST and fall away + -- dark to the east, so the flight descends eastward -- the mirror + -- of Red's bedroom stair. Walkable ($1B) and flattened for the + -- same reason as the flight up. + stair_down_e = { 11, 12, 27, 28 }, + -- The potted palm that flanks Mr. Fuji's shrine on 7F and stands + -- in Agatha's room ($27/$2F crown, $37/$3F fronds, $3D/$3E + -- planter): a plant, so the THIN standee pool, like every other + -- interior plant. It is drawn 24px tall across THREE tile rows, + -- which is why the box path dropped its crown row entirely and + -- kept a 16px planter; a standee takes the whole drawing at its + -- real height. It backs onto the wall ring, and a separate pool + -- keeps it from being absorbed into it. + prop = { 39, 47, 55, 63, 61, 62 }, + -- (Nothing else needs an entry. The tower's floor ($01) and 5F's + -- healing pad ($22) are walkable, so rule 3 lays them flat where + -- they are drawn; the mass, ring, graves, stairs and palm above + -- are every other tile the eight maps place.) + }, + + -- The two Underground Paths (Route 5<->6 north-south, Route 7<->8 + -- west-east): one blockset, two maps, and the plainest geometry in + -- the game -- a tiled corridor cut out of black. Most of it was + -- already right, because the black it is cut from ($10) is solid + -- black and the void rule flattens it (`10v00` over every border + -- block), and the lattice floor ($0B/$0C) with the diagonal shadow + -- strip along its west wall ($15/$18) are walkable and lie flat. + -- Two things were not. + -- + -- The SOUTH rim. The corridor's near wall is drawn as a single 8px + -- light line ($02) with black under it, so the volume builder + -- measured a one-row drawing and raised half a course -- an 8px + -- kerb (`02w08` along the whole bottom row of + -- UNDERGROUND_PATH_WEST_EAST) facing a proper 16px far wall + -- (`06w16`/`09w16`). Pinned, the corridor closes with one band all + -- round. The east and west rim lines ($16/$17) already read 16 -- + -- their columns repeat -- and join the same authored course so the + -- whole enclosure is one decision. + -- + -- The STAIRS. $03/$04 over $13/$14 is a real flight drawn in + -- profile, treads climbing to the upper right, and it is the warp + -- back up to the surface at both ends of both corridors. Its cell + -- is walkable, so it resolved to flat ground and the staircase was + -- a painting (`03g00 04g00 / 13g00 14g00`). stair_e builds the + -- flight the drawing shows, rising east. $14 is also the $14 + -- WATER-FALLBACK id: the walkable cell was hiding the trap here + -- rather than dodging it, and the pin settles it either way. + UNDERGROUND = { + wall = { 6, 9, -- $06 over $09, the far wall band + 2, -- $02, the near wall's light line + 22, 23 }, -- $16/$17, the east and west rims + stair_e = { 3, 4, 19, 20 }, -- $03/$04 over $13/$14 + -- The blockset also carries the wall's unplaced corner variants + -- ($05/$08 north-west, $07/$0A north-east, $11 south-west, $12 + -- south-east). Neither map places them; they belong with the + -- wall band above if one ever does. + }, }, -- Buildings whose whole sprite is voxelized band by band (lib/Buildings.lua, @@ -365,6 +2140,85 @@ return { -- placement. buildings = { OVERWORLD = { + -- assets/docs/buildings/B30: the POKEMON TOWER -- the one drawing + -- in the catalogue that STRADDLES A MAP BOUNDARY. Twelve of its + -- twenty rows stand in ROUTE_10's last rows (the 64px purple roof + -- band plus two window courses); the other eight -- five more + -- courses and the base with the door -- are on LAVENDER_TOWN, + -- where the tower begins at tile row 0. Modelled per map it came + -- out as two buildings, one per half. + -- + -- `topRows` composites ROUTE_10's twelve rows ABOVE the matched + -- grid, so the model is built from the COMPLETE twenty-row drawing + -- -- 96px of facade under the real 64px roof -- while placement + -- still matches only the eight Lavender rows. Its `claimOnly` + -- twin below claims the ROUTE_10 rows and stamps nothing, so the + -- roof half does not also stand as its own building. + -- + -- BOTH COME FIRST IN THIS LIST ON PURPOSE. The tower's upper + -- twelve rows are `gabled_block_6x6` tile for tile -- the artist + -- drew the top of the tower as an ordinary six-cell block -- so + -- that template matches at ROUTE_10 (24,132) too. Placement is + -- first-claim-wins (see Buildings.build), so claiming here before + -- the generic block reaches it is what keeps the second tower from + -- being stamped behind this one. + -- + -- The 13-id last row is kept from the first attempt: the bare + -- 12-wide body grid is B14's lower two-thirds tile for tile, and + -- the extra id stops this template stamping inside + -- `flat_block_6x6`'s three placements (`matches` walks a row to + -- its own length, while `read` composites only #tiles[1] columns, + -- so a 13th id constrains placement without entering the drawing). + { + id = "pokemon_tower", + topRows = { + { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + }, + tiles = { + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79, 17 }, + }, + roofRows = 64, roofBack = 8, roofFront = 8, roofCycle = { 8, 31 }, + slab = 4, frontEave = 4, ledge = nil, + }, + -- the tower's roof half, where it actually stands on ROUTE_10: + -- claimed flat so the drawing does not ALSO fold up as a building + -- behind the modelled tower (see topRows above) + { + id = "pokemon_tower_top", + claimOnly = true, + tiles = { + { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, + { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, + { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, + { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, + }, + }, -- assets/docs/buildings/B07: the two-storey gabled house. Coursed cap over -- a gable wall, an awning with its double black underline, then the -- ground floor with the door. 7 placements, Red's and Blue's among @@ -976,25 +2830,145 @@ return { }, }, + -- The approach to the Pokemon League: two maps, INDIGO_PLATEAU (the + -- forecourt the League itself stands on) and ROUTE_23 (the long + -- climb up to it, with its badge-check gates). Everything this + -- blockset draws is one piece of architecture -- striated rock + -- walls, white pillars, and the bird STATUES that line both the + -- avenue and the plaza. 29 of its 73 blocks are never placed, so + -- every tile named below really is one of these two maps'. + -- + -- A statue is built exactly like the badge gyms' (see GYM above): + -- one cell of FIGURE ($10/$12 over $28/$29) standing on one cell of + -- PLINTH ($15/$16 the cap over $30/$31 the plaque). 47 of them -- + -- 12 on INDIGO_PLATEAU, six a side down the avenue, and 35 more + -- across ROUTE_23's plaza (blocks $42/$43; the $25/$26 twins that + -- stand the same statue on grass are never placed). + -- + -- What the detector made of them is the bug this entry exists for. + -- On the avenue the statues stack with NO gap: the plinth's plaque + -- row is drawn directly above the next figure's head, so the + -- flood-fill joined all six of a column into ONE region 24 tile rows + -- tall, and the volume builder's repeat scan read 32px down one half + -- of the drawing and 24px down the other. Each row came out as a + -- continuous stepped RIDGE of boxes wearing the statue art folded + -- onto its south face -- probed INDIGO_PLATEAU tiles (16,12)-(17,35) + -- at 32/24 and (22,12)-(23,35) mirrored. ROUTE_23's plaza statues + -- stand alone and fared no better: 24px boxes with the figure's top + -- row skipped outright. + -- + -- Pinned the gyms' way the ridge becomes statues. The plinth is a + -- SOLID 16px `wall` block; the figure is a per-pixel cutout 5 voxels + -- deep (the thin `prop` pool) that rides the plinth's top face + -- through the authored-box support rule and collapses to the + -- plinth's SINGLE cell of footprint -- Structures' wall-support case, + -- so the base never marches backwards. + -- + -- The one thing the gyms did not have to deal with: $28/$29 is + -- SHARED. The same bird is drawn again at the foot of every white + -- pillar, framed there by the pillar's black edge ($25/$26 over that + -- same $28/$29, blocks $18/$1B) -- 80 of them, 76 down ROUTE_23 and + -- four on INDIGO_PLATEAU (its two outer corners and the pair + -- flanking the League's recess). So $25/$26 joins the same pool: + -- pinning half a cell would have stood a half-height bird under a + -- wall. That in turn is why the pillar itself is pinned -- see the + -- last paragraph of `wall`. PLATEAU = { - -- assets/docs/buildings/B23: the Victory Road entrance on - -- Route 23: a long rock face with two barred doors cut into - -- it. Not a house -- what the roof band reads is the pale top - -- of the cliff seen from above, and the facade is the - -- striated rock below it. - { - id = "victory_road_gate", - tiles = { - { 37, 38, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 37, 38 }, - { 40, 41, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 40, 41 }, - { 21, 22, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 15, 15, 15, 15, 21, 22 }, - { 5, 6, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 15, 15, 15, 15, 5, 6 }, - { 5, 6, 15, 15, 15, 15, 15, 15, 11, 12, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 11, 12, 15, 15, 15, 15, 5, 6 }, - { 21, 22, 14, 14, 14, 14, 14, 14, 27, 28, 14, 14, 14, 14, 14, 14, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 27, 28, 14, 14, 14, 14, 21, 22 }, - }, - roofRows = 16, roofBack = 7, roofFront = 8, roofCycle = { 9, 12 }, - slab = 4, frontEave = 4, ledge = nil, - }, + -- ONE 16px course for every piece of masonry here. + -- + -- $03 is the striated rock face, 2436 placements and the bulk of + -- both maps. It already read 16 nearly everywhere, but in the + -- columns of INDIGO_PLATEAU's rim that stand over a corner pillar + -- the repeat scan came out 24 -- a stagger in the plateau's + -- skyline (probed `03w24` at tiles (8,0)-(9,2), (12,0)-(13,2) and + -- their two mirrors). Authored, the rim is one course. + -- + -- $0D/$0F/$0E are the League's outer wall -- top band, face and + -- base. The same three tiles draw the Pokemon League's own + -- facade, the long walls flanking the avenue, and every + -- badge-check gate down ROUTE_23. + -- + -- $15/$16 + $05/$06 + $30/$31 are the pilaster: cap, shaft, and + -- the plaque base. $15/$16 over $30/$31 IS the statue's plinth -- + -- the artist drew the same stone twice -- which is why one pin + -- serves the gate corners and the statues alike. + -- + -- $2E/$2F the white pillar shaft and $20/$21 its cap change no + -- HEIGHT: they derive 16 already. What the pin buys is + -- `authored`, which is exactly what the prop support rule tests. + -- Without it a pillar-foot bird finds no support, drops to ground + -- level, and leaves a hole punched clean through the pillar + -- (probed, shot, then fixed). + wall = { 3, + 13, 14, 15, + 21, 22, 48, 49, 5, 6, + 32, 33, 46, 47 }, + -- The statues, and the same bird at the pillar feet. Black-outline + -- segmented: the outline and everything it encloses stay, the sky + -- and the paving around them flood away. + prop = { 16, 18, 40, 41, 37, 38 }, + -- Round drawings, one voxel ball per 16x16 cell -- the treatment + -- the overworld's canopies and Celadon's hedge take. + -- + -- $07/$08 over $17/$18: the seven canopies planted on pillar tops + -- along ROUTE_23 (blocks $44/$45; the $0F block that tiles four of + -- them together is never placed). Boxed, the canopy art smeared + -- down the whole pillar column beneath it. + -- + -- $2A/$2B over $22/$1D: the boulders strewn across ROUTE_23's + -- middle terrace (blocks $02/$13/$16), one per cell and NOT + -- walkable. As boxes they sat flat enough to look painted onto + -- the path; as balls they read as the obstacles they are. + cylinder = { 7, 8, 23, 24, + 29, 34, 42, 43 }, + -- The Route 23 sign, one cell, block $48's only placement. + -- Unpinned it probed `09w00 0Aw00 / 19w08 1Aw08` -- an 8px stub + -- with its board skipped. Same thin plate on a stick every other + -- outdoor sign gets. + signpost = { 9, 10, 25, 26 }, + -- The Route 22 gate's roof, drawn from ABOVE and filling + -- ROUTE_23's last two block rows ($3D the tiling, $3E/$44 its + -- edges, $40/$41 the corners). Art on the TOP face -- the way + -- SHIP_PORT's hull is pinned -- rather than folded up a 16px kerb. + -- It stands past the map's last walkable row (you warp to + -- ROUTE_22_GATE before you reach it), so this is a tidy-up rather + -- than a fix. + roof = { 61, 62, 64, 65, 68 }, + -- The ground painted where a pinned figure's cell used to be: + -- $23, the pale paving both maps are floored with. It is also the + -- white the pillars are drawn in, so the cell a pillar-foot bird + -- vacates reads as more pillar -- where the neighbour vote left a + -- BLACK hole, having nothing to elect (all four neighbours of that + -- cell are wall). On the avenue and the plaza the statues' own + -- cells simply keep the paving they stand on. + prop_ground = { [16] = 35, [18] = 35, [40] = 35, [41] = 35, + [37] = 35, [38] = 35 }, + -- Deliberately NOT pinned: + -- + -- $14, the water -- 1446 placements, the pond on ROUTE_23's middle + -- terrace -- and its bank shading $32/$33/$1F. $14 and $32 are + -- two of the three stale-cache water ids the trap is named for, + -- but here they are honestly water: TILEANIM_WATER animates $14, + -- the pond is real, and $32/$33/$1F are drawn in the TOP half of + -- water cells as the waterline itself, so the cell rule dropping + -- them to -2 is what the art means. Left to fall through to the + -- engine's water set. ($48, the third trap id, is not in this + -- atlas at all -- it stops at $45.) + -- + -- $0B/$0C over $1B/$1C, the barred doors: the Pokemon League's two + -- and Victory Road's two. They are the tileset's own doorTiles, + -- so the door fold already stands them upright in the facade; + -- probed 16px identically before and after this entry. + -- + -- $23/$2C/$2D are in the walkable list outright, and $45 is the + -- tileset's grassTile, which derives its own standing-tuft pin. + -- + -- The Victory Road entrance is a `buildings` template + -- (victory_road_gate, at the bottom of this file): 36x6 tiles at + -- ROUTE_23 (0,58), and its ends are built out of $25/$26/$28/$29 + -- and $15/$16/$05/$06. Buildings claim their tiles before any of + -- the above can reach them -- probed `b` class over all 216 of + -- them, unchanged by this entry. }, }, } diff --git a/lib/Buildings.lua b/lib/Buildings.lua index 11ad190..7bfc6e0 100644 --- a/lib/Buildings.lua +++ b/lib/Buildings.lua @@ -98,9 +98,24 @@ local models = {} -- ":" -> prebuilt local quads -- Composite the template out of the atlas and flood the silhouette in from -- the border. Returns flat arrays indexed y * W + x. +-- +-- `topRows`, when a template carries it, is extra drawing rows composited +-- ABOVE the matched grid: rows of the same drawing that are not on the +-- map this template places on. The Pokemon Tower is the case that needs +-- it -- the drawing straddles the LAVENDER_TOWN / ROUTE_10 boundary, its +-- roof band and top window courses standing in the route's last rows, so +-- no single map's grid holds the whole building. The matcher never sees +-- topRows (placement is still by `tiles` alone); they exist so the MODEL +-- is built from the complete drawing and the tower rises to its real +-- height instead of folding as two half-buildings. local function read(t, data, perRow) local tiles = t.tiles - local bh, bw = #tiles, #tiles[1] + if t.topRows then + tiles = {} + for _, row in ipairs(t.topRows) do tiles[#tiles + 1] = row end + for _, row in ipairs(t.tiles) do tiles[#tiles + 1] = row end + end + local bh, bw = #tiles, #t.tiles[1] local W, H = bw * 8, bh * 8 local col, ax, ay = {}, {}, {} for sy = 0, H - 1 do @@ -258,7 +273,13 @@ local function measure(sp, t) shadeTexel[s] = shadeTexel[s] or shadeTexel[BLACK] or 0 end - return { top = top, ytop = ytop, D = H, + -- Depth is the MATCHED footprint, not the sprite height. The two are + -- the same number for every whole-drawing template (the sprite is + -- built from `tiles` alone), but a template with `topRows` has a + -- sprite taller than its footprint -- the tower's 16-row drawing + -- stands on the 8 rows of it that are actually on the map, and D = H + -- would have pushed its body 64px south into the town plaza. + return { top = top, ytop = ytop, D = #t.tiles * 8, recess = recess, interior = interior, shadeTexel = shadeTexel } end @@ -606,13 +627,42 @@ function Buildings.build(S, map, data, perRow) for ty = 0, th - bh do Budget.tick() for tx = 0, tw - bw do - if S.tileAt[keyOf(tx, ty)] == first and matches(S, t, tx, ty) then + -- A placement never stamps into cells another template already + -- claimed. Templates are matched independently, and one + -- drawing can satisfy two grids: the Pokemon Tower's upper + -- twelve rows on ROUTE_10 are a standard 6-cell block tile for + -- tile, so `gabled_block_6x6` matched there and stood a whole + -- second building behind the tower. First claim wins, so the + -- list order below is the priority order -- the tower's own + -- templates come first precisely so they take those cells. + local free = S.tileAt[keyOf(tx, ty)] == first + if free then + for r = 0, bh - 1 do + for c = 0, bw - 1 do + if S.skip[keyOf(tx + c, ty + r)] then + free = false + break + end + end + if not free then break end + end + end + if free and matches(S, t, tx, ty) then if not built then local key = tileset.id .. ":" .. index if not models[key] then - local sp = read(t, data, perRow) - local pr = measure(sp, t) - models[key] = emit(model(sp, pr, t), sp, atlasW, atlasH) + if t.claimOnly then + -- claim the cells, stamp nothing: the drawing here is + -- the off-map half of a building another map models in + -- full (the tower's roof rows on ROUTE_10 -- Lavender's + -- placement composites them via topRows). Left to the + -- detector they stood as a second half-building. + models[key] = {} + else + local sp = read(t, data, perRow) + local pr = measure(sp, t) + models[key] = emit(model(sp, pr, t), sp, atlasW, atlasH) + end end built = models[key] end @@ -669,6 +719,13 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh) { q[3][1] + mx, q[3][2], q[3][3] + mz }, { q[4][1] + mx, q[4][2], q[4][3] + mz }, uv = q.uv, shade = q.shade, + -- placements only ever scan the BODY, so a building is always this + -- map's own structure: the mesher's edge keep-rules must not eat + -- the parts that poke past the boundary (an edge-row house's eave + -- juts frontEave voxels into the neighbour's airspace, and the + -- neighbour-body mask read that overhang as a ring scrap -- which + -- opened the roof rim into the sky from across the seam) + own = true, } end end diff --git a/lib/ChunkMesher.lua b/lib/ChunkMesher.lua index 77c2d4b..f17ea51 100644 --- a/lib/ChunkMesher.lua +++ b/lib/ChunkMesher.lua @@ -430,7 +430,36 @@ local function runGeometry(map, bodyOnly, masks, sink) if s and S.skip[k] then -- an object stands here; paint its synthesized ground and let the -- prebuilt prism quads (appended below) carry the art - if S.ground[k] then topQuad(tx * 8, ty * 8, 0, S.ground[k], 1) end + local g = S.ground[k] + if g then + topQuad(tx * 8, ty * 8, 0, g, 1) + -- the claimed tile is still ground at height 0, and water next + -- door still recesses below it: without the same below-ground + -- side bands ordinary ground emits, the two-pixel shoreline + -- face is a slit into the sky behind the mesh -- which is + -- exactly what a building plot or a sign standing at the + -- waterline showed. Same bands, cut from the synthesized + -- ground's own art + for _, side in ipairs(SIDES) do + local nh = heightAt(tx + side[1], ty + side[2]) + if nh < 0 then + local d = side[3] + local lat = LATERAL[d] + local hl = lat and heightAt(tx + lat[1], ty + lat[2]) or 0 + local hr = lat and heightAt(tx + lat[3], ty + lat[4]) or 0 + for band = math.floor(nh / 8), -1 do + local y0 = math.max(nh, band * 8) + local y1 = math.min(0, band * 8 + 8) + if y1 > y0 then + sideQuad(d, tx * 8, ty * 8, y0, y1, g, + (band * 8 + 8) - y1, (band * 8 + 8) - y0, + sideShades(hl, hr, y0, y1, y0 <= nh, + Voxel3D.FACE_SHADE[d])) + end + end + end + end + end elseif s then local run = S.runs[k] local h = run and run.h or s.h @@ -610,6 +639,31 @@ local function runGeometry(map, bodyOnly, masks, sink) return overBody or not maskedClosed(x0, z0, x1, z1) end + -- A face lying EXACTLY on a body boundary plane is ambiguous to the + -- rect tests above: a body structure's outward facade (a Saffron row + -- house whose front row is the map's last row, its south wall on the + -- shared plane with Route 6) and the inward face of a ring scrap + -- occupy the same degenerate rect, and the strict overBody plus the + -- closed mask dropped BOTH -- which is why those facades were missing. + -- The winding tells them apart: a face pointing AWAY from the body + -- belongs to this map's own edge-row structure and nothing in the + -- neighbour will ever draw that plane, so it stays; a face pointing + -- INTO the body is the scrap the mask rules exist to kill, and falls + -- through to them. + local function outwardOnEdge(q, x0, z0, x1, z1) + if z0 == z1 and (z0 == 0 or z0 == bh) and x1 > 0 and x0 < bw then + local nz = (q[2][1] - q[1][1]) * (q[3][2] - q[1][2]) + - (q[2][2] - q[1][2]) * (q[3][1] - q[1][1]) + return (z0 == bh and nz > 0) or (z0 == 0 and nz < 0) + end + if x0 == x1 and (x0 == 0 or x0 == bw) and z1 > 0 and z0 < bh then + local nx = (q[2][2] - q[1][2]) * (q[3][3] - q[1][3]) + - (q[2][3] - q[1][3]) * (q[3][2] - q[1][2]) + return (x0 == bw and nx > 0) or (x0 == 0 and nx < 0) + end + return false + end + local scUV = { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } } local function quadUV(q) if q.uv then return q.uv end @@ -625,7 +679,13 @@ local function runGeometry(map, bodyOnly, masks, sink) local x1 = math.max(q[1][1], q[2][1], q[3][1], q[4][1]) local z0 = math.min(q[1][3], q[2][3], q[3][3], q[4][3]) local z1 = math.max(q[1][3], q[2][3], q[3][3], q[4][3]) - if keepQuad(x0, z0, x1, z1) then + -- q.own: a body-anchored structure's own quad (a building placed by + -- Buildings.build, whose scan never leaves the body). Exempt from + -- the edge keep-rules entirely: its eave legitimately overhangs the + -- boundary plane into the neighbour's airspace, and no variant of + -- the neighbour will ever draw that geometry + if q.own or outwardOnEdge(q, x0, z0, x1, z1) + or keepQuad(x0, z0, x1, z1) then push({ q[1], q[2], q[3], q[4] }, quadUV(q), groundShades(q, q.shade)) end end @@ -643,15 +703,17 @@ local function runGeometry(map, bodyOnly, masks, sink) -- round-tree stamps: the shared hull template translated per cell, -- through reusable scratch corners so expansion allocates nothing. - -- A hull spans at most its own 16px cell, so one rect test usually - -- answers for the whole stamp: strictly interior stamps keep every - -- quad, ring stamps buried under a neighbour body (or, body-only, ring - -- stamps full stop) skip without touching their quads. Only stamps - -- crossing a boundary walk quad by quad. + -- A hull spans at most its own footprint -- one 16px cell unless the + -- stamp carries a wider radius (the 2x2-cell canopy groups) -- so one + -- rect test usually answers for the whole stamp: strictly interior + -- stamps keep every quad, ring stamps buried under a neighbour body + -- (or, body-only, ring stamps full stop) skip without touching their + -- quads. Only stamps crossing a boundary walk quad by quad. local sc = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 } } for _, st in ipairs(S.roundStamps or {}) do local mx, mz = st.mx, st.mz - local sx0, sz0, sx1, sz1 = mx - 8, mz - 8, mx + 8, mz + 8 + local sr = st.r or 8 + local sx0, sz0, sx1, sz1 = mx - sr, mz - sr, mx + sr, mz + sr local interior = sx0 > 0 and sx1 < bw and sz0 > 0 and sz1 < bh local overBody = sx1 > 0 and sx0 < bw and sz1 > 0 and sz0 < bh local keepAll, skipAll @@ -704,15 +766,10 @@ function ChunkMesher.build(map, bodyOnly, masks) return sink.finish() end --- The tall-grass rows as their own mesh: VoxelScene draws it AFTER the --- characters so the southern row of a grass cell still overdraws a --- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary --- terrain could never do this). -local function buildGrassMesh(map) - local S = Structures.forMap(map) - if #S.grassQuads == 0 then return nil end +local function quadsMesh(quads) + if #quads == 0 then return nil end local verts, indices, n = {}, {}, 0 - for _, q in ipairs(S.grassQuads) do + for _, q in ipairs(quads) do for i = 1, 4 do local c = q[i] local uv = q.uv and q.uv[i] or { q.u, q.v } @@ -724,6 +781,33 @@ local function buildGrassMesh(map) return Voxel3D.newMesh(verts, indices) end +-- The tall-grass rows as their own mesh: VoxelScene draws it AFTER the +-- characters so the southern row of a grass cell still overdraws a +-- walker's feet (characters stamp over terrain, Gen 1 style, so ordinary +-- terrain could never do this). +local function buildGrassMesh(map) + return quadsMesh(Structures.forMap(map).grassQuads) +end + +-- The flower billboards as their own mesh, for the same reason as the +-- grass one: it draws AFTER the characters WITH the same camera-ward +-- pull, so a flower south of a walker occludes their feet and one north +-- of them hides behind them. Baked into the terrain mesh they lost that +-- depth fight against the pulled character card whenever the player +-- stood among flowers. Unlike grass this mesh still CASTS shadows (the +-- sun pass draws it): a handful of flowers per meadow, not thousands of +-- tufts. +local function buildFlowerMesh(map) + return quadsMesh(Structures.forMap(map).flowerQuads) +end + +-- Replace a cached slot, releasing whatever mesh it held. +local function swapSlot(c, slot, mesh) + local old = c[slot] + if old and old ~= mesh and old.release then pcall(old.release, old) end + c[slot] = mesh +end + -- ------------------------------------------------------------- the cache local function entry(id) @@ -736,11 +820,12 @@ local function entry(id) end local function releaseEntry(c) - for _, slot in ipairs({ "full", "body", "grass" }) do + for _, slot in ipairs({ "full", "body", "grass", "flowers" }) do local mesh = c[slot] if mesh and mesh.release then pcall(mesh.release, mesh) end c[slot] = nil end + c.stale = nil end -- ---------------------------------------------------------- async builds @@ -778,26 +863,48 @@ end local function runJob(job) local map = job.map local c = entry(job.id) - if c.grass == nil then + if c.grass == nil or c.flowers == nil or (c.stale and c.stale.aux) then local okG, grass = pcall(buildGrassMesh, map) - if (gen[job.id] or 0) ~= job.gen then return end - c.grass = (okG and grass) or false + local okF, flowers = pcall(buildFlowerMesh, map) + if (gen[job.id] or 0) ~= job.gen then + if okG and grass and grass.release then pcall(grass.release, grass) end + if okF and flowers and flowers.release then + pcall(flowers.release, flowers) + end + return + end + swapSlot(c, "grass", (okG and grass) or false) + swapSlot(c, "flowers", (okF and flowers) or false) + if c.stale then c.stale.aux = nil end end local sink = newSink() runGeometry(map, job.slot == "body", job.masks, sink) local mesh = sink.finish() - if (gen[job.id] or 0) ~= job.gen then return end - c[job.slot] = mesh or false + if (gen[job.id] or 0) ~= job.gen then + if mesh and mesh.release then pcall(mesh.release, mesh) end + return + end + swapSlot(c, job.slot, mesh or false) + if c.stale then + c.stale[job.slot] = nil + if not (c.stale.full or c.stale.body or c.stale.aux) then + c.stale = nil + end + end end -- Queue a build unless the slot is already cached or queued. Returns the -- cached mesh when there is one (false-cached misses return nil). -- `urgent` marks the current map's meshes: pump() gives those a bigger --- slice and runs them before neighbour jobs. +-- slice and runs them before neighbour jobs. A slot refresh() marked +-- stale queues its rebuild AND keeps handing back the old mesh, so a +-- one-block edit never drops the scene to the flat 2D path while the +-- replacement cooks. function ChunkMesher.request(map, bodyOnly, masks, urgent) local slot = bodyOnly and "body" or "full" local c = cache[map.id] - if c and c[slot] ~= nil then return c[slot] or nil end + local stale = c and c.stale and (c.stale[slot] or c.stale.aux) + if c and c[slot] ~= nil and not stale then return c[slot] or nil end local key = jobKey(map.id, slot) local job = jobIndex[key] if not job then @@ -808,7 +915,7 @@ function ChunkMesher.request(map, bodyOnly, masks, urgent) elseif urgent then job.urgent = true end - return nil + return (c and c[slot]) or nil end function ChunkMesher.pending() @@ -871,17 +978,26 @@ end function ChunkMesher.get(map, bodyOnly, masks) local slot = bodyOnly and "body" or "full" local c = entry(map.id) - if c.grass == nil then + if c.grass == nil or c.flowers == nil or (c.stale and c.stale.aux) then local okG, grass = pcall(buildGrassMesh, map) - c.grass = (okG and grass) or false + local okF, flowers = pcall(buildFlowerMesh, map) + swapSlot(c, "grass", (okG and grass) or false) + swapSlot(c, "flowers", (okF and flowers) or false) + if c.stale then c.stale.aux = nil end end - if c[slot] == nil then + if c[slot] == nil or (c.stale and c.stale[slot]) then local ok, mesh = pcall(ChunkMesher.build, map, bodyOnly, masks) if not ok then print("[warn] voxel mesh build failed for " .. tostring(map.id) .. ": " .. tostring(mesh)) end - c[slot] = (ok and mesh) or false + swapSlot(c, slot, (ok and mesh) or false) + if c.stale then + c.stale[slot] = nil + if not (c.stale.full or c.stale.body or c.stale.aux) then + c.stale = nil + end + end local key = jobKey(map.id, slot) local job = jobIndex[key] if job then finishJob(job, true) end @@ -901,6 +1017,39 @@ function ChunkMesher.grass(map) return c and c.grass or nil end +function ChunkMesher.flowers(map) + local c = cache[map.id] + return c and c.flowers or nil +end + +-- Rebuild a map's meshes IN PLACE: the stale meshes keep drawing while +-- replacements cook, and each slot swaps as its build lands. This is +-- the block-edit path (a cut tree, a door stamp) -- invalidate() drops +-- the mesh outright, and until the async rebuild landed the scene fell +-- to the flat 2D path, a whole-world blink for a one-block edit. +function ChunkMesher.refresh(mapId) + if not mapId then return ChunkMesher.invalidate() end + local c = cache[mapId] + -- nothing drawable cached: the plain drop costs nothing visible + if not (c and (c.full or c.body)) then + return ChunkMesher.invalidate(mapId) + end + Structures.invalidate(mapId) + gen[mapId] = (gen[mapId] or 0) + 1 + for i = #jobs, 1, -1 do + local job = jobs[i] + if job.id == mapId then + jobIndex[jobKey(job.id, job.slot)] = nil + table.remove(jobs, i) + end + end + -- false-cached slots count as stale too: a retry after a failed build + -- is exactly a rebuild + c.stale = { aux = true, + full = (c.full ~= nil) or nil, + body = (c.body ~= nil) or nil } +end + -- Evict everything outside `live` (a set of map ids): far maps' meshes -- are released -- GPU buffer and LOVE's CPU copy both -- and their -- Structures analysis dropped. The live set is the current map plus its diff --git a/lib/Structures.lua b/lib/Structures.lua index 8019046..3baf693 100644 --- a/lib/Structures.lua +++ b/lib/Structures.lua @@ -192,8 +192,8 @@ function Structures.forMap(map) -- still overdraws a walker's feet even though characters stamp over -- terrain.) S = { shapeAt = shapeAt, tileAt = tileAt, outdoor = Map.isOutdoor(def), - runs = {}, skip = {}, ground = {}, objectQuads = {}, - grassQuads = {}, roundStamps = {} } + runs = {}, skip = {}, ground = {}, doorFold = {}, objectQuads = {}, + grassQuads = {}, flowerQuads = {}, roundStamps = {} } Buildings.build(S, map, pixels(tileset), perRow) -- Fold doors into their buildings. A door cell is WALKABLE (the player @@ -205,6 +205,14 @@ function Structures.forMap(map) -- the fold then shows the door art standing at ground level in the -- building's front face. Door graphics only (the tileset's doorTiles); -- interior stair/mat warps stay flat. + -- + -- A PROFILE PIN WINS over the fold. The fold is detection, and rule 1 + -- of the resolution order is that an authored tile bypasses detection + -- -- but this used to overwrite shapeAt unconditionally, so a pin on + -- any tile the tileset also lists in doorTiles was dead on arrival. + -- Celadon Mansion is the case that found it: all four of its + -- staircases are door tiles, so `stair_e` / `stair_down_w` pins there + -- silently did nothing and the flights stayed painted on the floor. for cy = math.floor(y0 / 2), math.floor(y1 / 2) do for cx = math.floor(x0 / 2), math.floor(x1 / 2) do if map.doorTiles[map:cellTile(cx, cy)] then @@ -213,7 +221,15 @@ function Structures.forMap(map) if ns and ns.art == "upright" then for dy = 0, 1 do for dx = 0, 1 do - shapeAt[keyOf(cx * 2 + dx, cy * 2 + dy)] = shapes.classes.wall + local dk = keyOf(cx * 2 + dx, cy * 2 + dy) + local ds = shapeAt[dk] + if not (ds and ds.authored) then + shapeAt[dk] = shapes.classes.wall + -- remembered for buildVolume: a folded doorway column + -- answers to its REGION for height and top, not to its + -- own drawn extent (see the door adoption there) + S.doorFold[dk] = true + end end end end @@ -413,6 +429,30 @@ function Structures.forMap(map) -- renderer never draws a neighbour's ring, and standing scenery past a -- map's edge would poke into the map next door ---- Structures.buildGrass(S, map, 0, tw - 1, 0, th - 1, data) + + -- ---- flowers: the animated meadow tile stands as a 1px cutout ---- + Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data) + end + + -- ---- authored ground under pinned props ---- + -- The profile can name the tile a pinned prop stands on (a tileset + -- entry's prop_ground: prop tile id -> ground tile id), overriding + -- the neighbour vote. The cuttable bush stands on the plain grass + -- Cut itself leaves behind, not on whatever path its neighbours + -- happen to vote in. + do + local okP, prof = pcall(V.data, "voxel_heights") + local entry = okP and type(prof) == "table" and prof.tilesets + and prof.tilesets[tileset.id] + local pg = entry and entry.prop_ground + if type(pg) == "table" then + for k, skipped in pairs(S.skip) do + if skipped then + local g = pg[S.tileAt[k]] + if g then S.ground[k] = g end + end + end + end end -- unresolved claimed ground (a hull with no art match, headless @@ -479,15 +519,17 @@ end local ROUND_SHADE = { front = 1.0, back = 0.68, side = 0.78, top = 1.0, bottom = 0.55 } -local function roundTemplate(S, map, data, cx, cy, groundTiles) +local function roundTemplate(S, map, data, cx, cy, groundTiles, N, capRows) + N = N or 16 -- art canvas: 16 = one cell, 32 = 2x2 cells + local N2 = N / 2 local perRow = map.tileset.tilesPerRow or 16 local atlasW = map.tileset.imageWidth or 128 local atlasH = map.tileset.imageHeight or 48 - -- cell-space art access (16x16, row 0 = top) + -- cell-space art access (NxN, row 0 = top), anchored at cell (cx, cy) local function tileOf(px, py) - return S.tileAt[keyOf(cx * 2 + (px >= 8 and 1 or 0), - cy * 2 + (py >= 8 and 1 or 0))] + return S.tileAt[keyOf(cx * 2 + math.floor(px / 8), + cy * 2 + math.floor(py / 8))] end local function texel(px, py) local tile = tileOf(px, py) @@ -495,18 +537,18 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) math.floor(tile / perRow) * 8 + py % 8 end - -- shade class of every cell pixel, indexed py * 16 + px + -- shade class of every canvas pixel, indexed py * N + px local cls = {} - for py = 0, 15 do - for px = 0, 15 do + for py = 0, N - 1 do + for px = 0, N - 1 do local ax, ay = texel(px, py) local r, g, b, a = data:getPixel(ax, ay) - cls[py * 16 + px] = a == 0 and "off" - or Structures.shadeClass(math.min(r, g, b)) + cls[py * N + px] = a == 0 and "off" + or Structures.shadeClass(math.min(r, g, b)) end end - -- 4-connected flood from the cell border through `passable` classes + -- 4-connected flood from the canvas border through `passable` classes local function floodOutside(passable) local out, stack = {}, {} local function seed(i) @@ -515,16 +557,16 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) stack[#stack + 1] = i end end - for i = 0, 15 do - seed(i); seed(240 + i); seed(i * 16); seed(i * 16 + 15) + for i = 0, N - 1 do + seed(i); seed(N * (N - 1) + i); seed(i * N); seed(i * N + N - 1) end while #stack > 0 do local i = table.remove(stack) - local px = i % 16 + local px = i % N if px > 0 then seed(i - 1) end - if px < 15 then seed(i + 1) end - if i >= 16 then seed(i - 16) end - if i < 240 then seed(i + 16) end + if px < N - 1 then seed(i + 1) end + if i >= N then seed(i - N) end + if i < N * (N - 1) then seed(i + N) end end return out end @@ -533,23 +575,48 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local out = floodOutside({ off = true, dark = true, light = true, white = true }) local mask, enclosed = {}, 0 - for i = 0, 255 do + for i = 0, N * N - 1 do if not out[i] then mask[i] = true if cls[i] ~= "black" then enclosed = enclosed + 1 end end end - if enclosed < 32 then + if enclosed < N * N / 8 then out = floodOutside({ off = true, light = true, white = true }) mask = {} - for i = 0, 255 do + for i = 0, N * N - 1 do if not out[i] and cls[i] ~= "off" then mask[i] = true end end end local any = nil - for i = 0, 255 do any = any or mask[i] end + for i = 0, N * N - 1 do any = any or mask[i] end if not any then return {} end + -- a CAPPED hull (the stump): the top capRows rows of the mask are the + -- drawn cut face -- a surface seen at an angle, not body. Strip them + -- from the mask and remember their art span; the top-face quads below + -- project that ellipse across the round cap. + local capY0, capY1 = nil, nil + if capRows and capRows > 0 then + local top = nil + for iy = 0, N - 1 do + for ix = 0, N - 1 do + if mask[iy * N + ix] then top = iy break end + end + if top then break end + end + if top then + capY0 = top + capY1 = math.min(top + capRows - 1, N - 2) + for iy = capY0, capY1 do + for ix = 0, N - 1 do mask[iy * N + ix] = nil end + end + any = nil + for i = 0, N * N - 1 do any = any or mask[i] end + if not any then return {} end + end + end + -- the ground the ball stands on: the drawing's own background names -- it. Score every flat ground tile the map places against the cell's -- unmasked light pixels and keep the closest -- mid-forest trees have @@ -564,9 +631,9 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local ox = (t % perRow) * 8 local oy = math.floor(t / perRow) * 8 local score, n = 0, 0 - for py = 0, 15 do - for px = 0, 15 do - local i = py * 16 + px + for py = 0, N - 1 do + for px = 0, N - 1 do + local i = py * N + px local c = cls[i] if not mask[i] and (c == "light" or c == "white") then local ax, ay = texel(px, py) @@ -589,10 +656,10 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local z0, z1, src = {}, {}, {} local loRow, hiRow = {}, {} local yBot = nil - for iy = 0, 15 do + for iy = 0, N - 1 do local lo, hi = nil, nil - for ix = 0, 15 do - if mask[iy * 16 + ix] then + for ix = 0, N - 1 do + if mask[iy * N + ix] then lo = lo or ix hi = ix end @@ -603,7 +670,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local c = (lo + hi + 1) / 2 local hw = (hi - lo + 1) / 2 for ix = lo, hi do - local i = iy * 16 + ix + local i = iy * N + ix if mask[i] then local dx = ix + 0.5 - c local n = 1 @@ -611,7 +678,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) n = math.max(1, math.floor(2 * math.sqrt(hw * hw - dx * dx) + 0.5)) end - z0[i] = math.floor(8 - n / 2 + 0.5) + z0[i] = math.floor(N2 - n / 2 + 0.5) z1[i] = z0[i] + n src[i] = iy end @@ -621,28 +688,46 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- foot: rows under the mask repeat the bottom row's discs, wearing the -- bottom row's (outline-dark) pixels - for iy = yBot + 1, 15 do + for iy = yBot + 1, N - 1 do loRow[iy], hiRow[iy] = loRow[yBot], hiRow[yBot] for ix = loRow[yBot], hiRow[yBot] do - local b = yBot * 16 + ix + local b = yBot * N + ix if z0[b] then - local i = iy * 16 + ix + local i = iy * N + ix z0[i], z1[i], src[i] = z0[b], z1[b], yBot end end end + -- the round cap's top row and z extent, for the stump's ring + -- projection below + local capTopRow, capZ0, capZ1 = nil, nil, nil + if capY0 then + for iy = 0, N - 1 do + if loRow[iy] then capTopRow = iy break end + end + if capTopRow then + for ix = loRow[capTopRow], hiRow[capTopRow] do + local i = capTopRow * N + ix + if z0[i] then + capZ0 = math.min(capZ0 or z0[i], z0[i]) + capZ1 = math.max(capZ1 or z1[i], z1[i]) + end + end + end + end + local function solidAt(ix, iy, iz) - if ix < 0 or ix > 15 or iy < 0 or iy > 15 then return false end - local i = iy * 16 + ix + if ix < 0 or ix > N - 1 or iy < 0 or iy > N - 1 then return false end + local i = iy * N + ix return z0[i] ~= nil and iz >= z0[i] and iz < z1[i] end -- cap interiors sample the canopy a couple of rows below the rim, -- skipping outline-dark pixels local function deepTexel(ix, iy) - for iy2 = iy + 2, math.min(15, iy + 4) do - local i = iy2 * 16 + ix + for iy2 = iy + 2, math.min(N - 1, iy + 4) do + local i = iy2 * N + ix if mask[i] and cls[i] ~= "black" then return texel(ix, iy2) end end return texel(ix, iy) @@ -654,12 +739,12 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- ball paints solid black the moment the camera turns. The foot rows -- stay dark on purpose: their whole source row is outline-black. local function sideTexel(ix, iy) - local r = src[iy * 16 + ix] + local r = src[iy * N + ix] local dir = ix + ix < loRow[iy] + hiRow[iy] and 1 or -1 for step = 0, 3 do local x2 = ix + dir * step - local i2 = r * 16 + x2 - if x2 < 0 or x2 > 15 or not mask[i2] then break end + local i2 = r * N + x2 + if x2 < 0 or x2 > N - 1 or not mask[i2] then break end if cls[i2] ~= "black" then return texel(x2, r) end end return texel(ix, r) @@ -667,20 +752,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local quads = {} - for iy = 0, 15 do + for iy = 0, N - 1 do if loRow[iy] then - local yB, yT = 15 - iy, 16 - iy + local yB, yT = N - 1 - iy, N - iy -- front and back: the drawing per-pixel, columns merged where they -- share a chord plane; a run never crosses the 8px atlas tile seam -- (its u range must interpolate inside one tile) local ix = loRow[iy] while ix <= hiRow[iy] do - local i = iy * 16 + ix + local i = iy * N + ix if z0[i] then local ix2 = ix while ix2 + 1 <= hiRow[iy] do - local j = iy * 16 + ix2 + 1 + local j = iy * N + ix2 + 1 if z0[j] == z0[i] and z1[j] == z1[i] and math.floor((ix2 + 1) / 8) == math.floor(ix / 8) then ix2 = ix2 + 1 @@ -692,8 +777,8 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) local ax1 = (texel(ix2, src[i])) local u0, u1 = (ax0 + 0.05) / atlasW, (ax1 + 0.95) / atlasW local v0, v1 = (ay + 0.05) / atlasH, (ay + 0.95) / atlasH - local x0, x1 = ix - 8, ix2 - 7 - local zF, zB = z1[i] - 8, z0[i] - 8 + local x0, x1 = ix - N2, ix2 - N2 + 1 + local zF, zB = z1[i] - N2, z0[i] - N2 quads[#quads + 1] = { { x0, yB, zF }, { x1, yB, zF }, { x1, yT, zF }, { x0, yT, zF }, uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, @@ -713,11 +798,11 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) -- sides, steps, undersides: constant-texel quads over the z runs a -- neighbour doesn't cover for ix = loRow[iy], hiRow[iy] do - local i = iy * 16 + ix + local i = iy * N + ix if z0[i] then local ax, ay = texel(ix, src[i]) local u, v = (ax + 0.5) / atlasW, (ay + 0.5) / atlasH - local x0, x1 = ix - 8, ix - 7 + local x0, x1 = ix - N2, ix - N2 + 1 -- exposed z pieces against one neighbouring column local function pieces(nx, ny, emit) @@ -728,7 +813,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) while iz2 + 1 < z1[i] and not solidAt(nx, ny, iz2 + 1) do iz2 = iz2 + 1 end - emit(iz - 8, iz2 - 7, iz, iz2) + emit(iz - N2, iz2 - N2 + 1, iz, iz2) iz = iz2 + 1 else iz = iz + 1 @@ -757,7 +842,20 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) u = tu, v = tv, shade = ROUND_SHADE.top, } end - if izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then + if capTopRow and iy == capTopRow and capZ1 then + -- the CUT FACE (a capped hull's top): project the drawn + -- ellipse across the round cap voxel row by voxel row -- + -- its top arc at the cap's north rim, its bottom arc at + -- the south, the perspective the 2D art already implies + for iz = izA, izB do + local t = capZ1 - 1 > capZ0 + and (iz - capZ0) / (capZ1 - 1 - capZ0) or 0 + local ry = capY0 + math.floor(t * (capY1 - capY0) + 0.5) + local cax, cay = texel(ix, ry) + top(iz - N2, iz - N2 + 1, + (cax + 0.5) / atlasW, (cay + 0.5) / atlasH) + end + elseif izA == z0[i] and izB == z1[i] - 1 and izB - izA >= 2 then -- the dome cap: outline on the rim cells, canopy inside local du, dv = deepTexel(ix, iy) top(zA, zA + 1, u, v) @@ -767,7 +865,7 @@ local function roundTemplate(S, map, data, cx, cy, groundTiles) top(zA, zB, u, v) end end) - if iy < 15 then + if iy < N - 1 then pieces(ix, iy + 1, function(zA, zB) quads[#quads + 1] = { { x0, yB, zB }, { x1, yB, zB }, { x1, yB, zA }, { x0, yB, zA }, @@ -815,23 +913,91 @@ function Structures.buildCylinders(S, map, x0, x1, y0, y1, groundTiles) end local tsid = tostring(map.tileset.id or map.tileset.image or "?") + -- the stump class's drawn-ellipse height, hand-authored per tileset + -- (the profile's stump_cap, in art rows) + local stumpCap = 6 + do + local okP, prof = pcall(V.data, "voxel_heights") + local entry = okP and type(prof) == "table" and prof.tilesets + and prof.tilesets[map.tileset.id] + if entry and type(entry.stump_cap) == "number" then + stumpCap = entry.stump_cap + end + end + + -- cells consumed by a 2x2 `canopy` group; the scan runs north to + -- south, west to east, so an anchor always claims its partners + -- before they are visited + local grouped = {} for cy = math.floor(y0 / 2), math.floor(y1 / 2) do for cx = math.floor(x0 / 2), math.floor(x1 / 2) do Budget.tick() + local ckey = cy * 8192 + cx local k = keyOf(cx * 2, cy * 2) - local s = S.shapeAt[k] + local s = (not grouped[ckey]) and S.shapeAt[k] or nil local near = cx * 2 >= -ROUND_RING and cx * 2 < tw + ROUND_RING and cy * 2 >= -ROUND_RING and cy * 2 < th + ROUND_RING - if s and s.art == "cylinder" and near then + if s and s.art == "canopy" and near then + -- ONE 32px hull over the 2x2-cell drawing. The partner cells + -- must be round-pinned too, or the drawing is partial (a map + -- edit, a mod's stray anchor tile) and the anchor is left + -- alone rather than carved into a half-empty giant. + local whole = true + for _, d in ipairs({ { 1, 0 }, { 0, 1 }, { 1, 1 } }) do + local ps = S.shapeAt[keyOf((cx + d[1]) * 2, (cy + d[2]) * 2)] + if not (ps and (ps.art == "cylinder" or ps.art == "canopy")) then + whole = false + end + end + if whole then + local ground = false + if data then + local ids = {} + for dy = 0, 3 do + for dx = 0, 3 do + ids[#ids + 1] = S.tileAt[keyOf(cx * 2 + dx, cy * 2 + dy)] + end + end + local sig = tsid .. "|g32|" .. gsig .. "|" + .. table.concat(ids, ":") + local tpl = roundCache[sig] + if not tpl then + local tq, tbg = roundTemplate(S, map, data, cx, cy, + groundTiles, 32) + tpl = { quads = tq, bg = tbg } + roundCache[sig] = tpl + end + ground = tpl.bg or false + S.roundStamps[#S.roundStamps + 1] = + { quads = tpl.quads, mx = cx * 16 + 16, mz = cy * 16 + 16, + r = 16 } + end + for dy = 0, 3 do + for dx = 0, 3 do + local tk = keyOf(cx * 2 + dx, cy * 2 + dy) + S.skip[tk] = true + S.ground[tk] = ground + end + end + grouped[ckey + 1] = true + grouped[ckey + 8192] = true + grouped[ckey + 8193] = true + end + elseif s and s.art == "cylinder" and near then + -- a `stump`-class cell is the same hull with a cut face: its + -- top capRows of drawing project onto the round top + local cap = s.class == "stump" and stumpCap or nil local ground = false if data then - local sig = tsid .. "|" .. gsig .. "|" .. table.concat({ + local sig = tsid .. (cap and ("|c" .. cap) or "") .. "|" + .. gsig .. "|" .. table.concat({ S.tileAt[k], S.tileAt[keyOf(cx * 2 + 1, cy * 2)], S.tileAt[keyOf(cx * 2, cy * 2 + 1)], S.tileAt[keyOf(cx * 2 + 1, cy * 2 + 1)] }, ":") local tpl = roundCache[sig] if not tpl then - local tq, tbg = roundTemplate(S, map, data, cx, cy, groundTiles) + local tq, tbg = roundTemplate(S, map, data, cx, cy, + groundTiles, 16, cap) tpl = { quads = tq, bg = tbg } roundCache[sig] = tpl end @@ -1290,6 +1456,7 @@ function Structures.buildVolume(S, map, tiles) local runs = {} local heightVotes = {} + local repeatVotes = {} for tx, ys in pairs(cols) do -- visit each contiguous vertical run in this column local sorted = {} @@ -1320,12 +1487,35 @@ function Structures.buildVolume(S, map, tiles) break end end + -- A one-row TRIM at the column's foot hides a repeat from the + -- scan above, which anchors at the front tile: a cliff plateau + -- ends its south edge in a rounded corner tile, the corner + -- never recurs, and the column read its whole capped extent -- + -- a 48px fin (or a whole tent of them) sticking out of a 16px + -- mesa on Routes 3 and 4. When the two rows directly above the + -- front are IDENTICAL, the column is that repeat wearing a trim + -- foot: one course plus the trim is its drawn unit. Doorway + -- columns are untouched -- their run answers to the region + -- (see below) before the unit matters. + if not repeatRead and extent > 2 + and map:tileAt(tx, front - 1) == map:tileAt(tx, front - 2) then + unit = 2 + repeatRead = true + end + end + local isDoor = false + for ty = north, front do + if S.doorFold[keyOf(tx, ty)] then + isDoor = true + break + end end local run = { front = front, north = north, extent = extent, - unit = unit, fromRepeat = repeatRead } + unit = unit, fromRepeat = repeatRead, door = isDoor } runs[#runs + 1] = { tx = tx, run = run } local h = unit * 8 heightVotes[h] = (heightVotes[h] or 0) + 1 + if repeatRead then repeatVotes[h] = (repeatVotes[h] or 0) + 1 end end end @@ -1337,11 +1527,27 @@ function Structures.buildVolume(S, map, tiles) for h, n in pairs(heightVotes) do if n > modeN or (n == modeN and h > modeH) then modeH, modeN = h, n end end + -- whether the region's dominant columns are flat repeats (a cliff + -- mound's plateau) rather than drawn facades (a house's front) + local modeRepeat = (repeatVotes[modeH] or 0) * 2 > modeN for _, r in ipairs(runs) do local run = r.run local h = run.unit * 8 local adopted = false - if run.fromRepeat and modeH > h then + local flatDoor = false + if run.door then + -- A folded doorway column answers to its region ENTIRELY. Its own + -- reading spans the door plus everything drawn above it -- a + -- house's full height when the door is a house's, but a 32px + -- tower over a 16px plateau when the door is a cave mouth cut + -- into a cliff mound (Diglett's Cave: the entrance jumped a block + -- above the mound around it). Height and top both come from the + -- region: the mode height, roofed like a facade when the mode + -- columns are drawn facades, flat when they are flat repeats. + h = modeH + adopted = not modeRepeat + flatDoor = modeRepeat + elseif run.fromRepeat and modeH > h then h = modeH adopted = true end @@ -1359,7 +1565,8 @@ function Structures.buildVolume(S, map, tiles) -- whole roof area -- and a rooftop tilted into a 48px ramp reads -- wrong instantly. Distinct top rows -> slope; repeated -> level top. local roofRows = 0 - if S.outdoor and (not run.fromRepeat or adopted) and h >= 16 then + if S.outdoor and (not run.fromRepeat or adopted) and h >= 16 + and not flatDoor then roofRows = math.min(2, math.floor(h / 8) - 1) if roofRows > 0 and map:tileAt(r.tx, run.north) == map:tileAt(r.tx, run.north + 1) then @@ -1712,10 +1919,24 @@ function Structures.buildObject(S, map, region, cluster, -- the box's top with its feet on the box's north row, and the claimed -- tiles keep rendering as that box (wearing its plain art) instead of -- punching a floor-level hole through it. + -- + -- Only when the prop's OWN CELL IS BLOCKED, though. "Is something + -- drawn above me?" is not the same question as "am I standing on it": + -- a chair drawn against the north side of a table is above the table's + -- trim row too, and it was being lifted onto the tabletop -- three + -- chairs standing on the furniture in Cinnabar's trade room and + -- Fuchsia's meeting room, with the claimed cells re-tiled as tabletop + -- so the table marched two rows north with them. The world already + -- knows which is which: a thing that sits ON furniture occupies a + -- blocked cell (you cannot walk through the gym statue, Red's plant, + -- the PC), while a seat you walk up to is in a walkable one. local baseY, support = 0, nil if force then local bs = S.shapeAt[keyOf(cluster.minX, cluster.maxY + 1)] - if bs and bs.authored and bs.art == "upright" and (bs.h or 0) > 0 then + local blocked = not map:isWalkableCell(math.floor(cluster.minX / 2), + math.floor(cluster.maxY / 2)) + if blocked and bs and bs.authored and bs.art == "upright" + and (bs.h or 0) > 0 then baseY, support = bs.h, bs end end @@ -1844,7 +2065,17 @@ function Structures.buildObject(S, map, region, cluster, end for _, c in ipairs(cluster.tiles) do local k = keyOf(c[1], c[2]) - if support then + if support and support.class == "wall" then + -- a figure drawn above a FULL-HEIGHT block (the gym statue on its + -- plinth) is a statue on a pillar with ONE cell of footprint: the + -- block below already carries the whole base, so the drawn cell + -- becomes synthesized floor rather than a second block marching + -- the base backwards. Furniture supports (a monitor on its desk) + -- keep the box-extension below -- their drawn cell is the + -- furniture's own upper rows, and floor there would amputate it. + S.skip[k] = true + S.ground[k] = best + elseif support then -- the claimed tile keeps rendering as the box the prop stands on, -- wearing the art its own ROW would have without the drawing (the -- trim row stays trim); only when the whole row is the prop does @@ -1874,14 +2105,21 @@ end -- ---- tall grass ---- --- A tall-grass tile draws its four tuft segments in two ROWS: the art's --- top half is one row of grass, the bottom half another. Each row stands --- as a thin per-pixel slab at its own drawn depth inside the tile, over --- the flat grass base the tile already renders -- so the player walks --- BETWEEN rows, and the southern row occludes their feet the way the 2D --- grass overdraw did. Transparency respected: only the tuft strokes stand. --- Runs of adjacent pixels merge into single quads, and one template per --- grass tile id is stamped across the map (grass comes in fields). +-- A tall-grass CELL is four tufts: 2x2 tiles, and each 8x8 tile is one +-- whole clump of grass. Each tile stands as its own thin per-pixel slab +-- at ITS OWN depth -- the cell's north tile row in the north half of the +-- cell, the south row in the south half -- over the flat grass base the +-- tile already renders. So the player walks BETWEEN the two rows, and +-- the southern row occludes their feet the way the 2D grass overdraw +-- did. Transparency respected: only the tuft strokes stand. Runs of +-- adjacent pixels merge into single quads, and one template per grass +-- tile id is stamped across the map (grass comes in fields). +-- +-- One tile is ONE standing piece, full height. The first cut split each +-- tile again into its top and bottom four art rows and stood those at +-- two different depths, which cut every blade that runs down the tile +-- clean in half -- the two halves ended up 4px tall and 4px apart in +-- depth, so a clump read as two stubs rather than one tuft. local GRASS_THICK = 2 local function grassTemplate(map, data, tileId) @@ -1898,49 +2136,48 @@ local function grassTemplate(map, data, tileId) end local quads = {} - for half = 0, 1 do - local rowBase = half * 4 - local zMid = half == 0 and 2.5 or 6.5 - local zB, zF = zMid - GRASS_THICK / 2, zMid + GRASS_THICK / 2 - for iy = 0, 3 do - local yTop = 4 - iy - local yBot = yTop - 1 - local ix = 0 - while ix < 8 do - if opaque(ix, rowBase + iy) then - local ix2 = ix - while ix2 + 1 < 8 and opaque(ix2 + 1, rowBase + iy) do - ix2 = ix2 + 1 - end - local u0 = (ax0 + ix + 0.05) / atlasW - local u1 = (ax0 + ix2 + 0.95) / atlasW - local v0 = (ay0 + rowBase + iy + 0.05) / atlasH - local v1 = (ay0 + rowBase + iy + 0.95) / atlasH - quads[#quads + 1] = { -- front - { ix, yBot, zF }, { ix2 + 1, yBot, zF }, + -- the slab stands across the middle of its own tile, so the two tile + -- rows of a cell are half a cell apart in depth + local zMid = 4 + local zB, zF = zMid - GRASS_THICK / 2, zMid + GRASS_THICK / 2 + for iy = 0, 7 do + local yTop = 8 - iy + local yBot = yTop - 1 + local ix = 0 + while ix < 8 do + if opaque(ix, iy) then + local ix2 = ix + while ix2 + 1 < 8 and opaque(ix2 + 1, iy) do + ix2 = ix2 + 1 + end + local u0 = (ax0 + ix + 0.05) / atlasW + local u1 = (ax0 + ix2 + 0.95) / atlasW + local v0 = (ay0 + iy + 0.05) / atlasH + local v1 = (ay0 + iy + 0.95) / atlasH + quads[#quads + 1] = { -- front + { ix, yBot, zF }, { ix2 + 1, yBot, zF }, + { ix2 + 1, yTop, zF }, { ix, yTop, zF }, + uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, + shade = 1, + } + quads[#quads + 1] = { -- back + { ix2 + 1, yBot, zB }, { ix, yBot, zB }, + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, + uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, + shade = 0.68, + } + -- blade tips: a top strip where the row above is clear + if not opaque(ix, iy - 1) then + quads[#quads + 1] = { + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, { ix2 + 1, yTop, zF }, { ix, yTop, zF }, - uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, + uv = { { u0, v0 }, { u1, v0 }, { u1, v0 }, { u0, v0 } }, shade = 1, } - quads[#quads + 1] = { -- back - { ix2 + 1, yBot, zB }, { ix, yBot, zB }, - { ix, yTop, zB }, { ix2 + 1, yTop, zB }, - uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, - shade = 0.68, - } - -- blade tips: a top strip where the row above is clear - if not opaque(ix, rowBase + iy - 1) then - quads[#quads + 1] = { - { ix, yTop, zB }, { ix2 + 1, yTop, zB }, - { ix2 + 1, yTop, zF }, { ix, yTop, zF }, - uv = { { u0, v0 }, { u1, v0 }, { u1, v0 }, { u0, v0 } }, - shade = 1, - } - end - ix = ix2 + 1 - else - ix = ix + 1 end + ix = ix2 + 1 + else + ix = ix + 1 end end end @@ -1982,6 +2219,206 @@ function Structures.buildGrass(S, map, x0, x1, y0, y1, data) end end +-- ---- flowers ---- + +-- The animated flower tile stands up as a billboard ONE VOXEL deep, cut +-- to the drawing's darkest tones PLUS everything they enclose -- the +-- round-scenery hull's rule: flood the tile border through every +-- non-dark pixel, and what the flood cannot reach is the flower, its +-- pale petal insides included. The mesh is static and the flower is +-- not, so the geometry spans the UNION of that mask over the base art +-- and every animation frame, and TerrainAtlas rewrites the tile's slot +-- each step with only the CURRENT frame's mask opaque -- the rest keyed +-- to alpha, which the voxel shader discards. The standing silhouette +-- trims itself frame by frame in texture space; the sway animates +-- without a vertex moving, off the same engine clock as the flat path. +-- +-- The ground beneath is synthesized from the commonest flat neighbour, +-- like the ground under a detected prop: the tile's own slot no longer +-- holds art anyone can draw flat. +local FLOWER_THICK = 1 + +local function flowerFrames(tileset, tileId) + local out = {} + local ok, declared = pcall(function() + if tileset.animatedTiles then return tileset.animatedTiles end + local TileRenderer = require("src.render.TileRenderer") + return TileRenderer.defaultAnimatedTiles(tileset) + end) + if not ok then return out end + for _, spec in ipairs(type(declared) == "table" and declared or {}) do + if spec.kind == "frames" and spec.tile == tileId then + for _, path in pairs(spec.images or {}) do + local okF, frame = pcall(Assets.imageData, path) + if okF and frame then out[#out + 1] = frame end + end + end + end + return out +end + +local function flowerTemplate(map, data, tileId) + local tileset = map.tileset + local perRow = tileset.tilesPerRow or 16 + local atlasW = tileset.imageWidth or 128 + local atlasH = tileset.imageHeight or 48 + local ax0 = (tileId % perRow) * 8 + local ay0 = math.floor(tileId / perRow) * 8 + + -- per image: dark tones, then the border flood that finds what they + -- enclose. Each image closes over ITS OWN outline before the union -- + -- a pocket two frames only enclose together is not part of either. + local dark = {} + local function markMask(img, ox, oy) + local d, reach, stack = {}, {}, {} + for py = 0, 7 do + for px = 0, 7 do + local r, g, b, a = img:getPixel(ox + px, oy + py) + if a > 0 and math.min(r, g, b) <= 0.5 then + d[py * 8 + px] = true + end + end + end + for i = 0, 7 do + for _, s in ipairs({ i, 56 + i, i * 8, i * 8 + 7 }) do + if not d[s] and not reach[s] then + reach[s] = true + stack[#stack + 1] = s + end + end + end + while #stack > 0 do + local p = table.remove(stack) + local px, py = p % 8, math.floor(p / 8) + for _, dir in ipairs(DIRS4) do + local nx, ny = px + dir[1], py + dir[2] + if nx >= 0 and nx < 8 and ny >= 0 and ny < 8 then + local ni = ny * 8 + nx + if not d[ni] and not reach[ni] then + reach[ni] = true + stack[#stack + 1] = ni + end + end + end + end + for i = 0, 63 do + if d[i] or not reach[i] then dark[i] = true end + end + end + markMask(data, ax0, ay0) + for _, frame in ipairs(flowerFrames(tileset, tileId)) do + pcall(markMask, frame, 0, 0) + end + + local function on(px, py) + if px < 0 or px > 7 or py < 0 or py > 7 then return false end + return dark[py * 8 + px] == true + end + + local quads = {} + local zB = 4 - FLOWER_THICK / 2 -- one slab at the tile's middle + local zF = zB + FLOWER_THICK + for py = 0, 7 do + Budget.tick() + local yTop, yBot = 8 - py, 7 - py + local ix = 0 + while ix < 8 do + if on(ix, py) then + local ix2 = ix + while ix2 + 1 < 8 and on(ix2 + 1, py) do ix2 = ix2 + 1 end + local u0 = (ax0 + ix + 0.05) / atlasW + local u1 = (ax0 + ix2 + 0.95) / atlasW + local v0 = (ay0 + py + 0.05) / atlasH + local v1 = (ay0 + py + 0.95) / atlasH + quads[#quads + 1] = { -- front + { ix, yBot, zF }, { ix2 + 1, yBot, zF }, + { ix2 + 1, yTop, zF }, { ix, yTop, zF }, + uv = { { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, + shade = OBJ_SHADE.front, + } + quads[#quads + 1] = { -- back + { ix2 + 1, yBot, zB }, { ix, yBot, zB }, + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, + uv = { { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, + shade = OBJ_SHADE.back, + } + -- petal tips: a top strip where the row above is clear. The + -- strip samples its own row's texel, so a tip that is not in + -- the current frame discards with the face beneath it + if not on(ix, py - 1) then + quads[#quads + 1] = { + { ix, yTop, zB }, { ix2 + 1, yTop, zB }, + { ix2 + 1, yTop, zF }, { ix, yTop, zF }, + uv = { { u0, v0 }, { u1, v0 }, { u1, v0 }, { u0, v0 } }, + shade = OBJ_SHADE.top, + } + end + ix = ix2 + 1 + else + ix = ix + 1 + end + end + end + return quads +end + +function Structures.buildFlowers(S, map, tw, th, x0, x1, y0, y1, data) + local templates = {} + -- flowerQuads, not objectQuads: flowers sit on WALKABLE cells, so + -- their mesh draws after the characters with the character pull + -- (ChunkMesher's flower mesh) -- terrain-baked they lose the depth + -- fight against the pulled card whenever the player stands among them + local quads = S.flowerQuads + for ty = y0, y1 do + for tx = x0, x1 do + Budget.tick() + local k = keyOf(tx, ty) + local s = S.shapeAt[k] + if s and s.art == "flower" then + -- the tile's atlas slot carries only the standing cutout now, so + -- EVERY flower position -- ring included -- paints synthesized + -- ground instead of its own art: the commonest flat neighbour + -- that is not itself a flower, else the map's commonest ground + -- (forMap's end-of-build vote resolves the `false`) + S.skip[k] = true + local votes, best, bestN = {}, nil, 0 + for _, d in ipairs(DIRS4) do + local nk = keyOf(tx + d[1], ty + d[2]) + local ns = S.shapeAt[nk] + if ns and ns.flat and ns.class ~= "void" + and ns.class ~= "flower" then + local t = S.tileAt[nk] + votes[t] = (votes[t] or 0) + 1 + if votes[t] > bestN then best, bestN = t, votes[t] end + end + end + S.ground[k] = best or false + + -- standee BODY only, like grass: standing scenery past a map's + -- edge would poke into the map next door + if tx >= 0 and ty >= 0 and tx < tw and ty < th then + local tileId = S.tileAt[k] + local tpl = templates[tileId] + if not tpl then + tpl = flowerTemplate(map, data, tileId) + templates[tileId] = tpl + end + local wx, wz = tx * 8, ty * 8 + for _, q in ipairs(tpl) do + quads[#quads + 1] = { + { q[1][1] + wx, q[1][2], q[1][3] + wz }, + { q[2][1] + wx, q[2][2], q[2][3] + wz }, + { q[3][1] + wx, q[3][2], q[3][3] + wz }, + { q[4][1] + wx, q[4][2], q[4][3] + wz }, + uv = q.uv, shade = q.shade, + } + end + end + end + end + end +end + -- Drop one map's analysis (Cut changed the block layer) or everything. -- Hull templates key on art content (tileset + tiles), which a block edit -- cannot change, so only the full drop clears them (atlas reload). diff --git a/lib/TerrainAtlas.lua b/lib/TerrainAtlas.lua index ca3abf2..8d12e36 100644 --- a/lib/TerrainAtlas.lua +++ b/lib/TerrainAtlas.lua @@ -27,6 +27,9 @@ -- the first place (home/vcopy.asm rewrites the tile's VRAM bytes); the 2D -- overdraw is the port's workaround, not the original. +-- the mod namespace (see main.lua): V.require loads a sibling module +local V = ... + local Assets = require("src.render.Assets") local TileRenderer = require("src.render.TileRenderer") local PaletteFX = require("src.render.PaletteFX") @@ -146,6 +149,8 @@ local function learnShades(raw, baked, tile, perRow) return map end +local DIRS4 = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } } + -- One animated entry's tile slot, written into `out` at step `step`. local function patch(out, entry, spec, step) local perRow, tile = entry.perRow, spec.tile @@ -167,12 +172,59 @@ local function patch(out, entry, spec, step) local ok, frame = pcall(Assets.imageData, path) if not ok or not frame then return end local shades = entry.shades and entry.shades[tile] + -- a `cut` tile is the flower billboard's slot (Structures' + -- buildFlowers): only the frame's darkest tones AND what they + -- enclose stay opaque -- the round-scenery hull's rule, flooding + -- the frame border through every non-dark pixel so the pale petal + -- insides survive with the outline. The true background is keyed + -- to alpha; nothing else samples this slot (the ground under a + -- flower is synthesized), and the shader's discard is what lets + -- the billboard's silhouette change per frame under geometry that + -- never moves. + local mask = nil + if entry.cut and entry.cut[tile] then + local dark, reach, stack = {}, {}, {} + for y = 0, 7 do + for x = 0, 7 do + local r, _, _, a = frame:getPixel(x, y) + if a > 0 and shadeOf(r) >= 3 then dark[y * 8 + x] = true end + end + end + for i = 0, 7 do + for _, s in ipairs({ i, 56 + i, i * 8, i * 8 + 7 }) do + if not dark[s] and not reach[s] then + reach[s] = true + stack[#stack + 1] = s + end + end + end + while #stack > 0 do + local p = table.remove(stack) + local px, py = p % 8, math.floor(p / 8) + for _, d in ipairs(DIRS4) do + local nx, ny = px + d[1], py + d[2] + if nx >= 0 and nx < 8 and ny >= 0 and ny < 8 then + local ni = ny * 8 + nx + if not dark[ni] and not reach[ni] then + reach[ni] = true + stack[#stack + 1] = ni + end + end + end + end + mask = {} + for i = 0, 63 do mask[i] = dark[i] or not reach[i] end + end for y = 0, 7 do for x = 0, 7 do local r, g, b, a = frame:getPixel(x, y) - local col = shades and shades[shadeOf(r)] - if col and a > 0 then r, g, b = col[1], col[2], col[3] end - out:setPixel(dx + x, dy + y, r, g, b, a) + if mask and not mask[y * 8 + x] then + out:setPixel(dx + x, dy + y, 0, 0, 0, 0) + else + local col = shades and shades[shadeOf(r)] + if col and a > 0 then r, g, b = col[1], col[2], col[3] end + out:setPixel(dx + x, dy + y, r, g, b, a) + end end end end @@ -427,6 +479,28 @@ local function newEntry(map, base, baked) end end end + -- a frame-animated tile that resolved to the `flower` class stands as + -- a 1px billboard (Structures.buildFlowers), and its slot in THIS + -- copy carries only each frame's dark tones with the rest keyed to + -- alpha -- see patch(). Gated on the resolved shape so a profile that + -- pins the tile to something solid keeps a fully opaque slot, and on + -- the tileset art being readable: without pixels Structures cannot + -- have stood the billboard up (or synthesized the ground), and an + -- alpha-keyed slot under an ordinary flat quad is a hole in the world. + for _, spec in ipairs(specs) do + if spec.kind == "frames" then + local okCut, isFlower = pcall(function() + local okA, art = pcall(Assets.imageData, tileset.image) + if not (okA and art and art.getPixel) then return false end + local sh = V.require("TileShape").forMap(map)[spec.tile] + return sh ~= nil and sh.class == "flower" + end) + if okCut and isFlower then + e.cut = e.cut or {} + e.cut[spec.tile] = true + end + end + end return e end) return (ok and entry) or false diff --git a/lib/TileShape.lua b/lib/TileShape.lua index 00deba6..1e11c59 100644 --- a/lib/TileShape.lua +++ b/lib/TileShape.lua @@ -56,10 +56,20 @@ local FALLBACK_HEIGHTS = { tree = 16, roof = 28, cylinder = 16, + -- big round scenery: a 2x2-CELL drawing carved as ONE 32px voxel hull + -- (Viridian Forest's trees). The class pins only the drawing's + -- top-left corner tile; the other cells stay `cylinder` and are + -- claimed by the group build (see Structures.buildCylinders) + canopy = 32, + -- a cylinder hull whose drawn top is a CUT FACE (tree stumps): the + -- body builds from the bark rows and the drawn ellipse projects onto + -- the hull's round top + stump = 16, billboard = 16, signpost = 16, post = 16, grass = 0, + flower = 0, -- interior furniture: face-on drawings the detector would otherwise -- raise to wall height (or merge into the wall). A bed is drawn from -- above and lies low; tables and desks are boxes at their real height; @@ -103,6 +113,8 @@ local ART = { fence = "upright", sign = "upright", cylinder = "cylinder", + canopy = "canopy", + stump = "cylinder", billboard = "billboard", -- signposts share the billboard treatment but as their own pool at a -- 2-voxel depth: a sign is a thin plate on a stick, and the standard @@ -110,6 +122,11 @@ local ART = { signpost = "billboard", post = "post", grass = "grass", + -- animated flowers: flat synthesized ground PLUS a standing cutout of + -- the drawing's darkest tones, one voxel deep (see Structures' + -- buildFlowers). Height 0 so a build with no pixel access degrades to + -- the flat tile it always drew, not a box + flower = "flower", -- furniture: a bed's art depicts its top surface; tables and desks are -- boxes whose fronts fold up (the mesher's authored-fold rule). -- Stools, `prop` and `cutout` are standee pools alongside `billboard` @@ -185,12 +202,56 @@ local function authoredGroups(tilesetId, heights) return out end +-- Conditional pins: tile id -> list of { above = {tile ids}, class }. +-- +-- A pin is per TILE ID, and one graphic can mean two things. The route +-- gates' $32/$33 is the case that forced this: the artist reuses it for +-- the wall's dark base course AND for every service counter's front, and +-- it is the bottom row of its cell either way. Pinned `wall` the counter +-- stands a full 16px; pinned `counter` the wall bank corrugates 16/8 for +-- sixteen rows. Neither is right, and no per-tile pin can be, because +-- forMap resolves an id to ONE shape. +-- +-- What separates the two uses is what is drawn ABOVE: the wall's upper +-- course over a wall base, the counter's top over a counter front. So a +-- profile entry may carry `when_above = { [tile] = { { above = {...}, +-- class = "..." } } }`, evaluated per POSITION in TileShape.at, where +-- the map and coordinates are in hand. First match wins; no match keeps +-- the tile's ordinary pin. +local function authoredConditions(tilesetId, heights) + local s = load() + local entry = s and s.tilesets and s.tilesets[tilesetId] + local spec = entry and entry.when_above + if type(spec) ~= "table" then return nil end + local out, any = {}, false + for tile, rules in pairs(spec) do + if type(tile) == "number" and type(rules) == "table" then + local list = {} + for _, rule in ipairs(rules) do + if type(rule) == "table" and heights[rule.class] + and type(rule.above) == "table" then + local set = {} + for _, t in ipairs(rule.above) do set[t] = true end + list[#list + 1] = { above = set, class = rule.class } + end + end + if #list > 0 then + out[tile] = list + any = true + end + end + end + return any and out or nil +end + local function shapeFor(class, heights, authored) return { class = class, h = heights[class] or 0, art = ART[class] or "upright", - -- grass draws its flat ground base like any walkable tile; the - -- standing tuft rows are additive geometry from Structures - flat = ART[class] == "flat" or class == "grass", + -- grass and flowers draw a flat ground base like any walkable + -- tile; the standing tufts and cutouts are additive geometry + -- from Structures + flat = ART[class] == "flat" or class == "grass" + or class == "flower", authored = authored or false } end @@ -210,7 +271,29 @@ function TileShape.forMap(map) local count = math.floor((tileset.imageWidth or 128) / 8) * math.floor((tileset.imageHeight or 48) / 8) - local shapes = { classes = {} } + -- derived pin: a tile the tileset animates by FRAME REWRITE (the + -- overworld's flower) is already named by its animation spec, so like + -- tall grass it needs no profile entry anywhere. Hand-authoring still + -- wins -- a mod animating a wall tile this way keeps its wall by + -- listing it. Guarded because the spec seam is engine data a stub map + -- may not carry. + local flowerTiles = {} + do + local ok, declared = pcall(function() + if tileset.animatedTiles then return tileset.animatedTiles end + local TileRenderer = require("src.render.TileRenderer") + return TileRenderer.defaultAnimatedTiles(tileset) + end) + if ok then + for _, spec in ipairs(type(declared) == "table" and declared or {}) do + if spec.kind == "frames" and spec.tile then + flowerTiles[spec.tile] = true + end + end + end + end + + local shapes = { classes = {}, cond = authoredConditions(id, heights) } for class in pairs(FALLBACK_HEIGHTS) do shapes.classes[class] = shapeFor(class, heights) end @@ -222,6 +305,8 @@ function TileShape.forMap(map) -- derived pin: every tileset already names its tall-grass tile, so -- the standing-tuft treatment needs no profile entry anywhere shapes[t] = shapeFor("grass", heights, true) + elseif flowerTiles[t] then + shapes[t] = shapeFor("flower", heights, true) elseif map.waterTiles and map.waterTiles[t] then shapes[t] = shapes.classes.water elseif map.walkable and map.walkable[t] then @@ -241,6 +326,19 @@ end -- map:tileAt(tx, ty), passed in because every caller already has it. function TileShape.at(map, shapes, tile, tx, ty) local s = shapes[tile] + -- conditional pins first: they are authored answers that need the + -- POSITION to resolve, so they outrank both the flat pin on the same + -- tile and the cell rules below (see authoredConditions) + local rules = shapes.cond and shapes.cond[tile] + if rules then + local above = map:tileAt(tx, ty - 1) + for _, rule in ipairs(rules) do + if above and rule.above[above] then + shapes.classes[rule.class].authored = true + return shapes.classes[rule.class] + end + end + end if not s or s.authored then return s end local cx = math.floor(tx / 2) local cy = math.floor(ty / 2) diff --git a/lib/VoxelScene.lua b/lib/VoxelScene.lua index c4d0742..d8b584b 100644 --- a/lib/VoxelScene.lua +++ b/lib/VoxelScene.lua @@ -460,6 +460,14 @@ local function castShadows(state, terrain, nbMesh, posed, cx, cy, vw, vh, ShadowMap.draw(nbMesh[i], atlasFor(nb.map), Mat4.translate(nb.ox, 0, nb.oy)) end + -- flower billboards live outside the terrain mesh (they draw after the + -- characters, pulled -- see render), but the sun still sees them: a + -- handful of cutouts per meadow, unlike the grass left out below + ShadowMap.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil) + for _, nb in ipairs(state.neighbors or {}) do + ShadowMap.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map), + Mat4.translate(nb.ox, 0, nb.oy)) + end for _, p in ipairs(posed) do local def = p.sprite.def local frame, mirror = frameFor(def, p.facing, p.phase, p.flip) @@ -559,6 +567,23 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor) Voxel3D.draw(ChunkMesher.grass(nb.map), atlasFor(nb.map), Mat4.translate(nb.ox, 0, nb.oy), pull) end + -- flower billboards: pulled like the characters and the grass, MINUS + -- the depth of 8 world pixels along the view (8 sin a -- the camera + -- looks along (0, -cos a, -sin a), so that is exactly one tile row of + -- northness). A pure depth handicap with zero screen drift: every + -- flower is judged as if it stood one tile row further north. The + -- character card's feet plane sits at its cell's MIDDLE (py + 8), so + -- a flower on the walker's own cell (z +4 or +12 across the cell) + -- lands behind the card and the player obscures the patch they stand + -- ON, while the nearest flower of the cell south (+20) stays in front + -- and keeps overdrawing their feet. + local fpull = math.max(0, pull - 8 * math.sin(math.max(Voxel.angle, 0.05))) + Voxel3D.draw(ChunkMesher.flowers(state.map), atlasFor(state.map), nil, + fpull) + for _, nb in ipairs(state.neighbors or {}) do + Voxel3D.draw(ChunkMesher.flowers(nb.map), atlasFor(nb.map), + Mat4.translate(nb.ox, 0, nb.oy), fpull) + end return Voxel3D.endScene() end diff --git a/main.lua b/main.lua index 94f7fd8..fd9a5a7 100644 --- a/main.lua +++ b/main.lua @@ -344,9 +344,13 @@ end) -- tree that is no longer there. The 2D tile renderer invalidates its own -- caches off the same edit. +-- refresh, not invalidate: the stale mesh keeps drawing while the +-- replacement builds in the background, so a one-block edit (Cut, a +-- door stamp, the tree regrowing on re-entry) repopulates in place +-- instead of blinking the whole scene down to the flat 2D path mod.events:on("world.block_replaced", function(payload) local mapId = payload and (payload.mapId or (payload.map and payload.map.id)) - if mapId then ChunkMesher.invalidate(mapId) end + if mapId then ChunkMesher.refresh(mapId) end end) -- A reloaded map is rebuilt from scratch (warps that re-enter the same map, @@ -372,7 +376,7 @@ mod.events:on("map.reloaded", function(payload) if mapId then ChunkMesher.invalidate(mapId) end end) -mod.exports.version = "1.0.3" +mod.exports.version = "1.0.6" -- exposed so a companion mod can pin its own tiles' shapes or read the -- camera without reaching into this mod's file layout mod.exports.lib = V diff --git a/manifest.json b/manifest.json index be6dca3..89de570 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "DRAMATIC_SHAPE", "name": "Dramatic Shape Voxel Mod", - "version": "1.0.3", + "version": "1.0.6", "api": 2, "entry": "main.lua", "profile": "content", diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua index cc752fd..5447507 100644 --- a/tests/dramatic_shape_test.lua +++ b/tests/dramatic_shape_test.lua @@ -420,6 +420,98 @@ T.check(okClock and type(clockVal) == "number", "a clock that throws falls back to a working one rather than propagating") TileRenderer.animFrame = nil +-- ------- the flower's slot carries an animated SILHOUETTE, not a tile +-- +-- The flower tile stands in voxel mode as a billboard one voxel deep +-- (Structures.buildFlowers), cut to the drawing's darkest tones. Meshes +-- are static, so the geometry spans the union of every frame's dark +-- pixels and the animation lives in the atlas: patch() keys everything +-- lighter to alpha 0, the shader discards it, and the silhouette trims +-- itself frame by frame. Two halves to pin down: the CLASS is derived +-- (any frames-animated tile resolves `flower` with no profile entry), +-- and the PATCH writes alpha where the frame is not dark. + +local TileShape = run.loader.exports.DRAMATIC_SHAPE.lib.require("TileShape") + +local flowerSet = { + id = "T_FLOWER_PIN", image = "assets/tilesets/stub.png", + tilesPerRow = 16, imageWidth = 128, imageHeight = 48, + animatedTiles = { { tile = 0x03, kind = "frames", period = 20, + images = { "stub_flowerframe.png" }, + sequence = { 1 } } }, +} +local flowerShapes = TileShape.forMap({ tileset = flowerSet }) +T.eq(flowerShapes[0x03].class, "flower", + "a frames-animated tile is pinned `flower` with no profile entry, like grass") +T.check(flowerShapes[0x03].flat, + "the flower cell still counts as flat ground for its neighbours") +T.eq(flowerShapes[0x03].h, 0, + "and carries no height, so a build with no pixel access degrades to the flat tile") +T.check(flowerShapes[0x03].authored, + "the pin is authored-strength: cell walkability cannot re-file it as plain ground") + +-- the patch, observed through a recording atlas copy: a crafted frame +-- whose dark pixels form a diamond ring around one light pixel -- the +-- billboard must keep the ring AND the pale pixel it encloses, and key +-- the reachable background (light or transparent) to alpha +local slotPx = {} +local sectionNewImageData = love.image.newImageData +do + local crafted = fakePixels() + local ring = { ["1,0"] = true, ["0,1"] = true, + ["2,1"] = true, ["1,2"] = true } + local frame = fakePixels() + function frame:getPixel(x, y) + if ring[x .. "," .. y] then return 0.3, 0.3, 0.3, 1 end -- dark outline + if x == 7 and y == 0 then return 0.9, 0.9, 0.9, 0 end -- transparent + return 1, 1, 1, 1 -- light: petal inside at (1,1), + end -- background everywhere else + love.image.newImageData = function(a, b) + if type(a) == "number" then + local d = fakePixels(a, b) + function d:setPixel(x, y, r, g, b2, al) + slotPx[x .. "," .. y] = { r, g, b2, al } + end + return d + end + if tostring(a):find("flowerframe") then return frame end + return crafted + end +end + +TerrainAtlas.invalidate() +local fmap = { + id = "T_FLOWER_PATCH_MAP", + tileset = { + id = "T_FLOWER_PATCH", image = "assets/tilesets/stub2.png", + tilesPerRow = 16, imageWidth = 128, imageHeight = 48, + animatedTiles = { { tile = 0x03, kind = "frames", period = 20, + images = { "stub_flowerframe.png" }, + sequence = { 1 } } }, + }, + renderer = { image = base }, +} +local okFlower, flowerImg = pcall(TerrainAtlas.animate, fmap, nil, base, false) +T.check(okFlower and flowerImg ~= nil, + "a flower map animates: " .. tostring(flowerImg)) + +local fdx = (0x03 % 16) * 8 +local function slotAlpha(x, y) + local p = slotPx[(fdx + x) .. "," .. y] + return p and p[4] +end +T.eq(slotAlpha(1, 0), 1, + "a dark frame pixel lands opaque -- it is the standing cutout") +T.eq(slotAlpha(1, 1), 1, + "and so does the pale pixel the outline encloses -- the petal's inside " + .. "rides the billboard, not just its outline") +T.eq(slotAlpha(5, 5), 0, + "a background pixel is keyed to alpha, so the billboard's shader discards it") +T.eq(slotAlpha(7, 0), 0, + "a transparent frame pixel stays clear rather than painting black") + +love.image.newImageData = sectionNewImageData + TerrainAtlas.invalidate() love.image, love.graphics.newImage = realImage, realNewImage @@ -443,8 +535,10 @@ local ChunkMesher = run.loader.exports.DRAMATIC_SHAPE.lib.require("ChunkMesher") local Runtime = require("src.mods.Runtime") local realInvalidate = ChunkMesher.invalidate -local dropped = {} +local realRefresh = ChunkMesher.refresh +local dropped, refreshed = {}, {} ChunkMesher.invalidate = function(id) dropped[#dropped + 1] = id or "" end +ChunkMesher.refresh = function(id) refreshed[#refreshed + 1] = id or "" end Runtime.emit("map.reloaded", { mapId = "PALLET_TOWN", reason = "colors" }) T.eq(#dropped, 0, @@ -459,11 +553,16 @@ T.eq(dropped[1], "PALLET_TOWN", "and drops exactly the map that reloaded") Runtime.emit("map.reloaded", { mapId = "VIRIDIAN_CITY" }) T.eq(#dropped, 2, "a reload with no stated reason is treated as a real one") --- and the edits that genuinely change geometry are untouched by any of this +-- a block edit (Cut, a door stamp, the tree regrowing on re-entry) is a +-- REFRESH, not a drop: the stale mesh keeps drawing while the rebuild +-- cooks, so the scene never blinks down to the flat 2D path Runtime.emit("world.block_replaced", { mapId = "PALLET_TOWN" }) -T.eq(#dropped, 3, "a replaced block still drops the mesh it changed") +T.eq(#dropped, 2, "a replaced block does not drop the mesh outright") +T.eq(#refreshed, 1, "it refreshes the mesh in place instead") +T.eq(refreshed[1], "PALLET_TOWN", "and refreshes exactly the edited map") ChunkMesher.invalidate = realInvalidate +ChunkMesher.refresh = realRefresh -- ------- the non-colour palette modes must not come through as SGB -- diff --git a/tests/voxel_anim_probe.lua b/tests/voxel_anim_probe.lua index 5b6c5ff..3ff4475 100644 --- a/tests/voxel_anim_probe.lua +++ b/tests/voxel_anim_probe.lua @@ -94,6 +94,8 @@ return function(game) if S then local def = ow.map.def for _, spec in ipairs(specs) do + if not spec.tile then goto continue end -- "toggle" specs claim + -- tile LISTS, not one slot local flat, prop, run, first = 0, 0, 0, nil for ty = 0, def.height * 4 - 1 do for tx = 0, def.width * 4 - 1 do @@ -112,17 +114,32 @@ return function(game) .. " %d inside a prop%s"):format(spec.tile, flat, run, prop, first and (" -- first at cell " .. math.floor(first[1] / 2) .. "," .. math.floor(first[2] / 2)) or "")) + ::continue:: end end end -- one animation period is 20 logic frames at 60Hz, and TileRenderer.tick - -- runs on wall-clock steps, so ~22 rendered frames lands the next step + -- runs on wall-clock steps, so ~22 rendered frames lands the next step. + -- animFrame is an OPTIONAL engine seam this build may not export; the + -- mod's own clock (TerrainAtlas._animFrame) reads the same counter by + -- whatever route exists, so ask it first. + local function clock() + if TerrainAtlas and TerrainAtlas._animFrame then + local ok, f = pcall(TerrainAtlas._animFrame) + if ok and type(f) == "number" then return f end + end + if TileRenderer.animFrame then + local ok, f = pcall(TileRenderer.animFrame) + if ok and type(f) == "number" then return f end + end + return -1 + end for i = 1, shots do game.capturePath = ("%s/anim_%s_L%d_%02d.png"):format(DIR, mapId, level, i) wait(3) print(("[anim] shot %d at animFrame %d step %d"):format( - i, TileRenderer.animFrame(), math.floor(TileRenderer.animFrame() / 20) % 8)) + i, clock(), math.floor(clock() / 20) % 8)) dumpAtlas(tostring(i)) wait(22) end diff --git a/tests/voxel_building_probe.lua b/tests/voxel_building_probe.lua new file mode 100644 index 0000000..848a134 --- /dev/null +++ b/tests/voxel_building_probe.lua @@ -0,0 +1,67 @@ +-- Driver: which building models exist, and how much of this map is +-- claimed by objects? +-- +-- Buildings.build matches every profiled template against the whole map +-- and stamps each hit, so two templates whose tile grids both match one +-- drawing stamp TWO models into the same cells. +-- +-- READ THE CAVEAT BEFORE TRUSTING THIS. Buildings.stats() reports the +-- module's `models` cache, which is keyed ":" and is +-- GLOBAL AND CUMULATIVE for the session -- it lists every model built +-- since boot, including ones from maps visited earlier, and says nothing +-- about where they were placed. It is a memoisation table, not a +-- placement log. Reading it as "what is on this map" will convince you a +-- foreign building has stamped here when it has not. +-- +-- To answer "does template X place on map Y", match its `tiles` grid +-- against the map's tile grid OFFLINE (data/generated/maps.lua plus +-- tilesets.lua) -- no engine needed, and it gives you the coordinates. +-- And note a drawing can STRADDLE A MAP BOUNDARY (the Pokemon Tower's +-- roof is on ROUTE_10, its facade on LAVENDER_TOWN), in which case no +-- single map's grid holds the whole thing and each half meshes alone. +-- +-- BUILD_MAP=LAVENDER_TOWN POKEPORT_DRIVER=mods/DRAMATIC_SHAPE/tests/voxel_building_probe.lua lovec . +return function(game) + local U = dofile("tests/drivers/util.lua") + local mapId = os.getenv("BUILD_MAP") or "LAVENDER_TOWN" + U.teleport(game, mapId, tonumber(os.getenv("BUILD_X") or "3"), + tonumber(os.getenv("BUILD_Y") or "5"), "up") + U.wait(10) + + local V = game.mods.exports["DRAMATIC_SHAPE"] + V = V and V.lib + local Structures = V and V.require("Structures") + local Buildings = V and V.require("Buildings") + local ow = game.overworld + if not (Structures and Buildings and ow and ow.map) then + print("[bld] mod or map unavailable") + return + end + + -- placements are recorded as the templates are stamped, so force the + -- analysis first + Structures.forMap(ow.map) + + print(("[bld] %s"):format(mapId)) + local stats = Buildings.stats and Buildings.stats() or {} + local keys = {} + for k in pairs(stats) do keys[#keys + 1] = k end + table.sort(keys) + for _, k in ipairs(keys) do + local s = stats[k] + print(("[bld] %-24s voxels=%s shell=%s quads=%s"):format( + k, tostring(s.voxels), tostring(s.shell), tostring(s.quads))) + end + + -- and the per-cell truth: which cells a building claimed + local S = Structures.forMap(ow.map) + local def = ow.map.def + local claimed = 0 + for ty = 0, def.height * 4 - 1 do + for tx = 0, def.width * 4 - 1 do + if S.skip[(ty + 64) * 4096 + (tx + 64)] then claimed = claimed + 1 end + end + end + print(("[bld] tiles claimed by objects/buildings: %d"):format(claimed)) + print("[bld] done") +end diff --git a/tests/voxel_mound_probe.lua b/tests/voxel_mound_probe.lua new file mode 100644 index 0000000..11ad540 --- /dev/null +++ b/tests/voxel_mound_probe.lua @@ -0,0 +1,66 @@ +-- Driver: what did the detector decide over a rectangle of tiles? +-- +-- Prints, for every tile of MOUND_RECT on MOUND_MAP, the resolved shape +-- class and the volume run height Structures gave it -- the ground truth +-- for diagnosing cliff/mound columns that extrude to the wrong height +-- (Diglett's Cave entrance ridge, bug of 2026-07-27). Then a shot. +-- +-- POKEPORT_DRIVER=mods/DRAMATIC_SHAPE/tests/voxel_mound_probe.lua lovec . +-- +-- knobs (env): +-- MOUND_MAP map id (default ROUTE_2) +-- MOUND_RECT "tx0,ty0,tx1,ty1" in tiles (default 16,12,35,23) +-- MOUND_SPOT "x,y[,facing]" player cell (default 12,12,up) +-- SHOT_DIR output directory, must exist (default "shots") +return function(game) + local U = dofile("tests/drivers/util.lua") + local Pipelines = require("src.render.Pipelines") + + local DIR = os.getenv("SHOT_DIR") or "shots" + local mapId = os.getenv("MOUND_MAP") or "ROUTE_2" + local rx0, ry0, rx1, ry1 = (os.getenv("MOUND_RECT") or "16,12,35,23") + :match("^(%-?%d+),(%-?%d+),(%-?%d+),(%-?%d+)$") + rx0, ry0, rx1, ry1 = tonumber(rx0), tonumber(ry0), tonumber(rx1), tonumber(ry1) + local sx, sy, facing = (os.getenv("MOUND_SPOT") or "12,12,up") + :match("^%s*(%d+)%s*,%s*(%d+)%s*,?%s*(%a*)") + facing = (facing ~= "" and facing) or "up" + + local Zoom = require("src.render.Zoom") + Zoom.reset() + Pipelines.setLevel("tiltshift", 0) + U.teleport(game, mapId, tonumber(sx), tonumber(sy), facing) + U.wait(20) + Pipelines.setLevel("voxel", + math.floor(tonumber(os.getenv("MOUND_LEVEL")) or 3)) + U.wait(30) + + local V = game.mods.exports["DRAMATIC_SHAPE"] + V = V and V.lib + local Structures = V and V.require("Structures") + local ow = game.overworld + if Structures and ow and ow.map then + local S = Structures.forMap(ow.map) + local function keyOf(tx, ty) return (ty + 64) * 4096 + (tx + 64) end + print(("[mound] %s tiles (%d,%d)-(%d,%d): tile/class/h per column") + :format(mapId, rx0, ry0, rx1, ry1)) + for ty = ry0, ry1 do + local row = {} + for tx = rx0, rx1 do + local k = keyOf(tx, ty) + local s = S.shapeAt[k] + local run = S.runs[k] + local h = run and run.h or (s and s.h) or 0 + if S.skip[k] then h = 0 end + row[#row + 1] = ("%02X%s%02d"):format(S.tileAt[k] or 0xFF, + s and s.class:sub(1, 1) or "?", h) + end + print("[mound] " .. table.concat(row, " ")) + end + end + + game.capturePath = ("%s/mound_%s.png"):format(DIR, mapId) + U.wait(5) + Pipelines.setLevel("voxel", 0) + U.wait(5) + print("[mound] done") +end