NEON-29: Fix terrace B descend (geometry gap + simpler floor_block)
Raise TerraceStepB and TerracePlatformB 15mm so boxes are not coplanar with the main floor top, reducing Jolt internal-edge hangs. Remove descend lip stall, peel multipliers, and snap cap; keep a single floor_block_on_wall=false policy for the whole descend branch to avoid oscillation. Document in NEON-29 plan.pull/41/head
parent
275645c6cf
commit
f20957975d
|
|
@ -255,7 +255,7 @@ shape = SubResource("BoxShape3D_terrace_a")
|
|||
|
||||
[node name="TerraceStepB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000013 groups=["walkable"]]
|
||||
collision_mask = 3
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 8.5)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0.015, 8.5)
|
||||
|
||||
[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)
|
||||
|
|
@ -268,7 +268,7 @@ shape = SubResource("BoxShape3D_terrace_b_step")
|
|||
|
||||
[node name="TerracePlatformB" type="StaticBody3D" parent="World/NavigationRegion3D" unique_id=3000016 groups=["walkable"]]
|
||||
collision_mask = 3
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0, 13)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 0.015, 13)
|
||||
|
||||
[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)
|
||||
|
|
|
|||
|
|
@ -28,14 +28,6 @@ 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 from a step so Jolt cannot snap the capsule back onto the
|
||||
## upper surface across an internal edge (lip hang). Relaxed once feet are near the goal height.
|
||||
const DESCEND_LIP_SNAP_CAP: float = 0.12
|
||||
## Strong downward multiplier at a stalled lip (stall tick, wallish contact, or carry from prior).
|
||||
const DESCEND_LIP_GRAVITY_MULT: float = 7.0
|
||||
## Weaker always-on peel while on_floor and descending so internal edges unstick without waiting
|
||||
## for stall detection; kept < 1.0 so flat spans on a platform still allow horizontal motion.
|
||||
const DESCEND_WEAK_PEEL_MULT: float = 0.72
|
||||
## 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).
|
||||
|
|
@ -77,8 +69,6 @@ var _debug_last_idle_xz: Vector2 = Vector2.INF
|
|||
var _debug_idle_heartbeat: int = 0
|
||||
var _debug_last_transform_xz: Vector2 = Vector2.INF
|
||||
var _debug_trace_frame: int = 0
|
||||
## Frames of low horizontal progress while descending; drives lip peel + floor_block relax.
|
||||
var _descend_lip_stall_ticks: int = 0
|
||||
|
||||
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||
|
||||
|
|
@ -99,7 +89,6 @@ func set_authoritative_nav_goal(world_pos: Vector3) -> void:
|
|||
_step_assist_active = false
|
||||
_idle_anchor_active = false
|
||||
_idle_manual_correction_ticks = 0
|
||||
_descend_lip_stall_ticks = 0
|
||||
_nav_agent.set_target_position(world_pos)
|
||||
|
||||
|
||||
|
|
@ -110,7 +99,6 @@ func clear_nav_goal() -> void:
|
|||
_step_assist_active = false
|
||||
_idle_anchor_active = false
|
||||
_idle_manual_correction_ticks = 0
|
||||
_descend_lip_stall_ticks = 0
|
||||
_nav_agent.set_target_position(global_position)
|
||||
|
||||
|
||||
|
|
@ -147,7 +135,6 @@ func snap_to_server(world_pos: Vector3) -> void:
|
|||
_step_assist_active = false
|
||||
_idle_anchor_active = false
|
||||
_idle_manual_correction_ticks = 0
|
||||
_descend_lip_stall_ticks = 0
|
||||
_nav_agent.set_target_position(settled)
|
||||
reset_physics_interpolation()
|
||||
|
||||
|
|
@ -244,7 +231,9 @@ func _after_walk_move_and_slide() -> void:
|
|||
move_and_slide()
|
||||
|
||||
|
||||
func _set_horizontal_velocity_toward(point: Vector3, fallback_dir_xz: Vector3 = Vector3.ZERO) -> void:
|
||||
func _set_horizontal_velocity_toward(
|
||||
point: Vector3, fallback_dir_xz: Vector3 = Vector3.ZERO
|
||||
) -> void:
|
||||
var pos := global_position
|
||||
var dh: Vector3 = Vector3(point.x - pos.x, 0.0, point.z - pos.z)
|
||||
if dh.length_squared() < 1e-10:
|
||||
|
|
@ -268,79 +257,17 @@ func _apply_walk_air_gravity(delta: float) -> void:
|
|||
velocity += g_step
|
||||
|
||||
|
||||
func _walk_has_wallish_slide_contact() -> bool:
|
||||
if is_on_wall():
|
||||
return true
|
||||
for i: int in get_slide_collision_count():
|
||||
if get_slide_collision(i).get_normal().y < 0.52:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
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). Clearing floor_block for
|
||||
# goal_above + wallish broke floor→TerraceStepB: step assist + move_and_slide need the
|
||||
# default blocked wall/floor interaction to climb reliably on Jolt.
|
||||
# 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
|
||||
# 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
|
||||
if not goal_below:
|
||||
return
|
||||
# While clearly above the lower target, always relax — waiting for wallish/stall left step→floor
|
||||
# and platform→floor stuck with no recovery. Tighten again near landing so floor contact stays sane.
|
||||
if feet_y > _auth_walk_goal.y + 0.07:
|
||||
if goal_below:
|
||||
floor_block_on_wall = false
|
||||
return
|
||||
if _walk_has_wallish_slide_contact() or _descend_lip_stall_ticks >= 1:
|
||||
floor_block_on_wall = false
|
||||
|
||||
|
||||
func _apply_descend_lip_peel(
|
||||
delta: float, feet_y: float, carry_lip_from_prior_move: bool = false
|
||||
) -> void:
|
||||
if _auth_walk_goal.y >= feet_y - DESCEND_GOAL_Y_MARGIN:
|
||||
return
|
||||
if not is_on_floor():
|
||||
return
|
||||
var g := get_gravity() * delta
|
||||
if g.y >= 0.0:
|
||||
return
|
||||
var strong: bool = (
|
||||
_descend_lip_stall_ticks >= 1
|
||||
or _walk_has_wallish_slide_contact()
|
||||
or carry_lip_from_prior_move
|
||||
)
|
||||
var w: float = DESCEND_LIP_GRAVITY_MULT if strong else DESCEND_WEAK_PEEL_MULT
|
||||
velocity.y += g.y * w
|
||||
|
||||
|
||||
func _descend_update_lip_stall_after_move() -> void:
|
||||
if not _has_walk_goal:
|
||||
_descend_lip_stall_ticks = 0
|
||||
return
|
||||
var feet_y: float = capsule_feet_y(global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS)
|
||||
if _auth_walk_goal.y >= feet_y - DESCEND_GOAL_Y_MARGIN:
|
||||
_descend_lip_stall_ticks = 0
|
||||
return
|
||||
var want_h := Vector2(_auth_walk_goal.x - global_position.x, _auth_walk_goal.z - global_position.z)
|
||||
var hdist: float = want_h.length()
|
||||
var vh: float = Vector2(velocity.x, velocity.z).length()
|
||||
if hdist > 0.08 and vh < 0.52:
|
||||
_descend_lip_stall_ticks = mini(_descend_lip_stall_ticks + 1, 72)
|
||||
else:
|
||||
_descend_lip_stall_ticks = 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.08
|
||||
):
|
||||
fl = minf(fl, DESCEND_LIP_SNAP_CAP)
|
||||
return fl
|
||||
|
||||
|
||||
## When the authoritative goal shares XZ with the capsule (e.g. terrace above), horizontal
|
||||
|
|
@ -678,7 +605,6 @@ func _physics_process(delta: float) -> void:
|
|||
if horiz_dist <= ARRIVE_EPS and vert_err <= VERT_ARRIVE_EPS:
|
||||
velocity = Vector3.ZERO
|
||||
_has_walk_goal = false
|
||||
_descend_lip_stall_ticks = 0
|
||||
floor_block_on_wall = true
|
||||
_floor_angle_loose_ticks = FLOOR_ANGLE_LOOSE_TICKS_AFTER_STOP
|
||||
_step_assist_active = false
|
||||
|
|
@ -694,28 +620,24 @@ func _physics_process(delta: float) -> void:
|
|||
|
||||
_idle_manual_correction_ticks = 0
|
||||
|
||||
var feet_y: float = capsule_feet_y(global_position.y, CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS)
|
||||
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)
|
||||
|
||||
var nav_map: RID = _nav_agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(nav_map) == 0:
|
||||
var h_peel: float = Vector2(want_goal_h.x, want_goal_h.z).length()
|
||||
var pre_vh: float = Vector2(velocity.x, velocity.z).length()
|
||||
var lip_carry: bool = (
|
||||
_auth_walk_goal.y < feet_y - DESCEND_GOAL_Y_MARGIN
|
||||
and h_peel > 0.1
|
||||
and pre_vh < 0.55
|
||||
)
|
||||
_set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h)
|
||||
_apply_walk_air_gravity(delta)
|
||||
_apply_descend_lip_peel(delta, feet_y, lip_carry)
|
||||
floor_snap_length = _walk_floor_snap_length(feet_y)
|
||||
floor_snap_length = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING
|
||||
move_and_slide()
|
||||
_after_walk_move_and_slide()
|
||||
_descend_update_lip_stall_after_move()
|
||||
if _step_assist_active and (
|
||||
(is_on_floor() and not is_on_wall())
|
||||
or (not is_on_floor() and get_slide_collision_count() == 0)
|
||||
if (
|
||||
_step_assist_active
|
||||
and (
|
||||
(is_on_floor() and not is_on_wall())
|
||||
or (not is_on_floor() and get_slide_collision_count() == 0)
|
||||
)
|
||||
):
|
||||
_step_assist_active = false
|
||||
_debug_trace_transform("physics")
|
||||
|
|
@ -728,19 +650,17 @@ func _physics_process(delta: float) -> void:
|
|||
# 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:
|
||||
var h_peel2: float = Vector2(want_goal_h.x, want_goal_h.z).length()
|
||||
var pre_vh2: float = Vector2(velocity.x, velocity.z).length()
|
||||
var lip_carry2: bool = h_peel2 > 0.1 and pre_vh2 < 0.55
|
||||
_set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h)
|
||||
_apply_walk_air_gravity(delta)
|
||||
_apply_descend_lip_peel(delta, feet_y, lip_carry2)
|
||||
floor_snap_length = _walk_floor_snap_length(feet_y)
|
||||
floor_snap_length = WALK_STEP_ASSIST_SNAP if _step_assist_active else FLOOR_SNAP_MOVING
|
||||
move_and_slide()
|
||||
_after_walk_move_and_slide()
|
||||
_descend_update_lip_stall_after_move()
|
||||
if _step_assist_active and (
|
||||
(is_on_floor() and not is_on_wall())
|
||||
or (not is_on_floor() and get_slide_collision_count() == 0)
|
||||
if (
|
||||
_step_assist_active
|
||||
and (
|
||||
(is_on_floor() and not is_on_wall())
|
||||
or (not is_on_floor() and get_slide_collision_count() == 0)
|
||||
)
|
||||
):
|
||||
_step_assist_active = false
|
||||
_debug_trace_transform("physics")
|
||||
|
|
|
|||
|
|
@ -109,6 +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 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 / weak–strong 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.
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue