neon-sprawl/client/scripts/player.gd

147 lines
5.0 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: convex frustum bumps; `ns19_bump` group — on flat plateau skip nav waypoints (departure).
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
## Floor normal must be this “flat” to count as bump plateau (slopes use nav waypoints).
const NS19_BUMP_TOP_NORMAL_DOT: float = 0.992
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 _on_ns19_bump_plateau() -> bool:
if not is_on_floor():
return false
if get_floor_normal().dot(Vector3.UP) < NS19_BUMP_TOP_NORMAL_DOT:
return false
return _raycast_down_hits_group("ns19_bump", 2.5)
func _raycast_down_hits_group(group_name: String, cast_len: float) -> bool:
var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var from: Vector3 = global_position
var to: Vector3 = from + Vector3(0.0, -cast_len, 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(group_name):
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.04:
# Goal is lower and almost same xz (e.g. floor under the plateau): still need rim escape.
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
# Nav waypoints on the plateau often point down the slope first; goal-only + horizontal steer
# matches the NS-19 departure fix. Slopes stay on waypoints so obstacle routing still applies
# when crossing the district.
if _on_ns19_bump_plateau():
_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()