extends CharacterBody3D

## NS-23 path-follow; NS-24 idle. NEO-22: WASD locomotion via [method set_locomotion_wish_world_xz].
## Details in `client/README.md` (movement, idle).
##
## Summary: Jolt, no interp; dual floor_max_angle; rim/straddle, bump lip escape.
## Snap upright after motion. Do not set global_transform in _process.
## Walk assist may run a second move_and_slide().

const MOVE_SPEED: float = 5.0
## Moving: floor angle for seams onto stepped QA bumps (~50°).
const FLOOR_MAX_ANGLE_MOVING_DEG: float = 50.0
## Idle: tighter angle to reduce vertex / vertical-face flicker (~35°).
const FLOOR_MAX_ANGLE_IDLE_DEG: float = 35.0
## Physics ticks to keep moving floor angle after walk goal clears (arrival).
## Post-walk window for wider [member floor_max_angle] (~0.4 s @ 120 Hz); shorter tightens idle
## contact sooner and helps the anchor latch feel snappy on flat / mild slopes.
const FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP: int = 48
const ARRIVE_EPS: float = 0.35
const VERT_ARRIVE_EPS: float = 0.055
const DIRECT_APPROACH_RADIUS: float = 0.85
## Hysteresis for nav-path column steering (see `_physics_process`). At exactly
## [member DIRECT_APPROACH_RADIUS] the old test flipped every frame: path nudges
## tangentially → [code]horiz_dist[/code] crosses the radius → direct vs path
## alternates → velocity sign flips (Jolt ping-pong while [code]has_goal[/code]).
const NAV_COLUMN_STEER_ENTER_DIST: float = 0.74
const NAV_COLUMN_STEER_EXIT_DIST: float = 0.97
## First path waypoint used for column steer must be at least this far in XZ. [method
## NavigationAgent3D.get_next_path_position]
## can sit ~4 cm from the capsule; Jolt slides ~4 cm/tick → the “toward next” vector flips 180°
## every frame
## (`vlat && ncol` NEON-16 traces). A 22 cm gate matches foot-scale probes and stays stable on
## treads.
const NAV_PATH_STEER_MIN_LOOKAHEAD: float = 0.22
## If lookahead steering points more than ~105° from the click direction (XZ), use direct goal steer
## instead.
const NAV_PATH_STEER_MIN_GOAL_DOT: float = -0.25
## When the new lookahead unit is almost opposite the last applied nav steer, slerp slowly — stops
## ±[code]MOVE_SPEED[/code]
## flips while [code]vlat && ncol[/code] (Jolt nudges XZ → different first waypoint past [member
## NAV_PATH_STEER_MIN_LOOKAHEAD]).
const NAV_PATH_STEER_SMOOTH_DOT: float = 0.22
const NAV_PATH_STEER_SMOOTH_SLERP: float = 0.58
const NAV_PATH_STEER_SMOOTH_SLERP_OPPOSING: float = 0.16
## While [code]vlat && ncol[/code], ledge peel + zero floor snap fight [method move_and_slide] on
## approach treads
## (3-cycle NEON-16 traces: same XZ triplet, Y/feet_y ping-pong). Suspend only when
## [code]vsep[/code] is
## **tread-scale** — if this matched tall-pad descents (~0.48 m), column steering stays true while
## [code]vsep[/code]
## shrinks from metres to zero and peel was off for the last ~half metre (orange block slow float).
## Just above **two** QA tread drops (~0.208 m) so double risers still suspend.
const WALK_PEEL_COLUMN_SUSPEND_MAX_VSEP: float = 0.23
## Fallback when [code]vlat[/code] has unlatched (feet and goal both near the same slab Y) but
## [code]ncol[/code] is false because [code]horiz_dist[/code] is past column hysteresis — peel +
## zero
## snap still oscillates on violet treads (NEON-16 ~y=0.92). Slightly below a single tall lip so
## shallow
## lips (~0.35–0.47 m) still peel vs [member WALK_PEEL_SUSPEND_HORIZ_VERT_SEP].
const WALK_PEEL_SUSPEND_HORIZ_VERT_SEP: float = 0.46
const WALK_PEEL_SUSPEND_HORIZ_DIST: float = 5.0
## Broad [method _walk_peel_suspend_near_goal] (horiz + [member WALK_PEEL_SUSPEND_HORIZ_VERT_SEP])
## is for
## tread-height wobble when XZ is already near the click. On **tall pads** (orange reject block,
## terraces),
## feet can stay tens of cm above the goal while [code]horiz_dist < 5[/code] — suspend then kills
## ledge peel
## for the whole last ~0.46 m of drop → [code]is_on_floor()[/code] + no [code]velocity.y[/code]
## feels like a slow float.
## Only allow that broad branch once the feet are within this of the goal surface **while
## descending**.
const WALK_PEEL_SUSPEND_BROAD_MAX_FEET_ABOVE_GOAL: float = 0.22
## With [method _walk_peel_suspend_near_goal] (tread snap/peel stability), zero XZ [member velocity]
## when
## close in XZ but vertical arrival still unresolved — avoids band-edge flicker from a tight
## [code]vert_sep[/code]
## window. [member WALK_PEEL_SUSPEND_HORIZ_DIST] can be 5 m; cap XZ here so we never freeze long
## crosses.
const WALK_TREAD_FREEZE_HORIZ_MAX: float = 0.38
## Freeze only while vertical error is in a “tread wobble” band (past arrival noise, not a big
## drop).
const WALK_TREAD_FREEZE_VERT_ERR_MAX: float = 0.18
## [method _walk_has_close_floor_probe_below] often flips on stairs: next tread is ~0.104 m down but
## [member WALK_CONTINUATION_MAX_BELOW_FEET] is 0.078 m, so rays reject the hit → peel + zero snap
## 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
## Only apply [member WALK_COLUMN_NEAR_ARRIVE_VERT] when vertical error is already in this band —
## avoids
## treating a **plaza / block** drop (feet still >20 cm above goal) as “column stair wobble” while
## [code]horiz_dist[/code] is inside column steering (NEON-29-style skate + slow float after XZ).
const WALK_COLUMN_NEAR_ARRIVE_GATE_VERT_ERR: float = 0.22
## 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
## After [method _walk_clip_horizontal_velocity_against_vertical_contacts], Jolt can alternate which
## vertical face “wins” on stair/column seams → post-clip [code]velocity.xz[/code] flips ~180° at
## ~[code]MOVE_SPEED[/code] while [code]horiz_dist[/code] stays large ([code]ncol[/code]
## hysteresis).
## Dot below this vs last tick’s post-clip XZ → zero horizontal + brief suppress (NEO-14 traces).
const WALK_COL_SEAM_FLIP_DOT: float = -0.7
## Treat post-clip speed as “full fight” when above this fraction of [member MOVE_SPEED].
const WALK_COL_SEAM_SPEED_FRAC: float = 0.82
## Physics ticks to hold [code]velocity.xz == 0[/code] after a seam flip (odd/even frame
## alternation).
const WALK_COL_SEAM_SUPPRESS_TICKS: int = 3
## 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].
const WALK_VERT_ROUTE_LATCH_ON_SEP: float = 0.10
## Stay below [member WALK_VERT_ROUTE_LATCH_ON_SEP]. Old 0.038 unlatched while descending toward
## floor
## (vert_sep under ~4 cm) → [code]vlat[/code]/[code]ncol[/code] false → peel suspend never ran.
const WALK_VERT_ROUTE_LATCH_OFF_SEP: float = 0.085
const FLOOR_SNAP_MOVING: float = 0.32
## Idle floor snap length (stronger to pin rim contacts).
const FLOOR_SNAP_IDLE: float = 0.11
## Below this floor-normal dot up, idle tick uses **moving** `floor_max_angle` (rim).
const IDLE_RIM_MIN_FLOOR_UP_DOT: float = 0.968
## Uniform walkable slopes (e.g. physics QA ramp ~18°): stable idle when not tread+riser ridged.
## Below ~cos(28°): treat as too steep for this fast-path (leave corrective / rim logic).
const IDLE_SLOPE_STABLE_MIN_UP_DOT: float = 0.88
## Stable flat idle support: skip corrective idle motion when support is effectively level.
## 0.998 (~3.6°): 0.999 still saw open-floor HUD drift — Jolt [code]get_floor_normal()[/code] often
## reports ~0.9991–0.9992 on a flat box, so the old 0.999 gate rarely latched the idle anchor and
## [code]move_and_slide[/code] + rim/bump nudges kept nudging XZ.
const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.998
## While [member _idle_stable_latched], allow slightly tilted reported normals (tread/riser edge).
const STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT: float = 0.992
## Rare edge contacts can keep corrective nudges alive forever; budget them, then hold x/z.
const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 5
## Consecutive [code]raw_stable[/code] ticks before first idle anchor latch. Two was safe but Jolt
## heightmaps / slopes often insert a one-frame false negative → ~1 s extra jitter before latch.
const STABLE_IDLE_ENTER_STREAK_FRAMES: int = 1
## When not [code]raw_stable[/code] and not yet latched, decay the enter streak instead of zeroing
## (brief floor-normal flicker no longer wipes two good ticks).
const STABLE_IDLE_ENTER_MISS_DECAY: int = 1
## Consecutive unstable-idle physics ticks before leaving latched stable idle (thin tread lip).
const STABLE_IDLE_UNLATCH_TICKS: int = 10
## 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).
const DESCEND_GOAL_Y_MARGIN: float = 0.06
## Cap snap while dropping off a pad so Jolt cannot glue the capsule to the upper surface.
const DESCEND_LIP_SNAP_CAP: float = 0.1
## Y lift when blocked by wall-ish slide but nav goal is higher (stepped bumps).
const WALK_STEP_ASSIST_DELTA: float = 0.11
const WALK_STEP_ASSIST_COOLDOWN_TICKS: int = 8
## Floor-snap length while step-climbing. Small enough that it cannot reach the lower floor
## (~0.11 m away after the first lift) but large enough to catch the step surface once the
## capsule clears the face (~0.03 m away after the third lift for a 0.3 m step).
const WALK_STEP_ASSIST_SNAP: float = 0.09
## Do not step-assist when the goal surface is farther above the feet than this (meters).
## Matches [NavigationMesh] `agent_max_climb` in `main.tscn` so repeated lifts cannot bypass a
## two-level climb in one click (e.g. floor → **TerracePlatformB** violet deck at ~0.6 m — use
## **TerraceStepB** gold first, or a second click after standing on the step).
const WALK_STEP_ASSIST_MAX_SURFACE_DELTA: float = 0.35
## Ray below capsule feet while walking: if [method CharacterBody3D.is_on_floor] stays true off a
## ledge (snap + lip contacts), we still need gravity toward the lower floor — without re-applying
## gravity for every “goal below feet” tick on a whole upper deck (see [member
## _apply_walk_air_gravity]).
## Down-ray length under the capsule foot disk while walking (~[member
## NavigationMesh.agent_max_climb]).
const WALK_SUPPORT_PROBE_DEPTH: float = 0.32
const WALK_SUPPORT_PROBE_MIN_UP_DOT: float = 0.42
## Ignore ray hits farther below the feet than this (m). Set just **above** QA tread rise (~0.104 m
## in
## [code]main.tscn[/code]) so the next tread usually counts as continuation — stabilizes debounced
## peel/snap
## on violet stairs. Must stay **below** ~0.25 m so a gold→floor (~0.3 m) void still rejects the
## slab hit
## (no horizontal skate off the deck).
const WALK_CONTINUATION_MAX_BELOW_FEET: float = 0.108
## If capsule feet are farther above the click’s surface Y than this, always arm ledge peel (bypass
## probe streak). Keep this **just above** a single QA tread drop (~0.104 m) so stair treads still
## use
## [method _walk_has_close_floor_probe_below]; use a **small** value so tall drops (block ~2 m,
## terrace
## ~0.6 m) never exit this “deep” band until feet are almost on the goal surface — otherwise the
## last
## ~40 cm reverted to probe-only, lip [code]is_on_floor()[/code] + full snap could **horizontal
## skate**
## with almost no peel until XZ arrival.
const WALK_DEEP_DESCENT_FEET_ABOVE_GOAL: float = 0.12
## [method get_gravity] can be ~0 with missing/odd project defaults; ledge peel + airborne walk
## still need a pull.
const WALK_FALLBACK_GRAVITY_Y: float = -9.81
## Multiplier on project gravity for walk ledge peel, walk-airborne, and idle-airborne (arcade
## descent).
const PLAYER_VERTICAL_ACCEL_MULTIPLIER: float = 9.0
## Scale [method _apply_walk_post_slide_ledge_peel] integration when feet are still far above a
## **lower**
## goal (block / terrace lip). Keeps peel sub-linear time from feeling like a hover; tread-sized
## drops stay ~1×.
const WALK_LEDGE_PEEL_BOOST_SEP_1: float = 0.28
const WALK_LEDGE_PEEL_BOOST_SEP_2: float = 0.75
const WALK_LEDGE_PEEL_BOOST_SEP_3: float = 1.35
const WALK_LEDGE_PEEL_BOOST_MUL_1: float = 1.65
const WALK_LEDGE_PEEL_BOOST_MUL_2: float = 2.75
const WALK_LEDGE_PEEL_BOOST_MUL_3: float = 4.25
## CapsuleShape3D in scene: height = 1.0 (cylinder portion), radius = 0.4.
## Total half-height from body origin to physical bottom = 0.5 + 0.4 = 0.9
## (`CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS`).
## `CAPSULE_HALF_HEIGHT` alone (0.5) is the cylinder half; used only where that
## distinction matters (e.g. the descend-bypass feet estimate).
const CAPSULE_HALF_HEIGHT: float = 0.5
const BUMP_COLLISION_CONSTS_SCRIPT: Script = preload(
	"res://scripts/random_floor_bump_collision_constants.gd"
)
const PLAYER_LOCOMOTION_WASD_SCRIPT := preload("res://scripts/player_locomotion_wasd.gd")
## Idle radial push per tick off bump lip / wall (meters; 120 Hz physics).
const IDLE_BUMP_ESCAPE_STEP: float = 0.014
## Player capsule radius; wall touch can happen past axis distance **`col_r`**.
const PLAYER_CAPSULE_RADIUS: float = 0.4
const DEBUG_IDLE_TRACE_DELTA_EPS: float = 0.0001
const DEBUG_IDLE_TRACE_HEARTBEAT_FRAMES: int = 60
const DEBUG_TRANSFORM_TRACE_DELTA_EPS: float = 0.000001
@export var debug_idle_trace: bool = false

