NEON-29: Stable idle latch + Y anchor for approach treads

Hold latched zero-vel idle through STABLE_IDLE_UNLATCH_TICKS consecutive
unstable readings so thin tread lips do not drop into corrective move_and_slide
every other frame. Idle anchor now freezes Y as well as XZ. Reset latch on
goal/snap/arrival; seed latch on arrival when support is stable after idle tick.
Update hold_idle_anchor GdUnit expectation.
pull/41/head
VinPropane 2026-04-12 17:06:30 -04:00
parent ff6c337e1d
commit 8d9ad0ecd3
2 changed files with 30 additions and 3 deletions

View File

@ -32,6 +32,8 @@ const IDLE_RIM_MIN_FLOOR_UP_DOT: float = 0.968
const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.999
## Rare edge contacts can keep corrective nudges alive forever; budget them, then hold x/z.
const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 8
## Consecutive unstable-idle physics ticks before leaving latched stable idle (thin tread lip).
const STABLE_IDLE_UNLATCH_TICKS: int = 2
## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**).
const IDLE_RIM_SETTLE_STEP: float = 0.004
## Used by tests and vertical routing checks (feet vs goal surface).
@ -77,6 +79,10 @@ var _step_assist_cooldown: int = 0
var _step_assist_active: bool = false
var _idle_anchor_active: bool = false
var _idle_anchor_xz: Vector2 = Vector2.ZERO
var _idle_anchor_y: float = 0.0
## Latched stable idle: stay on zero-vel + anchor through brief Jolt false negatives on treads.
var _idle_stable_latched: bool = false
var _idle_stable_unlatch_streak: int = 0
var _idle_manual_correction_ticks: int = 0
var _debug_last_idle_xz: Vector2 = Vector2.INF
var _debug_idle_heartbeat: int = 0
@ -103,6 +109,8 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void:
_step_assist_active = false
_idle_anchor_active = false
_idle_manual_correction_ticks = 0
_idle_stable_latched = false
_idle_stable_unlatch_streak = 0
_walk_nav_column_steering = false
_nav_agent.set_target_position(world_pos)
@ -114,6 +122,8 @@ func clear_nav_goal() -> void:
_step_assist_active = false
_idle_anchor_active = false
_idle_manual_correction_ticks = 0
_idle_stable_latched = false
_idle_stable_unlatch_streak = 0
_walk_nav_column_steering = false
_nav_agent.set_target_position(global_position)
@ -151,6 +161,8 @@ func snap_to_server(world_pos: Vector3) -> void:
_step_assist_active = false
_idle_anchor_active = false
_idle_manual_correction_ticks = 0
_idle_stable_latched = false
_idle_stable_unlatch_streak = 0
_walk_nav_column_steering = false
_nav_agent.set_target_position(settled)
reset_physics_interpolation()
@ -416,9 +428,10 @@ func _hold_idle_anchor() -> void:
var pos := global_position
if not _idle_anchor_active:
_idle_anchor_xz = Vector2(pos.x, pos.z)
_idle_anchor_y = pos.y
_idle_anchor_active = true
return
global_position = Vector3(_idle_anchor_xz.x, pos.y, _idle_anchor_xz.y)
global_position = Vector3(_idle_anchor_xz.x, _idle_anchor_y, _idle_anchor_xz.y)
func _debug_trace_idle_state(tag: String) -> void:
@ -624,7 +637,17 @@ func _physics_process(delta: float) -> void:
)
if not _has_walk_goal:
floor_block_on_wall = true
if _stable_idle_support():
var raw_stable: bool = _stable_idle_support()
if raw_stable:
_idle_stable_unlatch_streak = 0
_idle_stable_latched = true
elif _idle_stable_latched:
_idle_stable_unlatch_streak += 1
if _idle_stable_unlatch_streak >= STABLE_IDLE_UNLATCH_TICKS:
_idle_stable_latched = false
_idle_stable_unlatch_streak = 0
_idle_anchor_active = false
if _idle_stable_latched:
_idle_manual_correction_ticks = 0
velocity = Vector3.ZERO
floor_snap_length = FLOOR_SNAP_IDLE
@ -685,10 +708,14 @@ func _physics_process(delta: float) -> void:
_step_assist_active = false
_idle_anchor_active = false
_idle_manual_correction_ticks = 0
_idle_stable_latched = false
_idle_stable_unlatch_streak = 0
_walk_nav_column_steering = false
_nav_agent.set_target_position(global_position)
_physics_idle_tick(delta)
if _stable_idle_support():
_idle_stable_latched = true
_idle_stable_unlatch_streak = 0
_idle_manual_correction_ticks = 0
velocity = Vector3.ZERO
floor_snap_length = FLOOR_SNAP_IDLE

View File

@ -103,7 +103,7 @@ func test_hold_idle_anchor_sets_anchor_first_then_clamps_xz() -> void:
p.global_position = Vector3(1.2, 0.7, 2.3)
p.call("_hold_idle_anchor")
assert_that(p.global_position).is_equal(Vector3(1.0, 0.7, 2.0))
assert_that(p.global_position).is_equal(Vector3(1.0, 0.5, 2.0))
func test_snap_to_server_clears_idle_anchor() -> void: