diff --git a/client/README.md b/client/README.md index 7348f8f..1ede538 100644 --- a/client/README.md +++ b/client/README.md @@ -54,7 +54,7 @@ Elevated walkables use **distinct albedo tints** in `scenes/main.tscn` so screen With the game server running ([`server/README.md`](../server/README.md)), each valid floor click sends a **`MoveCommand`** (**`POST`**) and a follow-up **`GET`** for **`PositionState`**. The server still **snaps** authority to the target (NEON-4/19); the client **moves** toward that verified position using **`NavigationAgent3D`** + a **baked mesh** instead of teleporting on the **`GET`** (NEON-8). **Boot** `sync_from_server()` **snaps** once so spawn matches the server. -**Tradeoff (prototype):** With a valid nav map, horizontal motion follows **`NavigationAgent3D.get_next_path_position()`** (or direct approach near the goal). **No descend bypass:** bee-lining in xz whenever the pick **Y** is below the capsule **feet** applied gravity for the entire crossing of a raised pad toward a lower target (e.g. **TerracePlatformA** or **TerracePlatformB**) and broke movement; **terraces and stepped QA bumps rely on the baked mesh + step assist** instead. **Automatic routing around tall obstacles on one click is still not guaranteed** — use **several clicks** to go around e.g. the gray **`Obstacle`** when needed. +**Tradeoff (prototype):** Horizontal motion is a **direct XZ bee-line** toward the server’s verified target. The baked **`NavigationMesh`** is still used when the goal surface is **clearly above or below** the feet and the capsule is already **within `DIRECT_APPROACH_RADIUS`** of the goal column — so terraces / steps can follow the mesh toward a ramp or tread instead of hugging a vertical face. **`get_next_path_position()`** is no longer the default steer (it regressed under **Jolt** at **120 Hz**). **No descend bypass** for gravity while on an upper deck (same as before). **Automatic routing around tall obstacles on one click is still not guaranteed** — use **several clicks** to go around e.g. the gray **`Obstacle`** when needed. **NEON-7 / movement QA bumps:** On **run**, **`spawn_short_random_bumps`** adds **two** green cylinders, each on its own **`StaticBody3D`** **sibling** of **`Floor`** under **`NavigationRegion3D`** (**`walkable`** on bump roots — avoids compound **internal-edge** jitter vs floor+cylinder on one body). Bump meshes use Godot group **`random_floor_bump_mesh`**. **Collision radius** = mesh **+ `COLLISION_RADIUS_EXTRA`** (see **`scripts/random_floor_bump_collision_constants.gd`**, capped by **`COLLISION_RADIUS_MAX`**). **`bake_navigation_mesh(false)`** after spawn. @@ -69,7 +69,7 @@ With the game server running ([`server/README.md`](../server/README.md)), each v 1. From repo root: `cd server/NeonSprawl.Server && dotnet run` (note the URL/port, usually `http://localhost:5253`). 2. If the port differs, set **`base_url`** on **`PositionAuthorityClient`** accordingly (e.g. `http://127.0.0.1:5253`). 3. Open the client in Godot and run the main scene (**F5**). The player should **snap** to the server’s default position (e.g. **(-5, 0.9, -5)** per `Game:DefaultPosition` / NEON-6 walk demo). -4. **Left-click** the floor: the client **POST**s the target, then **GET**s position; the capsule **walks** toward the authoritative target (follows **`NavigationAgent3D`** waypoints when the map is ready, or direct approach when near the goal). Server-rejected clicks show the reject label and do **not** start a path. +4. **Left-click** the floor: the client **POST**s the target, then **GET**s position; the capsule **walks** in **XZ** straight toward that target (nav path only for **vertical** routing when you are already near the goal column). Server-rejected clicks show the reject label and do **not** start a path. 5. Click the **pedestal top** (orange box at ~(7.5, 0, −6.5)) to confirm **`vertical_step_exceeded`** rejection — the top is ~2.5 m above floor, which exceeds `MaxVerticalStep`. The `MoveRejectFarPad` (blue pad at (9, 9)) is now a normal walkable surface; clicking it from any distance should succeed. 6. After a move (or at spawn), **stand idle** with no click goal for **10+ seconds** and confirm the capsule does **not** visibly vibrate or drift in **x/z** on flat floor, then repeat on the **random green bumps** (positions change each run). @@ -98,7 +98,7 @@ The main scene includes a **prototype terminal** at the map center (same world * ## Movement prototype (NEON-2 → NEON-8) -**`player.gd`** uses **`NavigationAgent3D.get_next_path_position()`** + **`move_and_slide()`** for horizontal motion when following the mesh (NEON-8). **`snap_to_server()`** remains for **boot** (and would apply for any future hard reconcile). +**`player.gd`** steers **XZ** toward the authoritative goal and uses **`NavigationAgent3D`**’s **current path** only for the **vertical-routing** case above; **`move_and_slide()`** does the motion (NEON-8). **`snap_to_server()`** remains for **boot** (and would apply for any future hard reconcile). - The avatar is on **physics layer 2** with **collision_mask** **3** (scans layers **1** and **2** so all walkables/occluders pair reliably). Environment **StaticBody3D** nodes stay on **collision_layer** **1** with **collision_mask** **3** as well. The pick ray uses **mask 1** only, so clicks pass through the avatar and hit the floor. diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 9a29609..a57f32a 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -669,41 +669,20 @@ func _physics_process(delta: float) -> void: ) _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: - _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) - _apply_walk_air_gravity(delta) - floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h) - move_and_slide() - _after_walk_move_and_slide() - _clear_step_assist_after_walk_move() - _debug_trace_transform("physics") - _snap_capsule_upright() - return - - if horiz_dist <= DIRECT_APPROACH_RADIUS: - if want_goal_h.length_squared() > 1e-12: - _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) - else: - _set_horizontal_velocity_from_nav_path_or_goal(want_goal_h) + # Horizontal motion toward the **authoritative** click target (XZ), not toward + # `NavigationAgent3D.get_next_path_position()`. Waypoint steering regressed badly under Jolt / + # 120 Hz (next point barely advancing, path finished edge cases, map iteration timing), which + # reads as “stuck” or a few centimeters per click. Use the baked path only when we are close in + # XZ but the goal surface is clearly above or below the feet — same column as a terrace / + # step / drop — so the mesh can steer us toward a ramp or tread instead of hugging a wall. + var needs_vertical_routing: bool = ( + _auth_walk_goal.y > feet_y + DESCEND_GOAL_Y_MARGIN + or _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN + ) + if needs_vertical_routing and horiz_dist <= DIRECT_APPROACH_RADIUS: + _set_horizontal_velocity_from_nav_path_or_goal(want_goal_h) else: - # Tight path_desired_distance can make the agent oscillate around a corner without ever - # satisfying the advance threshold; finished-with-no-path can also happen if the map was - # empty or the target projected oddly. In those cases keep bee-lining in XZ toward the goal. - if _nav_agent.is_navigation_finished() and horiz_dist > ARRIVE_EPS: - _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) - else: - var next_path_position: Vector3 = _nav_agent.get_next_path_position() - var to_next_h: Vector3 = Vector3( - next_path_position.x - global_position.x, 0.0, next_path_position.z - global_position.z - ) - # When the next mesh point is almost under the capsule (same XZ), still steer using the - # segment from agent to next point if it has length; else fall back to full goal direction - # (not arbitrary +X) so vertical-offset goals and nav edges resolve. - if to_next_h.length_squared() > 0.0025: - _set_horizontal_velocity_toward(next_path_position, want_goal_h) - else: - _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) + _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) _apply_walk_air_gravity(delta) floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h) diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 242b3fe..1ec9389 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -112,6 +112,7 @@ 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. +- **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`**. - **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`**.