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 ## Pre-fractured sibling (Lane 2 contract #2): a scene whose leaf `chunk_*` meshes are ## the real shards. When set, shatter() spawns those instead of primitive cubes. Main ## auto-assigns it from a `.fractured.glb` next to the intact GLB. @export var fractured_scene: PackedScene = null ## Hits to destroy. 1 = normal (one smash breaks it). The printer boss takes several. @export var hits_to_break: int = 1 ## Boss props get a bigger death burst (more juice particles) + survive extra hits. @export var is_boss: bool = false var _hits_taken := 0 # 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. Bosses (hits_to_break > 1) soak hits and visibly rock before ## the final blow shatters them; a normal prop (hits_to_break == 1) breaks on hit one. func smash(impulse: Vector3) -> void: if _shattered: return _hits_taken += 1 if _hits_taken < hits_to_break: # survives this hit — a physical knockback so the boss reacts to the beating sleeping = false apply_central_impulse(impulse.limit_length(6.0) * 0.25) return shatter(impulse) func shatter(impulse: Vector3) -> void: if _shattered: return _shattered = true var p: Dictionary = PROFILES.get(kind, PROFILES["wood"]) var burst := 24 if is_boss else int(p["shards"]) # boss dies in a bigger cloud of juice smashed.emit(kind, global_position, burst) _release_supported() # drop what I was holding up BEFORE I leave the tree — the cascade if fractured_scene != null: _spawn_chunks(impulse) # real GLB shards (Lane 2) else: _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))) ## Real destruction: instance the pre-fractured sibling and turn each `chunk_*` mesh into ## its own rigid body that inherits our velocity + the smash impulse. Chunks are named ## chunk_00…chunk_NN with each origin at its own centroid, in this model's local space ## (Lane 2 contract #2). We add the template at our transform so each chunk's global ## transform is already correct, then reparent it onto a fresh body. Debris group so the ## HUD count + Juice keep working. Falls back to primitive shards if the template is empty. func _spawn_chunks(impulse: Vector3) -> void: var parent := get_parent() if parent == null: return var tmpl: Node3D = fractured_scene.instantiate() parent.add_child(tmpl) tmpl.global_transform = global_transform # chunks now sit exactly where the intact body was var chunks: Array[MeshInstance3D] = [] _collect_chunk_meshes(tmpl, chunks) if chunks.is_empty(): tmpl.queue_free() var p: Dictionary = PROFILES.get(kind, PROFILES["wood"]) _spawn_shards(int(p["shards"]), p["color"], impulse) return var inherited := linear_velocity var here := global_position for chunk in chunks: var gt := chunk.global_transform var body := RigidBody3D.new() body.add_to_group("debris") body.mass = 0.25 # reparent the chunk mesh onto the body, centred on the body origin chunk.owner = null chunk.get_parent().remove_child(chunk) body.add_child(chunk) chunk.transform = Transform3D.IDENTITY var ab := chunk.get_aabb() var col := CollisionShape3D.new() var box := BoxShape3D.new() box.size = ab.size.max(Vector3(0.02, 0.02, 0.02)) col.shape = box col.position = ab.get_center() body.add_child(col) parent.add_child(body) body.global_transform = gt # fling outward from the smash centre + inherit motion + the hit impulse var out := (gt.origin - here) out = out.normalized() if out.length() > 0.001 else Vector3(randf_range(-1, 1), 0.5, randf_range(-1, 1)).normalized() body.linear_velocity = inherited + out * randf_range(1.2, 3.2) + impulse * 0.3 body.angular_velocity = Vector3(randf_range(-8, 8), randf_range(-8, 8), randf_range(-8, 8)) tmpl.queue_free() # the now-empty template wrapper func _collect_chunk_meshes(n: Node, acc: Array[MeshInstance3D]) -> void: if n is MeshInstance3D and n.name.to_lower().begins_with("chunk"): acc.append(n) for c in n.get_children(): _collect_chunk_meshes(c, acc) 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))