NEON-29: Require forward floor probe to keep walk snap/gravity sane off ledges

Walk support used any ray hit under the foot disk, so the center sample
could still see the platform while the leading edge was over a void.
is_on_floor() stayed true from the lip, snap stayed strong, and vy was
zeroed each tick — horizontal skate until XZ arrival.

Now require a center hit plus at least one forward-biased sample in the
movement direction when horizontal intent is non-zero; use velocity XZ
for probe direction so nav/path steering matches the rays.
pull/41/head
VinPropane 2026-04-12 18:08:01 -04:00
parent 1f6625c659
commit 6bd1acd9e7
1 changed files with 21 additions and 6 deletions

View File

@ -365,16 +365,30 @@ func _walk_ray_hit_up_floor(space: PhysicsDirectSpaceState3D, off_xz: Vector2) -
return n.dot(Vector3.UP) > WALK_SUPPORT_PROBE_MIN_UP_DOT return n.dot(Vector3.UP) > WALK_SUPPORT_PROBE_MIN_UP_DOT
## True if **any** sample under the foot disk hits walk-like ground within [member WALK_SUPPORT_PROBE_DEPTH]. ## Walkable support for **continued** forward motion within [member WALK_SUPPORT_PROBE_DEPTH].
## Uses [param move_dir_xz] so the **leading** edge off a deck is checked (center-only rays stayed over mesh). ## Requires a hit under the foot origin **and** at least one sample **ahead** in [param move_dir_xz]
## (same construction as [method _walk_probe_xz_offsets]). If only the center hits while the leading
## foot is over a void, [method CharacterBody3D.is_on_floor] can stay true from a lip — returning
## true here would suppress gravity and keep moving snap until XZ arrival (ledge skate).
## When [param move_dir_xz] is ~zero (idle / no horizontal intent), center-only is enough.
func _walk_has_close_floor_probe_below(move_dir_xz: Vector2) -> bool: func _walk_has_close_floor_probe_below(move_dir_xz: Vector2) -> bool:
var w3d := get_world_3d() var w3d := get_world_3d()
if w3d == null: if w3d == null:
return true return true
var space: PhysicsDirectSpaceState3D = w3d.direct_space_state var space: PhysicsDirectSpaceState3D = w3d.direct_space_state
if not _walk_ray_hit_up_floor(space, Vector2.ZERO):
return false
var len2: float = move_dir_xz.length_squared()
if len2 < 1e-10:
return true
var inv_len: float = 1.0 / sqrt(len2)
var md := Vector2(move_dir_xz.x * inv_len, move_dir_xz.y * inv_len)
for off2: Vector2 in _walk_probe_xz_offsets(move_dir_xz): for off2: Vector2 in _walk_probe_xz_offsets(move_dir_xz):
if _walk_ray_hit_up_floor(space, off2): if off2.length_squared() < 1e-12:
return true continue
if off2.dot(md) > 0.0:
if _walk_ray_hit_up_floor(space, off2):
return true
return false return false
@ -922,9 +936,10 @@ func _physics_process(delta: float) -> void:
else: else:
_set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h) _set_horizontal_velocity_toward(_auth_walk_goal, want_goal_h)
var walk_move_dir_xz := Vector2(want_goal_h.x, want_goal_h.z) # Prefer actual horizontal velocity so probes match nav/path steering, not only click bearing.
var walk_move_dir_xz := Vector2(velocity.x, velocity.z)
if walk_move_dir_xz.length_squared() < 1e-10: if walk_move_dir_xz.length_squared() < 1e-10:
walk_move_dir_xz = Vector2(velocity.x, velocity.z) walk_move_dir_xz = Vector2(want_goal_h.x, want_goal_h.z)
_apply_walk_air_gravity(delta, walk_move_dir_xz) _apply_walk_air_gravity(delta, walk_move_dir_xz)
floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h, walk_move_dir_xz) floor_snap_length = _walk_floor_snap_length(feet_y, want_goal_h, walk_move_dir_xz)