extends Node3D class_name FPSCameraRig ## FPS camera rig — handles mouse look, head bob, FOV kick, wall-run tilt. ## Attach as child of the CharacterBody3D player. Camera3D is a child of this node. @export var sensitivity: float = 0.002 @export var pitch_limit: float = 89.0 var params: MovementParams var camera: Camera3D var _bob_timer: float = 0.0 var _target_tilt: float = 0.0 var _current_tilt: float = 0.0 var _target_fov: float = 90.0 var _weapon_fov_override: float = -1.0 func _ready() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) camera = get_node_or_null("Camera3D") if camera and params: camera.fov = params.base_fov _target_fov = params.base_fov # Explicitly enable callbacks — set_script() at runtime doesn't auto-register them set_process_input(true) set_process(true) func _input(event: InputEvent) -> void: if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: var is_ads = camera and camera.fov < params.base_fov - 5.0 var sens = SettingsManager.ads_sensitivity if is_ads else SettingsManager.mouse_sensitivity # Yaw: rotate the player (parent) get_parent().rotate_y(-event.relative.x * sens) # Pitch: rotate this pivot rotation.x = clampf(rotation.x - event.relative.y * sens, deg_to_rad(-pitch_limit), deg_to_rad(pitch_limit)) # Toggle mouse capture with Escape if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) else: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _process(delta: float) -> void: if not camera or not params: return var player: CharacterBody3D = get_parent() as CharacterBody3D if not player: return # ── FOV kick ────────────────────────────────────────────────────────── _target_fov = params.base_fov var hspeed := Vector2(player.velocity.x, player.velocity.z).length() # Smoothly scale FOV based on speed above walking speed var speed_factor = clampf((hspeed - params.walk_speed) / (params.walk_speed * 0.5), 0.0, 1.0) _target_fov = lerpf(params.base_fov, params.dash_fov, speed_factor) if _weapon_fov_override > 0.0: _target_fov = _weapon_fov_override camera.fov = lerpf(camera.fov, _target_fov, 1.0 - exp(-params.fov_lerp_speed * delta)) # ── Head bob ────────────────────────────────────────────────────────── var sm = player.get_node_or_null("MovementStateMachine") var is_sliding = sm and sm.current_state == "slide" if player.is_on_floor() and hspeed > 1.0 and not is_sliding: _bob_timer += delta * params.head_bob_frequency * (hspeed / params.walk_speed) var bob_y := sin(_bob_timer) * params.head_bob_amplitude var bob_x := cos(_bob_timer * 0.5) * params.head_bob_amplitude * 0.5 camera.position.y = lerp(camera.position.y, bob_y, 0.3) camera.position.x = lerp(camera.position.x, bob_x, 0.3) else: _bob_timer = 0.0 camera.position.y = lerp(camera.position.y, 0.0, 0.15) camera.position.x = lerp(camera.position.x, 0.0, 0.15) # ── Wall-run tilt ───────────────────────────────────────────────────── _current_tilt = lerpf(_current_tilt, _target_tilt, 1.0 - exp(-params.wall_run_tilt_speed * delta)) camera.rotation.z = deg_to_rad(_current_tilt) ## Called by the movement system to set wall-run tilt direction. ## side: -1.0 (left wall), 1.0 (right wall), 0.0 (no tilt) func set_wall_tilt(side: float) -> void: if params: _target_tilt = side * params.wall_run_tilt_angle else: _target_tilt = side * 12.0 func set_weapon_fov(fov: float) -> void: _weapon_fov_override = fov ## Called to reset tilt (e.g., on landing). func clear_wall_tilt() -> void: _target_tilt = 0.0