extends CharacterBody3D ## NS-23 path-follow; NS-24 idle. 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). const FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP: int = 96 const ARRIVE_EPS: float = 0.35 const VERT_ARRIVE_EPS: float = 0.055 const DIRECT_APPROACH_RADIUS: float = 0.85 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 ## Stable flat idle support: skip corrective idle motion when support is effectively level. const STABLE_IDLE_FLOOR_MIN_UP_DOT: float = 0.9998 ## Rare edge contacts can keep corrective nudges alive forever; budget them, then hold x/z. const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 8 ## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**). const IDLE_RIM_SETTLE_STEP: float = 0.004 const DESCEND_GOAL_Y_MARGIN: float = 0.06 ## Cap floor snap while descending from a step so Jolt cannot snap the capsule back onto the ## upper surface across an internal edge (lip hang). Relaxed once feet are near the goal height. const DESCEND_LIP_SNAP_CAP: float = 0.12 ## Y lift when blocked by wall-ish slide but nav goal is higher (stepped bumps). const WALK_STEP_ASSIST_DELTA: float = 0.11 ## Skip assist when horizontal velocity already aligns enough with want-dir (corners slide slowly). const WALK_STEP_ASSIST_MAX_FORWARD_DOT: float = 0.52 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 ## 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" ) ## 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_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 @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 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 _nav_agent.set_target_position(world_pos) func clear_nav_goal() -> void: velocity = Vector3.ZERO _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 _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 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 # 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 _nav_agent.set_target_position(settled) reset_physics_interpolation() ## 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 ## Returns true when slide collisions contain a wall-ish contact that opposes forward motion. ## The `is_on_wall()` short-circuit was removed: it returned true for any wall contact regardless ## of direction, firing the assist for sideways bump-skin contacts unrelated to step climbing. 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() 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.04: return true return false 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: 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 # Compute want direction before the support/blocking checks so both can use it. var want: Vector3 = to_h.normalized() var vel_h: Vector3 = Vector3(velocity.x, 0.0, velocity.z) if not _step_assist_wallish_blocks(want) or vel_h.dot(want) > WALK_STEP_ASSIST_MAX_FORWARD_DOT: return false if not _step_assist_has_support(want): return false # Target capsule centre once standing on the goal surface. 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 _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() velocity = dh * MOVE_SPEED velocity.y = 0.0 ## While walking, integrate gravity when airborne so Jolt can register floor contacts and ## `move_and_slide` is not stuck with vy=0 above the surface (expanded district / terraces). func _apply_walk_air_gravity(delta: float) -> void: var g_step: Vector3 = get_gravity() * delta if not is_on_floor(): velocity += g_step return # Descending toward a lower surface: `is_on_floor()` can stay true on the upper step while # horizontal motion is blocked (internal edge / lip). `is_on_wall()` is unreliable here, and # a far floor click keeps large horiz delta so wall heuristics never ran. Always integrate # downward acceleration while on the upper surface; `move_and_slide` + floor collision # absorbs it on flat spans, and it peels the capsule off the lip when forward motion stalls. var feet_y: float = capsule_feet_y(global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS) var descending: bool = ( _has_walk_goal and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN ) if descending and g_step.y < 0.0: velocity.y += g_step.y func _walk_floor_snap_length(feet_y: float) -> float: var fl: float = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING if ( _has_walk_goal and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN and feet_y > _auth_walk_goal.y + 0.08 ): fl = minf(fl, DESCEND_LIP_SNAP_CAP) return fl ## 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: var pos: Vector3 = global_position var path: PackedVector3Array = _nav_agent.get_current_navigation_path() for i: int in range(path.size()): var p: Vector3 = path[i] var dh: Vector3 = Vector3(p.x - pos.x, 0.0, p.z - pos.z) if dh.length_squared() > 0.0025: _set_horizontal_velocity_toward(p, fallback_goal_xz) return _set_horizontal_velocity_toward(_auth_walk_goal, fallback_goal_xz) 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 += get_gravity() * delta floor_snap_length = FLOOR_SNAP_MOVING move_and_slide() static func idle_support_is_stable( on_floor: bool, floor_normal: Vector3, _slide_normals: Array[Vector3], loose_ticks: int ) -> bool: ## `_slide_normals` is reserved for future extra-contact analysis. if not on_floor or loose_ticks > 0: return false if floor_normal.dot(Vector3.UP) < STABLE_IDLE_FLOOR_MIN_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: return idle_support_is_stable( is_on_floor(), get_floor_normal(), _current_slide_normals(), _floor_angle_loose_ticks ) func _hold_idle_anchor() -> void: var pos := global_position if not _idle_anchor_active: _idle_anchor_xz = Vector2(pos.x, pos.z) _idle_anchor_active = true return global_position = Vector3(_idle_anchor_xz.x, pos.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( ( ( "NEON-16 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 print( ( ( "NEON-16 transform trace [%s] frame=%d pos=%s vel=%s has_goal=%s " + "on_floor=%s loose=%d stable=%s anchor=%s anchor_xz=%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, ] ) ) _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: var upish := 0 var wallish := 0 for i: int in get_slide_collision_count(): var ny: float = get_slide_collision(i).get_normal().y if ny > 0.65: upish += 1 elif ny < 0.45: wallish += 1 return upish >= 1 and wallish >= 1 func _maybe_idle_rim_settle_nudge() -> bool: if get_slide_collision_count() < 1: return false var shallow_floor: bool = ( is_on_floor() and get_floor_normal().dot(Vector3.UP) < IDLE_RIM_MIN_FLOOR_UP_DOT ) if not shallow_floor and not _idle_ridged_floor_contacts(): 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(delta: float) -> void: _debug_trace_frame += 1 var use_loose_floor_angle: bool = _has_walk_goal or _floor_angle_loose_ticks > 0 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 if shallow_idle_floor or _idle_ridged_floor_contacts(): 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 ) if not _has_walk_goal: if _stable_idle_support(): _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 # Airborne: do not freeze XZ without a physics step — same mid-air hang as idle tick. _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() 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) # 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. 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: velocity = Vector3.ZERO _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 _nav_agent.set_target_position(global_position) _physics_idle_tick(delta) if _apply_idle_manual_correction(): _idle_manual_correction_ticks += 1 _debug_trace_transform("physics") _snap_capsule_upright() return _idle_manual_correction_ticks = 0 var nav_map: RID = _nav_agent.get_navigation_map() if NavigationServer3D.map_get_iteration_id(nav_map) == 0: _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) _apply_walk_air_gravity(delta) floor_snap_length = _walk_floor_snap_length( capsule_feet_y(global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS) ) move_and_slide() _after_walk_move_and_slide() if _step_assist_active and ( (is_on_floor() and not is_on_wall()) or (not is_on_floor() and get_slide_collision_count() == 0) ): _step_assist_active = false _debug_trace_transform("physics") _snap_capsule_upright() return # Use actual capsule bottom (total half = CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS = 0.9 m) # so the descend bypass only fires when the goal surface is genuinely below the player's # physical feet. Using CAPSULE_HALF_HEIGHT alone (0.5) was ~0.4 m too high: from the first # step (body 1.2 → code-feet 0.7) it incorrectly fired for the platform goal (Y=0.6 < 0.64), # using FLOOR_SNAP_MOVING to pull the capsule back to the step after every assist lift. var feet_y: float = capsule_feet_y(global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS) if _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN: _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) _apply_walk_air_gravity(delta) floor_snap_length = _walk_floor_snap_length(feet_y) move_and_slide() _after_walk_move_and_slide() if _step_assist_active and ( (is_on_floor() and not is_on_wall()) or (not is_on_floor() and get_slide_collision_count() == 0) ): _step_assist_active = false _debug_trace_transform("physics") _snap_capsule_upright() return if horiz_dist <= DIRECT_APPROACH_RADIUS: if want_goal_h.length_squared() > 1e-12: _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) else: _set_horizontal_velocity_from_nav_path_or_goal(want_goal_h) else: var next_path_position: Vector3 = _nav_agent.get_next_path_position() var to_next_h: Vector3 = Vector3( next_path_position.x - global_position.x, 0.0, next_path_position.z - global_position.z ) # When the next mesh point is almost under the capsule (same XZ), still steer using the # segment from agent to next point if it has length; else fall back to full goal direction # (not arbitrary +X) so vertical-offset goals and nav edges resolve. if to_next_h.length_squared() > 0.0025: _set_horizontal_velocity_toward(next_path_position, want_goal_h) else: _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) _apply_walk_air_gravity(delta) floor_snap_length = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING move_and_slide() _after_walk_move_and_slide() if _step_assist_active: if is_on_floor() and not is_on_wall(): # Landed cleanly on a surface — done climbing. _step_assist_active = false elif not is_on_floor() and get_slide_collision_count() == 0: # Floating free with no contacts: assist must have fired spuriously. # Clear the flag so FLOOR_SNAP_MOVING restores normal gravity next tick. _step_assist_active = false _debug_trace_transform("physics") _snap_capsule_upright()