var _has_walk_goal: bool = false
var _auth_walk_goal: Vector3 = Vector3.ZERO
var _floor_angle_loose_ticks: int = 0
var _step_assist_cooldown: int = 0
## True while the player is actively climbing a step; suppresses the normal large floor
## snap so it cannot pull the capsule back to the lower surface between assist cycles.
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_stable_enter_streak: int = 0
var _idle_manual_correction_ticks: int = 0
var _debug_last_idle_xz: Vector2 = Vector2.INF
var _debug_idle_heartbeat: int = 0
var _debug_last_transform_xz: Vector2 = Vector2.INF
var _debug_trace_frame: int = 0
## True while we use [method _set_horizontal_velocity_from_nav_path_or_goal] for vertical routing.
var _walk_nav_column_steering: bool = false
## Last horizontal nav-path steer unit (XZ); eases 180° waypoint switches (see [member
## NAV_PATH_STEER_SMOOTH_DOT]).
var _walk_nav_path_steer_hz: Vector3 = Vector3.ZERO
## Schmitt latch for vertical routing (see [member WALK_VERT_ROUTE_LATCH_ON_SEP]).
var _walk_vert_route_latched: bool = false
## GROUNDED [method move_and_slide] clears downward [code]velocity.y[/code] on floor; accumulate
## peel
## speed here and apply [code]global_position.y[/code] so terrace lips can actually drop.
var _walk_ledge_peel_vy: float = 0.0
## Pre-[method move_and_slide] debounce for probe flicker (see [member
## WALK_PEEL_PROBE_FALSE_FRAMES]).
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
## Post-clip XZ from last physics tick; used with [member WALK_COL_SEAM_FLIP_DOT] while [code]vlat
## && ncol[/code].
var _walk_col_seam_prev_hz: Vector2 = Vector2.ZERO
var _walk_col_seam_suppress_ticks: int = 0
## Normalized world XZ direction from WASD (Y ignored). Zero when idle.
var _locomotion_wish_world_xz: Vector3 = Vector3.ZERO
var _wasd_locomotion: RefCounted
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D


func _ready() -> void:
	physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
	_nav_agent.avoidance_enabled = false
	_nav_agent.set_target_position(global_position)
	floor_block_on_wall = true
	_wasd_locomotion = PLAYER_LOCOMOTION_WASD_SCRIPT.new(
		self, CAPSULE_HALF_HEIGHT, PLAYER_CAPSULE_RADIUS, MOVE_SPEED, FLOOR_SNAP_MOVING
	)
	if debug_idle_trace:
		set_notify_transform(true)


func set_authoritative_nav_goal(world_pos: Vector3) -> void:
	_auth_walk_goal = world_pos
	_has_walk_goal = true
	_floor_angle_loose_ticks = 0
	_step_assist_active = false
	_idle_anchor_active = false
	_idle_manual_correction_ticks = 0
	_idle_stable_latched = false
	_idle_stable_unlatch_streak = 0
	_idle_stable_enter_streak = 0
	_walk_nav_column_steering = false
	_walk_vert_route_latched = false
	_walk_nav_path_steer_hz = Vector3.ZERO
	_walk_ledge_peel_vy = 0.0
	_walk_peel_no_probe_streak = 0
	_walk_debounced_wants_ledge_peel = false
	_walk_column_stuck_frame = 0
	_walk_col_seam_prev_hz = Vector2.ZERO
	_walk_col_seam_suppress_ticks = 0
	_nav_agent.set_target_position(world_pos)


func clear_nav_goal() -> void:
	velocity = Vector3.ZERO
	_walk_ledge_peel_vy = 0.0
	_has_walk_goal = false
	_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
	_step_assist_active = false
	_idle_anchor_active = false
	_idle_manual_correction_ticks = 0
	_idle_stable_latched = false
	_idle_stable_unlatch_streak = 0
	_idle_stable_enter_streak = 0
	_walk_nav_column_steering = false
	_walk_vert_route_latched = false
	_walk_nav_path_steer_hz = Vector3.ZERO
	_walk_peel_no_probe_streak = 0
	_walk_debounced_wants_ledge_peel = false
	_walk_column_stuck_frame = 0
	_walk_col_seam_prev_hz = Vector2.ZERO
	_walk_col_seam_suppress_ticks = 0
	_wasd_locomotion.reset_for_nav_or_snap()
	_nav_agent.set_target_position(global_position)


## Repath on a new nav map (e.g. after [NavigationRegion3D] rebake when geometry is removed).
## Rebaking can leave [NavigationAgent3D] on a stale map RID; rebind to the world's map first.
func sync_navigation_agent_after_map_rebuild(nav_region: NavigationRegion3D) -> void:
	var map_rid: RID = RID()
	var w3d := get_world_3d()
	if w3d != null:
		map_rid = w3d.navigation_map
	if map_rid == RID() and is_instance_valid(nav_region):
		map_rid = NavigationServer3D.region_get_map(nav_region.get_region_rid())
	if map_rid != RID():
		_nav_agent.set_navigation_map(map_rid)
	if _has_walk_goal:
		_nav_agent.set_target_position(_auth_walk_goal)
	else:
		_nav_agent.set_target_position(global_position)


func set_locomotion_wish_world_xz(dir: Vector3) -> void:
	var h := Vector3(dir.x, 0.0, dir.z)
	if h.length_squared() < 1e-10:
		_locomotion_wish_world_xz = Vector3.ZERO
		_wasd_locomotion.reset_scrape_feet_lock()
	else:
		_locomotion_wish_world_xz = h.normalized()


func snap_to_server(world_pos: Vector3) -> void:
	# The server stores the surface Y of the last move target, not the capsule body-centre Y.
	# Clamping to the minimum standing height (total half = 0.9 m) prevents the player from
	# being placed underground (which, combined with a stale is_on_floor() returning true,
	# would skip all corrective move_and_slide() calls and leave the capsule stuck).
	var total_half: float = CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
	var settled := Vector3(world_pos.x, maxf(world_pos.y, total_half), world_pos.z)
	global_position = settled
	velocity = Vector3.ZERO
	_has_walk_goal = false
	_walk_ledge_peel_vy = 0.0
	_walk_peel_no_probe_streak = 0
	_walk_debounced_wants_ledge_peel = false
	# Force corrective idle ticks so Jolt resolves any residual physics state before walking.
	_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
	_step_assist_cooldown = 0
	_step_assist_active = false
	_idle_anchor_active = false
	_idle_manual_correction_ticks = 0
	_idle_stable_latched = false
	_idle_stable_unlatch_streak = 0
	_idle_stable_enter_streak = 0
	_walk_nav_column_steering = false
	_walk_vert_route_latched = false
	_walk_nav_path_steer_hz = Vector3.ZERO
	_walk_col_seam_prev_hz = Vector2.ZERO
	_walk_col_seam_suppress_ticks = 0
	_wasd_locomotion.reset_for_nav_or_snap()
	_nav_agent.set_target_position(settled)
	reset_physics_interpolation()
	_locomotion_wish_world_xz = Vector3.ZERO


## Returns true when the capsule has something to stand on **or** is pressing against a step face
## in the direction of travel.  `want_dir_xz` is the normalised horizontal goal direction.
## Using `is_on_wall()` as a blanket short-circuit was too broad: any floor-bump side-contact
## satisfied it, causing the assist to fire prematurely and then be unable to snap back.
func _step_assist_has_support(want_dir_xz: Vector3) -> bool:
	if is_on_floor():
		return true
	var w2 := Vector2(want_dir_xz.x, want_dir_xz.z)
	var have_dir: bool = w2.length_squared() > 1e-8
	if have_dir:
		w2 = w2.normalized()
	for i: int in get_slide_collision_count():
		var n: Vector3 = get_slide_collision(i).get_normal()
		if n.y > 0.35:
			return true  # upward-facing = floor support
		# Wall-ish contact opposing forward direction = step face immediately ahead.
		if have_dir and abs(n.y) < 0.52:
			var n2 := Vector2(n.x, n.z)
			if n2.length_squared() > 1e-8 and n2.normalized().dot(w2) < -0.2:
				return true
	return false


## True when a vertical-ish contact opposes motion toward the goal. Jolt often reports a wall
## via `is_on_wall()` + `get_wall_normal()` at box lips while slide normals are ambiguous; the
## old slide-only loop missed head-on climbs onto e.g. TerracePlatformA (teal pad).
func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool:
	var w2 := Vector2(want_dir_xz.x, want_dir_xz.z)
	if w2.length_squared() < 1e-8:
		return false
	w2 = w2.normalized()
	if is_on_wall():
		var wn: Vector3 = get_wall_normal()
		var wnh := Vector2(wn.x, wn.z)
		if wnh.length_squared() > 1e-10:
			wnh = wnh.normalized()
			# Wall pushes back against forward (not parallel skimming along the face).
			if wnh.dot(w2) < -0.02:
				return true
	for i: int in get_slide_collision_count():
		var n: Vector3 = get_slide_collision(i).get_normal()
		if n.y > 0.52:
			continue
		var n2 := Vector2(n.x, n.z)
		if n2.length_squared() < 1e-8:
			continue
		n2 = n2.normalized()
		if n2.dot(w2) < -0.02:
			return true
	return false


## Remove horizontal velocity into **vertical** contacts using last tick's slide normals (and
## [method get_wall_normal] when [method is_on_wall]). Capsule lips / floor seams often never set
## [method is_on_wall] while [method move_and_slide] still fights full [code]MOVE_SPEED[/code] into
## the face → jitter. Same plane test as [method _step_assist_wallish_blocks]: [code]dot <
## -0.02[/code].
func _walk_clip_horizontal_velocity_against_vertical_contacts(want_goal_h: Vector3) -> void:
	var v2 := Vector2(velocity.x, velocity.z)
	for _iter: int in range(4):
		var v_before: Vector2 = v2
		for i: int in get_slide_collision_count():
			var n: Vector3 = get_slide_collision(i).get_normal()
			if n.y > 0.52:
				continue
			var n2 := Vector2(n.x, n.z)
			if n2.length_squared() < 1e-10:
				continue
			n2 = n2.normalized()
			var dot_v: float = v2.dot(n2)
			if dot_v < -0.02:
				v2 -= n2 * dot_v
		if is_on_wall():
			var wn: Vector3 = get_wall_normal()
			if wn.y <= 0.35:
				var wnh := Vector2(wn.x, wn.z)
				if wnh.length_squared() > 1e-10:
					wnh = wnh.normalized()
					var dot_w: float = v2.dot(wnh)
					if dot_w < -0.02:
						v2 -= wnh * dot_w
		if v2.distance_squared_to(v_before) < 1e-12:
			break
	# Goal through a vertical blocker but only micro tangential slip survives → kill floaty XZ.
	var want3 := Vector3(want_goal_h.x, 0.0, want_goal_h.z)
	if want3.length_squared() > 1e-10 and _step_assist_wallish_blocks(want3.normalized()):
		if v2.length_squared() < 0.0025:
			v2 = Vector2.ZERO
	velocity.x = v2.x
	velocity.z = v2.y


## While [code]_walk_vert_route_latched && _walk_nav_column_steering[/code], seam contacts can make
## [method _walk_clip_horizontal_velocity_against_vertical_contacts] output ~opposite
## [code]velocity.xz[/code]
## on consecutive ticks at nearly [member MOVE_SPEED] (slide normal order / riser vs tread).
func _walk_damp_vert_route_column_seam_flip() -> void:
	if not (_walk_vert_route_latched and _walk_nav_column_steering):
		_walk_col_seam_prev_hz = Vector2.ZERO
		_walk_col_seam_suppress_ticks = 0
		return
	if _walk_col_seam_suppress_ticks > 0:
		_walk_col_seam_suppress_ticks -= 1
		velocity.x = 0.0
		velocity.z = 0.0
		_walk_col_seam_prev_hz = Vector2.ZERO
		return
	var hz := Vector2(velocity.x, velocity.z)
	var len2: float = hz.length_squared()
	var thr: float = MOVE_SPEED * WALK_COL_SEAM_SPEED_FRAC
	var thr2: float = thr * thr
	if len2 < thr2 * 0.2:
		if len2 > 1e-8:
			_walk_col_seam_prev_hz = hz
		else:
			_walk_col_seam_prev_hz = Vector2.ZERO
		return
	var pl2: float = _walk_col_seam_prev_hz.length_squared()
	if pl2 < thr2 * 0.16:
		_walk_col_seam_prev_hz = hz
		return
	if hz.normalized().dot(_walk_col_seam_prev_hz.normalized()) < WALK_COL_SEAM_FLIP_DOT:
		velocity.x = 0.0
		velocity.z = 0.0
		_walk_col_seam_prev_hz = Vector2.ZERO
		_walk_col_seam_suppress_ticks = WALK_COL_SEAM_SUPPRESS_TICKS
	else:
		_walk_col_seam_prev_hz = hz


func _step_assist_can_raise_by(dy: float) -> bool:
	if dy <= 0.0001:
		return false
	return not test_move(global_transform, Vector3(0.0, dy, 0.0), null, safe_margin)


func _try_walk_step_assist() -> bool:
	if not _has_walk_goal:
		return false
	# Use actual capsule bottom (cylindrical half-height + hemisphere radius) so the guard
	# triggers whenever the goal surface is meaningfully above the player's feet — not the
	# capsule centre, which sits ~0.9 m above the floor.
	var actual_feet_y: float = global_position.y - CAPSULE_HALF_HEIGHT - PLAYER_CAPSULE_RADIUS
	if (
		_auth_walk_goal.y < actual_feet_y + 0.025
		or _auth_walk_goal.y > actual_feet_y + WALK_STEP_ASSIST_MAX_SURFACE_DELTA
	):
		return false
	var pos: Vector3 = global_position
	var to_h: Vector3 = Vector3(_auth_walk_goal.x - pos.x, 0.0, _auth_walk_goal.z - pos.z)
	if to_h.length_squared() < 0.03:
		return false
	var want: Vector3 = to_h.normalized()
	if not (_step_assist_wallish_blocks(want) and _step_assist_has_support(want)):
		return false
	var target_center_y: float = _auth_walk_goal.y + CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
	var lift: float = clampf(target_center_y - global_position.y, 0.0, WALK_STEP_ASSIST_DELTA)
	if lift <= 0.002 or not _step_assist_can_raise_by(lift):
		return false
	global_position.y += lift
	return true


func _after_walk_move_and_slide() -> void:
	if _step_assist_cooldown > 0:
		_step_assist_cooldown -= 1
	if _step_assist_cooldown > 0:
		return
	if _try_walk_step_assist():
		_step_assist_active = true
		_step_assist_cooldown = WALK_STEP_ASSIST_COOLDOWN_TICKS
		# Reduced snap so the lower floor (0.11 m below lifted capsule) is out of range.
		floor_snap_length = WALK_STEP_ASSIST_SNAP
		move_and_slide()


func _clear_step_assist_after_walk_move() -> void:
	if not _step_assist_active:
		return
	if is_on_floor() and not is_on_wall():
		_step_assist_active = false
	elif not is_on_floor() and get_slide_collision_count() == 0:
		_step_assist_active = false


func _set_horizontal_velocity_toward(
	point: Vector3, fallback_dir_xz: Vector3 = Vector3.ZERO
) -> void:
	var pos := global_position
	var dh: Vector3 = Vector3(point.x - pos.x, 0.0, point.z - pos.z)
	if dh.length_squared() < 1e-10:
		var fb2 := Vector2(fallback_dir_xz.x, fallback_dir_xz.z)
		if fb2.length_squared() > 1e-10:
			fb2 = fb2.normalized()
			dh = Vector3(fb2.x, 0.0, fb2.y)
		else:
			dh = Vector3(1.0, 0.0, 0.0)
	else:
		dh = dh.normalized()
	# Same tick as [method _apply_walk_air_gravity]: zeroing [code]velocity.y[/code] every frame while
	# airborne capped fall to one gravity delta per tick (~constant sink rate off tall pads).
	var vy_air: float = velocity.y
	velocity = dh * MOVE_SPEED
	velocity.y = 0.0 if is_on_floor() else vy_air


func _walk_probe_xz_offsets(move_dir_xz: Vector2) -> Array[Vector2]:
	var offs: Array[Vector2] = [Vector2.ZERO]
	var len2: float = move_dir_xz.length_squared()
	var r: float = PLAYER_CAPSULE_RADIUS
	if len2 > 1e-8:
		var inv_len: float = 1.0 / sqrt(len2)
		var f := Vector2(move_dir_xz.x * inv_len, move_dir_xz.y * inv_len)
		var perp := Vector2(-f.y, f.x)
		offs.append(f * r * 0.58)
		offs.append(f * r * 0.30 + perp * r * 0.42)
		offs.append(f * r * 0.30 - perp * r * 0.42)
	else:
		offs.append(Vector2(1.0, 0.0) * r * 0.52)
		offs.append(Vector2(-1.0, 0.0) * r * 0.52)
		offs.append(Vector2(0.0, 1.0) * r * 0.52)
		offs.append(Vector2(0.0, -1.0) * r * 0.52)
	return offs


func _walk_ray_hit_up_floor(space: PhysicsDirectSpaceState3D, off_xz: Vector2) -> bool:
	var feet: Vector3 = (
		global_position - Vector3(0.0, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS, 0.0)
	)
	var probe_from := Vector3(feet.x + off_xz.x, feet.y + 0.04, feet.z + off_xz.y)
	var probe_end := Vector3(
		feet.x + off_xz.x, feet.y - WALK_SUPPORT_PROBE_DEPTH, feet.z + off_xz.y
	)
	var q := PhysicsRayQueryParameters3D.create(probe_from, probe_end)
	q.collision_mask = collision_mask
	q.exclude = [get_rid()]
	var res: Dictionary = space.intersect_ray(q)
	if res.is_empty():
		return false
	if not res.has("normal") or not res.has("position"):
		return false
	var hit_y: float = (res["position"] as Vector3).y
	if hit_y < feet.y - WALK_CONTINUATION_MAX_BELOW_FEET:
		return false
	var n: Vector3 = res["normal"]
	return n.dot(Vector3.UP) > WALK_SUPPORT_PROBE_MIN_UP_DOT


## Horizontal bearing for walk ledge / continuation rays. Must not stay ~zero while descending: a
## floor click **under** the capsule ([code]want_goal_h ≈ 0[/code]) used to leave
## [code]walk_move_dir_xz[/code]
## empty so [method _walk_has_close_floor_probe_below] took the center-only fast path and never
## applied ledge gravity until XZ arrival.
func _resolve_walk_probe_dir_xz(feet_y: float, want_goal_h: Vector3) -> Vector2:
	var pos := global_position
	var to_g := Vector2(_auth_walk_goal.x - pos.x, _auth_walk_goal.z - pos.z)
	var vxz := Vector2(velocity.x, velocity.z)
	var wh := Vector2(want_goal_h.x, want_goal_h.z)
	var descending: bool = _has_walk_goal and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN
	var out: Vector2
	if descending:
		if to_g.length_squared() >= 1e-6:
			out = to_g
		elif wh.length_squared() >= 1e-8:
			out = wh
		else:
			var picked_from_path: bool = false
			var path: PackedVector3Array = _nav_agent.get_current_navigation_path()
			var min_l2: float = NAV_PATH_STEER_MIN_LOOKAHEAD * NAV_PATH_STEER_MIN_LOOKAHEAD
			for i: int in range(path.size()):
				var d := Vector2(path[i].x - pos.x, path[i].z - pos.z)
				if d.length_squared() >= min_l2:
					out = d
					picked_from_path = true
					break
			if not picked_from_path:
				if to_g.length_squared() > 1e-12:
					out = to_g
				elif vxz.length_squared() >= 1e-8:
					out = vxz
				else:
					out = Vector2(1.0, 0.0)
	else:
		if vxz.length_squared() < 1e-10:
			out = wh
		else:
			out = vxz
	return out


