extends Node3D class_name Office ## LEVEL 01 — the open-plan office. ## ## The prototype ran on a grey disc in a black void, which quietly broke the premise: a ## game about wrecking your workplace needs a workplace. This is that workplace, laid out ## after the Dunder Mifflin floor plan — the shape everyone already knows. ## ## z- (back) ## +--------------------------------------------------+ ## | MANAGER | CONFERENCE | | WAREHOUSE | ## | (glass) | | | door | ## |----------+---------------+ +-----------| ## | | | ## | B U L L P E N | BREAK | x+ ## | (4 facing desk pairs) | ROOM | ## | [copier] | | ## |----------+ +-----------| ## | RECEPTION| entrance | ## +--------------------------------------------------+ ## z+ (front / street) ## ## Walls, desks, partitions and counters are boxes built in code — they ARE boxes, so ## modelling them would be wasted effort. Anything with a silhouette a box can't fake ## (chairs, vending machines, the plant) comes from tools/gen_office_props.py. ## ## This script owns the SHELL and the FITTINGS: everything static and non-smashable, plus ## the lighting. Smashable furniture stays Main's business, so the two can be tuned ## independently — Main asks this class where things go via the `spot_*` helpers. ## ## Godot 4.7 GDScript 2.0. # ---------------------------------------------------------------- floor plan const X_MIN := -11.0 const X_MAX := 11.0 const Z_BACK := -8.0 const Z_FRONT := 8.0 const H := 2.75 # drop ceiling const WALL_T := 0.16 # interior room boundaries const MGR_X := -6.6 # manager's office: X_MIN..MGR_X, Z_BACK..MGR_Z const MGR_Z := -4.4 const CONF_X0 := -6.6 # conference room const CONF_X1 := -0.4 const CONF_Z := -4.4 const BREAK_X := 5.4 # break room: BREAK_X..X_MAX, BREAK_Z..Z_FRONT const BREAK_Z := -1.2 const RECEP_Z := 4.6 # reception partition const DOOR_W := 1.1 const P := "res://assets/store/" var _spots_desk: Array[Transform3D] = [] # where Main should put smashable desks var _spots_chair: Array[Transform3D] = [] var _spot_copier := Transform3D.IDENTITY # ---------------------------------------------------------------- materials var _m_carpet: StandardMaterial3D var _m_wall: StandardMaterial3D var _m_ceil: StandardMaterial3D var _m_trim: StandardMaterial3D var _m_glass: StandardMaterial3D var _m_part: StandardMaterial3D var _m_wood: StandardMaterial3D func build() -> void: _materials() _shell() _interior_walls() _ceiling_grid() _lights_rig() _bullpen() _reception() _conference() _manager() _break_room() _dressing() func _materials() -> void: _m_carpet = StandardMaterial3D.new() _m_carpet.albedo_color = Color(0.30, 0.30, 0.32) # the grey office carpet _m_carpet.roughness = 1.0 _m_wall = StandardMaterial3D.new() _m_wall.albedo_color = Color(0.80, 0.78, 0.72) # magnolia, of course _m_wall.roughness = 0.96 _m_ceil = StandardMaterial3D.new() _m_ceil.albedo_color = Color(0.88, 0.88, 0.86) _m_ceil.roughness = 0.98 _m_trim = StandardMaterial3D.new() _m_trim.albedo_color = Color(0.13, 0.12, 0.13) _m_trim.roughness = 0.7 _m_part = StandardMaterial3D.new() _m_part.albedo_color = Color(0.44, 0.44, 0.40) # cubicle partition fabric _m_part.roughness = 1.0 _m_wood = StandardMaterial3D.new() _m_wood.albedo_color = Color(0.42, 0.29, 0.17) _m_wood.roughness = 0.55 _m_glass = StandardMaterial3D.new() _m_glass.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA _m_glass.albedo_color = Color(0.66, 0.74, 0.78, 0.15) _m_glass.roughness = 0.05 _m_glass.cull_mode = BaseMaterial3D.CULL_DISABLED # ---------------------------------------------------------------- primitives func _slab(size: Vector3, centre: Vector3, mat: Material, collide := true, shadow := true) -> StaticBody3D: var body := StaticBody3D.new() var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = size mi.mesh = bm mi.material_override = mat if not shadow: mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF body.add_child(mi) if collide: var col := CollisionShape3D.new() var sh := BoxShape3D.new() sh.size = size col.shape = sh body.add_child(col) add_child(body) body.position = centre return body ## A wall running along X (or Z) with a doorway punched in it. func _wall_x(z: float, x0: float, x1: float, mat: Material, door_at := INF) -> void: if door_at == INF: _slab(Vector3(x1 - x0, H, WALL_T), Vector3((x0 + x1) * 0.5, H * 0.5, z), mat) return var d0 := door_at - DOOR_W * 0.5 var d1 := door_at + DOOR_W * 0.5 if d0 > x0: _slab(Vector3(d0 - x0, H, WALL_T), Vector3((x0 + d0) * 0.5, H * 0.5, z), mat) if x1 > d1: _slab(Vector3(x1 - d1, H, WALL_T), Vector3((d1 + x1) * 0.5, H * 0.5, z), mat) _slab(Vector3(DOOR_W, H - 2.05, WALL_T), Vector3(door_at, (H + 2.05) * 0.5, z), mat) func _wall_z(x: float, z0: float, z1: float, mat: Material, door_at := INF) -> void: if door_at == INF: _slab(Vector3(WALL_T, H, z1 - z0), Vector3(x, H * 0.5, (z0 + z1) * 0.5), mat) return var d0 := door_at - DOOR_W * 0.5 var d1 := door_at + DOOR_W * 0.5 if d0 > z0: _slab(Vector3(WALL_T, H, d0 - z0), Vector3(x, H * 0.5, (z0 + d0) * 0.5), mat) if z1 > d1: _slab(Vector3(WALL_T, H, z1 - d1), Vector3(x, H * 0.5, (d1 + z1) * 0.5), mat) _slab(Vector3(WALL_T, H - 2.05, DOOR_W), Vector3(x, (H + 2.05) * 0.5, door_at), mat) ## Static set dressing from a GLB, with a box collider sized to its own mesh so you can't ## walk through the vending machine. func _glb(path: String, pos: Vector3, yaw := 0.0) -> Node3D: if not ResourceLoader.exists(path): return null var packed := load(path) as PackedScene if packed == null: return null var body := StaticBody3D.new() add_child(body) body.position = pos body.rotation.y = yaw var n: Node3D = packed.instantiate() body.add_child(n) var ab := _mesh_aabb(n) if ab.size != Vector3.ZERO: var col := CollisionShape3D.new() var sh := BoxShape3D.new() sh.size = ab.size col.shape = sh col.position = ab.get_center() body.add_child(col) return body func _mesh_aabb(root: Node3D) -> AABB: var acc := AABB() var started := false for m in _meshes(root): var mi := m as MeshInstance3D var a := mi.get_aabb() a = mi.transform * a if not started: acc = a started = true else: acc = acc.merge(a) return acc func _meshes(n: Node, acc: Array = []) -> Array: if n is MeshInstance3D: acc.append(n) for c in n.get_children(): _meshes(c, acc) return acc ## Chairs are NOT placed here. They're the most satisfying thing in an office to send ## across the room, so they're spawned by Main as smashable rigid bodies; Office only ## says where they start. func _chair(pos: Vector3, yaw: float) -> void: _spots_chair.append(Transform3D(Basis(Vector3.UP, yaw), pos)) # ---------------------------------------------------------------- shell func _shell() -> void: var w := X_MAX - X_MIN var d := Z_FRONT - Z_BACK var cx := (X_MIN + X_MAX) * 0.5 var cz := (Z_BACK + Z_FRONT) * 0.5 _slab(Vector3(w + 1.0, 0.30, d + 1.0), Vector3(cx, -0.15, cz), _m_carpet) # outer walls; the +X wall is the window wall, so it's built in _windows() _wall_x(Z_BACK - WALL_T * 0.5, X_MIN, X_MAX, _m_wall) _wall_x(Z_FRONT + WALL_T * 0.5, X_MIN, X_MAX, _m_wall, -8.6) # entrance _wall_z(X_MIN - WALL_T * 0.5, Z_BACK, Z_FRONT, _m_wall) _windows() ## The window wall. Dunder Mifflin's bullpen has that whole run of blinded windows down ## one side, and it's what stops an interior reading as a sealed box. func _windows() -> void: var x := X_MAX + WALL_T * 0.5 var y0 := 0.85 var y1 := 2.35 _slab(Vector3(WALL_T, y0, Z_FRONT - Z_BACK), Vector3(x, y0 * 0.5, (Z_BACK + Z_FRONT) * 0.5), _m_wall) _slab(Vector3(WALL_T, H - y1, Z_FRONT - Z_BACK), Vector3(x, (H + y1) * 0.5, (Z_BACK + Z_FRONT) * 0.5), _m_wall) # mullions every 2.4 m, glazing between var z := Z_BACK while z < Z_FRONT - 0.1: var z1: float = minf(z + 2.4, Z_FRONT) _slab(Vector3(0.05, y1 - y0, z1 - z - 0.12), Vector3(X_MAX - 0.02, (y0 + y1) * 0.5, (z + z1) * 0.5), _m_glass, false, false) _slab(Vector3(WALL_T, y1 - y0, 0.12), Vector3(x, (y0 + y1) * 0.5, z1), _m_trim) # venetian blind, half-raised _slab(Vector3(0.06, 0.55, z1 - z - 0.14), Vector3(X_MAX - 0.10, y1 - 0.28, (z + z1) * 0.5), _m_ceil, false, false) z = z1 func _ceiling_grid() -> void: var w := X_MAX - X_MIN var d := Z_FRONT - Z_BACK var cx := (X_MIN + X_MAX) * 0.5 var cz := (Z_BACK + Z_FRONT) * 0.5 _slab(Vector3(w, 0.16, d), Vector3(cx, H + 0.08, cz), _m_ceil, true, false) # suspended-tile T-bar, purely visual — it's the texture of an office ceiling var gx := X_MIN while gx <= X_MAX + 0.01: _slab(Vector3(0.05, 0.03, d), Vector3(gx, H - 0.02, cz), _m_trim, false, false) gx += 1.22 var gz := Z_BACK while gz <= Z_FRONT + 0.01: _slab(Vector3(w, 0.03, 0.05), Vector3(cx, H - 0.02, gz), _m_trim, false, false) gz += 1.22 # ---------------------------------------------------------------- interior walls func _interior_walls() -> void: # manager's office — half solid, half glass so you can be watched through it _wall_x(MGR_Z, X_MIN, MGR_X, _m_wall, -9.2) _slab(Vector3(WALL_T, 1.0, MGR_Z - Z_BACK), Vector3(MGR_X, 0.5, (Z_BACK + MGR_Z) * 0.5), _m_wall) _slab(Vector3(0.06, H - 1.05, MGR_Z - Z_BACK), Vector3(MGR_X, (H + 1.0) * 0.5, (Z_BACK + MGR_Z) * 0.5), _m_glass, true, false) # conference room _wall_x(CONF_Z, CONF_X0, CONF_X1, _m_wall, -3.4) _wall_z(CONF_X1, Z_BACK, CONF_Z, _m_wall) # break room _wall_z(BREAK_X, BREAK_Z, Z_FRONT, _m_wall, 2.2) _wall_x(BREAK_Z, BREAK_X, X_MAX, _m_wall) # warehouse roller door on the back wall _slab(Vector3(2.6, 2.4, 0.12), Vector3(7.8, 1.2, Z_BACK + 0.06), _m_trim) for i in range(9): _slab(Vector3(2.5, 0.06, 0.05), Vector3(7.8, 0.18 + i * 0.26, Z_BACK + 0.14), _m_ceil, false, false) # ---------------------------------------------------------------- lighting ## Recessed fluorescent troffers in the ceiling grid. These ARE the light: the old scene ## lit everything with one outdoor directional lamp, which is exactly why it read as ## "props on a plane" rather than "inside somewhere". func _lights_rig() -> void: var panel := StandardMaterial3D.new() panel.albedo_color = Color(0.97, 0.97, 0.93) panel.emission_enabled = true panel.emission = Color(1.0, 0.98, 0.92) panel.emission_energy_multiplier = 1.5 var xs := [-8.5, -4.6, -0.7, 3.2, 7.1] var zs := [-6.2, -2.6, 1.0, 4.6] for xc in xs: for zc in zs: var mi := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = Vector3(1.14, 0.05, 0.58) mi.mesh = bm mi.material_override = panel mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(mi) mi.position = Vector3(xc, H - 0.06, zc) var l := OmniLight3D.new() l.light_color = Color(1.0, 0.98, 0.93) l.light_energy = 1.9 l.omni_range = 6.5 l.omni_attenuation = 1.2 l.shadow_enabled = false # 20 shadow-casting omnis is not worth the cost add_child(l) l.position = Vector3(xc, H - 0.20, zc) # two shadow-casters so props still sit in contact shadows for zc in [-2.0, 3.0]: var key := OmniLight3D.new() key.light_color = Color(1.0, 0.97, 0.92) key.light_energy = 1.5 key.omni_range = 13.0 key.shadow_enabled = true add_child(key) key.position = Vector3(-1.0, H - 0.30, zc) # daylight through the window wall var sun := DirectionalLight3D.new() sun.light_color = Color(0.82, 0.88, 1.0) sun.light_energy = 0.7 sun.rotation_degrees = Vector3(-30, -104, 0) sun.shadow_enabled = true add_child(sun) func configure_environment(env: Environment) -> void: env.background_mode = Environment.BG_COLOR env.background_color = Color(0.55, 0.62, 0.72) # daylight past the glass env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.50, 0.50, 0.50) env.ambient_light_energy = 0.5 env.ssao_enabled = true env.ssao_radius = 0.8 env.ssao_intensity = 1.5 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC env.tonemap_exposure = 1.0 # ---------------------------------------------------------------- bullpen ## Four pairs of desks facing each other across a low partition — the Jim-and-Dwight ## arrangement, which is the single most recognisable thing about the floor plan. ## Rows are 4.8 m apart, not 3.6. At 3.6 the two rows' chairs met back-to-back in the ## middle of the aisle with 3 cm to spare, which Jolt resolved by firing them out of the ## building — and it left no room to walk between the rows either. const CLUSTERS := [ Vector2(-4.6, -2.0), Vector2(-4.6, 2.8), Vector2(-0.4, -2.0), Vector2(-0.4, 2.8), ] func _bullpen() -> void: for c in CLUSTERS: _partition(Vector3(c.x, 0.0, c.y), 1.9) for s in [-1.0, 1.0]: var t := Transform3D(Basis(Vector3.UP, 0.0 if s > 0.0 else PI), Vector3(c.x, 0.0, c.y + s * 0.78)) _spots_desk.append(t) # NOTE: the bullpen chairs are NOT listed here. Their offset depends on how # deep office-desk.glb actually is, which only Main knows once it has measured # the mesh — guessing a constant here put chairs inside desks. # the copier alcove, between the bullpen and the break room _slab(Vector3(WALL_T, H, 2.6), Vector3(3.6, H * 0.5, -0.3), _m_wall) _spot_copier = Transform3D(Basis(Vector3.UP, deg_to_rad(-90.0)), Vector3(3.0, 0.0, -0.3)) ## A cubicle divider: fabric panel on a dark base rail. func _partition(at: Vector3, length: float) -> void: _slab(Vector3(length, 1.18, 0.07), at + Vector3(0, 0.62, 0), _m_part) _slab(Vector3(length + 0.06, 0.09, 0.11), at + Vector3(0, 1.24, 0), _m_trim) for s in [-1.0, 1.0]: _slab(Vector3(0.09, 1.24, 0.30), at + Vector3(s * length * 0.5, 0.62, 0), _m_trim) func desk_spots() -> Array[Transform3D]: return _spots_desk func chair_spots() -> Array[Transform3D]: return _spots_chair func copier_spot() -> Transform3D: return _spot_copier # ---------------------------------------------------------------- rooms func _reception() -> void: # the counter you meet on the way in, with the waiting-area chairs _slab(Vector3(0.70, 1.10, 3.0), Vector3(-9.9, 0.55, RECEP_Z + 1.0), _m_wood) _slab(Vector3(0.92, 0.08, 3.2), Vector3(-9.9, 1.14, RECEP_Z + 1.0), _m_trim) _slab(Vector3(1.8, 1.10, 0.70), Vector3(-9.6, 0.55, RECEP_Z - 0.15), _m_wood) _wall_z(-6.9, RECEP_Z, Z_FRONT, _m_wall, 6.8) for i in range(3): _chair(Vector3(-7.6, 0.0, 5.5 + i * 0.85), deg_to_rad(-90)) _glb(P + "office-plant.glb", Vector3(-6.4, 0.0, 6.6)) func _conference() -> void: var cx := (CONF_X0 + CONF_X1) * 0.5 var cz := (Z_BACK + CONF_Z) * 0.5 # the long table everyone dreads _slab(Vector3(3.9, 0.09, 1.35), Vector3(cx, 0.74, cz), _m_wood) for s in [-1.0, 1.0]: _slab(Vector3(0.16, 0.72, 0.90), Vector3(cx + s * 1.6, 0.36, cz), _m_trim) for i in range(4): var x := cx - 1.35 + i * 0.9 _chair(Vector3(x, 0.0, cz - 1.15), 0.0) _chair(Vector3(x, 0.0, cz + 1.15), PI) # whiteboard _slab(Vector3(2.4, 1.15, 0.05), Vector3(cx, 1.6, Z_BACK + 0.10), _m_ceil) _slab(Vector3(2.5, 1.25, 0.03), Vector3(cx, 1.6, Z_BACK + 0.07), _m_trim) func _manager() -> void: var cx := (X_MIN + MGR_X) * 0.5 var cz := (Z_BACK + MGR_Z) * 0.5 _chair(Vector3(cx, 0.0, cz - 0.9), 0.0) _chair(Vector3(cx - 0.8, 0.0, cz + 1.1), PI) _glb(P + "office-plant.glb", Vector3(X_MIN + 0.7, 0.0, MGR_Z - 0.8)) func _break_room() -> void: var cz := (BREAK_Z + Z_FRONT) * 0.5 # kitchen counter along the window wall _slab(Vector3(0.62, 0.88, 3.4), Vector3(X_MAX - 0.42, 0.44, cz + 0.6), _m_wood) _slab(Vector3(0.70, 0.06, 3.5), Vector3(X_MAX - 0.42, 0.90, cz + 0.6), _m_trim) _glb(P + "microwave.glb", Vector3(X_MAX - 0.45, 0.93, cz + 1.7), deg_to_rad(-90)) _glb(P + "vending-machine.glb", Vector3(BREAK_X + 0.9, 0.0, Z_FRONT - 0.6), PI) _glb(P + "vending-machine.glb", Vector3(BREAK_X + 1.9, 0.0, Z_FRONT - 0.6), PI) # two round tables with chairs for i in range(2): var tx := 7.4 var tz := BREAK_Z + 1.6 + i * 2.6 _slab(Vector3(1.05, 0.07, 1.05), Vector3(tx, 0.73, tz), _m_ceil) _slab(Vector3(0.12, 0.70, 0.12), Vector3(tx, 0.36, tz), _m_trim) _slab(Vector3(0.62, 0.05, 0.62), Vector3(tx, 0.03, tz), _m_trim) for a in range(3): var ang := deg_to_rad(a * 120.0 + 40.0) _chair(Vector3(tx + sin(ang) * 0.95, 0.0, tz + cos(ang) * 0.95), ang + PI) func _dressing() -> void: _glb(P + "office-plant.glb", Vector3(2.9, 0.0, 6.4)) _glb(P + "office-plant.glb", Vector3(-0.9, 0.0, -3.6)) # wall clock, because every office has one you stare at _slab(Vector3(0.42, 0.42, 0.05), Vector3(-3.4, 2.15, Z_FRONT - 0.02), _m_ceil) _slab(Vector3(0.48, 0.48, 0.03), Vector3(-3.4, 2.15, Z_FRONT - 0.01), _m_trim) ## Where the player should start: just inside the entrance, looking into the bullpen. func spawn_point() -> Vector3: return Vector3(-8.6, 0.0, 7.2) func spawn_yaw() -> float: return deg_to_rad(196.0)