destroyulator/game/scripts/Levels.gd
Monster Robot Party 8e61640c38 LANE10: SITE 7 — the real shop, imported from its own database
The level the whole game started as, and the only one that isn't authored.

robotmonster.party/store/ is assembled procedurally from Postgres (the wowplatter
virtual_* schema, served by recordgod), and this game's sites are already data — so
tools/import_store.py reads the live shop and emits assets/store/shop_floor.json:
the actual 6.66 x 9.51 m floor, 35 racks across 21 archetypes, 278 bins carrying
their real genre labels (HARD TRANCE, $12-$15 EURO/ITALO HOUSE), 21,159 records
of stock. Levels.record_store() loads it; 'store' joins ORDER as the seventh site.

The shop's schema IS the cascade. virtual_crate.rack_id already records which rack
holds which bin, so bins arrive frozen and supported_by their rack and none of the
structure is authored. To carry that, smashables gained two optional generic keys —
'id' and 'supported_by' — so any level can express support as data. Every existing
level is unaffected.

Two reconciliations, because a storefront is a renderer:
- Mesh sizes are a lie: 21 archetypes share a few stand-in GLBs, so each piece
  carries 'fit' (the archetype's real dims from the shop's table) and _glb_piece
  scales the visual to it and builds the collider from it. Also fixed racks being
  flattened to the floor — pos_y is a piece's BOTTOM, which is what sit_on means,
  so the ceiling beams, aircon and wall shelves now stay up where they belong.
- Nothing collides in a renderer: racks that sat inside each other for years get
  separated (weighted, so racks barely move and aisles keep their shape — 3 m of
  correction across 35), rack footprints inset 1.5 cm a side for the few genuinely
  wedged units, and the importer AUDITS ITS OWN OUTPUT with the same test
  probe_overlap runs. Bins are deliberately not separated from their rack: cargo
  inside a container is what the shop means, they spawn frozen, and the audit skips
  frozen bodies because a frozen body cannot be depenetrated across the room.

dev/probe_store.gd: 22 racks hold bins, 255 labels survived, and smashing the rack
carrying 22 bins releases exactly those 22 while the other 256 stay frozen.
All gates green: smoke clean, 7/7 sites 0.00 m/s, overlap CLEAN, LANE9 probes pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 00:49:49 +10:00

800 lines
38 KiB
GDScript

