From d81e915a0a5e9006a5f154e8bb443d41a107265f Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 12 Apr 2026 00:42:25 -0400 Subject: [PATCH] =?UTF-8?q?NEON-29:=20Teal=20pad=20lips=20=E2=80=94=20wall?= =?UTF-8?q?=20normal=20assist,=20rim=20descend,=20+12mm=20lift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step assist: use is_on_wall + get_wall_normal opposing want; keep slide loop with dot < -0.02. Descend: relax floor_block on shallow floor normal at internal edges; widen vertical-ish slide threshold slightly. Raise TerracePlatformA Y by 0.012 m vs main floor for coplanarity. --- client/README.md | 2 +- client/scenes/main.tscn | 2 +- client/scripts/player.gd | 22 +++++++++++++++++----- docs/plans/NEON-29-implementation-plan.md | 1 + 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/client/README.md b/client/README.md index 49489f9..c6dba71 100644 --- a/client/README.md +++ b/client/README.md @@ -40,7 +40,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. +- **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 in the scene to reduce coplanar internal edges with Jolt; 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. - **Occluders** are plain `StaticBody3D` with no `walkable` tag; the nav bake excludes their top surfaces, so they remain true obstacles for routing. diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index ec6dd13..f8ca0ab 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -255,7 +255,7 @@ shape = SubResource("BoxShape3D_obstacle_d") [node name="TerracePlatformA" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000010 groups=["walkable"]] collision_mask = 3 -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13, 0, -14) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13, 0.012, -14) [node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerracePlatformA" unique_id=3000011] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 3030701..9c9649c 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -161,14 +161,22 @@ func _step_assist_has_support(want_dir_xz: Vector3) -> bool: return false -## Returns true when slide collisions contain a wall-ish contact that opposes forward motion. -## The `is_on_wall()` short-circuit was removed: it returned true for any wall contact regardless -## of direction, firing the assist for sideways bump-skin contacts unrelated to step climbing. +## True when a vertical-ish contact opposes motion toward the goal. Jolt often reports a wall +## via `is_on_wall()` + `get_wall_normal()` at box lips while slide normals are ambiguous; the +## old slide-only loop missed head-on climbs onto e.g. TerracePlatformA (teal pad). func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool: var w2 := Vector2(want_dir_xz.x, want_dir_xz.z) if w2.length_squared() < 1e-8: return false w2 = w2.normalized() + if is_on_wall(): + var wn: Vector3 = get_wall_normal() + var wnh := Vector2(wn.x, wn.z) + if wnh.length_squared() > 1e-10: + wnh = wnh.normalized() + # Wall pushes back against forward (not parallel skimming along the face). + if wnh.dot(w2) < -0.02: + return true for i: int in get_slide_collision_count(): var n: Vector3 = get_slide_collision(i).get_normal() if n.y > 0.52: @@ -177,7 +185,7 @@ func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool: if n2.length_squared() < 1e-8: continue n2 = n2.normalized() - if n2.dot(w2) < -0.04: + if n2.dot(w2) < -0.02: return true return false @@ -271,9 +279,13 @@ func _apply_floor_block_for_descend_at_wall(feet_y: float) -> void: if is_on_wall(): floor_block_on_wall = false return + # Terrace lip: floor normal tilts off UP when straddling an internal edge (teal pad, etc.). + if is_on_floor() and get_floor_normal().dot(Vector3.UP) < 0.992: + floor_block_on_wall = false + return for i: int in range(get_slide_collision_count()): var n: Vector3 = get_slide_collision(i).get_normal() - if absf(n.y) < 0.52: + if absf(n.y) < 0.58: floor_block_on_wall = false return diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 7650cc2..3e559b0 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -111,6 +111,7 @@ No new automated GDScript tests are added for this story — there is no new GDS - **Step assist undone by floor snap (resolved):** Each `_try_walk_step_assist()` cycle lifts the capsule 0.11 m. The prior `floor_snap_length = FLOOR_SNAP_MOVING = 0.32 m` meant the snap always reached the lower floor (0.11 m below the lifted bottom), pulling the capsule straight back down every tick. The player "nudged and stopped" because each lift was immediately cancelled. Fixed: added `WALK_STEP_ASSIST_SNAP = 0.09 m` constant (below 0.11 m so the snap cannot reach the previous floor, but above 0.03 m so it catches the step surface once cleared) and a `_step_assist_active` boolean flag. During active climbing, `floor_snap_length` is set to `WALK_STEP_ASSIST_SNAP` in both `_after_walk_move_and_slide()` and the main physics loop. The flag is cleared when the player lands cleanly on a floor without wall contact (`is_on_floor() and not is_on_wall()`), or when the nav goal is cleared/snapped. - **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. ## Open questions / risks