## Walkable support for **continued** forward motion within [member WALK_SUPPORT_PROBE_DEPTH].
## Requires a hit under the foot origin **and** at least one sample **ahead** in [param move_dir_xz]
## (same construction as [method _walk_probe_xz_offsets]). If only the center hits while the leading
## foot is over a void, [method CharacterBody3D.is_on_floor] can stay true from a lip — returning
## true here would suppress gravity and keep moving snap until XZ arrival (ledge skate).
## When [param move_dir_xz] is ~zero (idle / no horizontal intent), center-only is enough.
func _walk_has_close_floor_probe_below(move_dir_xz: Vector2) -> bool:
	var w3d := get_world_3d()
	if w3d == null:
		return true
	var space: PhysicsDirectSpaceState3D = w3d.direct_space_state
	if not _walk_ray_hit_up_floor(space, Vector2.ZERO):
		return false
	var len2: float = move_dir_xz.length_squared()
	if len2 < 1e-10:
		return true
	var inv_len: float = 1.0 / sqrt(len2)
	var md := Vector2(move_dir_xz.x * inv_len, move_dir_xz.y * inv_len)
	for off2: Vector2 in _walk_probe_xz_offsets(move_dir_xz):
		if off2.length_squared() < 1e-12:
			continue
		if off2.dot(md) > 0.0:
			if _walk_ray_hit_up_floor(space, off2):
				return true
	return false


## Airborne walk ticks only. Do **not** add gravity just because the nav goal is below the feet —
## that stays true for the whole pad cross and poisons horizontal motion.
## Do apply gravity when on floor but no walkable surface lies under the foot disk (ledge void).
## With GROUNDED [method move_and_slide], [code]velocity.y[/code] clears on floor; terrace lips use
## [method _apply_walk_post_slide_ledge_peel] instead of gravity-on-velocity alone.
func _apply_walk_air_gravity(
	delta: float, _feet_y: float, _move_dir_xz: Vector2 = Vector2.ZERO
) -> void:
	if not is_on_floor():
		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_COLUMN_SUSPEND_MAX_VSEP
	):
		return true
	var descending: bool = _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN
	if descending and feet_y > _auth_walk_goal.y + WALK_PEEL_SUSPEND_BROAD_MAX_FEET_ABOVE_GOAL:
		return false
	return vsep <= WALK_PEEL_SUSPEND_HORIZ_VERT_SEP and horiz_dist <= WALK_PEEL_SUSPEND_HORIZ_DIST


func _walk_tread_freeze_horizontal_active(_feet_y: float, horiz_dist: float) -> bool:
	if not is_on_floor() or not _has_walk_goal:
		return false
	if _auth_walk_goal.y > _feet_y + 0.04:
		return false
	if not _walk_peel_suspend_near_goal(_feet_y, horiz_dist):
		return false
	if horiz_dist > WALK_TREAD_FREEZE_HORIZ_MAX:
		return false
	var cap_half: float = CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
	var verr: float = vertical_arrival_error(_auth_walk_goal.y, global_position.y, cap_half)
	return verr > VERT_ARRIVE_EPS and verr <= WALK_TREAD_FREEZE_VERT_ERR_MAX


func _walk_refresh_ledge_peel_debounce(feet_y: float, move_dir_xz: Vector2) -> void:
	var probe_ok: bool = _walk_has_close_floor_probe_below(move_dir_xz)
	var deep: bool = feet_y > _auth_walk_goal.y + WALK_DEEP_DESCENT_FEET_ABOVE_GOAL
	if deep:
		_walk_peel_no_probe_streak = WALK_PEEL_PROBE_FALSE_FRAMES
	elif not probe_ok:
		_walk_peel_no_probe_streak += 1
	else:
		_walk_peel_no_probe_streak = 0
	_walk_debounced_wants_ledge_peel = (
		deep or _walk_peel_no_probe_streak >= WALK_PEEL_PROBE_FALSE_FRAMES
	)


## After [method move_and_slide], peel the capsule down when the authoritative goal is below the
## feet
## but the engine still reports floor contact (GROUNDED mode ate negative [code]velocity.y[/code]).
## Uses a small accumulated peel speed so displacement matches [code]∫ g dt[/code], not [code]g
## Δt[/code] (wrong units).
func _apply_walk_post_slide_ledge_peel(
	delta: float, feet_y: float, _move_dir_xz: Vector2, horiz_dist: float
) -> void:
	if not _has_walk_goal:
		_walk_ledge_peel_vy = 0.0
		return
	if not is_on_floor():
		_walk_ledge_peel_vy = 0.0
		return
	# Do not use [member DESCEND_GOAL_Y_MARGIN] here: peel stopped while feet were still ~6 cm above
	# the goal surface (slow float to rest at XZ). Stop only once vertical arrival matches.
	var cap_half: float = CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
	if vertical_arrival_error(_auth_walk_goal.y, global_position.y, cap_half) <= VERT_ARRIVE_EPS:
		_walk_ledge_peel_vy = 0.0
		return
	if _auth_walk_goal.y > feet_y + 0.04:
		_walk_ledge_peel_vy = 0.0
		return
	if _walk_peel_suspend_near_goal(feet_y, horiz_dist):
		_walk_ledge_peel_vy = 0.0
		return
	if not _walk_debounced_wants_ledge_peel:
		_walk_ledge_peel_vy = 0.0
		return
	# Debounce was computed pre-slide; do not re-probe here (post-slide rays re-flicker on treads).
	var sep_below_feet: float = feet_y - _auth_walk_goal.y
	var peel_boost: float = 1.0
	if sep_below_feet > WALK_LEDGE_PEEL_BOOST_SEP_3:
		peel_boost = WALK_LEDGE_PEEL_BOOST_MUL_3
	elif sep_below_feet > WALK_LEDGE_PEEL_BOOST_SEP_2:
		peel_boost = WALK_LEDGE_PEEL_BOOST_MUL_2
	elif sep_below_feet > WALK_LEDGE_PEEL_BOOST_SEP_1:
		peel_boost = WALK_LEDGE_PEEL_BOOST_MUL_1
	_walk_ledge_peel_vy += _player_vertical_accel(delta).y * delta * peel_boost
	var dy: float = _walk_ledge_peel_vy * delta
	if absf(dy) > 1e-9:
		# Raw [code]global_position[/code] edits may be overwritten vs Jolt after [method move_and_slide].
		move_and_collide(Vector3(0.0, dy, 0.0), false, safe_margin, false)


func _walk_floor_snap_length(
	feet_y: float,
	want_goal_h: Vector3,
	_move_dir_xz: Vector2 = Vector2.ZERO,
	horiz_dist: float = INF,
) -> float:
	if _step_assist_active:
		return WALK_STEP_ASSIST_SNAP
	if _has_walk_goal:
		# Strong moving snap + lip [code]is_on_floor()[/code] otherwise cancels gravity every tick.
		if (
			is_on_floor()
			and not _walk_peel_suspend_near_goal(feet_y, horiz_dist)
			and _walk_debounced_wants_ledge_peel
		):
			return 0.0
		if want_goal_h.length_squared() > 1e-10:
			var want: Vector3 = want_goal_h.normalized()
			# Climb: cap snap only for a **meaningful** rise (not shallow tread-to-tread), or we
			# fight the approach stairs with snap ↔ assist oscillation.
			if _auth_walk_goal.y > feet_y + 0.12 and _step_assist_wallish_blocks(want):
				return WALK_STEP_ASSIST_SNAP
		# Lip cap only while debounced peel is armed. Otherwise [code]feet_y[/code] wobble on treads
		# crosses [code]feet_y > goal + 0.08[/code] / margins → 0.32 m vs 0.1 m snap flips each tick
		# (~2–3 cm body Y jitter on green stairs, x≈19).
		var descend_lip_geom: bool = (
			_auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN and feet_y > _auth_walk_goal.y + 0.08
		)
		if (
			descend_lip_geom
			and _walk_debounced_wants_ledge_peel
			and not _walk_peel_suspend_near_goal(feet_y, horiz_dist)
		):
			return minf(FLOOR_SNAP_MOVING, DESCEND_LIP_SNAP_CAP)
	return FLOOR_SNAP_MOVING


