destroyulator/scripts/Main.gd
Monster Robot Party a8b735f79e Destroyulator prototype: first-person cascade smasher (Godot 4.7)
Mac-native (Apple Silicon / Metal), Jolt physics.
- First-person controller + semi-opaque viewmodel hands
- Punch (L-click) / kick (R-click) via a swappable MeleeAttack resource
- Structural support-graph cascade: knock the legs -> the rack pancakes
- Brittle vinyl records shatter on hard impact
- Day-0 stress gate (B: rain 500) — holds 60fps on the M3 Ultra

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:25:35 +10:00

197 lines
6.8 KiB
GDScript

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]