NEON-29: Descend on-floor gravity + snap cap; revert terrace Y lift

Reverts the 15mm TerraceStepB/TerracePlatformB root offset that regressed
floor-to-step movement.

When the walk goal is below the feet, apply gravity even while is_on_floor
so lip contacts are not stuck with vy cleared every tick; cap descend
floor_snap_length so the upper surface does not win over internal edges.
pull/41/head
VinPropane 2026-04-12 00:17:09 -04:00
parent f20957975d
commit 3efe65d9bd
3 changed files with 40 additions and 9 deletions

View File

@ -255,7 +255,7 @@ shape = SubResource("BoxShape3D_terrace_a")
[node name="TerraceStepB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000013 groups=["walkable"]] [node name="TerraceStepB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000013 groups=["walkable"]]
collision_mask = 3 collision_mask = 3
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0.015, 8.5) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 8.5)
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerraceStepB" unique_id=3000014] [node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerraceStepB" unique_id=3000014]
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)
@ -268,7 +268,7 @@ shape = SubResource("BoxShape3D_terrace_b_step")
[node name="TerracePlatformB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000016 groups=["walkable"]] [node name="TerracePlatformB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000016 groups=["walkable"]]
collision_mask = 3 collision_mask = 3
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0.015, 13) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 13)
[node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerracePlatformB" unique_id=3000017] [node name="MeshInstance3D" type="MeshInstance3D" parent="World/NavigationRegion3D/TerracePlatformB" unique_id=3000017]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)

View File

@ -28,6 +28,13 @@ const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 8
## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**). ## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**).
const IDLE_RIM_SETTLE_STEP: float = 0.004 const IDLE_RIM_SETTLE_STEP: float = 0.004
const DESCEND_GOAL_Y_MARGIN: float = 0.06 const DESCEND_GOAL_Y_MARGIN: float = 0.06
## Cap floor snap while descending so a long snap cannot glue the capsule to the upper surface
## across an internal edge; widened again once feet are near the goal height.
const DESCEND_LIP_SNAP_CAP: float = 0.1
## While the nav goal is below the feet, `is_on_floor()` at a terrace lip still skips
## `_apply_walk_air_gravity`, leaving vy cleared by `_set_horizontal_velocity_toward` — add
## gravity explicitly so descent is not stuck until the contact finally breaks.
const DESCEND_ON_FLOOR_GRAVITY_MULT: float = 2.5
## Y lift when blocked by wall-ish slide but nav goal is higher (stepped bumps). ## Y lift when blocked by wall-ish slide but nav goal is higher (stepped bumps).
const WALK_STEP_ASSIST_DELTA: float = 0.11 const WALK_STEP_ASSIST_DELTA: float = 0.11
## Skip assist when horizontal velocity already aligns enough with want-dir (corners slide slowly). ## Skip assist when horizontal velocity already aligns enough with want-dir (corners slide slowly).
@ -257,14 +264,35 @@ func _apply_walk_air_gravity(delta: float) -> void:
velocity += g_step velocity += g_step
func _apply_descend_on_floor_gravity(delta: float, feet_y: float) -> void:
if not _has_walk_goal:
return
if _auth_walk_goal.y >= feet_y - DESCEND_GOAL_Y_MARGIN:
return
var g_step: Vector3 = get_gravity() * delta
if g_step.y >= 0.0:
return
if is_on_floor():
velocity.y += g_step.y * DESCEND_ON_FLOOR_GRAVITY_MULT
func _walk_floor_snap_length(feet_y: float) -> float:
var fl: float = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING
if (
_has_walk_goal
and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN
and feet_y > _auth_walk_goal.y + 0.06
):
fl = minf(fl, DESCEND_LIP_SNAP_CAP)
return fl
func _update_floor_block_on_wall_for_terraces(feet_y: float) -> void: func _update_floor_block_on_wall_for_terraces(feet_y: float) -> void:
floor_block_on_wall = true floor_block_on_wall = true
if not _has_walk_goal: if not _has_walk_goal:
return return
# Only relax for descending (step/platform → lower surface). One stable policy avoids # Only relax for descending (step/platform → lower surface). One stable policy avoids
# frame-to-frame floor_block / snap / peel fighting (oscillation at terrace lips). Terrace B # frame-to-frame floor_block toggling at terrace lips.
# geometry is offset slightly above the main floor in the district scene so this is not
# fighting coplanar internal edges.
var goal_below: bool = _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN var goal_below: bool = _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN
if goal_below: if goal_below:
floor_block_on_wall = false floor_block_on_wall = false
@ -629,7 +657,8 @@ func _physics_process(delta: float) -> void:
if NavigationServer3D.map_get_iteration_id(nav_map) == 0: if NavigationServer3D.map_get_iteration_id(nav_map) == 0:
_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) _apply_walk_air_gravity(delta)
floor_snap_length = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING _apply_descend_on_floor_gravity(delta, feet_y)
floor_snap_length = _walk_floor_snap_length(feet_y)
move_and_slide() move_and_slide()
_after_walk_move_and_slide() _after_walk_move_and_slide()
if ( if (
@ -652,7 +681,8 @@ func _physics_process(delta: float) -> void:
if _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN: if _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN:
_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) _apply_walk_air_gravity(delta)
floor_snap_length = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING _apply_descend_on_floor_gravity(delta, feet_y)
floor_snap_length = _walk_floor_snap_length(feet_y)
move_and_slide() move_and_slide()
_after_walk_move_and_slide() _after_walk_move_and_slide()
if ( if (
@ -686,7 +716,8 @@ func _physics_process(delta: float) -> void:
_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) _apply_walk_air_gravity(delta)
floor_snap_length = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING _apply_descend_on_floor_gravity(delta, feet_y)
floor_snap_length = _walk_floor_snap_length(feet_y)
move_and_slide() move_and_slide()
_after_walk_move_and_slide() _after_walk_move_and_slide()
if _step_assist_active: if _step_assist_active:

View File

@ -109,7 +109,7 @@ No new automated GDScript tests are added for this story — there is no new GDS
- **Arrival check using wrong capsule reference (resolved):** `vertical_arrival_error` was called with `CAPSULE_HALF_HEIGHT = 0.5`, giving code-feet at `body_y 0.5 = 0.4` at floor level. Step surfaces (Y = 0.3) are only 0.1 m from that reference. When the Jolt physics engine nudges the capsule body down to ~0.8 m (bottom hemisphere contacts the step edge), code-feet becomes 0.3 — matching the step surface Y and making `vert_err = 0 ≤ VERT_ARRIVE_EPS`. If the player is also within `ARRIVE_EPS = 0.35 m` horizontally, the arrival check fires immediately, clearing the nav goal before the capsule has moved. Fixed: pass `CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS` (= 0.9) to `vertical_arrival_error` so actual feet = `body_y 0.9`. Step goal vert_err is now 0.3 >> 0.055 from floor height; arrival fires correctly only once the step assist has lifted the capsule onto the surface (body_y = 1.2, vert_err ≈ 0). Two regression tests added. - **Arrival check using wrong capsule reference (resolved):** `vertical_arrival_error` was called with `CAPSULE_HALF_HEIGHT = 0.5`, giving code-feet at `body_y 0.5 = 0.4` at floor level. Step surfaces (Y = 0.3) are only 0.1 m from that reference. When the Jolt physics engine nudges the capsule body down to ~0.8 m (bottom hemisphere contacts the step edge), code-feet becomes 0.3 — matching the step surface Y and making `vert_err = 0 ≤ VERT_ARRIVE_EPS`. If the player is also within `ARRIVE_EPS = 0.35 m` horizontally, the arrival check fires immediately, clearing the nav goal before the capsule has moved. Fixed: pass `CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS` (= 0.9) to `vertical_arrival_error` so actual feet = `body_y 0.9`. Step goal vert_err is now 0.3 >> 0.055 from floor height; arrival fires correctly only once the step assist has lifted the capsule onto the surface (body_y = 1.2, vert_err ≈ 0). Two regression tests added.
- **snap_to_server places capsule underground (resolved):** The server stores the raw surface Y from the client's click (e.g. 0.6 for a terrace platform), not the capsule body-centre Y (which should be 0.6 + 0.9 = 1.5). `snap_to_server` was setting `global_position.y` directly to this surface Y, putting the capsule bottom at 0.3 m. With `_floor_angle_loose_ticks = 0` and a stale `is_on_floor() = true`, the idle path's `_stable_idle_support()` check passed and held the player without calling `move_and_slide()`, preventing Jolt from resolving the penetration. Fixed: clamp `global_position.y ≥ CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS` (= 0.9 m) in `snap_to_server`; also set `_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP` (96 ticks) to force corrective idle physics before the next walk goal. - **snap_to_server places capsule underground (resolved):** The server stores the raw surface Y from the client's click (e.g. 0.6 for a terrace platform), not the capsule body-centre Y (which should be 0.6 + 0.9 = 1.5). `snap_to_server` was setting `global_position.y` directly to this surface Y, putting the capsule bottom at 0.3 m. With `_floor_angle_loose_ticks = 0` and a stale `is_on_floor() = true`, the idle path's `_stable_idle_support()` check passed and held the player without calling `move_and_slide()`, preventing Jolt from resolving the penetration. Fixed: clamp `global_position.y ≥ CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS` (= 0.9 m) in `snap_to_server`; also set `_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP` (96 ticks) to force corrective idle physics before the next walk goal.
- **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 step/platform → floor stuck / oscillation (resolved):** `TerraceStepB` and `TerracePlatformB` had box bottoms **coplanar** with the main floor top (Y = 0), which tends to produce bad **internal-edge** behavior with Jolt + `CharacterBody3D`. Raised both roots by **0.015 m** so collision is slightly above the floor slab. Removed descend-only **lip stall / weakstrong peel / snap-cap** toggles in `player.gd` that could flip `floor_block_on_wall` and vertical forces frame-to-frame; while the nav goal is below the feet (descend), `floor_block_on_wall` stays **false** for the whole descend until arrival clears the goal. - **Terrace B descend / climb (resolved, iterated):** A **0.015 m** root lift on `TerraceStepB` / `TerracePlatformB` was tried to break coplanar internal edges with the main floor; it correlated with **floor → step** sticking and was **reverted**. **Descend** stayed bad because `_set_horizontal_velocity_toward` clears **Y** each tick and `_apply_walk_air_gravity` only runs when **`!is_on_floor()`**, so a terrace lip still reported as on-floor got **no** downward acceleration (“very slow gravity” until the contact broke). **Fix:** `_apply_descend_on_floor_gravity` applies gravity while **on floor** when the nav goal is below the feet; **`DESCEND_LIP_SNAP_CAP`** limits `floor_snap_length` until feet are near goal Y; while descending, **`floor_block_on_wall`** stays **false** (stable, no per-frame toggling).
## Open questions / risks ## Open questions / risks