neon-sprawl/client/scripts/player.gd

153 lines
5.1 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 waypoints + stored server goal for arrival.
## NS-19: any underfoot ray into `ns19_bump` → skip nav (waypoints); bumps are small vs obstacles.
const MOVE_SPEED: float = 5.0
const ARRIVE_EPS: float = 0.35
## Must be **below** NS-19 bump rise (~0.120.15) or we "arrive" while still on the plateau when the
## goal is on the floor beside the bump (small horiz offset + ~0.15 m vertical).
const VERT_ARRIVE_EPS: float = 0.055
const MAX_CLIMB_SPEED: float = 2.6
const MAX_DESCENT_SPEED: float = 2.2
const DIRECT_APPROACH_RADIUS: float = 0.85
const NS19_RAY_DOWN: float = 3.0
## Origins (world-space offset from body) before casting down. Wider xz helps the 1 m Bump A rim
## where a single vertical line can clear the frustum; Bump B stays covered too.
const NS19_UNDERFOOT_OFFSETS: Array[Vector3] = [
Vector3(0, 0, 0),
Vector3(0, -0.45, 0),
Vector3(0, -0.82, 0),
Vector3(0.42, -0.28, 0),
Vector3(-0.42, -0.28, 0),
Vector3(0, -0.28, 0.42),
Vector3(0, -0.28, -0.42),
]
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 _should_skip_nav_for_ns19_bump() -> bool:
for off: Vector3 in NS19_UNDERFOOT_OFFSETS:
if _ns19_bump_under_ray(global_position + off):
return true
return false
func _ns19_bump_under_ray(from: Vector3) -> bool:
var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var to: Vector3 = from + Vector3(0.0, -NS19_RAY_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)
if hit.is_empty():
return false
var col: Variant = hit.get("collider")
var n: Node = col as Node
while n:
if n.is_in_group("ns19_bump"):
return true
n = n.get_parent()
return false
func _steer_toward_world_point(point: Vector3) -> void:
var pos := global_position
var delta_h: Vector3 = Vector3(point.x - pos.x, 0.0, point.z - pos.z)
# Full 3D toward a waypoint that is mostly *below* (same xz, on the slope) while standing on
# the plateau yields almost no horizontal speed, so you never reach the rim. Nav in xz + floor
# slide handles ramps and departure from bump tops.
if delta_h.length_squared() > 1e-8:
velocity = delta_h.normalized() * MOVE_SPEED
return
var dy: float = point.y - pos.y
if dy > 0.08:
var delta: Vector3 = point - pos
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
elif dy < 0.0:
# Same xz, goal slightly lower (e.g. -0.03): old threshold left velocity at zero (“hang”).
var escape_h := Vector3(-global_transform.basis.z.x, 0.0, -global_transform.basis.z.z)
if escape_h.length_squared() < 1e-8:
escape_h = Vector3(1.0, 0.0, 0.0)
velocity = escape_h.normalized() * MOVE_SPEED
else:
velocity = Vector3.ZERO
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
# Waypoints route around obstacles; steering stays horizontal-only (see _steer_toward_world_point)
# so bump plateaus do not get a mostly-vertical first segment. If next waypoint aligns under the
# player (small xz delta), fall back to steering toward the goal xz.
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()
return
# On bump: skip nav waypoints (drop-first paths); bump area is small vs the obstacle.
if _should_skip_nav_for_ns19_bump():
_steer_toward_world_point(_auth_walk_goal)
move_and_slide()
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()