extends Node3D ## Destroyulator — cascade prototype. ## ## Builds a little record rack out of primitives and lets you smash it. The point ## isn't the art (that's a one-line swap for your real .glb racks later) — it's to ## prove two things on your actual M3, today: ## 1. THE DAY-0 GATE: press S to rain 500 rigid bodies and watch the FPS hold. ## 2. THE CASCADE: smash a frozen shelf -> the crates on it fall -> jackets spill ## -> records tumble out -> records crack when they hit the floor (brittle). ## ## Controls: WASD move · Space jump · L-click punch · R-click kick · Esc release mouse ## · B = rain 500 (stress gate) · R = reset @onready var _cam: Camera3D var _world: Node3D # everything smashable/debris lives here so reset is one line var _hud: Label var _smash_count := 0 var _juice: Juice func _ready() -> void: randomize() _setup_world() _juice = Juice.new() add_child(_juice) _juice.setup(_cam) # _setup_world() set _cam to the player's camera _build_rack(Vector3.ZERO) _setup_hud() # ---------------------------------------------------------------- world / camera func _setup_world() -> void: var we := WorldEnvironment.new() var env := Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.07, 0.06, 0.09) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.5, 0.5, 0.62) env.ambient_light_energy = 0.6 we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-52, -38, 0) sun.light_energy = 1.3 sun.shadow_enabled = true add_child(sun) # First-person player replaces the old fixed camera. add_child() runs its _ready() # synchronously, so player.camera exists on the next line; we reuse it for HUD/debug. var player := Player.new() add_child(player) player.global_position = Vector3(0, 0, 4) # stand back, facing -Z toward the rack _cam = player.camera # floor var floor_body := StaticBody3D.new() var fmesh := MeshInstance3D.new() var plane := BoxMesh.new() plane.size = Vector3(24, 0.2, 24) fmesh.mesh = plane var fmat := StandardMaterial3D.new() fmat.albedo_color = Color(0.13, 0.12, 0.15) fmesh.material_override = fmat floor_body.add_child(fmesh) var fcol := CollisionShape3D.new() var fshape := BoxShape3D.new() fshape.size = plane.size fcol.shape = fshape floor_body.add_child(fcol) floor_body.position = Vector3(0, -0.1, 0) add_child(floor_body) _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" func _build_rack(origin: Vector3) -> void: # two racks form a smashable back wall of the booth _glb_piece(RACK_GLB, "wood", Vector2(origin.x - 0.95, origin.z - 0.7), true) _glb_piece(RACK_GLB, "wood", Vector2(origin.x + 0.95, origin.z - 0.7), true) # crates of records on the floor in front, in two rows var spots := [ Vector2(-0.8, 0.5), Vector2(0.0, 0.5), Vector2(0.8, 0.5), Vector2(-0.4, 1.2), Vector2(0.4, 1.2), ] for spot in spots: var xz: Vector2 = spot + Vector2(origin.x, origin.z) var crate := _glb_piece(CRATE_GLB, "wood", xz, false) var crate_top: float = crate["top"] # a few records stacked on the crate; smashing the crate spills them for i in range(3): var jitter := Vector2(randf_range(-0.06, 0.06), randf_range(-0.06, 0.06)) var rec := _glb_piece(RECORD_GLB, "vinyl", xz + jitter, false, crate_top + float(i) * 0.03) (crate["piece"] as Smashable).supports.append(rec["piece"]) ## 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) -> Dictionary: var s := Smashable.new() s.kind = kind s.start_frozen = frozen var packed := load(path) as PackedScene var vis: Node3D = packed.instantiate() s.add_child(vis) _world.add_child(s) # in-tree so transforms/AABBs resolve 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.global_position = Vector3(xz.x, sit_on - ab.position.y, xz.y) # AABB bottom lands on sit_on s.smashed.connect(_on_smashed) return {"piece": s, "top": sit_on + ab.size.y} ## 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 s.smashed.connect(_on_smashed) return s func _on_smashed(kind: String, at: Vector3, shards: int) -> void: _smash_count += 1 if _juice != null: _juice.impact(kind, at, shards) # ---------------------------------------------------------------- input func _unhandled_input(event: InputEvent) -> void: # Melee (L-click punch / R-click kick) and Esc live in Player.gd. Here we keep the # debug keys. Rain moved off S -> B, because the player now uses S to walk backward. if event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_B: _rain(500) elif event.keycode == KEY_R: _reset() # 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 await get_tree().process_frame _build_rack(Vector3.ZERO) # ---------------------------------------------------------------- HUD func _setup_hud() -> void: var layer := CanvasLayer.new() add_child(layer) _hud = Label.new() _hud.position = Vector2(16, 12) _hud.add_theme_font_size_override("font_size", 18) _hud.add_theme_color_override("font_color", Color(1, 0.36, 0.62)) layer.add_child(_hud) 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 c := _juice.combo() if _juice != null else 0 var combo_str := (" COMBO x%d" % c) if c > 1 else "" _hud.text = "FPS %d bodies %d smashed %d%s\nWASD move Space jump L-click punch R-click kick Esc release B: rain 500 R: reset" % [ Engine.get_frames_per_second(), bodies, _smash_count, combo_str]