127 sprites had been generated and about eight had ever been looked at. The pipeline will happily render the wrong object, confidently and well-lit, and no assertion can tell — barrier and flightCase are both "a grey box with edges" as far as any test goes. tools/gen/contact_sheet.py puts every shipped sprite on one page at integer upscale on the venue's own floor colour. It immediately showed about twenty props reading as pale blobs. First instinct was that the vertex baker had lost the albedo. That was wrong: sat=0 on ashUrn, marbleSink and pillar is honest, because stainless and marble and concrete really are grey. The fault was BRIGHTNESS. Props that read sit at median luminance 23-123 (patioHeater 23, arcade 45, poolTable 53, cdj 68); the blobs were 150-190. The venue is painted at ~35 under a 50% black sheet and props are lit by the room's own LIGHTS, so a prop arriving at 180 doesn't read as a bright object — it reads as self-illuminated. Every prop that worked before worked by accident of subject matter; the first pale subjects exposed the gap. props_import --dim scales RGB until median opaque luminance is <=110. Only ever darkens, so anything already in band passes through untouched. Fixed 48 sprites with no regeneration at all — the 512px Blender renders were still in art_incoming, which is exactly why that directory is kept. Also: poster7 shipped with 29% of its pixels and poster9 with 42%, because the near-black-to-alpha pass was eating the dark half of a dark poster. Posters are rectangular plates on a wall, so 4-9 now keep their background and fill the frame. And cf_flux --probe was reporting EXHAUSTED on an account with a full 10,000 neurons: the probe prompt was "a grey square", Cloudflare's safety filter refused it as NSFW, and every non-429 fell through to SystemExit. A health check that can't tell "refused" from "empty" will eventually call a healthy system dead. Gate: lint clean, build clean, 821 tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
361 lines
18 KiB
Python
361 lines
18 KiB
Python
#!/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.
|
|
# keep_bg + "fills the frame": a poster IS a rectangular plate on a wall, and
|
|
# the importer's near-black-to-alpha pass was eating the dark half of the
|
|
# darker ones — poster7 shipped with 29% of its pixels and poster9 with 42%.
|
|
*[
|
|
Asset(f"poster{i}", "flat", (32, 16),
|
|
f"{p}, fills the entire frame edge to edge, flat to camera, "
|
|
"no border, no background visible, video game texture art",
|
|
4530 + i, gen=(512, 768), quantise=True, keep_bg=True)
|
|
for i, p in enumerate([
|
|
"a grimy 90s Australian pub rock gig poster, torn and faded",
|
|
"a rave flyer poster, acid green and magenta",
|
|
"a missing cat poster, weathered and rain damaged",
|
|
"a techno club night poster, stark black and white",
|
|
"a meat raffle and poker night notice, photocopied",
|
|
"a covers band poster with a bad photo of the band",
|
|
], 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}")
|