From 003750c7ae1e47afb2151b0577a9f28cbb67a6ae Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 5 Apr 2026 01:52:04 -0400 Subject: [PATCH] NS-23: Direct step-down while on ns19_bump up to auth horiz limit Nav paths often wrap along bump rims when the goal is far; the 8 m cap then disabled step-off and left rim-following. Detect NS-19 bumps via group on footprint rays; when any hit is a bump and vertical drop is in the NS-19 band, steer straight to the goal up to 18 m (server max step). Non-bump podiums keep the 8 m close cap. Single footprint pass returns max_y + bump. --- client/scripts/player.gd | 77 +++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 7a016f8..fda87b0 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -3,7 +3,7 @@ extends CharacterBody3D ## NS-23: Follow server move target on walkable geometry. ## ## Horizontal velocity toward nav waypoint or goal; `velocity.y` stays 0 except a short **step-off** -## nudge (see `_nav_short_step_down_use_goal_direct`) so NS-19 bumps can drop without full gravity. +## nudge (see `_use_direct_step_down_steering`) so NS-19 bumps can drop without full gravity. ## Move **legality** is server MoveCommand only — not reimplemented here. ## ## Nav for detours; short local step-down (feet vs goal Y, not body origin) may bypass waypoints. @@ -17,7 +17,10 @@ const STEP_OFF_FLOOR_MARGIN: float = 0.04 ## Only NS-19-sized step downs (not cliffs); allows a wider horiz cap without bee-lining big drops. const STEP_OFF_MIN_DROP: float = 0.055 const STEP_OFF_MAX_DROP: float = 0.24 -const STEP_OFF_MAX_HORIZ: float = 8.0 +## Past this, only direct step-down when footprint still touches `ns19_bump` (nav rim paths). +const STEP_OFF_CLOSE_HORIZ: float = 8.0 +## Match server `MovementValidation.MaxHorizontalStep` default; no bee-line past this. +const STEP_OFF_MAX_HORIZ_AUTH: float = 18.0 const FLOOR_PROBE_BODY_Y_OFFSET: float = -0.55 ## Extra xz rays for step-off; max Y keeps bump support when center ray sees floor first. const STEP_FOOTPRINT_HALF: float = 0.2 @@ -55,13 +58,17 @@ func snap_to_server(world_pos: Vector3) -> void: _nav_agent.set_target_position(world_pos) -func _ray_floor_y_at(from: Vector3) -> float: +func _ray_floor_hit(from: Vector3) -> Dictionary: var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state var to: Vector3 = from + Vector3(0.0, -FLOOR_PROBE_DOWN, 0.0) var q: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, to) q.collision_mask = 1 q.exclude = [get_rid()] - var hit: Dictionary = space.intersect_ray(q) + return space.intersect_ray(q) + + +func _ray_floor_y_at(from: Vector3) -> float: + var hit: Dictionary = _ray_floor_hit(from) if hit.is_empty(): return from.y var pv: Variant = hit.get("position") @@ -70,6 +77,15 @@ func _ray_floor_y_at(from: Vector3) -> float: return from.y +func _collider_in_ns19_bump(collider: Variant) -> bool: + var n: Node = collider as Node + while n: + if n.is_in_group("ns19_bump"): + return true + n = n.get_parent() + return false + + func _floor_y_under_body() -> float: var y_center: float = _ray_floor_y_at(global_position) var lower: Vector3 = global_position + Vector3(0.0, FLOOR_PROBE_BODY_Y_OFFSET, 0.0) @@ -77,9 +93,8 @@ func _floor_y_under_body() -> float: return maxf(y_center, y_lower) -## Max floor Y under capsule + xz offsets (step-off). Fights lip rays that hit ground too early. -func _floor_y_max_under_capsule_footprint() -> float: - var best: float = _floor_y_under_body() +## One ray pass: max floor Y under footprint + whether any hit is an NS-19 bump (`ns19_bump` group). +func _footprint_step_down_state() -> Dictionary: var p: Vector3 = global_position var h: float = STEP_FOOTPRINT_HALF var oxz: Array[Vector3] = [ @@ -92,11 +107,37 @@ func _floor_y_max_under_capsule_footprint() -> float: Vector3(-h, 0.0, h), Vector3(-h, 0.0, -h), ] + var best_y: float = 0.0 + var got: bool = false + var on_bump: bool = false + for yoff: float in [0.0, FLOOR_PROBE_BODY_Y_OFFSET]: + var from_c: Vector3 = p + Vector3(0.0, yoff, 0.0) + var hit_c: Dictionary = _ray_floor_hit(from_c) + if not hit_c.is_empty(): + var yc: float = (hit_c.get("position") as Vector3).y + if not got: + best_y = yc + got = true + else: + best_y = maxf(best_y, yc) + if not on_bump and _collider_in_ns19_bump(hit_c.get("collider")): + on_bump = true for d: Vector3 in oxz: - best = maxf(best, _ray_floor_y_at(p + d)) - var lower_from: Vector3 = p + d + Vector3(0.0, FLOOR_PROBE_BODY_Y_OFFSET, 0.0) - best = maxf(best, _ray_floor_y_at(lower_from)) - return best + for yoff2: float in [0.0, FLOOR_PROBE_BODY_Y_OFFSET]: + var from_o: Vector3 = p + d + Vector3(0.0, yoff2, 0.0) + var hit_o: Dictionary = _ray_floor_hit(from_o) + if not hit_o.is_empty(): + var yo: float = (hit_o.get("position") as Vector3).y + if not got: + best_y = yo + got = true + else: + best_y = maxf(best_y, yo) + if not on_bump and _collider_in_ns19_bump(hit_o.get("collider")): + on_bump = true + if not got: + best_y = _floor_y_under_body() + return {"max_y": best_y, "touches_ns19_bump": on_bump} ## Footing under goal xz (surface Y). Server goal.y may be capsule center (e.g. 0.9) or pick surface @@ -111,8 +152,10 @@ func _destination_footing_y() -> float: return y -func _nav_short_step_down_use_goal_direct() -> bool: - var floor_y: float = _floor_y_max_under_capsule_footprint() +func _use_direct_step_down_steering() -> bool: + var st: Dictionary = _footprint_step_down_state() + var floor_y: float = float(st["max_y"]) + var on_bump: bool = bool(st["touches_ns19_bump"]) var dest_floor_y: float = _destination_footing_y() if dest_floor_y >= floor_y - STEP_OFF_FLOOR_MARGIN: return false @@ -122,9 +165,11 @@ func _nav_short_step_down_use_goal_direct() -> bool: var hd: float = Vector2( _auth_walk_goal.x - global_position.x, _auth_walk_goal.z - global_position.z ).length() - if hd > STEP_OFF_MAX_HORIZ: + if hd > STEP_OFF_MAX_HORIZ_AUTH: return false - return true + if on_bump: + return true + return hd <= STEP_OFF_CLOSE_HORIZ func _set_horizontal_velocity_toward(point: Vector3) -> void: @@ -161,7 +206,7 @@ func _physics_process(_delta: float) -> void: move_and_slide() return - if _nav_short_step_down_use_goal_direct(): + if _use_direct_step_down_steering(): _set_horizontal_velocity_toward(_auth_walk_goal) velocity.y = -STEP_OFF_DESCEND_SPEED move_and_slide()