extends Camera3D @export var height: float = 25.0 @export var horizontal_distance: float = 25.0 @export var horizontal_angle_deg: float = 45.0 # Perspective zoom (FOV) @export var fov_min: float = 20.0 @export var fov_max: float = 75.0 @export var default_fov: float = 30.0 @export var focus_slice_thickness: float = 6.0 @export var blur_transition: float = 15.0 var _shake_amount: float = 0.0 var _shake_decay: float = 0.0 var _user_fov: float = 0.0 # Trailing-camera state: _focus is the smoothed point the camera frames (it lags # the bull), _drag_offset is the velocity-driven trail that slides it behind. var _focus: Vector3 = Vector3.ZERO var _drag_offset: Vector3 = Vector3.ZERO var _focus_init: bool = false func _ready() -> void: set_as_top_level(true) projection = Camera3D.PROJECTION_PERSPECTIVE fov = default_fov _user_fov = default_fov near = 0.1 far = 150.0 func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("zoom_in"): _user_fov = clamp(_user_fov / 1.1, fov_min, fov_max) if event.is_action_pressed("zoom_out"): _user_fov = clamp(_user_fov * 1.1, fov_min, fov_max) # Pinch-to-zoom hook for the touch UI. `ratio` is the two-finger spread this frame over # last frame: >1 (fingers parting) narrows the FOV to zoom in, <1 widens it to pull back, # mirroring the scroll-wheel zoom_in/zoom_out feel. func adjust_zoom(ratio: float) -> void: if ratio <= 0.0: return _user_fov = clampf(_user_fov / ratio, fov_min, fov_max) func _process(delta: float) -> void: fov = lerpf(fov, _user_fov, delta * 8.0) var angle_rad := deg_to_rad(horizontal_angle_deg) var offset := Vector3( sin(angle_rad) * horizontal_distance, height, cos(angle_rad) * horizontal_distance ) var target := get_parent() as Node3D var focus := _drag_focus(target, delta) global_position = focus + offset look_at(focus, Vector3.UP) _update_dof(focus) if _shake_amount > 0.005: global_position += Vector3( randf_range(-_shake_amount, _shake_amount), randf_range(-_shake_amount * 0.25, _shake_amount * 0.25), randf_range(-_shake_amount, _shake_amount) ) _shake_amount = move_toward(_shake_amount, 0.0, _shake_decay * delta) # Smoothed follow point that drags behind the bull. The target trail sits opposite # the bull's flat velocity (cam_drag metres per m/s); the offset and the focus both # ease toward their targets so the camera lags and settles rather than snapping. func _drag_focus(target: Node3D, delta: float) -> Vector3: var pos := target.global_position if not _focus_init: _focus = pos _focus_init = true var vel := Vector3.ZERO if target is CharacterBody3D: vel = (target as CharacterBody3D).velocity vel.y = 0.0 var drag_target := -vel * DP.f("cam_drag") _drag_offset = _drag_offset.lerp(drag_target, clampf(DP.f("cam_drag_speed") * delta, 0.0, 1.0)) _focus = _focus.lerp(pos + _drag_offset, clampf(DP.f("cam_follow_lerp") * delta, 0.0, 1.0)) return _focus func trigger_hit(strength: float) -> void: if not Settings.screen_shake: return _shake_amount = strength * 0.5 _shake_decay = _shake_amount / 0.4 func _update_dof(target_pos: Vector3) -> void: var cam_attr: CameraAttributesPractical = null if attributes is CameraAttributesPractical: cam_attr = attributes if cam_attr == null: return var distance := global_position.distance_to(target_pos) cam_attr.dof_blur_near_distance = max(distance - focus_slice_thickness * 0.5, near + 0.1) cam_attr.dof_blur_far_distance = distance + focus_slice_thickness * 0.5 cam_attr.dof_blur_near_transition = blur_transition cam_attr.dof_blur_far_transition = blur_transition cam_attr.dof_blur_near_enabled = false cam_attr.dof_blur_far_enabled = false