extends RefCounted
class_name Levels
## The workplaces, as data. Each is a Floorplan spec (see Floorplan.gd for the schema).
##
## The point of these being data and not four subclasses: a new site is authorable in
## minutes, they can be diffed, and a generator can later emit the same structure.
##
## They are deliberately NOT reskins. Each differs in the three things a player actually
## reads — the palette, the light, and what the walls are made of:
##
## SCRANTON ..... magnolia, grey carpet, drop ceiling, fluorescent troffers, daylight
## down one glazed wall. The baseline office.
## PAWNEE ....... civic beige and blue-grey, low partitions everywhere, a public
## counter, pinboards. Municipal, over-partitioned, no privacy.
## THE HOUSE .... timber floor, white walls, 3 m ceiling, pendant lights, a glass wall
## onto a pool. Nobody here has an office; they work at a dining table.
## THE BASEMENT . concrete, no windows at all, exposed services, bare strip lights, a
## wall of server racks. The light is green and everything is junk.
##
## Godot 4.7 GDScript 2.0.
const ORDER := ["scranton", "pawnee", "house", "basement", "grocer", "backrooms", "store"]
static func get_level(id: String) -> Dictionary:
match id:
"pawnee": return pawnee()
"house": return house()
"basement": return basement()
"backrooms": return backrooms()
"grocer": return grocer()
"store": return record_store()
return scranton()
static func next_id(id: String) -> String:
var i := ORDER.find(id)
return ORDER[(i + 1) % ORDER.size()] if i >= 0 else ORDER[0]
# ================================================================ LEVEL 01
static func scranton() -> Dictionary:
return {
"name": "SCRANTON BRANCH",
"subtitle": "paper company · 2nd floor",
"bounds": {"x0": -11.0, "x1": 11.0, "z0": -8.0, "z1": 8.0, "h": 2.75},
"palette": {
"floor": Color(0.30, 0.30, 0.32), "wall": Color(0.80, 0.78, 0.72),
"ceiling": Color(0.88, 0.88, 0.86), "trim": Color(0.13, 0.12, 0.13),
"partition": Color(0.44, 0.44, 0.40), "wood": Color(0.42, 0.29, 0.17),
"accent": Color(0.36, 0.42, 0.50),
},
"floor_style": "carpet",
"ceiling": "tile",
"walls": [
# manager's office — half solid, half glass, so you can be watched through it
{"a": "x", "at": -4.4, "from": -11.0, "to": -6.6, "door": -9.2},
{"a": "z", "at": -6.6, "from": -8.0, "to": -4.4, "style": "glass_top"},
# conference room
{"a": "x", "at": -4.4, "from": -6.6, "to": -0.4, "door": -3.4},
{"a": "z", "at": -0.4, "from": -8.0, "to": -4.4},
# break room
{"a": "z", "at": 5.4, "from": -1.2, "to": 8.0, "door": 2.2},
{"a": "x", "at": -1.2, "from": 5.4, "to": 11.0},
# copier alcove
{"a": "z", "at": 3.6, "from": -1.6, "to": 1.0},
# reception screen
{"a": "z", "at": -6.9, "from": 4.6, "to": 8.0, "door": 6.8},
],
"windows": [{"a": "z", "at": 11.0, "from": -8.0, "to": 8.0,
"y0": 0.85, "y1": 2.35, "blinds": true}],
"lights": {"xs": [-8.5, -4.6, -0.7, 3.2, 7.1], "zs": [-6.2, -2.6, 1.0, 4.6],
"color": Color(1.0, 0.98, 0.93), "energy": 1.9, "style": "troffer"},
"sun": {"color": Color(0.82, 0.88, 1.0), "energy": 0.7,
"rot": Vector3(-30, -104, 0)},
"env": {"bg": Color(0.55, 0.62, 0.72), "ambient": Color(0.50, 0.50, 0.50),
"ambient_energy": 0.5},
"clusters": [Vector2(-4.6, -2.0), Vector2(-4.6, 2.8),
Vector2(-0.4, -2.0), Vector2(-0.4, 2.8)],
"cluster_gap": 0.78,
"slabs": [
# reception counter
{"size": Vector3(0.70, 1.10, 3.0), "at": Vector3(-9.9, 0.55, 5.6), "mat": "wood"},
{"size": Vector3(0.92, 0.08, 3.2), "at": Vector3(-9.9, 1.14, 5.6), "mat": "trim"},
{"size": Vector3(1.8, 1.10, 0.70), "at": Vector3(-9.6, 0.55, 4.45), "mat": "wood"},
# conference table + whiteboard
{"size": Vector3(3.9, 0.09, 1.35), "at": Vector3(-3.5, 0.74, -6.2), "mat": "wood"},
{"size": Vector3(0.16, 0.72, 0.90), "at": Vector3(-5.1, 0.36, -6.2), "mat": "trim"},
{"size": Vector3(0.16, 0.72, 0.90), "at": Vector3(-1.9, 0.36, -6.2), "mat": "trim"},
{"size": Vector3(2.5, 1.25, 0.03), "at": Vector3(-3.5, 1.6, -7.93), "mat": "trim"},
{"size": Vector3(2.4, 1.15, 0.05), "at": Vector3(-3.5, 1.6, -7.90), "mat": "ceiling"},
# break room kitchen + tables
{"size": Vector3(0.62, 0.88, 3.4), "at": Vector3(10.58, 0.44, 4.0), "mat": "wood"},
{"size": Vector3(0.70, 0.06, 3.5), "at": Vector3(10.58, 0.90, 4.0), "mat": "trim"},
{"size": Vector3(1.05, 0.07, 1.05), "at": Vector3(7.4, 0.73, 0.4), "mat": "ceiling"},
{"size": Vector3(0.12, 0.70, 0.12), "at": Vector3(7.4, 0.36, 0.4), "mat": "trim"},
{"size": Vector3(1.05, 0.07, 1.05), "at": Vector3(7.4, 0.73, 3.0), "mat": "ceiling"},
{"size": Vector3(0.12, 0.70, 0.12), "at": Vector3(7.4, 0.36, 3.0), "mat": "trim"},
# warehouse roller door
{"size": Vector3(2.6, 2.4, 0.12), "at": Vector3(7.8, 1.2, -7.94), "mat": "trim"},
],
"props": [
{"glb": "store-column-white", "at": Vector3(-2.2, 0.0, -3.4)},
{"glb": "store-column-white", "at": Vector3(2.2, 0.0, -3.4)},
{"glb": "store-aircon", "at": Vector3(2.6, 2.35, -7.9)},
{"glb": "office-plant", "at": Vector3(-6.4, 0.0, 6.6)},
{"glb": "office-plant", "at": Vector3(2.9, 0.0, 6.4)},
{"glb": "vending-machine", "at": Vector3(6.3, 0.0, 7.4), "yaw": PI},
{"glb": "vending-machine", "at": Vector3(7.3, 0.0, 7.4), "yaw": PI},
{"glb": "microwave", "at": Vector3(10.55, 0.93, 5.1), "yaw": -PI / 2},
],
"chairs": [
{"at": Vector3(-7.6, 0.0, 5.5), "yaw": -PI / 2},
{"at": Vector3(-7.6, 0.0, 6.35), "yaw": -PI / 2},
{"at": Vector3(-7.6, 0.0, 7.2), "yaw": -PI / 2},
{"at": Vector3(-4.85, 0.0, -7.35), "yaw": 0.0},
{"at": Vector3(-3.95, 0.0, -7.35), "yaw": 0.0},
{"at": Vector3(-3.05, 0.0, -7.35), "yaw": 0.0},
{"at": Vector3(-2.15, 0.0, -7.35), "yaw": 0.0},
{"at": Vector3(-4.85, 0.0, -5.05), "yaw": PI},
{"at": Vector3(-3.95, 0.0, -5.05), "yaw": PI},
{"at": Vector3(-3.05, 0.0, -5.05), "yaw": PI},
{"at": Vector3(-2.15, 0.0, -5.05), "yaw": PI},
{"at": Vector3(-8.8, 0.0, -7.1), "yaw": 0.0},
{"at": Vector3(-9.6, 0.0, -5.1), "yaw": PI},
{"at": Vector3(8.0, 0.0, 1.13), "yaw": 2.0},
{"at": Vector3(7.7, 0.0, -0.49), "yaw": 4.1},
{"at": Vector3(6.5, 0.0, 0.56), "yaw": 0.2},
{"at": Vector3(8.0, 0.0, 3.73), "yaw": 2.0},
{"at": Vector3(7.7, 0.0, 2.11), "yaw": 4.1},
{"at": Vector3(6.5, 0.0, 3.16), "yaw": 0.2},
],
"copier": {"at": Vector3(3.0, 0.0, -0.3), "yaw": -PI / 2},
"smashables": [
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -1.6)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -0.9)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -0.2)},
{"glb": "water-cooler", "kind": "steel", "at": Vector2(-6.2, 3.4)},
{"glb": "water-cooler", "kind": "steel", "at": Vector2(5.9, -0.6)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.3, 5.2)},
# an archive box is 1.35 m across, so a "couple of boxes dumped together" is a
# PILE, not two of them side by side inside each other
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(1.6, 5.4)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(1.74, 5.26),
"sit_on": 0.40, "yaw": 0.10},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(6.4, -3.3)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(6.52, -3.44),
"sit_on": 0.40, "yaw": -0.08},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-9.7, 1.2)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(-6.0, -1.0)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(1.0, 1.0)},
{"glb": "desk-phone", "kind": "plastic", "at": Vector2(-9.8, 5.0), "sit_on": 1.18},
{"glb": "rack", "kind": "wood", "at": Vector2(9.4, -6.6), "frozen": true},
{"glb": "rack", "kind": "wood", "at": Vector2(9.4, -4.3), "frozen": true},
{"glb": "turntable", "kind": "wood", "at": Vector2(6.9, -5.9)},
{"glb": "guillotine", "kind": "steel", "at": Vector2(4.5, 1.2)},
{"glb": "stapler", "kind": "steel", "at": Vector2(-5.25, -2.78), "sit_on": 0.75},
],
"records": [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)],
"gauntlet": {"at": Vector3(0.0, 0.0, -3.9), "yaw": PI,
"stand": Vector3(0.0, 0.0, -1.7), "shredder": Vector3(2.7, 0.0, -2.4)},
"spawn": {"at": Vector3(-8.6, 0.0, 7.2), "yaw": deg_to_rad(196.0)},
}
# ================================================================ LEVEL 02
static func pawnee() -> Dictionary:
return {
"name": "PAWNEE CITY HALL",
"subtitle": "parks & recreation · 4th floor",
"bounds": {"x0": -10.0, "x1": 10.0, "z0": -7.0, "z1": 7.5, "h": 2.70},
"palette": {
"floor": Color(0.34, 0.36, 0.40), "wall": Color(0.84, 0.80, 0.68),
"ceiling": Color(0.90, 0.89, 0.85), "trim": Color(0.30, 0.22, 0.14),
"partition": Color(0.52, 0.50, 0.42), "wood": Color(0.50, 0.34, 0.19),
"accent": Color(0.16, 0.36, 0.28),
},
"floor_style": "carpet",
"ceiling": "tile",
"walls": [
# the department head's office, and the meeting room beside it
{"a": "x", "at": -3.4, "from": -10.0, "to": -5.6, "door": -7.0},
{"a": "z", "at": -5.6, "from": -7.0, "to": -3.4, "style": "glass_top"},
{"a": "x", "at": -3.4, "from": -5.6, "to": 0.4, "door": -1.6},
{"a": "z", "at": 0.4, "from": -7.0, "to": -3.4},
# records room, and the corridor wall that makes this place a warren
{"a": "z", "at": 4.6, "from": -7.0, "to": -2.0, "door": -4.4},
{"a": "x", "at": -2.0, "from": 4.6, "to": 10.0},
# the public counter divides the front third from everything else
{"a": "x", "at": 3.6, "from": -10.0, "to": 3.0, "door": -8.0, "style": "half"},
],
"windows": [{"a": "x", "at": 7.5, "from": -10.0, "to": 10.0,
"y0": 0.95, "y1": 2.30, "blinds": true}],
"lights": {"xs": [-7.5, -3.4, 0.7, 4.8, 8.4], "zs": [-5.2, -1.6, 2.0, 5.6],
"color": Color(1.0, 0.96, 0.86), "energy": 1.8, "style": "troffer",
"key_energy": 1.4},
"sun": {"color": Color(0.90, 0.88, 0.78), "energy": 0.55,
"rot": Vector3(-34, 20, 0)},
"env": {"bg": Color(0.62, 0.66, 0.62), "ambient": Color(0.50, 0.48, 0.44),
"ambient_energy": 0.52, "exposure": 1.02},
"clusters": [Vector2(-6.0, 0.4), Vector2(-1.4, 0.4), Vector2(2.6, 0.4)],
"cluster_gap": 0.78,
"slabs": [
# the public counter itself — the thing between you and the citizens
{"size": Vector3(6.6, 1.06, 0.66), "at": Vector3(-1.2, 0.53, 5.4), "mat": "wood"},
{"size": Vector3(6.9, 0.07, 0.86), "at": Vector3(-1.2, 1.10, 5.4), "mat": "trim"},
{"size": Vector3(0.66, 1.06, 2.2), "at": Vector3(2.1, 0.53, 6.4), "mat": "wood"},
# meeting room table
{"size": Vector3(2.9, 0.09, 1.25), "at": Vector3(-2.6, 0.74, -5.2), "mat": "wood"},
{"size": Vector3(0.14, 0.72, 0.84), "at": Vector3(-3.7, 0.36, -5.2), "mat": "trim"},
{"size": Vector3(0.14, 0.72, 0.84), "at": Vector3(-1.5, 0.36, -5.2), "mat": "trim"},
# pinboards — municipal offices are 40% pinboard by surface area
{"size": Vector3(2.2, 1.20, 0.05), "at": Vector3(5.0, 1.65, -6.93), "mat": "accent"},
{"size": Vector3(2.3, 1.30, 0.03), "at": Vector3(5.0, 1.65, -6.90), "mat": "trim"},
{"size": Vector3(0.05, 1.20, 2.6), "at": Vector3(-9.93, 1.62, 2.0), "mat": "accent"},
{"size": Vector3(0.03, 1.30, 2.7), "at": Vector3(-9.90, 1.62, 2.0), "mat": "trim"},
# department head's desk
{"size": Vector3(1.7, 0.08, 0.85), "at": Vector3(-7.4, 0.74, -5.4), "mat": "wood"},
{"size": Vector3(1.5, 0.66, 0.72), "at": Vector3(-7.4, 0.35, -5.4), "mat": "wood"},
],
"props": [
{"glb": "office-plant", "at": Vector3(-9.3, 0.0, 6.6)},
{"glb": "office-plant", "at": Vector3(3.2, 0.0, -2.6)},
{"glb": "office-plant", "at": Vector3(-9.3, 0.0, -1.0)},
{"glb": "sofa", "at": Vector3(-6.6, 0.0, 6.5), "yaw": PI},
{"glb": "vending-machine", "at": Vector3(9.2, 0.0, 6.6), "yaw": -PI / 2},
{"glb": "microwave", "at": Vector3(9.3, 0.90, 1.2), "yaw": -PI / 2},
{"glb": "store-aircon", "at": Vector3(-6.0, 2.30, -6.9)},
],
"chairs": [
{"at": Vector3(-7.4, 0.0, -4.2), "yaw": PI},
{"at": Vector3(-8.6, 0.0, -6.3), "yaw": 0.0},
{"at": Vector3(-3.5, 0.0, -6.3), "yaw": 0.0},
{"at": Vector3(-2.6, 0.0, -6.3), "yaw": 0.0},
{"at": Vector3(-1.7, 0.0, -6.3), "yaw": 0.0},
{"at": Vector3(-3.5, 0.0, -4.1), "yaw": PI},
{"at": Vector3(-2.6, 0.0, -4.1), "yaw": PI},
{"at": Vector3(-1.7, 0.0, -4.1), "yaw": PI},
{"at": Vector3(-4.0, 0.0, 6.4), "yaw": PI},
{"at": Vector3(0.4, 0.0, 6.5), "yaw": PI},
{"at": Vector3(1.2, 0.0, 6.5), "yaw": PI},
],
"copier": {"at": Vector3(6.4, 0.0, 2.4), "yaw": PI},
"smashables": [
# a cabinet is 0.80 m wide; a row of them 0.70 m apart is one cabinet
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(5.4, -3.2)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(6.26, -3.2)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(7.12, -3.2)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(7.98, -3.2)},
{"glb": "water-cooler", "kind": "steel", "at": Vector2(3.9, 4.2)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.6, -5.4)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.74, -5.26),
"sit_on": 0.40, "yaw": 0.12},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-4.8, 3.0)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(-8.8, -3.9)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(4.4, 0.9)},
{"glb": "guillotine", "kind": "steel", "at": Vector2(6.4, 3.6), "sit_on": 0.0},
{"glb": "desk-phone", "kind": "plastic", "at": Vector2(-6.6, -5.4), "sit_on": 0.78},
{"glb": "crt-monitor", "kind": "glass", "at": Vector2(-7.6, -5.2), "sit_on": 0.78},
],
"records": [],
"gauntlet": {"at": Vector3(7.2, 0.0, -5.8), "yaw": PI,
"stand": Vector3(7.2, 0.0, -3.9), "shredder": Vector3(5.2, 0.0, -6.2)},
"spawn": {"at": Vector3(-8.0, 0.0, 6.6), "yaw": deg_to_rad(178.0)},
}
# ================================================================ LEVEL 03
static func house() -> Dictionary:
return {
"name": "THE INCUBATOR",
"subtitle": "seed round · someone's house",
"bounds": {"x0": -10.0, "x1": 10.0, "z0": -8.0, "z1": 8.0, "h": 3.05},
"palette": {
"floor": Color(0.46, 0.33, 0.20), "wall": Color(0.92, 0.91, 0.88),
"ceiling": Color(0.95, 0.95, 0.94), "trim": Color(0.18, 0.17, 0.17),
"partition": Color(0.72, 0.72, 0.70), "wood": Color(0.40, 0.27, 0.16),
"accent": Color(0.22, 0.44, 0.62),
},
"floor_style": "timber",
"ceiling": "flat",
"walls": [
# only two real walls: a bedroom-turned-office and the bathroom nobody cleans
{"a": "z", "at": -4.6, "from": -8.0, "to": -2.4, "door": -4.0},
{"a": "x", "at": -2.4, "from": -10.0, "to": -4.6, "door": -7.4},
{"a": "z", "at": 4.0, "from": -8.0, "to": -5.2, "door": -6.6},
# the kitchen island's back wall
{"a": "x", "at": -1.0, "from": 4.0, "to": 10.0, "style": "half"},
],
"windows": [{"a": "x", "at": 8.0, "from": -10.0, "to": 10.0,
"y0": 0.35, "y1": 2.70, "blinds": false}],
"lights": {"xs": [-6.5, -1.5, 3.5, 7.5], "zs": [-5.0, 0.0, 5.0],
"color": Color(1.0, 0.90, 0.76), "energy": 2.4, "style": "pendant",
"key_energy": 1.7},
"sun": {"color": Color(1.0, 0.94, 0.80), "energy": 1.5,
"rot": Vector3(-46, -96, 0)},
"env": {"bg": Color(0.70, 0.80, 0.90), "ambient": Color(0.56, 0.55, 0.52),
"ambient_energy": 0.62, "exposure": 1.05},
# nobody has a desk; they work at the big table
"clusters": [Vector2(1.6, -1.4), Vector2(1.6, 2.6)],
"cluster_gap": 0.80,
"slabs": [
# kitchen island + counter
{"size": Vector3(2.6, 0.92, 1.05), "at": Vector3(4.2, 0.46, 5.6), "mat": "wood"},
{"size": Vector3(2.8, 0.08, 1.20), "at": Vector3(4.2, 0.95, 5.6), "mat": "ceiling"},
{"size": Vector3(6.0, 0.90, 0.65), "at": Vector3(5.0, 0.45, 7.5), "mat": "ceiling"},
{"size": Vector3(6.2, 0.06, 0.75), "at": Vector3(5.0, 0.92, 7.5), "mat": "trim"},
# the long table everyone actually works at
{"size": Vector3(1.5, 0.09, 4.6), "at": Vector3(-4.4, 0.75, 0.6), "mat": "wood"},
{"size": Vector3(0.12, 0.72, 0.12), "at": Vector3(-4.9, 0.36, -1.4), "mat": "trim"},
{"size": Vector3(0.12, 0.72, 0.12), "at": Vector3(-3.9, 0.36, 2.6), "mat": "trim"},
# whiteboard, obviously
{"size": Vector3(0.05, 1.40, 3.0), "at": Vector3(-9.93, 1.75, 1.0), "mat": "ceiling"},
{"size": Vector3(0.03, 1.50, 3.1), "at": Vector3(-9.90, 1.75, 1.0), "mat": "trim"},
# a low bookshelf run
{"size": Vector3(0.42, 0.90, 3.2), "at": Vector3(-9.7, 0.45, -5.0), "mat": "wood"},
# pool decking visible through the glass
{"size": Vector3(7.0, 0.10, 16.0), "at": Vector3(12.6, -0.05, 0.0),
"mat": "ceiling", "collide": false},
],
"props": [
{"glb": "sofa", "at": Vector3(-6.6, 0.0, 5.4), "yaw": -PI / 2},
{"glb": "sofa", "at": Vector3(-2.0, 0.0, 6.6), "yaw": PI},
{"glb": "office-plant", "at": Vector3(-0.6, 0.0, 3.2)},
{"glb": "office-plant", "at": Vector3(7.2, 0.0, -6.4)},
{"glb": "office-plant", "at": Vector3(-9.2, 0.0, 7.2)},
{"glb": "microwave", "at": Vector3(5.4, 0.95, 7.4), "yaw": PI},
{"glb": "server-rack", "at": Vector3(9.0, 0.0, -6.8), "yaw": -PI / 2},
],
"chairs": [
{"at": Vector3(-6.0, 0.0, -1.4), "yaw": -PI / 2},
{"at": Vector3(-6.0, 0.0, -0.2), "yaw": -PI / 2},
{"at": Vector3(-6.0, 0.0, 1.4), "yaw": -PI / 2},
{"at": Vector3(-6.0, 0.0, 2.6), "yaw": -PI / 2},
{"at": Vector3(-2.8, 0.0, -1.0), "yaw": PI / 2},
{"at": Vector3(-2.8, 0.0, 1.8), "yaw": PI / 2},
{"at": Vector3(4.2, 0.0, 4.2), "yaw": 0.0},
{"at": Vector3(5.2, 0.0, 4.2), "yaw": 0.0},
{"at": Vector3(-7.6, 0.0, -6.4), "yaw": 0.2},
],
"copier": {"at": Vector3(-8.6, 0.0, -3.4), "yaw": PI / 2},
"smashables": [
{"glb": "crt-monitor", "kind": "glass", "at": Vector2(-4.4, -2.2)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.4, 2.2)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.54, 2.06),
"sit_on": 0.40, "yaw": -0.14},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-8.2, 6.4)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(-2.6, -3.2)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(6.2, 3.0)},
{"glb": "water-cooler", "kind": "steel", "at": Vector2(1.0, 6.8)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(-9.3, -1.6)},
{"glb": "turntable", "kind": "wood", "at": Vector2(-9.4, -6.6)},
{"glb": "desk-phone", "kind": "plastic", "at": Vector2(4.0, 5.6), "sit_on": 0.99},
{"glb": "guillotine", "kind": "steel", "at": Vector2(-9.4, -3.4)},
],
"records": [Vector2(-8.4, -7.0), Vector2(-7.6, -7.0)],
"gauntlet": {"at": Vector3(-7.2, 0.0, -7.2), "yaw": PI,
"stand": Vector3(-7.2, 0.0, -5.2), "shredder": Vector3(-5.0, 0.0, -6.8)},
"spawn": {"at": Vector3(-1.0, 0.0, 7.0), "yaw": deg_to_rad(186.0)},
}
# ================================================================ LEVEL 04
static func basement() -> Dictionary:
return {
"name": "SUB-LEVEL 4",
"subtitle": "i.t. department · no windows",
"bounds": {"x0": -12.0, "x1": 12.0, "z0": -6.0, "z1": 6.0, "h": 2.90},
"palette": {
"floor": Color(0.22, 0.22, 0.23), "wall": Color(0.50, 0.53, 0.46),
"ceiling": Color(0.20, 0.20, 0.21), "trim": Color(0.10, 0.10, 0.10),
"partition": Color(0.34, 0.36, 0.32), "wood": Color(0.30, 0.20, 0.12),
"accent": Color(0.52, 0.10, 0.09),
},
"floor_style": "concrete",
"ceiling": "flat",
"walls": [
# the boss's glass box, bolted into a room that never wanted one
{"a": "z", "at": 4.6, "from": -6.0, "to": 1.2, "door": -1.0,
"style": "glass_top"},
{"a": "x", "at": 1.2, "from": 4.6, "to": 12.0},
# server room behind a security door
{"a": "z", "at": -6.4, "from": -6.0, "to": 6.0, "door": 1.0},
# a stub wall someone put up and nobody took down
{"a": "x", "at": -1.4, "from": -4.0, "to": -1.6},
],
"windows": [], # it is a basement
"lights": {"xs": [-9.0, -4.5, 0.0, 4.5, 9.0], "zs": [-3.6, 0.4, 4.0],
"color": Color(0.86, 0.96, 0.84), "energy": 2.0, "style": "strip",
"key_energy": 1.1},
"env": {"bg": Color(0.06, 0.07, 0.06), "ambient": Color(0.32, 0.36, 0.32),
"ambient_energy": 0.40, "exposure": 1.08},
"clusters": [Vector2(3.6, 0.0)],
"cluster_gap": 0.82,
"slabs": [
# exposed services, because there's no ceiling to hide them
{"size": Vector3(24.0, 0.22, 0.22), "at": Vector3(0.0, 2.70, -3.0),
"mat": "trim", "collide": false},
{"size": Vector3(24.0, 0.16, 0.16), "at": Vector3(0.0, 2.62, 1.4),
"mat": "trim", "collide": false},
{"size": Vector3(24.0, 0.30, 0.10), "at": Vector3(0.0, 2.78, 4.2),
"mat": "trim", "collide": false},
# boss's desk in the glass box
{"size": Vector3(1.8, 0.08, 0.86), "at": Vector3(8.0, 0.74, 5.4), "mat": "wood"},
{"size": Vector3(1.6, 0.66, 0.74), "at": Vector3(8.0, 0.35, 5.4), "mat": "wood"},
# workbench along the back
{"size": Vector3(5.0, 0.08, 0.80), "at": Vector3(-6.0, 0.90, -5.5), "mat": "trim"},
{"size": Vector3(0.10, 0.88, 0.70), "at": Vector3(-8.4, 0.44, -5.5), "mat": "trim"},
{"size": Vector3(0.10, 0.88, 0.70), "at": Vector3(-3.6, 0.44, -5.5), "mat": "trim"},
# the famous red door
{"size": Vector3(1.10, 2.10, 0.10), "at": Vector3(1.0, 1.05, -6.35),
"mat": "accent"},
],
"props": [
{"glb": "server-rack", "at": Vector3(-10.6, 0.0, -4.4), "yaw": PI / 2},
{"glb": "server-rack", "at": Vector3(-10.6, 0.0, -2.4), "yaw": PI / 2},
{"glb": "server-rack", "at": Vector3(-10.6, 0.0, -0.4), "yaw": PI / 2},
{"glb": "server-rack", "at": Vector3(-10.6, 0.0, 1.6), "yaw": PI / 2},
{"glb": "sofa", "at": Vector3(-4.0, 0.0, 3.4), "yaw": 0.15},
{"glb": "office-plant", "at": Vector3(11.2, 0.0, 5.4)},
{"glb": "vending-machine", "at": Vector3(-0.4, 0.0, 5.4), "yaw": PI},
{"glb": "microwave", "at": Vector3(-5.0, 0.95, -5.5), "yaw": PI},
],
"chairs": [
{"at": Vector3(8.0, 0.0, 4.1), "yaw": PI},
{"at": Vector3(6.6, 0.0, 5.9), "yaw": 0.3},
{"at": Vector3(-6.6, 0.0, -4.2), "yaw": PI},
{"at": Vector3(-1.0, 0.0, 2.2), "yaw": 1.2},
],
"copier": {"at": Vector3(0.4, 0.0, -4.4), "yaw": 0.0},
"smashables": [
{"glb": "crt-monitor", "kind": "glass", "at": Vector2(-7.0, -5.5), "sit_on": 0.94},
{"glb": "crt-monitor", "kind": "glass", "at": Vector2(-5.0, -4.6)},
{"glb": "crt-monitor", "kind": "glass", "at": Vector2(-8.4, -3.2)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(10.8, 3.0)},
{"glb": "filing-cabinet", "kind": "steel", "at": Vector2(10.8, 2.2)},
# nobody in an IT basement puts a box down on its own
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.6, -4.8)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.46, -4.92),
"sit_on": 0.40, "yaw": 0.09},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.58, -4.70),
"sit_on": 0.80, "yaw": -0.16},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(5.6, -4.8)},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(5.72, -4.94),
"sit_on": 0.40, "yaw": -0.11},
{"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(5.58, -4.68),
"sit_on": 0.80, "yaw": 0.20},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(2.0, 2.6)},
{"glb": "wastebin", "kind": "plastic", "at": Vector2(9.0, 4.4)},
{"glb": "water-cooler", "kind": "steel", "at": Vector2(0.6, 2.2)},
{"glb": "shredder", "kind": "plastic", "at": Vector2(-2.2, -2.0)},
{"glb": "desk-phone", "kind": "plastic", "at": Vector2(8.3, 5.4), "sit_on": 0.78},
{"glb": "turntable", "kind": "wood", "at": Vector2(-9.0, 4.4)},
{"glb": "guillotine", "kind": "steel", "at": Vector2(-6.0, -4.2)},
],
"records": [Vector2(-8.2, 5.0), Vector2(-7.5, 5.0)],
"gauntlet": {"at": Vector3(-3.4, 0.0, -3.4), "yaw": PI,
"stand": Vector3(-3.4, 0.0, -1.5), "shredder": Vector3(-2.2, 0.0, -2.0)},
"spawn": {"at": Vector3(1.0, 0.0, -5.2), "yaw": deg_to_rad(0.0)},
}
# ================================================================ LEVEL 00
## THE BACKROOMS. The only site whose walls are GENERATED rather than authored, which is
## both the honest test of the Floorplan spec and the right call for the fiction: this
## place is supposed to feel endless and samey, and a seeded RNG produces that better
## than a human placing slabs ever would.
##
## Layout follows the reference photographs rather than a maze algorithm — the Backrooms
## aren't corridors, they're big offset SLABS sitting in an open floor with gaps between
## them, so that's what this emits: partial wall runs along a coarse grid, plus pillars.
## A proper maze would be legible; this is deliberately not.
##
## Everything smashable here is INVISIBLE (see Dust.gd). You find it by walking into it.
const BR_SEED := 0xBACC
const BR_HALF := 19.0
const BR_H := 2.55 # low. lower than any real office.
static func backrooms() -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = BR_SEED
var walls: Array = []
# Wall runs on a 4.6 m lattice. Each line gets a few segments with gaps between, so
# you can always get through somewhere but never see far.
# Density matters more than it looks. The first pass put a run on every lattice line
# in both axes and the result had NO sightline longer than a few metres — which kills
# the entity outright, because a stalker you can never see can never be stared at. The
# reference photographs are mostly open floor with occasional slabs, so: skip roughly
# a third of the lines, and leave big gaps.
var lines := [-13.8, -9.2, -4.6, 0.0, 4.6, 9.2, 13.8]
for axis in ["x", "z"]:
for at in lines:
if rng.randf() < 0.34:
continue # this line is simply open floor
var cursor := -BR_HALF
while cursor < BR_HALF - 2.0:
var gap := rng.randf_range(4.5, 12.0)
cursor += gap
if cursor >= BR_HALF - 2.0:
break
var run := rng.randf_range(3.0, 7.5)
var end: float = minf(cursor + run, BR_HALF)
if end - cursor > 1.4:
walls.append({"a": axis, "at": at, "from": cursor, "to": end})
cursor = end
# freestanding pillars, because every Backrooms photo has one in the way
var slabs: Array = []
for i in 16:
var px := rng.randf_range(-BR_HALF + 2.0, BR_HALF - 2.0)
var pz := rng.randf_range(-BR_HALF + 2.0, BR_HALF - 2.0)
slabs.append({"size": Vector3(0.62, BR_H, 0.62),
"at": Vector3(px, BR_H * 0.5, pz), "mat": "wall"})
# skirting-height trim run around the outer wall, the one detail the photos all share
for s in [-1.0, 1.0]:
slabs.append({"size": Vector3(BR_HALF * 2.0, 0.12, 0.06),
"at": Vector3(0.0, 0.06, s * (BR_HALF - 0.03)), "mat": "trim",
"collide": false})
slabs.append({"size": Vector3(0.06, 0.12, BR_HALF * 2.0),
"at": Vector3(s * (BR_HALF - 0.03), 0.06, 0.0), "mat": "trim",
"collide": false})
# a dense grid of buzzing troffers — the light IS the horror
var lx: Array = []
var lz: Array = []
var v := -BR_HALF + 2.3
while v < BR_HALF:
lx.append(v)
lz.append(v)
v += 3.05
# sparse, wrong office objects. All invisible until you dust them.
var smash: Array = []
var kinds := [["office-chair", "plastic"], ["wastebin", "plastic"],
["cardboard-box", "cardboard"], ["filing-cabinet", "steel"],
["crt-monitor", "glass"], ["desk-phone", "plastic"],
["water-cooler", "steel"], ["office-plant", "wood"]]
# dense enough that a single cloud finds you two or three things — the level is
# a search, and a search with nothing in it is just a walk
# Rejection sampling: the widest thing on this list is a 1.35 m cardboard box, so
# anything landing inside 1.6 m of a spot already taken gets redrawn. Two props
# spawned inside each other are a filing cabinet launched across the maze on frame
# one, which in a level built on silence is not the surprise you wanted.
var taken: Array = []
var tries := 0
while smash.size() < 78 and tries < 4000:
tries += 1
var spot := Vector2(rng.randf_range(-BR_HALF + 2.5, BR_HALF - 2.5),
rng.randf_range(-BR_HALF + 2.5, BR_HALF - 2.5))
var clear := true
for t in taken:
if (spot - (t as Vector2)).length() < 1.6:
clear = false
break
if not clear:
continue
taken.append(spot)
var k: Array = kinds[rng.randi() % kinds.size()]
smash.append({
"glb": String(k[0]), "kind": String(k[1]), "at": spot,
"yaw": rng.randf_range(0.0, TAU),
})
return {
"name": "LEVEL 0",
"subtitle": "you noclipped out of reality",
"invisible": true, # Dust.gd hides every smashable here
"bounds": {"x0": -BR_HALF, "x1": BR_HALF, "z0": -BR_HALF, "z1": BR_HALF,
"h": BR_H},
"palette": {
# the whole point is that there is only one colour
"floor": Color(0.52, 0.47, 0.30), "wall": Color(0.76, 0.72, 0.42),
"ceiling": Color(0.80, 0.77, 0.55), "trim": Color(0.62, 0.58, 0.34),
"partition": Color(0.74, 0.70, 0.40), "wood": Color(0.62, 0.55, 0.32),
"accent": Color(0.70, 0.66, 0.38),
},
"floor_style": "carpet",
"ceiling": "tile",
"walls": walls,
"windows": [], # there is no outside
"slabs": slabs,
"lights": {"xs": lx, "zs": lz, "color": Color(1.0, 0.97, 0.80),
"energy": 1.15, "style": "troffer", "key_energy": 0.9},
"env": {"bg": Color(0.30, 0.28, 0.16), "ambient": Color(0.62, 0.58, 0.36),
"ambient_energy": 0.70, "exposure": 1.12},
"clusters": [],
"props": [],
"chairs": [],
"smashables": smash,
"records": [],
"gauntlet": {"at": Vector3(0.0, 0.0, -2.2), "yaw": PI,
"stand": Vector3(0.0, 0.0, 0.2), "shredder": Vector3(3.0, 0.0, -1.0)},
"spawn": {"at": Vector3(0.0, 0.0, 16.5), "yaw": PI},
}
# ================================================================ THE GROCER
## A greengrocer, and the only site whose contents are mostly SOFT.
##
## Everything else in the game shatters; here the stock bursts. That one material change
## rewrites the level: produce squashes instead of breaking, a pyramid crushes its own
## bottom layer as it collapses, and the floor fills with juice you then slip in. The
## glass bottles behind the juice bar are the exception, and the contrast is the point —
## one shelf in the room still rewards a proper swing.
##
## Bright, clean, warm track lighting on a pale floor, because the whole level is about
## making a mess and a mess needs somewhere clean to happen.
static func grocer() -> Dictionary:
var stacks: Array = []
# Angled island displays. `at` is the TOP surface a pyramid piles onto; Main builds
# the pyramid itself so the physics settles honestly rather than being placed.
var islands := [
[Vector3(-5.4, 0.95, -2.2), "apple", 5],
[Vector3(-5.4, 0.95, 1.4), "orange", 5],
[Vector3(-2.0, 0.95, -2.2), "tomato", 5],
[Vector3(-2.0, 0.95, 1.4), "apple", 4],
[Vector3(1.4, 0.95, -2.2), "cabbage", 3],
[Vector3(1.4, 0.95, 1.4), "orange", 4],
[Vector3(4.8, 0.80, -0.4), "melon", 3],
]
for i in islands:
stacks.append({"at": i[0], "item": String(i[1]), "rows": int(i[2])})
var slabs: Array = []
# the island tables themselves: a plinth, a timber top, and a raised back edge so a
# pyramid has something to fail against
for i in islands:
var p: Vector3 = i[0]
var big: bool = String(i[1]) == "melon"
var w := 2.4 if big else 1.5
var d := 1.9 if big else 1.5
var h: float = p.y
slabs.append({"size": Vector3(w, h - 0.10, d),
"at": Vector3(p.x, (h - 0.10) * 0.5, p.z), "mat": "trim"})
slabs.append({"size": Vector3(w + 0.14, 0.10, d + 0.14),
"at": Vector3(p.x, h - 0.05, p.z), "mat": "wood"})
for s in [-1.0, 1.0]:
slabs.append({"size": Vector3(w + 0.14, 0.10, 0.06),
"at": Vector3(p.x, h + 0.05, p.z + s * (d * 0.5 + 0.04)), "mat": "wood"})
slabs.append({"size": Vector3(0.06, 0.10, d + 0.14),
"at": Vector3(p.x + s * (w * 0.5 + 0.04), h + 0.05, p.z), "mat": "wood"})
# juice bar along the back, with shelving above it for the bottles
slabs.append({"size": Vector3(7.0, 1.05, 0.75), "at": Vector3(-3.0, 0.525, -7.4),
"mat": "trim"})
slabs.append({"size": Vector3(7.2, 0.09, 0.92), "at": Vector3(-3.0, 1.10, -7.4),
"mat": "wood"})
for k in range(3):
slabs.append({"size": Vector3(6.6, 0.06, 0.34),
"at": Vector3(-3.0, 1.62 + k * 0.46, -7.72), "mat": "wood"})
# chiller run down the right wall
slabs.append({"size": Vector3(0.80, 2.10, 8.0), "at": Vector3(10.4, 1.05, 1.0),
"mat": "trim"})
for k in range(4):
slabs.append({"size": Vector3(0.86, 0.05, 8.0),
"at": Vector3(10.3, 0.55 + k * 0.46, 1.0), "mat": "ceiling"})
var smash: Array = []
# bottles on the bar shelves — the one thing in here that still wants a proper swing
for k in range(3):
for b in range(11):
smash.append({"glb": "juice-bottle", "kind": "glass",
"at": Vector2(-6.0 + b * 0.60, -7.72), "sit_on": 1.65 + k * 0.46})
for b in range(9):
smash.append({"glb": "juice-bottle", "kind": "glass",
"at": Vector2(-5.6 + b * 0.62, -7.4), "sit_on": 1.15})
# loose crates of stock on the floor
for c in [Vector2(-8.6, 3.4), Vector2(-8.6, 4.4), Vector2(-8.0, 5.4),
Vector2(7.4, -4.6), Vector2(8.2, -4.0), Vector2(7.8, 5.2)]:
smash.append({"glb": "produce-crate", "kind": "wood", "at": c,
"yaw": randf_range(0.0, TAU)})
# a few bananas and cabbages just lying about
for b in [Vector2(-7.2, 0.4), Vector2(3.2, 4.2), Vector2(6.0, 2.0)]:
smash.append({"glb": "produce-banana", "kind": "produce", "at": b})
for c in [Vector2(-9.4, -2.0), Vector2(2.4, -5.6)]:
smash.append({"glb": "produce-cabbage", "kind": "produce", "at": c})
return {
"name": "THE GREENGROCER",
"subtitle": "everything here is soft except the bottles",
"bounds": {"x0": -11.0, "x1": 11.0, "z0": -8.0, "z1": 8.0, "h": 3.30},
"palette": {
"floor": Color(0.70, 0.68, 0.64), "wall": Color(0.90, 0.89, 0.85),
"ceiling": Color(0.20, 0.20, 0.21), "trim": Color(0.15, 0.15, 0.16),
"partition": Color(0.30, 0.44, 0.40), "wood": Color(0.52, 0.36, 0.20),
"accent": Color(0.20, 0.48, 0.42),
},
"floor_style": "lino",
"ceiling": "flat",
"walls": [],
"windows": [{"a": "z", "at": 8.0, "from": -11.0, "to": 11.0,
"y0": 0.35, "y1": 2.85, "blinds": false}],
"lights": {"xs": [-8.0, -4.0, 0.0, 4.0, 8.0], "zs": [-6.0, -2.0, 2.0, 6.0],
"color": Color(1.0, 0.94, 0.84), "energy": 2.4, "style": "strip",
"key_energy": 1.8},
"sun": {"color": Color(1.0, 0.96, 0.88), "energy": 1.1,
"rot": Vector3(-40, 8, 0)},
"env": {"bg": Color(0.76, 0.82, 0.88), "ambient": Color(0.60, 0.58, 0.55),
"ambient_energy": 0.66, "exposure": 1.04},
"clusters": [],
"chairs": [],
"props": [
{"glb": "office-plant", "at": Vector3(-10.2, 0.0, 7.0)},
{"glb": "office-plant", "at": Vector3(9.6, 0.0, 7.0)},
# the bag roll. Its post stands on the floor; the station is the roll itself.
{"glb": "bag-roll", "at": Vector3(-0.3, 0.0, 3.6), "yaw": PI},
],
# Brass shop scales hung down the aisles, dial at eye level and the dish at chest
# height — where a real one hangs, where you look INTO the pan rather than at its
# unlit underside, and where you keep walking into it.
"scales": [
Vector3(-3.7, 1.58, -0.4), Vector3(-0.3, 1.58, -0.4),
# the counter one hangs higher, so you can still see the bottle shelf
Vector3(3.1, 1.58, -0.4), Vector3(-3.0, 1.88, -6.5),
],
"stations": [
{"kind": "bags", "at": Vector3(-0.3, 1.1, 3.6), "label": "bag up the order"},
],
"slabs": slabs,
"smashables": smash,
"stacks": stacks,
"records": [],
"gauntlet": {"at": Vector3(-8.6, 0.0, -6.0), "yaw": PI,
"stand": Vector3(-8.6, 0.0, -4.0), "shredder": Vector3(-6.4, 0.0, -5.4)},
"spawn": {"at": Vector3(0.0, 0.0, 6.4), "yaw": PI},
}
# ================================================================ SITE 7
## THE MONSTER ROBOT PARTY — the shop the whole game started as.
##
## The only site that is IMPORTED rather than authored. The live storefront at
## robotmonster.party/store/ is assembled procedurally from Postgres (the wowplatter
## virtual_* schema, served by recordgod), and this game's sites are data, so
## tools/import_store.py reads the real shop and emits assets/store/shop_floor.json:
## 35 racks, 278 bins carrying their real genre labels, and 21,159 records of stock.
##
## The shop's schema IS the cascade. virtual_crate.rack_id already records which rack
## holds which bin, so every crate arrives frozen and `supported_by` its rack — knock a
## rack over and the actual bins that actually sit on it actually go. None of that is
## authored; it comes off the shop floor.
##
## Re-run the importer when the real shop is rearranged and this level follows.
const STORE_JSON := "res://assets/store/shop_floor.json"
static func record_store() -> Dictionary:
var raw := FileAccess.get_file_as_string(STORE_JSON)
var data = JSON.parse_string(raw) if raw != "" else null
if data == null:
push_warning("[store] %s missing — run tools/import_store.py" % STORE_JSON)
data = {"room": {"w": 6.66, "d": 9.51, "h": 2.89}, "smashables": [], "slabs": [],
"spawn": {"at": [0.0, 3.0]}}
var room: Dictionary = data.get("room", {})
var w: float = float(room.get("w", 6.66))
var dp: float = float(room.get("d", 9.51))
var h: float = float(room.get("h", 2.89))
# JSON has no Vector types — rehydrate into what Floorplan/Main expect.
var smash: Array = []
for e in data.get("smashables", []):
var s: Dictionary = e.duplicate()
var a: Array = s["at"]
s["at"] = Vector2(float(a[0]), float(a[1]))
smash.append(s)
var slabs: Array = []
for e in data.get("slabs", []):
var sz: Array = e["size"]
var at: Array = e["at"]
slabs.append({"size": Vector3(float(sz[0]), float(sz[1]), float(sz[2])),
"at": Vector3(float(at[0]), float(at[1]), float(at[2])),
"mat": String(e.get("mat", "wood"))})
var sp: Array = data.get("spawn", {}).get("at", [0.0, 3.0])
return {
"name": "MONSTER ROBOT PARTY",
"subtitle": "your own shop. you know exactly what everything cost.",
"bounds": {"x0": -w * 0.5, "x1": w * 0.5, "z0": -dp * 0.5, "z1": dp * 0.5, "h": h},
"palette": {
"floor": Color(0.42, 0.31, 0.22), "wall": Color(0.93, 0.92, 0.90),
"ceiling": Color(0.17, 0.16, 0.19), "trim": Color(0.19, 0.18, 0.21),
"partition": Color(0.35, 0.20, 0.34), "wood": Color(0.52, 0.36, 0.20),
"accent": Color(1.0, 0.36, 0.62), # the shop's own pink
},
"floor_style": "timber",
"ceiling": "flat",
"walls": [],
# the shopfront: glazing down the street side, where the window display is
"windows": [{"a": "z", "at": dp * 0.5, "from": -w * 0.5, "to": w * 0.5,
"y0": 0.55, "y1": 2.45, "blinds": false}],
"lights": {"xs": [-1.9, 1.9], "zs": [-3.4, -1.1, 1.2, 3.5],
"color": Color(1.0, 0.93, 0.86), "energy": 2.1, "style": "strip",
"key_energy": 1.5},
"sun": {"color": Color(1.0, 0.95, 0.88), "energy": 0.9, "rot": Vector3(-38, 14, 0)},
"env": {"bg": Color(0.72, 0.76, 0.82), "ambient": Color(0.58, 0.55, 0.56),
"ambient_energy": 0.62, "exposure": 1.02},
"clusters": [],
"chairs": [],
"props": [],
"slabs": slabs,
"smashables": smash,
"stacks": [],
# a few playable records loose on the counter side, for the ritual
"records": [],
"spawn": {"at": Vector3(float(sp[0]), 0.0, float(sp[1])), "yaw": PI},
}