extends RigidBody3D class_name Smashable ## A piece of the world that can be smashed. ## ## The cascade lives here: frame pieces start FROZEN (they act static and hold the ## stuff above them up). When you smash a frame piece it vanishes into shards, so ## everything resting on it falls — no scripted "release" needed, gravity does it. ## Brittle kinds (vinyl, glass) also shatter on their own when they hit something ## hard enough — that's the satisfying TAIL of the cascade: the record survives the ## tumble out of its jacket, then cracks a beat later when it smacks the floor. @export_enum("wood", "cardboard", "vinyl", "glass", "steel") var kind: String = "wood" @export var start_frozen: bool = false # per-material behaviour — this table is where "material identity" is authored. # brittle_speed 0.0 == not brittle (won't self-shatter on impact). const PROFILES := { "wood": {"color": Color(0.42, 0.28, 0.15), "shards": 7, "brittle_speed": 0.0, "bounce": 0.05}, "cardboard": {"color": Color(0.66, 0.5, 0.32), "shards": 4, "brittle_speed": 0.0, "bounce": 0.0}, "vinyl": {"color": Color(0.04, 0.04, 0.05), "shards": 6, "brittle_speed": 2.2, "bounce": 0.12}, "glass": {"color": Color(0.55, 0.8, 0.85), "shards": 12, "brittle_speed": 1.6, "bounce": 0.0}, "steel": {"color": Color(0.7, 0.72, 0.76), "shards": 0, "brittle_speed": 0.0, "bounce": 0.25}, } signal smashed(kind: String, at: Vector3, shard_count: int) var _shattered := false var _speed := 0.0 # last physics-frame speed, sampled before any collision resolves ## Pieces this one holds up. When THIS shatters, these get released (unfrozen) so ## gravity drops them. Wired by Main._build_rack (e.g. post.supports.append(shelf)). ## Losing ANY primary support drops the load — arcade feel, no partial-support math. var supports: Array[Node] = [] var _released := false # so a piece is only released once (idempotent, cycle-safe) func _ready() -> void: add_to_group("smashable") var p: Dictionary = PROFILES.get(kind, PROFILES["wood"]) physics_material_override = PhysicsMaterial.new() physics_material_override.bounce = p["bounce"] if start_frozen: freeze_mode = RigidBody3D.FREEZE_MODE_STATIC freeze = true if float(p["brittle_speed"]) > 0.0: contact_monitor = true max_contacts_reported = 4 body_entered.connect(_on_body_entered) func _physics_process(_dt: float) -> void: if not freeze: _speed = linear_velocity.length() func _on_body_entered(_body: Node) -> void: if _shattered: return var p: Dictionary = PROFILES.get(kind, PROFILES["wood"]) if _speed >= float(p["brittle_speed"]): shatter(Vector3.ZERO) ## Called by the hammer. func smash(impulse: Vector3) -> void: shatter(impulse) func shatter(impulse: Vector3) -> void: if _shattered: return _shattered = true var p: Dictionary = PROFILES.get(kind, PROFILES["wood"]) smashed.emit(kind, global_position, int(p["shards"])) _release_supported() # drop what I was holding up BEFORE I leave the tree — the cascade _spawn_shards(int(p["shards"]), p["color"], impulse) queue_free() ## Wake every piece this one was holding up. Guarded on both ends (each child's own ## _released flag) so a shared load or an accidental cycle can't loop. func _release_supported() -> void: for node in supports: if is_instance_valid(node) and node is Smashable: node.release() ## Turn a frozen support into a live dynamic body and give it a comedic nudge so it ## visibly topples instead of sinking straight down. Idempotent: two legs releasing ## the same shelf only drop it once. func release() -> void: if _released: return _released = true if _shattered: return # already gone; nothing to drop if freeze: freeze = false # gravity + forces resume immediately sleeping = false # defensive against a Jolt sleep frame apply_central_impulse(Vector3(randf_range(-0.6, 0.6), 0.15, randf_range(-0.6, 0.6))) func _spawn_shards(n: int, color: Color, impulse: Vector3) -> void: if n <= 0: return var parent := get_parent() if parent == null: return var mat := StandardMaterial3D.new() mat.albedo_color = color var inherited := linear_velocity for i in n: var s := RigidBody3D.new() s.add_to_group("debris") s.mass = 0.08 var sz := randf_range(0.03, 0.09) var mesh := MeshInstance3D.new() var bm := BoxMesh.new() bm.size = Vector3(sz, sz, sz) mesh.mesh = bm mesh.material_override = mat s.add_child(mesh) var col := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = bm.size col.shape = shape s.add_child(col) parent.add_child(s) s.global_position = global_position + Vector3(randf_range(-0.08, 0.08), randf_range(-0.08, 0.08), randf_range(-0.08, 0.08)) var kick := Vector3(randf_range(-1, 1), randf_range(0.3, 1.4), randf_range(-1, 1)).normalized() * randf_range(0.6, 2.2) s.linear_velocity = inherited + kick + impulse * 0.3 s.angular_velocity = Vector3(randf_range(-7, 7), randf_range(-7, 7), randf_range(-7, 7))