Creepy little follower
All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 48s
Deploy Promiscuity Character API / deploy (push) Successful in 47s
Deploy Promiscuity Crafting API / deploy (push) Successful in 48s
Deploy Promiscuity Inventory API / deploy (push) Successful in 47s
Deploy Promiscuity Locations API / deploy (push) Successful in 47s
Deploy Promiscuity Mail API / deploy (push) Successful in 47s
k8s smoke test / test (push) Successful in 10s

This commit is contained in:
Zeeshaun 2026-03-26 14:33:17 -05:00
parent a283969e4c
commit 58f32e3f51
2 changed files with 169 additions and 37 deletions

View File

@ -1,15 +1,24 @@
extends Node3D extends Node3D
@export var left_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeLeft/Pupil") @export var body_path: NodePath = NodePath("Body")
@export var right_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeRight/Pupil") @export var left_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeLeft/Pupil")
@export var camera_path: NodePath @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_origin_path: NodePath = NodePath("Body/HeadPivot")
@export var look_reference_path: NodePath = NodePath("Body") @export var look_reference_path: NodePath = NodePath("Body")
@export var look_target_path: NodePath = NodePath("") @export var look_target_path: NodePath = NodePath("")
@export var lock_vertical: bool = true @export var follow_enabled: bool = true
@export var vertical_unlock_height: float = 0.6 @export var follow_speed: float = 2.8
@export var vertical_lock_smooth_speed: float = 6.0 @export var follow_acceleration: float = 8.0
@export var vertical_lock_hold_time: float = 0.3 @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 max_look_angle_deg: float = 90.0
@export var eye_return_speed: float = 0.2 @export var eye_return_speed: float = 0.2
@export var max_offset: float = 0.08 @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_yaw_deg: float = 55.0
@export var head_max_pitch_deg: float = 22.0 @export var head_max_pitch_deg: float = 22.0
var _left_pupil: Node3D var _left_pupil: Node3D
var _right_pupil: Node3D var _right_pupil: Node3D
var _left_base: Vector3 var _body: RigidBody3D
var _right_base: Vector3 var _left_base: Vector3
var _camera: Camera3D var _right_base: Vector3
var _look_origin: Node3D var _camera: Camera3D
var _look_origin: Node3D
var _head: Node3D var _head: Node3D
var _head_base_rot: Vector3 var _head_base_rot: Vector3
var _vertical_lock_factor: float = 1.0 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_reference: Node3D
var _look_target: Node3D var _look_target: Node3D
var _follow_velocity := Vector3.ZERO
func _ready() -> void: func _ready() -> void:
_left_pupil = get_node_or_null(left_pupil_path) as Node3D _body = get_node_or_null(body_path) as RigidBody3D
_right_pupil = get_node_or_null(right_pupil_path) as Node3D _left_pupil = get_node_or_null(left_pupil_path) as Node3D
if _left_pupil: _right_pupil = get_node_or_null(right_pupil_path) as Node3D
_left_base = _left_pupil.position if _left_pupil:
_left_base = _left_pupil.position
if _right_pupil: if _right_pupil:
_right_base = _right_pupil.position _right_base = _right_pupil.position
_camera = _resolve_camera() _camera = _resolve_camera()
@ -45,16 +57,25 @@ func _ready() -> void:
_look_reference = get_node_or_null(look_reference_path) as Node3D _look_reference = get_node_or_null(look_reference_path) as Node3D
_look_target = get_node_or_null(look_target_path) as Node3D _look_target = get_node_or_null(look_target_path) as Node3D
_head = get_node_or_null(head_path) as Node3D _head = get_node_or_null(head_path) as Node3D
if _head: if _head:
_head_base_rot = _head.rotation _head_base_rot = _head.rotation
if _body:
_body.top_level = false
func _physics_process(_delta: float) -> void: _body.freeze = true
_update_pupils() _body.sleeping = true
_body.contact_monitor = true
_body.max_contacts_reported = 8
func _process(_delta: float) -> void:
_update_pupils()
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: func _update_pupils() -> void:
@ -122,11 +143,110 @@ func _update_pupils() -> void:
_update_eye(_left_pupil, _left_base, dir_world) _update_eye(_left_pupil, _left_base, dir_world)
if _right_pupil: if _right_pupil:
_update_eye(_right_pupil, _right_base, dir_world) _update_eye(_right_pupil, _right_base, dir_world)
if _head: if _head:
_update_head(dir_world) _update_head(dir_world)
func _resolve_camera() -> Camera3D: 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(""): if camera_path != NodePath(""):
var from_path := get_node_or_null(camera_path) as Camera3D var from_path := get_node_or_null(camera_path) as Camera3D
if from_path: if from_path:

View File

@ -255,3 +255,15 @@ func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void:
if cam: if cam:
cam.current = true cam.current = true
vehicle_exited.emit(null) 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()