From 58f32e3f51ae3dab6a303e4f91ad91a7df78c4ac Mon Sep 17 00:00:00 2001 From: Zeeshaun Date: Thu, 26 Mar 2026 14:33:17 -0500 Subject: [PATCH] Creepy little follower --- game/scenes/Characters/repo_bot.gd | 194 +++++++++++++++++++++++------ game/scenes/player.gd | 12 ++ 2 files changed, 169 insertions(+), 37 deletions(-) diff --git a/game/scenes/Characters/repo_bot.gd b/game/scenes/Characters/repo_bot.gd index e789b09..0af1a4f 100644 --- a/game/scenes/Characters/repo_bot.gd +++ b/game/scenes/Characters/repo_bot.gd @@ -1,15 +1,24 @@ -extends Node3D - -@export var left_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeLeft/Pupil") -@export var right_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeRight/Pupil") -@export var camera_path: NodePath +extends Node3D + +@export var body_path: NodePath = NodePath("Body") +@export var left_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeLeft/Pupil") +@export var right_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeRight/Pupil") +@export var camera_path: NodePath @export var look_origin_path: NodePath = NodePath("Body/HeadPivot") @export var look_reference_path: NodePath = NodePath("Body") @export var look_target_path: NodePath = NodePath("") -@export var lock_vertical: bool = true -@export var vertical_unlock_height: float = 0.6 -@export var vertical_lock_smooth_speed: float = 6.0 -@export var vertical_lock_hold_time: float = 0.3 +@export var follow_enabled: bool = true +@export var follow_speed: float = 2.8 +@export var follow_acceleration: float = 8.0 +@export var follow_start_distance: float = 3.5 +@export var follow_stop_distance: float = 1.9 +@export var follow_turn_speed: float = 8.0 +@export var observed_stop_dot: float = 0.45 +@export var require_line_of_sight: bool = false +@export var lock_vertical: bool = true +@export var vertical_unlock_height: float = 0.6 +@export var vertical_lock_smooth_speed: float = 6.0 +@export var vertical_lock_hold_time: float = 0.3 @export var max_look_angle_deg: float = 90.0 @export var eye_return_speed: float = 0.2 @export var max_offset: float = 0.08 @@ -19,25 +28,28 @@ extends Node3D @export var head_max_yaw_deg: float = 55.0 @export var head_max_pitch_deg: float = 22.0 -var _left_pupil: Node3D -var _right_pupil: Node3D -var _left_base: Vector3 -var _right_base: Vector3 -var _camera: Camera3D -var _look_origin: Node3D +var _left_pupil: Node3D +var _right_pupil: Node3D +var _body: RigidBody3D +var _left_base: Vector3 +var _right_base: Vector3 +var _camera: Camera3D +var _look_origin: Node3D var _head: Node3D var _head_base_rot: Vector3 var _vertical_lock_factor: float = 1.0 -var _vertical_hold_timer: float = 0.0 +var _vertical_hold_timer: float = 0.0 var _look_reference: Node3D var _look_target: Node3D - - +var _follow_velocity := Vector3.ZERO + + func _ready() -> void: - _left_pupil = get_node_or_null(left_pupil_path) as Node3D - _right_pupil = get_node_or_null(right_pupil_path) as Node3D - if _left_pupil: - _left_base = _left_pupil.position + _body = get_node_or_null(body_path) as RigidBody3D + _left_pupil = get_node_or_null(left_pupil_path) as Node3D + _right_pupil = get_node_or_null(right_pupil_path) as Node3D + if _left_pupil: + _left_base = _left_pupil.position if _right_pupil: _right_base = _right_pupil.position _camera = _resolve_camera() @@ -45,16 +57,25 @@ func _ready() -> void: _look_reference = get_node_or_null(look_reference_path) as Node3D _look_target = get_node_or_null(look_target_path) as Node3D _head = get_node_or_null(head_path) as Node3D - if _head: - _head_base_rot = _head.rotation - - -func _physics_process(_delta: float) -> void: - _update_pupils() - - -func _process(_delta: float) -> void: - _update_pupils() + if _head: + _head_base_rot = _head.rotation + if _body: + _body.top_level = false + _body.freeze = true + _body.sleeping = true + _body.contact_monitor = true + _body.max_contacts_reported = 8 + + +func _physics_process(delta: float) -> void: + _update_follow(delta) + _sync_body_to_root() + _update_pupils() + + +func _process(_delta: float) -> void: + _sync_body_to_root() + _update_pupils() func _update_pupils() -> void: @@ -122,11 +143,110 @@ func _update_pupils() -> void: _update_eye(_left_pupil, _left_base, dir_world) if _right_pupil: _update_eye(_right_pupil, _right_base, dir_world) - if _head: - _update_head(dir_world) - - -func _resolve_camera() -> Camera3D: + if _head: + _update_head(dir_world) + + +func _update_follow(delta: float) -> void: + if _body == null: + return + if not follow_enabled: + _slow_to_stop(delta) + return + if _look_target == null and look_target_path != NodePath(""): + _look_target = get_node_or_null(look_target_path) as Node3D + if _look_target == null: + _slow_to_stop(delta) + return + if _look_target.has_method("is_in_vehicle") and bool(_look_target.call("is_in_vehicle")): + _slow_to_stop(delta) + return + if _is_observed(): + _slow_to_stop(delta) + return + + var to_target := _look_target.global_position - _body.global_position + to_target.y = 0.0 + var distance := to_target.length() + if distance <= 0.001: + _slow_to_stop(delta) + return + + var desired_velocity := Vector3.ZERO + if distance > follow_start_distance: + desired_velocity = to_target.normalized() * follow_speed + elif distance <= follow_stop_distance: + desired_velocity = Vector3.ZERO + else: + var blend := inverse_lerp(follow_stop_distance, follow_start_distance, distance) + desired_velocity = to_target.normalized() * (follow_speed * blend) + + _follow_velocity = _follow_velocity.move_toward(desired_velocity, follow_acceleration * delta) + global_position += _follow_velocity * delta + + if _follow_velocity.length_squared() > 0.0001: + var facing_dir := _follow_velocity.normalized() + var target_yaw := atan2(-facing_dir.x, -facing_dir.z) + rotation.y = lerp_angle(rotation.y, target_yaw, follow_turn_speed * delta) + + +func _slow_to_stop(delta: float) -> void: + _follow_velocity = _follow_velocity.move_toward(Vector3.ZERO, follow_acceleration * delta) + global_position += _follow_velocity * delta + + +func _sync_body_to_root() -> void: + if _body == null: + return + _body.position = Vector3.ZERO + _body.rotation = Vector3.ZERO + + +func _is_observed() -> bool: + var observer_origin := Vector3.ZERO + var observer_forward := Vector3.ZERO + if _look_target != null and _look_target.has_method("get_look_origin") and _look_target.has_method("get_look_direction"): + observer_origin = _look_target.call("get_look_origin") + observer_forward = _look_target.call("get_look_direction") + else: + var active_camera := _resolve_active_camera() + if active_camera == null: + return false + observer_origin = active_camera.global_position + observer_forward = -active_camera.global_basis.z + + if observer_forward.length_squared() <= 0.0001: + return false + observer_forward = observer_forward.normalized() + var to_bot := _body.global_position - observer_origin + if to_bot.length_squared() <= 0.0001: + return true + var dir_to_bot := to_bot.normalized() + if observer_forward.dot(dir_to_bot) < observed_stop_dot: + return false + if not require_line_of_sight: + return true + var query := PhysicsRayQueryParameters3D.create(observer_origin, _body.global_position) + query.exclude = [_body.get_rid()] + if _look_target is CollisionObject3D: + query.exclude.append((_look_target as CollisionObject3D).get_rid()) + var hit := get_world_3d().direct_space_state.intersect_ray(query) + return hit.is_empty() + + +func _resolve_active_camera() -> Camera3D: + if _camera != null and _camera.is_inside_tree() and _camera.current: + return _camera + var viewport_cam := get_viewport().get_camera_3d() + if viewport_cam != null: + _camera = viewport_cam + return viewport_cam + if _camera == null or not _camera.is_inside_tree(): + _camera = _resolve_camera() + return _camera + + +func _resolve_camera() -> Camera3D: if camera_path != NodePath(""): var from_path := get_node_or_null(camera_path) as Camera3D if from_path: diff --git a/game/scenes/player.gd b/game/scenes/player.gd index db880cf..beac417 100644 --- a/game/scenes/player.gd +++ b/game/scenes/player.gd @@ -255,3 +255,15 @@ func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void: if cam: cam.current = true vehicle_exited.emit(null) + + +func is_in_vehicle() -> bool: + return _in_vehicle + + +func get_look_origin() -> Vector3: + return global_position + Vector3.UP * 1.2 + + +func get_look_direction() -> Vector3: + return (-global_basis.z).normalized()