diff --git a/scenes/player.tscn b/scenes/player.tscn index 5120c6c..8c7e9d7 100644 --- a/scenes/player.tscn +++ b/scenes/player.tscn @@ -1,7 +1,7 @@ [gd_scene format=3 uid="uid://player"] [ext_resource type="Script" path="res://scripts/network/player.gd" id="1_player"] -[ext_resource type="Script" path="res://client/characters/character/fps_camera.gd" id="2_fps_camera"] +[ext_resource type="Script" path="res://scripts/characters/fps_camera.gd" id="2_fps_camera"] [ext_resource type="Script" path="res://scripts/network/player_net_input.gd" id="3_player_net_input"] [sub_resource type="CapsuleShape3D" id="1"] diff --git a/scripts/characters/fps_camera.gd b/scripts/characters/fps_camera.gd new file mode 100644 index 0000000..6153f82 --- /dev/null +++ b/scripts/characters/fps_camera.gd @@ -0,0 +1,220 @@ +## FpsCamera — first-person camera view effects for tactical FPS. +## +## Manages view bobbing (head bob from movement), weapon sway, and FOV kick. +## The parent FPSCharacterController handles mouse look (yaw/pitch). +## +## Place as a Camera3D child of FPSCharacterController. +class_name FpsCamera +extends Camera3D + +# --------------------------------------------------------------------------- +# Exports +# --------------------------------------------------------------------------- + +## Enable view bobbing (head bob when walking/running). +@export var view_bob_enabled: bool = true + +## How much the camera bobs horizontally (units). +@export_range(0.0, 0.5) var bob_amplitude_h: float = 0.04 + +## How much the camera bobs vertically. +@export_range(0.0, 0.5) var bob_amplitude_v: float = 0.04 + +## Frequency multiplier for bobbing (higher = faster). +@export_range(0.5, 5.0) var bob_frequency: float = 2.5 + +## Sprint bob multiplier (more aggressive). +@export_range(1.0, 3.0) var bob_sprint_mult: float = 1.6 + +## Crouch bob multiplier (less movement). +@export_range(0.1, 1.0) var bob_crouch_mult: float = 0.4 + +## FOV kick on sprint (tactical FOV increase). +@export_range(0.0, 10.0) var sprint_fov_kick: float = 3.0 + +## FOV kick when firing weapon. +@export_range(-5.0, 10.0) var shoot_fov_kick: float = 0.5 + +## How fast FOV recovers. +@export var fov_recovery_speed: float = 6.0 + +## Enable weapon sway (subtle rotation from movement). +@export var weapon_sway_enabled: bool = true + +## Maximum weapon sway rotation (degrees). +@export_range(0.0, 5.0) var sway_max_rotation: float = 1.5 + +## Sway responsiveness (higher = snappier). +@export_range(1.0, 20.0) var sway_response: float = 8.0 + +# --------------------------------------------------------------------------- +# Internal +# --------------------------------------------------------------------------- + +## Reference to parent controller (duck-typed — uses has_method instead of class_name). +var _controller: Node = null + +## Bob time accumulator. +var _bob_time: float = 0.0 + +## Current bob offset. +var _bob_offset: Vector2 = Vector2.ZERO + +## Current weapon node (child named "Weapon" or first MeshInstance3D). +var _weapon: Node3D = null + +## Weapon rest position (local transform when no sway). +var _weapon_rest: Transform3D + +## FOV state. +var _base_fov: float = 75.0 +var _current_fov_offset: float = 0.0 + +## Sway state (used as both current value and interpolation target). +var _sway_current: Vector2 = Vector2.ZERO +var _sway_target: Vector2 = Vector2.ZERO + +## Base eye Y set by parent controller's crouch logic (for additive bob). +var _base_eye_y: float = 0.0 + +# --------------------------------------------------------------------------- +# Lifecycle +# --------------------------------------------------------------------------- + +func _ready() -> void: + _controller = get_parent() + if _controller == null or not _controller.has_method(&"is_sprinting"): + push_warning("FpsCamera: Parent has no is_sprinting() — view features disabled.") + + _base_fov = fov + + # Find weapon node (for sway) + for child in get_children(): + if child is Node3D: + _weapon = child + _weapon_rest = child.transform + break + + +func _process(delta: float) -> void: + if _controller == null: + return + + # Capture the eye height set by parent controller's crouch logic + # before we apply view bob offsets (which must be additive). + _base_eye_y = position.y + + # FOV management + _update_fov(delta) + + # View bobbing + if view_bob_enabled: + _update_bob(delta) + + # Weapon sway + if weapon_sway_enabled and _weapon: + _update_sway(delta) + + +# --------------------------------------------------------------------------- +# FOV +# --------------------------------------------------------------------------- + +func _update_fov(delta: float) -> void: + var target_offset: float = 0.0 + + # Sprint FOV kick — sustained while sprinting (not just on transition) + if _controller.is_sprinting(): + target_offset = sprint_fov_kick + + # Shoot FOV kick is triggered externally via trigger_shoot_fov() + # and decays toward the sprint/walk target naturally via move_toward. + _current_fov_offset = move_toward(_current_fov_offset, target_offset, fov_recovery_speed * delta) + fov = _base_fov + _current_fov_offset + + +# --------------------------------------------------------------------------- +# View bobbing +# --------------------------------------------------------------------------- + +func _update_bob(delta: float) -> void: + var crouch_amount: float = _controller.get_crouch_amount() + var sprinting: bool = _controller.is_sprinting() + + # Calculate bob speed multiplier from controller state + var speed_mult: float = 1.0 + if sprinting: + speed_mult = bob_sprint_mult + elif crouch_amount > 0.0: + speed_mult = lerpf(1.0, bob_crouch_mult, crouch_amount) + + if speed_mult < 0.01: + # Stationary — reset to zero + _bob_offset = _bob_offset.move_toward(Vector2.ZERO, delta * 10.0) + else: + _bob_time += delta * bob_frequency * speed_mult + _bob_offset = Vector2( + sin(_bob_time * 2.0) * bob_amplitude_h * speed_mult, + sin(_bob_time) * bob_amplitude_v * speed_mult + ) + + # Apply bob to camera position (relative to parent's eye offset) + position.x = _bob_offset.x + # Bob is additive on Y so it does NOT overwrite the crouch eye height + # set by the parent FPSCharacterController._update_crouch(). + position.y = _base_eye_y + _bob_offset.y + + +# --------------------------------------------------------------------------- +# Weapon sway +# --------------------------------------------------------------------------- + +func _update_sway(delta: float) -> void: + # Sway follows mouse movement indirectly via controller yaw/pitch changes + # Use velocity for physics-based sway: recentre over time + var vel := _controller.velocity as Vector3 + var sway_h: float = clamp(vel.x * 0.003, -sway_max_rotation, sway_max_rotation) + var sway_v: float = clamp(vel.z * 0.003, -sway_max_rotation, sway_max_rotation) + # Add a tiny amount from pitch/yaw delta (not mouse, from movement direction change) + _sway_target = Vector2( + move_toward(_sway_target.x, sway_h, sway_response * delta), + move_toward(_sway_target.y, sway_v, sway_response * delta) + ) + + # Apply to weapon node as small rotation offsets + if _weapon: + var target := Transform3D( + Basis.from_euler(Vector3( + deg_to_rad(_sway_target.y), + deg_to_rad(_sway_target.x), + 0.0 + )), + _weapon_rest.origin + ) + _weapon.transform = _weapon.transform.interpolate_with(target, sway_response * delta * 0.5) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +## Trigger a shoot FOV kick. Call from weapon script. +func trigger_shoot_fov() -> void: + _current_fov_offset += shoot_fov_kick + + +## Reset view to resting state (for respawn). +func reset_view() -> void: + _bob_time = 0.0 + _bob_offset = Vector2.ZERO + _current_fov_offset = 0.0 + _sway_current = Vector2.ZERO + _sway_target = Vector2.ZERO + fov = _base_fov + if _weapon: + _weapon.transform = _weapon_rest + + +## Get the current bob offset for external effects. +func get_bob_offset() -> Vector2: + return _bob_offset diff --git a/scripts/characters/fps_camera.gd.uid b/scripts/characters/fps_camera.gd.uid new file mode 100644 index 0000000..89b01e4 --- /dev/null +++ b/scripts/characters/fps_camera.gd.uid @@ -0,0 +1 @@ +uid://b6rixkmc2v0lt