diff --git a/client/README.md b/client/README.md index 1ede538..fd976aa 100644 --- a/client/README.md +++ b/client/README.md @@ -41,7 +41,7 @@ Elevated walkables use **distinct albedo tints** in `scenes/main.tscn` so screen ### Height variation notes - **Single-step terraces (A, C):** 0.3 m rise — within `agent_max_climb = 0.35`, so the nav mesh routes directly from floor onto the pad; one click suffices. **`TerracePlatformA`** (teal) sits **12 mm** above the main floor slab and has **cardinal walkable treads** (`TerracePlatformA_Approach`) so movement does not rely on a bare vertical box lip; nav rebakes at startup. -- **Two-level terrace B:** floor → `TerraceStepB` (0.3 m) → `TerracePlatformB` (additional 0.3 m). Each transition is within climb limit, so the agent can route the full ascent in a single click-to-move target anywhere on the platform. +- **Two-level terrace B:** floor → `TerraceStepB` (**gold**, 0.3 m) → `TerracePlatformB` (**violet**, +0.3 m). **`agent_max_climb = 0.35`** applies to the nav mesh **and** to **walk step assist**: you cannot cheese the violet deck from the floor in one move — click the **gold step** (or the platform after you are already on the step), not the violet top from below. - **Occluders** are plain `StaticBody3D` with no `walkable` tag; the nav bake excludes their top surfaces, so they remain true obstacles for routing. ### Designer / QA limits diff --git a/client/scripts/player.gd b/client/scripts/player.gd index a57f32a..5c1bec8 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -38,6 +38,11 @@ const WALK_STEP_ASSIST_COOLDOWN_TICKS: int = 8 ## (~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 ## 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`). @@ -206,6 +211,8 @@ func _try_walk_step_assist() -> bool: 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: diff --git a/client/test/player_test.gd b/client/test/player_test.gd index a912ae6..84c4573 100644 --- a/client/test/player_test.gd +++ b/client/test/player_test.gd @@ -154,3 +154,8 @@ func test_flat_floor_goal_is_not_below_feet_height_margin() -> void: goal_y < PLAYER_SCRIPT.capsule_feet_y(body_origin_y, 0.5) - descend_margin ) assert_that(below_feet_margin).is_false() + + +func test_step_assist_max_surface_delta_matches_navigation_mesh_agent_max_climb() -> void: + # Keep in sync with NavigationMesh `agent_max_climb` in `scenes/main.tscn`. + assert_that(PLAYER_SCRIPT.get("WALK_STEP_ASSIST_MAX_SURFACE_DELTA") as float).is_equal(0.35) diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 1ec9389..89efd8f 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -113,6 +113,7 @@ No new automated GDScript tests are added for this story — there is no new GDS - **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. - **Nav waypoint steering dropped (movement):** Following **`get_next_path_position()`** by default caused **near-zero** horizontal progress under **Jolt** + **120 Hz** (waypoint advance / finished edge cases), read as universal **stuck** movement even with the server up. **`player.gd`** now **bee-lines in XZ** toward **`_auth_walk_goal`** and uses **`_set_horizontal_velocity_from_nav_path_or_goal`** only when **`needs_vertical_routing`** (goal **Y** vs feet beyond **`DESCEND_GOAL_Y_MARGIN`**) **and** **`horiz_dist <= DIRECT_APPROACH_RADIUS`**. +- **Step assist vs `agent_max_climb`:** Nav **`agent_max_climb = 0.35`** only constrained mesh links; **bee-line + chained step assist** could still climb **TerracePlatformB** (~**0.6 m** from floor) in one click. **`_try_walk_step_assist`** now returns false when **`_auth_walk_goal.y > feet + WALK_STEP_ASSIST_MAX_SURFACE_DELTA`** (**0.35**, same as the mesh). - **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**. **Follow-up:** tread **run** was **0.45 m** along the climb axis while the capsule **diameter is 0.8 m** — the body was wider than each tread, causing straddle / stuck motion when **entering** and blocking horizontal progress toward the lip when **exiting**. Treads widened to **1.0 m** run with positions re-centered. **`NavigationAgent3D`** `path_desired_distance` / `target_desired_distance` were briefly lowered to **0.22** but that caused **floor** movement to **stall** (waypoint threshold tighter than practical motion at `MOVE_SPEED` / physics dt, plus finished-without-arrival edge cases); **reverted to 0.35** and **`player.gd`** now **bee-lines in XZ** when **`is_navigation_finished()`** yet **`horiz_dist > ARRIVE_EPS`**.