#!/usr/bin/env python3 """assets.py — the not-tonight generation manifest (docs/VENUES.md §6). One entry per sprite the three new venues need. `run_batch.py` walks this list; nothing here executes anything itself, so it stays readable and diffable. Two routes, per the A/B recorded in docs/SPACES.md: flat — flux draws the finished sprite. For things with no volume: signage, screens, boards, wall art, backdrops. Flux picking its own camera does not matter when the object IS a flat plane. mesh — flux draws a clean PRODUCT SHOT, TRELLIS.2 reconstructs it, Blender renders it from the ONE fixed ortho camera every prop shares. For anything with volume. The product shot is deliberately NOT a top-down: TRELLIS reconstructs best from a clear view of the object, and the game's camera angle is applied at render time by `rake`. `rake` follows the house rule the hard way round (LANEHANDOVER, FABLE-SOLO-25): **60° for low and wide, ~76° for tall.** Lowering the rake for a tall object to "see more face" is wrong — a tall object is already all face, and at 55° it renders as a featureless slab. """ from __future__ import annotations from typing import NamedTuple # Shared prompt tails. The mesh tail is tuned for TRELLIS: one object, no # cropping, plain ground, even light, nothing for the reconstructor to mistake # for geometry. MESH_TAIL = ( "single object centred, full object visible, product photograph, " "plain mid-grey seamless background, soft even studio lighting, sharp focus, " "no people, no text, no lettering, no watermark" ) FLAT_TAIL = ( "front-on orthographic view, flat to camera, no perspective, " "dark background, grimy nightclub, video game texture art, " # flux likes to sign its work in the bottom corner. Invisible on a 16px # prop, very visible on a 640px street plate. "no watermark, no logo, no signature" ) class Asset(NamedTuple): kind: str route: str # 'flat' | 'mesh' size: tuple[int, int] # ship size in px, must match PROP_SIZES/TARGETS prompt: str seed: int rake: int = 60 # mesh route only gen: tuple[int, int] = (1024, 1024) # generation resolution quantise: bool = True # True for PLATES (backdrops, skylines, a roller door filling its wall): # they are meant to be opaque, and the importer's near-black-to-alpha pass # would eat exactly the parts that make a night scene look like night. # False for OBJECTS, which want cutting out of their background. keep_bg: bool = False def M(kind: str, size: tuple[int, int], seed: int, rake: int, subject: str) -> Asset: return Asset(kind, "mesh", size, f"{subject}, {MESH_TAIL}", seed, rake=rake) def F(kind: str, size: tuple[int, int], seed: int, subject: str, gen: tuple[int, int] = (1024, 1024), quantise: bool = True, keep_bg: bool = False) -> Asset: return Asset(kind, "flat", size, f"{subject}, {FLAT_TAIL}", seed, gen=gen, quantise=quantise, keep_bg=keep_bg) # ---- The Royal — suburban pub ------------------------------------------------- ROYAL = [ M("pokie", (16, 32), 4400, 76, "a single poker machine gaming cabinet, glowing screen, red and gold trim, worn"), M("bistroTable", (16, 16), 4401, 60, "a small square pub bistro table with two dining chairs, laminate top"), M("picnicTable", (32, 16), 4402, 60, "a weathered wooden picnic table with attached bench seats, beer garden furniture"), M("trough", (48, 16), 4403, 70, "a long stainless steel trough urinal, wall mounted, public bar toilet fixture"), M("keg", (16, 16), 4404, 76, "a stack of two stainless steel beer kegs, brewery barrels"), M("jukebox", (16, 32), 4405, 76, "a classic jukebox with illuminated arched top, chrome and coloured lights"), M("raffleDrum", (16, 16), 4406, 60, "a small wire mesh raffle barrel on a stand with a crank handle"), M("bainMarie", (48, 16), 4407, 60, "a stainless steel bain-marie hot food counter with sneeze guard, pub bistro"), M("beerUmbrella", (32, 32), 4408, 80, "a large outdoor patio umbrella open, canvas canopy seen from high above"), M("ashtray", (16, 16), 4409, 60, "a battered metal outdoor ashtray bin on a short pole"), F("tabBoard", (48, 16), 4410, "a bank of three wall mounted betting screens showing horse racing odds, glowing blue"), F("dartboard", (16, 16), 4411, "a dartboard on a scuffed wooden backboard with darts in it"), F("tvSport", (32, 16), 4412, "a wall mounted flat screen television showing a night rugby match, glowing"), ] # ---- Elevate — rooftop -------------------------------------------------------- ELEVATE = [ M("cocktailBar", (96, 16), 4420, 60, "a short modern cocktail bar counter with backlit bottle shelving, marble top"), M("banquette", (32, 32), 4421, 60, "a curved upholstered velvet banquette booth seat, luxury lounge furniture"), M("ropePost", (16, 16), 4422, 76, "a single polished brass rope stanchion post with red velvet rope"), M("planter", (32, 16), 4423, 60, "a long rectangular concrete planter box full of green foliage and grasses"), M("balustrade", (64, 16), 4424, 76, "a section of frameless glass balustrade railing with a steel handrail"), M("djPlinth", (48, 32), 4425, 60, "a raised white platform plinth stage with a DJ controller on top"), M("marbleSink", (48, 16), 4426, 70, "a luxury marble bathroom vanity with two vessel basins and brass taps"), M("ashUrn", (16, 16), 4427, 76, "a tall stainless steel standing ashtray urn, hotel lobby style"), M("champBucket", (16, 16), 4428, 60, "a silver champagne bucket on a stand full of ice and a bottle"), M("loungeChair", (16, 16), 4429, 60, "a low modern outdoor rattan lounge chair with a cushion"), F("liftDoor", (32, 32), 4430, "a pair of closed brushed steel lift elevator doors with a call panel"), F("skylineCard", (128, 32), 4431, "a distant night city skyline of lit office towers, warm gold windows, " "seen across a dark horizon", gen=(2048, 512), quantise=False, keep_bg=True), ] # ---- ROOM — concrete warehouse ----------------------------------------------- ROOM = [ M("stackF1", (32, 48), 4440, 76, "a huge professional nightclub speaker stack tower, three stacked black " "bass bins and horns, sound system rig"), M("subBass", (32, 16), 4441, 60, "a large black subwoofer bass bin cabinet lying wide, professional audio"), M("plywoodBar", (128, 16), 4442, 60, "a rough improvised bar counter made of plywood sheets on scaffolding poles"), M("pallet", (32, 16), 4443, 60, "a stack of two wooden shipping pallets"), M("wheelieBin", (16, 16), 4444, 76, "a large plastic wheelie rubbish bin with the lid closed, industrial"), M("oldCouch", (48, 16), 4445, 60, "a sagging ruined old fabric three seater couch, stained and torn"), M("pillar", (16, 16), 4446, 76, "a square raw concrete structural column pillar, industrial warehouse"), M("strobe", (16, 16), 4447, 76, "a black professional strobe light fixture on a short stand"), M("smokeMachine", (16, 16), 4448, 60, "a black fog smoke machine unit with a nozzle, stage equipment"), M("cableSnake", (32, 16), 4449, 60, "a coiled bundle of thick black stage power cables taped to the floor"), M("brokenBasin", (32, 16), 4450, 70, "a cracked dirty ceramic wall mounted bathroom basin with exposed pipes"), M("canStack", (16, 16), 4451, 60, "a stack of plain unbranded aluminium drink cans in a cardboard tray"), M("cdj", (32, 32), 4452, 60, "a professional CDJ media player deck, black with a large jog wheel"), F("rollerDoor", (64, 32), 4453, "a large corrugated steel roller shutter door, half open, loading dock", keep_bg=True), F("ruleBoard", (32, 32), 4454, "a scuffed black letter board sign with white push-in plastic letters, " "cryptic house rules, chipped frame"), ] # ---- Material plates (docs/VENUES.md §1: `FloorLayout.materials`) ------------- # Stamped one-per-tile and TINTED to the venue palette, so these want to be # grain and pattern, not colour — the hue is applied at bake time. 16x16 is the # game's tile, and a plate that reads as "carpet" at 16px is coloured noise with # structure, which is also what pub carpet looks like from head height. TEXTURE_TAIL = ( "seamless repeating texture, flat lay, straight overhead, evenly lit, " "no objects, no text, no watermark, tileable" ) def T(kind: str, seed: int, subject: str) -> Asset: return Asset(f"tex:{kind}", "flat", (16, 16), f"{subject}, {TEXTURE_TAIL}", seed, gen=(1024, 1024), quantise=False, keep_bg=True) TEXTURES = [ T("carpet", 4490, "gaudy patterned red and navy pub carpet, worn, swirling 1990s design"), T("concrete", 4491, "raw grey polished concrete warehouse floor, cracks and stains"), T("deck", 4492, "pale grey composite timber rooftop decking boards"), T("parquet", 4493, "scuffed wooden parquet dance floor, herringbone blocks"), T("looTile", 4494, "small square white ceramic bathroom floor tiles with grimy grout"), T("paving", 4495, "grey concrete paver slabs, wet, outdoor courtyard"), T("plaster", 4496, "flat painted plaster wall, scuffed and nicotine stained"), T("brick", 4497, "dark painted brick wall, mortar lines"), T("timber", 4498, "varnished dark timber bar counter top, grain visible"), T("steel", 4499, "brushed stainless steel panel, faint scratches"), ] # ---- Wall surfaces (flat by nature — flux draws these directly) --------------- WALLS = [ F("mirror", (32, 16), 4480, "a large wall mirror in a plain frame reflecting dim room lights"), F("graffiti", (32, 16), 4481, "a concrete toilet wall completely covered in layered marker tags and scrawl"), ] # ---- Staff figures (set dressing props, never patrons) ------------------------ STAFF = [ M("staffBartender", (16, 32), 4460, 76, "a single bartender standing, black shirt, apron, arms at sides, full body"), M("staffDj", (16, 32), 4461, 76, "a single DJ standing wearing headphones around the neck, full body"), M("staffGlassie", (16, 32), 4462, 76, "a single hospitality worker standing holding a stack of empty pint glasses, full body"), M("staffSecurity", (16, 32), 4463, 76, "a single security guard standing, black bomber jacket, arms folded, full body"), ] # ---- Door-scene street plates, one per venue --------------------------------- STREETS = [ F("street:theRoyal", (640, 176), 4470, "a wide blank painted brick pub wall at night, green tiled dado, torn posters, " "a downpipe, wet footpath, warm amber spill, NO doors, NO entrance, no windows", gen=(2048, 576), quantise=False, keep_bg=True), F("street:elevate", (640, 176), 4471, "a wide polished marble and dark glass building wall at night, planters along the base, " "cool white downlight, NO doors, NO entrance", gen=(2048, 576), quantise=False, keep_bg=True), F("street:room", (640, 176), 4472, "a wide blank unmarked concrete warehouse wall at night, no signage, heavy graffiti, " "sodium orange light, puddles, rain, NO doors, NO entrance", gen=(2048, 576), quantise=False, keep_bg=True), ] # ---- Wave 2: filling the rooms out ------------------------------------------ # Reviewers flagged two genuinely empty spaces (The Royal's central carpet, ROOM's # east hall) and the venues share almost no small dressing — the fire kit, signage # and bins every real room is full of and no game room ever has. FILLER = [ # The Royal's middle M("pieWarmer", (32, 16), 4500, 60, "a glass fronted heated pie warmer cabinet on a pub counter"), M("binRound", (16, 16), 4501, 76, "a round stainless steel swing top rubbish bin"), M("sandwichBoard", (16, 16), 4502, 60, "a wooden A-frame chalkboard sandwich board sign standing open"), F("noticeBoard", (32, 16), 4503, "a cork pub notice board covered in pinned paper flyers and raffle tickets"), F("ceilingFan", (32, 32), 4504, "a dusty four blade ceiling fan seen from directly below, dark"), # ROOM's east hall and general industrial M("flightCase", (32, 16), 4505, 60, "a black road flight case with metal corners and latches"), M("barrier", (32, 16), 4506, 76, "a galvanised steel crowd control barrier fence section"), M("distroBox", (16, 16), 4507, 76, "a grey industrial electrical distribution box with thick cables"), F("fireHose", (16, 16), 4508, "a red fire hose reel mounted on a wall"), # Elevate M("firepit", (32, 16), 4509, 60, "a modern rectangular gas fire pit table with glass surround, lit"), M("topiary", (16, 16), 4510, 76, "a neatly clipped ball topiary shrub in a tall planter"), # Every venue: the kit a real room has and a game room never does M("fireExt", (16, 16), 4511, 76, "a red fire extinguisher on a wall bracket"), F("exitSign", (16, 16), 4512, "a glowing green illuminated fire exit sign, running man pictogram"), F("cctv", (16, 16), 4513, "a small white dome CCTV security camera on a ceiling mount"), # More staff figures (set dressing, never patrons) M("staffCleaner", (16, 32), 4514, 76, "a single cleaner standing holding a mop, hi vis vest, full body"), M("staffChef", (16, 32), 4515, 76, "a single kitchen cook standing in whites and apron, arms at sides, full body"), ] # ---- Wave 3: the door scene ------------------------------------------------- # The rope is where the player spends most of the night and it was still 28 # coloured rectangles behind the one street plate. All FRONT-ON and all flat: # this is a tableau, not a top-down room, so flux picking its own camera — # the thing that forces furniture onto the mesh route — does not apply. DOOR_TAIL = ( "front-on flat elevation, night, wet, video game background art, " "no people, no watermark, no logo" ) def D(kind: str, size: tuple[int, int], seed: int, subject: str, gen: tuple[int, int] = (1024, 1024)) -> Asset: return Asset(kind, "flat", size, f"{subject}, {DOOR_TAIL}", seed, gen=gen, quantise=False, keep_bg=True) def DCUT(kind: str, size: tuple[int, int], seed: int, subject: str) -> Asset: """Door-scene object that wants cutting out of its background.""" return Asset(kind, "flat", size, f"{subject}, {DOOR_TAIL}", seed, gen=(1024, 1024), quantise=True, keep_bg=False) DOORSCENE = [ # The kebab shop is the moral centre of the whole game (design §5) and it was # three rectangles and the word KEBABS. D("kebabShop", (112, 106), 4520, "a late night kebab takeaway shopfront, glowing yellow sign board, " "steamy window, vertical rotisserie inside, tiled counter"), # One venue façade per rung of the ladder — the first thing you see of a # promotion, standing in front of that venue's street plate. D("front:theRoyal", (116, 140), 4521, "a suburban Australian pub facade at night, painted brick, frosted windows, " "a lit beer brand sign, single timber door"), D("front:voltage", (116, 140), 4522, "a nightclub facade at night, black painted brick, pink neon strip, " "a dark recessed doorway"), D("front:elevate", (116, 140), 4523, "a polished hotel lobby entrance at night, glass and brushed steel, " "warm downlight, a lift lobby visible inside"), D("front:room", (116, 140), 4524, "a blank concrete warehouse facade at night, no signage at all, " "one steel service door, sodium light above it"), DCUT("stanchion", (16, 32), 4525, "a single brass rope barrier stanchion post, front view, isolated on black"), DCUT("streetBin", (24, 32), 4526, "a battered council street rubbish bin, front view, isolated on black"), DCUT("streetLamp", (24, 96), 4527, "a tall street lamp post lit with orange sodium light, front view, isolated on black"), DCUT("taxi", (96, 48), 4528, "a waiting taxi cab seen from the side at night, headlights on, isolated on black"), # Six more gig posters. Mangled text is CORRECT here and always has been — # docs/SPACES.md keeps "no text" out of the poster prompts on purpose, # because unreadable print is what a real pub poster wall looks like. *[ Asset(f"poster{i}", "flat", (32, 16), p, 4530 + i, gen=(512, 768), quantise=True) for i, p in enumerate([ "a grimy 90s Australian pub rock gig poster, portrait, torn edges", "a rave flyer poster, acid green and magenta, portrait, torn edges", "a missing cat poster stapled to a pole, portrait, weathered", "a techno club night poster, stark black and white, portrait", "a meat raffle and poker night notice, portrait, photocopied", "a covers band poster with a bad photo of the band, portrait", ], start=4) ], ] ALL: list[Asset] = [ *ROYAL, *ELEVATE, *ROOM, *TEXTURES, *WALLS, *STAFF, *FILLER, *STREETS, *DOORSCENE, ] BY_KIND = {a.kind: a for a in ALL} if __name__ == "__main__": flat = sum(1 for a in ALL if a.route == "flat") print(f"{len(ALL)} assets — {len(ALL) - flat} mesh, {flat} flat") for a in ALL: print(f" {a.route:4} {a.kind:16} {a.size[0]:>4}x{a.size[1]:<3} rake={a.rake}")