diff --git a/client/scripts/player.gd b/client/scripts/player.gd index f311e9b..5a1f27e 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -32,19 +32,12 @@ 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.16 -const WALK_STEP_ASSIST_COOLDOWN_TICKS: int = 1 +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 -## 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`). @@ -77,8 +70,6 @@ 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 @@ -98,7 +89,6 @@ 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) @@ -109,7 +99,6 @@ 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) @@ -146,7 +135,6 @@ 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() @@ -250,32 +238,13 @@ 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 +func _clear_step_assist_after_walk_move() -> void: + if not _step_assist_active: 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 + 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( @@ -310,9 +279,9 @@ func _walk_floor_snap_length(feet_y: float, want_goal_h: Vector3) -> float: if _has_walk_goal: if want_goal_h.length_squared() > 1e-10: var want: Vector3 = want_goal_h.normalized() - # Climb: FLOOR_SNAP_MOVING reaches past the lip and snaps the capsule back to the slab - # below every tick — fights step assist on cardinal box faces (teal pad). - if _auth_walk_goal.y > feet_y + 0.035 and _step_assist_wallish_blocks(want): + # 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 if ( _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN @@ -325,14 +294,12 @@ func _walk_floor_snap_length(feet_y: float, want_goal_h: Vector3) -> float: ## 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, horiz_dist: float) -> void: +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 - var vh: float = Vector2(velocity.x, velocity.z).length() - var descending_stall: bool = horiz_dist > 0.12 and vh < 0.55 if is_on_wall(): floor_block_on_wall = false return @@ -345,8 +312,6 @@ func _apply_floor_block_for_descend_at_wall(feet_y: float, horiz_dist: float) -> if absf(n.y) < 0.58: floor_block_on_wall = false return - if descending_stall: - floor_block_on_wall = false ## When the authoritative goal shares XZ with the capsule (e.g. terrace above), horizontal @@ -689,7 +654,6 @@ 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(): @@ -703,7 +667,7 @@ func _physics_process(delta: float) -> void: 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, horiz_dist) + _apply_floor_block_for_descend_at_wall(feet_y) var nav_map: RID = _nav_agent.get_navigation_map() if NavigationServer3D.map_get_iteration_id(nav_map) == 0: @@ -712,7 +676,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() - _finalize_walk_move_tick(horiz_dist, feet_y, vert_err) + _clear_step_assist_after_walk_move() _debug_trace_transform("physics") _snap_capsule_upright() return @@ -739,6 +703,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() - _finalize_walk_move_tick(horiz_dist, feet_y, vert_err) + _clear_step_assist_after_walk_move() _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 69c9ec5..df8f7dd 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -112,9 +112,8 @@ No new automated GDScript tests are added for this story — there is no new GDS - **Terrace B descend / climb (resolved, iterated):** Scene lifts and **descend-bypass** bee-lines caused regressions (stuck climbs, lip oscillation, wrong-direction slides). **Removed the descend bypass:** vertical moves (e.g. platform ↔ floor) use the **baked nav path + step assist** only. **Walk gravity** is **`!is_on_floor()`** again — never “goal below feet” while still walking on an upper surface (that applied gravity across the whole **TerracePlatformB** deck and destroyed horizontal motion). **`velocity.y`** is cleared by the horizontal steer each tick as before. - **Lip stuck (floor→platform / platform→floor):** QA screenshots for this were on **`TerracePlatformA`** (10 × 10 large SE pad, **0.3 m** single step — not the NW **Terrace B** step/platform pair). Step assist required **`vel_h.dot(want) ≤ 0.52`**, so a **head-on** push into the vertical face (dot ≈ 1) **never** triggered the lift. **Removed** that gate. For **platform→floor**, **`floor_block_on_wall = false`** only when the nav goal is **below** the feet **and** **`is_on_wall()`** or a **wall-ish slide normal** exists, so open deck walking stays stable. - **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 cardinal / tread shake (rollback):** Aggressive tweaks (**0.16** lift, **1**-tick cooldown, **`descending_stall`** toggling **`floor_block_on_wall`**, **seam-based** step-assist clear, **walk-stall nav replan**) caused **oscillation** on the small approach treads. **Reverted** to **`WALK_STEP_ASSIST_DELTA = 0.11`**, **`WALK_STEP_ASSIST_COOLDOWN_TICKS = 8`**, **no** stall replan, **no** `descending_stall` branch, **no** seam-based assist clear — only the original clear (clean floor without wall, or airborne with no slides). **Climb** wallish snap cap applies only when **`goal.y > feet_y + 0.12`** so shallow tread-to-tread motion is not capped to assist snap every frame. - **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