initial commit
DramaticShape · 2026-07-26 ·
parent none (root commit)
initial commit
Changed files
CHANGELOG.md | 545 ++++++++
README.md | 120 ++
assets/docs/buidling_to_voxel/sprite_extract.py | 62 +
assets/docs/buidling_to_voxel/sprite_to_voxel_methodology.md | 216 ++++
assets/docs/buidling_to_voxel/voxel_build_verify.py | 162 +++
assets/docs/buildings/B01-unnamed-building.md | 129 ++
assets/docs/buildings/B02-unnamed-building.md | 130 ++
assets/docs/buildings/B03-unnamed-building.md | 134 ++
assets/docs/buildings/B04-unnamed-building.md | 111 ++
assets/docs/buildings/B05-unnamed-building.md | 133 ++
assets/docs/buildings/B06-unnamed-building.md | 130 ++
assets/docs/buildings/B07-unnamed-building.md | 122 ++
assets/docs/buildings/B08-unnamed-building.md | 143 +++
assets/docs/buildings/B09-unnamed-building.md | 124 ++
assets/docs/buildings/B10-unnamed-building.md | 131 ++
assets/docs/buildings/B11-unnamed-building.md | 102 ++
assets/docs/buildings/B12-unnamed-building.md | 99 ++
assets/docs/buildings/B13-unnamed-building.md | 119 ++
assets/docs/buildings/B14-unnamed-building.md | 140 +++
assets/docs/buildings/B15-unnamed-building.md | 144 +++
assets/docs/buildings/B16-unnamed-building.md | 129 ++
assets/docs/buildings/B17-unnamed-building.md | 103 ++
assets/docs/buildings/B18-unnamed-building.md | 121 ++
assets/docs/buildings/B19-unnamed-building.md | 135 ++
assets/docs/buildings/B20-unnamed-building.md | 208 ++++
assets/docs/buildings/B21-unnamed-building.md | 142 +++
assets/docs/buildings/B22-unnamed-building.md | 168 +++
assets/docs/buildings/B23-unnamed-building.md | 109 ++
assets/docs/buildings/B24-unnamed-building.md | 145 +++
assets/docs/buildings/B25-unnamed-building.md | 142 +++
assets/docs/buildings/B26-unnamed-building.md | 138 +++
assets/docs/buildings/B27-unnamed-building.md | 116 ++
assets/docs/buildings/B28-unnamed-building.md | 120 ++
assets/docs/buildings/B29-unnamed-building.md | 123 ++
assets/docs/buildings/B30-unnamed-building.md | 108 ++
assets/docs/buildings/B31-unnamed-building.md | 126 ++
assets/docs/buildings/B32-unnamed-building.md | 156 +++
assets/docs/buildings/B33-unnamed-building.md | 125 ++
assets/docs/buildings/B34-unnamed-building.md | 97 ++
assets/docs/buildings/README.md | 288 +++++
assets/docs/buildings/REMAINING.md | 88 ++
assets/voxels/agatha.lua | 322 +++++
assets/voxels/balding_guy.lua | 148 +++
assets/voxels/beauty.lua | 299 +++++
assets/voxels/bike_shop_clerk.lua | 155 +++
assets/voxels/biker.lua | 330 +++++
assets/voxels/bird.lua | 292 +++++
assets/voxels/blue.lua | 308 +++++
assets/voxels/boulder.lua | 189 +++
assets/voxels/brunette_girl.lua | 299 +++++
assets/voxels/bruno.lua | 336 +++++
assets/voxels/captain.lua | 149 +++
assets/voxels/channeler.lua | 267 ++++
assets/voxels/clerk.lua | 151 +++
assets/voxels/clipboard.lua | 127 ++
assets/voxels/cook.lua | 330 +++++
assets/voxels/cooltrainer_f.lua | 302 +++++
assets/voxels/cooltrainer_m.lua | 300 +++++
assets/voxels/daisy.lua | 270 ++++
assets/voxels/fairy.lua | 334 +++++
assets/voxels/fisher.lua | 320 +++++
assets/voxels/fishing_guru.lua | 168 +++
assets/voxels/fossil.lua | 138 +++
assets/voxels/gambler.lua | 272 ++++
assets/voxels/gambler_asleep.lua | 234 ++++
assets/voxels/gameboy_kid.lua | 161 +++
assets/voxels/gentleman.lua | 323 +++++
assets/voxels/giovanni.lua | 270 ++++
assets/voxels/girl.lua | 282 +++++
assets/voxels/gramps.lua | 161 +++
assets/voxels/granny.lua | 157 +++
assets/voxels/guard.lua | 155 +++
assets/voxels/gym_guide.lua | 139 +++
assets/voxels/hiker.lua | 285 +++++
assets/voxels/koga.lua | 296 +++++
assets/voxels/lance.lua | 367 ++++++
assets/voxels/link_receptionist.lua | 155 +++
assets/voxels/little_boy.lua | 131 ++
assets/voxels/little_girl.lua | 257 ++++
assets/voxels/lorelei.lua | 273 ++++
assets/voxels/middle_aged_man.lua | 310 +++++
assets/voxels/middle_aged_woman.lua | 295 +++++
assets/voxels/mom.lua | 151 +++
assets/voxels/monster.lua | 367 ++++++
assets/voxels/mr_fuji.lua | 287 +++++
assets/voxels/nurse.lua | 163 +++
assets/voxels/oak.lua | 312 +++++
assets/voxels/old_amber.lua | 96 ++
assets/voxels/paper.lua | 120 ++
assets/voxels/poke_ball.lua | 115 ++
assets/voxels/pokedex.lua | 139 +++
assets/voxels/red.lua | 299 +++++
assets/voxels/red_bike.lua | 341 +++++
assets/voxels/rocker.lua | 267 ++++
assets/voxels/rocket.lua | 328 +++++
assets/voxels/safari_zone_worker.lua | 154 +++
assets/voxels/sailor.lua | 294 +++++
assets/voxels/scientist.lua | 281 +++++
assets/voxels/seel.lua | 332 +++++
assets/voxels/silph_president.lua | 163 +++
assets/voxels/silph_worker_f.lua | 262 ++++
assets/voxels/silph_worker_m.lua | 160 +++
assets/voxels/snorlax.lua | 183 +++
assets/voxels/super_nerd.lua | 299 +++++
assets/voxels/swimmer.lua | 223 ++++
assets/voxels/waiter.lua | 290 +++++
assets/voxels/warden.lua | 163 +++
assets/voxels/youngster.lua | 288 +++++
data/voxel_heights.lua | 1000 +++++++++++++++
lib/BuildBudget.lua | 61 +
lib/Buildings.lua | 694 +++++++++++
lib/ChunkMesher.lua | 963 ++++++++++++++
lib/Mat4.lua | 112 ++
lib/ModSetting.lua | 132 ++
lib/ShadowMap.lua | 392 ++++++
lib/SpriteBillboards.lua | 200 +++
lib/Structures.lua | 2001 ++++++++++++++++++++++++++++++
lib/TerrainAtlas.lua | 344 +++++
lib/TileShape.lua | 259 ++++
lib/TiltShift.lua | 177 +++
lib/Voxel3D.lua | 582 +++++++++
lib/VoxelGrid.lua | 71 ++
lib/VoxelModels.lua | 147 +++
lib/VoxelScene.lua | 433 +++++++
lib/VoxelState.lua | 111 ++
lib/WorldCurve.lua | 101 ++
main.lua | 248 ++++
manifest.json | 17 +
mod.card | 30 +
tests/dramatic_shape_test.lua | 173 +++
tools/__pycache__/build_voxels.cpython-313.pyc | Bin 0 -> 35994 bytes
tools/__pycache__/building_images.cpython-313.pyc | Bin 0 -> 7823 bytes
tools/__pycache__/building_voxels.cpython-313.pyc | Bin 0 -> 36647 bytes
tools/build_voxels.py | 645 ++++++++++
tools/building_images.py | 112 ++
tools/building_voxels.py | 911 ++++++++++++++
136 files changed, 31933 insertions(+)
Diff
Diff truncated at 200.0 KB — fetch the full change with git show 702d8049f0 after cloning.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f91f08d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,545 @@
+# Changelog
+
+## Unreleased
+
+Real shadows. The sun moves to the southeast and drops to 45 degrees, so
+shadows fall northwest -- up and to the left on screen -- and run about as
+long as the thing throwing them is tall.
+
+### Added
+
+- `lib/ShadowMap.lua`: the scene rendered once from the sun into an
+ orthographic depth map, which the main pass then samples per fragment.
+ What the sun cannot see is in shadow, whatever surface it is, so a
+ shadow climbs a wall, drapes over a roof and slides across a passing
+ NPC with no case in the code -- and every caster is simply whatever the
+ pass draws. The terrain mesh goes in, which means buildings, trees,
+ ledges, signs and every prop cast, where before only characters did.
+- Depth is packed into two 8-bit channels of an ordinary color canvas
+ (~16 bits over a ~700px frustum, a hundredth of a world pixel).
+ Readable depth textures are the least portable corner of the graphics
+ API, and the mod's contract is that an unsupported driver falls back
+ rather than errors: `available()` reports, and VoxelScene keeps the old
+ flat decals when it says no.
+- The map resolution is picked per frame from a 1024/1536/2048 ladder
+ against a 0.45 world-pixels-per-texel target, because the light frustum
+ is fitted to the world view and that swings 3x between the closest zoom
+ and a maximised window at the widest. The frustum is snapped to whole
+ texels, without which every shadow edge in the world crawls as you walk.
+- Ambient occlusion, the genuine article: each vertex counts the
+ neighbours crowding it and steps down once per neighbour, on top faces
+ (four corners, three neighbours each) and now on upright faces too --
+ the crease a wall rises out of, and the inside corners where flanking
+ columns box it in. It is the complement of the shadow pass rather than
+ a duplicate: the map draws the long directional shadow, this draws the
+ dark seam in every corner the sky cannot see into, at scales finer than
+ a shadow map texel.
+- Plus a ground-contact term for the prebuilt prop quads -- per-pixel
+ plants, signs and lone trees, and the round-tree stamps. Those arrive
+ from Structures already finished, so the neighbour counting has no
+ columns to count; what it can still say is that the floor blocks half
+ the sky, so a voxel's first 6px of rise ramps back to full light. It is
+ what stops a prop reading as pasted over the ground rather than
+ standing on it, and outdoors it is most of what AO does at all, since
+ trees and posts are nearly all prop geometry.
+- One knob for the lot: `AO_STRENGTH` in `ChunkMesher` scales every term
+ (they are written as darkening amounts, not multipliers), against a
+ floor that keeps a crank from punching holes of pure black.
+- The **V-CURVE** row in OPTIONS: the curved world, the Animal Crossing
+ horizon. Every vertex is pushed down by the square of its horizontal
+ distance from the camera's focus, and that is the whole effect -- a
+ quadratic is nearly zero near its vertex, so the ground being played on
+ stays flat, and the falloff accelerates, so the far edge rolls away over
+ a near horizon and the town reads as sitting on a small sphere.
+- It is deliberately NOT a fisheye. A fisheye is a LENS -- a screen-space
+ warp -- which bends straight lines everywhere including right in front of
+ the player, resamples every pixel to do it, and would leave this mode's
+ art blurred and crawling. Bending the WORLD is one line in the vertex
+ shader: lines near the camera stay straight and not a pixel is resampled.
+- Displacing along Y only is what keeps it readable rather than
+ nauseating: the drop depends on where a column stands, not how tall it
+ is, so the world tips away and the buildings on it stay upright.
+- Shadows and the wireframe ride along for free. Both are already worked
+ out before the bend -- the shadow map in flat world space, the grid in
+ model space -- so the bend carries them exactly as if they had been
+ painted on, and neither the light frustum nor the grid needs to know the
+ curve exists. `Voxel3D.project` applies the same drop on the CPU, which
+ is what keeps the overworld's 2D field FX on their ground points.
+- The strength scales with the view height, so a rung reads the same at
+ every zoom, and the ladder is calibrated against the far edge of the
+ visible ground rather than against nothing.
+- `lib/ModSetting.lua`: the ladder/store/rows a setting of this mod's own
+ needs, now that there are two of them. V-GRID's copy of it moved here.
+- A **75 degree** rung on the VOXEL ladder, below the 15/35/50 it shared
+ with the engine's TILT: low enough to read as a diorama shot from table
+ height. Tilt could not have it -- its flat plane degenerates into a
+ horizon line down there -- but geometry only gets more of itself to show.
+- The **V-GRID** row in OPTIONS: a one-display-pixel wireframe along every
+ voxel edge, 3D Dot Game Heroes style. Every mesh here is built one unit
+ per voxel in its OWN model space, so the seams are that space's integer
+ planes, and reading them in model space rather than world space is what
+ keeps them glued to a thing however it is posed -- a character's slab
+ leans back by the camera's pitch and its seams lean with it.
+- The seams fade out where a voxel shrinks under about 3 display pixels.
+ Survey zoom draws a world pixel at roughly a display pixel, and a wall
+ seen nearly edge-on squashes one to nothing at any zoom; drawn anyway,
+ the lines land closer together than they are wide and the wireframe
+ stops being a wireframe and becomes a flat 45% dimming of the scene.
+- `lib/VoxelGrid.lua` owns the toggle. It is NOT a pipeline: it owns no
+ pass of the frame, it parameterises the voxel one, so it has nothing to
+ put in `drawWorld` or `present` and the registry rightly rejects it. A
+ plain mod setting instead -- `options:define` for the store and the mod
+ manager's page, `ui.options.rows` for the row in OPTIONS next to VOXEL
+ and T-SHIFT. Both rows read and write the one stored value.
+- The wireframe is a SECOND COMPILATION of the scene shader rather than a
+ branch inside it, because it needs shader derivatives (`fwidth`) -- the
+ one part of the mode a driver can refuse. A refusal costs the grid and
+ nothing else.
+- `tests/drivers/voxel_shadow_probe.lua`: reports the fitted frustum and
+ the resolution rung, dumps the map itself, and shoots a stand point at
+ every pitch. `SHADOW_SUN="kx,kz"` retunes the bearing for one run,
+ `SHADOW_GRID=1` forces the wireframe on, and `SHADOW_ZOOM` pins the
+ zoom, without which two runs are not comparable -- a driver inherits
+ whatever the player left in `options.lua`, and the world view size (which
+ the light frustum is fitted to) swings 3x across that range.
+
+### Fixed
+
+- VOID FILL did nothing in voxel mode, in two separate ways.
+
+ **BLACK crashed the build.** The mode is not a block at all --
+ `TileRenderer.borderBlockFor` answers `false` for it -- and `Structures`
+ added 1 to that `false`. The arithmetic threw, which failed the mesh
+ build for every map in the neighbourhood, which left the mode with no
+ terrain and dropped it to the flat 2D path entirely. It now builds no
+ ring: `tileLookup` answers nil past the body and those keys are never
+ written, which the rest of the file already copes with -- every
+ neighbour query in it reaches one step outside the analysed range and
+ reads nil for its trouble, so an absent cell is the shape "nothing" has
+ always had here.
+
+ **WATER changed nothing on screen.** The ring is BAKED INTO THE MESH in
+ this mode rather than drawn each frame, and nothing dropped the cache
+ when the option moved, so the old ring simply stayed until the meshes
+ were invalidated for some other reason. The pipeline's update hook now
+ polls `TileRenderer.voidFill` and invalidates on a change -- polled
+ rather than hooked because the engine changes it from three places (the
+ options row, `applyOptions` on load, `setVoidFill`) and none of them
+ announces it, and checked ahead of the active() gate so switching it
+ while voxel mode is off still drops what is cached.
+
+ `tests/drivers/voxel_void_probe.lua` walks the three modes and reports
+ the border block, whether the mesh built and whether the scene took the
+ 3D path. It deliberately does NOT invalidate the cache itself, since
+ doing so would hide the second half of this.
+
+- Water and flowers did not animate. The 2D path animates them by
+ OVERDRAWING the animated cells on top of the static tile layer each
+ frame, which a single static mesh has no equivalent of -- the geometry
+ samples one texture and that is that. So `TerrainAtlas` animates the
+ texture instead: a private copy of the atlas whose animated tile slots
+ are rewritten when the step advances, which moves every instance of that
+ tile across the whole mesh at once. Which is what the Game Boy does in
+ the first place (`home/vcopy.asm` rewrites the tile's VRAM bytes); the
+ overdraw is the port's workaround for a tile layer, not the original.
+ ~130 pixels of work three times a second, on the same
+ `TileRenderer.animFrame` clock the 2D path uses, so the two can never
+ disagree about which frame they are on.
+- The frame files (`flower1..3.png`) are raw grayscale and have to land on
+ the colours of the tile they replace, but the two recolour paths do not
+ share a rule -- SGB bakes one world palette over everything, RED++ picks
+ a palette group per tile graphic. So the shade mapping is LEARNED from
+ the atlas: read the static tile's slot in the raw art and in the finished
+ atlas side by side and ask what each shade became. Right under both
+ without this file knowing which one ran.
+- Terrain art was off the pixel grid by up to half a pixel, with one art
+ pixel per tile sampled twice and another never at all. A tile is 8
+ texels across 8 world pixels -- one texel per pixel exactly -- and
+ `ChunkMesher`'s uv inset squeezed that art into a 7-texel sample range
+ while the quad still covered 8 world pixels, so it advanced 7/8 of a
+ texel per pixel and drifted. The inset exists to stop the rasteriser
+ reaching a neighbouring tile along a shared edge, but half a texel was
+ fifty times more than that needs: 0.02 is as safe (interpolation error
+ is nowhere near it) and drifts 0.25% of a pixel across a whole tile.
+ Nothing showed the fault until the voxel wireframe drew the grid those
+ pixels were supposed to be sitting on.
+
+### Changed
+
+- `Voxel3D.SHADOW_KX/KZ` are -0.85 / -0.55, from +0.30 / +0.45: the sun
+ crosses to the southeast and drops from 62 degrees to 45. The bearing
+ leans WEST of northwest on purpose -- a character is drawn as a slab
+ leaning away from the camera, which covers the ground due north of its
+ feet, so a shadow thrown straight up-screen lands entirely underneath
+ the figure casting it and is never seen.
+- `Voxel3D.SHADOW_ALPHA` 0.32 -> 0.40, a quarter darker.
+- `FACE_SHADE` east 0.78 -> 0.84 and west 0.78 -> 0.72. The two were equal
+ because the old sun sat due northwest and they were symmetric about it;
+ under a southeastern sun east is a lit flank and west a shaded one.
+- A character's shadow lookup runs off the UPRIGHT card the sun saw, not
+ the leaning slab the camera sees (`Voxel3D.draw`'s `sunModel`). Casting
+ the leaning slab instead would shrink every shadow to nothing as the
+ camera flattened toward top-down; looking up with the leaned position
+ put each sprite's own card across its front.
+- The contact-shadow term in `ChunkMesher` was a one-directional stripe
+ keyed to a northwestern sun -- two neighbours, one corner, top faces
+ only. It is now the ambient occlusion above.
+- The light frustum is fitted to the ground the CAMERA CAN SEE rather than
+ to a view-sized box around the focus, and both of its margins are now
+ asymmetric -- for opposite reasons. The camera sits south of its focus
+ and looks north, so the ground it sees runs far north and barely south;
+ the sun sits southeast, so the casters for that ground stand south and
+ east of it. Paying for a view-sized box plus caster margin on all four
+ sides covered about a third of what was on screen at 75 degrees, and
+ overpaid at 15.
+- Shadows ease off at the frustum's rim instead of ending on it. Past the
+ low rungs the horizon is further out than any box worth paying for, and
+ a covered region that simply stops draws a hard line across the middle
+ distance where every shadow ends at once.
+
+The Pokemon Center interiors. One `POKECENTER` group in
+`data/voxel_heights.lua` plus one new class, and because
+VIRIDIAN_POKECENTER places every tile the tileset's other maps use, the
+one pin set covers all eleven Centers and the Celadon Hotel.
+
+### Added
+
+- A `counter` class (8px, upright): half-cell furniture. One 8px band,
+ so exactly the drawing's bottom row stands up as the front and every
+ row above it rides the top face in drawn order. `table`'s 12px could
+ not be retuned for it; the houses share that class.
+
+- `tilesets.POKECENTER` in `data/voxel_heights.lua`. Before it, the
+ detector merged the wall-touching counters and healing machines into
+ the wall band and towered them 3-6 blocks, flattened the machines'
+ near-black screens to void, read the pillar bases, plant pots and
+ machine bodies as ponds (the $14/$32/$48 stale-cache water fallback),
+ boxed each plant pair into one hedge cube, and extruded the lounge
+ seat -- a PERSON is drawn into its tile art -- into a monolith wearing
+ his face.
+- The pins, by shape: the wall band, windows, poster, pillars and the
+ 16px machine bodies are `wall`; the counters (with the nurse's tray)
+ are `counter` and the PC's desk is `table`; the machine screens and
+ the PC are `billboard`, standing on the pinned boxes below them; the
+ potted plants are `prop` standees like every other interior plant.
+- The lounge couch with the man sitting on it is a `counter` box: its
+ bottom row stands up as the couch's front and the cushion and the man
+ ride the top face, each drawn exactly once.
+ He cannot be stood upright, and the reason is structural rather than a
+ tuning question. His skin pixels span two tile rows and stop dead at
+ the row 9/10 seam; folding two rows upright requires both to share a
+ class, which makes the box two tiles deep, and a fully folded box
+ repeats its north row across its whole top face. So every upright
+ arrangement puts his head on screen two or three times -- as a 16px
+ seat-back, on the front and twice more on the top; as a 32px bookcase,
+ a cabinet taller than the room's own walls. Dropping the seat in front
+ of him to floor level only changes which copy you see. Nor can he be a
+ standee: the drawing has no floor margin, so all three non-black
+ shades touch the cluster rim and the mask drains 307 of its 420
+ interior pixels -- 46% of him even segmented alone, because his skin
+ is the same light shade as the couch behind him.
+- Survey evidence: full before/after passes of VIRIDIAN_POKECENTER at
+ 15/35/50 degrees, plus spot-checks of CELADON_POKECENTER and
+ CELADON_HOTEL (shared tileset, both inherit correctly) and of
+ REDS_HOUSE_1F, OAKS_LAB and VIRIDIAN_CITY (unchanged -- the new class
+ is additive and no other tileset lists it).
+
+## 1.3.0
+
+Terrain meshing goes asynchronous, instanced and bounded. The first voxel
+frame used to build every neighbourhood map synchronously (a ~2.4s
+freeze), retain every map's analysis forever (gigabytes over a
+cross-region trek), and string stray pixels along map seams.
+
+### Changed
+
+- Mesh builds stream in the background. `ChunkMesher` queues per-map
+ build jobs and `pump()` -- driven from the pipeline's update -- runs
+ them inside a few-millisecond frame budget (`lib/BuildBudget.lua`
+ suspends the build coroutine mid-loop when the slice is spent). The
+ camera tween holds at flat until the current map's terrain exists, so
+ toggling voxel mode shows a handful of flat frames instead of a frozen
+ one; neighbours pop in as they finish. Warp fades prefetch the
+ destination (the pipeline update ticks while the Transition covers the
+ screen, with a wider pump slice), so a door exit lands on terrain that
+ is already built.
+- Vertex packing goes through FFI into one native buffer
+ (`Mesh:setVertices(ByteData)`) instead of a Lua table per vertex --
+ the headless table path remains for the pure `geometry()` API and its
+ suite.
+- Round-tree hulls are carved once per (tileset, art, ground set) and
+ kept as stamps -- template plus cell offset, expanded during vertex
+ packing -- instead of materialized per-cell quad tables. A route's
+ border forest was ~500 quads x hundreds of cells of retained heap.
+- Mesh and analysis caches evict down to the live neighbourhood (current
+ map + rendered neighbours, plus one set of history so a house
+ round-trip keeps the town warm). Evicted meshes are released
+ explicitly. Memory over Pallet -> Mt Moon: was ~2.9GB and monotonic,
+ now oscillates between ~90 and 200MB.
+
+### Fixed
+
+- Stray pixels along map seams: ring props (border-tree hulls) whose
+ quad CENTER sat exactly on a neighbour body's edge line escaped the
+ strict point-in-rect mask and survived as fragments of otherwise
+ dropped trees. Object quads now keep/drop by their full extent,
+ boundary inclusive; props straddling the body edge also stay whole
+ instead of shedding their outer half.
+- The one-step "ledge hop" when crossing a connection into a tree-ringed
+ map: the seam step stands the player one cell off the new map, where
+ `Map:cellTile` border-extends into the borderBlock -- a raised tile on
+ maps ringed with trees. Off-map ground now reads as height 0 (the
+ departed neighbour's flat walkway, which is what is actually rendered
+ there).
+
+## 1.2.0
+
+Buildings stop being boxes wearing their own elevation. A profiled
+building is voxelized from its own sprite, band by band -- the pipeline
+written up in `assets/docs/buidling_to_voxel/`.
+
+### Added
+
+- `lib/Buildings.lua`: a building archetype. Where the volume path folds a
+ whole drawing upright (roof, facade and sloped ends alike) into one box,
+ this classifies each BAND of the drawing by the 3D surface it depicts and
+ applies the matching operation: top-facing rows lay flat over the
+ footprint, the facade extrudes straight back, an awning band juts past
+ the walls, and the drawn taper at the ends becomes a stepped slope in
+ elevation. Every visible voxel carries a real texel of the drawing, so
+ the model recolours with the atlas.
+- A `buildings` section in `data/voxel_heights.lua`. A building is matched
+ by its exact tile grid (the drawings are catalogued in `assets/docs/buildings/`),
+ so one entry covers every map that places the same art -- Red's house,
+ Blue's house, Bill's, the Copycat's and the two Fuchsia houses are one
+ seven-placement entry, and Oak's lab is a second. Only the band table is
+ authored; the silhouette, the taper rate, the eave height and every
+ window and doorway are measured off the pixels.
+- The flat-roofed civic block and its sixteen relatives -- every Pokemon
+ Center and every Poke Mart, Fuchsia Gym, the museum, the Game Corner,
+ Celadon Mansion and its department store, the Power Plant, the Route 5
+ and Route 22 gates, Silph Co and five anonymous scenery blocks. They are
+ one architecture drawn at eleven different footprints, from 4x4 cells up
+ to Silph Co's 8x12, and they share one band table: their lattice is drawn
+ from straight above, so the measured taper comes out flat and the whole
+ band is depth under one level roof. No new roof mode was needed -- the
+ drawn profile was always the shape, and a drawing with no taper simply
+ yields a level one.
+- Pewter's museum hall (`assets/docs/buildings/B24`). It is the one
+ sloped-roof building in this pass, and the only one so far whose roof
+ texture is not a plain repeat: the drawing states its own period by
+ repeating the whole lattice-and-course motif, rows 8..31 again at 32..55,
+ so the cycle is 24 rows and the roof carries its drawn courses across the
+ depth instead of a bare lattice. The band below them is the roof's
+ fascia, wider than the wall it covers, so it belongs to the roof and
+ lands on the south rim. Note the museum is TWO drawings: this hall and
+ the east entrance beside it, which is B18 and shares its drawing with the
+ Route 2 gate.
+- The rest of the 2:1 sloped-roof buildings: both gym drawings (the
+ standard one at Cinnabar, Pewter, Vermilion and Viridian plus the
+ Fighting Dojo, and the wider Celadon / Cerulean / Saffron one), the 4x2
+ cottage that houses Mr Fuji, the Cubone house, Bill's grandpa, the Name
+ Rater and the Viridian school, Cerulean's three wide houses, the day
+ care, and three scenery blocks -- among them B01, which at 19 placements
+ is the commonest drawing in the game. Each one's roof band is pixel for
+ pixel one of the three already authored, so they take that sibling's band
+ table unchanged: 16 rows for Red's house's, 32 for Oak's lab's, 64 for
+ the museum's.
+- The Safari Zone rest houses and the Victory Road entrance, the first
+ buildings outside the `OVERWORLD` tileset -- `buildings` is keyed by
+ tileset and until now only had the one section. The rest house's
+ corrugated roof repeats every 5 rows rather than the overworld lattice's
+ 8, which is the point of `roofCycle` being authored per building.
+- Route 10's scenery block, via a new `seal` field. Its drawing has no
+ black base course -- it ends on a row of light brick -- so the silhouette
+ flood climbed in from the south border through the mortar and hollowed
+ the wall out, leaving 72% of the sprite in 65 pieces. `seal` names the
+ sides a drawing runs off rather than closing, and the flood does not seed
+ there: sealed, it is 95% in one piece, and the model is its twin the
+ museum's. No other building sets it, and none changes by a voxel.
+- Together these take the mod to 31 of the catalogue's 34 drawings and 144
+ of its 147 placements. The last three, and why each resists, are written
+ up in `assets/docs/buildings/REMAINING.md`.
+- A roof no longer runs past the drawing's own silhouette. These sprites
+ are inset from their boxes, and the columns outside the inset carry no
+ roof: they now get none, and the fascia belongs to the outermost columns
+ the drawing actually paints. Before, they raised a four-voxel slab of
+ black kerb the full depth of the building, flanking its walls at ground
+ level. Nothing shipped hit this -- Red's house and Oak's lab are drawn
+ edge to edge -- so no existing model changes by a single voxel.
+- Windows and doorways sink a voxel behind their frames, found rather than
+ listed: a pane is a non-black region the drawing seals off behind its own
+ black outline. A nested frame (the door's own little window) layers for
+ free.
+- `tools/building_voxels.py`: the reference implementation of the same
+ algorithm, with the geometric asserts and isometric previews Stage 5 of
+ the methodology calls for. It and the runtime agree exactly on voxel and
+ shell counts for all 19 templates -- 96,617 / 12,866 for Red's house up
+ to 3,715,963 / 146,762 for Silph Co, which ship as 3,410 and 13,994
+ quads. Its slope asserts read the drawn columns only and stand down for a
+ roof with no taper, where the check is that the roof is level instead;
+ its previews fit the projection to the model rather than to a fixed
+ camera, so a building taller or deeper than the first two still lands on
+ the canvas.
+
+### Changed
+
+- A voxelized building is as tall as its facade plus its roof slab rather
+ than as tall as its drawing: the roof rows are DEPTH now, not height, so
+ Red's house is 36px over a 4x3-cell plot instead of a 48px cube.
+- Round trees (the `cylinder` pin: lone canopies and the border tree
+ wall) stop being lathes -- the sprite wrapped around a 12-segment
+ column read as exactly that, art smeared on a barrel. Each cell is now
+ a real voxel hull: the canopy is segmented out of its cell as the
+ darkest-pixel outline plus everything it encloses (which also drops
+ the background grass that used to inflate every row to full width, and
+ the cast shadow under the ball), and each mask row runs its own span's
+ circular chord in depth -- the front view is the sprite pixel for
+ pixel, the plan view is the sprite's width profile turned in depth.
+ Dithered art with no closed outline (the tree wall) falls back to
+ light-shades-only flooding, per the methodology doc's boundary rule.
+ Sides de-outline like building extrusions so flanks read as canopy
+ rather than solid black, and dome caps keep their outline on the rim
+ while the interior samples the canopy a couple of rows deeper.
+- A `post` standee pool, pinned for the overworld's vertical fence-post
+ cell (tiles 14/85 -- across every map the pair appears only as this
+ cell). The detector already turns HORIZONTAL fence runs (tile 57) into
+ per-post standees, but a vertical run of repeated cells trips its
+ scenery-repetition guard and fell to the volume path as a
+ fence-textured tower (Viridian's west line, Route 25).
+ `post` extracts every CELL as its own cluster -- pooled clustering
+ would stand the whole line up as one drawing-tall slab at one depth --
+ and classifies pixels the way the detector does (non-white is body)
+ rather than by the pinned-prop outline rule, which would strip the
+ posts to black skeletons; at the detector's own 6px depth, pinned and
+ detected fences look alike.
+- Town signs move from the `billboard` pool to a new `signpost` pool: the
+ same per-pixel standing slab, but 2 voxels thin instead of 10. A sign
+ is a plate on a stick, and the standee body that keeps a TV from
+ vanishing at shallow angles read as a solid block of furniture here.
+- The ground under a round tree matches the tree's own drawn background
+ instead of the map's commonest ground tile. The hull's segmentation
+ already knows which pixels are NOT the tree; those pixels are scored
+ against every flat ground tile the map places and the closest art
+ wins, per template -- so border trees drawn over checker grass stand
+ on checker grass even on a map that is mostly pale path (the old
+ fallback painted path under every mid-forest tree, which has no flat
+ neighbour to vote with). The drawn cast shadow stays out of the score:
+ no ground tile carries a shadow, and its darks would drag every match.
+
+### Fixed
+
+- The generic town-house tileset (`HOUSE` -- Blue's house, Daisy at her
+ table, and eighteen more homes, the schoolhouse and the trashed house
+ among them) is now pinned in `data/voxel_heights.lua` the way Red's
+ rooms already were: the dining table stops towering as a wall-height
+ volume and sits at table height with its front folded upright, stools
+ become seat-high boxes that characters sit on, the corner potted
+ plants become per-pixel standees instead of texture-smeared box
+ stacks, the bookcases get clean capped tops, and the wall band (with
+ its window, picture and the schoolhouse blackboard) stays one 16px
+ face. The schoolhouse's open book stands on the pinned tabletop as a
+ cutout, and the trashed house's ransacked table corner keeps table
+ height.
+- Interior door mats lie flat again in Red's and the generic houses.
+ Their collision tile is $14, which the engine's stale-cache fallback
+ counts as water in every tileset, so the rug recessed into a pond lip;
+ a `ground` pin now overrides the water read.
+
+## 1.1.0
+
+Interior furniture gets the shapes it depicts (docs/voxel-survey.md is the
+procedure that found and verified these).
+
+### Added
+
+- Furniture shape classes in the profile: `bed` (a low slab wearing its
+ top-down art), `table` and `desk` (boxes at their drawn height whose
+ faces fold the artwork up), `relief` (a prop drawn from above -- a
+ game console -- lying flat and extruding a few voxels inside its
+ outline), the standee pools `billboard` (10px) / `prop` (5px) /
+ `stool` (5px, and characters standing on its walkable cell sit at
+ seat height) / `cutout` (one voxel: pure profile, for the vase on the
+ table), and the stair archetypes `stair_e`/`stair_w` (a rising flight
+ of real steps) and `stair_down_e`/`stair_down_w` (a sunken stairwell
+ descending below the floor -- stairs that lead down). Separate pools
+ cluster separately, so touching drawings never merge into one cutout.
+- Standee clusters split into per-pixel connected components, each
+ standing on its own feet in the depth band of the row it is drawn in:
+ two stools stacked in adjacent cells become two stools, and no
+ fragment of a drawing ever floats at its bounding-box height. A
+ `cutout` keeps only its largest component -- a cast shadow's drawn
+ edge is background, not a floating scrap.
+- `bookcase` class: free-standing shelf drawings collapse in ranks onto
+ one-cell-deep boxes at their full drawn height, back rows becoming
+ hidden floor; the trim row above a rank -- undetected structure or a
+ row pinned `table`, since the same trim tiles cap other furniture --
+ is adopted as its cap. Pinned for Oak's Lab (`DOJO`).
+- Oak's Lab tables pinned `table`: the starter-ball display and the
+ north tables stand at real table height, the display frame's black
+ corner brackets no longer auto-extract into standing prisms, and the
+ Poke Ball / Pokedex sprites ride the authored height onto the
+ tabletops.
+- Pinned props drawn directly above a pinned box stand ON it: the PC
+ monitor on its desk, the flower pot on the dining table.
+- Profile pins for `REDS_HOUSE_1` / `REDS_HOUSE_2` (Red's house and the
+ Copycat's, both floors): bed, stools, tables, PC desk with its
+ standing monitor, bookcases, TV standing on the floor behind the game
+ console's relief, potted plant, flower pot, both staircases, and the
+ wall/window band.
+- `tests/drivers/voxel_survey.lua`: screenshot-survey driver behind the
+ repeatable inspection procedure (SURVEY_MAP / SURVEY_SPOTS /
+ SURVEY_LEVELS / SHOT_DIR), documented in docs/voxel-survey.md.
+
+### Changed
+
+- Pinned props are segmented the way the art is authored: objects wear a
+ black outline, so background is the shades touching the cluster's edge
+ (white floor around a TV, grey tabletop around a vase) flooded in from
+ the aprons; the outline, its interior, paint whites and anything they
+ enclose survive -- pixel-perfect cutouts on any surface.
+- Indoor structure analysis floods background from all four aprons and
+ accepts ground contact on any side (outdoors keeps the south-only rule
+ that protects roofs), so face-on furniture drawings voxelize per pixel
+ instead of rising as wall-height volumes.
+- Profile-pinned standees are 10px deep (detected props stay 6px), so a
+ deliberate object like a TV keeps a body at shallow camera angles.
+- Authored upright boxes fold their artwork up every face (flanks and
+ back wear the front stack darkened) and top faces keep the drawn
+ tabletop: face-on rows wear the row above the fold instead of
+ repeating their front art lying flat, and a run that folded entirely
+ tops with the furniture row drawn above it (a bookcase's shelf trim).
+- Characters no longer ride a pinned stair tile's class height: stairs
+ are walked through at floor level, fixing the step-up onto thin air in
+ front of stairwells.
+
+## 1.0.0
+
+First release, ported from the engine-internal voxel branch onto the
+`render_pipelines` mod API.
+
+### Added
+
+- `voxel` render pipeline: 3D diorama overworld with extruded terrain,
+ depth-buffered occlusion, leaning sprite billboards, drop shadows and
+ contact AO. VOXEL options row and hotkey `6` (OFF / 15 / 35 / 50).
+- `tiltshift` render pipeline: a `worldPresent` post-process giving the
+ miniature-photo look. T-SHIFT options row and hotkey `9` (OFF / 1 / 2 / 3).
+- Carved voxel models for 67 overworld sprites, plus the
+ `tools/build_voxels.py` that produced them.
+- `data/voxel_heights.lua`, the hand-authored tile shape profile.
+
+### Changed from the pre-mod version
+
+- The level is no longer the mod's to keep. The engine owns the ladder, the
+ options rows, the hotkeys, persistence and the TILT exclusion; the mod
+ keeps only the camera-angle tween.
+- Persistence moved from `save.options.voxel` / `save.options.tiltshift` to
+ `save.options.pipelines.voxel` / `.tiltshift`.
+- Hotkeys moved from `4`/`9` to `6`/`9`: the fork already uses `4` for
+ survey zoom.
+- The tilt-shift pass is a declared `worldPresent` stage rather than a call
+ spliced into the world draw, so it composes with any world pipeline
+ instead of only this one.
+- The cut-tree animation now draws in voxel mode; the pre-mod version
+ omitted it from the 3D field-effect list.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fad3d8f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,120 @@
+# Dramatic Shape Voxel Mod
+
+The overworld as a 3D diorama. Terrain is extruded into real geometry,
+occlusion comes from a depth buffer rather than a y-sort, characters stand
+as leaning sprite slabs, a shadow map throws real cast shadows across
+whatever they land on, and an optional tilt-shift pass sells the
+miniature-model look.
+
+Purely presentational. Nothing here reaches collision, movement, triggers
+or scripts — it changes what the world *looks* like and nothing about what
+it *is*. Battles, menus and cutscenes are untouched; only the free-roam
+overworld draws differently.
+
+## Using it
+
+| control | does |
+| --- | --- |
+| `6`, or the **VOXEL** options row | OFF → 15 → 35 → 50 → 75 → OFF (camera pitch) |
+| `9`, or the **T-SHIFT** options row | OFF → 1 → 2 → 3 → OFF (miniature blur) |
+| the **V-GRID** options row | OFF / ON — a one-pixel wireframe on every voxel |
+| the **V-CURVE** options row | OFF → 1 → 2 → 3 — bend the world over the horizon |
+
+75 degrees is a rung the engine's flat **TILT** could not have: it projects
+the world canvas as one rigid plane, which degenerates into a horizon line
+down there, where real geometry only gets more of itself to show.
+
+V-CURVE is the *curved world* — every vertex pushed down by the square of
+its horizontal distance from the camera's focus, so the ground under you
+stays flat and the map bends away over a near horizon. It is not a fisheye:
+a fisheye is a lens, a screen-space warp that bends straight lines
+everywhere and resamples every pixel to do it. This bends the geometry, so
+lines near the camera stay straight and the art stays crisp.
+
+Both pipeline levels persist in `save.options.pipelines`, and V-GRID and
+V-CURVE in `save.options.modOptions.DRAMATIC_SHAPE` — the same place the mod
+manager's own settings page for this mod reads, so each row is one setting
+in two places. VOXEL and the engine's **TILT** are two answers to the same
+question, so switching one on switches the other off.
+
+Without shader or depth-canvas support the rows still cycle but the world
+stays 2D — the mode reports itself unavailable and the engine keeps the
+vanilla path.
+
+## How it is built
+
+This is a *rendering pipeline* mod (see `docs/modding.md`). `main.lua`
+registers two records in the engine's `render_pipelines` registry and
+writes almost nothing else — the ladder, the options rows, the hotkeys,
+persistence and the free-roam gate are all engine plumbing driven from
+those records:
+
+- **`voxel`** — a `drawWorld` pipeline. Replaces the world pass with a
+ rendered 3D scene and returns it as one window-resolution canvas.
+- **`tiltshift`** — a `worldPresent` pipeline. Post-processes the finished
+ world *before* the UI composites, so the diorama blurs and the text boxes
+ in front of it stay crisp.
+
+Everything else lives in `lib/`, loaded through the mod's own namespace
+(`V.require`) rather than `package.path`, which a mod directory is not on:
+
+| module | role |
+| --- | --- |
+| `Voxel3D` | the 3D pass: shader, depth buffer, camera, projection |
+| `ShadowMap` | the sun's own pass: what it cannot see is in shadow |
+| `WorldCurve` | the curved world: the Animal Crossing horizon |
+| `VoxelGrid` | the wireframe toggle |
+| `ModSetting` | a setting of this mod's own: ladder, store, options row |
+| `Mat4` | the 4x4 matrix math the camera and model transforms need |
+| `VoxelState` | the camera angle and its ease; the engine owns the level |
+| `VoxelScene` | assembles one frame: shadows, terrain, characters, grass |
+| `ChunkMesher` | a map's block layer → a terrain mesh, cached per map |
+| `Structures` | flood-fills drawn things and decides how tall each is |
+| `Buildings` | a profiled building sprite → a voxel model, band by band |
+| `TileShape` | resolves every tile to an extrusion class and art mode |
+| `TerrainAtlas` | the texture geometry samples: SGB palette bakes, and the animated water/flower tiles |
+| `SpriteBillboards` | a sprite frame → a leaning slab with side relief |
+| `VoxelModels` | the carved character models in `assets/voxels/` |
+| `TiltShift` | the two-pass separable blur and saturation lift |
+
+`data/voxel_heights.lua` is the hand-authored shape profile: listing a tile
+there pins its height and art mode over the automatic detector, and its
+`buildings` section names whole building sprites to voxelize band by band
+(`assets/docs/buidling_to_voxel/` is the pipeline; `assets/docs/buildings/` catalogues the
+drawings). Another mod can pin its own tiles by shipping the same file, and
+can replace a character's model with `overrides/voxels/<name>.lua` — models
+resolve through the engine's asset search path before falling back to ours.
+
+`tools/build_voxels.py` is what carved `assets/voxels/` out of the
+overworld sprite sheets. `tools/building_voxels.py` is the reference
+implementation of the building voxelizer — it builds the same models
+offline, asserts their geometry and renders isometric previews, and its
+voxel and shell counts are what `lib/Buildings.lua` is checked against.
+
+## Engine seams it relies on
+
+The mod reads a few engine internals (hence `"permissions":
+["engine_internals"]`), all of them deliberate read-only seams:
+
+- `Player:pose()` / `NPC:pose()` — the sheet, position, facing and step
+ phase a frame renders to, so 2D and 3D can never disagree about a pose.
+- `SpriteRenderer:resolveImage()` and `SpriteRenderer.STAND/WALK` — the
+ live sprite image and frame tables, so palette modes and sprite-replacing
+ mods apply to the geometry with no second copy of the logic.
+- `TileRenderer.recolorSample` — the exact shade cutoffs the 2D palette
+ shader uses, so baked terrain lands on the same colours as flat tiles.
+- `TileRenderer.animFrame()` / `.atlasImageData()` / `.defaultAnimatedTiles()`
+ — the tile-animation clock, the pixels behind the atlas texture, and the
+ vanilla water/flower spec. A static mesh cannot overdraw animated cells
+ the way the tile layer does, so it animates the texture instead; all
+ three are needed to do that on the engine's own clock and colours.
+
+## Tests
+
+```sh
+luajit mods/DRAMATIC_SHAPE/tests/dramatic_shape_test.lua
+```
+
+Runs headless: the mod loads, both pipelines land in the registry with the
+shape the engine dispatches on, the ladders and persistence round-trip, and
+the whole thing stays inert on a machine with no 3D support.
diff --git a/assets/docs/buidling_to_voxel/sprite_extract.py b/assets/docs/buidling_to_voxel/sprite_extract.py
new file mode 100644
index 0000000..97b6a83
--- /dev/null
+++ b/assets/docs/buidling_to_voxel/sprite_extract.py
@@ -0,0 +1,62 @@
+"""Stage 1: sample a GB sprite to native resolution, extract palette +
+silhouette (light-only flood fill), write sprite_data.json.
+Usage: python3 sprite_extract.py <sprite.png>"""
+import json
+from collections import deque, Counter
+from PIL import Image
+
+import sys
+SRC = sys.argv[1] if len(sys.argv) > 1 else 'sprite.png'
+img = Image.open(SRC).convert('RGB')
+W, H = img.size
+sx, sy = W // 64, H // 48 # 8x8 blocks
+assert sx * 64 == W and sy * 48 == H, (W, H)
+
+# sample block centers -> 64x48
+px = img.load()
+grid = [[px[x * sx + sx // 2, y * sy + sy // 2] for x in range(64)] for y in range(48)]
+
+# palette
+counts = Counter(c for row in grid for c in row)
+pal = sorted(counts, key=lambda c: -counts[c])
+print('palette:', [(('#%02x%02x%02x' % c), counts[c]) for c in pal])
+idx = {c: i for i, c in enumerate(pal)}
+
+lum = lambda c: 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
+
+# flood fill "outside" from border pixels, blocked by the dark outline
+outside = [[False] * 64 for _ in range(48)]
+q = deque()
+for x in range(64):
+ for y in (0, 47):
+ if lum(grid[y][x]) > 60 and not outside[y][x]:
+ outside[y][x] = True; q.append((x, y))
+for y in range(48):
+ for x in (0, 63):
+ if lum(grid[y][x]) > 60 and not outside[y][x]:
+ outside[y][x] = True; q.append((x, y))
+while q:
+ x, y = q.popleft()
+ for nx, ny in ((x+1,y),(x-1,y),(x,y+1),(x,y-1)):
+ if 0 <= nx < 64 and 0 <= ny < 48 and not outside[ny][nx] and lum(grid[ny][nx]) > 60:
+ outside[ny][nx] = True; q.append((nx, ny))
+
+n_out = sum(r.count(True) for r in outside)
+print('outside pixels:', n_out, '(doc says 218)')
+
+# per-band outside count for sanity vs the doc's silhouette table
+for r in range(6):
+ band = sum(outside[y][x] for y in range(r*8, r*8+8) for x in range(64))
+ print(f' r{r+1}: {band}')
+
+# ascii mask (o = outside)
+for y in range(48):
+ print(''.join('o' if outside[y][x] else '.' for x in range(64)))
+
+rows = []
+for y in range(48):
+ rows.append(''.join('.' if outside[y][x] else '%x' % idx[grid[y][x]] for x in range(64)))
+
+json.dump({'pal': ['#%02x%02x%02x' % c for c in pal], 'rows': rows},
+ open('sprite_data.json', 'w'))
+print('wrote sprite_data.json')
diff --git a/assets/docs/buidling_to_voxel/sprite_to_voxel_methodology.md b/assets/docs/buidling_to_voxel/sprite_to_voxel_methodology.md
new file mode 100644
index 0000000..40bea60
--- /dev/null
+++ b/assets/docs/buidling_to_voxel/sprite_to_voxel_methodology.md
@@ -0,0 +1,216 @@
+# Sprite → Voxel Methodology
+## Translating Game Boy ¾-view overworld sprites into 3D voxel models
+
+Worked example: Red's house exterior — `PALLET_TOWN` blocks 56/57/60/61, 64×48 px
+(`reds_house_voxel.html`). The same pipeline applies to any GB building sprite.
+
+---
+
+## Core principle
+
+A Game Boy overworld sprite is a fake-3D projection: it packs several different
+3D facings into one flat image. Roof tops are drawn as if seen from above,
+walls as if seen from the front, and sloped surfaces as diagonal silhouettes.
+Voxelization is therefore **not** one operation — it is (1) classifying each
+region of the sprite by which 3D surface it depicts, then (2) applying the
+matching geometric operation per region.
+
+Two invariants govern everything:
+
+1. **Every visible voxel color comes from a sprite pixel** wherever the lookup
+ lands inside the silhouette. Synthesis is allowed only for geometry the
+ sprite implies but never paints (undersides, depth extensions, interiors),
+ and synthesized cells must continue the sprite's own periodic patterns and
+ 4-shade palette.
+2. **The sprite is ground truth, not the tile documentation.** The tile-grid
+ doc described 1px ground flanks; the actual sprite has 5px insets on the
+ ground floor and a 3-level wedding-cake silhouette. Always extract from the
+ real pixels and verify any doc claims against the extracted mask.
+
+---
+
+## Stage 1 — Extraction
+
+Sample the sprite down to its native resolution (uploads are usually integer
+upscales; sample the center of each scale×scale block, never bilinear).
+Quantize to the palette — for GB art expect exactly `#ffffff / #aaaaaa /
+#555555 / #000000`. Anything else in the histogram means the sampling grid is
+misaligned.
+
+Compute the silhouette with a flood fill from the image border that spreads
+**only through light pixels (luminance > 130)**. This threshold is load-bearing:
+the black outline (#000) and the dark shading (#555) together form the
+boundary. A naive "not black" threshold lets the fill eat the #555 shaded
+flanks and corrupts the silhouette — that bug produced a 398-vs-148 pixel
+discrepancy on the first pass here.
+
+Output: a JSON of the palette plus one string per row (`.` = outside the
+outline, digit = palette index). Dump the mask as ASCII and read it — this is
+where the building's real structure becomes visible, and it is the input to
+Stage 2. Tooling: `sprite_extract.py`.
+
+## Stage 2 — Band classification
+
+Segment the sprite rows into horizontal bands and assign each a facing. The
+cues generalize:
+
+| Cue in the mask / pixels | Reads as | 3D treatment |
+| --- | --- | --- |
+| Top band, plain fill with full-width course lines | Roof top seen from above | Lay horizontal (flat) |
+| Silhouette tapering at a constant rate (here 2 px per row) with slat/stripe texture | Sloped roof surface; the taper rate **is** the slope | Stepped diagonal surface, 1 down per (taper rate) out |
+| Band containing window/door frames | Vertical facade | Straight extrusion |
+| Full-width band with a black underline sitting above an inset band | Ledge / awning overhang | Extrusion + protrusion |
+| Dark `#555` runs beside a facade under a taper | Shadow on the wall beneath an eave | Leave as wall — the geometry above produces the shadow's meaning |
+
+The band table for Red's house, which Blue's house shares verbatim:
+
+| Sprite rows | Content | Treatment |
+| --- | --- | --- |
+| 0–15, x16–47 | Coursed grey panel, black rims, highlight course | Flat horizontal top |
+| 0–15, flanks | White/dark slats, 2 px-per-row taper | Sloped ends, 2:1 |
+| 16–23 | Gable wall, 3 windows, slat flank pixels | Extrude; roof solid overwrites the corners |
+| 24–31 | Awning slab, double black underline | Extrude + 2-voxel ledge front/back |
+| 32–47 | Ground floor siding, door (own inner pane), 2 windows | Extrude; recess panes |
+
+**Interpretation rules settled during this build — do not relearn them:**
+a top-facing band must end up level everywhere (no synthesized skirts rising
+through it); a tapering silhouette on a top band means *sloped surfaces in
+elevation*, not chamfered corners in plan; and the slope's eave tips should
+land where the sprite's taper pixels stop (here: just above the awning band).
+
+## Stage 3 — Geometry construction
+
+**Vertical bands** extrude straight back to depth `D` (26 here). Front and
+back layers take the sprite pixel; interior layers take a **de-outlined**
+color — if the pixel is black, walk inward up to 3 px for the first non-black
+color. Without this, the side faces of the model are solid outline-black
+slabs. With it, sides read as material with black front/back corner edges,
+which is the correct GB-cartoon look. The back face becomes a mirror of the
+front for free; that is sprite-pure and acceptable.
+
+**Ledges** replicate the band's front pixels ±2 voxels in z past the walls.
+The sprite's own black-underline rows become the visible dark underside.
+
+**Recesses** delete the front voxel of every *non-black* pixel inside a
+window/door rect. Frames stay proud; the identically-colored voxel behind
+becomes the pane, one voxel deep. Rects with nested frames (the door's inner
+window) produce layered relief automatically.
+
+**The flat top** lays the top-facing rows horizontal. The band is shallower
+than the house (16 rows vs 34+ of depth), so extend it by **cycling a mid-row
+band whose period matches the course rhythm** (rows 5–8 here, period 4) —
+this continues both course lines and slat columns seamlessly. Map the
+outermost sprite rows to the front/back rims so the black-line/fascia trim
+survives.
+
+**Sloped ends** are driven by an elevation profile `T(x)`: flat at `YTOP`
+over the plateau, dropping 1 voxel per (taper rate = 2) columns outward to
+the eave tips. Build a solid of constant vertical thickness (4) following
+`T(x)`, spanning the full roof depth including overhangs. Build order
+matters: walls first, then the roof solid **overwrites** wall voxels it
+intersects, then **trim** any wall voxel above `T(x)` so nothing pokes
+through the surface. The wall strip left exposed beneath the slope shows the
+sprite's own #555 shadow pixels — the sprite encodes this geometry.
+
+**Outline pass**: cells on the roof's outer boundary get black (top layer),
+grey (second layer), black (below) — reproducing the sprite's
+black-grey-black fascia — and all interior undersides are dark.
+
+Parameters used here, to tune per building: depth `D=26`; roof overhang 4 in
+x and z beyond the walls; slab thickness 4; `YTOP=35`, derived so the eave
+tips (after the 10-step drop) land one voxel above the awning band, matching
+where the sprite's taper ends.
+
+## Stage 4 — Color sourcing off the sprite
+
+For any roof cell, map z to a sprite row (`roofSy`), then look up `(x, row)`.
+If that lands inside the silhouette, use the pixel. If it lands outside
+(overhang extensions), continue the sprite's periodic texture: the slat
+rhythm is period 3 with a per-side phase (left flank white at `x % 3 == 1`,
+right at `x % 3 == 2` — derive the phases from the actual pixels, and mind
+negative-modulo semantics in JS). Plain top areas fall back to the mid grey.
+Ground plane and path are presentation-only and the single place non-palette
+colors are permitted.
+
+## Stage 5 — Verification (non-negotiable)
+
+Every bug in this build was caught by one of these, none by eyeballing alone:
+
+1. **Dual implementation parity.** Build the identical algorithm in a
+ reference implementation (Python) and in the shipping runtime (JS in the
+ HTML). Diff total voxel count and post-cull shell count — they must match
+ exactly (final build: 58,356 / 14,169).
+2. **Numeric asserts on intent.** Flatness: the set of y-layers above the
+ walls must be exactly the slab layers. Slope: the top-surface profile at
+ mid-depth must read tips → 1-per-2 steps → flat plateau, mirrored. Zero
+ wall voxels with `y > T(x)`. Full wall coverage by the roof footprint.
+3. **Isometric preview.** A ~60-line painter's-algorithm render (sort by
+ `(x+z, y)`, draw top/left/right faces of shell voxels) catches texture and
+ layering mistakes cheaply before touching the runtime.
+4. **Hidden-face culling.** Drop voxels whose 6 neighbors all exist before
+ instancing; render the shell as one InstancedMesh.
+
+Tooling: `voxel_build_verify.py` (builds, asserts, renders previews).
+
+## Repeat checklist
+
+1. Obtain the sprite; sample to native resolution via block centers.
+2. Extract palette + silhouette (light-only flood fill, threshold 130);
+ review the ASCII mask.
+3. Segment rows into bands using the Stage-2 cues; write the band table
+ before writing any geometry code.
+4. Measure taper rates from the mask; derive `T(x)`, `YTOP`, overhangs, `D`.
+5. Build: extrude verticals (de-outlined interiors) → ledges → recesses →
+ flat top (mid-row cycling) → sloped solids (overwrite, then trim) →
+ outline pass → ground presentation.
+6. Verify: parity counts, profile/flatness/poke asserts, iso preview.
+7. Ship: embed palette + row strings in the HTML; the builder runs
+ client-side and doubles as the reference implementation of the algorithm.
+
+## Applying this in the mod
+
+Because tilesets are shared, the band table can be keyed by tile id rather
+than by sprite: each id gets a treatment record (extrude / lay-flat / slope,
+plus ledge, recess, warp flags) in the spirit of
+`mods/DRAMATIC_SHAPE/data/voxel_heights.lua`. Pinning blocks 56/57/60/61 with
+the profile above voxelizes Blue's house identically for free and propagates
+to the Fuchsia City, Route 25, and Saffron City instances of the same art.
+The door's lower-left tile (27) is the warp/walkable tile — keep its recessed
+front face aligned with the collision cell so the 3D doorway matches
+`REDS_HOUSE_1F`'s warp.
+
+### What shipping it settled
+
+The mod implements this as `mods/DRAMATIC_SHAPE/lib/Buildings.lua`, driven by
+a `buildings` list in the profile. Three things changed from the sketch
+above, each for a reason worth keeping:
+
+**Key the band table by the building's tile GRID, not by tile id.** A tile
+id is not a band: tile 23 is the house's awning course *and* the top of its
+roof *and* the eave course that ends Oak's lab's much taller roof. Matching
+the exact grid (`../buildings/` catalogues one per building, with every
+map that places it) is unambiguous, still shares one entry across all seven
+placements of Red's house, and cost nothing to verify — a scan of all 222
+maps returns exactly the catalogued placements.
+
+**Measure everything measurable.** Only the band table needs a human to read
+the drawing. The silhouette, the taper rate, the eave height and every
+window and doorway come off the pixels: a pane is a non-black region the
+drawing seals behind its own black frame, and `T(x) = YTOP - topRow(x)`
+falls straight out of the mask — which is also what makes `YTOP` stop being
+hand-tuned. The eave tips landing one voxel above the awning, tuned by hand
+here, then happens by itself.
+
+**Depth is the plot, and the drawn row → depth mapping has a direction.**
+In a diorama `D` is free; on a map it is the building's footprint (48px for
+Red's house, 64 for the lab), which is why the roof band has to be cycled so
+far. And the drawing looks at the roof from the north: its top rows are the
+FAR edge and its bottom rows the eave over the facade. Getting that backwards
+is invisible in the counts and in a symmetric preview — it shows up in game
+as a fascia along the wrong rim.
+
+One colour note: the outline pass's grey fascia band (`GREY` at `t-1`) is
+right for the raw GB palette but comes out white once the atlas is
+recoloured, turning every sloped end into a black-and-white zip. The
+drawing's own eave is black / `#555` / black, and using that reads correctly
+under every palette.
diff --git a/assets/docs/buidling_to_voxel/voxel_build_verify.py b/assets/docs/buidling_to_voxel/voxel_build_verify.py
new file mode 100644
index 0000000..c970d74
--- /dev/null
+++ b/assets/docs/buidling_to_voxel/voxel_build_verify.py
@@ -0,0 +1,162 @@
+"""Stages 3+5: reference voxel builder (mirrors the JS in the HTML),
+hidden-face culling, and isometric painter previews for verification.
+Run after sprite_extract.py in the same directory."""
+import json
+from PIL import Image, ImageDraw
+
+data = json.load(open('sprite_data.json'))
+rows = data['rows']
+WHITE, GREY, DARK, BLACK = 0, 1, 2, 3
+PAL = [(0xff, 0xff, 0xff), (0xaa, 0xaa, 0xaa), (0x55, 0x55, 0x55), (0x00, 0x00, 0x00),
+ (0xb7, 0xc7, 0x8d), (0xb1, 0xc1, 0x87), (0xd9, 0xd3, 0xc2)]
+GROUND1, GROUND2, PATH = 4, 5, 6
+
+inside = lambda x, y: 0 <= x < 64 and 0 <= y < 48 and rows[y][x] != '.'
+col = lambda x, y: int(rows[y][x])
+
+D = 26 # wall depth in voxels
+vox = {}
+
+def interior_color(sx, sy):
+ """side faces shouldn't be solid outline-black: sample inward past the outline"""
+ c = col(sx, sy)
+ if c != BLACK:
+ return c
+ step = 1 if sx < 32 else -1
+ for d in range(1, 4):
+ nx = sx + step * d
+ if inside(nx, sy) and col(nx, sy) != BLACK:
+ return col(nx, sy)
+ return c
+
+# ---- walls: sprite rows 16..47 extruded straight back --------------------
+for sy in range(16, 48):
+ y = 47 - sy
+ for sx in range(64):
+ if not inside(sx, sy):
+ continue
+ front, mid = col(sx, sy), interior_color(sx, sy)
+ for z in range(D):
+ vox[(sx, y, z)] = front if (z == 0 or z == D - 1) else mid
+
+# ---- awning ledge: rows 24..31 protrude 2 front and back -----------------
+for sy in range(24, 32):
+ y = 47 - sy
+ for sx in range(64):
+ if inside(sx, sy):
+ c = col(sx, sy)
+ for z in (-2, -1, D, D + 1):
+ vox[(sx, y, z)] = c
+
+# ---- recess panes: delete the front voxel of every non-black pixel -------
+RECESS = [(16, 23, 17, 23), (32, 39, 17, 23), (40, 47, 17, 23), # gable windows
+ (32, 39, 33, 39), (40, 47, 33, 39), # ground windows
+ (19, 28, 34, 45)] # door interior
+for x0, x1, y0, y1 in RECESS:
+ for sy in range(y0, y1 + 1):
+ for sx in range(x0, x1 + 1):
+ if inside(sx, sy) and col(sx, sy) != BLACK:
+ vox.pop((sx, 47 - sy, 0), None)
+
+# ---- roof: flat top over x16..47, DIAGONAL sloped ends (1 down per 2 out)
+ROOF_Z0, ROOF_Z1 = -4, 29
+ROOF_X0, ROOF_X1 = -4, 67
+YTOP = 35
+
+def T(x): # elevation profile = the sprite's / \ taper
+ if x < 16: return YTOP - ((16 - x + 1) // 2)
+ if x > 47: return YTOP - ((x - 47 + 1) // 2)
+ return YTOP # [2,1]/[3,1]: horizontal, flat
+
+def roof_sy(z):
+ df, db = z - ROOF_Z0, ROOF_Z1 - z
+ if db <= 6: return db # back rows 0..6 (rim + courses)
+ if df <= 8: return 15 - df # front rows 15..7 (fascia + rim)
+ return 5 + ((df - 9) % 4) # middle: cycle rows 5..8 (course rhythm)
+
+def roof_col(x, z):
+ sy = roof_sy(z)
+ if 0 <= x < 64 and inside(x, sy):
+ return col(x, sy) # real sprite pixels: slats on ends, courses on top
+ if x < 16: return WHITE if x % 3 == 1 else DARK
+ if x > 47: return WHITE if x % 3 == 2 else DARK
+ return GREY
+
+for x in range(ROOF_X0, ROOF_X1 + 1):
+ t = T(x)
+ for z in range(ROOF_Z0, ROOF_Z1 + 1):
+ outer = x in (ROOF_X0, ROOF_X1) or z in (ROOF_Z0, ROOF_Z1)
+ for y in range(t - 3, t + 1):
+ if y == t:
+ c = BLACK if outer else roof_col(x, z)
+ elif outer:
+ c = GREY if y == t - 1 else BLACK
+ else:
+ c = DARK
+ vox[(x, y, z)] = c
+
+# trim wall corners that would poke above the sloped ends
+for x in range(0, 64):
+ t = T(x)
+ if t < 31:
+ for y in range(t + 1, 32):
+ for z in range(0, D):
+ vox.pop((x, y, z), None)
+
+# ---- ground plate + path -------------------------------------------------
+for x in range(-10, 75):
+ for z in range(-12, 39):
+ vox[(x, -1, z)] = GROUND1 if (x + z) & 1 else GROUND2
+for x in range(17, 31):
+ for z in range(-12, 0):
+ vox[(x, -1, z)] = PATH
+
+# ---- cull to shell -------------------------------------------------------
+shell = []
+for (x, y, z), c in vox.items():
+ if all((x + dx, y + dy, z + dz) in vox for dx, dy, dz in
+ ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1))):
+ continue
+ shell.append((x, y, z, c))
+print('voxels:', len(vox), 'shell:', len(shell))
+
+# ---- isometric preview (front = -z, so mirror z for the render) ----------
+S = 4
+ZM = 40
+pts = []
+for x, y, z, c in shell:
+ pts.append((x, y, ZM - z, c))
+pts.sort(key=lambda p: (p[0] + p[2], p[1]))
+
+def P(x, y, z):
+ return (2 * (x - z) * S + 1250, ((x + z) - 2 * y) * S + 300)
+
+img = Image.new('RGB', (1800, 900), (0xca, 0xdc, 0x9f))
+dr = ImageDraw.Draw(img)
+has = {(x, y, z) for x, y, z, c in pts}
+def shade(c, f):
+ r, g, b = PAL[c]
+ return (int(r * f), int(g * f), int(b * f))
+for x, y, z, c in pts:
+ if (x, y + 1, z) not in has: # top
+ dr.polygon([P(x, y + 1, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 1.0))
+ if (x + 1, y, z) not in has: # right (+x)
+ dr.polygon([P(x + 1, y, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x + 1, y, z + 1)], fill=shade(c, 0.62))
+ if (x, y, z + 1) not in has: # toward viewer (original -z front)
+ dr.polygon([P(x, y, z + 1), P(x + 1, y, z + 1), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 0.82))
+img.save('preview_front.png')
+
+# second angle: from the back-right, no mirror
+pts2 = sorted(((x, y, z, c) for x, y, z, c in shell), key=lambda p: (p[0] + p[2], p[1]))
+img2 = Image.new('RGB', (1800, 900), (0xca, 0xdc, 0x9f))
+dr2 = ImageDraw.Draw(img2)
+has2 = {(x, y, z) for x, y, z, c in pts2}
+for x, y, z, c in pts2:
+ if (x, y + 1, z) not in has2:
+ dr2.polygon([P(x, y + 1, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 1.0))
+ if (x + 1, y, z) not in has2:
+ dr2.polygon([P(x + 1, y, z), P(x + 1, y + 1, z), P(x + 1, y + 1, z + 1), P(x + 1, y, z + 1)], fill=shade(c, 0.62))
+ if (x, y, z + 1) not in has2:
+ dr2.polygon([P(x, y, z + 1), P(x + 1, y, z + 1), P(x + 1, y + 1, z + 1), P(x, y + 1, z + 1)], fill=shade(c, 0.82))
+img2.save('preview_back.png')
+print('previews written')
diff --git a/assets/docs/buildings/B01-unnamed-building.md b/assets/docs/buildings/B01-unnamed-building.md
new file mode 100644
index 0000000..fe92a26
--- /dev/null
+++ b/assets/docs/buildings/B01-unnamed-building.md
@@ -0,0 +1,129 @@
+# B01 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 3 cells** (8 x 6 tiles of 8px, 64 x 48 px). Appears **19 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..3; tile column c=1..8, tile row r=1..6. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | C C | C C | D E |
+ | F G | H H | H H | I J |
+ +-----+-----+-----+-----+
+ y2 | K L | M M | M M | N O |
+ | P Q | Q Q | R R | R S |
+ +-----+-----+-----+-----+
+ y3 | P M | M M | M M | M S |
+ | T U | U U | U U | U V |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 5 6 | 7 7 | 7 7 | 8 9 |
+ | 21 22 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+
+ y2 | 37 38 | 10 10 | 10 10 | 40 41 |
+ | 15 34 | 34 34 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 78 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 7, 7, 7, 7, 8, 9 }, -- r1
+ { 21, 22, 23, 23, 23, 23, 24, 25 }, -- r2
+ { 37, 38, 10, 10, 10, 10, 40, 41 }, -- r3
+ { 15, 34, 34, 34, 75, 75, 75, 31 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 78, 26, 26, 26, 26, 26, 26, 79 }, -- r6
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 7, used 4x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 1x
+- `G` = tile 22, used 1x
+- `H` = tile 23, used 4x
+- `I` = tile 24, used 1x
+- `J` = tile 25, used 1x
+- `K` = tile 37, used 1x
+- `L` = tile 38, used 1x
+- `M` = tile 10, used 10x
+- `N` = tile 40, used 1x
+- `O` = tile 41, used 1x
+- `P` = tile 15, used 2x
+- `Q` = tile 34, used 3x
+- `R` = tile 75, used 3x
+- `S` = tile 31, used 2x
+- `T` = tile 78, used 1x
+- `U` = tile 26, used 6x
+- `V` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID
+```
+
+Every cell is solid - this building has no door of its own.
+
+## Silhouette
+
+186 of the 3072 px inside the box are ground outside the black outline, so the 64x48 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c7:16px, c8:48px
+ r3 c1:5px, c8:5px
+ r4 c1:8px, c8:8px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| SAFFRON_CITY | (36,1) | - | scenery, no entrance |
+| SAFFRON_CITY | (2,3) | - | scenery, no entrance |
+| SAFFRON_CITY | (10,3) | - | scenery, no entrance |
+| SAFFRON_CITY | (4,9) | - | scenery, no entrance |
+| SAFFRON_CITY | (8,9) | - | scenery, no entrance |
+| SAFFRON_CITY | (28,9) | - | scenery, no entrance |
+| SAFFRON_CITY | (32,9) | - | scenery, no entrance |
+| SAFFRON_CITY | (4,27) | - | scenery, no entrance |
+| SAFFRON_CITY | (12,27) | - | scenery, no entrance |
+| SAFFRON_CITY | (16,27) | - | scenery, no entrance |
+| SAFFRON_CITY | (20,27) | - | scenery, no entrance |
+| SAFFRON_CITY | (32,27) | - | scenery, no entrance |
+| SAFFRON_CITY | (4,33) | - | scenery, no entrance |
+| SAFFRON_CITY | (8,33) | - | scenery, no entrance |
+| SAFFRON_CITY | (12,33) | - | scenery, no entrance |
+| SAFFRON_CITY | (16,33) | - | scenery, no entrance |
+| SAFFRON_CITY | (24,33) | - | scenery, no entrance |
+| SAFFRON_CITY | (28,33) | - | scenery, no entrance |
+| SAFFRON_CITY | (32,33) | - | scenery, no entrance |
diff --git a/assets/docs/buildings/B02-unnamed-building.md b/assets/docs/buildings/B02-unnamed-building.md
new file mode 100644
index 0000000..bd721b2
--- /dev/null
+++ b/assets/docs/buildings/B02-unnamed-building.md
@@ -0,0 +1,130 @@
+# B02 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 4 cells** (8 x 8 tiles of 8px, 64 x 64 px). Appears **15 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..4; tile column c=1..8, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | B B | B B | B C |
+ | D E | E E | E E | E D |
+ +-----+-----+-----+-----+
+ y2 | D E | E E | E E | E D |
+ | F G | G G | G G | G H |
+ +-----+-----+-----+-----+
+ y3 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y4 | I L | L L | L L | L K |
+ | M N | N N | N N | N O |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y4 | 15 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r7
+ { 78, 26, 26, 26, 26, 26, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 6x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 12x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 6x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 6x
+- `K` = tile 31, used 3x
+- `L` = tile 75, used 12x
+- `M` = tile 78, used 1x
+- `N` = tile 26, used 6x
+- `O` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID
+```
+
+Every cell is solid - this building has no door of its own.
+
+## Silhouette
+
+258 of the 4096 px inside the box are ground outside the black outline, so the 64x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c8:25px
+ r2 c1:24px, c8:24px
+ r3 c1:24px, c8:24px
+ r4 c1:24px, c8:24px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+ r7 c1:8px, c8:8px
+ r8 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (28,6) | - | scenery, no entrance |
+| CELADON_CITY | (32,6) | - | scenery, no entrance |
+| CELADON_CITY | (36,6) | - | scenery, no entrance |
+| CELADON_CITY | (38,16) | (39,19) | `CELADON_MART_5F` |
+| CELADON_CITY | (42,16) | - | scenery, no entrance |
+| CELADON_CITY | (2,24) | - | scenery, no entrance |
+| CELADON_CITY | (14,24) | - | scenery, no entrance |
+| CELADON_CITY | (18,24) | - | scenery, no entrance |
+| CELADON_CITY | (26,24) | - | scenery, no entrance |
+| CELADON_CITY | (38,24) | - | scenery, no entrance |
+| CELADON_CITY | (18,28) | - | scenery, no entrance |
+| SAFFRON_CITY | (0,32) | - | scenery, no entrance |
+| SAFFRON_CITY | (36,32) | - | scenery, no entrance |
+| VERMILION_CITY | (14,0) | - | scenery, no entrance |
+| VERMILION_CITY | (20,0) | - | scenery, no entrance |
diff --git a/assets/docs/buildings/B03-unnamed-building.md b/assets/docs/buildings/B03-unnamed-building.md
new file mode 100644
index 0000000..0a26fda
--- /dev/null
+++ b/assets/docs/buildings/B03-unnamed-building.md
@@ -0,0 +1,134 @@
+# B03 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 4 cells** (8 x 8 tiles of 8px, 64 x 64 px). Appears **15 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..4; tile column c=1..8, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | B B | B B | B C |
+ | D E | E E | E E | E D |
+ +-----+-----+-----+-----+
+ y2 | D E | E E | E E | E D |
+ | F G | G G | G G | G H |
+ +-----+-----+-----+-----+
+ y3 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y4 | I L | M N | L L | L K |
+ | O P | * Q | P P | P R |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y4 | 15 75 | 11 12 | 75 75 | 75 31 |
+ | 78 26 | 27 28 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 11, 12, 75, 75, 75, 31 }, -- r7
+ { 78, 26, 27, 28, 26, 26, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 6x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 12x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 6x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 6x
+- `K` = tile 31, used 3x
+- `L` = tile 75, used 10x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 4x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID
+ y4 SOLID DOOR SOLID SOLID
+```
+
+Enterable cell: (2,4).
+
+## Silhouette
+
+258 of the 4096 px inside the box are ground outside the black outline, so the 64x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c8:25px
+ r2 c1:24px, c8:24px
+ r3 c1:24px, c8:24px
+ r4 c1:24px, c8:24px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+ r7 c1:8px, c8:8px
+ r8 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (32,16) | (33,19) | `GAME_CORNER_PRIZE_ROOM` |
+| CELADON_CITY | (30,24) | (31,27) | `CELADON_DINER` |
+| CELADON_CITY | (34,24) | (35,27) | `CELADON_CHIEF_HOUSE` |
+| CELADON_CITY | (42,24) | (43,27) | `CELADON_HOTEL` |
+| CERULEAN_CITY | (12,22) | (13,25) | `BIKE_SHOP` |
+| ROUTE_2 | (2,12) | - | scenery, no entrance |
+| ROUTE_2 | (2,40) | (3,43) | `VIRIDIAN_FOREST_SOUTH_GATE` |
+| ROUTE_5 | (16,24) | (17,27) | `UNDERGROUND_PATH_ROUTE_5` |
+| ROUTE_6 | (16,10) | (17,13) | `UNDERGROUND_PATH_ROUTE_6` |
+| ROUTE_7 | (4,10) | (5,13) | `UNDERGROUND_PATH_ROUTE_7` |
+| SAFFRON_CITY | (28,26) | (29,29) | `MR_PSYCHICS_HOUSE` |
+| VERMILION_CITY | (6,0) | (7,3) | `VERMILION_OLD_ROD_HOUSE` |
+| VERMILION_CITY | (8,10) | (9,13) | `POKEMON_FAN_CLUB` |
+| VERMILION_CITY | (14,10) | (15,13) | `VERMILION_TRADE_HOUSE` |
+| VERMILION_CITY | (22,16) | (23,19) | `VERMILION_PIDGEY_HOUSE` |
diff --git a/assets/docs/buildings/B04-unnamed-building.md b/assets/docs/buildings/B04-unnamed-building.md
new file mode 100644
index 0000000..07f631f
--- /dev/null
+++ b/assets/docs/buildings/B04-unnamed-building.md
@@ -0,0 +1,111 @@
+# B04 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 2 cells** (8 x 4 tiles of 8px, 64 x 32 px). Appears **12 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..2; tile column c=1..8, tile row r=1..4. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | C C | C C | D E |
+ | F G | H H | H H | I J |
+ +-----+-----+-----+-----+
+ y2 | K L | M N | O O | P Q |
+ | R S | * T | S S | S U |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 5 6 | 7 7 | 7 7 | 8 9 |
+ | 21 22 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+
+ y2 | 37 38 | 11 12 | 10 10 | 40 41 |
+ | 78 26 | 27 28 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 7, 7, 7, 7, 8, 9 }, -- r1
+ { 21, 22, 23, 23, 23, 23, 24, 25 }, -- r2
+ { 37, 38, 11, 12, 10, 10, 40, 41 }, -- r3
+ { 78, 26, 27, 28, 26, 26, 26, 79 }, -- r4
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 7, used 4x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 1x
+- `G` = tile 22, used 1x
+- `H` = tile 23, used 4x
+- `I` = tile 24, used 1x
+- `J` = tile 25, used 1x
+- `K` = tile 37, used 1x
+- `L` = tile 38, used 1x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 10, used 2x
+- `P` = tile 40, used 1x
+- `Q` = tile 41, used 1x
+- `R` = tile 78, used 1x
+- `S` = tile 26, used 4x
+- `T` = tile 28, used 1x
+- `U` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID DOOR SOLID SOLID
+```
+
+Enterable cell: (2,2).
+
+## Silhouette
+
+154 of the 2048 px inside the box are ground outside the black outline, so the 64x32 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c7:16px, c8:48px
+ r3 c1:5px, c8:5px
+ r4 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| FUCHSIA_CITY | (10,26) | (11,27) | `FUCHSIA_BILLS_GRANDPAS_HOUSE` |
+| LAVENDER_TOWN | (6,8) | (7,9) | `MR_FUJIS_HOUSE` |
+| LAVENDER_TOWN | (2,12) | (3,13) | `LAVENDER_CUBONE_HOUSE` |
+| LAVENDER_TOWN | (6,12) | (7,13) | `NAME_RATERS_HOUSE` |
+| PEWTER_CITY | (28,12) | (29,13) | `PEWTER_NIDORAN_HOUSE` |
+| PEWTER_CITY | (6,28) | (7,29) | `PEWTER_SPEECH_HOUSE` |
+| ROUTE_12 | (10,76) | (11,77) | `ROUTE_12_SUPER_ROD_HOUSE` |
+| ROUTE_16 | (6,4) | (7,5) | `ROUTE_16_FLY_HOUSE` |
+| ROUTE_2 | (14,18) | (15,19) | `ROUTE_2_TRADE_HOUSE` |
+| ROUTE_8 | (12,2) | (13,3) | `UNDERGROUND_PATH_ROUTE_8` |
+| VIRIDIAN_CITY | (20,8) | (21,9) | `VIRIDIAN_NICKNAME_HOUSE` |
+| VIRIDIAN_CITY | (20,14) | (21,15) | `VIRIDIAN_SCHOOL_HOUSE` |
diff --git a/assets/docs/buildings/B05-unnamed-building.md b/assets/docs/buildings/B05-unnamed-building.md
new file mode 100644
index 0000000..cf9898e
--- /dev/null
+++ b/assets/docs/buildings/B05-unnamed-building.md
@@ -0,0 +1,133 @@
+# B05 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 4 cells** (8 x 8 tiles of 8px, 64 x 64 px). Appears **11 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..4; tile column c=1..8, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | B B | B B | B C |
+ | D E | E E | E E | E D |
+ +-----+-----+-----+-----+
+ y2 | D E | E E | E E | E D |
+ | F G | G G | G G | G H |
+ +-----+-----+-----+-----+
+ y3 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y4 | I L | M N | O P | L K |
+ | Q R | * S | T T | R U |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y4 | 15 75 | 11 12 | 66 67 | 75 31 |
+ | 78 26 | 27 28 | 74 74 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 11, 12, 66, 67, 75, 31 }, -- r7
+ { 78, 26, 27, 28, 74, 74, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 6x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 12x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 6x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 6x
+- `K` = tile 31, used 3x
+- `L` = tile 75, used 8x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 66, used 1x
+- `P` = tile 67, used 1x
+- `Q` = tile 78, used 1x
+- `R` = tile 26, used 2x
+- `S` = tile 28, used 1x
+- `T` = tile 74, used 2x
+- `U` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID
+ y4 SOLID DOOR SOLID SOLID
+```
+
+Enterable cell: (2,4).
+
+## Silhouette
+
+258 of the 4096 px inside the box are ground outside the black outline, so the 64x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c8:25px
+ r2 c1:24px, c8:24px
+ r3 c1:24px, c8:24px
+ r4 c1:24px, c8:24px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+ r7 c1:8px, c8:8px
+ r8 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (40,6) | (41,9) | `CELADON_POKECENTER` |
+| CERULEAN_CITY | (18,14) | (19,17) | `CERULEAN_POKECENTER` |
+| CINNABAR_ISLAND | (10,8) | (11,11) | `CINNABAR_POKECENTER` |
+| FUCHSIA_CITY | (18,24) | (19,27) | `FUCHSIA_POKECENTER` |
+| LAVENDER_TOWN | (2,2) | (3,5) | `LAVENDER_POKECENTER` |
+| PEWTER_CITY | (12,22) | (13,25) | `PEWTER_POKECENTER` |
+| ROUTE_10 | (10,16) | (11,19) | `ROCK_TUNNEL_POKECENTER` |
+| ROUTE_4 | (10,2) | (11,5) | `MT_MOON_POKECENTER` |
+| SAFFRON_CITY | (8,26) | (9,29) | `SAFFRON_POKECENTER` |
+| VERMILION_CITY | (10,0) | (11,3) | `VERMILION_POKECENTER` |
+| VIRIDIAN_CITY | (22,22) | (23,25) | `VIRIDIAN_POKECENTER` |
diff --git a/assets/docs/buildings/B06-unnamed-building.md b/assets/docs/buildings/B06-unnamed-building.md
new file mode 100644
index 0000000..c2fba44
--- /dev/null
+++ b/assets/docs/buildings/B06-unnamed-building.md
@@ -0,0 +1,130 @@
+# B06 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 4 cells** (8 x 8 tiles of 8px, 64 x 64 px). Appears **8 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..4; tile column c=1..8, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | B B | B B | B C |
+ | D E | E E | E E | E D |
+ +-----+-----+-----+-----+
+ y2 | D E | E E | E E | E D |
+ | F G | G G | G G | G H |
+ +-----+-----+-----+-----+
+ y3 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y4 | I L | M N | O P | L K |
+ | Q R | * S | T T | R U |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y4 | 15 75 | 11 12 | 68 69 | 75 31 |
+ | 78 26 | 27 28 | 74 74 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 11, 12, 68, 69, 75, 31 }, -- r7
+ { 78, 26, 27, 28, 74, 74, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 6x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 12x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 6x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 6x
+- `K` = tile 31, used 3x
+- `L` = tile 75, used 8x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 68, used 1x
+- `P` = tile 69, used 1x
+- `Q` = tile 78, used 1x
+- `R` = tile 26, used 2x
+- `S` = tile 28, used 1x
+- `T` = tile 74, used 2x
+- `U` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID
+ y4 SOLID DOOR SOLID SOLID
+```
+
+Enterable cell: (2,4).
+
+## Silhouette
+
+258 of the 4096 px inside the box are ground outside the black outline, so the 64x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c8:25px
+ r2 c1:24px, c8:24px
+ r3 c1:24px, c8:24px
+ r4 c1:24px, c8:24px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+ r7 c1:8px, c8:8px
+ r8 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CERULEAN_CITY | (24,22) | (25,25) | `CERULEAN_MART` |
+| CINNABAR_ISLAND | (14,8) | (15,11) | `CINNABAR_MART` |
+| FUCHSIA_CITY | (4,10) | (5,13) | `FUCHSIA_MART` |
+| LAVENDER_TOWN | (14,10) | (15,13) | `LAVENDER_MART` |
+| PEWTER_CITY | (22,14) | (23,17) | `PEWTER_MART` |
+| SAFFRON_CITY | (24,8) | (25,11) | `SAFFRON_MART` |
+| VERMILION_CITY | (22,10) | (23,13) | `VERMILION_MART` |
+| VIRIDIAN_CITY | (28,16) | (29,19) | `VIRIDIAN_MART` |
diff --git a/assets/docs/buildings/B07-unnamed-building.md b/assets/docs/buildings/B07-unnamed-building.md
new file mode 100644
index 0000000..746537b
--- /dev/null
+++ b/assets/docs/buildings/B07-unnamed-building.md
@@ -0,0 +1,122 @@
+# B07 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 3 cells** (8 x 6 tiles of 8px, 64 x 48 px). Appears **7 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..3; tile column c=1..8, tile row r=1..6. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | C C | C C | D E |
+ | F G | H H | H H | I J |
+ +-----+-----+-----+-----+
+ y2 | K L | M N | M M | O P |
+ | Q H | H H | H H | H R |
+ +-----+-----+-----+-----+
+ y3 | S N | T U | M M | N V |
+ | W X | * Y | X X | X Z |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 5 6 | 7 7 | 7 7 | 8 9 |
+ | 21 22 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+
+ y2 | 37 38 | 10 34 | 10 10 | 40 41 |
+ | 92 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+
+ y3 | 15 34 | 11 12 | 10 10 | 34 31 |
+ | 78 26 | 27 28 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 7, 7, 7, 7, 8, 9 }, -- r1
+ { 21, 22, 23, 23, 23, 23, 24, 25 }, -- r2
+ { 37, 38, 10, 34, 10, 10, 40, 41 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 34, 11, 12, 10, 10, 34, 31 }, -- r5
+ { 78, 26, 27, 28, 26, 26, 26, 79 }, -- r6
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 7, used 4x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 1x
+- `G` = tile 22, used 1x
+- `H` = tile 23, used 10x
+- `I` = tile 24, used 1x
+- `J` = tile 25, used 1x
+- `K` = tile 37, used 1x
+- `L` = tile 38, used 1x
+- `M` = tile 10, used 5x
+- `N` = tile 34, used 3x
+- `O` = tile 40, used 1x
+- `P` = tile 41, used 1x
+- `Q` = tile 92, used 1x
+- `R` = tile 93, used 1x
+- `S` = tile 15, used 1x
+- `T` = tile 11, used 1x
+- `U` = tile 12, used 1x
+- `V` = tile 31, used 1x
+- `W` = tile 78, used 1x
+- `X` = tile 26, used 4x
+- `Y` = tile 28, used 1x
+- `Z` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID DOOR SOLID SOLID
+```
+
+Enterable cell: (2,3).
+
+## Silhouette
+
+218 of the 3072 px inside the box are ground outside the black outline, so the 64x48 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c7:16px, c8:48px
+ r3 c1:5px, c8:5px
+ r4 c1:24px, c8:24px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| FUCHSIA_CITY | (26,25) | (27,27) | `WARDENS_HOUSE` |
+| FUCHSIA_CITY | (30,25) | (31,27) | `FUCHSIA_GOOD_ROD_HOUSE` |
+| PALLET_TOWN | (4,3) | (5,5) | `REDS_HOUSE_1F` |
+| PALLET_TOWN | (12,3) | (13,5) | `BLUES_HOUSE` |
+| ROUTE_25 | (44,1) | (45,3) | `BILLS_HOUSE` |
+| SAFFRON_CITY | (6,3) | (7,5) | `COPYCATS_HOUSE_1F` |
+| SAFFRON_CITY | (12,9) | (13,11) | `SAFFRON_PIDGEY_HOUSE` |
diff --git a/assets/docs/buildings/B08-unnamed-building.md b/assets/docs/buildings/B08-unnamed-building.md
new file mode 100644
index 0000000..1f553e4
--- /dev/null
+++ b/assets/docs/buildings/B08-unnamed-building.md
@@ -0,0 +1,143 @@
+# B08 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **4 x 6 cells** (8 x 12 tiles of 8px, 64 x 96 px). Appears **6 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..6; tile column c=1..8, tile row r=1..12. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | B B | B B | B C |
+ | D E | E E | E E | E D |
+ +-----+-----+-----+-----+
+ y2 | D E | E E | E E | E D |
+ | F G | G G | G G | G H |
+ +-----+-----+-----+-----+
+ y3 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y4 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y5 | I J | J J | J J | J K |
+ | I L | L L | L L | L K |
+ +-----+-----+-----+-----+
+ y6 | I L | L L | L L | L K |
+ | M N | N N | N N | N O |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+
+ y6 | 15 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 75, 75, 75, 75, 75, 75, 31 }, -- r11
+ { 78, 26, 26, 26, 26, 26, 26, 79 }, -- r12
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 6x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 12x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 6x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 7x
+- `J` = tile 10, used 18x
+- `K` = tile 31, used 7x
+- `L` = tile 75, used 24x
+- `M` = tile 78, used 1x
+- `N` = tile 26, used 6x
+- `O` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID
+```
+
+Every cell is solid - this building has no door of its own.
+
+## Silhouette
+
+322 of the 6144 px inside the box are ground outside the black outline, so the 64x96 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c8:25px
+ r2 c1:24px, c8:24px
+ r3 c1:24px, c8:24px
+ r4 c1:24px, c8:24px
+ r5 c1:8px, c8:8px
+ r6 c1:8px, c8:8px
+ r7 c1:8px, c8:8px
+ r8 c1:8px, c8:8px
+ r9 c1:8px, c8:8px
+ r10 c1:8px, c8:8px
+ r11 c1:8px, c8:8px
+ r12 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (2,4) | - | scenery, no entrance |
+| SAFFRON_CITY | (6,16) | - | scenery, no entrance |
+| SAFFRON_CITY | (10,16) | - | scenery, no entrance |
+| SAFFRON_CITY | (24,16) | - | scenery, no entrance |
+| SAFFRON_CITY | (28,16) | - | scenery, no entrance |
+| SAFFRON_CITY | (32,16) | - | scenery, no entrance |
diff --git a/assets/docs/buildings/B09-unnamed-building.md b/assets/docs/buildings/B09-unnamed-building.md
new file mode 100644
index 0000000..efc9c64
--- /dev/null
+++ b/assets/docs/buildings/B09-unnamed-building.md
@@ -0,0 +1,124 @@
+# B09 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 4 cells** (12 x 8 tiles of 8px, 96 x 64 px). Appears **5 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..4; tile column c=1..12, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J K | K J | J J | J L |
+ | I K | K K | K K | K K | K K | K L |
+ +-----+-----+-----+-----+-----+-----+
+ y4 | I K | K K | M N | J J | K K | K L |
+ | O P | P P | * Q | P P | P P | P R |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 75 | 75 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y4 | 15 75 | 75 75 | 11 12 | 10 10 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 27 28 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 31 }, -- r7
+ { 78, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 10x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 20x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 10x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 10x
+- `K` = tile 75, used 18x
+- `L` = tile 31, used 3x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 8x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID DOOR SOLID SOLID SOLID
+```
+
+Enterable cell: (3,4).
+
+## Silhouette
+
+258 of the 6144 px inside the box are ground outside the black outline, so the 96x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c12:25px
+ r2 c1:24px, c12:24px
+ r3 c1:24px, c12:24px
+ r4 c1:24px, c12:24px
+ r5 c1:8px, c12:8px
+ r6 c1:8px, c12:8px
+ r7 c1:8px, c12:8px
+ r8 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (26,16) | (28,19) | `GAME_CORNER` |
+| CINNABAR_ISLAND | (4,0) | (6,3) | `POKEMON_MANSION_1F` |
+| CINNABAR_ISLAND | (4,6) | (6,9) | `CINNABAR_LAB` |
+| FUCHSIA_CITY | (16,0) | (18,3) | `SAFARI_ZONE_GATE` |
+| FUCHSIA_CITY | (20,10) | (22,13) | `FUCHSIA_MEETING_ROOM` |
diff --git a/assets/docs/buildings/B10-unnamed-building.md b/assets/docs/buildings/B10-unnamed-building.md
new file mode 100644
index 0000000..e06b9a7
--- /dev/null
+++ b/assets/docs/buildings/B10-unnamed-building.md
@@ -0,0 +1,131 @@
+# B10 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 4 cells** (12 x 8 tiles of 8px, 96 x 64 px). Appears **5 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..4; tile column c=1..12, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | D E |
+ | F G | H H | H H | H H | H H | G I |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | F G | H H | H H | H H | H H | G I |
+ | F J | K K | K K | K K | K K | L I |
+ +-----+-----+-----+-----+-----+-----+
+ y3 | M N | O O | P Q | R P | O O | S T |
+ | U P | P P | P P | P P | P P | P V |
+ +-----+-----+-----+-----+-----+-----+
+ y4 | U O | O O | O O | O O | W X | O V |
+ | Y Z | Z Z | Z Z | Z Z | * a | Z b |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 5 6 | 83 83 | 83 83 | 83 83 | 83 83 | 8 9 |
+ | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ | 21 22 | 23 23 | 23 23 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+---------+---------+
+ y3 | 37 38 | 10 10 | 34 47 | 63 34 | 10 10 | 40 41 |
+ | 15 34 | 34 34 | 34 34 | 34 34 | 34 34 | 34 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 11 12 | 10 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 27 28 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, -- r1
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r2
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r3
+ { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, -- r4
+ { 37, 38, 10, 10, 34, 47, 63, 34, 10, 10, 40, 41 }, -- r5
+ { 15, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 11, 12, 10, 31 }, -- r7
+ { 78, 26, 26, 26, 26, 26, 26, 26, 27, 28, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 83, used 8x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 3x
+- `G` = tile 56, used 4x
+- `H` = tile 18, used 16x
+- `I` = tile 25, used 3x
+- `J` = tile 22, used 1x
+- `K` = tile 23, used 8x
+- `L` = tile 24, used 1x
+- `M` = tile 37, used 1x
+- `N` = tile 38, used 1x
+- `O` = tile 10, used 12x
+- `P` = tile 34, used 12x
+- `Q` = tile 47, used 1x
+- `R` = tile 63, used 1x
+- `S` = tile 40, used 1x
+- `T` = tile 41, used 1x
+- `U` = tile 15, used 2x
+- `V` = tile 31, used 2x
+- `W` = tile 11, used 1x
+- `X` = tile 12, used 1x
+- `Y` = tile 78, used 1x
+- `Z` = tile 26, used 8x
+- `a` = tile 28, used 1x
+- `b` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID DOOR SOLID
+```
+
+Enterable cell: (5,4).
+
+## Silhouette
+
+186 of the 6144 px inside the box are ground outside the black outline, so the 96x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c11:16px, c12:48px
+ r5 c1:5px, c12:5px
+ r6 c1:8px, c12:8px
+ r7 c1:8px, c12:8px
+ r8 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CINNABAR_ISLAND | (14,0) | (18,3) | `CINNABAR_GYM` |
+| PEWTER_CITY | (12,14) | (16,17) | `PEWTER_GYM` |
+| SAFFRON_CITY | (22,0) | (26,3) | `FIGHTING_DOJO` |
+| VERMILION_CITY | (8,16) | (12,19) | `VERMILION_GYM` |
+| VIRIDIAN_CITY | (28,4) | (32,7) | `VIRIDIAN_GYM` |
diff --git a/assets/docs/buildings/B11-unnamed-building.md b/assets/docs/buildings/B11-unnamed-building.md
new file mode 100644
index 0000000..752b3d9
--- /dev/null
+++ b/assets/docs/buildings/B11-unnamed-building.md
@@ -0,0 +1,102 @@
+# B11 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 2 cells** (12 x 4 tiles of 8px, 96 x 32 px). Appears **5 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..2; tile column c=1..12, tile row r=1..4. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | D E |
+ | F G | H H | H H | H H | H H | I J |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | K L | M N | O M | M O | M M | P Q |
+ | R S | S S | S S | S S | S S | S T |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 5 6 | 7 7 | 7 7 | 7 7 | 7 7 | 8 9 |
+ | 21 22 | 23 23 | 23 23 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 37 38 | 10 34 | 35 10 | 10 35 | 10 10 | 40 41 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 9 }, -- r1
+ { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, -- r2
+ { 37, 38, 10, 34, 35, 10, 10, 35, 10, 10, 40, 41 }, -- r3
+ { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r4
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 7, used 8x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 1x
+- `G` = tile 22, used 1x
+- `H` = tile 23, used 8x
+- `I` = tile 24, used 1x
+- `J` = tile 25, used 1x
+- `K` = tile 37, used 1x
+- `L` = tile 38, used 1x
+- `M` = tile 10, used 5x
+- `N` = tile 34, used 1x
+- `O` = tile 35, used 2x
+- `P` = tile 40, used 1x
+- `Q` = tile 41, used 1x
+- `R` = tile 78, used 1x
+- `S` = tile 26, used 10x
+- `T` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+```
+
+Every cell is solid - this building has no door of its own.
+
+## Silhouette
+
+154 of the 3072 px inside the box are ground outside the black outline, so the 96x32 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c11:16px, c12:48px
+ r3 c1:5px, c12:5px
+ r4 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (20,14) | - | scenery, no entrance |
+| CERULEAN_CITY | (14,10) | - | scenery, no entrance |
+| CERULEAN_CITY | (34,10) | - | scenery, no entrance |
+| CERULEAN_CITY | (18,24) | - | scenery, no entrance |
+| CERULEAN_CITY | (28,24) | - | scenery, no entrance |
diff --git a/assets/docs/buildings/B12-unnamed-building.md b/assets/docs/buildings/B12-unnamed-building.md
new file mode 100644
index 0000000..87ec66f
--- /dev/null
+++ b/assets/docs/buildings/B12-unnamed-building.md
@@ -0,0 +1,99 @@
+# B12 - Unnamed building
+
+
+
+`FOREST` tileset, **4 x 2 cells** (8 x 4 tiles of 8px, 64 x 32 px). Appears **5 times** in the game. Tile ids index `assets/generated/tilesets/forest.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..4, y=1..2; tile column c=1..8, tile row r=1..4. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4
+ +-----+-----+-----+-----+
+ y1 | A B | B B | B B | B C |
+ | D E | E E | E E | E F |
+ +-----+-----+-----+-----+
+ y2 | G H | I J | K K | H L |
+ | M H | * N | H H | H O |
+ +-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4
+ +---------+---------+---------+---------+
+ y1 | 8 9 | 9 9 | 9 9 | 9 12 |
+ | 24 25 | 25 25 | 25 25 | 25 28 |
+ +---------+---------+---------+---------+
+ y2 | 40 41 | 42 43 | 1 1 | 41 44 |
+ | 56 41 | 58 59 | 41 41 | 41 60 |
+ +---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 8, 9, 9, 9, 9, 9, 9, 12 }, -- r1
+ { 24, 25, 25, 25, 25, 25, 25, 28 }, -- r2
+ { 40, 41, 42, 43, 1, 1, 41, 44 }, -- r3
+ { 56, 41, 58, 59, 41, 41, 41, 60 }, -- r4
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 58, used 1x
+- `A` = tile 8, used 1x
+- `B` = tile 9, used 6x
+- `C` = tile 12, used 1x
+- `D` = tile 24, used 1x
+- `E` = tile 25, used 6x
+- `F` = tile 28, used 1x
+- `G` = tile 40, used 1x
+- `H` = tile 41, used 6x
+- `I` = tile 42, used 1x
+- `J` = tile 43, used 1x
+- `K` = tile 1, used 2x
+- `L` = tile 44, used 1x
+- `M` = tile 56, used 1x
+- `N` = tile 59, used 1x
+- `O` = tile 60, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID
+ y2 SOLID DOOR SOLID SOLID
+```
+
+Enterable cell: (2,2).
+
+## Silhouette
+
+101 of the 2048 px inside the box are ground outside the black outline, so the 64x32 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:17px, c8:17px
+ r2 c1:16px, c8:17px
+ r3 c1:10px, c8:8px
+ r4 c1:8px, c8:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| SAFARI_ZONE_CENTER | (16,18) | (17,19) | `SAFARI_ZONE_CENTER_REST_HOUSE` |
+| SAFARI_ZONE_EAST | (24,8) | (25,9) | `SAFARI_ZONE_EAST_REST_HOUSE` |
+| SAFARI_ZONE_NORTH | (34,2) | (35,3) | `SAFARI_ZONE_NORTH_REST_HOUSE` |
+| SAFARI_ZONE_WEST | (2,2) | (3,3) | `SAFARI_ZONE_SECRET_HOUSE` |
+| SAFARI_ZONE_WEST | (10,10) | (11,11) | `SAFARI_ZONE_WEST_REST_HOUSE` |
diff --git a/assets/docs/buildings/B13-unnamed-building.md b/assets/docs/buildings/B13-unnamed-building.md
new file mode 100644
index 0000000..3172b70
--- /dev/null
+++ b/assets/docs/buildings/B13-unnamed-building.md
@@ -0,0 +1,119 @@
+# B13 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 4 cells** (12 x 8 tiles of 8px, 96 x 64 px). Appears **4 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..4; tile column c=1..12, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y4 | I L | L L | L L | L L | L L | L K |
+ | M N | N N | N N | N N | N N | N O |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y4 | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r7
+ { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 10x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 20x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 10x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 10x
+- `K` = tile 31, used 3x
+- `L` = tile 75, used 20x
+- `M` = tile 78, used 1x
+- `N` = tile 26, used 10x
+- `O` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID
+```
+
+Every cell is solid - this building has no door of its own.
+
+## Silhouette
+
+258 of the 6144 px inside the box are ground outside the black outline, so the 96x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c12:25px
+ r2 c1:24px, c12:24px
+ r3 c1:24px, c12:24px
+ r4 c1:24px, c12:24px
+ r5 c1:8px, c12:8px
+ r6 c1:8px, c12:8px
+ r7 c1:8px, c12:8px
+ r8 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| ROUTE_15 | (8,6) | - | scenery, no entrance |
+| ROUTE_16 | (18,2) | - | scenery, no entrance |
+| ROUTE_7 | (12,8) | - | scenery, no entrance |
+| ROUTE_8 | (2,8) | - | scenery, no entrance |
diff --git a/assets/docs/buildings/B14-unnamed-building.md b/assets/docs/buildings/B14-unnamed-building.md
new file mode 100644
index 0000000..8abc1c1
--- /dev/null
+++ b/assets/docs/buildings/B14-unnamed-building.md
@@ -0,0 +1,140 @@
+# B14 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 6 cells** (12 x 12 tiles of 8px, 96 x 96 px). Appears **3 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..6; tile column c=1..12, tile row r=1..12. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y4 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y5 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y6 | I L | L L | L L | L L | L L | L K |
+ | M N | N N | N N | N N | N N | N O |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y6 | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r11
+ { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r12
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 10x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 20x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 10x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 7x
+- `J` = tile 10, used 30x
+- `K` = tile 31, used 7x
+- `L` = tile 75, used 40x
+- `M` = tile 78, used 1x
+- `N` = tile 26, used 10x
+- `O` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID SOLID SOLID
+```
+
+Every cell is solid - this building has no door of its own.
+
+## Silhouette
+
+322 of the 9216 px inside the box are ground outside the black outline, so the 96x96 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c12:25px
+ r2 c1:24px, c12:24px
+ r3 c1:24px, c12:24px
+ r4 c1:24px, c12:24px
+ r5 c1:8px, c12:8px
+ r6 c1:8px, c12:8px
+ r7 c1:8px, c12:8px
+ r8 c1:8px, c12:8px
+ r9 c1:8px, c12:8px
+ r10 c1:8px, c12:8px
+ r11 c1:8px, c12:8px
+ r12 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (14,4) | - | scenery, no entrance |
+| ROUTE_16 | (18,6) | - | scenery, no entrance |
+| ROUTE_18 | (34,4) | - | scenery, no entrance |
diff --git a/assets/docs/buildings/B15-unnamed-building.md b/assets/docs/buildings/B15-unnamed-building.md
new file mode 100644
index 0000000..4a78d79
--- /dev/null
+++ b/assets/docs/buildings/B15-unnamed-building.md
@@ -0,0 +1,144 @@
+# B15 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 6 cells** (12 x 12 tiles of 8px, 96 x 96 px). Appears **3 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..6; tile column c=1..12, tile row r=1..12. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y4 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y5 | I J | J J | J L | L J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y6 | I L | L L | M N | J J | L L | L K |
+ | O P | P P | * Q | P P | P P | P R |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 75 | 75 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y6 | 15 75 | 75 75 | 11 12 | 10 10 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 27 28 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 31 }, -- r11
+ { 78, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79 }, -- r12
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 10x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 20x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 10x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 7x
+- `J` = tile 10, used 30x
+- `K` = tile 31, used 7x
+- `L` = tile 75, used 38x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 8x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID DOOR SOLID SOLID SOLID
+```
+
+Enterable cell: (3,6).
+
+## Silhouette
+
+322 of the 9216 px inside the box are ground outside the black outline, so the 96x96 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c12:25px
+ r2 c1:24px, c12:24px
+ r3 c1:24px, c12:24px
+ r4 c1:24px, c12:24px
+ r5 c1:8px, c12:8px
+ r6 c1:8px, c12:8px
+ r7 c1:8px, c12:8px
+ r8 c1:8px, c12:8px
+ r9 c1:8px, c12:8px
+ r10 c1:8px, c12:8px
+ r11 c1:8px, c12:8px
+ r12 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (22,4) | (24,9) | `CELADON_MANSION_1F` |
+| ROUTE_12 | (8,16) | (10,21) | `ROUTE_12_GATE_1F` |
+| ROUTE_6 | (8,2) | (10,7) | `ROUTE_6_GATE` |
diff --git a/assets/docs/buildings/B16-unnamed-building.md b/assets/docs/buildings/B16-unnamed-building.md
new file mode 100644
index 0000000..f0d0598
--- /dev/null
+++ b/assets/docs/buildings/B16-unnamed-building.md
@@ -0,0 +1,129 @@
+# B16 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **8 x 4 cells** (16 x 8 tiles of 8px, 128 x 64 px). Appears **3 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..8, y=1..4; tile column c=1..16, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | C C | C C | D E |
+ | F G | H H | H H | H H | H H | H H | H H | G I |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | F G | H H | H H | H H | H H | H H | H H | G I |
+ | F J | K K | K K | K K | K K | K K | K K | L I |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | M N | O O | P Q | R P | P Q | R P | O O | S T |
+ | U P | P P | P P | P P | P P | P P | P P | P V |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | U O | O O | O O | O O | O O | O O | W X | O V |
+ | Y Z | Z Z | Z Z | Z Z | Z Z | Z Z | * a | Z b |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 5 6 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 8 9 |
+ | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ | 21 22 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 37 38 | 10 10 | 34 47 | 63 34 | 34 47 | 63 34 | 10 10 | 40 41 |
+ | 15 34 | 34 34 | 34 34 | 34 34 | 34 34 | 34 34 | 34 34 | 34 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 11 12 | 10 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 26 26 | 26 26 | 27 28 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, -- r1
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r2
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r3
+ { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, -- r4
+ { 37, 38, 10, 10, 34, 47, 63, 34, 34, 47, 63, 34, 10, 10, 40, 41 }, -- r5
+ { 15, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 12, 10, 31 }, -- r7
+ { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 28, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 83, used 12x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 3x
+- `G` = tile 56, used 4x
+- `H` = tile 18, used 24x
+- `I` = tile 25, used 3x
+- `J` = tile 22, used 1x
+- `K` = tile 23, used 12x
+- `L` = tile 24, used 1x
+- `M` = tile 37, used 1x
+- `N` = tile 38, used 1x
+- `O` = tile 10, used 16x
+- `P` = tile 34, used 18x
+- `Q` = tile 47, used 2x
+- `R` = tile 63, used 2x
+- `S` = tile 40, used 1x
+- `T` = tile 41, used 1x
+- `U` = tile 15, used 2x
+- `V` = tile 31, used 2x
+- `W` = tile 11, used 1x
+- `X` = tile 12, used 1x
+- `Y` = tile 78, used 1x
+- `Z` = tile 26, used 12x
+- `a` = tile 28, used 1x
+- `b` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID DOOR SOLID
+```
+
+Enterable cell: (7,4).
+
+## Silhouette
+
+186 of the 8192 px inside the box are ground outside the black outline, so the 128x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c15:16px, c16:48px
+ r5 c1:5px, c16:5px
+ r6 c1:8px, c16:8px
+ r7 c1:8px, c16:8px
+ r8 c1:8px, c16:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (6,24) | (12,27) | `CELADON_GYM` |
+| CERULEAN_CITY | (24,16) | (30,19) | `CERULEAN_GYM` |
+| SAFFRON_CITY | (28,0) | (34,3) | `SAFFRON_GYM` |
diff --git a/assets/docs/buildings/B17-unnamed-building.md b/assets/docs/buildings/B17-unnamed-building.md
new file mode 100644
index 0000000..e0e664f
--- /dev/null
+++ b/assets/docs/buildings/B17-unnamed-building.md
@@ -0,0 +1,103 @@
+# B17 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 2 cells** (12 x 4 tiles of 8px, 96 x 32 px). Appears **3 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..2; tile column c=1..12, tile row r=1..4. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | D E |
+ | F G | H H | H H | H H | H H | I J |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | K L | M N | O P | P O | P P | Q R |
+ | S T | * U | T T | T T | T T | T V |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 5 6 | 7 7 | 7 7 | 7 7 | 7 7 | 8 9 |
+ | 21 22 | 23 23 | 23 23 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 37 38 | 11 12 | 35 10 | 10 35 | 10 10 | 40 41 |
+ | 78 26 | 27 28 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 9 }, -- r1
+ { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, -- r2
+ { 37, 38, 11, 12, 35, 10, 10, 35, 10, 10, 40, 41 }, -- r3
+ { 78, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r4
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 7, used 8x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 1x
+- `G` = tile 22, used 1x
+- `H` = tile 23, used 8x
+- `I` = tile 24, used 1x
+- `J` = tile 25, used 1x
+- `K` = tile 37, used 1x
+- `L` = tile 38, used 1x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 35, used 2x
+- `P` = tile 10, used 4x
+- `Q` = tile 40, used 1x
+- `R` = tile 41, used 1x
+- `S` = tile 78, used 1x
+- `T` = tile 26, used 8x
+- `U` = tile 28, used 1x
+- `V` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID DOOR SOLID SOLID SOLID SOLID
+```
+
+Enterable cell: (2,2).
+
+## Silhouette
+
+154 of the 3072 px inside the box are ground outside the black outline, so the 96x32 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c11:16px, c12:48px
+ r3 c1:5px, c12:5px
+ r4 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CERULEAN_CITY | (8,10) | (9,11) | `CERULEAN_BADGE_HOUSE` |
+| CERULEAN_CITY | (26,10) | (27,11) | `CERULEAN_TRASHED_HOUSE` |
+| CERULEAN_CITY | (12,14) | (13,15) | `CERULEAN_TRADE_HOUSE` |
diff --git a/assets/docs/buildings/B18-unnamed-building.md b/assets/docs/buildings/B18-unnamed-building.md
new file mode 100644
index 0000000..f9789b5
--- /dev/null
+++ b/assets/docs/buildings/B18-unnamed-building.md
@@ -0,0 +1,121 @@
+# B18 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **6 x 4 cells** (12 x 8 tiles of 8px, 96 x 64 px). Appears **2 times** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..6, y=1..4; tile column c=1..12, tile row r=1..8. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6
+ +-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+
+ y4 | I L | M N | L L | L L | L L | L K |
+ | O P | * Q | P P | P P | P P | P R |
+ +-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6
+ +---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+
+ y4 | 15 75 | 11 12 | 75 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 27 28 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 75, 11, 12, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r7
+ { 78, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r8
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 10x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 20x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 10x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 3x
+- `J` = tile 10, used 10x
+- `K` = tile 31, used 3x
+- `L` = tile 75, used 18x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 8x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID DOOR SOLID SOLID SOLID SOLID
+```
+
+Enterable cell: (2,4).
+
+## Silhouette
+
+258 of the 6144 px inside the box are ground outside the black outline, so the 96x64 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c12:25px
+ r2 c1:24px, c12:24px
+ r3 c1:24px, c12:24px
+ r4 c1:24px, c12:24px
+ r5 c1:8px, c12:8px
+ r6 c1:8px, c12:8px
+ r7 c1:8px, c12:8px
+ r8 c1:8px, c12:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| PEWTER_CITY | (18,2) | (19,5) | `MUSEUM_1F` |
+| ROUTE_2 | (14,36) | (15,39) | `ROUTE_2_GATE` |
diff --git a/assets/docs/buildings/B19-unnamed-building.md b/assets/docs/buildings/B19-unnamed-building.md
new file mode 100644
index 0000000..18c98ac
--- /dev/null
+++ b/assets/docs/buildings/B19-unnamed-building.md
@@ -0,0 +1,135 @@
+# B19 - Unnamed building
+
+
+
+`PLATEAU` tileset, **20 x 5 cells** (40 x 10 tiles of 8px, 320 x 80 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/plateau.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..20, y=1..5; tile column c=1..40, tile row r=1..10. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | A B |
+ | D E | F F | F F | F F | F F | F F | G H | C C | C C | C C | C C | C C | C C | G H | F F | F F | F F | F F | F F | D E |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | I J | K K | K K | K K | I J | K K | L M | C C | C C | C C | C C | C C | C C | L M | K K | I J | K K | K K | K K | I J |
+ | N O | K K | K K | K K | N O | K K | L M | C C | C C | C C | C C | C C | C C | L M | K K | N O | K K | K K | K K | N O |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | N O | K K | K K | K K | N O | K K | A B | C C | C C | C C | C C | C C | C C | A B | K K | N O | K K | K K | K K | N O |
+ | I J | P P | P P | P P | I J | P P | D E | F F | F F | F F | F F | F F | F F | D E | P P | I J | P P | P P | P P | I J |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | Q Q | Q Q | Q Q | Q Q | Q Q | Q Q | I J | K K | K K | K K | K K | K K | K K | I J | Q Q | Q Q | Q Q | Q Q | Q Q | Q Q |
+ | R Q | Q Q | R Q | Q Q | R Q | Q Q | N O | K K | K K | K K | K K | K K | K K | N O | R Q | Q Q | R Q | Q Q | R Q | Q Q |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y5 | Q Q | Q Q | Q Q | Q Q | Q Q | Q Q | N O | K K | K K | S T | S T | K K | K K | N O | Q Q | Q Q | Q Q | Q Q | Q Q | Q Q |
+ | Q Q | R Q | Q Q | R Q | Q Q | R Q | I J | P P | P P | * U | * U | P P | P P | I J | Q Q | R Q | Q Q | R Q | Q Q | R Q |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 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 | 3 3 | 3 3 | 37 38 |
+ | 40 41 | 13 13 | 13 13 | 13 13 | 13 13 | 13 13 | 32 33 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 32 33 | 13 13 | 13 13 | 13 13 | 13 13 | 13 13 | 40 41 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 21 22 | 15 15 | 15 15 | 15 15 | 21 22 | 15 15 | 46 47 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 46 47 | 15 15 | 21 22 | 15 15 | 15 15 | 15 15 | 21 22 |
+ | 5 6 | 15 15 | 15 15 | 15 15 | 5 6 | 15 15 | 46 47 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 46 47 | 15 15 | 5 6 | 15 15 | 15 15 | 15 15 | 5 6 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 5 6 | 15 15 | 15 15 | 15 15 | 5 6 | 15 15 | 37 38 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 3 3 | 37 38 | 15 15 | 5 6 | 15 15 | 15 15 | 15 15 | 5 6 |
+ | 21 22 | 14 14 | 14 14 | 14 14 | 21 22 | 14 14 | 40 41 | 13 13 | 13 13 | 13 13 | 13 13 | 13 13 | 13 13 | 40 41 | 14 14 | 21 22 | 14 14 | 14 14 | 14 14 | 21 22 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 | 21 22 | 15 15 | 15 15 | 15 15 | 15 15 | 15 15 | 15 15 | 21 22 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 |
+ | 45 35 | 35 35 | 45 35 | 35 35 | 45 35 | 35 35 | 5 6 | 15 15 | 15 15 | 15 15 | 15 15 | 15 15 | 15 15 | 5 6 | 45 35 | 35 35 | 45 35 | 35 35 | 45 35 | 35 35 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y5 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 | 5 6 | 15 15 | 15 15 | 11 12 | 11 12 | 15 15 | 15 15 | 5 6 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 | 35 35 |
+ | 35 35 | 45 35 | 35 35 | 45 35 | 35 35 | 45 35 | 21 22 | 14 14 | 14 14 | 27 28 | 27 28 | 14 14 | 14 14 | 21 22 | 35 35 | 45 35 | 35 35 | 45 35 | 35 35 | 45 35 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 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, 3, 3, 3, 3, 37, 38 }, -- r1
+ { 40, 41, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 32, 33, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 32, 33, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 40, 41 }, -- r2
+ { 21, 22, 15, 15, 15, 15, 15, 15, 21, 22, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 21, 22, 15, 15, 15, 15, 15, 15, 21, 22 }, -- r3
+ { 5, 6, 15, 15, 15, 15, 15, 15, 5, 6, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 5, 6, 15, 15, 15, 15, 15, 15, 5, 6 }, -- r4
+ { 5, 6, 15, 15, 15, 15, 15, 15, 5, 6, 15, 15, 37, 38, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 37, 38, 15, 15, 5, 6, 15, 15, 15, 15, 15, 15, 5, 6 }, -- r5
+ { 21, 22, 14, 14, 14, 14, 14, 14, 21, 22, 14, 14, 40, 41, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 40, 41, 14, 14, 21, 22, 14, 14, 14, 14, 14, 14, 21, 22 }, -- r6
+ { 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 21, 22, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 21, 22, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35 }, -- r7
+ { 45, 35, 35, 35, 45, 35, 35, 35, 45, 35, 35, 35, 5, 6, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 5, 6, 45, 35, 35, 35, 45, 35, 35, 35, 45, 35, 35, 35 }, -- r8
+ { 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 5, 6, 15, 15, 15, 15, 11, 12, 11, 12, 15, 15, 15, 15, 5, 6, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35 }, -- r9
+ { 35, 35, 45, 35, 35, 35, 45, 35, 35, 35, 45, 35, 21, 22, 14, 14, 14, 14, 27, 28, 27, 28, 14, 14, 14, 14, 21, 22, 35, 35, 45, 35, 35, 35, 45, 35, 35, 35, 45, 35 }, -- r10
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 2x
+- `A` = tile 37, used 4x
+- `B` = tile 38, used 4x
+- `C` = tile 3, used 84x
+- `D` = tile 40, used 4x
+- `E` = tile 41, used 4x
+- `F` = tile 13, used 32x
+- `G` = tile 32, used 2x
+- `H` = tile 33, used 2x
+- `I` = tile 21, used 12x
+- `J` = tile 22, used 12x
+- `K` = tile 15, used 80x
+- `L` = tile 46, used 4x
+- `M` = tile 47, used 4x
+- `N` = tile 5, used 12x
+- `O` = tile 6, used 12x
+- `P` = tile 14, used 24x
+- `Q` = tile 35, used 84x
+- `R` = tile 45, used 12x
+- `S` = tile 11, used 2x
+- `T` = tile 12, used 2x
+- `U` = tile 28, used 2x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 DOOR DOOR DOOR DOOR DOOR DOOR SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID DOOR DOOR DOOR DOOR DOOR DOOR
+ y5 DOOR DOOR DOOR DOOR DOOR DOOR SOLID SOLID SOLID DOOR DOOR SOLID SOLID SOLID DOOR DOOR DOOR DOOR DOOR DOOR
+```
+
+Enterable cells: (1,4), (2,4), (3,4), (4,4), (5,4), (6,4), (15,4), (16,4), (17,4), (18,4), (19,4), (20,4), (1,5), (2,5), (3,5), (4,5), (5,5), (6,5), (10,5), (11,5), (15,5), (16,5), (17,5), (18,5), (19,5), (20,5).
+
+## Silhouette
+
+1621 of the 25600 px inside the box are ground outside the black outline, so the 320x80 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:14px, c2:14px, c3:4px, c4:4px, c5:4px, c6:4px, c7:4px, c8:4px, c9:4px, c10:4px, c11:4px, c12:4px, c13:4px, c14:4px, c15:4px, c16:4px, c17:4px, c18:4px, c19:4px, c20:4px, c21:4px, c22:4px, c23:4px, c24:4px, c25:4px, c26:4px, c27:4px, c28:4px, c29:4px, c30:4px, c31:4px, c32:4px, c33:4px, c34:4px, c35:4px, c36:4px, c37:4px, c38:4px, c39:14px, c40:14px
+ r2 c1:10px, c2:10px, c3:16px, c4:16px, c5:16px, c6:16px, c7:16px, c8:16px, c9:16px, c10:16px, c11:16px, c12:16px, c13:15px, c14:15px, c15:1px, c27:15px, c28:15px, c29:16px, c30:16px, c31:16px, c32:16px, c33:16px, c34:16px, c35:16px, c36:16px, c37:16px, c38:16px, c39:10px, c40:10px
+ r3 c1:10px, c2:10px, c9:10px, c10:10px, c13:8px, c14:8px, c15:1px, c27:8px, c28:8px, c31:10px, c32:10px, c39:10px, c40:10px
+ r4 c1:8px, c2:8px, c9:8px, c10:8px, c13:8px, c14:8px, c15:1px, c27:8px, c28:8px, c31:8px, c32:8px, c39:8px, c40:8px
+ r5 c1:8px, c2:8px, c9:8px, c10:8px, c13:13px, c14:13px, c15:1px, c27:13px, c28:13px, c31:8px, c32:8px, c39:8px, c40:8px
+ r6 c1:10px, c2:10px, c3:9px, c4:8px, c5:8px, c6:8px, c7:8px, c8:8px, c9:10px, c10:10px, c11:9px, c12:8px, c13:10px, c14:10px, c15:16px, c16:16px, c17:16px, c18:16px, c19:16px, c20:16px, c21:16px, c22:16px, c23:16px, c24:16px, c25:16px, c26:16px, c27:10px, c28:10px, c29:9px, c30:8px, c31:10px, c32:10px, c33:9px, c34:8px, c35:8px, c36:8px, c37:8px, c38:8px, c39:10px, c40:10px
+ r7 c13:10px, c14:10px, c27:10px, c28:10px
+ r8 c13:8px, c14:8px, c27:8px, c28:8px
+ r9 c13:8px, c14:8px, c27:8px, c28:8px
+ r10 c13:15px, c14:15px, c15:9px, c16:8px, c17:8px, c18:8px, c23:8px, c24:8px, c25:8px, c26:8px, c27:15px, c28:15px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| INDIGO_PLATEAU | (0,1) | (9,5) | `INDIGO_PLATEAU_LOBBY` |
+| INDIGO_PLATEAU | (0,1) | (10,5) | `INDIGO_PLATEAU_LOBBY` |
diff --git a/assets/docs/buildings/B20-unnamed-building.md b/assets/docs/buildings/B20-unnamed-building.md
new file mode 100644
index 0000000..203e74e
--- /dev/null
+++ b/assets/docs/buildings/B20-unnamed-building.md
@@ -0,0 +1,208 @@
+# B20 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **8 x 12 cells** (16 x 24 tiles of 8px, 128 x 192 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..8, y=1..12; tile column c=1..16, tile row r=1..24. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y5 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y6 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y7 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y8 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y9 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y10 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y11 | I J | J J | J L | L J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y12 | I L | L L | M N | J J | L L | L L | L L | L K |
+ | O P | P P | * Q | P P | P P | P P | P P | P R |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y6 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y7 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y8 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y9 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y10 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y11 | 15 10 | 10 10 | 10 75 | 75 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y12 | 15 75 | 75 75 | 11 12 | 10 10 | 75 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 27 28 | 26 26 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r11
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r12
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r13
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r14
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r15
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r16
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r17
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r18
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r19
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r20
+ { 15, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r21
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r22
+ { 15, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r23
+ { 78, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r24
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 14x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 28x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 14x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 19x
+- `J` = tile 10, used 126x
+- `K` = tile 31, used 19x
+- `L` = tile 75, used 138x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 12x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y7 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y8 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y9 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y10 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y11 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y12 SOLID SOLID DOOR SOLID SOLID SOLID SOLID SOLID
+```
+
+Enterable cell: (3,12).
+
+## Silhouette
+
+514 of the 24576 px inside the box are ground outside the black outline, so the 128x192 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c16:25px
+ r2 c1:24px, c16:24px
+ r3 c1:24px, c16:24px
+ r4 c1:24px, c16:24px
+ r5 c1:8px, c16:8px
+ r6 c1:8px, c16:8px
+ r7 c1:8px, c16:8px
+ r8 c1:8px, c16:8px
+ r9 c1:8px, c16:8px
+ r10 c1:8px, c16:8px
+ r11 c1:8px, c16:8px
+ r12 c1:8px, c16:8px
+ r13 c1:8px, c16:8px
+ r14 c1:8px, c16:8px
+ r15 c1:8px, c16:8px
+ r16 c1:8px, c16:8px
+ r17 c1:8px, c16:8px
+ r18 c1:8px, c16:8px
+ r19 c1:8px, c16:8px
+ r20 c1:8px, c16:8px
+ r21 c1:8px, c16:8px
+ r22 c1:8px, c16:8px
+ r23 c1:8px, c16:8px
+ r24 c1:8px, c16:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| SAFFRON_CITY | (16,10) | (18,21) | `SILPH_CO_1F` |
diff --git a/assets/docs/buildings/B21-unnamed-building.md b/assets/docs/buildings/B21-unnamed-building.md
new file mode 100644
index 0000000..eb04ebb
--- /dev/null
+++ b/assets/docs/buildings/B21-unnamed-building.md
@@ -0,0 +1,142 @@
+# B21 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **12 x 6 cells** (24 x 12 tiles of 8px, 192 x 96 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..12, y=1..6; tile column c=1..24, tile row r=1..12. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B B | B B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E E | E E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E E | E E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G G | G G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | I J | J J | J J | J J | J J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y5 | I J | J J | J J | J J | J J | J J | J L | L J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y6 | I L | L L | L L | L L | L L | L L | M N | J J | L L | L L | L L | L K |
+ | O P | P P | P P | P P | P P | P P | * Q | P P | P P | P P | P P | P R |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 75 | 75 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y6 | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 11 12 | 10 10 | 75 75 | 75 75 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 26 26 | 26 26 | 27 28 | 26 26 | 26 26 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r11
+ { 78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79 }, -- r12
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 22x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 44x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 22x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 7x
+- `J` = tile 10, used 66x
+- `K` = tile 31, used 7x
+- `L` = tile 75, used 86x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 20x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID SOLID SOLID DOOR SOLID SOLID SOLID SOLID SOLID
+```
+
+Enterable cell: (7,6).
+
+## Silhouette
+
+322 of the 18432 px inside the box are ground outside the black outline, so the 192x96 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c24:25px
+ r2 c1:24px, c24:24px
+ r3 c1:24px, c24:24px
+ r4 c1:24px, c24:24px
+ r5 c1:8px, c24:8px
+ r6 c1:8px, c24:8px
+ r7 c1:8px, c24:8px
+ r8 c1:8px, c24:8px
+ r9 c1:8px, c24:8px
+ r10 c1:8px, c24:8px
+ r11 c1:8px, c24:8px
+ r12 c1:8px, c24:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| ROUTE_22 | (2,0) | (8,5) | `ROUTE_22_GATE` |
diff --git a/assets/docs/buildings/B22-unnamed-building.md b/assets/docs/buildings/B22-unnamed-building.md
new file mode 100644
index 0000000..4374c5e
--- /dev/null
+++ b/assets/docs/buildings/B22-unnamed-building.md
@@ -0,0 +1,168 @@
+# B22 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **8 x 8 cells** (16 x 16 tiles of 8px, 128 x 128 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..8, y=1..8; tile column c=1..16, tile row r=1..16. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y5 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y6 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y7 | I J | J J | J L | L J | J L | L J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y8 | I L | L L | M N | J J | M N | J J | O P | L K |
+ | Q R | R R | * S | R R | * S | R R | T T | R U |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y6 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y7 | 15 10 | 10 10 | 10 75 | 75 10 | 10 75 | 75 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y8 | 15 75 | 75 75 | 11 12 | 10 10 | 11 12 | 10 10 | 68 69 | 75 31 |
+ | 78 26 | 26 26 | 27 28 | 26 26 | 27 28 | 26 26 | 74 74 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r11
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r12
+ { 15, 10, 10, 10, 10, 75, 75, 10, 10, 75, 75, 10, 10, 10, 10, 31 }, -- r13
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r14
+ { 15, 75, 75, 75, 11, 12, 10, 10, 11, 12, 10, 10, 68, 69, 75, 31 }, -- r15
+ { 78, 26, 26, 26, 27, 28, 26, 26, 27, 28, 26, 26, 74, 74, 26, 79 }, -- r16
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 2x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 14x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 28x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 14x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 11x
+- `J` = tile 10, used 70x
+- `K` = tile 31, used 11x
+- `L` = tile 75, used 78x
+- `M` = tile 11, used 2x
+- `N` = tile 12, used 2x
+- `O` = tile 68, used 1x
+- `P` = tile 69, used 1x
+- `Q` = tile 78, used 1x
+- `R` = tile 26, used 8x
+- `S` = tile 28, used 2x
+- `T` = tile 74, used 2x
+- `U` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y7 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y8 SOLID SOLID DOOR SOLID DOOR SOLID SOLID SOLID
+```
+
+Enterable cells: (3,8), (5,8).
+
+## Silhouette
+
+386 of the 16384 px inside the box are ground outside the black outline, so the 128x128 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c16:25px
+ r2 c1:24px, c16:24px
+ r3 c1:24px, c16:24px
+ r4 c1:24px, c16:24px
+ r5 c1:8px, c16:8px
+ r6 c1:8px, c16:8px
+ r7 c1:8px, c16:8px
+ r8 c1:8px, c16:8px
+ r9 c1:8px, c16:8px
+ r10 c1:8px, c16:8px
+ r11 c1:8px, c16:8px
+ r12 c1:8px, c16:8px
+ r13 c1:8px, c16:8px
+ r14 c1:8px, c16:8px
+ r15 c1:8px, c16:8px
+ r16 c1:8px, c16:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| CELADON_CITY | (6,6) | (8,13) | `CELADON_MART_1F` |
+| CELADON_CITY | (6,6) | (10,13) | `CELADON_MART_1F` |
diff --git a/assets/docs/buildings/B23-unnamed-building.md b/assets/docs/buildings/B23-unnamed-building.md
new file mode 100644
index 0000000..dd1a5a2
--- /dev/null
+++ b/assets/docs/buildings/B23-unnamed-building.md
@@ -0,0 +1,109 @@
+# B23 - Unnamed building
+
+
+
+`PLATEAU` tileset, **18 x 3 cells** (36 x 6 tiles of 8px, 288 x 48 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/plateau.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..18, y=1..3; tile column c=1..36, tile row r=1..6. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | C C | A B |
+ | D E | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | F F | D E |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | G H | I I | I I | I I | I I | I I | I I | I I | J K | C C | C C | C C | C C | J K | I I | I I | I I | G H |
+ | L M | I I | I I | I I | I I | I I | I I | I I | J K | C C | C C | C C | C C | J K | I I | I I | I I | L M |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | L M | I I | I I | I I | N O | I I | I I | I I | J K | C C | C C | C C | C C | J K | N O | I I | I I | L M |
+ | G H | P P | P P | P P | * Q | P P | P P | P P | J K | C C | C C | C C | C C | J K | * Q | P P | P P | G H |
+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 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 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 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 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 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 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 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 }, -- r1
+ { 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 }, -- r2
+ { 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 }, -- r3
+ { 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 }, -- r4
+ { 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 }, -- r5
+ { 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 }, -- r6
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 2x
+- `A` = tile 37, used 2x
+- `B` = tile 38, used 2x
+- `C` = tile 3, used 64x
+- `D` = tile 40, used 2x
+- `E` = tile 41, used 2x
+- `F` = tile 13, used 32x
+- `G` = tile 21, used 4x
+- `H` = tile 22, used 4x
+- `I` = tile 15, used 56x
+- `J` = tile 46, used 8x
+- `K` = tile 47, used 8x
+- `L` = tile 5, used 4x
+- `M` = tile 6, used 4x
+- `N` = tile 11, used 2x
+- `O` = tile 12, used 2x
+- `P` = tile 14, used 16x
+- `Q` = tile 28, used 2x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID DOOR SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID DOOR SOLID SOLID SOLID
+```
+
+Enterable cells: (5,3), (15,3).
+
+## Silhouette
+
+1297 of the 13824 px inside the box are ground outside the black outline, so the 288x48 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:14px, c2:14px, c3:4px, c4:4px, c5:4px, c6:4px, c7:4px, c8:4px, c9:4px, c10:4px, c11:4px, c12:4px, c13:4px, c14:4px, c15:4px, c16:4px, c17:4px, c18:4px, c19:4px, c20:4px, c21:4px, c22:4px, c23:4px, c24:4px, c25:4px, c26:4px, c27:4px, c28:4px, c29:4px, c30:4px, c31:4px, c32:4px, c33:4px, c34:4px, c35:14px, c36:14px
+ r2 c1:10px, c2:10px, c3:16px, c4:16px, c5:16px, c6:16px, c7:16px, c8:16px, c9:16px, c10:16px, c11:16px, c12:16px, c13:16px, c14:16px, c15:16px, c16:16px, c17:16px, c18:16px, c19:16px, c20:16px, c21:16px, c22:16px, c23:16px, c24:16px, c25:16px, c26:16px, c27:16px, c28:16px, c29:16px, c30:16px, c31:16px, c32:16px, c33:16px, c34:16px, c35:10px, c36:10px
+ r3 c1:10px, c2:10px, c17:8px, c18:8px, c19:4px, c20:4px, c21:4px, c22:4px, c23:4px, c24:4px, c25:4px, c26:4px, c27:8px, c28:8px, c35:10px, c36:10px
+ r4 c1:8px, c2:8px, c17:8px, c18:8px, c19:1px, c27:8px, c28:8px, c35:8px, c36:8px
+ r5 c1:8px, c2:8px, c17:8px, c18:8px, c19:1px, c27:8px, c28:8px, c29:34px, c30:34px, c35:8px, c36:8px
+ r6 c1:15px, c2:15px, c3:9px, c4:8px, c5:8px, c6:8px, c7:8px, c8:8px, c11:8px, c12:8px, c13:8px, c14:8px, c15:8px, c16:8px, c17:8px, c18:8px, c19:1px, c27:8px, c28:8px, c29:18px, c30:18px, c31:9px, c32:8px, c33:8px, c34:8px, c35:15px, c36:15px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| ROUTE_23 | (0,29) | (4,31) | `VICTORY_ROAD_1F` |
+| ROUTE_23 | (0,29) | (14,31) | `VICTORY_ROAD_2F` |
diff --git a/assets/docs/buildings/B24-unnamed-building.md b/assets/docs/buildings/B24-unnamed-building.md
new file mode 100644
index 0000000..5108ee6
--- /dev/null
+++ b/assets/docs/buildings/B24-unnamed-building.md
@@ -0,0 +1,145 @@
+# B24 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **8 x 6 cells** (16 x 12 tiles of 8px, 128 x 96 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..8, y=1..6; tile column c=1..16, tile row r=1..12. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | C C | C C | C C | C C | C C | C C | D E |
+ | F G | H H | H H | H H | H H | H H | H H | G I |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | F G | H H | H H | H H | H H | H H | H H | G I |
+ | F J | K K | K K | K K | K K | K K | K K | L I |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | F G | H H | H H | H H | H H | H H | H H | G I |
+ | F G | H H | H H | H H | H H | H H | H H | G I |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | F J | K K | K K | K K | K K | K K | K K | L I |
+ | M N | O O | O O | O O | O O | O O | O O | P Q |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y5 | R S | S S | S S | S S | S T | T S | S S | S U |
+ | R T | T T | T T | T T | T T | T T | T T | T U |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y6 | R T | T T | T T | T T | V W | S S | T T | T U |
+ | X Y | Y Y | Y Y | Y Y | * Z | Y Y | Y Y | Y a |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 5 6 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 8 9 |
+ | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ | 21 22 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 24 25 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ | 21 56 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 56 25 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 21 22 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 24 25 |
+ | 37 38 | 34 34 | 34 34 | 34 34 | 34 34 | 34 34 | 34 34 | 40 41 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 10 | 10 75 | 75 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y6 | 15 75 | 75 75 | 75 75 | 75 75 | 11 12 | 10 10 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 27 28 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9 }, -- r1
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r2
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r3
+ { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, -- r4
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r5
+ { 21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25 }, -- r6
+ { 21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25 }, -- r7
+ { 37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 75, 75, 75, 75, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 31 }, -- r11
+ { 78, 26, 26, 26, 26, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79 }, -- r12
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 5, used 1x
+- `B` = tile 6, used 1x
+- `C` = tile 83, used 12x
+- `D` = tile 8, used 1x
+- `E` = tile 9, used 1x
+- `F` = tile 21, used 6x
+- `G` = tile 56, used 8x
+- `H` = tile 18, used 48x
+- `I` = tile 25, used 6x
+- `J` = tile 22, used 2x
+- `K` = tile 23, used 24x
+- `L` = tile 24, used 2x
+- `M` = tile 37, used 1x
+- `N` = tile 38, used 1x
+- `O` = tile 34, used 12x
+- `P` = tile 40, used 1x
+- `Q` = tile 41, used 1x
+- `R` = tile 15, used 3x
+- `S` = tile 10, used 14x
+- `T` = tile 75, used 26x
+- `U` = tile 31, used 3x
+- `V` = tile 11, used 1x
+- `W` = tile 12, used 1x
+- `X` = tile 78, used 1x
+- `Y` = tile 26, used 12x
+- `Z` = tile 28, used 1x
+- `a` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID DOOR SOLID SOLID SOLID
+```
+
+Enterable cell: (5,6).
+
+## Silhouette
+
+202 of the 12288 px inside the box are ground outside the black outline, so the 128x96 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:48px, c2:16px, c15:16px, c16:48px
+ r8 c1:5px, c16:5px
+ r9 c1:8px, c16:8px
+ r10 c1:8px, c16:8px
+ r11 c1:8px, c16:8px
+ r12 c1:8px, c16:8px
+```
+
+## Where it stands
+
+| map | cell (x,y) | door | leads to |
+| --- | --- | --- | --- |
+| PEWTER_CITY | (10,2) | (14,7) | `MUSEUM_1F` |
diff --git a/assets/docs/buildings/B25-unnamed-building.md b/assets/docs/buildings/B25-unnamed-building.md
new file mode 100644
index 0000000..b194fe3
--- /dev/null
+++ b/assets/docs/buildings/B25-unnamed-building.md
@@ -0,0 +1,142 @@
+# B25 - Unnamed building
+
+
+
+`OVERWORLD` tileset, **8 x 6 cells** (16 x 12 tiles of 8px, 128 x 96 px). Appears **1 time** in the game. Tile ids index `assets/generated/tilesets/overworld.png`, 16 per row.
+
+Coordinates are 1-based and local to the building: cell x=1..8, y=1..6; tile column c=1..16, tile row r=1..12. No terrain padding is included - edge rows and columns that were pure ground have been trimmed.
+
+## Symbol grid
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y1 | A B | B B | B B | B B | B B | B B | B B | B C |
+ | D E | E E | E E | E E | E E | E E | E E | E D |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y2 | D E | E E | E E | E E | E E | E E | E E | E D |
+ | F G | G G | G G | G G | G G | G G | G G | G H |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y3 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y4 | I J | J J | J J | J J | J J | J J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y5 | I J | J J | J J | J J | J L | L J | J J | J K |
+ | I L | L L | L L | L L | L L | L L | L L | L K |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+ y6 | I L | L L | L L | L L | M N | J J | L L | L K |
+ | O P | P P | P P | P P | * Q | P P | P P | P R |
+ +-----+-----+-----+-----+-----+-----+-----+-----+
+```
+
+## Same grid, raw tile ids
+
+```
+ x1 x2 x3 x4 x5 x6 x7 x8
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y1 | 76 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 83 | 83 77 |
+ | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y2 | 90 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 18 | 18 90 |
+ | 92 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 23 | 23 93 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y3 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y4 | 15 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y5 | 15 10 | 10 10 | 10 10 | 10 10 | 10 75 | 75 10 | 10 10 | 10 31 |
+ | 15 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 75 | 75 31 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+ y6 | 15 75 | 75 75 | 75 75 | 75 75 | 11 12 | 10 10 | 75 75 | 75 31 |
+ | 78 26 | 26 26 | 26 26 | 26 26 | 27 28 | 26 26 | 26 26 | 26 79 |
+ +---------+---------+---------+---------+---------+---------+---------+---------+
+```
+
+As a 1-based Lua array, `rows[r][c]`:
+
+```lua
+local rows = {
+ { 76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77 }, -- r1
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r2
+ { 90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90 }, -- r3
+ { 92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93 }, -- r4
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r5
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r6
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31 }, -- r7
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r8
+ { 15, 10, 10, 10, 10, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 31 }, -- r9
+ { 15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31 }, -- r10
+ { 15, 75, 75, 75, 75, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 31 }, -- r11
+ { 78, 26, 26, 26, 26, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79 }, -- r12
+}
+```
+
+## Legend
+
+_Legend pending._
+
+- `*` = tile 27, used 1x
+- `A` = tile 76, used 1x
+- `B` = tile 83, used 14x
+- `C` = tile 77, used 1x
+- `D` = tile 90, used 4x
+- `E` = tile 18, used 28x
+- `F` = tile 92, used 1x
+- `G` = tile 23, used 14x
+- `H` = tile 93, used 1x
+- `I` = tile 15, used 7x
+- `J` = tile 10, used 42x
+- `K` = tile 31, used 7x
+- `L` = tile 75, used 54x
+- `M` = tile 11, used 1x
+- `N` = tile 12, used 1x
+- `O` = tile 78, used 1x
+- `P` = tile 26, used 12x
+- `Q` = tile 28, used 1x
+- `R` = tile 79, used 1x
+
+
+
+## Collision
+
+Cell walkability reads the **bottom-left 8px tile** of each cell (`src/world/Map.lua:6`):
+
+```
+ y1 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y2 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y3 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y4 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y5 SOLID SOLID SOLID SOLID SOLID SOLID SOLID SOLID
+ y6 SOLID SOLID SOLID SOLID DOOR SOLID SOLID SOLID
+```
+
+Enterable cell: (5,6).
+
+## Silhouette
+
+322 of the 12288 px inside the box are ground outside the black outline, so the 128x96 box is **not** a solid rectangle - a pixel-perfect cutout has to follow the outline. Terrain pixels by tile row:
+
+```
+ r1 c1:25px, c16:25px
+ r2 c1:24px, c16:24px
+ r3 c1:24px, c16:24px
+ r4 c1:24px, c16:24px
+ r5 c1:8px, c16:8px
+ r6