nomeansnoquest/tools/gen_rooms.py
m3ultra 2650c8b024 v1.1 — full MODELBEAST painting pass + jazzcore soundtrack
All six rooms are now flux_local matte paintings quantized by the engine (basement, gig, alley, office, Roskilde, field), with rooms reduced to invisible sim ops: walkability, depth, water, the footlight chases, and the NO MEANS NO! spray painted OVER the brick painting. The office desk gets a real walk-behind from the priority bands. Soundtrack: tools/gen_music.py synthesizes five loop-safe mood overrides (music/*.wav) in the band's own meters — 7/4 walking bass for the basement, the 9/8 riff floored for gig and Roskilde, chromatic noir for the alley, puck-rock bounce for the office, a 5/4 drone for the field. Fixed the office east-door lane (crates pinched the exit trigger) and removed the alley's unreachable north exit. Playthrough: 60/60, won, byte-identical twice, one intentional death.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 21:18:00 +10:00

474 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate NOMEANSNO QUEST rooms as MRPCI RoomDoc JSON.
Every room is a MODELBEAST matte painting (pics/*.png, quantized by the
engine at load) plus *invisible* ops: walkability, depth, water — and the
few visible overlays that must move or glow (footlight chases, the spray
paint). Run from the repo root: python3 tools/gen_rooms.py
"""
import json
import os
OUT = os.path.join(os.path.dirname(__file__), "..", "rooms")
def cube(r, g, b):
return 32 + 36 * r + 6 * g + b
def gray(i):
return 16 + min(i, 15)
EMBER = list(range(248, 256))
KEEP = "Keep"
def ink(color=None, pri=KEEP, ctl=KEEP, hot=None):
return {"color": color, "pri": pri, "ctl": ctl, "hot": hot}
def look(c):
return ink(color=c)
def wall(c=None):
return ink(color=c, ctl={"Set": 1})
def open_floor():
return ink(pri="Band", ctl={"Set": 0})
def water(c=None):
return ink(color=c, ctl={"Set": 2})
def band_of(y):
return min(15, y * 16 // 190)
def rect(x, y, w, h, i):
return {"Rect": {"x": x, "y": y, "w": w, "h": h, "ink": i}}
def line(pts, i):
return {"Line": {"pts": pts, "ink": i}}
def ellipse(cx, cy, rx, ry, i):
return {"Ellipse": {"cx": cx, "cy": cy, "rx": rx, "ry": ry, "ink": i}}
def poly(pts, i):
return {"Polygon": {"pts": pts, "ink": i}}
def fbm(x, y, w, h, ramp, scale, octaves, seed, i):
return {"FbmFill": {"x": x, "y": y, "w": w, "h": h, "ramp": ramp, "scale": scale, "octaves": octaves, "seed": seed, "ink": i}}
# The standard sim skeleton over a painting: everything above floor_y is
# scenery (blocked), everything below walks and depth-sorts by row.
def sim_base(floor_y):
return [
rect(0, 0, 320, floor_y, wall()),
rect(0, floor_y, 320, 190 - floor_y, open_floor()),
]
# Spray letters for the wall (drawn OVER the painting).
SEGS = {
"N": [((0, 6), (0, 0)), ((0, 0), (4, 6)), ((4, 6), (4, 0))],
"O": [((0, 0), (4, 0)), ((4, 0), (4, 6)), ((4, 6), (0, 6)), ((0, 6), (0, 0))],
"M": [((0, 6), (0, 0)), ((0, 0), (2, 3)), ((2, 3), (4, 0)), ((4, 0), (4, 6))],
"E": [((4, 0), (0, 0)), ((0, 0), (0, 6)), ((0, 6), (4, 6)), ((0, 3), (3, 3))],
"A": [((0, 6), (2, 0)), ((2, 0), (4, 6)), ((1, 4), (3, 4))],
"S": [((4, 0), (0, 1)), ((0, 1), (4, 5)), ((4, 5), (0, 6))],
"!": [((0, 0), (0, 4)), ((0, 6), (0, 6))],
}
def spray(text, x0, y0, s, color):
ops = []
x = x0
for ch in text:
if ch == " ":
x += 3 * s
continue
for (a, b) in SEGS.get(ch, []):
pts = [[x + a[0] * s, y0 + a[1] * s], [x + b[0] * s, y0 + b[1] * s]]
ops.append(line(pts, look(color)))
ops.append(line([[p[0] + 1, p[1]] for p in pts], look(color)))
x += 6 * s
return ops
def hotspot(name, r=None, msgs=None, **kw):
h = {"name": name}
if r:
h["rect"] = r
if msgs:
h["msgs"] = msgs
h.update(kw)
return h
def prop(name, sprite, x, y, **kw):
p = {"name": name, "sprite": sprite, "x": x, "y": y}
p.update(kw)
return p
def npc(name, sprite, x, y, **kw):
n = {"name": name, "sprite": sprite, "x": x, "y": y}
n.update(kw)
return n
def node(says, *choices):
return {"says": says, "choices": list(choices)}
def choice(text, goto, **kw):
c = {"text": text, "goto": goto}
c.update(kw)
return c
ROOMS = {}
# =============================================================================
# Room 0 — The Basement, Victoria BC, 1979 (pics/basement.png)
# =============================================================================
ops = sim_base(118)
ops.append(rect(0, 118, 84, 44, wall())) # washer + cabinet footprint
ops.append(rect(240, 118, 80, 24, wall())) # under-stair clutter line
# The D.O.A. poster is OUR overlay (the painting left that wall bare).
ops.append(rect(244, 36, 26, 36, wall(gray(14))))
ops.append(rect(247, 40, 20, 9, wall(cube(4, 0, 0))))
ops.append(rect(247, 53, 20, 3, wall(gray(3))))
ops.append(rect(247, 59, 14, 3, wall(gray(3))))
ops.append(rect(247, 65, 17, 3, wall(gray(3))))
ROOMS[0] = {
"name": "The Basement, 1979",
"enter_text": "Wood panel, concrete, one bare bulb. John's kit crowds the rug; the 180-pound Portastudio owns the workbench. Through the wall, Mom's washer keeps better time than most drummers.",
"background": "basement",
"ops": ops,
"scale": {"horizon_y": 100, "min_scale": 0.72, "full_y": 186},
"spawn": [180, 160],
"exits": [None, 1, 3, None],
"exit_flags": ["", "", "demo_done", ""],
"exit_blocked": ["", "", "No demo, no destiny. The Portastudio is right there.", ""],
"hotspots": [
hotspot("gig poster", [242, 34, 30, 40], msgs={
"look": "D.O.A. — TONIGHT — UVic. Vancouver's finest, one ferry away. John already circled it.",
"do": "You're going. Obviously you're going. (Up the stairs, east.)",
}),
hotspot("washer", [0, 84, 84, 76], msgs={
"look": "Mom's washing machine, mid-cycle. A wet 4/4 that never rushes and never drags.",
"listen": "Chunk. Chunk. Chunk. Honestly? Tighter than most bands you've seen.",
"do": "It's Mom's. Some things are sacred.",
}),
hotspot("window", [186, 34, 46, 36], msgs={
"look": "Grey Victoria daylight through glass that hasn't been washed since the Diefenbaker administration.",
}),
hotspot("workbench", [116, 88, 82, 28], msgs={
"look": "The workbench holds the TEAC four-track like an altar holds a relic.",
}),
hotspot("stairs", [244, 60, 76, 56], msgs={
"look": "Up to daylight, dinner, and — tonight — a ferry to a D.O.A. show.",
}),
],
"props": [
prop("portastudio", "portastudio", 156, 101,
synonyms="four-track teac tascam recorder deck",
msgs={
"look": "The TEAC Portastudio. 180 pounds of overdubbing possibility. It wants tape and it wants a reason.",
"do": "It powers up with a hum like a small appliance dreaming big.",
"listen": "Tape hiss. The sound of potential.",
}),
prop("castle box", "castlebox", 218, 154,
synonyms="box junk castle",
msgs={
"look": "A box of gear from Castle, the cover band you both did time in. Cables, setlists nobody needs, and — is that a fresh reel of tape?",
},
solid=True),
prop("drum kit", "drumkit", 112, 150,
synonyms="drums kit",
msgs={
"look": "John's kit, tuned like a jazz kit because he IS a jazz kid. The high school band has no idea what it's incubating.",
"play": "You are a bassist. There are laws.",
},
solid=True),
],
"npcs": [
npc("John", "john", 152, 164, portrait="john-face", wander=10,
msgs={"look": "Your kid brother. Eight years younger, already swings harder than drummers twice his age."},
dialogue=[
node("Sticks spin in his fingers. “So? Are we a band, or are we two guys who own instruments?”",
choice("We need a sound of our own first.", 1),
choice("Heard D.O.A. play UVic tonight.", 2),
choice("Later, John.", -1)),
node("“No guitar player, eh? Fine. You be the guitar. I'll be the horn section.” He plays a bar of 9/8 like it's nothing.",
choice("That. Exactly that.", -1)),
node("“Then what are we still doing in the basement? GO. Take notes. Loud ones.”",
choice("On it.", -1)),
]),
npc("Mom", "mom", 98, 146, portrait="mom-face", wander=6,
msgs={"look": "Mom, running the laundry room like mission control."},
dialogue=[
node("“Play all you like, love — the whites are done at eight.” She pauses. “The loud one in 7/4 was nice.”",
choice("You could tell it was 7/4?", 1),
choice("Thanks, Mom.", -1)),
node("“I've been listening to that washer for twenty years, Robert. I know time when I hear it.”",
choice("...Mom might be the best musician in the house.", -1)),
]),
],
"music": 1,
"script": "room0.rhai",
}
# =============================================================================
# Room 1 — D.O.A. at UVic, 1980 (pics/gig.png)
# =============================================================================
ops = sim_base(122)
# Footlight chase across the painted stage lip.
for i, x in enumerate(range(44, 276, 12)):
ops.append(rect(x, 118, 8, 3, wall(EMBER[i % 8])))
ROOMS[1] = {
"name": "UVic — D.O.A., 1980",
"enter_text": "A university hall pretending it's a bunker. D.O.A. detonate on stage. The floor moves like weather.",
"background": "gig",
"ops": ops,
"cycles": [{"start": 248, "len": 8, "period": 2, "reverse": False, "active": True}],
"scale": {"horizon_y": 118, "min_scale": 0.8, "full_y": 186},
"spawn": [160, 165],
"exits": [None, None, 2, 0],
"hotspots": [
hotspot("stage", [40, 10, 240, 110], msgs={
"do": "Security gives you the look that says: no.",
"listen": "Loud enough to reset your heartbeat to theirs.",
}),
hotspot("pit", [80, 130, 160, 50], trigger=True, msgs={
"look": "The pit. A weather system with elbows.",
}),
],
"props": [
prop("flyer", "flyer", 294, 170,
synonyms="handbill paper",
msgs={"look": "A gig flyer, boot-printed. You'll keep it forever and lie about why."},
takeable=True),
],
"npcs": [
npc("Joey", "joey", 150, 116, fixed_scale=0.8,
msgs={
"look": "Joey Shithead, mid-war-cry. Vancouver hardcore, delivered at terminal velocity.",
"talk": "He can't hear you. Nobody will hear anything until Thursday.",
}),
],
"music": 3,
"script": "room1.rhai",
}
# =============================================================================
# Room 2 — The alley, and the wall (pics/alley.png)
# =============================================================================
ops = sim_base(137)
ops += spray("NO MEANS NO!", 34, 48, 4, gray(15))
ops += spray("NO MEANS NO!", 35, 49, 4, gray(12))
ops.append(ellipse(45, 162, 26, 6, water()))
ops.append(ellipse(250, 160, 22, 5, water()))
ROOMS[2] = {
"name": "The Alley",
"enter_text": "Behind the venue, rain stitching the puddles. Someone has written three words on the brick, big enough to be a law.",
"background": "alley",
"ops": ops,
"scale": {"horizon_y": 128, "min_scale": 0.8, "full_y": 186},
"spawn": [160, 165],
"exits": [None, None, None, 0],
"hotspots": [
hotspot("graffiti", [28, 28, 264, 106], msgs={
"do": "You touch the brick. It's cold. The words stay.",
}),
hotspot("puddle", [16, 150, 90, 22], msgs={
"look": "The words float upside-down in the puddle, still true both ways.",
}),
],
"props": [
prop("dumpster", "dumpster", 284, 158,
msgs={
"look": "A dumpster keeping the alley's secrets. Every scene in every city has this exact dumpster.",
"smell": "1980 called. It wants nothing back.",
},
solid=True),
],
"npcs": [],
"music": 6,
"weather": 1,
"script": "room2.rhai",
}
# =============================================================================
# Room 3 — Alternative Tentacles, San Francisco, 1987 (pics/office.png)
# =============================================================================
ops = sim_base(147)
ops.append(rect(112, 147, 122, 30, wall())) # the desk: walk behind it, not through it
# Record crates hug the corners but leave the door lanes open.
ops.append(rect(0, 160, 66, 30, wall())) # record crates, left
ops.append(rect(258, 168, 62, 22, wall())) # record crates, right (the east door lane stays open)
ROOMS[3] = {
"name": "Alternative Tentacles, 1987",
"enter_text": "San Francisco. The office smells of newsprint, coffee, and litigation. Every poster on the wall is somebody's whole life.",
"background": "office",
"ops": ops,
"scale": {"horizon_y": 130, "min_scale": 0.8, "full_y": 186},
"spawn": [88, 183],
"exits": [None, 4, None, 0],
"exit_flags": ["", "signed", "", ""],
"exit_blocked": ["", "Europe doesn't book the unsigned. Talk to Jello.", "", ""],
"hotspots": [
hotspot("poster wall", [104, 0, 216, 100], msgs={
"look": "Dead Kennedys. D.O.A. Butthole Surfers. A wall of bands that never asked permission.",
"do": "You straighten one thumbtack. History looks marginally tidier.",
}),
hotspot("window", [0, 0, 104, 110], msgs={
"look": "Fog rolling off the bay, gold light on the bridge. Even the weather here has a record deal.",
}),
hotspot("record crates", [0, 148, 74, 42], msgs={
"look": "Crates of vinyl heading to distributors on four continents. The machine, fed.",
}),
hotspot("desk", [110, 100, 126, 76], msgs={
"look": "The desk: contracts, a mountain of cassettes, and a rubber tarantula used as a paperweight.",
}),
],
"props": [],
"npcs": [
npc("Jello", "biafra", 214, 184, portrait="biafra-face", wander=8,
msgs={"look": "Jello Biafra, sorting demos like a heron reads a river."},
dialogue=[
node("The Canadians. The brothers. The ones who sound like — what did Trouser Press say — Wire on psychotic steroids?",
choice("Slip him the demo tape.", 1, requires_flag="demo_done", sets_flag="signed", points=10),
choice("We're NoMeansNo. And no means no.", 2),
choice("Just admiring the tarantula.", -1)),
node("He plays thirty seconds of Sex Mad. Both eyebrows achieve escape velocity. “Licensed. International. Don't shave, don't die, and tour Europe until Europe apologizes.”",
choice("Deal.", -1)),
node("“Good name. Better policy.” He gestures at the demo mountain. “Feed the machine, gentlemen.”",
choice("(Feed it.)", 0)),
]),
],
"music": 4,
"script": "room3.rhai",
}
# =============================================================================
# Room 4 — Roskilde main stage, 1994 (pics/roskilde.png)
# =============================================================================
ops = []
ops.append(rect(0, 0, 320, 112, wall()))
ops.append(rect(0, 110, 320, 6, wall(gray(1))))
for i, x in enumerate(range(4, 316, 12)):
ops.append(rect(x, 113, 8, 4, wall(EMBER[i % 8])))
ops.append(fbm(0, 118, 320, 72, [cube(1, 0, 0), cube(2, 1, 0), cube(1, 0, 0)], 20.0, 2, 54, ink(color=cube(1, 0, 0), pri="Band", ctl={"Set": 0})))
for y in range(126, 190, 10):
ops.append(line([[0, y], [319, y]], look(cube(0, 0, 0))))
ops.append(poly([[70, 122], [96, 122], [88, 132], [78, 132]], ink(color=gray(3), pri={"Set": band_of(132)})))
ops.append(poly([[200, 122], [226, 122], [218, 132], [208, 132]], ink(color=gray(3), pri={"Set": band_of(132)})))
ROOMS[4] = {
"name": "Roskilde Main Stage, 1994",
"enter_text": "Denmark. Sixty thousand people breathing as one animal in the dark. Sepultura's van died in Germany, Peter Gabriel just walked off, and somebody has to be next.",
"background": "roskilde",
"ops": ops,
"cycles": [{"start": 248, "len": 8, "period": 2, "reverse": False, "active": True}],
"scale": {"horizon_y": 112, "min_scale": 0.8, "full_y": 186},
"spawn": [50, 160],
"exits": [None, 5, None, 3],
"exit_flags": ["", "legend", "", ""],
"exit_blocked": ["", "Not before the encore. Play the show.", "", ""],
"hotspots": [
hotspot("crowd", [0, 30, 320, 80], msgs={
"look": "A field of lighters to the horizon. Sixty thousand Danes who came for Sepultura and stayed for whatever this is about to be.",
"listen": "The roar arrives in waves, like surf that learned your name.",
"talk": "You raise one fist. SIXTY THOUSAND PEOPLE RAISE IT BACK.",
}),
hotspot("monitors", [66, 118, 164, 18], msgs={
"look": "Monitor wedges, mixed for a prog band. John will fix that with volume.",
}),
],
"props": [
prop("the rig", "rig", 276, 148,
synonyms="amps amp stack backline",
msgs={"look": "A borrowed backline the size of a rowhouse. Rob's overdrive will make it honest."},
solid=True),
],
"npcs": [
npc("stage manager", "stagehand", 96, 150, wander=20,
msgs={"look": "A Danish stage manager with a clipboard and eleven headaches."},
dialogue=[
node("Sepultura is IN A DITCH near Flensburg. I have sixty thousand people and a hole in the schedule after Peter Gabriel. You are either legends tonight, or you are filler.",
choice("We'll take the main stage.", 1, sets_flag="mainstage", points=10),
choice("We're... technically the tent act.", 2),
choice("Give us a minute.", -1)),
node("“Then GO.” He points at the rig with the clipboard. “And whatever a 'jazzcore' is — do it LOUDLY.”",
choice("Always.", -1)),
node("'Were.'” He crosses something out with terrifying finality.",
choice("(Reconsider.)", 0)),
]),
],
"music": 3,
"weather": 3,
"script": "room4.rhai",
}
# =============================================================================
# Room 5 — A farm field outside Poznan, 1997 (pics/field.png)
# =============================================================================
ops = sim_base(112)
ops.append(rect(0, 124, 52, 10, wall())) # fence line, left
ops.append(rect(268, 124, 52, 10, wall())) # fence line, right
ops.append(ellipse(152, 166, 22, 4, water()))
ops.append(ellipse(176, 180, 18, 4, water()))
ROOMS[5] = {
"name": "A Field Outside Poznan, 1997",
"enter_text": "Poland. Dawn the color of dishwater. Your van — your entire livelihood with wheels on it — sits in a farm field, and a man in a tracksuit is leaning on it.",
"background": "field",
"ops": ops,
"scale": {"horizon_y": 106, "min_scale": 0.5, "full_y": 186},
"spawn": [36, 168],
"exits": [None, None, None, 4],
"hotspots": [
hotspot("farmhouse", [66, 88, 48, 22], msgs={
"look": "A farmhouse minding its own business at a professional level.",
}),
hotspot("crows", [120, 20, 180, 60], msgs={
"look": "Crows, circling like they've been paid to set the tone.",
"listen": "They have opinions about your situation.",
}),
],
"props": [
prop("van", "van", 170, 126,
synonyms="tourvan vehicle",
msgs={
"look": "The van. Every amp, every pedal, every unwashed sleeping bag — the whole band, parked in the wrong country.",
},
solid=True),
],
"npcs": [
npc("mobster", "mobster", 226, 146, portrait="mobster-face",
msgs={"look": "Tracksuit, flat cap, and the calm of a man who has never once hurried."},
dialogue=[
node("“Nice van,” he says, in better English than yours. “Shame about the neighborhood. Storage fees in this district are… irregular.”",
choice("Name your price.", 1),
choice("Do you KNOW who we are?", 2, kills=True),
choice("We'll come back.", -1)),
node("“Two and a half thousand. American. The van, the amps, the little drum machine — all of it, untouched.” He pats the van gently, like a horse.",
choice("Strike the deal.", 3, sets_flag="deal_struck"),
choice("Outrageous.", -1)),
node("In a field outside a town you cannot pronounce, it turns out that nobody, in fact, knows who you are.", ),
node("“Pleasure doing business with professionals.” He steps away from the van and lights a cigarette against the wind, first try.",
choice("(Get the money.)", -1)),
]),
],
"music": 2,
"weather": 1,
"script": "room5.rhai",
}
def main():
os.makedirs(OUT, exist_ok=True)
for n, doc in ROOMS.items():
path = os.path.join(OUT, f"room{n}.json")
with open(path, "w") as f:
json.dump(doc, f, indent=1, ensure_ascii=False)
print(f" room{n}.json ({len(doc.get('ops', []))} ops)")
if __name__ == "__main__":
main()