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>
800 lines
38 KiB
GDScript
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},
|
|
}
|