## Descend only: relax `floor_block_on_wall` when pressed against a vertical face so Jolt can
## slide off terrace lips. Never when the goal is above the feet (climb) — avoids platform-wide
## toggling that broke horizontal motion.
func _apply_floor_block_for_descend_at_wall(feet_y: float) -> void:
	floor_block_on_wall = true
	if not _has_walk_goal:
		return
	if _auth_walk_goal.y >= feet_y - DESCEND_GOAL_Y_MARGIN:
		return
	if is_on_wall():
		floor_block_on_wall = false
		return
	# Terrace lip: floor normal tilts off UP when straddling an internal edge (teal pad, etc.).
	if is_on_floor() and get_floor_normal().dot(Vector3.UP) < 0.992:
		floor_block_on_wall = false
		return
	for i: int in range(get_slide_collision_count()):
		var n: Vector3 = get_slide_collision(i).get_normal()
		if absf(n.y) < 0.58:
			floor_block_on_wall = false
			return


## When the authoritative goal shares XZ with the capsule (e.g. terrace above), horizontal
## `full_to_goal` is zero and steering must follow the baked path toward a ramp/step, not +X.
func _set_horizontal_velocity_from_nav_path_or_goal(fallback_goal_xz: Vector3) -> void:
	# `get_next_path_position()` alone still ping-pongs: the engine’s “next” point can sit within
	# Jolt’s per-tick slide distance, so the horizontal seek vector flips 180° each tick.
	# Scan the baked path for the first XZ waypoint at least [member NAV_PATH_STEER_MIN_LOOKAHEAD]
	# away (unlike the old 5 cm scan). Reject segments that point opposite the click (XZ).
	if _nav_agent.is_navigation_finished():
		_walk_nav_path_steer_hz = Vector3.ZERO
		_set_horizontal_velocity_toward(_auth_walk_goal, fallback_goal_xz)
		return
	var pos := global_position
	var min_l2: float = NAV_PATH_STEER_MIN_LOOKAHEAD * NAV_PATH_STEER_MIN_LOOKAHEAD
	var steer_point: Vector3 = _auth_walk_goal
	var path: PackedVector3Array = _nav_agent.get_current_navigation_path()
	if path.size() > 0:
		var chosen: bool = false
		for i: int in range(path.size()):
			var wp: Vector3 = path[i]
			var dx: float = wp.x - pos.x
			var dz: float = wp.z - pos.z
			if dx * dx + dz * dz >= min_l2:
				steer_point = wp
				chosen = true
				break
		if not chosen:
			steer_point = path[path.size() - 1]
	var dh: Vector3 = Vector3(steer_point.x - pos.x, 0.0, steer_point.z - pos.z)
	if dh.length_squared() < 1e-10:
		_walk_nav_path_steer_hz = Vector3.ZERO
		_set_horizontal_velocity_toward(_auth_walk_goal, fallback_goal_xz)
		return
	var dir: Vector3 = dh.normalized()
	var gh: Vector3 = Vector3(fallback_goal_xz.x, 0.0, fallback_goal_xz.z)
	if gh.length_squared() > 1e-10:
		gh = gh.normalized()
		if dir.dot(gh) < NAV_PATH_STEER_MIN_GOAL_DOT:
			_walk_nav_path_steer_hz = Vector3.ZERO
			_set_horizontal_velocity_toward(_auth_walk_goal, fallback_goal_xz)
			return
	var dir_hz := Vector3(dir.x, 0.0, dir.z)
	if dir_hz.length_squared() < 1e-10:
		_walk_nav_path_steer_hz = Vector3.ZERO
		_set_horizontal_velocity_toward(_auth_walk_goal, fallback_goal_xz)
		return
	dir_hz = dir_hz.normalized()
	if _walk_nav_path_steer_hz.length_squared() > 1e-10:
		var prev := Vector3(_walk_nav_path_steer_hz.x, 0.0, _walk_nav_path_steer_hz.z).normalized()
		var dotp: float = dir_hz.dot(prev)
		var t: float = (
			NAV_PATH_STEER_SMOOTH_SLERP_OPPOSING
			if dotp < NAV_PATH_STEER_SMOOTH_DOT
			else NAV_PATH_STEER_SMOOTH_SLERP
		)
		dir_hz = prev.slerp(dir_hz, t).normalized()
	_walk_nav_path_steer_hz = dir_hz
	var vy_air: float = velocity.y
	velocity = dir_hz * MOVE_SPEED
	velocity.y = 0.0 if is_on_floor() else vy_air


func _effective_gravity(_delta: float) -> Vector3:
	var g: Vector3 = get_gravity()
	if g.length_squared() < 0.25:
		return Vector3(0.0, WALK_FALLBACK_GRAVITY_Y, 0.0)
	return g


func _player_vertical_accel(_delta: float) -> Vector3:
	return _effective_gravity(_delta) * PLAYER_VERTICAL_ACCEL_MULTIPLIER


static func capsule_feet_y(body_origin_y: float, capsule_half_height: float) -> float:
	return body_origin_y - capsule_half_height


static func vertical_arrival_error(
	goal_y: float, body_origin_y: float, capsule_half_height: float
) -> float:
	return absf(goal_y - capsule_feet_y(body_origin_y, capsule_half_height))


func _physics_idle_tick(delta: float) -> void:
	if is_on_floor():
		velocity = Vector3.ZERO
		floor_snap_length = FLOOR_SNAP_IDLE
	else:
		# Ledge / post-arrival hover: without gravity, vy stays 0 and FLOOR_SNAP_IDLE (0.11 m)
		# cannot reach the lower floor — the capsule idles in mid-air until a new walk goal runs.
		velocity.x = 0.0
		velocity.z = 0.0
		velocity += _player_vertical_accel(delta) * delta
		floor_snap_length = FLOOR_SNAP_MOVING
	move_and_slide()


static func idle_slide_contacts_are_ridged(slide_normals: Array[Vector3]) -> bool:
	var upish := 0
	var wallish := 0
	for n: Vector3 in slide_normals:
		var ny: float = n.y
		if ny > 0.65:
			upish += 1
		# Steeper than ~70° from horizontal — shallow seam faces (ny≈0.4) are not “lip” walls.
		elif ny < 0.35:
			wallish += 1
	return upish >= 1 and wallish >= 1


## [method idle_slide_contacts_are_ridged] is true on heightmap facet seams (up + shallow “wall”
## micro-normals) while the floor normal’s up component still sits in the smooth single-slope band.
## Treat
## that like a uniform ramp: do not run idle rim nudges / stair-lip stability against it (QA hill).
## Same for **nearly level** aggregate normals (hill crest / corner): facet pairs are not
## tread+riser.
static func idle_ridged_stair_lip_only(slide_normals: Array[Vector3], floor_up_dot: float) -> bool:
	if not idle_slide_contacts_are_ridged(slide_normals):
		return false
	if floor_up_dot >= STABLE_IDLE_FLOOR_MIN_UP_DOT:
		return false
	if floor_up_dot >= IDLE_SLOPE_STABLE_MIN_UP_DOT and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT:
		return false
	return true


static func idle_support_is_stable(
	on_floor: bool,
	floor_normal: Vector3,
	slide_normals: Array[Vector3],
	loose_ticks: int,
	min_flat_up_dot: float = STABLE_IDLE_FLOOR_MIN_UP_DOT,
) -> bool:
	if not on_floor:
		return false
	var up_dot: float = floor_normal.dot(Vector3.UP)
	var ridged: bool = idle_ridged_stair_lip_only(slide_normals, up_dot)
	# Single-surface inclines (walk ramps): flatter than [member STABLE_IDLE_FLOOR_MIN_UP_DOT] but not
	# stair lip geometry — latch stable idle instead of endless corrective [method _physics_idle_tick].
	if (
		not ridged
		and up_dot >= IDLE_SLOPE_STABLE_MIN_UP_DOT
		and up_dot < STABLE_IDLE_FLOOR_MIN_UP_DOT
	):
		return true
	# Entering stable idle uses [member STABLE_IDLE_FLOOR_MIN_UP_DOT] (0.998). On stair treads,
	# [code]get_floor_normal()[/code] often sits ~0.996–0.997 with a riser contact — never latches,
	# so idle stays in corrective [method _maybe_idle_rim_settle_nudge] + [method _physics_idle_tick]
	# forever (HUD jitter while **not** walking). Use the latched hold threshold (0.992) when ridged.
	var required_up: float = min_flat_up_dot
	if ridged and min_flat_up_dot >= STABLE_IDLE_FLOOR_MIN_UP_DOT - 1e-6:
		required_up = STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT
	if up_dot < required_up:
		return false
	# Post-stop **ridged** slides (tread + riser) used to force unstable idle for the entire
	# `loose_ticks` window even when the **floor** is level — e.g. idle on a flat approach tread
	# hugging a vertical face stayed in corrective `move_and_slide` ~0.8 s (HUD X/Y/Z noise).
	# Only demand rim settle when the reported floor is already shallow (lip straddle), not mere
	# proximity to a riser on an otherwise flat tread.
	if loose_ticks > 0 and ridged and up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT:
		return false
	return true


