nomeansnoquest/tools/gen_rooms.py
m3ultra 50817ced59 NOMEANSNO QUEST v1.0 — the first MRPCI game
Six rooms of band history: the 1979 basement (Portastudio, Mom, the Castle box), D.O.A. at UVic, the alley wall that named the band, Alternative Tentacles 1987 (Jello signs you), the Roskilde main stage 1994 (MODELBEAST flux matte painting quantized by the engine; ops paint only walkability + cycling footlights), and the Polish field 1997 (the $2,500 van ransom — with one fatal dialogue option to prove checkpoint mercy). 60 points, deterministic playthrough in tests/, zero engine forks: pure game data + rhai. Generated art via tools/gen_sprites.py + gen_rooms.py.

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

562 lines
26 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate NOMEANSNO QUEST rooms as MRPCI RoomDoc JSON.
Six rooms, one band history: the basement, the D.O.A. gig, the alley wall,
Alternative Tentacles, Roskilde '94, and a Polish farm field in '97.
Run from the repo root: python3 tools/gen_rooms.py
"""
import json
import os
OUT = os.path.join(os.path.dirname(__file__), "..", "rooms")
# --- palette helpers (MRPCI base palette layout) ---------------------------
def cube(r, g, b): # 6x6x6 color cube at 32..248
return 32 + 36 * r + 6 * g + b
def gray(i): # 16-step gray ramp at 16..32
return 16 + min(i, 15)
EMBER = list(range(248, 256))
# --- ink helpers ------------------------------------------------------------
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 floor(c):
return ink(color=c, pri="Band", ctl={"Set": 0})
def wall(c):
return ink(color=c, ctl={"Set": 1})
def water(c):
return ink(color=c, ctl={"Set": 2})
def depth(c, band):
return ink(color=c, pri={"Set": band})
def band_of(y):
return min(15, y * 16 // 190)
# --- op helpers ---------------------------------------------------------------
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 vgrad(x, y, w, h, ramp, i):
return {"VGradient": {"x": x, "y": y, "w": w, "h": h, "ramp": ramp, "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}}
def voronoi(x, y, w, h, cell, colors, edge_color, edge, seed, i):
return {"VoronoiFill": {"x": x, "y": y, "w": w, "h": h, "cell": cell, "colors": colors, "edge_color": edge_color, "edge": edge, "seed": seed, "ink": i}}
def scatter(x, y, w, h, colors, density, seed, i):
return {"Scatter": {"x": x, "y": y, "w": w, "h": h, "colors": colors, "density": density, "seed": seed, "ink": i}}
# Crude 5x7-ish spray letters for the graffiti wall (scaled by s).
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)))
# a second pass one pixel off = fat spray line
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
# =============================================================================
ops = []
# Wood-panel walls with vertical seams.
ops.append(fbm(0, 0, 320, 100, [cube(2, 1, 0), cube(2, 1, 0), cube(3, 2, 1), cube(2, 1, 0)], 34.0, 2, 3, wall(0)))
for x in range(0, 320, 26):
ops.append(line([[x, 0], [x, 99]], ink(color=cube(1, 0, 0), ctl={"Set": 1})))
# Window well, top right — grey Victoria daylight.
ops.append(rect(268, 10, 34, 22, wall(gray(4))))
ops.append(rect(271, 13, 28, 16, wall(cube(3, 3, 4))))
ops.append(line([[285, 13], [285, 28]], wall(gray(4))))
# Concrete floor.
ops.append(voronoi(0, 100, 320, 90, 44, [gray(6), gray(6), gray(5), gray(6)], gray(5), 1.0, 9, floor(0)))
# Laundry corner, west: washer + a shelf of detergent.
ops.append(rect(6, 62, 30, 38, wall(gray(12))))
ops.append(rect(10, 68, 22, 22, wall(gray(9))))
ops.append(ellipse(21, 79, 8, 8, wall(cube(2, 3, 4))))
ops.append(rect(24, 64, 6, 3, wall(cube(4, 1, 1))))
ops.append(rect(4, 40, 36, 6, wall(cube(2, 1, 0))))
ops.append(rect(8, 30, 8, 10, wall(cube(4, 4, 0))))
ops.append(rect(20, 32, 8, 8, wall(cube(1, 3, 3))))
# The jam corner rug.
ops.append(ellipse(96, 148, 44, 14, floor(cube(1, 0, 0))))
# Workbench table along the north wall for the Portastudio.
ops.append(rect(128, 84, 66, 16, wall(cube(2, 1, 0))))
ops.append(rect(128, 84, 66, 3, wall(cube(3, 2, 1))))
# Gig poster on the wall.
ops.append(rect(214, 38, 26, 34, wall(gray(14))))
ops.append(rect(217, 42, 20, 8, wall(cube(4, 0, 0))))
ops.append(rect(217, 54, 20, 3, wall(gray(3))))
ops.append(rect(217, 60, 14, 3, wall(gray(3))))
# Stairs up, east edge.
for i in range(5):
ops.append(rect(292 - 0, 96 - i * 8, 28, 8, wall(cube(2, 1, 0)) if i % 2 == 0 else wall(cube(3, 2, 1))))
ops.append(rect(292, 100, 28, 90, floor(gray(5))))
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.",
"ops": ops,
"scale": {"horizon_y": 92, "min_scale": 0.72, "full_y": 186},
"spawn": [160, 150],
"exits": [None, 1, 3, None],
"exit_flags": ["", "", "demo_done", ""],
"exit_blocked": ["", "", "No demo, no destiny. The Portastudio is right there.", ""],
"hotspots": [
hotspot("gig poster", [212, 36, 30, 38], 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. (Walk east, up the stairs.)",
}),
hotspot("washer", [4, 60, 34, 42], 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", [266, 8, 38, 26], msgs={
"look": "Grey Victoria daylight. The Island doesn't hurry, and neither does the rain.",
}),
hotspot("workbench", [126, 82, 70, 20], msgs={
"look": "The workbench holds the TEAC four-track like an altar holds a relic.",
}),
],
"props": [
prop("portastudio", "portastudio", 160, 104,
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.",
},
solid=True),
prop("castle box", "castlebox", 224, 146,
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", 92, 136,
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", 66, 148, portrait="john-face", wander=12,
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", 26, 126, 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
# =============================================================================
ops = []
ops.append(vgrad(0, 0, 320, 96, [cube(0, 0, 1), cube(1, 0, 1), cube(0, 0, 0)], wall(0)))
# Spotlight cones.
ops.append(poly([[70, 0], [130, 0], [170, 92], [110, 92]], wall(cube(2, 1, 3))))
ops.append(poly([[200, 0], [250, 0], [210, 92], [160, 92]], wall(cube(3, 1, 1))))
# Stage.
ops.append(rect(56, 58, 208, 38, wall(cube(1, 0, 0))))
ops.append(rect(56, 58, 208, 4, wall(cube(2, 1, 0))))
# Backline silhouettes (the rest of D.O.A.).
ops.append(rect(90, 40, 14, 22, wall(gray(1))))
ops.append(rect(220, 44, 16, 18, wall(gray(1))))
ops.append(ellipse(97, 38, 4, 4, wall(gray(1))))
ops.append(ellipse(228, 42, 4, 4, wall(gray(1))))
# Footlight chase strip — the ember cycle makes it run.
for i, x in enumerate(range(58, 262, 12)):
ops.append(rect(x, 92, 8, 4, wall(EMBER[i % 8])))
# Crowd floor.
ops.append(vgrad(0, 96, 320, 94, [gray(2), gray(1)], floor(0)))
# Heads in the dark (front rows).
for i, x in enumerate(range(16, 300, 24)):
y = 108 + (i % 3) * 9
ops.append(ellipse(x, y, 6, 5, look(gray(1))))
ops.append(scatter(0, 96, 320, 40, [gray(3), gray(2)], 14, 21, look(0)))
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.",
"ops": ops,
"scale": {"horizon_y": 96, "min_scale": 0.7, "full_y": 186},
"spawn": [160, 164],
"exits": [None, None, 2, 0],
"hotspots": [
hotspot("stage", [56, 20, 208, 76], msgs={
"do": "Security gives you the look that says: no.",
"listen": "Loud enough to reset your heartbeat to theirs.",
}),
hotspot("pit", [80, 110, 160, 50], trigger=True, msgs={
"look": "The pit. A weather system with elbows.",
}),
],
"props": [
prop("flyer", "flyer", 288, 150,
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, 90, fixed_scale=0.85,
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
# =============================================================================
ops = []
# Face-on brick wall.
ops.append(fbm(0, 0, 320, 112, [cube(2, 1, 1), cube(2, 1, 1), cube(3, 1, 1)], 24.0, 2, 31, wall(0)))
for y in range(6, 110, 9):
ops.append(line([[0, y], [319, y]], ink(color=cube(1, 0, 0), ctl={"Set": 1})))
# Drips of old paint and rust.
ops.append(scatter(0, 0, 320, 112, [cube(1, 0, 0), gray(6)], 8, 32, ink(color=None, ctl={"Set": 1})))
# THE WORDS. White spray, uneven, perfect.
ops += spray("NO MEANS NO!", 46, 40, 4, gray(15))
ops += spray("NO MEANS NO!", 47, 41, 4, gray(12))
# Wet pavement.
ops.append(voronoi(0, 112, 320, 78, 12, [gray(4), gray(5), gray(3), cube(1, 1, 2)], gray(1), 1.4, 33, floor(0)))
ops.append(ellipse(120, 156, 34, 7, water(cube(1, 1, 3))))
ops.append(scatter(0, 112, 320, 78, [gray(8), cube(1, 1, 3)], 6, 34, look(0)))
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.",
"ops": ops,
"scale": {"horizon_y": 105, "min_scale": 0.75, "full_y": 186},
"spawn": [160, 160],
"exits": [1, None, None, 0],
"hotspots": [
hotspot("graffiti", [40, 30, 250, 74], msgs={
"do": "You touch the brick. It's cold. The words stay.",
}, synonyms=None),
hotspot("puddle", [86, 148, 70, 16], msgs={
"look": "The words float upside-down in the puddle, still true both ways.",
}),
],
"props": [
prop("dumpster", "dumpster", 282, 150,
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
# =============================================================================
ops = []
ops.append(vgrad(0, 0, 320, 96, [cube(4, 3, 2), cube(3, 2, 2)], wall(0)))
# Bay window with a slice of San Francisco.
ops.append(rect(20, 14, 60, 44, wall(gray(2))))
ops.append(rect(24, 18, 52, 36, wall(cube(2, 3, 5))))
ops.append(line([[24, 40], [75, 34]], wall(cube(4, 1, 0)))) # a bridge, roughly
ops.append(rect(38, 44, 4, 10, wall(gray(6))))
ops.append(rect(56, 40, 4, 14, wall(gray(6))))
# The poster wall: a decade of Alternative Tentacles in thumbtacks.
px, py = 110, 12
for i in range(10):
c = [cube(4, 0, 0), cube(0, 3, 3), cube(4, 3, 0), cube(2, 0, 3), cube(0, 4, 1)][i % 5]
ops.append(rect(px + (i % 5) * 38, py + (i // 5) * 34, 30, 26, wall(c)))
ops.append(rect(px + (i % 5) * 38 + 4, py + (i // 5) * 34 + 4, 22, 4, wall(gray(1))))
# Wood floor.
ops.append(fbm(0, 96, 320, 94, [cube(3, 2, 1), cube(2, 1, 0), cube(3, 2, 1)], 22.0, 2, 41, floor(0)))
for y in range(104, 190, 12):
ops.append(line([[0, y], [319, y]], look(cube(2, 1, 0))))
# Crates of records, south wall (walk around them).
ops.append(rect(0, 168, 90, 22, ink(color=cube(2, 1, 0), ctl={"Set": 1})))
ops.append(voronoi(2, 170, 86, 18, 6, [cube(4, 0, 0), cube(0, 0, 4), cube(4, 4, 0), gray(3), cube(0, 3, 1)], None, 0.6, 42, ink(color=None, ctl=KEEP)))
ops.append(rect(230, 168, 90, 22, ink(color=cube(2, 1, 0), ctl={"Set": 1})))
ops.append(voronoi(232, 170, 86, 18, 6, [cube(3, 0, 3), cube(0, 2, 4), cube(4, 2, 0), gray(4)], None, 0.6, 43, ink(color=None, ctl=KEEP)))
# The desk (Jello's), mid-room, depth-correct.
ops.append(rect(150, 100, 74, 22, ink(color=cube(2, 1, 0), pri={"Set": band_of(122)}, ctl={"Set": 1})))
ops.append(rect(150, 100, 74, 4, ink(color=cube(3, 2, 1), pri={"Set": band_of(122)}, ctl=KEEP)))
ops.append(rect(158, 92, 18, 8, ink(color=gray(13), pri={"Set": band_of(122)}, ctl=KEEP))) # the demo pile
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.",
"ops": ops,
"scale": {"horizon_y": 90, "min_scale": 0.72, "full_y": 186},
"spawn": [60, 140],
"exits": [None, 4, None, 0],
"exit_flags": ["", "signed", "", ""],
"exit_blocked": ["", "Europe doesn't book the unsigned. Talk to Jello.", "", ""],
"hotspots": [
hotspot("poster wall", [108, 8, 200, 74], 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", [18, 12, 64, 48], msgs={
"look": "Fog rolling off the bay, gold light on the bridge. Even the weather here has a record deal.",
}),
hotspot("record crates", [0, 160, 92, 30], msgs={
"look": "Crates of vinyl heading to distributors on four continents. The machine, fed.",
}),
hotspot("desk", [148, 96, 78, 30], msgs={
"look": "The desk: contracts, cassettes, and a rubber tarantula used as a paperweight.",
}),
],
"props": [],
"npcs": [
npc("Jello", "biafra", 246, 132, portrait="biafra-face", wander=14,
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
# =============================================================================
ops = []
# The MODELBEAST matte painting (pics/roskilde.png) is the sky, the towers
# and sixty thousand lighters. Ops only add what the sim needs: walls where
# the crowd is, and the stage you actually stand on.
ops.append(rect(0, 0, 320, 112, ink(color=None, ctl={"Set": 1})))
# Stage-edge shadow line over the painting's seam.
ops.append(rect(0, 110, 320, 6, wall(gray(1))))
# Footlight chase along the stage lip (the ember cycle runs over the art).
for i, x in enumerate(range(4, 316, 12)):
ops.append(rect(x, 113, 8, 4, wall(EMBER[i % 8])))
# The stage itself — you stand ON it. Boards run toward the crowd.
ops.append(fbm(0, 118, 320, 72, [cube(1, 0, 0), cube(2, 1, 0), cube(1, 0, 0)], 20.0, 2, 54, floor(0)))
for y in range(126, 190, 10):
ops.append(line([[0, y], [319, y]], look(cube(0, 0, 0))))
# Monitor wedges facing away.
ops.append(poly([[70, 122], [96, 122], [88, 132], [78, 132]], depth(gray(3), band_of(132))))
ops.append(poly([[200, 122], [226, 122], [218, 132], [208, 132]], depth(gray(3), 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, 40, 320, 70], 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
# =============================================================================
ops = []
ops.append(vgrad(0, 0, 320, 66, [gray(7), gray(5), gray(6)], wall(0)))
ops.append(scatter(0, 8, 320, 30, [gray(2)], 2, 61, look(0))) # distant crows
# Distant farm.
ops.append(rect(30, 44, 26, 20, wall(cube(2, 1, 0))))
ops.append(poly([[26, 46], [43, 34], [60, 46]], wall(cube(1, 0, 0))))
ops.append(ellipse(84, 52, 12, 12, wall(cube(0, 1, 0))))
ops.append(rect(82, 60, 4, 8, wall(cube(2, 1, 0))))
# The field.
ops.append(fbm(0, 66, 320, 124, [cube(1, 2, 0), cube(0, 1, 0), cube(1, 2, 1), cube(2, 2, 0)], 14.0, 3, 62, floor(0)))
# Mud track where a van was definitely driven in the dark.
ops.append(voronoi(60, 120, 200, 40, 16, [cube(2, 1, 0), cube(1, 0, 0), cube(2, 2, 1)], cube(1, 0, 0), 1.2, 63, floor(0)))
ops.append(ellipse(150, 168, 30, 6, water(cube(1, 1, 2))))
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.",
"ops": ops,
"scale": {"horizon_y": 70, "min_scale": 0.55, "full_y": 186},
"spawn": [30, 160],
"exits": [None, None, None, 4],
"hotspots": [
hotspot("farmhouse", [24, 30, 40, 36], msgs={
"look": "A farmhouse minding its own business at a professional level.",
}),
],
"props": [
prop("van", "van", 180, 118,
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", 236, 142, 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():
# strip helper keys serde doesn't know
for h in doc.get("hotspots", []):
h.pop("synonyms", None)
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()