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 func _ready() -> void: randomize() _setup_world() _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 rack # One frame (frozen) holding shelves; on each shelf, crates; in each crate, # cardboard jackets standing up with brittle vinyl records tucked between them. func _build_rack(origin: Vector3) -> void: var shelf_ys := [0.45, 1.15] # 4 posts (frozen wood) — the primary supports for BOTH shelves. var posts: Array[Smashable] = [] for sx in [-0.62, 0.62]: for sz in [-0.28, 0.28]: posts.append(_piece("wood", Vector3(0.06, 1.5, 0.06), origin + Vector3(sx, 0.75, sz), true)) for y in shelf_ys: # frozen shelf slab — smash THIS (or any leg) to drop everything above it. var shelf := _piece("wood", Vector3(1.34, 0.05, 0.62), origin + Vector3(0, y, 0), true) # every leg holds up every shelf: knock ANY leg -> both shelves drop (arcade call). for post in posts: post.supports.append(shelf) # two crates resting on the shelf; the shelf holds them up. for cx in [-0.34, 0.34]: var crate_pos: Vector3 = origin + Vector3(cx, y + 0.22, 0) var crate := _piece("wood", Vector3(0.5, 0.34, 0.5), crate_pos, false) shelf.supports.append(crate) # shelf gone -> crate released too # jackets + a record sit loose in the crate — already dynamic, they fall on # their own once the crate/shelf is gone. Do NOT add them to supports. var idx := 0 for jz in [-0.14, -0.05, 0.05, 0.14]: _piece("cardboard", Vector3(0.34, 0.34, 0.03), crate_pos + Vector3(0, 0.12, jz), false) if idx == 1: # a record tucked in, lying flat-ish; brittle -> cracks on hard landing _piece("vinyl", Vector3(0.15, 0.02, 0.15), crate_pos + Vector3(0, 0.24, jz + 0.02), false, true) idx += 1 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 # ---------------------------------------------------------------- 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() _hud.text = "FPS %d bodies %d smashed %d\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]