func _current_slide_normals() -> Array[Vector3]:
	var normals: Array[Vector3] = []
	for i: int in get_slide_collision_count():
		normals.append(get_slide_collision(i).get_normal())
	return normals


func _stable_idle_support() -> bool:
	var min_dot: float = (
		STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT if _idle_stable_latched else STABLE_IDLE_FLOOR_MIN_UP_DOT
	)
	return idle_support_is_stable(
		is_on_floor(),
		get_floor_normal(),
		_current_slide_normals(),
		_floor_angle_loose_ticks,
		min_dot
	)


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, _idle_anchor_y, _idle_anchor_xz.y)


func _debug_trace_idle_state(tag: String) -> void:
	if not debug_idle_trace or _has_walk_goal:
		_debug_idle_heartbeat = 0
		_debug_last_idle_xz = Vector2.INF
		return
	var pos_xz := Vector2(global_position.x, global_position.z)
	var should_log: bool = false
	if _debug_last_idle_xz == Vector2.INF:
		should_log = true
	elif pos_xz.distance_to(_debug_last_idle_xz) > DEBUG_IDLE_TRACE_DELTA_EPS:
		should_log = true
	elif _debug_idle_heartbeat >= DEBUG_IDLE_TRACE_HEARTBEAT_FRAMES:
		should_log = true
	if should_log:
		push_warning(
			(
				(
					"NEO-14 idle trace [%s] pos=%s vel=%s on_floor=%s loose=%d "
					+ "stable=%s anchor=%s anchor_xz=%s floor=%s slides=%s"
				)
				% [
					tag,
					global_position,
					velocity,
					is_on_floor(),
					_floor_angle_loose_ticks,
					_stable_idle_support(),
					_idle_anchor_active,
					_idle_anchor_xz,
					get_floor_normal(),
					_current_slide_normals(),
				]
			)
		)
		_debug_last_idle_xz = pos_xz
		_debug_idle_heartbeat = 0
	else:
		_debug_idle_heartbeat += 1


func _debug_trace_transform(tag: String) -> void:
	if not debug_idle_trace:
		return
	var pos_xz := Vector2(global_position.x, global_position.z)
	if (
		_debug_last_transform_xz != Vector2.INF
		and pos_xz.distance_to(_debug_last_transform_xz) < DEBUG_TRANSFORM_TRACE_DELTA_EPS
		and tag != "physics"
	):
		return
	var extra: String = ""
	if _has_walk_goal:
		var fy: float = capsule_feet_y(
			global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
		)
		var vsep: float = absf(_auth_walk_goal.y - fy)
		extra = (
			" feet_y=%s vert_sep=%.4f vlat=%s ncol=%s"
			% [fy, vsep, _walk_vert_route_latched, _walk_nav_column_steering]
		)
	print(
		(
			(
				"NEO-14 transform trace [%s] frame=%d pos=%s vel=%s has_goal=%s "
				+ "on_floor=%s loose=%d stable=%s anchor=%s anchor_xz=%s%s"
			)
			% [
				tag,
				_debug_trace_frame,
				global_position,
				velocity,
				_has_walk_goal,
				is_on_floor(),
				_floor_angle_loose_ticks,
				_stable_idle_support(),
				_idle_anchor_active,
				_idle_anchor_xz,
				extra,
			]
		)
	)
	_debug_last_transform_xz = pos_xz


func _notification(what: int) -> void:
	if what == NOTIFICATION_TRANSFORM_CHANGED:
		_debug_trace_transform("notify")


func _idle_ridged_floor_contacts() -> bool:
	return idle_slide_contacts_are_ridged(_current_slide_normals())


func _maybe_idle_rim_settle_nudge() -> bool:
	if get_slide_collision_count() < 1:
		return false
	var floor_up_dot: float = get_floor_normal().dot(Vector3.UP)
	var shallow_floor: bool = is_on_floor() and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT
	var ridged: bool = idle_ridged_stair_lip_only(_current_slide_normals(), floor_up_dot)
	# Uniform ramps (one sloped floor, no tread+riser) still read “shallow” vs
	# [member IDLE_RIM_MIN_FLOOR_UP_DOT]; rim nudges slide along the ramp every tick (jitter).
	if shallow_floor and not ridged:
		return false
	if not shallow_floor and not ridged:
		return false
	var h := Vector3.ZERO
	if shallow_floor:
		h = Vector3(get_floor_normal().x, 0.0, get_floor_normal().z)
	if h.length_squared() < 1e-12:
		for i: int in get_slide_collision_count():
			var n: Vector3 = get_slide_collision(i).get_normal()
			if n.y < 0.5:
				var nh: Vector3 = Vector3(n.x, 0.0, n.z)
				if nh.length_squared() > h.length_squared():
					h = nh
	if h.length_squared() < 1e-12:
		return false
	var step: Vector3 = -h.normalized() * IDLE_RIM_SETTLE_STEP
	if test_move(global_transform, step, null, safe_margin):
		return false
	global_position += step
	return true


func _maybe_idle_bump_proximity_escape() -> bool:
	var feet_y: float = global_position.y - CAPSULE_HALF_HEIGHT
	for node: Node in get_tree().get_nodes_in_group(
		BUMP_COLLISION_CONSTS_SCRIPT.RANDOM_FLOOR_BUMP_MESH_GROUP
	):
		if not node is MeshInstance3D:
			continue
		var mi := node as MeshInstance3D
		var mesh: Mesh = mi.mesh
		if not mesh is CylinderMesh:
			continue
		var cyl := mesh as CylinderMesh
		var c: Vector3 = mi.global_position
		var r_mesh: float = cyl.top_radius
		var h: float = cyl.height
		var col_r: float = minf(
			r_mesh + BUMP_COLLISION_CONSTS_SCRIPT.COLLISION_RADIUS_EXTRA,
			BUMP_COLLISION_CONSTS_SCRIPT.COLLISION_RADIUS_MAX
		)
		var dx: float = global_position.x - c.x
		var dz: float = global_position.z - c.z
		var d: float = sqrt(dx * dx + dz * dz)
		if d < 1e-5:
			continue
		var bottom_y: float = c.y - h * 0.5
		var top_y: float = c.y + h * 0.5
		if feet_y < bottom_y - 0.22 or feet_y > top_y + 0.4:
			continue
		var radial: Vector3 = Vector3(dx / d, 0.0, dz / d)
		var away: Vector3 = radial * IDLE_BUMP_ESCAPE_STEP
		var wall_band_outer: float = col_r + PLAYER_CAPSULE_RADIUS + 0.22
		var should_escape: bool = d > r_mesh * 0.9 and d < col_r + 0.32
		# On / above the disc, hugging the **visual** rim — step onto open slab.
		should_escape = (
			should_escape
			or (
				feet_y >= bottom_y - 0.06
				and feet_y <= top_y + 0.1
				and d >= r_mesh * 0.86
				and d <= r_mesh + 0.14
			)
		)
		# Beside vertical cylinder: axis distance may exceed **`col_r`** while capsule still touches wall.
		should_escape = (
			should_escape
			or (
				feet_y <= top_y + 0.14
				and d > r_mesh * 0.82
				and d < wall_band_outer
				and (is_on_wall() or _idle_ridged_floor_contacts())
			)
		)
		if not should_escape:
			continue
		if test_move(global_transform, away, null, safe_margin):
			return false
		global_position += away
		return true
	return false


func _apply_idle_manual_correction() -> bool:
	# Prefer explicit bump-rim escape over the generic shallow-floor settle so spawned
	# cylinder edges resolve radially off the lip instead of orbiting around it.
	if _maybe_idle_bump_proximity_escape():
		return true
	return _maybe_idle_rim_settle_nudge()


