diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 7a25b54..7c592e5 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -201,7 +201,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0.9, -5) collision_layer = 2 collision_mask = 1 floor_max_angle = 0.872665 -floor_snap_length = 0.28 +floor_snap_length = 0.38 safe_margin = 0.045 script = ExtResource("2_player") diff --git a/client/scripts/player.gd b/client/scripts/player.gd index 6a1d601..b3cc6c6 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -2,39 +2,18 @@ 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 `_use_direct_step_down_steering`) so stepped 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. +## Plain Godot pattern: horizontal `velocity` toward `NavigationAgent3D`’s next path point (or the +## goal when finishing / final approach). `velocity.y` stays 0; height comes from `move_and_slide` +## and floor handling. No custom step-down / bump heuristics — obstacle pathing is best-effort; +## small steps rely on the engine + scene (snap, collider shapes). Move **legality** is server-only. const MOVE_SPEED: float = 5.0 const ARRIVE_EPS: float = 0.35 -const VERT_ARRIVE_EPS: float = 0.055 +## Stop when close in xz only; server `y` may be surface or capsule center. const DIRECT_APPROACH_RADIUS: float = 0.85 -const FLOOR_PROBE_DOWN: float = 4.0 -const STEP_OFF_FLOOR_MARGIN: float = 0.04 -## Only small 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 -## Past this, only direct step-down when footprint still touches a bump-step collider (nav rim). -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 -## Downward vy only while step-off bypass is active (not global gravity). -const STEP_OFF_DESCEND_SPEED: float = 1.75 -## If next waypoint is within this xz of us, steer to goal (rim / underfoot waypoints). -const NEXT_WAYPOINT_MIN_HORIZ_SQ: float = 0.01 -## Editor group on stepped bump colliders; value must match `main.tscn`. -const WALK_BUMP_STEP_GROUP: StringName = &"ns19_bump" var _has_walk_goal: bool = false var _auth_walk_goal: Vector3 = Vector3.ZERO -var _footprint_cache_frame: int = -1 -var _footprint_cache: Dictionary = {} @onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D @@ -62,128 +41,6 @@ func snap_to_server(world_pos: Vector3) -> void: _nav_agent.set_target_position(world_pos) -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()] - 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") - if pv is Vector3: - return (pv as Vector3).y - return from.y - - -func _collider_in_bump_step_group(collider: Variant) -> bool: - var n: Node = collider as Node - while n: - if n.is_in_group(WALK_BUMP_STEP_GROUP): - 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) - var y_lower: float = _ray_floor_y_at(lower) - return maxf(y_center, y_lower) - - -## One ray pass: max floor Y under footprint + whether any hit uses `WALK_BUMP_STEP_GROUP`. -func _compute_footprint_step_down_state() -> Dictionary: - var p: Vector3 = global_position - var h: float = STEP_FOOTPRINT_HALF - var oxz: Array[Vector3] = [ - Vector3(h, 0.0, 0.0), - Vector3(-h, 0.0, 0.0), - Vector3(0.0, 0.0, h), - Vector3(0.0, 0.0, -h), - Vector3(h, 0.0, h), - Vector3(h, 0.0, -h), - 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_bump_step_group(hit_c.get("collider")): - on_bump = true - for d: Vector3 in oxz: - 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_bump_step_group(hit_o.get("collider")): - on_bump = true - if not got: - best_y = _floor_y_under_body() - return {"max_y": best_y, "touches_bump_step": on_bump} - - -func _footprint_step_down_state() -> Dictionary: - var f: int = Engine.get_physics_frames() - if f != _footprint_cache_frame: - _footprint_cache = _compute_footprint_step_down_state() - _footprint_cache_frame = f - return _footprint_cache - - -## Footing under goal xz (surface Y). Server goal.y may be capsule center (e.g. 0.9) or pick surface -## (e.g. 0) — do not compare that directly to `_floor_y_under_body()` (always surface). -func _destination_footing_y() -> float: - var g: Vector3 = _auth_walk_goal - var probe_top: float = maxf(global_position.y, g.y) + 4.0 - var probe: Vector3 = Vector3(g.x, probe_top, g.z) - var y: float = _ray_floor_y_at(probe) - if y >= probe_top - 0.05: - return g.y - return y - - -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_bump_step"]) - var dest_floor_y: float = _destination_footing_y() - if dest_floor_y >= floor_y - STEP_OFF_FLOOR_MARGIN: - return false - var drop: float = floor_y - dest_floor_y - if drop < STEP_OFF_MIN_DROP or drop > STEP_OFF_MAX_DROP: - return false - 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_AUTH: - return false - if on_bump: - return true - return hd <= STEP_OFF_CLOSE_HORIZ - - func _set_horizontal_velocity_toward(point: Vector3) -> void: var pos := global_position var dh: Vector3 = Vector3(point.x - pos.x, 0.0, point.z - pos.z) @@ -201,14 +58,7 @@ func _physics_process(_delta: float) -> void: var full_to_goal: Vector3 = _auth_walk_goal - global_position var horiz_dist: float = Vector2(full_to_goal.x, full_to_goal.z).length() - var vert_err_capsule: float = absf(full_to_goal.y) - var dest_foot_y: float = _destination_footing_y() - var fp: Dictionary = _footprint_step_down_state() - var max_foot_y: float = float(fp["max_y"]) - # Same lip issue as step-off: `_floor_y_under_body()` can read floor early and match `dest_foot_y` - # while xz is within ARRIVE_EPS — false “arrived”, goal clears. Use footprint max_y. - var footing_match: bool = absf(max_foot_y - dest_foot_y) <= VERT_ARRIVE_EPS - if horiz_dist <= ARRIVE_EPS and (vert_err_capsule <= VERT_ARRIVE_EPS or footing_match): + if horiz_dist <= ARRIVE_EPS: velocity = Vector3.ZERO _has_walk_goal = false _nav_agent.set_target_position(global_position) @@ -221,22 +71,9 @@ func _physics_process(_delta: float) -> void: move_and_slide() return - if _use_direct_step_down_steering(): - _set_horizontal_velocity_toward(_auth_walk_goal) - velocity.y = -STEP_OFF_DESCEND_SPEED - move_and_slide() - return - - if horiz_dist <= DIRECT_APPROACH_RADIUS: + if _nav_agent.is_navigation_finished() or horiz_dist <= DIRECT_APPROACH_RADIUS: _set_horizontal_velocity_toward(_auth_walk_goal) else: - var next_path_position: Vector3 = _nav_agent.get_next_path_position() - var to_next_h: Vector3 = Vector3( - next_path_position.x - global_position.x, 0.0, next_path_position.z - global_position.z - ) - if to_next_h.length_squared() > NEXT_WAYPOINT_MIN_HORIZ_SQ: - _set_horizontal_velocity_toward(next_path_position) - else: - _set_horizontal_velocity_toward(_auth_walk_goal) + _set_horizontal_velocity_toward(_nav_agent.get_next_path_position()) move_and_slide()