NEON-29: Teal pad lips — wall normal assist, rim descend, +12mm lift

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.
pull/41/head
VinPropane 2026-04-12 00:42:25 -04:00
parent b4c0defc29
commit d81e915a0a
4 changed files with 20 additions and 7 deletions

View File

@ -40,7 +40,7 @@ Elevated walkables use **distinct albedo tints** in `scenes/main.tscn` so screen
### Height variation notes ### 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. - **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. - **Occluders** are plain `StaticBody3D` with no `walkable` tag; the nav bake excludes their top surfaces, so they remain true obstacles for routing.

View File

@ -255,7 +255,7 @@ shape = SubResource("BoxShape3D_obstacle_d")
[node name="TerracePlatformA" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000010 groups=["walkable"]] [node name="TerracePlatformA" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000010 groups=["walkable"]]
collision_mask = 3 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] [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) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0)

View File

@ -161,14 +161,22 @@ func _step_assist_has_support(want_dir_xz: Vector3) -> bool:
return false return false
## Returns true when slide collisions contain a wall-ish contact that opposes forward motion. ## True when a vertical-ish contact opposes motion toward the goal. Jolt often reports a wall
## The `is_on_wall()` short-circuit was removed: it returned true for any wall contact regardless ## via `is_on_wall()` + `get_wall_normal()` at box lips while slide normals are ambiguous; the
## of direction, firing the assist for sideways bump-skin contacts unrelated to step climbing. ## old slide-only loop missed head-on climbs onto e.g. TerracePlatformA (teal pad).
func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool: func _step_assist_wallish_blocks(want_dir_xz: Vector3) -> bool:
var w2 := Vector2(want_dir_xz.x, want_dir_xz.z) var w2 := Vector2(want_dir_xz.x, want_dir_xz.z)
if w2.length_squared() < 1e-8: if w2.length_squared() < 1e-8:
return false return false
w2 = w2.normalized() 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(): for i: int in get_slide_collision_count():
var n: Vector3 = get_slide_collision(i).get_normal() var n: Vector3 = get_slide_collision(i).get_normal()
if n.y > 0.52: 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: if n2.length_squared() < 1e-8:
continue continue
n2 = n2.normalized() n2 = n2.normalized()
if n2.dot(w2) < -0.04: if n2.dot(w2) < -0.02:
return true return true
return false return false
@ -271,9 +279,13 @@ func _apply_floor_block_for_descend_at_wall(feet_y: float) -> void:
if is_on_wall(): if is_on_wall():
floor_block_on_wall = false floor_block_on_wall = false
return 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()): for i: int in range(get_slide_collision_count()):
var n: Vector3 = get_slide_collision(i).get_normal() 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 floor_block_on_wall = false
return return

View File

@ -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. - **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. - **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. - **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 ## Open questions / risks