func _snap_capsule_upright() -> void:
	# TODO(NS-24 follow-on): With facing yaw, clear pitch/roll only; keep Y rotation.
	# Full **`Basis.IDENTITY`** would erase look direction.
	if global_transform.basis.orthonormalized().is_equal_approx(Basis.IDENTITY):
		return
	var p: Vector3 = global_position
	global_transform = Transform3D(Basis.IDENTITY, p)


func _physics_process_no_walk_goal(delta: float) -> void:
	floor_block_on_wall = true
	var raw_stable: bool = _stable_idle_support()
	if raw_stable:
		_idle_stable_unlatch_streak = 0
		if not _idle_stable_latched:
			_idle_stable_enter_streak += 1
			if _idle_stable_enter_streak >= STABLE_IDLE_ENTER_STREAK_FRAMES:
				_idle_stable_latched = true
		else:
			_idle_stable_enter_streak = 0
	elif _idle_stable_latched:
		var crest_quiet: bool = (
			is_on_floor()
			and get_floor_normal().dot(Vector3.UP) >= STABLE_IDLE_FLOOR_MIN_UP_DOT - 0.001
		)
		if raw_stable or crest_quiet:
			_idle_stable_unlatch_streak = 0
		else:
			_idle_stable_unlatch_streak += 1
		if _idle_stable_unlatch_streak >= STABLE_IDLE_UNLATCH_TICKS:
			_idle_stable_latched = false
			_idle_stable_unlatch_streak = 0
			_idle_stable_enter_streak = 0
			_idle_anchor_active = false
	else:
		_idle_stable_enter_streak = maxi(
			0, _idle_stable_enter_streak - STABLE_IDLE_ENTER_MISS_DECAY
		)
	if _idle_stable_latched:
		_idle_manual_correction_ticks = 0
		velocity = Vector3.ZERO
		floor_snap_length = FLOOR_SNAP_IDLE
		_hold_idle_anchor()
		_debug_trace_idle_state("stable")
		_debug_trace_transform("physics")
		_snap_capsule_upright()
		return
	if _idle_manual_correction_ticks >= IDLE_MANUAL_CORRECTION_MAX_TICKS:
		if is_on_floor():
			velocity = Vector3.ZERO
			floor_snap_length = FLOOR_SNAP_IDLE
			if _floor_angle_loose_ticks > 0:
				_floor_angle_loose_ticks -= 1
			_hold_idle_anchor()
			_debug_trace_idle_state("budgeted")
			_debug_trace_transform("physics")
			_snap_capsule_upright()
			return
		_idle_anchor_active = false
		_physics_idle_tick(delta)
		if _floor_angle_loose_ticks > 0:
			_floor_angle_loose_ticks -= 1
		_debug_trace_idle_state("budgeted")
		_debug_trace_transform("physics")
		_snap_capsule_upright()
		return
	_idle_anchor_active = false
	_physics_idle_tick(delta)
	if _apply_idle_manual_correction():
		_idle_manual_correction_ticks += 1
	else:
		_idle_manual_correction_ticks = 0
	if _floor_angle_loose_ticks > 0:
		_floor_angle_loose_ticks -= 1
	_debug_trace_idle_state("corrective")
	_debug_trace_transform("physics")
	_snap_capsule_upright()


func _physics_process(delta: float) -> void:
	_debug_trace_frame += 1
	# Do not tie wide floor angle to `_floor_angle_loose_ticks` alone: flat walks that kept a wide
	# angle for the whole loose window made `move_and_slide` + idle snap jitter XZ. Use moving max
	# angle only while walking or when idle support is shallow / ridged.
	var use_loose_floor_angle: bool = _has_walk_goal
	if not _has_walk_goal:
		var floor_up_dot: float = get_floor_normal().dot(Vector3.UP)
		var shallow_idle_floor: bool = is_on_floor() and floor_up_dot < IDLE_RIM_MIN_FLOOR_UP_DOT
		# Raw facet ridging reads true on a heightmap **crest** while [code]get_floor_normal()[/code] is
		# already ~level; widening [member floor_max_angle] there makes [method move_and_slide] hunt
		# forever (HUD jitter on the pink hill tip).
		if (
			(shallow_idle_floor or idle_slide_contacts_are_ridged(_current_slide_normals()))
			and floor_up_dot < STABLE_IDLE_FLOOR_MIN_UP_DOT
		):
			use_loose_floor_angle = true
	floor_max_angle = deg_to_rad(
		FLOOR_MAX_ANGLE_MOVING_DEG if use_loose_floor_angle else FLOOR_MAX_ANGLE_IDLE_DEG
	)
	var wish_active: bool = _locomotion_wish_world_xz.length_squared() > 1e-10
	if wish_active:
		use_loose_floor_angle = true
		floor_max_angle = deg_to_rad(FLOOR_MAX_ANGLE_MOVING_DEG)
		_wasd_locomotion.run_wasd(delta)
		return
	if not _has_walk_goal:
		_physics_process_no_walk_goal(delta)
		return

	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)
	# Feet + vertical routing before arrival: column stairs keep [code]vert_err[/code] above
	# [member VERT_ARRIVE_EPS] while XZ is 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
	)
	var vert_ok: float = VERT_ARRIVE_EPS
	if (
		is_on_floor()
		and _walk_vert_route_latched
		and _walk_nav_column_steering
		and vert_err < WALK_COLUMN_NEAR_ARRIVE_GATE_VERT_ERR
	):
		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
		_walk_peel_no_probe_streak = 0
		_walk_debounced_wants_ledge_peel = false
		floor_block_on_wall = true
		_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
		_step_assist_active = false
		_idle_anchor_active = false
		_idle_manual_correction_ticks = 0
		_idle_stable_latched = false
		_idle_stable_unlatch_streak = 0
		_idle_stable_enter_streak = 0
		_walk_nav_column_steering = false
		_walk_vert_route_latched = false
		_walk_col_seam_prev_hz = Vector2.ZERO
		_walk_col_seam_suppress_ticks = 0
		_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_stable_enter_streak = 0
			_idle_manual_correction_ticks = 0
			velocity = Vector3.ZERO
			floor_snap_length = FLOOR_SNAP_IDLE
			_hold_idle_anchor()
			_debug_trace_idle_state("arrival_stable")
			_debug_trace_transform("physics")
			_snap_capsule_upright()
			return
		if _apply_idle_manual_correction():
			_idle_manual_correction_ticks += 1
		_debug_trace_transform("physics")
		_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

	_apply_floor_block_for_descend_at_wall(feet_y)

	# Horizontal motion toward the **authoritative** click target (XZ) by default. Use the baked
	# path only while **airborne** with column vertical routing (`on_floor=false` + `vlat && ncol`):
	# mid-air the next waypoint can carry tangential intent around a column. On **floor**, the same
	# path sampling on a narrow nav ribbon + shallow rim normals still flipped ±[code]MOVE_SPEED[/code]
	# every tick (Z seam traces: [code]ncol=true[/code] stable [code]on_floor[/code], ~4 cm Z wobble).
	# Shallow on-floor column approach uses direct goal steer plus
	# [method _walk_clip_horizontal_velocity_against_vertical_contacts].
	if needs_vertical_routing and _walk_nav_column_steering and not is_on_floor():
		_set_horizontal_velocity_from_nav_path_or_goal(want_goal_h)
	else:
		_walk_nav_path_steer_hz = Vector3.ZERO
		_set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h)

	_walk_clip_horizontal_velocity_against_vertical_contacts(want_goal_h)
	_walk_damp_vert_route_column_seam_flip()

	if _walk_tread_freeze_horizontal_active(feet_y, horiz_dist):
		velocity.x = 0.0
		velocity.z = 0.0

	var walk_move_dir_xz: Vector2 = _resolve_walk_probe_dir_xz(feet_y, want_goal_h)
	_walk_refresh_ledge_peel_debounce(feet_y, walk_move_dir_xz)

	_apply_walk_air_gravity(delta, feet_y, walk_move_dir_xz)
	floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h, walk_move_dir_xz, horiz_dist)
	move_and_slide()
	var feet_after: float = capsule_feet_y(
		global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS
	)
	_apply_walk_post_slide_ledge_peel(delta, feet_after, walk_move_dir_xz, horiz_dist)
	_after_walk_move_and_slide()
	_clear_step_assist_after_walk_move()
	_debug_trace_transform("physics")
	_snap_capsule_upright()
