neon-sprawl/client/scripts/player.gd

159 lines
4.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

extends CharacterBody3D
## NS-23: Nav agent suggests waypoints; arrival uses stored server goal only (not agent “finished”).
## NS-19 bumps: **autostep** via `test_move` — up+forward to mount, down+forward to leave (pure down
## hits the bump volume youre standing on).
const MOVE_SPEED: float = 5.0
const ARRIVE_EPS: float = 0.35
const VERT_ARRIVE_EPS: float = 0.16
const MAX_CLIMB_SPEED: float = 2.6
const MAX_DESCENT_SPEED: float = 2.2
const DIRECT_APPROACH_RADIUS: float = 0.85
const STEP_MAX_HEIGHT: float = 0.32
const STEP_PROBE_FWD: float = 0.38
const STUCK_HORIZ_SPEED: float = 0.28
## Drop tries when leaving a bump (combined with forward over the lip).
const LEDGE_DROP_STEPS = [0.08, 0.12, 0.18, 0.24, 0.32]
var _has_walk_goal: bool = false
var _auth_walk_goal: Vector3 = Vector3.ZERO
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
_nav_agent.set_target_position(global_position)
func set_authoritative_nav_goal(world_pos: Vector3) -> void:
_auth_walk_goal = world_pos
_has_walk_goal = true
_nav_agent.set_target_position(world_pos)
func clear_nav_goal() -> void:
velocity = Vector3.ZERO
_has_walk_goal = false
_nav_agent.set_target_position(global_position)
func snap_to_server(world_pos: Vector3) -> void:
global_position = world_pos
velocity = Vector3.ZERO
_has_walk_goal = false
_nav_agent.set_target_position(world_pos)
func _steer_toward_world_point(point: Vector3) -> void:
var delta: Vector3 = point - global_position
if delta.length_squared() < 1e-8:
velocity = Vector3.ZERO
return
var desired: Vector3 = delta.normalized() * MOVE_SPEED
desired.y = clampf(desired.y, -MAX_DESCENT_SPEED, MAX_CLIMB_SPEED)
velocity = desired
## Godot `test_move`: returns **true** if the motion would hit something (blocked).
func _try_autostep(delta: float, full_to_goal: Vector3) -> bool:
var to_goal_h: Vector3 = Vector3(
full_to_goal.x, 0.0, full_to_goal.z
)
if to_goal_h.length_squared() < 0.0225:
return false
var dir_h: Vector3 = to_goal_h.normalized()
var fwd_len: float = maxf(MOVE_SPEED * delta, 0.1)
var motion_fwd: Vector3 = dir_h * minf(fwd_len, STEP_PROBE_FWD)
var xf: Transform3D = global_transform
if not test_move(xf, motion_fwd):
return false
if full_to_goal.y > 0.02:
return _autostep_up(xf, motion_fwd, dir_h)
if full_to_goal.y < -0.02:
return _autostep_down(xf, motion_fwd, dir_h)
return false
func _autostep_up(xf: Transform3D, motion_fwd: Vector3, dir_h: Vector3) -> bool:
var motion_up: Vector3 = Vector3(0.0, STEP_MAX_HEIGHT, 0.0)
if test_move(xf, motion_up):
return false
var xf_up := Transform3D(xf.basis, xf.origin + motion_up)
if test_move(xf_up, motion_fwd):
return false
global_position += motion_up + motion_fwd
velocity.x = dir_h.x * MOVE_SPEED
velocity.z = dir_h.z * MOVE_SPEED
velocity.y = 0.0
return true
func _autostep_down(xf: Transform3D, motion_fwd: Vector3, dir_h: Vector3) -> bool:
for drop: float in LEDGE_DROP_STEPS:
var motion_off: Vector3 = motion_fwd + Vector3(0.0, -drop, 0.0)
if test_move(xf, motion_off):
continue
global_position += motion_off
velocity.x = dir_h.x * MOVE_SPEED
velocity.z = dir_h.z * MOVE_SPEED
velocity.y = -minf(MAX_DESCENT_SPEED, 1.2)
return true
return false
func _physics_process(delta: float) -> void:
if not _has_walk_goal:
velocity = Vector3.ZERO
move_and_slide()
return
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: float = absf(full_to_goal.y)
if horiz_dist <= ARRIVE_EPS and vert_err <= VERT_ARRIVE_EPS:
velocity = Vector3.ZERO
_has_walk_goal = false
_nav_agent.set_target_position(global_position)
move_and_slide()
return
var nav_map: RID = _nav_agent.get_navigation_map()
if NavigationServer3D.map_get_iteration_id(nav_map) == 0:
_steer_toward_world_point(_auth_walk_goal)
move_and_slide()
_maybe_autostep_after_slide(delta, full_to_goal, horiz_dist)
return
if horiz_dist <= DIRECT_APPROACH_RADIUS:
_steer_toward_world_point(_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() > 0.0025:
_steer_toward_world_point(next_path_position)
else:
_steer_toward_world_point(_auth_walk_goal)
move_and_slide()
_maybe_autostep_after_slide(delta, full_to_goal, horiz_dist)
func _maybe_autostep_after_slide(
delta: float, full_to_goal: Vector3, horiz_dist: float
) -> void:
if not _has_walk_goal:
return
if not is_on_floor() and not is_on_wall():
return
var hspd: float = Vector2(velocity.x, velocity.z).length()
var want_step: bool = hspd < STUCK_HORIZ_SPEED
if not want_step and is_on_wall():
want_step = horiz_dist > 0.12 and (
full_to_goal.y > 0.03 or full_to_goal.y < -0.03
)
if want_step and _try_autostep(delta, full_to_goal):
move_and_slide()