diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 7da7270..a9a43fb 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -28,13 +28,10 @@ const IDLE_MANUAL_CORRECTION_MAX_TICKS: int = 8 ## Horizontal nudge per tick for rim / straddle settle (**`_maybe_idle_rim_settle_nudge`**). const IDLE_RIM_SETTLE_STEP: float = 0.004 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. +## Cap floor snap for the whole descend (goal below feet) so a long snap does not glue the +## capsule to the upper surface across internal edges. No secondary feet_y band — that caused +## snap-length flip-flop and oscillation at the lip. 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). const WALK_STEP_ASSIST_DELTA: float = 0.11 ## Skip assist when horizontal velocity already aligns enough with want-dir (corners slide slowly). @@ -252,52 +249,38 @@ func _set_horizontal_velocity_toward( dh = Vector3(1.0, 0.0, 0.0) else: dh = dh.normalized() - velocity = dh * MOVE_SPEED - velocity.y = 0.0 + # Only steer XZ. Setting `velocity.y = 0` every tick prevented gravity from accumulating + # while descending (lip reported as on-floor) and forced artificial peel multipliers that + # fought floor snap and `floor_block_on_wall`. + velocity.x = dh.x * MOVE_SPEED + velocity.z = dh.z * MOVE_SPEED -## While walking, integrate gravity when airborne so Jolt can register floor contacts and -## `move_and_slide` is not stuck with vy=0 above the surface (expanded district / terraces). -func _apply_walk_air_gravity(delta: float) -> void: +func _walk_descending_by_goal(feet_y: float) -> bool: + return _has_walk_goal and _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN + + +## Gravity while walking: normal fall when airborne, and when the nav goal is below the feet +## (descend) even if `is_on_floor()` at a terrace lip. Stomp Y when grounded on a non-descend +## move so flat walking does not inherit stale vertical velocity. +func _apply_walk_gravity(delta: float, feet_y: float) -> void: var g_step: Vector3 = get_gravity() * delta - if not is_on_floor(): + if (not is_on_floor()) or _walk_descending_by_goal(feet_y): 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 _stomp_walk_vertical_when_grounded_not_descending(feet_y: float) -> void: + if is_on_floor() and not _walk_descending_by_goal(feet_y): + velocity.y = 0.0 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 - ): + if _walk_descending_by_goal(feet_y): fl = minf(fl, DESCEND_LIP_SNAP_CAP) return fl -func _update_floor_block_on_wall_for_terraces(feet_y: float) -> void: - floor_block_on_wall = true - if not _has_walk_goal: - return - # Only relax for descending (step/platform → lower surface). One stable policy avoids - # frame-to-frame floor_block toggling at terrace lips. - var goal_below: bool = _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN - if goal_below: - floor_block_on_wall = false - - ## When the authoritative goal shares XZ with the capsule (e.g. terrace above), horizontal ## `full_to_goal` is zero and steering must follow the baked path toward a ramp/step, not +X. func _set_horizontal_velocity_from_nav_path_or_goal(fallback_goal_xz: Vector3) -> void: @@ -651,13 +634,13 @@ func _physics_process(delta: float) -> void: var feet_y: float = capsule_feet_y( global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS ) - _update_floor_block_on_wall_for_terraces(feet_y) + floor_block_on_wall = true 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) - _apply_descend_on_floor_gravity(delta, feet_y) + _apply_walk_gravity(delta, feet_y) + _stomp_walk_vertical_when_grounded_not_descending(feet_y) floor_snap_length = _walk_floor_snap_length(feet_y) move_and_slide() _after_walk_move_and_slide() @@ -678,10 +661,10 @@ func _physics_process(delta: float) -> void: # physical feet. Using CAPSULE_HALF_HEIGHT alone (0.5) was ~0.4 m too high: from the first # step (body 1.2 → code-feet 0.7) it incorrectly fired for the platform goal (Y=0.6 < 0.64), # using FLOOR_SNAP_MOVING to pull the capsule back to the step after every assist lift. - if _auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN: + if _walk_descending_by_goal(feet_y): _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) - _apply_walk_air_gravity(delta) - _apply_descend_on_floor_gravity(delta, feet_y) + _apply_walk_gravity(delta, feet_y) + _stomp_walk_vertical_when_grounded_not_descending(feet_y) floor_snap_length = _walk_floor_snap_length(feet_y) move_and_slide() _after_walk_move_and_slide() @@ -715,8 +698,8 @@ func _physics_process(delta: float) -> void: else: _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) - _apply_walk_air_gravity(delta) - _apply_descend_on_floor_gravity(delta, feet_y) + _apply_walk_gravity(delta, feet_y) + _stomp_walk_vertical_when_grounded_not_descending(feet_y) floor_snap_length = _walk_floor_snap_length(feet_y) move_and_slide() _after_walk_move_and_slide() diff --git a/docs/plans/NEON-29-implementation-plan.md b/docs/plans/NEON-29-implementation-plan.md index 9daaf9e..a79db16 100644 --- a/docs/plans/NEON-29-implementation-plan.md +++ b/docs/plans/NEON-29-implementation-plan.md @@ -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. - **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. -- **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). +- **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** fixes iterated again: `_set_horizontal_velocity_toward` had been resetting **`velocity.y = 0`** every tick, so gravity never accumulated during walk (lip + on-floor made this look like “slow gravity”). **On-floor gravity multipliers** and **`floor_block_on_wall = false`** for descend then **fought** floor snap and brought back **oscillation**. **Current approach:** steer **XZ only**; apply **`get_gravity() * delta`** when **`!is_on_floor()`** *or* the nav goal is below the feet; **stomp `velocity.y`** only when **on floor** and **not** descending; keep **`floor_block_on_wall` true**; cap **`floor_snap_length`** for the **whole** descend (no extra feet_y band). ## Open questions / risks