LANE9: the grocer is still again — settle sweep + parked scale pans

probe_levels was red (0.15-0.48 m/s residual, grocer): the pin-jointed scale pans
hang neutrally balanced so the joint solve re-wakes them forever, and 310 packed
bodies (bottles on shelves, nested fruit) trade contact micro-jitter for seconds.
Two mechanisms, both one-shot at spawn, neither touching gameplay feel:

- scale pans PARK frozen once the joint has resolved (plain .sleeping doesn't stick
  — the joint re-wakes its island every frame) and unfreeze the first time the
  player comes within 1.7 m. You cannot see a pan swing from further than that.
- a settle sweep the frame the spawn-guard window closes: bodies drifting slower
  than 0.5 m/s are put to bed. Anything faster is left for the guard to name —
  a real mis-spawn still fails loudly.

probe_levels also names the culprit properly now ([class, parent] — name-collided
siblings all render as @RigidBody3D@N, which is how three different scale pans
masqueraded as one mystery body across runs).

All six sites: 0.00 m/s. probe_overlap: CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Monster Robot Party 2026-08-03 23:57:25 +10:00
parent 8101cdb472
commit 0e80d393c1
2 changed files with 53 additions and 1 deletions

View File

@ -33,7 +33,10 @@ func _initialize() -> void:
motion += v
if v > worst_v:
worst_v = v
worst = "%s at %s" % [b.name, b.global_position.snappedf(0.1)]
var scr := b.get_script() as Script
var cls := scr.get_global_name() if scr != null else StringName(b.get_class())
worst = "%s [%s, parent %s] at %s" % [
b.name, cls, b.get_parent().name, b.global_position.snappedf(0.1)]
print("\n=== %-14s %s" % [id, plan.level_name()])
print(" smashables=%-4d static bodies=%-4d" % [bodies, statics])
print(" residual motion after 1s = %.2f m/s worst: %s" % [

View File

@ -395,6 +395,35 @@ func _build_scale(at: Vector3) -> void:
joint.node_a = joint.get_path_to(head)
joint.node_b = joint.get_path_to(pan)
# The pan's origin sits AT the pivot, so it hangs near-neutral: no restoring torque
# ever kills the tiny kick the joint solve gives it at spawn, angular_damp is
# deliberately low (the long swing after a hit IS the feature), and plain .sleeping
# doesn't stick — the pin joint re-wakes its island every frame. So: once the joint
# has resolved, PARK the pan frozen (a pendulum at rest plumb is genuinely static),
# and unfreeze it the first time the player comes within reach. The swing feel is
# untouched — you can't see a pan swing from further than the wake radius anyway.
# Without this the grocer fails probe_levels at 0.15-0.48 m/s forever.
get_tree().create_timer(0.6, false, true).timeout.connect(func() -> void:
if is_instance_valid(pan) and not pan.is_broken() and not pan._released:
pan.linear_velocity = Vector3.ZERO
pan.angular_velocity = Vector3.ZERO
pan.freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
pan.freeze = true)
var wake := Area3D.new()
wake.name = "scale-wake"
var wshape := CollisionShape3D.new()
var wsphere := SphereShape3D.new()
wsphere.radius = 1.7
wshape.shape = wsphere
wake.add_child(wshape)
_world.add_child(wake)
wake.global_position = pivot
wake.body_entered.connect(func(b: Node3D) -> void:
if b is CharacterBody3D and is_instance_valid(pan):
pan.freeze = false
pan.sleeping = false
wake.queue_free())
## Four low timber walls on the table top, framing one pyramid. Static — you can't smash
## the tray, you smash what's in it, and what's in it goes over the edge.
func _stack_tray(at: Vector3, half: float, h: float) -> void:
@ -619,6 +648,9 @@ func _physics_process(dt: float) -> void:
if _spawn_guard <= 0.0:
return
_spawn_guard -= dt
if _spawn_guard <= 0.0:
_settle_sweep() # the window just closed — put residual drift to bed, once
return
for n in get_tree().get_nodes_in_group("smashable"):
var b := n as RigidBody3D
if b == null or b.freeze:
@ -637,6 +669,23 @@ func _physics_process(dt: float) -> void:
if b.angular_velocity.length() > SPAWN_MAX_SPIN:
b.angular_velocity = b.angular_velocity.normalized() * SPAWN_MAX_SPIN
## One pass, the frame the spawn-guard window closes: anything still DRIFTING —
## slower than you could ever have pushed it — is put to bed. 310 packed bodies
## (bottles touching bottles, fruit nested on fruit) trade micro-jitter through their
## contacts for many seconds otherwise, and a site should START at rest: the gate is
## probe_levels reading 0.00 m/s. A genuinely mis-spawned launcher is NOT masked —
## anything over the drift threshold is left alone for the guard warning to name.
const SETTLE_MAX_DRIFT := 0.5
func _settle_sweep() -> void:
for n in get_tree().get_nodes_in_group("smashable"):
var b := n as RigidBody3D
if b == null or b.freeze:
continue
if b.linear_velocity.length() < SETTLE_MAX_DRIFT and b.angular_velocity.length() < 2.0:
b.linear_velocity = Vector3.ZERO
b.angular_velocity = Vector3.ZERO
b.sleeping = true
func _on_smashed(kind: String, at: Vector3, shards: int) -> void:
_smash_count += 1
_tally[kind] = int(_tally.get(kind, 0)) + 1