From 8d1785d288ad11de98ed805ca140deeffaa8a923 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 12 Apr 2026 00:53:36 -0400 Subject: [PATCH] NEON-29: Clear step assist on seam walls; replan nav on walk stall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step assist stayed active when is_on_wall from mesh seams after landing on the goal height — keep reduced snap and odd slides. Clear assist when vert_err and feet vs goal show we are on the target slab. If horizontal speed stays low with a far goal, refresh NavigationAgent3D target after 8 ticks to break stale paths. --- client/scripts/player.gd | 58 ++++++++++++++++------- docs/plans/NEON-29-implementation-plan.md | 1 + 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 65730af..f311e9b 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -38,6 +38,13 @@ const WALK_STEP_ASSIST_COOLDOWN_TICKS: int = 1 ## (~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 +## Clear step-assist when feet match the goal slab even if `is_on_wall()` (mesh seams / tread lip). +const STEP_ASSIST_CLEAR_VERT_ERR: float = 0.1 +const STEP_ASSIST_CLEAR_FEET_BELOW_GOAL_MAX: float = 0.05 +## Low horizontal speed with an active far goal: refresh nav target to break stale paths. +const WALK_STALL_VH_MAX: float = 0.22 +const WALK_STALL_HORIZ_MIN: float = 0.4 +const WALK_STALL_REPATH_TICKS: int = 8 ## 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`). @@ -70,6 +77,7 @@ 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 +var _walk_stall_repath_ticks: int = 0 @onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D @@ -90,6 +98,7 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void: _step_assist_active = false _idle_anchor_active = false _idle_manual_correction_ticks = 0 + _walk_stall_repath_ticks = 0 _nav_agent.set_target_position(world_pos) @@ -100,6 +109,7 @@ func clear_nav_goal() -> void: _step_assist_active = false _idle_anchor_active = false _idle_manual_correction_ticks = 0 + _walk_stall_repath_ticks = 0 _nav_agent.set_target_position(global_position) @@ -136,6 +146,7 @@ func snap_to_server(world_pos: Vector3) -> void: _step_assist_active = false _idle_anchor_active = false _idle_manual_correction_ticks = 0 + _walk_stall_repath_ticks = 0 _nav_agent.set_target_position(settled) reset_physics_interpolation() @@ -239,6 +250,34 @@ func _after_walk_move_and_slide() -> void: move_and_slide() +func _finalize_walk_move_tick(horiz_dist: float, feet_y: float, vert_err: float) -> void: + var vh_post: float = Vector2(velocity.x, velocity.z).length() + if _step_assist_active: + 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 + elif ( + is_on_floor() + and vert_err <= STEP_ASSIST_CLEAR_VERT_ERR + and feet_y >= _auth_walk_goal.y - STEP_ASSIST_CLEAR_FEET_BELOW_GOAL_MAX + ): + _step_assist_active = false + if not _has_walk_goal: + _walk_stall_repath_ticks = 0 + return + if horiz_dist > WALK_STALL_HORIZ_MIN: + if vh_post < WALK_STALL_VH_MAX: + _walk_stall_repath_ticks += 1 + if _walk_stall_repath_ticks >= WALK_STALL_REPATH_TICKS: + _nav_agent.set_target_position(_auth_walk_goal) + _walk_stall_repath_ticks = 0 + else: + _walk_stall_repath_ticks = 0 + else: + _walk_stall_repath_ticks = 0 + + func _set_horizontal_velocity_toward( point: Vector3, fallback_dir_xz: Vector3 = Vector3.ZERO ) -> void: @@ -650,6 +689,7 @@ func _physics_process(delta: float) -> void: _step_assist_active = false _idle_anchor_active = false _idle_manual_correction_ticks = 0 + _walk_stall_repath_ticks = 0 _nav_agent.set_target_position(global_position) _physics_idle_tick(delta) if _apply_idle_manual_correction(): @@ -672,14 +712,7 @@ func _physics_process(delta: float) -> void: floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h) 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 + _finalize_walk_move_tick(horiz_dist, feet_y, vert_err) _debug_trace_transform("physics") _snap_capsule_upright() return @@ -706,13 +739,6 @@ func _physics_process(delta: float) -> void: floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h) 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 + _finalize_walk_move_tick(horiz_dist, feet_y, vert_err) _debug_trace_transform("physics") _snap_capsule_upright() diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 5738cda..69c9ec5 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -114,6 +114,7 @@ No new automated GDScript tests are added for this story — there is no new GDS - **Teal pad still stuck (follow-up):** Step assist used **slide normals only**; Jolt often exposes the lip via **`is_on_wall()`** / **`get_wall_normal()`** instead — re-added a **directional** wall check (`wnh.dot(want) < -0.02`). Descend relax now also triggers on **shallow floor normal** (`get_floor_normal().dot(UP) < 0.992`) and slightly wider vertical-ish slide band. **`TerracePlatformA`** root **Y = 0.012 m** breaks **coplanarity** with the main floor without the larger gap that hurt **TerraceStepB** approach. - **Teal cardinal both ways (follow-up):** **Climb:** **`FLOOR_SNAP_MOVING` (0.32 m)** was still active while **`_step_assist_wallish_blocks`** was true, snapping the capsule back to the lower floor each tick and cancelling lifts — cap snap to **`WALK_STEP_ASSIST_SNAP`** in that state; **bigger per-tick lift** (`WALK_STEP_ASSIST_DELTA` **0.16**) and **cooldown 1** tick. **Descend:** reintroduced **`DESCEND_LIP_SNAP_CAP`** when clearly above the lower goal, plus **`descending_stall`** (`horiz_dist` not trivial, **`vh < 0.55`**) to relax **`floor_block_on_wall`** when Jolt reports neither wall nor shallow floor at a cardinal lip. - **Teal pad — geometry wins:** Script-only mitigations still failed in practice; **`TerracePlatformA_Approach`** adds **twelve** `walkable` **`StaticBody3D`** treads (three per cardinal) with **~0.104 m** rise each, **darker teal** albedo, flush to the platform lip — nav + `CharacterBody3D` use **sloped contact** instead of fighting a **single vertical face**. +- **Intermittent “stuck until re-click”:** **`_step_assist_active`** only cleared when **`not is_on_wall()`**; harmless **wall** flags from tread/platform seams left **reduced snap** on and blocked the old “clean landing” path — added clear when **feet are on the goal slab** (`vert_err` / feet vs goal Y). **Nav** can go stale when physics stalls (`vh` low, goal still far): after **8** ticks, **`set_target_position(_auth_walk_goal)`** again to replan. ## Open questions / risks