neon-sprawl/client/scripts/player.gd

1139 lines
46 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
## 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 = 1.12
## 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
## On tread edges, path vs direct or waypoint lookahead can flip the horizontal seek vector ~180° each
## physics tick → [method move_and_slide] + snap Y noise (~7 cm, NEON-16). Keep prior bearing if the new
## direction is more than ~110° from current horizontal [member velocity].
const WALK_STEER_REVERSE_REJECT_DOT: float = -0.35
const WALK_STEER_MIN_CUR_SPEED_SQ: 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 peel and allow normal snap when the
## feet are within this vertical distance of the goal **surface** (m).
const WALK_PEEL_SUSPEND_VERT_SEP: float = 0.48
## 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 [member WALK_PEEL_SUSPEND_VERT_SEP]
## so shallow lips (~0.48 m) still peel; widened from 0.34 to cover ~0.350.47 m stair/goal gaps.
const WALK_PEEL_SUSPEND_HORIZ_VERT_SEP: float = 0.46
const WALK_PEEL_SUSPEND_HORIZ_DIST: float = 5.0
## [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
## 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
## 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.99910.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 = 8
## Consecutive [code]raw_stable[/code] ticks required before first idle anchor latch (entry debounce).
const STABLE_IDLE_ENTER_STREAK_FRAMES: int = 2
## 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). Must stay **below** approach tread rise
## (~0.104 m in [code]main.tscn[/code]): if the next tread counts as “continuation”, [method _walk_has_close_floor_probe_below]
## stays true on stairs while [code]is_on_floor()[/code] flickers — ledge peel never runs and the capsule
## glides horizontally with only weak airborne gravity. Still below a full gold→floor drop (~0.3 m) so
## void probes toward the slab reject the lower hit.
const WALK_CONTINUATION_MAX_BELOW_FEET: float = 0.078
## If capsule feet are farther above the clicks surface Y than this, always use walk gravity + zero
## moving snap (rays can still read “support” while [method CharacterBody3D.is_on_floor] hugs a lip).
## ~0.42 m: **TerracePlatformB** top (~0.6 m) → floor (0) qualifies; **TerraceStepB** (~0.3 m) → floor does
## not — shallow drops still rely on [method _walk_has_close_floor_probe_below].
const WALK_DEEP_DESCENT_FEET_ABOVE_GOAL: float = 0.42
## [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
## 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_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
## 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
@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
_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_ledge_peel_vy = 0.0
_walk_peel_no_probe_streak = 0
_walk_debounced_wants_ledge_peel = false
_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_peel_no_probe_streak = 0
_walk_debounced_wants_ledge_peel = false
_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
_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
_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
## 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
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
if _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
# Compute want direction before the support/blocking checks so both can use it.
var want: Vector3 = to_h.normalized()
if not _step_assist_wallish_blocks(want):
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 _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 _walk_clamp_steering_reverse(desired_dir_xz: Vector3) -> Vector3:
var d2 := Vector2(desired_dir_xz.x, desired_dir_xz.z)
if d2.length_squared() < 1e-10:
return desired_dir_xz
d2 = d2.normalized()
var cur := Vector2(velocity.x, velocity.z)
if cur.length_squared() < WALK_STEER_MIN_CUR_SPEED_SQ:
return Vector3(d2.x, 0.0, d2.y)
var cur_n := cur.normalized()
if d2.dot(cur_n) < WALK_STEER_REVERSE_REJECT_DOT:
return Vector3(cur_n.x, 0.0, cur_n.y)
return Vector3(d2.x, 0.0, d2.y)
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()
dh = _walk_clamp_steering_reverse(dh)
velocity = dh * MOVE_SPEED
velocity.y = 0.0
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
)
if descending:
if to_g.length_squared() >= 1e-6:
return to_g
if wh.length_squared() >= 1e-8:
return wh
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:
return d
if to_g.length_squared() > 1e-12:
return to_g
if vxz.length_squared() >= 1e-8:
return vxz
return Vector2(1.0, 0.0)
if vxz.length_squared() < 1e-10:
return wh
return vxz
## 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 cross of a raised pad (e.g. TerracePlatformA / B)
## toward a floor click and poisons horizontal motion / slide resolution.
## Do apply gravity when [code]is_on_floor()[/code] is still true but there is no walkable surface
## within [member WALK_SUPPORT_PROBE_DEPTH] under the foot disk (ledge / void while moving).
## Only while **airborne**. With default [member MotionMode.MOTION_MODE_GROUNDED], [method move_and_slide]
## clears downward [code]velocity.y[/code] every tick while [method is_on_floor] — adding gravity on the
## velocity does **not** move the body off a terrace lip; use [method _apply_walk_post_slide_ledge_peel].
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_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:
return true
return vsep <= WALK_PEEL_SUSPEND_HORIZ_VERT_SEP and horiz_dist <= WALK_PEEL_SUSPEND_HORIZ_DIST
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: it stopped peel while feet were still ~6 cm above
# the goal surface → slow “float to rest” at XZ destination. Only stop 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).
_walk_ledge_peel_vy += _player_vertical_accel(delta).y * delta
var dy: float = _walk_ledge_peel_vy * delta
if absf(dy) > 1e-9:
# Raw [code]global_position[/code] edits can be ignored/overwritten vs Jolt sync 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
# (~23 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 engines “next” point can sit within
# Jolts 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():
_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:
_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:
_set_horizontal_velocity_toward(_auth_walk_goal, fallback_goal_xz)
return
dir = _walk_clamp_steering_reverse(dir)
velocity = dir * MOVE_SPEED
velocity.y = 0.0
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
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
if floor_normal.dot(Vector3.UP) < min_flat_up_dot:
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 idle_slide_contacts_are_ridged(slide_normals)
and floor_normal.dot(Vector3.UP) < 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(
(
(
"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
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(
(
(
"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%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 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
# Do not tie wide floor angle to `_floor_angle_loose_ticks` alone: after a walk on **flat**
# ground that still widened angle for ~0.8 s, `move_and_slide` + idle snap caused visible XZ
# jitter. 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
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:
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:
_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 = 0
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
# 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
_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
_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
_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:
_set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h)
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()