extends Node3D ## Destroyulator — LEVEL 01, the office. ## ## Main is the glue: it builds the level shell (Office), spawns everything SMASHABLE into ## it, owns score/tally, and routes each Smashable's three outcomes (break / dent / ## futile) to Juice and the HUD. ## ## Two things it still proves, from the prototype days: ## THE DAY-0 GATE: press B to rain 500 rigid bodies and watch the FPS hold. ## THE CASCADE: smash a frozen desk -> the monitor on it falls; smash a crate -> ## the records spill -> a record cracks a beat later when it lands. ## ## Controls: see KEYS_LINE below, or the Dev HUD (H cycles HUD styles). @onready var _cam: Camera3D var _world: Node3D # everything smashable/debris lives here so reset is one line var _hud: Hud var _smash_count := 0 var _score := 0 var _tally := {} # material -> how many of it you've destroyed (work-order HUD) ## Score per material. Steel is worth the most because it's the hardest to break, which ## is what makes the sledgehammer worth its cooldown. const POINTS := { "paper": 5, "cardboard": 10, "plastic": 20, "vinyl": 25, "wood": 30, "glass": 40, "steel": 60, } var _juice: Juice var _player: Player # kept so GameMode can reach player.grab var _records: Array[Record] = [] # the live records, handed to GameMode each round var _game_mode: GameMode var _plan: Floorplan # the level shell: walls, fittings, lighting — see Levels.gd var _level_id := "scranton" var _rage: Rage # the tilt meter — see Rage.gd var _rage_hud: RageOverlay var _tasks: Tasks var _shift: CubicleHell var _env_node: WorldEnvironment var _ambience: Ambience var _modes: Modes var _gauntlet: Gauntlet var _dust: Dust func _ready() -> void: randomize() _setup_world() _juice = Juice.new() add_child(_juice) _juice.setup(_cam) # _setup_world() set _cam to the player's camera _populate() _spawn_guard = SPAWN_GUARD_TIME # --- CUBICLE HELL: the tilt meter, the job, and the payslip that scores both --- _rage = Rage.new() add_child(_rage) _rage_hud = RageOverlay.new() _rage_hud.setup(_rage) add_child(_rage_hud) _tasks = Tasks.new() add_child(_tasks) _tasks.setup(_player, _rage) _register_stations() _player.tasks = _tasks _shift = CubicleHell.new() add_child(_shift) _shift.setup(_rage, _tasks) _shift.begin() # the provocations you can't play around: flickering tube, fish in the microwave _ambience = Ambience.new() add_child(_ambience) _modes = Modes.new() add_child(_modes) _modes.changed.connect(_on_mode_changed) _gauntlet = Gauntlet.new() add_child(_gauntlet) _dust = Dust.new() add_child(_dust) # rules layer: the record modes still exist behind M _game_mode = GameMode.new() add_child(_game_mode) _game_mode.setup(_player.grab) _game_mode.begin_free_play(_records) _setup_hud() _ambience.setup(_rage, _hud) _ambience.adopt_lights(_plan) _gauntlet.setup(_player, _hud) _player.gauntlet = _gauntlet _dust.setup(_player, _cam, _rage, _hud) _player.dust = _dust _dust.adopt(get_tree(), bool(_plan.spec.get("invisible", false))) _modes.begin(get_tree().get_nodes_in_group("smashable").size()) ## Every desk is a spreadsheet you have to fill in; the copier is the copier. func _register_stations() -> void: var i := 1 for t in _plan.desk_spots(): # alternating, so walking to "a desk" isn't always the same minigame if i % 2 == 1: _tasks.register(Tasks.Kind.SPREADSHEET, (t as Transform3D).origin, "work at desk %d" % i) else: _tasks.register(Tasks.Kind.STAPLES, (t as Transform3D).origin, "de-staple the bundle on desk %d" % i) i += 1 if _plan.has_copier(): _tasks.register(Tasks.Kind.COPIER, _plan.copier_spot().origin, "use the photocopier") # a station wherever the site put a guillotine for e in _plan.smashables(): var d: Dictionary = e if String(d.get("glb", "")) == "guillotine": var a: Vector2 = d["at"] _tasks.register(Tasks.Kind.GUILLOTINE, Vector3(a.x, 0.9, a.y), "trim the ream") # ---------------------------------------------------------------- world / camera func _setup_world() -> void: # The level shell: floor, walls, ceiling, windows, fittings and ALL the lighting. # It owns the environment too, because interior lighting and the ambient/tonemap # settings are one decision, not two. _plan = Floorplan.new() add_child(_plan) _plan.build(Levels.get_level(_level_id)) _env_node = WorldEnvironment.new() var env := Environment.new() _plan.configure_environment(env) _env_node.environment = env add_child(_env_node) # First-person player. add_child() runs its _ready() synchronously, so player.camera # and player.grab exist on the next line; we reuse them. _player = Player.new() add_child(_player) _player.global_position = _plan.spawn_point() _player.rotation.y = _plan.spawn_yaw() _cam = _player.camera _world = Node3D.new() _world.name = "World" add_child(_world) # ---------------------------------------------------------------- the store # Real Monster Robot Party assets: wooden racks (frozen backdrop you can smash) and # crates of records in front. Colliders auto-size to each GLB's mesh AABB and every # piece is dropped so its bottom rests exactly on the floor — no hardcoded dimensions. const RACK_GLB := "res://assets/store/rack.glb" const CRATE_GLB := "res://assets/store/crate.glb" const RECORD_GLB := "res://assets/store/record.glb" # Lane 2 hero props (each ships a .fractured.glb sibling → real shard-by-shard smashing) const DESK_GLB := "res://assets/store/office-desk.glb" const PRINTER_GLB := "res://assets/store/office-printer.glb" const CRT_GLB := "res://assets/store/crt-monitor.glb" const CABINET_GLB := "res://assets/store/filing-cabinet.glb" const COOLER_GLB := "res://assets/store/water-cooler.glb" const BOX_GLB := "res://assets/store/cardboard-box.glb" const TURNTABLE_GLB := "res://assets/store/turntable.glb" const CHAIR_GLB := "res://assets/store/office-chair.glb" const P := "res://assets/store/" ## Mass by material, so a thrown record can't tip a filing cabinet but a sledgehammer ## still can. Everything defaulted to 1 kg before, which is why the level fell over. const MASSES := { "paper": 0.3, "cardboard": 1.2, "plastic": 6.0, "vinyl": 0.6, "wood": 18.0, "glass": 12.0, "steel": 40.0, } # Generated sleeve art (Nano Banana): each file is a 3x3 sheet of nine original # fictional album covers. A record picks one tile at random via uv1 scale/offset — # one texture in VRAM, eighteen different-looking records on the floor. const SLEEVE_ATLASES: Array[String] = [ "res://assets/art/sleeves_a.jpg", "res://assets/art/sleeves_b.jpg", ] var _sleeve_textures: Array[Texture2D] = [] var _records_skinned := 0 ## Fill the office with the SMASHABLE half of the level. The Office class owns the shell ## and the static fittings; everything spawned here is a rigid body you can wreck. func _populate() -> void: _records.clear() # stale refs from the freed world; repopulated by _place_record # --- the bullpen: a desk at every cluster spot, each with a monitor on it --- var spots := _plan.desk_spots() for i in spots.size(): var t: Transform3D = spots[i] var xz := Vector2(t.origin.x, t.origin.z) var desk := _glb_piece(DESK_GLB, "wood", xz, true, 0.0, t.basis.get_euler().y) var top: float = desk["top"] var crt := _glb_piece(CRT_GLB, "glass", xz + Vector2(0.0, 0.10), false, top) (desk["piece"] as Smashable).supports.append(crt["piece"]) # Chair goes behind the desk, offset by the desk's OWN measured depth. Office.gd # used to guess a constant here and kept putting chairs inside desks (or, once # pushed out far enough to clear them, inside each other across the aisle). var depth: float = (desk["aabb"] as AABB).size.z var back := signf(t.origin.z - _plan.desk_cluster(i).y) if is_zero_approx(back): back = 1.0 _glb_piece(CHAIR_GLB, "plastic", xz + Vector2(0.0, back * (depth * 0.5 + 0.42)), false, 0.0, t.basis.get_euler().y) # --- the copier alcove: the PRINTER is the level boss, on its own desk --- if _plan.has_copier(): var cs := _plan.copier_spot() var cxz := Vector2(cs.origin.x, cs.origin.z) var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y) var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"]) var boss := printer["piece"] as Smashable boss.is_boss = true boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge boss.mass = 8.0 # heavy: hits rock it but don't shove it off the desk (cdesk["piece"] as Smashable).supports.append(printer["piece"]) # --- everything else this site contains, straight from its spec --- for e in _plan.smashables(): var d: Dictionary = e _glb_piece(P + String(d["glb"]) + ".glb", String(d.get("kind", "wood")), d["at"], bool(d.get("frozen", false)), float(d.get("sit_on", 0.0)), float(d.get("yaw", 0.0))) # --- crates of records, where the site has any. Records stand side by side ON the # --- crate lid, offset by more than a sleeve is thick: they used to be stacked 3 cm # --- apart vertically while being 30 cm TALL, so Jolt resolved 27 cm of # --- interpenetration explosively on frame one. for spot in _plan.spec.get("records", []): var crate := _glb_piece(CRATE_GLB, "wood", spot, false) var crate_top: float = crate["top"] for i in range(3): var rec := _place_record(spot + Vector2(randf_range(-0.02, 0.02), (float(i) - 1.0) * 0.045), crate_top) (crate["piece"] as Smashable).supports.append(rec) # --- chairs: the best thing in an office to send across the room --- for t in _plan.chair_spots(): _glb_piece(CHAIR_GLB, "plastic", Vector2(t.origin.x, t.origin.z), false, 0.0, t.basis.get_euler().y) print("[art] records skinned with sleeve art: ", _records_skinned) ## Spawn one grabbable Record (sleeve + nested disc) resting on `sit_on`. Mirrors ## _glb_piece's "drop the AABB bottom onto sit_on" trick, but the Record builds its own ## visuals/colliders/disc in _ready, so we just skin the sleeve and place it. func _place_record(xz: Vector2, sit_on: float) -> Record: var rec := Record.new() rec.name = "record" rec.setup() var g: String = GameMode.GENRES[randi() % GameMode.GENRES.size()] rec.sleeve_genre = g rec.disc_genre = g # matches until GameMode plants a mismatch _world.add_child(rec) # _ready() splits record.glb, builds the disc if rec.sleeve_visual != null: _skin_record(rec.sleeve_visual) # random cover art on the jacket faces var ab := _body_local_aabb(rec, rec.sleeve_visual) if rec.sleeve_visual != null else AABB() rec.global_position = Vector3(xz.x, sit_on - ab.position.y, xz.y) _wire(rec) # the empty sleeve breaking counts + juices var disc := rec.disc_body() if disc != null: _wire(disc) # the cracked disc counts + juices too _records.append(rec) return rec ## Instance a GLB as the VISUAL of a Smashable, give it a box collider auto-sized to ## the mesh AABB, and drop it so its bottom rests at height `sit_on`. Returns ## { piece: Smashable, top: float } so a caller can stack the next thing on top. func _glb_piece(path: String, kind: String, xz: Vector2, frozen: bool, sit_on := 0.0, yaw := 0.0) -> Dictionary: var s := Smashable.new() s.kind = kind s.start_frozen = frozen s.mass = float(MASSES.get(kind, 5.0)) s.name = path.get_file().get_basename() # so physics diagnostics name the culprit # real destruction: if Lane 2 shipped a .fractured.glb next to this GLB, use it var frac := path.get_basename() + ".fractured.glb" if ResourceLoader.exists(frac): s.fractured_scene = load(frac) as PackedScene var packed := load(path) as PackedScene var vis: Node3D = packed.instantiate() s.add_child(vis) if kind == "vinyl": _skin_record(vis) # random sleeve art on the jacket faces _world.add_child(s) # in-tree so transforms/AABBs resolve # AABB is measured BEFORE yaw so the collider stays axis-aligned in body space; the # body is then rotated as a whole, which keeps box/mesh in agreement. var ab := _body_local_aabb(s, vis) if ab.size == Vector3.ZERO: ab = AABB(Vector3(-0.2, 0.0, -0.2), Vector3(0.4, 0.4, 0.4)) # fallback var col := CollisionShape3D.new() var box := BoxShape3D.new() box.size = ab.size col.shape = box col.position = ab.get_center() s.add_child(col) s.rotation.y = yaw s.global_position = Vector3(xz.x, sit_on - ab.position.y, xz.y) # AABB bottom lands on sit_on s.set_meta("spawn_pos", s.global_position) # for the spawn-guard warning _wire(s) return {"piece": s, "top": sit_on + ab.size.y, "aabb": ab} ## Give a record's sleeve faces a random cover from the generated 3x3 sheets. ## The record.glb sleeve cube carries M_Sleeve_Front / M_Sleeve_Back materials ## with real UVs — we duplicate them and window one tile of the atlas via ## uv1_scale/uv1_offset (no image slicing, one texture serves nine covers). func _skin_record(vis: Node3D) -> void: if _sleeve_textures.is_empty(): for p in SLEEVE_ATLASES: var t := load(p) as Texture2D if t != null: _sleeve_textures.append(t) if _sleeve_textures.is_empty(): return # art not imported yet — records stay plain var tex: Texture2D = _sleeve_textures[randi() % _sleeve_textures.size()] var tile_col := randi() % 3 var tile_row := randi() % 3 var mis: Array[MeshInstance3D] = [] _collect_meshes(vis, mis) var did := false for mi in mis: if mi.mesh == null: continue for si in mi.mesh.get_surface_count(): var mat := mi.mesh.surface_get_material(si) if mat == null or not (mat is BaseMaterial3D): continue var nm: String = mat.resource_name if nm.contains("Sleeve_Front") or nm.contains("Sleeve_Back"): var dup := mat.duplicate() as BaseMaterial3D dup.albedo_color = Color.WHITE dup.albedo_texture = tex dup.uv1_scale = Vector3(1.0 / 3.0, 1.0 / 3.0, 1.0) dup.uv1_offset = Vector3(tile_col / 3.0, tile_row / 3.0, 0.0) mi.set_surface_override_material(si, dup) did = true if did: _records_skinned += 1 ## Combined AABB of every MeshInstance3D under `visual_root`, in `body`'s local space ## (independent of where `body` currently sits in the world). func _body_local_aabb(body: Node3D, visual_root: Node3D) -> AABB: var mis: Array[MeshInstance3D] = [] _collect_meshes(visual_root, mis) var inv := body.global_transform.affine_inverse() var acc := AABB() var started := false for mi in mis: var a := mi.get_aabb() var rel := inv * mi.global_transform for i in range(8): var corner := a.position + Vector3( a.size.x * float(i & 1), a.size.y * float((i >> 1) & 1), a.size.z * float((i >> 2) & 1)) var p := rel * corner if not started: acc = AABB(p, Vector3.ZERO) started = true else: acc = acc.expand(p) return acc func _collect_meshes(n: Node, acc: Array[MeshInstance3D]) -> void: if n is MeshInstance3D: acc.append(n) for c in n.get_children(): _collect_meshes(c, acc) func _piece(kind: String, size: Vector3, pos: Vector3, frozen: bool, cylinder := false) -> Smashable: var s := Smashable.new() s.kind = kind s.start_frozen = frozen var mat := StandardMaterial3D.new() mat.albedo_color = Smashable.PROFILES[kind]["color"] var mesh := MeshInstance3D.new() mesh.material_override = mat var col := CollisionShape3D.new() if cylinder: var cm := CylinderMesh.new() cm.top_radius = size.x cm.bottom_radius = size.x cm.height = size.y mesh.mesh = cm var cs := CylinderShape3D.new() cs.radius = size.x cs.height = size.y col.shape = cs else: var bm := BoxMesh.new() bm.size = size mesh.mesh = bm var bs := BoxShape3D.new() bs.size = size col.shape = bs s.add_child(mesh) s.add_child(col) _world.add_child(s) # _ready() runs here (kind + frozen already set) s.global_position = pos _wire(s) return s # ---------------------------------------------------------------- spawn guard ## A prop that spawns even slightly inside another collider gets depenetrated by Jolt, ## and depenetration is not gentle: a chair overlapping a table left the building at ## 450 m/s on frame one. Placing 30-odd props by formula means this WILL happen again ## the next time a room is re-laid-out, so it's handled systemically rather than by ## nudging coordinates until the symptom goes away. ## ## For the first fraction of a second, dynamic bodies are speed-limited — enough for a ## real overlap to push apart calmly, far too little to launch anything. Each offender ## is named once in a warning, so the underlying overlap stays visible and fixable ## instead of being silently papered over. const SPAWN_GUARD_TIME := 0.75 const SPAWN_MAX_SPEED := 2.5 const SPAWN_MAX_SPIN := 10.0 var _spawn_guard := 0.0 var _guard_warned := {} func _physics_process(dt: float) -> void: if _spawn_guard <= 0.0: return _spawn_guard -= dt for n in get_tree().get_nodes_in_group("smashable"): var b := n as RigidBody3D if b == null or b.freeze: continue var v := b.linear_velocity.length() if v > SPAWN_MAX_SPEED: if not _guard_warned.has(b): _guard_warned[b] = true # report where it was PLACED, not where it is now — at 500 m/s a body is # already 8 m away by the time this runs, which points at nothing useful var at = b.get_meta("spawn_pos", b.global_position) push_warning("[spawn] %s (%s) placed overlapping something at %s (%.0f m/s) — clamped" % [ b.name, (b as Smashable).kind if b is Smashable else "?", (at as Vector3).snappedf(0.01), v]) b.linear_velocity = b.linear_velocity.normalized() * SPAWN_MAX_SPEED if b.angular_velocity.length() > SPAWN_MAX_SPIN: b.angular_velocity = b.angular_velocity.normalized() * SPAWN_MAX_SPIN func _on_smashed(kind: String, at: Vector3, shards: int) -> void: _smash_count += 1 _tally[kind] = int(_tally.get(kind, 0)) + 1 if _juice != null: _juice.impact(kind, at, shards) # score AFTER impact() so this break's own combo step is counted _score += int(POINTS.get(kind, 15)) * maxi(_juice.combo(), 1) else: _score += int(POINTS.get(kind, 15)) # Rage drains a FLAT amount per object; the payslip bills each object at its own # value. That gap is the whole decision during a meltdown — break a lot of cheap # things fast, or take one expensive swing and pay for it. if _rage != null: _rage.on_smashed() if _shift != null: _shift.on_smashed(int(POINTS.get(kind, 15))) if _modes != null: _modes.on_smashed(int(POINTS.get(kind, 15))) if _game_mode != null: _game_mode.on_smashed() # scores the mess during a FindMisfiled round ## A swing that couldn't hurt what it hit. No score, no combo — just the clank, plus a ## one-time nudge toward the right tool. func _on_resisted(kind: String, at: Vector3) -> void: if _juice != null: _juice.resist(kind, at) if _hud != null: _hud.toast("%s shrugs it off — try a heavier tool" % kind.to_upper()) func _on_damaged(kind: String, at: Vector3, frac: float) -> void: if _juice != null: _juice.dent(kind, at, frac) ## Every Smashable routes its three outcomes here. func _wire(s: Smashable) -> void: s.smashed.connect(_on_smashed) s.resisted.connect(_on_resisted) s.damaged.connect(_on_damaged) # ---------------------------------------------------------------- input func _unhandled_input(event: InputEvent) -> void: # Swings, weapon slots (1-6 / wheel / Q), grab (E/G/drag) and Esc live in Player.gd. # Number keys are the LOADOUT now, so game modes moved to M and the HUD cycles on H. if event is InputEventKey and event.pressed and not event.echo: match (event as InputEventKey).keycode: KEY_B: _rain(500) KEY_R: _reset() KEY_M: if _modes != null: _modes.cycle() KEY_G: if _modes != null and _modes.mode == Modes.M.GAUNTLET: _start_gauntlet() KEY_H: if _hud != null: _hud.cycle() KEY_L: _load_level(Levels.next_id(_level_id)) KEY_F: if _game_mode != null: _game_mode.report() # report the disc you're holding (FindMisfiled) ## Swap sites without restarting: tear down the shell and everything in it, build the ## new spec, and re-arm the shift. The player is moved to the new spawn because the old ## one is very likely now inside a wall. func _load_level(id: String) -> void: _level_id = id for c in _world.get_children(): c.queue_free() _plan.queue_free() if _env_node != null: _env_node.queue_free() await get_tree().process_frame _plan = Floorplan.new() add_child(_plan) _plan.build(Levels.get_level(_level_id)) _env_node = WorldEnvironment.new() var env := Environment.new() _plan.configure_environment(env) _env_node.environment = env add_child(_env_node) _player.global_position = _plan.spawn_point() _player.rotation.y = _plan.spawn_yaw() _player.velocity = Vector3.ZERO _smash_count = 0 _score = 0 _tally.clear() _populate() _spawn_guard = SPAWN_GUARD_TIME _guard_warned.clear() _tasks.clear_stations() _register_stations() if _ambience != null: _ambience.adopt_lights(_plan) if _dust != null: var blind := bool(_plan.spec.get("invisible", false)) _dust.adopt(get_tree(), blind) if blind: _player.select(5) # you will need the extinguisher if _game_mode != null: _game_mode.begin_free_play(_records) if _shift != null: _shift.begin() if _hud != null: _hud.toast("%s — %s" % [_plan.level_name(), _plan.subtitle()]) ## Switching mode re-arms whatever that mode needs. Cubicle Hell and the Gauntlet are ## mutually exclusive — you cannot be doing your job and shredding the Henderson file at ## the same time — so entering one shuts the other down. func _on_mode_changed(m: int) -> void: var hell := m == Modes.M.CUBICLE_HELL _shift.running = hell _ambience.running = hell _tasks.clear_stations() if hell: _register_stations() _shift.begin() _gauntlet.running = false if m == Modes.M.GAUNTLET: _start_gauntlet() _modes.begin(get_tree().get_nodes_in_group("smashable").size()) if _hud != null: _hud.toast(_modes.name_of()) ## Build the cabinet bank in this site's records area and stand the player at the row. ## Placing it relative to wherever the player happened to be standing put it through ## walls and behind them; every level now nominates the spot. func _start_gauntlet() -> void: var g: Dictionary = _plan.spec.get("gauntlet", {}) var origin: Vector3 = g.get("at", _player.global_position + (-_player.global_transform.basis.z) * 3.4) var yaw := float(g.get("yaw", _player.rotation.y + PI)) _gauntlet.begin(_world, origin, yaw, _wire, _find_shredder(g)) if g.has("stand"): _player.global_position = g["stand"] _player.velocity = Vector3.ZERO _player.rotation.y = yaw + PI # face the row ## The level's perfect tool. Deliberately NOT signposted — see Gauntlet.gd. func _find_shredder(g: Dictionary) -> Node3D: for n in get_tree().get_nodes_in_group("smashable"): if (n as Node).name.begins_with("shredder"): return n as Node3D # the site doesn't already have one, so put it where the spec says — in the room, # in plain sight, and completely unremarked upon var at: Vector3 = g.get("shredder", Vector3( _player.global_position.x + 2.2, 0.0, _player.global_position.z - 2.6)) var res := _glb_piece(P + "shredder.glb", "plastic", Vector2(at.x, at.z), false) return res["piece"] as Node3D # The Day-0 stress gate: rain N bodies, watch the FPS counter hold. func _rain(n: int) -> void: for i in n: var b := RigidBody3D.new() b.add_to_group("debris") var mesh := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = Vector3(0.12, 0.12, 0.12) mesh.mesh = bm b.add_child(mesh) var col := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = bm.size col.shape = shape b.add_child(col) _world.add_child(b) b.global_position = Vector3(randf_range(-2.2, 2.2), randf_range(3.5, 8.0), randf_range(-2.2, 2.2)) func _reset() -> void: for c in _world.get_children(): c.queue_free() _smash_count = 0 _score = 0 _tally.clear() await get_tree().process_frame _populate() _spawn_guard = SPAWN_GUARD_TIME _guard_warned.clear() if _shift != null: _shift.begin() # re-arm whatever mode we're in against the fresh set of records if _game_mode != null: if _game_mode.mode == GameMode.Mode.FIND_MISFILED: _game_mode.begin_find_misfiled(_records) else: _game_mode.begin_free_play(_records) ## Switch mode and rebuild the store fresh so a round always starts clean. func _switch_mode(m: int) -> void: if _game_mode == null: return _game_mode.mode = m # _reset() reads this to pick begin_free_play/begin_find_misfiled await _reset() # ---------------------------------------------------------------- HUD func _setup_hud() -> void: _hud = Hud.new() add_child(_hud) if _player != null: _player.weapon_changed.connect(_on_weapon_changed) ## Whichever mode is running gets the status line. func _mode_line() -> String: if _modes == null: return "" match _modes.mode: Modes.M.CUBICLE_HELL: return _shift.hud_line() if _shift != null else "" Modes.M.GAUNTLET: return _gauntlet.hud_line() if _gauntlet != null else "" return _modes.hud_line() func _on_weapon_changed(w: Weapon) -> void: if _hud != null and w != null: _hud.toast(w.display_name) const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · L site · B rain · R reset" func _process(_dt: float) -> void: if _hud == null: return var bodies := get_tree().get_nodes_in_group("smashable").size() \ + get_tree().get_nodes_in_group("debris").size() var w: Weapon = _player.weapon() if _player != null else null if _tasks != null: var slip := PackedStringArray() if _modes != null and _modes.mode == Modes.M.CUBICLE_HELL \ and _shift != null and not _shift.running and _shift.time_left <= 0.0: slip = _shift.payslip() _hud.feed_work(_tasks.panel_state(), _tasks.prompt(), slip) _hud.feed({ "fps": Engine.get_frames_per_second(), "bodies": bodies, "smashed": _smash_count, "score": _score, "tally": _tally, "combo": _juice.combo() if _juice != null else 0, "combo_decay": _juice.combo_decay() if _juice != null else 0.0, "weapon": w.display_name if w != null else "-", "weapon_blurb": w.blurb if w != null else "", "weapon_stats": ("pow %.1f cd %.2fs reach %.1fm" % [w.power, w.cooldown, w.reach]) if w != null else "", "slot": _player.slot() if _player != null else 0, "mode_line": _mode_line() + ("\n" + _dust.hud_line() if _dust != null and _dust.active else ""), "mode_name": _modes.name_of() if _modes != null else "", "headline": _modes.headline() if _modes != null else "", "keys": KEYS_LINE, "rage": _rage.value if _rage != null else 0.0, "rage_state": _rage.state_name() if _rage != null else "", "rage_status": _rage.status_line() if _rage != null else "", "pay": _shift.wages() if _shift != null else 0, "damages": _shift.damages if _shift != null else 0, "net": _shift.net() if _shift != null else 0, })