class_name OverworldCamera extends Camera3D const COLLISION_MARGIN:float = 0.3 @export_category("Target") @export var targetNode:Node3D = null @export var pivotOffset:Vector3 = Vector3(0, 1.2, 0) @export_category("Orbit") @export var distance:float = 7.0 @export var minDistance:float = 1.5 @export var orbitSensitivity:float = 144.0 @export var orbitAcceleration:float = 600.0 @export var pitchMin:float = -10.0 @export var pitchMax:float = 70.0 @export_category("Mouse") @export var mouseSensitivity:float = 0.3 @export_category("Auto-center") @export var autoReturnDelay:float = 2.0 @export var autoReturnSpeed:float = 90.0 @export var autoReturnPitch:float = 30.0 @export_category("Collision") @export_flags_3d_physics var collisionMask:int = 1 @export var distanceReturnSpeed:float = 5.0 var _yaw:float = 0.0 var _pitch:float = 30.0 var _currentDistance:float = 0.0 var _camVelocity:Vector2 = Vector2.ZERO var _idleTimer:float = 0.0 var _mouseDelta:Vector2 = Vector2.ZERO var _rightMouseHeld:bool = false func _ready() -> void: _currentDistance = distance func _input(event:InputEvent) -> void: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: _rightMouseHeld = event.pressed Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _rightMouseHeld else Input.MOUSE_MODE_VISIBLE elif event is InputEventMouseMotion and _rightMouseHeld: _mouseDelta += event.relative func _process(delta:float) -> void: if targetNode == null: return var xMult:float = -1.0 if SETTINGS.invertCameraX else 1.0 var yMult:float = 1.0 if SETTINGS.invertCameraY else -1.0 var orbitInput:Vector2 = Input.get_vector( "camera_orbit_left", "camera_orbit_right", "camera_orbit_up", "camera_orbit_down" ) # Controller input with speed ramp; instant stop on release var controllerActive:bool = orbitInput.length() > 0.01 if controllerActive: _camVelocity = _camVelocity.move_toward( orbitInput * orbitSensitivity * SETTINGS.cameraSpeedController, orbitAcceleration * delta ) _yaw += _camVelocity.x * delta * xMult _pitch += _camVelocity.y * delta * yMult else: _camVelocity = Vector2.ZERO # Right-click mouse drag var mouseActive:bool = _mouseDelta.length_squared() > 0.0 if mouseActive: _yaw += _mouseDelta.x * mouseSensitivity * SETTINGS.cameraSpeedMouse * xMult _pitch += _mouseDelta.y * mouseSensitivity * SETTINGS.cameraSpeedMouse * yMult _mouseDelta = Vector2.ZERO # Auto-center behind player: only when player is moving and camera has been idle var centerPressed:bool = Input.is_action_just_pressed("center_camera") if centerPressed: _idleTimer = autoReturnDelay elif controllerActive or mouseActive: _idleTimer = 0.0 else: var playerBody := targetNode as CharacterBody3D if playerBody != null and playerBody.velocity.length_squared() > 0.1: _idleTimer += delta else: _idleTimer = 0.0 if _idleTimer >= autoReturnDelay: _pitch = move_toward(_pitch, autoReturnPitch, autoReturnSpeed * delta) var behindYaw:float = rad_to_deg(atan2( targetNode.global_transform.basis.z.x, targetNode.global_transform.basis.z.z )) var yawDiff:float = fposmod(behindYaw - _yaw + 180.0, 360.0) - 180.0 _yaw += move_toward(0.0, yawDiff, autoReturnSpeed * delta) _pitch = clamp(_pitch, pitchMin, pitchMax) var pivot:Vector3 = targetNode.global_transform.origin + pivotOffset var yawRad:float = deg_to_rad(_yaw) var pitchRad:float = deg_to_rad(_pitch) var dir:Vector3 = Vector3( sin(yawRad) * cos(pitchRad), sin(pitchRad), cos(yawRad) * cos(pitchRad) ) # Snap in instantly when terrain blocks; restore smoothly when clear var collisionDist:float = _resolveDistance(pivot, dir) if collisionDist < _currentDistance: _currentDistance = collisionDist else: _currentDistance = move_toward(_currentDistance, distance, distanceReturnSpeed * delta) global_transform.origin = pivot + dir * _currentDistance look_at(pivot, Vector3.UP) # Ray from pivot outward; returns the safe camera distance along dir. func _resolveDistance(pivot:Vector3, dir:Vector3) -> float: var space:PhysicsDirectSpaceState3D = get_world_3d().direct_space_state var query:PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(pivot, pivot + dir * distance) query.collision_mask = collisionMask var hit:Dictionary = space.intersect_ray(query) if hit.is_empty(): return distance var hitDist:float = (hit["position"] - pivot).length() - COLLISION_MARGIN return maxf(hitDist, minDistance)