"""Reference implementation of the building voxelizer -- the Python half of the dual-implementation parity check demanded by assets/docs/buidling_to_voxel/sprite_to_voxel_methodology.md (Stage 5). It composites a building out of the tileset atlas exactly the way the map does, extracts palette + silhouette (light-only flood fill), builds the voxel model with the same rules lib/Buildings.lua ships, asserts the geometric intent, and renders isometric previews. python mods/DRAMATIC_SHAPE/tools/building_voxels.py [outdir] Keep the TEMPLATES table below in sync with the `buildings` section of data/voxel_heights.lua: the two implementations are meant to be independent restatements of the same algorithm, and the voxel/shell counts they print must match. """ import os import sys from collections import deque, Counter from PIL import Image, ImageDraw ROOT = os.path.dirname(os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__))))) TILESETS = os.path.join(ROOT, "assets/generated/tilesets") PER_ROW = 16 WHITE, GREY, DARK, BLACK = 0, 1, 2, 3 # by luminance, light first # --- the shape profile, mirroring data/voxel_heights.lua's `buildings` ------ TEMPLATES = { # B07: Red's house / Blue's house / Bill's / the Copycat's -- 7 placements "gabled_house": dict( tiles=[ [5, 6, 7, 7, 7, 7, 8, 9], [21, 22, 23, 23, 23, 23, 24, 25], [37, 38, 10, 34, 10, 10, 40, 41], [92, 23, 23, 23, 23, 23, 23, 93], [15, 34, 11, 12, 10, 10, 34, 31], [78, 26, 27, 28, 26, 26, 26, 79], ], roof_rows=16, roof_back=7, roof_front=9, roof_cycle=(5, 8), slab=4, front_eave=4, ledge=(24, 31), ), # B31: Oak's lab -- the same architecture with a roof band twice as deep "lab": dict( tiles=[ [5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25], [37, 38, 10, 10, 10, 75, 75, 10, 10, 10, 40, 41], [15, 34, 34, 34, 75, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 11, 12, 10, 10, 10, 10, 10, 31], [78, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B03: the flat-roofed commercial block -- 15 placements, the most of # any voxelized drawing. The lattice is drawn from straight above, so # the measured taper is flat; the eave course is the roof's own south # rim, lab-style. Its sprite is inset from its box: the outer columns # carry no roof. "flat_commercial": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 75, 11, 12, 75, 75, 75, 31], [78, 26, 27, 28, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B05: every Pokemon Center in the game (Celadon, Cerulean, # Cinnabar, Fuchsia, Lavender, Pewter, Saffron, Vermilion, Viridian, # Mt Moon, Rock Tunnel). B03's block with the POKe sign hung beside # the door; the sign is too wide to be a pane, so it stays flush. "pokecenter": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 75, 11, 12, 66, 67, 75, 31], [78, 26, 27, 28, 74, 74, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B06: every Poke Mart (Cerulean, Cinnabar, Fuchsia, Lavender, # Pewter, Saffron, Vermilion, Viridian). The Center's twin, MART on # the sign. "pokemart": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 75, 11, 12, 68, 69, 75, 31], [78, 26, 27, 28, 74, 74, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B02: the plain 4x4 block: one window course over blank brick and # no door. 15 placements, scenery in every city bar the Celadon Mart # roof stair. "flat_block_4x4": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 75, 75, 75, 75, 75, 75, 31], [78, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B08: the same block two cells deeper, 6 placements, all scenery. "flat_block_4x6": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 31], [15, 75, 75, 75, 75, 75, 75, 31], [78, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B13: the 6x4 scenery block, 4 placements. "flat_block_6x4": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B14: the 6x6 scenery block, 3 placements. "flat_block_6x6": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B27: the 8x4 scenery block, one placement on Route 11. "flat_block_8x4": dict( tiles=[ [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], [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], [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], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B09: the wide storefront: Celadon's Game Corner, the Pokemon # Mansion, Cinnabar Lab, the Safari Zone gate and Fuchsia's meeting # room. "game_corner": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 31], [78, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B15: Celadon Mansion and the Route 6 and Route 12 gates. "celadon_mansion": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 75, 75, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 75, 75, 75, 11, 12, 10, 10, 75, 75, 75, 31], [78, 26, 26, 26, 27, 28, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B18: the Route 2 gate, and the museum's east entrance beside it in # Pewter. The museum hall itself is B24 below -- a sloped roof. "route_2_gate": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 75, 11, 12, 75, 75, 75, 75, 75, 75, 75, 31], [78, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B29: Fuchsia Gym, the block with GYM on the sign. "fuchsia_gym": dict( tiles=[ [76, 83, 83, 83, 83, 83, 83, 83, 83, 83, 83, 77], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [90, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 90], [92, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 93], [15, 10, 10, 10, 34, 47, 63, 34, 10, 10, 10, 31], [15, 75, 75, 75, 34, 34, 34, 34, 75, 75, 75, 31], [15, 75, 11, 12, 10, 10, 10, 10, 75, 75, 75, 31], [78, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B28: the Route 5 underground-path gate. "route_5_gate": dict( tiles=[ [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], [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], [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], [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], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B21: the Route 22 league gate, the widest of the family at 12 # cells. "route_22_gate": dict( tiles=[ [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], [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], [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], [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], [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], [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], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B25: the Power Plant. "power_plant": dict( tiles=[ [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], [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], [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], [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], [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], [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], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B22: Celadon's department store: six window courses over the MART # sign. "celadon_mart": dict( tiles=[ [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], [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], [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], [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], [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], [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], [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], [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], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B20: Silph Co. Twelve cells of plot and ten courses of windows # under the same roof band, so it stands as the tallest thing in # Kanto. "silph_co": dict( tiles=[ [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], [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], [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], [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], [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], [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], [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], [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], [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], [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], [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], [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], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B24: the Pewter museum's hall, and the only building in the family # with a SLOPED roof: the same 2:1 taper the lab and Red's house are # drawn with, over a roof band twice the lab's depth. The drawing # repeats its whole lattice-and-course motif -- rows 8..31 again at # 32..55 -- which is what fixes the cycle at 24 rather than the bare # lattice's 8: the drawing proves the period. The last band (rows # 56..63) is the roof's fascia, wider than the wall it covers, so it # stays in the roof band and lands on the south rim. "museum": dict( tiles=[ [ 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], [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], [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], [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], [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], [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], ], roof_rows=64, roof_back=8, roof_front=8, roof_cycle=(8, 31), slab=4, front_eave=4, ledge=None, ), # B10: the gym. Cinnabar, Pewter, Vermilion and Viridian wear this # drawing, and so does the Fighting Dojo next door to Saffron's. # Oak's lab's roof band exactly, tile for tile, over a facade with # GYM on the sign. "gym": dict( tiles=[ [ 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25], [37, 38, 10, 10, 34, 47, 63, 34, 10, 10, 40, 41], [15, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 31], [15, 10, 10, 10, 10, 10, 10, 10, 11, 12, 10, 31], [78, 26, 26, 26, 26, 26, 26, 26, 27, 28, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B16: the big-city gym: Celadon, Cerulean and Saffron. Two cells # wider than the standard gym, and it carries the GYM sign twice. "gym_large": dict( tiles=[ [ 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], [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], [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], [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], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B01: the commonest drawing in the game at 19 placements, and every # one of them scenery: a gabled block with two window courses and no # door. Red's house's roof band, tile for tile, but no awning under # it. "gabled_block_4x3": dict( tiles=[ [ 5, 6, 7, 7, 7, 7, 8, 9], [21, 22, 23, 23, 23, 23, 24, 25], [37, 38, 10, 10, 10, 10, 40, 41], [15, 34, 34, 34, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 31], [78, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=16, roof_back=7, roof_front=9, roof_cycle=(5, 8), slab=4, front_eave=4, ledge=None, ), # B04: the little 4x2 cottage, 12 placements and nearly all of them # somebody's home: Mr Fuji's, the Cubone house, Bill's grandpa's, # the Name Rater's, the Viridian school house, the Route 8 # underground path. "gabled_cottage": dict( tiles=[ [ 5, 6, 7, 7, 7, 7, 8, 9], [21, 22, 23, 23, 23, 23, 24, 25], [37, 38, 11, 12, 10, 10, 40, 41], [78, 26, 27, 28, 26, 26, 26, 79], ], roof_rows=16, roof_back=7, roof_front=9, roof_cycle=(5, 8), slab=4, front_eave=4, ledge=None, ), # B17: the wide 6x2 house: Cerulean's badge, trade and trashed # houses. "gabled_house_wide": dict( tiles=[ [ 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 9], [21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25], [37, 38, 11, 12, 35, 10, 10, 35, 10, 10, 40, 41], [78, 26, 27, 28, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=16, roof_back=7, roof_front=9, roof_cycle=(5, 8), slab=4, front_eave=4, ledge=None, ), # B11: the 6x2 scenery block, 5 placements, no door. "gabled_block_6x2": dict( tiles=[ [ 5, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 9], [21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25], [37, 38, 10, 34, 35, 10, 10, 35, 10, 10, 40, 41], [78, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=16, roof_back=7, roof_front=9, roof_cycle=(5, 8), slab=4, front_eave=4, ledge=None, ), # B34: the 4x2 scenery block, one placement in Fuchsia. "gabled_block_4x2": dict( tiles=[ [ 5, 6, 7, 7, 7, 7, 8, 9], [21, 22, 23, 23, 23, 23, 24, 25], [37, 38, 10, 34, 10, 10, 40, 41], [78, 26, 26, 26, 26, 26, 26, 79], ], roof_rows=16, roof_back=7, roof_front=9, roof_cycle=(5, 8), slab=4, front_eave=4, ledge=None, ), # B33: the Route 5 day care. "daycare": dict( tiles=[ [ 5, 6, 83, 83, 83, 83, 8, 9], [21, 56, 18, 18, 18, 18, 56, 25], [21, 56, 18, 18, 18, 18, 56, 25], [21, 22, 23, 23, 23, 23, 24, 25], [37, 38, 10, 10, 10, 10, 40, 41], [15, 34, 34, 34, 34, 34, 34, 31], [15, 10, 10, 10, 11, 12, 10, 31], [78, 26, 26, 26, 27, 28, 26, 79], ], roof_rows=32, roof_back=7, roof_front=8, roof_cycle=(5, 12), slab=4, front_eave=4, ledge=None, ), # B26: the Route 10 scenery block, structurally the museum's twin. # Needs `seal`: its drawing has no black base course, so unsealed # the flood climbs in from the south border and hollows the wall out # (72% surviving in 65 pieces, against 95% in one). "gabled_block_6x6": dict( tiles=[ [ 5, 6, 83, 83, 83, 83, 83, 83, 83, 83, 8, 9], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 56, 18, 18, 18, 18, 18, 18, 18, 18, 56, 25], [21, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 25], [37, 38, 34, 34, 34, 34, 34, 34, 34, 34, 40, 41], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], [15, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 31], [15, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 31], ], roof_rows=64, roof_back=8, roof_front=8, roof_cycle=(8, 31), slab=4, front_eave=4, ledge=None, seal="s", ), # B12: the Safari Zone rest houses. A corrugated roof over a plank # facade; the stripe repeats every 5 rows, not the OVERWORLD # lattice's 8. "safari_rest_house": dict( tiles=[ [ 8, 9, 9, 9, 9, 9, 9, 12], [24, 25, 25, 25, 25, 25, 25, 28], [40, 41, 42, 43, 1, 1, 41, 44], [56, 41, 58, 59, 41, 41, 41, 60], ], roof_rows=17, roof_back=5, roof_front=3, roof_cycle=(5, 9), slab=4, front_eave=4, ledge=None, tileset="forest", ), # B23: the Victory Road entrance on Route 23: a rock face with two # barred doors. The roof band is the pale cliff top seen from above. "victory_road_gate": dict( tiles=[ [37, 38, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 37, 38], [40, 41, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 40, 41], [21, 22, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 15, 15, 15, 15, 21, 22], [ 5, 6, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 15, 15, 15, 15, 15, 15, 5, 6], [ 5, 6, 15, 15, 15, 15, 15, 15, 11, 12, 15, 15, 15, 15, 15, 15, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 11, 12, 15, 15, 15, 15, 5, 6], [21, 22, 14, 14, 14, 14, 14, 14, 27, 28, 14, 14, 14, 14, 14, 14, 46, 47, 3, 3, 3, 3, 3, 3, 3, 3, 46, 47, 27, 28, 14, 14, 14, 14, 21, 22], ], roof_rows=16, roof_back=7, roof_front=8, roof_cycle=(9, 12), slab=4, front_eave=4, ledge=None, tileset="plateau", ), } # a recess is a window or a doorway: a non-black region the art seals off # behind its own black frame. Anything wider or taller than this is a band # of the facade itself (siding courses, the awning's grey field). RECESS_MAX = 24 # --------------------------------------------------------------- stage 1 -- def sprite(tiles, seal="", tileset="overworld"): """Composite the building and read it the way Structures reads the map: palette index per pixel plus the light-only silhouette flood. `seal` names the sides the drawing runs off (a string of n/s/e/w). The flood does not seed there: a drawing trimmed flush to its art, whose base course is brick rather than the black threshold every other building stands on, would otherwise be hollowed out through its own mortar.""" atlas = Image.open(os.path.join(TILESETS, tileset + ".png")).convert("RGB") h, w = len(tiles), len(tiles[0]) W, H = w * 8, h * 8 im = Image.new("RGB", (W, H)) for r, row in enumerate(tiles): for c, t in enumerate(row): ax, ay = (t % PER_ROW) * 8, (t // PER_ROW) * 8 im.paste(atlas.crop((ax, ay, ax + 8, ay + 8)), (c * 8, r * 8)) px = im.load() counts = Counter(px[x, y] for y in range(H) for x in range(W)) lum = lambda c: 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2] pal = sorted(counts, key=lambda c: -lum(c)) # white, grey, dark, black assert len(pal) == 4, pal idx = {c: i for i, c in enumerate(pal)} col = [[idx[px[x, y]] for x in range(W)] for y in range(H)] # The flood spreads only through LIGHT pixels: the black outline and the # #555 shading together are the boundary. A "not black" threshold lets it # eat the shaded flanks and the silhouette collapses. out = [[False] * W for _ in range(H)] q = deque() def seed(x, y): if not out[y][x] and col[y][x] <= GREY: out[y][x] = True q.append((x, y)) for x in range(W): if "n" not in seal: seed(x, 0) if "s" not in seal: seed(x, H - 1) for y in range(H): if "w" not in seal: seed(0, y) if "e" not in seal: seed(W - 1, 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 < W and 0 <= ny < H: seed(nx, ny) # source texel per pixel, so every voxel can name where its colour came from src = [[((tiles[y // 8][x // 8] % PER_ROW) * 8 + x % 8, (tiles[y // 8][x // 8] // PER_ROW) * 8 + y % 8) for x in range(W)] for y in range(H)] return dict(W=W, H=H, col=col, out=out, src=src, pal=pal) # --------------------------------------------------------------- stage 2 -- def profile(sp, t): """Everything the band table implies, measured off the mask.""" W, H = sp["W"], sp["H"] inside = lambda x, y: 0 <= x < W and 0 <= y < H and not sp["out"][y][x] # the taper IS the slope: the topmost drawn row of each column top = [] for x in range(W): r = next((y for y in range(H) if inside(x, y)), t["roof_rows"]) top.append(min(r, t["roof_rows"])) wall_h = H - t["roof_rows"] ytop = wall_h - 1 + t["slab"] # Recesses: the panes the art seals behind a black frame. Non-black # pixels of the facade split into components across the black outline; # a component small enough to be a window or a doorway sinks one voxel. wall_y0 = t["roof_rows"] comp = {} recess = set() for sy in range(wall_y0, H): for sx in range(W): if (sx, sy) in comp or not inside(sx, sy) or sp["col"][sy][sx] == BLACK: continue cells, stack = [], [(sx, sy)] comp[(sx, sy)] = True x0 = x1 = sx y0 = y1 = sy while stack: cx, cy = stack.pop() cells.append((cx, cy)) x0, x1 = min(x0, cx), max(x1, cx) y0, y1 = min(y0, cy), max(y1, cy) for nx, ny in ((cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)): if (ny >= wall_y0 and (nx, ny) not in comp and inside(nx, ny) and sp["col"][ny][nx] != BLACK): comp[(nx, ny)] = True stack.append((nx, ny)) if x1 - x0 + 1 <= RECESS_MAX and y1 - y0 + 1 <= RECESS_MAX: recess.update(cells) return dict(top=top, wall_h=wall_h, ytop=ytop, recess=recess, inside=inside, D=H, W=W, H=H) # --------------------------------------------------------------- stage 3 -- def build(sp, pr, t): """The voxel model. Order is load-bearing: walls, ledge, recesses, then the roof solid overwrites what it intersects and the walls are trimmed to the roof's underside.""" W, H, D = pr["W"], pr["H"], pr["D"] inside, top, ytop = pr["inside"], pr["top"], pr["ytop"] col, src = sp["col"], sp["src"] slab = t["slab"] def T(x): return ytop - top[max(0, min(W - 1, x))] def interior(sx, sy): """Side faces must not be slabs of outline black: walk inward for the first painted colour, the way the drawing's own shading does.""" if col[sy][sx] != BLACK: return sx step = 1 if sx < W // 2 else -1 for d in range(1, 4): nx = sx + step * d if inside(nx, sy) and col[sy][nx] != BLACK: return nx return sx # the roof's drawn span: a sprite inset from its box leaves outer # columns undrawn in the roof band, and they carry no roof at all roofed = [x for x in range(W) if top[x] < t["roof_rows"]] x0d, x1d = (roofed[0], roofed[-1]) if roofed else (0, W - 1) def trimmed(x, y): """Under the roof's underside -- a column with no roof over it has no underside, and must not be cut by a profile never drawn.""" return top[max(0, min(W - 1, x))] < t["roof_rows"] and y > T(x) - slab vox = {} def put(x, y, z, sx, sy): vox[(x, y, z)] = (col[sy][sx], src[sy][sx]) # ---- walls: the facade rows extruded straight back, trimmed under the roof for sy in range(t["roof_rows"], H): y = H - 1 - sy for sx in range(W): if not inside(sx, sy) or trimmed(sx, y): continue ix = interior(sx, sy) for z in range(D): if z == 0 or z == D - 1: put(sx, y, z, sx, sy) else: put(sx, y, z, ix, sy) # a base course: the drawing's last row is the ground the house stands # on, so without this the wall floats one voxel over its own plot for x in range(W): for z in range(D): if (x, 0, z) not in vox and (x, 1, z) in vox: vox[(x, 0, z)] = vox[(x, 1, z)] # ---- ledge: the awning slab juts two voxels past the walls if t["ledge"]: l0, l1 = t["ledge"] for sy in range(l0, l1 + 1): y = H - 1 - sy for sx in range(W): if inside(sx, sy) and not trimmed(sx, y): for z in (-2, -1, D, D + 1): put(sx, y, z, sx, sy) # ---- recesses: the front voxel of every pane sinks, its frame stays proud for sx, sy in pr["recess"]: vox.pop((sx, H - 1 - sy, D - 1), None) # ---- roof: flat top over the plateau, stepped diagonal ends z0, z1 = 0, D - 1 + t["front_eave"] back, front = t["roof_back"], t["roof_front"] c0, c1 = t["roof_cycle"] def roof_sy(z): df, db = z - z0, z1 - z # from the north / the south edge if df < back: return df # north rim: the drawing's top rows if db < front: return t["roof_rows"] - 1 - db # south rim: fascia and eave course return c0 + (df - c0) % (c1 - c0 + 1) shade_px = {} for sy in range(H): for sx in range(W): if inside(sx, sy): shade_px.setdefault(col[sy][sx], (sx, sy)) for x in roofed: tt = T(x) for z in range(z0, z1 + 1): outer = x == x0d or x == x1d or z == z0 or z == z1 # the slope's texture is the drawing's own: clamping into the # column's first drawn row keeps flank battens running down the # slope instead of falling off the silhouette sy = max(roof_sy(z), pr["top"][x]) for y in range(tt - slab + 1, tt + 1): if y == tt and not outer: put(x, y, z, x, sy) else: # the rim: the drawing's own eave -- a black outline # over a shaded fascia, closed by the outline again if not outer: shade = DARK elif y == tt or y == tt - slab + 1: shade = BLACK else: shade = DARK px = shade_px.get(shade) or shade_px[BLACK] put(x, y, z, px[0], px[1]) return vox # --------------------------------------------------------------- stage 5 -- def verify(vox, sp, pr, t): W, H, D = pr["W"], pr["H"], pr["D"] ytop, top, slab = pr["ytop"], pr["top"], t["slab"] T = lambda x: ytop - top[max(0, min(W - 1, x))] for (x, y, z), _ in vox.items(): assert y <= T(x), f"voxel pokes through the roof at {x},{y},{z}" # The roof surface reads over the columns the drawing actually paints: # a sprite inset from its box says nothing about the rest. roofed = [x for x in range(W) if top[x] < t["roof_rows"]] assert roofed, "no roof band is drawn" prof = [T(x) for x in roofed] assert prof == prof[::-1], "the roof is not symmetric" plateau = [i for i, v in enumerate(prof) if v == ytop] assert plateau, "no flat top" if max(top[x] for x in roofed) > 1: # a slope: eave tip, one step per N columns, flat plateau for i in range(1, plateau[0]): assert 0 <= prof[i] - prof[i - 1] <= 1, "the taper is not monotonic" # the taper rate the drawing sets is the slope: one step per N # columns, the same N the whole way down to the eave tip steps = [i for i in range(1, plateau[0] + 1) if prof[i] != prof[i - 1]] # a rate needs two steps to be a rate. One step is a lip, not a # slope, and there is nothing to hold it to. if len(steps) >= 2: rate = steps[1] - steps[0] assert all(b - a == rate for a, b in zip(steps, steps[1:])), \ f"the slope is not a constant {rate}:1" assert prof[0] == ytop - (plateau[0] + rate - 1) // rate, \ "the eave tip does not land where the drawn taper ends" else: # flat: one level roof the whole drawn span, give or take the # drawing's own corner rounding assert all(ytop - v <= 1 for v in prof), "the flat roof is not level" # every wall column carries roof over it -- and a column the roof never # reaches carries nothing at all, rather than being silently trimmed away over = set(roofed) for x in range(W): for z in range(D): if x not in over: assert not any((x, y, z) in vox for y in range(ytop + 1)), \ f"wall stands where no roof reaches at {x},{z}" elif any((x, y, z) in vox for y in range(0, T(x) - slab + 1)): assert (x, T(x), z) in vox, f"wall uncovered at {x},{z}" shell = [k for k in vox if not all( (k[0] + d[0], k[1] + d[1], k[2] + d[2]) in vox for d in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)))] return shell def preview(shell, vox, sp, name, out, flip, canvas=(1700, 900), pad=20): # Mirroring depth swings the camera round to the other side. Mirror # about the model's own depth range, not a fixed one, or a building # deeper or shallower than the first two walks off the canvas. zs = [z for _, _, z in shell] zlo, zhi = min(zs), max(zs) pts = [(x, y, (zlo + zhi - z) if flip else z, vox[(x, y, z)][0]) for (x, y, z) in shell] pts.sort(key=lambda p: (p[0] + p[2], p[1])) # Fit the projection to the model: Silph Co is four times the height of # Red's house and would otherwise render off the edge. px = lambda x, z: 2 * (x - z) py = lambda x, y, z: (x + z) - 2 * y xs = [px(x, z) for x, _, z, _ in pts] + [px(x + 1, z + 1) for x, _, z, _ in pts] ys = [py(x, y, z) for x, y, z, _ in pts] + [py(x + 1, y + 1, z + 1) for x, y, z, _ in pts] W, H = canvas S = max(1, min((W - 2 * pad) // max(1, max(xs) - min(xs)), (H - 2 * pad) // max(1, max(ys) - min(ys)))) ox = pad - min(xs) * S + (W - 2 * pad - (max(xs) - min(xs)) * S) // 2 oy = pad - min(ys) * S + (H - 2 * pad - (max(ys) - min(ys)) * S) // 2 P = lambda x, y, z: (px(x, z) * S + ox, py(x, y, z) * S + oy) img = Image.new("RGB", canvas, (0xca, 0xdc, 0x9f)) dr = ImageDraw.Draw(img) has = {(x, y, z) for x, y, z, _ in pts} sh = lambda c, f: tuple(int(v * f) for v in sp["pal"][c]) for x, y, z, c in pts: if (x, y + 1, z) not in has: 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=sh(c, 1.0)) if (x + 1, y, z) not in has: 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=sh(c, 0.62)) if (x, y, z + 1) not in has: 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=sh(c, 0.82)) img.save(os.path.join(out, name)) def main(): out = sys.argv[1] if len(sys.argv) > 1 else "." os.makedirs(out, exist_ok=True) for name, t in TEMPLATES.items(): sp = sprite(t["tiles"], t.get("seal", ""), t.get("tileset", "overworld")) pr = profile(sp, t) vox = build(sp, pr, t) shell = verify(vox, sp, pr, t) print(f"{name}: {sp['W']}x{sp['H']} sprite, depth {pr['D']}, " f"height {pr['ytop'] + 1} -> voxels {len(vox)} " f"shell {len(shell)} recessed {len(pr['recess'])}") preview(shell, vox, sp, f"{name}_front.png", out, True) preview(shell, vox, sp, f"{name}_back.png", out, False) if __name__ == "__main__": main()