feat: first-person reload animation on the viewmodel

The weapon dips down-and-inward with a roll when a reload starts, holds
through the reload, and rises as it completes — driven by the weapon's
actual reload_timer so the motion always matches the reload duration.
Complements the reload ring UI and the third-person reload clip.

Smoke 0 failures, movement 11/11.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-19 12:04:49 -04:00
co-authored by Claude Fable 5
parent 7920aecec5
commit 39af109893
+15
View File
@@ -67,6 +67,7 @@ var _recoil_yaw: float = 0.0
var _target_drift_offset: Vector3 = Vector3.ZERO var _target_drift_offset: Vector3 = Vector3.ZERO
var _current_drift_offset: Vector3 = Vector3.ZERO var _current_drift_offset: Vector3 = Vector3.ZERO
var _swap_pitch: float = 0.0 var _swap_pitch: float = 0.0
var _reload_dip: float = 0.0
func add_recoil(pitch: float, yaw: float) -> void: func add_recoil(pitch: float, yaw: float) -> void:
_recoil_pitch += pitch * 20.0 # Scale up for visual model recoil _recoil_pitch += pitch * 20.0 # Scale up for visual model recoil
@@ -151,6 +152,20 @@ func _process(_delta: float) -> void:
if _swap_pitch > 0.1: if _swap_pitch > 0.1:
vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(_swap_pitch)) vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(_swap_pitch))
vm_camera.translate_object_local(Vector3(0, -_swap_pitch * 0.005, 0)) vm_camera.translate_object_local(Vector3(0, -_swap_pitch * 0.005, 0))
# Reload animation: the weapon dips down-and-inward with a roll,
# holds through the reload, and rises as it completes.
var aw = weapons.get(active_slot)
if aw and "reloading" in aw and aw.reloading \
and "reload_timer" in aw and "reload_time" in aw and aw.reload_time > 0.0:
var t: float = clampf(1.0 - aw.reload_timer / aw.reload_time, 0.0, 1.0)
_reload_dip = lerpf(_reload_dip, sin(minf(t * 1.4, 1.0) * PI), 10.0 * _delta)
else:
_reload_dip = lerpf(_reload_dip, 0.0, 10.0 * _delta)
if _reload_dip > 0.01:
vm_camera.translate_object_local(Vector3(0.03 * _reload_dip, 0.14 * _reload_dip, 0))
vm_camera.rotate_object_local(Vector3.FORWARD, deg_to_rad(-14.0 * _reload_dip))
vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(9.0 * _reload_dip))
vm_camera.fov = lerpf(vm_camera.fov, target_fov, 10.0 * _delta) vm_camera.fov = lerpf(vm_camera.fov, target_fov, 10.0 * _delta)