destroyulator/game/dev/probe_grocer.gd
Monster Robot Party c96c2f1a67 LANE8: the greengrocer — soft stock, stacks that collapse, and litres
The only site whose contents are mostly SOFT, and that one material change rewrites the
level. `produce` is a new Smashable material that throws no shards at all — Splat.gd
bursts it instead: coloured pulp, a puddle that STAYS, a squelch, and litres counted,
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 make the level play itself, both falling out of existing systems rather
than needing new ones:
  * produce has a low brittle_speed, so anything heavy landing on it squashes it — which
    means a pyramid crushes its OWN bottom layer as it collapses, with no special code.
    Knock one off the top and the pile does the rest. Shove test: 257 items -> 45, and
    42 litres of juice out of the collapse.
  * puddles are slippery, cutting ACCELERATION rather than top speed so it reads as
    sliding rather than as being slowed down. The more mess you make, the less the floor
    cooperates.

Stacks are built bottom-up by Main._build_stack and nothing is glued or frozen — they
stand because they are stacked, so pulling one out of the bottom row does what you'd
hope. 258 produce items spawn and settle to 0.00 m/s with no spawn crush. The glass
bottles behind the juice bar are the deliberate exception: one shelf in the room that
still rewards a proper swing, and the contrast between litres and shards is the joke.

Round produce is authored CENTRED, not floor-centred like every other prop in this repo,
because a rolling body whose origin sits on the floor plane wobbles like a loaded die.
_glb_piece grew a `centred` flag for it.

REAL BUG FOUND, and it was costing hits in every level: _strike took the NEAREST collider
of any kind, so a static surface could eat a swing. Leaning over a display table, the
table edge is a few centimetres nearer than the fruit piled on it, so every swing hit the
table and nothing broke — 0/306 for the whole first take. It now prefers a Smashable and
only falls back to loose bodies if there isn't one. The offices were quietly losing hits
on desks and shelves the same way.

tools/gen_produce.py: apple, orange, tomato, watermelon, cabbage, banana hand, glass
juice bottle, display crate. The melon's stripes were radial boxes first time and it came
out looking like a sea mine; they're thin lenses through the middle now.

dev/probe_grocer.gd verifies stacks settle, that produce yields litres and NO debris,
that a collapse crushes, and that none of it leaks to the next site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:38:35 +10:00

84 lines
2.8 KiB
GDScript

extends SceneTree
## The greengrocer: do the pyramids build and SETTLE, does produce burst instead of
## shattering, does a collapse crush its own bottom layer, and does the floor get
## slippery.
## Godot --headless --path game --script dev/probe_grocer.gd
func _initialize() -> void:
var main: Node = (load("res://main.tscn") as PackedScene).instantiate()
get_root().add_child(main)
await process_frame
main._load_level("grocer")
for i in 10:
await physics_frame
var splat = main.get("_splat")
var produce := _count("produce")
print("site : ", main.get("_plan").level_name())
print("produce : %d glass: %d total smashables: %d" % [
produce, _count("glass"), get_nodes_in_group("smashable").size()])
# --- do the stacks hold? ---
for i in 120:
await physics_frame
var moving := 0
var total := 0.0
for n in get_nodes_in_group("smashable"):
var b := n as RigidBody3D
if b == null or b.freeze:
continue
var v := b.linear_velocity.length()
total += v
if v > 0.15:
moving += 1
print("after 2s : %d still moving, total %.2f m/s (stacks should be at rest)" % [
moving, total])
print("survived : %d produce (spawn crush would show up as losses)" % _count("produce"))
# --- squash one: does it burst rather than shard? ---
var before_l: float = splat.litres
var debris_before := get_nodes_in_group("debris").size()
var victim: Smashable = null
for n in get_nodes_in_group("smashable"):
if (n as Smashable).kind == "produce":
victim = n
break
var at := victim.global_position
victim.shatter(Vector3.UP)
await process_frame
print("squash one: litres %.2f -> %.2f debris %d -> %d (must NOT rise: fruit has no shards)" % [
before_l, splat.litres, debris_before, get_nodes_in_group("debris").size()])
# --- drop a melon on a pile: does the collapse crush? ---
var n0 := _count("produce")
for n in get_nodes_in_group("smashable"):
var b := n as Smashable
if b != null and b.kind == "produce":
b.apply_central_impulse(Vector3(randf_range(-3, 3), 0, randf_range(-3, 3)))
for i in 150:
await physics_frame
print("shoved all: %d -> %d produce %.1f litres from the collapse" % [
n0, _count("produce"), splat.litres])
# --- slippery? ---
var player = main.get("_player")
player.global_position = Vector3(at.x, 0.0, at.z)
await process_frame
print("slip here : %.2f (0 = dry floor)" % splat.slip_under_player())
print("hud : ", splat.hud_line())
# --- and none of it leaks to the office ---
main._load_level("scranton")
for i in 6:
await physics_frame
print("at office : litres %.1f (reset) produce %d" % [splat.litres, _count("produce")])
quit()
func _count(kind: String) -> int:
var n := 0
for s in get_nodes_in_group("smashable"):
if (s as Smashable) != null and (s as Smashable).kind == kind:
n += 1
return n