not-tonight/tools/gen/assets.py
m3ultra 4caa901df2 Four venues, four rooms: the ladder stops being one map with different numbers
The venue ladder has always listed four venues in data/venues.ts, but they
shared ONE floor map and differed only by difficulty knobs — so a promotion you
survived a whole week for looked exactly like the room you had just left.

- venueMap.ts grows a FloorLayout contract (map + props + lights + posts +
  probes + palette) and the newGrid() primitives every room is painted with.
  Voltage stays IN venueMap rather than moving to layouts/, so the new rooms can
  import primitives from it without closing an import cycle.
- Three new rooms in src/scenes/floor/layouts/: The Royal (horseshoe public bar,
  pool room, pokies corner, trough + two cubicles, a beer garden that is plainly
  the nicest room in the pub, and a DJ "corner" that is a folding table because
  this pub never built a booth), Elevate (open-air rooftop — most of the grid is
  sky, the DJ is on an unwalled plinth, you arrive by lift), ROOM (concrete
  warehouse, pillars you path around, central booth, loading-dock smoking area,
  twelve lights in the whole venue).
- Nothing about a room is a module singleton any more. FloorView, sweep and
  FloorDemoScene take the layout; the door picks its street plate by venue id.
- FloorDemoScene's six hardcoded station spots (TAPS_SPOT, DECKS_SPOT, ...) were
  Voltage's tile coordinates. At four venues a hardcoded (47,3) puts the bar
  shift inside The Royal's pool room, so each layout now names its own stations
  and the scene reads them from there.
- tests/floor/layoutInvariants.ts is an executable rulebook: perimeter holds,
  every walkable tile reachable from the entry, anchors on walkable ground,
  posts not sealed (a post on a sealed tile freezes the player for the night —
  that has shipped twice), probes reachable, props on-grid. Proved against
  Voltage BEFORE the new rooms were authored against it.

Dev route #floor:<venueId> boots the floor straight into one room, because
otherwise seeing ROOM means surviving three weeks of the ladder.

Gate: lint clean, build clean, 821 tests passing (was 784).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 19:51:54 +10:00

204 lines
9.8 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"),
]
# ---- 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 suburban Australian pub frontage at night, painted brick, tiled dado, "
"a lit beer sign, bins, wet footpath, warm amber light",
gen=(2048, 576), quantise=False, keep_bg=True),
F("street:elevate", (640, 176), 4471,
"a wide polished lobby entrance of a rooftop bar at night, brushed steel lift doors, "
"marble, planters, cool white downlight, glass",
gen=(2048, 576), quantise=False, keep_bg=True),
F("street:room", (640, 176), 4472,
"a wide unmarked concrete warehouse wall at night, no signage, a single steel door, "
"graffiti, sodium orange streetlight, puddles, rain",
gen=(2048, 576), quantise=False, keep_bg=True),
]
ALL: list[Asset] = [*ROYAL, *ELEVATE, *ROOM, *STAFF, *STREETS]
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}")