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