NEON-29: End column stair ghost-walk (arrive + stuck release).

Traces showed has_goal=true, vlat&&ncol, MOVE_SPEED XZ flipping while
stable=false: vert_err ~0.31 stayed above VERT_ARRIVE_EPS so arrival never
cleared the goal — not idle jitter.

Run vertical-route latch before arrival; when on floor with vlat&&ncol,
allow vert_err up to WALK_COLUMN_NEAR_ARRIVE_VERT (0.36 m) if horiz_dist
<= ARRIVE_EPS. Backup: clear_nav_goal after WALK_COLUMN_STUCK_FRAMES with
net drift < WALK_COLUMN_STUCK_MAX_DRIFT in the same regime.
pull/41/head
VinPropane 2026-04-12 19:57:54 -04:00
parent 50a8b609be
commit 2333d29ece
1 changed files with 68 additions and 23 deletions

View File

@ -49,6 +49,15 @@ const WALK_TREAD_FREEZE_VERT_ERR_MAX: float = 0.18
## alternate with full snap each tick (~y≈1.0 jitter). Require this many consecutive probe-fail frames
## before arming peel/snap aggression; [member WALK_DEEP_DESCENT_FEET_ABOVE_GOAL] still arms immediately.
const WALK_PEEL_PROBE_FALSE_FRAMES: int = 2
## [member VERT_ARRIVE_EPS] is too tight on column stairs: feet oscillate ~±6 cm vs goal surface while XZ is
## already inside [member ARRIVE_EPS] → never [code]has_goal=false[/code], full [member MOVE_SPEED] path
## steer flips forever (looks like “idle” jitter; NEON-16 traces).
const WALK_COLUMN_NEAR_ARRIVE_VERT: float = 0.36
## If [code]vlat && ncol[/code] and net displacement stays below this for [member WALK_COLUMN_STUCK_FRAMES],
## clear the walk goal — path lookahead limit cycle with no progress (backup if widened arrival is not enough).
const WALK_COLUMN_STUCK_FRAMES: int = 28
const WALK_COLUMN_STUCK_MAX_DRIFT: float = 0.045
const WALK_COLUMN_STUCK_HORIZ_MAX: float = 0.68
## Hysteresis for [code]needs_vertical_routing[/code]: [code]feet_y[/code] wobbles with
## [code]move_and_slide[/code] on treads (~7 cm), so a single margin (see [member DESCEND_GOAL_Y_MARGIN])
## can flip path vs direct steering every tick → velocity sign ping-pong while [code]has_goal[/code].
@ -160,6 +169,8 @@ var _walk_ledge_peel_vy: float = 0.0
var _walk_peel_no_probe_streak: int = 0
## True after streak / deep-descent check; drives zero snap and [method _apply_walk_post_slide_ledge_peel].
var _walk_debounced_wants_ledge_peel: bool = false
var _walk_column_stuck_frame: int = 0
var _walk_column_stuck_origin: Vector3 = Vector3.ZERO
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
@ -187,6 +198,7 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void:
_walk_ledge_peel_vy = 0.0
_walk_peel_no_probe_streak = 0
_walk_debounced_wants_ledge_peel = false
_walk_column_stuck_frame = 0
_nav_agent.set_target_position(world_pos)
@ -205,6 +217,7 @@ func clear_nav_goal() -> void:
_walk_vert_route_latched = false
_walk_peel_no_probe_streak = 0
_walk_debounced_wants_ledge_peel = false
_walk_column_stuck_frame = 0
_nav_agent.set_target_position(global_position)
@ -494,6 +507,29 @@ func _apply_walk_air_gravity(delta: float, _feet_y: float, _move_dir_xz: Vector2
velocity += _player_vertical_accel(delta) * delta
func _walk_try_release_column_stuck_orbit(horiz_dist: float) -> bool:
if not _has_walk_goal or not is_on_floor():
_walk_column_stuck_frame = 0
return false
if not (_walk_vert_route_latched and _walk_nav_column_steering):
_walk_column_stuck_frame = 0
return false
if horiz_dist > WALK_COLUMN_STUCK_HORIZ_MAX:
_walk_column_stuck_frame = 0
return false
if _walk_column_stuck_frame == 0:
_walk_column_stuck_origin = global_position
_walk_column_stuck_frame += 1
if _walk_column_stuck_frame < WALK_COLUMN_STUCK_FRAMES:
return false
var drift: float = global_position.distance_to(_walk_column_stuck_origin)
_walk_column_stuck_frame = 0
if drift > WALK_COLUMN_STUCK_MAX_DRIFT:
return false
clear_nav_goal()
return true
func _walk_peel_suspend_near_goal(feet_y: float, horiz_dist: float) -> bool:
var vsep: float = absf(_auth_walk_goal.y - feet_y)
if _walk_vert_route_latched and _walk_nav_column_steering and vsep < WALK_PEEL_SUSPEND_VERT_SEP:
@ -1057,15 +1093,33 @@ func _physics_process(delta: float) -> void:
var full_to_goal: Vector3 = _auth_walk_goal - global_position
var horiz_dist: float = Vector2(full_to_goal.x, full_to_goal.z).length()
var want_goal_h: Vector3 = Vector3(full_to_goal.x, 0.0, full_to_goal.z)
# Use actual capsule bottom (CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS = 0.9) so that a
# step-surface goal (e.g. Y=0.3) is never considered "arrived" while the player is still at
# floor height. Using only CAPSULE_HALF_HEIGHT (0.5) gives code-feet at Y=0.4, which is too
# close to step surfaces and causes premature arrival when Jolt nudges the body down to ~0.8
# while the capsule hemisphere contacts the step edge.
# Feet + vertical routing **before** arrival: column stair oscillation keeps [code]vert_err[/code]
# above [member VERT_ARRIVE_EPS] while XZ is already inside [member ARRIVE_EPS] — widen vertical
# tolerance only for [code]vlat && ncol[/code] on floor (see [member WALK_COLUMN_NEAR_ARRIVE_VERT]).
var feet_y: float = capsule_feet_y(
global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
)
var vert_sep: float = absf(_auth_walk_goal.y - feet_y)
if not _walk_vert_route_latched:
if vert_sep > WALK_VERT_ROUTE_LATCH_ON_SEP:
_walk_vert_route_latched = true
else:
if vert_sep < WALK_VERT_ROUTE_LATCH_OFF_SEP:
_walk_vert_route_latched = false
var needs_vertical_routing: bool = _walk_vert_route_latched
if not needs_vertical_routing:
_walk_nav_column_steering = false
elif horiz_dist <= NAV_COLUMN_STEER_ENTER_DIST:
_walk_nav_column_steering = true
elif horiz_dist >= NAV_COLUMN_STEER_EXIT_DIST:
_walk_nav_column_steering = false
var vert_err: float = vertical_arrival_error(
_auth_walk_goal.y, global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
)
if horiz_dist <= ARRIVE_EPS and vert_err <= VERT_ARRIVE_EPS:
var vert_ok: float = VERT_ARRIVE_EPS
if is_on_floor() and _walk_vert_route_latched and _walk_nav_column_steering:
vert_ok = maxf(VERT_ARRIVE_EPS, WALK_COLUMN_NEAR_ARRIVE_VERT)
if horiz_dist <= ARRIVE_EPS and vert_err <= vert_ok:
velocity = Vector3.ZERO
_has_walk_goal = false
_walk_ledge_peel_vy = 0.0
@ -1101,31 +1155,22 @@ func _physics_process(delta: float) -> void:
_snap_capsule_upright()
return
if _walk_try_release_column_stuck_orbit(horiz_dist):
floor_block_on_wall = true
_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
_physics_idle_tick(delta)
_debug_trace_transform("physics")
_snap_capsule_upright()
return
_idle_manual_correction_ticks = 0
var feet_y: float = capsule_feet_y(
global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
)
_apply_floor_block_for_descend_at_wall(feet_y)
# Horizontal motion toward the **authoritative** click target (XZ) by default. Use the baked
# path only when vertical routing is latched **and** column hysteresis says so **and** we are
# on floor — mid-air column steering flips XZ velocity each tick against `move_and_slide`
# (NEON-16: `on_floor=false` + `vlat && ncol` while falling).
var vert_sep: float = absf(_auth_walk_goal.y - feet_y)
if not _walk_vert_route_latched:
if vert_sep > WALK_VERT_ROUTE_LATCH_ON_SEP:
_walk_vert_route_latched = true
else:
if vert_sep < WALK_VERT_ROUTE_LATCH_OFF_SEP:
_walk_vert_route_latched = false
var needs_vertical_routing: bool = _walk_vert_route_latched
if not needs_vertical_routing:
_walk_nav_column_steering = false
elif horiz_dist <= NAV_COLUMN_STEER_ENTER_DIST:
_walk_nav_column_steering = true
elif horiz_dist >= NAV_COLUMN_STEER_EXIT_DIST:
_walk_nav_column_steering = false
if needs_vertical_routing and _walk_nav_column_steering and is_on_floor():
_set_horizontal_velocity_from_nav_path_or_goal(want_goal_h)
else: