extends Node class_name Splat ## Produce doesn't break. It bursts. ## ## Everything else in this game shatters into shards, which is the wrong verb entirely ## for a tomato. So `produce` gets its own death: a wet burst of pulp in the fruit's own ## colour, a puddle left on the floor that STAYS, a squelch, and a litre counter — because ## "how much juice did you make" is a far better score for a greengrocer than "how many ## objects did you destroy". ## ## Two knock-ons that make the level play itself: ## * Anything heavy enough landing on produce squashes it, so a collapsing pyramid ## crushes its own bottom layer without any special code. Knock one melon off the top ## and the pile does the rest. ## * Puddles are SLIPPERY. Make enough juice and the floor stops cooperating, which is ## a self-inflicted hazard — the more fun you have, the worse the footing gets. ## ## Godot 4.7 GDScript 2.0. ## Litres per item, roughly by size. A watermelon is worth a lot of tomatoes. const YIELD := { "apple": 0.18, "orange": 0.22, "tomato": 0.12, "melon": 2.40, "cabbage": 0.35, "banana": 0.14, "generic": 0.20, } ## Juice colour per item. This is the whole visual identity of the level's mess. const JUICE := { "apple": Color(0.86, 0.78, 0.34), "orange": Color(0.94, 0.52, 0.08), "tomato": Color(0.68, 0.10, 0.07), "melon": Color(0.88, 0.24, 0.30), "cabbage": Color(0.72, 0.82, 0.48), "banana": Color(0.92, 0.85, 0.52), "generic": Color(0.85, 0.55, 0.25), } ## A body must land at least this hard to squash what it lands on. const CRUSH_SPEED := 2.2 ## Puddles beyond this are recycled oldest-first, so a long session stays bounded. const MAX_PUDDLES := 90 ## How close to a puddle counts as standing in it. const PUDDLE_R := 0.55 var litres := 0.0 var squashed := 0 var _host: Node3D var _player: Node3D var _puddles: Array = [] # [{node, pos, r}] var _snd: Array[AudioStreamWAV] = [] var _players: Array[AudioStreamPlayer3D] = [] var _next_player := 0 func setup(host: Node3D, player: Node3D) -> void: _host = host _player = player for i in 3: _snd.append(_squelch(0.9 + i * 0.15)) # a small pool, because a collapsing pyramid squashes a dozen things in one frame for i in 8: var p := AudioStreamPlayer3D.new() p.unit_size = 5.0 p.max_db = 2.0 host.add_child(p) _players.append(p) func reset() -> void: litres = 0.0 squashed = 0 for e in _puddles: var n = e["node"] if is_instance_valid(n): n.queue_free() _puddles.clear() ## Which produce is this? Derived from the node name, which Main sets from the GLB ## filename (`produce-apple` -> `apple`). static func kind_of(n: Node) -> String: var s := String(n.name).to_lower() for k in YIELD.keys(): if s.contains(k): return k return "generic" # ---------------------------------------------------------------- the burst ## Called instead of the usual shard spawn when a `produce` Smashable dies. func burst(at: Vector3, item: String, host: Node3D) -> void: var col: Color = JUICE.get(item, JUICE["generic"]) var vol: float = YIELD.get(item, YIELD["generic"]) litres += vol squashed += 1 _pulp(at, col, host, vol) _puddle(at, col, vol) _squelch_at(at) func _pulp(at: Vector3, col: Color, host: Node3D, vol: float) -> void: var p := GPUParticles3D.new() p.one_shot = true p.explosiveness = 1.0 p.amount = clampi(int(28 + vol * 40.0), 24, 140) p.lifetime = 1.5 p.local_coords = false var pm := ParticleProcessMaterial.new() pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE pm.emission_sphere_radius = 0.05 pm.direction = Vector3(0, 1, 0) pm.spread = 80.0 pm.initial_velocity_min = 1.4 pm.initial_velocity_max = 4.6 + vol pm.gravity = Vector3(0, -12.0, 0) pm.scale_min = 0.5 pm.scale_max = 1.6 pm.angular_velocity_min = -500.0 pm.angular_velocity_max = 500.0 p.process_material = pm var mesh := BoxMesh.new() mesh.size = Vector3(0.028, 0.028, 0.028) var mat := StandardMaterial3D.new() mat.albedo_color = col mat.roughness = 0.35 mesh.material = mat p.draw_pass_1 = mesh host.add_child(p) p.global_position = at p.emitting = true p.finished.connect(p.queue_free) ## A flat quad on the floor. Not a decal projector — this is one unshaded quad slightly ## above the carpet, which is cheap, and at grazing angles it reads exactly the same. func _puddle(at: Vector3, col: Color, vol: float) -> void: var r := clampf(0.22 + vol * 0.30, 0.22, 0.95) var mi := MeshInstance3D.new() var q := QuadMesh.new() q.size = Vector2(r * 2.0, r * 2.0) mi.mesh = q var m := StandardMaterial3D.new() m.albedo_color = Color(col.r, col.g, col.b, 0.72) m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED m.cull_mode = BaseMaterial3D.CULL_DISABLED m.render_priority = -1 mi.material_override = m mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _host.add_child(mi) mi.global_position = Vector3(at.x, 0.012, at.z) mi.rotation = Vector3(-PI * 0.5, randf_range(0.0, TAU), 0.0) _puddles.append({"node": mi, "pos": mi.global_position, "r": r}) while _puddles.size() > MAX_PUDDLES: var old = _puddles.pop_front() if is_instance_valid(old["node"]): old["node"].queue_free() func _squelch_at(at: Vector3) -> void: if _players.is_empty(): return var p := _players[_next_player] _next_player = (_next_player + 1) % _players.size() p.stream = _snd[randi() % _snd.size()] p.pitch_scale = randf_range(0.86, 1.18) p.global_position = at p.play() # ---------------------------------------------------------------- slipping ## How slippery the ground is under the player, 0..1. Player reads this to cut its ## acceleration — the more juice you've made, the less the floor cooperates. func slip_under_player() -> float: if _player == null: return 0.0 var here := _player.global_position var worst := 0.0 for e in _puddles: var p: Vector3 = e["pos"] var d := Vector2(here.x - p.x, here.z - p.z).length() var reach: float = float(e["r"]) + PUDDLE_R * 0.5 if d < reach: worst = maxf(worst, 1.0 - d / reach) return worst func hud_line() -> String: if squashed <= 0: return "" var slip := slip_under_player() return "%.1f litres · %d squashed%s" % [ litres, squashed, " · SLIPPERY" if slip > 0.35 else ""] # ---------------------------------------------------------------- audio ## Wet, short, and low. Noise through a falling low-pass, roughly. func _squelch(pitch: float) -> AudioStreamWAV: var rate := 22050 var dur := 0.30 var n := int(rate * dur) var out := PackedFloat32Array() out.resize(n) var lp := 0.0 for i in n: var t := float(i) / float(rate) var env := exp(-t * 14.0) * (1.0 - exp(-t * 200.0)) var raw := (randf() * 2.0 - 1.0) # sweep the filter closed so it goes from a splat to a glop var k := clampf(0.55 - t * 1.4, 0.06, 0.55) lp += (raw - lp) * k var s := lp * 1.5 * env s += sin(TAU * 120.0 * pitch * t) * 0.22 * env out[i] = clampf(s, -1.0, 1.0) var w := AudioStreamWAV.new() w.format = AudioStreamWAV.FORMAT_16_BITS w.mix_rate = rate w.stereo = false var b := PackedByteArray() b.resize(n * 2) for i in n: b.encode_s16(i * 2, int(out[i] * 32767.0)) w.data = b return w