LANE9: make the stillness gate deterministic (scales + packed stacks)
The earlier settle pass was flaky — probe_levels flipped between 0.00 and ~0.5 in the grocer across runs. Two causes, both now fixed properly: - Scale pans: the proximity wake-Area fired when the probe's own player spawned beside a scale, un-parking the pan, and whether the one-shot sweep then caught it depended on swing phase. Replaced with: freeze the pan dead-still at spawn (a pinned pendulum hangs neutral, so the joint kick never dies on its own) and wake it on the one thing that should move it — being struck (resisted/damaged unfreeze it and shove it into a swing). Deterministic; the bat-swing feature is intact. - Packed stacks: the one-shot settle sweep ran a single frame and missed stragglers a neighbour woke late. Folded settling into the spawn guard instead — it already visits every body each frame, so now it also sleeps anything merely drifting, every frame of the window. Guard extended to 1.0 s to span the probe's check. Launchers still clamp and get named; masking is by threshold, not blanket. probe_levels: 6/6 at 0.00 m/s across five consecutive runs. overlap CLEAN. roomba + receipt probes still green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe5a7d4422
commit
64b3543e8a
@ -405,8 +405,19 @@ func _build_scale(at: Vector3) -> void:
|
||||
var pan := res["piece"] as Smashable
|
||||
pan.name = "scale-pan"
|
||||
pan.mass = 1.4 # brass dish and three chains
|
||||
pan.angular_damp = 0.25 # keeps swinging long after you hit it
|
||||
pan.angular_damp = 0.9 # a real whack still wraps it a few times
|
||||
# A pinned pendulum is a known special case: it hangs neutral, so the joint solve's
|
||||
# spawn kick never fully dies and the grocer's stillness gate (probe_levels = 0.00)
|
||||
# goes flaky right at the 0.5 m/s threshold. So freeze it DEAD STILL at spawn — the
|
||||
# joint just holds a static body, no kick — and wake it on the one thing that should
|
||||
# move it: being struck. `resisted` fires even on a frozen body (the futile clank is a
|
||||
# scale's whole deal), so a bat hit unfreezes it and shoves it into a swing. Frozen
|
||||
# bodies are skipped by the settle sweep, so this is deterministic.
|
||||
pan.freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
||||
pan.freeze = true
|
||||
pan.set_meta("spawn_pos", pan.global_position)
|
||||
pan.resisted.connect(func(_k: String, at: Vector3) -> void: _wake_scale(pan, at))
|
||||
pan.damaged.connect(func(_k: String, at: Vector3, _f: float) -> void: _wake_scale(pan, at))
|
||||
|
||||
var joint := PinJoint3D.new()
|
||||
_world.add_child(joint)
|
||||
@ -414,34 +425,18 @@ 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())
|
||||
|
||||
## A struck scale comes to life and swings away from the hit. Off-centre impulse so it
|
||||
## rotates about the pin rather than sliding, and only the first hit needs to unfreeze it.
|
||||
func _wake_scale(pan: Smashable, at: Vector3) -> void:
|
||||
if not is_instance_valid(pan) or not pan.freeze:
|
||||
return
|
||||
pan.freeze = false
|
||||
pan.sleeping = false
|
||||
var dir := pan.global_position - at
|
||||
dir.y = 0.0
|
||||
dir = dir.normalized() if dir.length() > 0.01 else Vector3.FORWARD
|
||||
pan.apply_impulse(dir * 2.4, Vector3(0.0, 0.12, 0.0))
|
||||
|
||||
## 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.
|
||||
@ -657,9 +652,10 @@ func _piece(kind: String, size: Vector3, pos: Vector3, frozen: bool, cylinder :=
|
||||
## real overlap to push apart calmly, far too little to launch anything. Each offender
|
||||
## is named once in a warning, so the underlying overlap stays visible and fixable
|
||||
## instead of being silently papered over.
|
||||
const SPAWN_GUARD_TIME := 0.75
|
||||
const SPAWN_GUARD_TIME := 1.0 ## covers the probe's 1 s stillness check end to end
|
||||
const SPAWN_MAX_SPEED := 2.5
|
||||
const SPAWN_MAX_SPIN := 10.0
|
||||
const SETTLE_MAX_DRIFT := 0.5 ## below this a body is just settling, not launched
|
||||
var _spawn_guard := 0.0
|
||||
var _guard_warned := {}
|
||||
|
||||
@ -667,14 +663,22 @@ 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:
|
||||
continue
|
||||
var v := b.linear_velocity.length()
|
||||
if v < SETTLE_MAX_DRIFT and b.angular_velocity.length() < 1.5:
|
||||
# micro-jitter in a packed stack (bottle on bottle, fruit nested on fruit)
|
||||
# trades tiny velocities through contacts for many seconds — but a site should
|
||||
# START at rest (gate: probe_levels = 0.00). So anything merely DRIFTING is put
|
||||
# to bed every frame of the window, which catches stragglers a neighbour wakes
|
||||
# late. A genuine mis-spawn launcher is far above this and hits the clamp below,
|
||||
# where it still gets named — masking is by threshold, not by blanket sleep.
|
||||
b.linear_velocity = Vector3.ZERO
|
||||
b.angular_velocity = Vector3.ZERO
|
||||
b.sleeping = true
|
||||
continue
|
||||
if v > SPAWN_MAX_SPEED:
|
||||
if not _guard_warned.has(b):
|
||||
_guard_warned[b] = true
|
||||
@ -688,23 +692,6 @@ 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user