Feat/12 add effects and better animations to fpv #17
@@ -84,7 +84,6 @@ func _process(delta: float) -> void:
|
|||||||
_current_tilt = lerpf(_current_tilt, _target_tilt, 1.0 - exp(-params.wall_run_tilt_speed * delta))
|
_current_tilt = lerpf(_current_tilt, _target_tilt, 1.0 - exp(-params.wall_run_tilt_speed * delta))
|
||||||
camera.rotation.z = deg_to_rad(_current_tilt)
|
camera.rotation.z = deg_to_rad(_current_tilt)
|
||||||
|
|
||||||
|
|
||||||
## Called by the movement system to set wall-run tilt direction.
|
## Called by the movement system to set wall-run tilt direction.
|
||||||
## side: -1.0 (left wall), 1.0 (right wall), 0.0 (no tilt)
|
## side: -1.0 (left wall), 1.0 (right wall), 0.0 (no tilt)
|
||||||
func set_wall_tilt(side: float) -> void:
|
func set_wall_tilt(side: float) -> void:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ var grapple_swing_player: AudioStreamPlayer
|
|||||||
# UI
|
# UI
|
||||||
var hit_marker: Control
|
var hit_marker: Control
|
||||||
var _hit_marker_tween: Tween
|
var _hit_marker_tween: Tween
|
||||||
|
var reload_ring: Control
|
||||||
|
|
||||||
# Grapple
|
# Grapple
|
||||||
var grapple_rope: MeshInstance3D
|
var grapple_rope: MeshInstance3D
|
||||||
@@ -214,6 +215,11 @@ func _setup_hit_marker() -> void:
|
|||||||
rect.rotation = angle
|
rect.rotation = angle
|
||||||
hit_marker.add_child(rect)
|
hit_marker.add_child(rect)
|
||||||
|
|
||||||
|
# Reload Ring
|
||||||
|
reload_ring = load("res://ui/reload_ring.gd").new()
|
||||||
|
reload_ring.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
_damage_layer.add_child(reload_ring)
|
||||||
|
|
||||||
func _setup_grapple() -> void:
|
func _setup_grapple() -> void:
|
||||||
grapple_rope = MeshInstance3D.new()
|
grapple_rope = MeshInstance3D.new()
|
||||||
var rope_mesh = CylinderMesh.new()
|
var rope_mesh = CylinderMesh.new()
|
||||||
@@ -770,6 +776,20 @@ func _process(delta: float) -> void:
|
|||||||
shield_bar.value = shield
|
shield_bar.value = shield
|
||||||
shield_label.text = "%d / %d" % [ceil(shield), max_shield]
|
shield_label.text = "%d / %d" % [ceil(shield), max_shield]
|
||||||
|
|
||||||
|
# Update Reload Ring
|
||||||
|
if is_instance_valid(reload_ring) and is_instance_valid(camera):
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman:
|
||||||
|
var slot = wman.get("active_slot")
|
||||||
|
if slot != null and wman.weapons.has(slot):
|
||||||
|
var w = wman.weapons[slot]
|
||||||
|
if "reloading" in w and w.reloading and "reload_timer" in w and "reload_time" in w:
|
||||||
|
reload_ring.progress = 1.0 - (w.reload_timer / w.reload_time)
|
||||||
|
else:
|
||||||
|
reload_ring.progress = 0.0
|
||||||
|
else:
|
||||||
|
reload_ring.progress = 0.0
|
||||||
|
|
||||||
if is_dead:
|
if is_dead:
|
||||||
# Continuously follow the ragdoll torso
|
# Continuously follow the ragdoll torso
|
||||||
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
extends Control
|
||||||
|
class_name ReloadRing
|
||||||
|
|
||||||
|
var progress: float = 0.0:
|
||||||
|
set(val):
|
||||||
|
progress = clampf(val, 0.0, 1.0)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
var radius: float = 16.0
|
||||||
|
var thickness: float = 4.0
|
||||||
|
var color: Color = Color(1.0, 1.0, 1.0, 0.8)
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if progress <= 0.0 or progress >= 1.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Draw background arc (darker)
|
||||||
|
draw_arc(Vector2.ZERO, radius, 0, PI * 2.0, 32, Color(0, 0, 0, 0.4), thickness, true)
|
||||||
|
|
||||||
|
# Draw progress arc
|
||||||
|
var end_angle = -PI / 2.0 + (PI * 2.0 * progress)
|
||||||
|
draw_arc(Vector2.ZERO, radius, -PI / 2.0, end_angle, 32, color, thickness, true)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c5eanbqnnm4kn
|
||||||
@@ -11,6 +11,7 @@ class_name BaseHitscanWeapon
|
|||||||
@export var max_range: float = 100.0
|
@export var max_range: float = 100.0
|
||||||
@export var automatic: bool = true
|
@export var automatic: bool = true
|
||||||
@export var spread_angle: float = 0.01
|
@export var spread_angle: float = 0.01
|
||||||
|
@export var recoil_amplitude: float = 0.05
|
||||||
|
|
||||||
@export var penetration_count: int = 0
|
@export var penetration_count: int = 0
|
||||||
@export var penetration_damage_penalty: float = 0.5
|
@export var penetration_damage_penalty: float = 0.5
|
||||||
@@ -46,15 +47,19 @@ func _process(delta: float) -> void:
|
|||||||
fire_cooldown -= delta
|
fire_cooldown -= delta
|
||||||
|
|
||||||
if reloading:
|
if reloading:
|
||||||
reload_timer -= delta
|
var spin_speed = PI * 4.0
|
||||||
var spin_speed = (PI * 2.0) / reload_time
|
|
||||||
model_root.rotation.x += spin_speed * delta
|
model_root.rotation.x += spin_speed * delta
|
||||||
|
|
||||||
if reload_timer <= 0.0:
|
if reload_timer > 0.0:
|
||||||
current_ammo = max_ammo
|
reload_timer -= delta
|
||||||
reloading = false
|
else:
|
||||||
model_root.rotation.x = 0.0
|
# Continue the loop until facing normal direction (multiples of 2 PI)
|
||||||
print(weapon_name, " Reloaded!")
|
var target_rot = ceil(model_root.rotation.x / (PI * 2.0)) * (PI * 2.0)
|
||||||
|
if target_rot - model_root.rotation.x < spin_speed * delta * 1.5:
|
||||||
|
current_ammo = max_ammo
|
||||||
|
reloading = false
|
||||||
|
model_root.rotation.x = 0.0
|
||||||
|
print(weapon_name, " Reloaded!")
|
||||||
|
|
||||||
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
|
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
|
||||||
_fire()
|
_fire()
|
||||||
@@ -93,6 +98,9 @@ func _fire() -> void:
|
|||||||
_shoot_hitscan()
|
_shoot_hitscan()
|
||||||
_play_muzzle_flash()
|
_play_muzzle_flash()
|
||||||
|
|
||||||
|
if get_parent() and get_parent().has_method("add_recoil"):
|
||||||
|
get_parent().add_recoil(recoil_amplitude, randf_range(-recoil_amplitude * 0.5, recoil_amplitude * 0.5))
|
||||||
|
|
||||||
if fire_sound:
|
if fire_sound:
|
||||||
# Slight pitch variation
|
# Slight pitch variation
|
||||||
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class_name BaseProjectileWeapon
|
|||||||
@export var max_range: float = 200.0
|
@export var max_range: float = 200.0
|
||||||
@export var automatic: bool = true
|
@export var automatic: bool = true
|
||||||
@export var spread_angle: float = 0.01
|
@export var spread_angle: float = 0.01
|
||||||
|
@export var recoil_amplitude: float = 0.05
|
||||||
|
|
||||||
@export var projectile_speed: float = 50.0
|
@export var projectile_speed: float = 50.0
|
||||||
@export var projectile_gravity: float = 0.0
|
@export var projectile_gravity: float = 0.0
|
||||||
@@ -45,15 +46,19 @@ func _process(delta: float) -> void:
|
|||||||
fire_cooldown -= delta
|
fire_cooldown -= delta
|
||||||
|
|
||||||
if reloading:
|
if reloading:
|
||||||
reload_timer -= delta
|
var spin_speed = PI * 4.0
|
||||||
var spin_speed = (PI * 2.0) / reload_time
|
|
||||||
model_root.rotation.x += spin_speed * delta
|
model_root.rotation.x += spin_speed * delta
|
||||||
|
|
||||||
if reload_timer <= 0.0:
|
if reload_timer > 0.0:
|
||||||
current_ammo = max_ammo
|
reload_timer -= delta
|
||||||
reloading = false
|
else:
|
||||||
model_root.rotation.x = 0.0
|
# Continue the loop until facing normal direction (multiples of 2 PI)
|
||||||
print(weapon_name, " Reloaded!")
|
var target_rot = ceil(model_root.rotation.x / (PI * 2.0)) * (PI * 2.0)
|
||||||
|
if target_rot - model_root.rotation.x < spin_speed * delta * 1.5:
|
||||||
|
current_ammo = max_ammo
|
||||||
|
reloading = false
|
||||||
|
model_root.rotation.x = 0.0
|
||||||
|
print(weapon_name, " Reloaded!")
|
||||||
|
|
||||||
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
|
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
|
||||||
_fire()
|
_fire()
|
||||||
@@ -92,6 +97,9 @@ func _fire() -> void:
|
|||||||
_shoot_projectile()
|
_shoot_projectile()
|
||||||
_play_muzzle_flash()
|
_play_muzzle_flash()
|
||||||
|
|
||||||
|
if get_parent() and get_parent().has_method("add_recoil"):
|
||||||
|
get_parent().add_recoil(recoil_amplitude, randf_range(-recoil_amplitude * 0.5, recoil_amplitude * 0.5))
|
||||||
|
|
||||||
if fire_sound:
|
if fire_sound:
|
||||||
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
||||||
fire_sound.play()
|
fire_sound.play()
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ func _setup_viewmodel_viewport() -> void:
|
|||||||
dir_light.light_energy = 0.5
|
dir_light.light_energy = 0.5
|
||||||
add_child(dir_light)
|
add_child(dir_light)
|
||||||
|
|
||||||
|
var _bob_timer: float = 0.0
|
||||||
|
var _recoil_pitch: float = 0.0
|
||||||
|
var _recoil_yaw: float = 0.0
|
||||||
|
|
||||||
|
func add_recoil(pitch: float, yaw: float) -> void:
|
||||||
|
_recoil_pitch += pitch * 20.0 # Scale up for visual model recoil
|
||||||
|
_recoil_yaw += yaw * 20.0
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if is_instance_valid(vm_camera) and is_instance_valid(camera):
|
if is_instance_valid(vm_camera) and is_instance_valid(camera):
|
||||||
vm_camera.global_transform = camera.global_transform
|
vm_camera.global_transform = camera.global_transform
|
||||||
@@ -68,6 +76,41 @@ func _process(_delta: float) -> void:
|
|||||||
else:
|
else:
|
||||||
var over_speed_factor = clampf((hspeed - 11.0) / 19.0, 0.0, 1.0) # maxes out at 30 m/s
|
var over_speed_factor = clampf((hspeed - 11.0) / 19.0, 0.0, 1.0) # maxes out at 30 m/s
|
||||||
target_fov = lerpf(72.0, 80.0, over_speed_factor)
|
target_fov = lerpf(72.0, 80.0, over_speed_factor)
|
||||||
|
|
||||||
|
# Weapon Viewmodel Bobbing and Drift
|
||||||
|
if player.is_on_floor() and hspeed > 1.0:
|
||||||
|
# Scale bobbing frequency by walk speed (assuming base walk speed is ~10-11)
|
||||||
|
_bob_timer += _delta * 12.0 * (hspeed / 11.0)
|
||||||
|
var bob_y = sin(_bob_timer) * 0.015
|
||||||
|
var bob_x = cos(_bob_timer * 0.5) * 0.01
|
||||||
|
vm_camera.translate_object_local(Vector3(bob_x, bob_y, 0))
|
||||||
|
else:
|
||||||
|
_bob_timer = 0.0
|
||||||
|
if not player.is_on_floor():
|
||||||
|
# Airborne Drift
|
||||||
|
# Convert player velocity into camera's local space
|
||||||
|
var local_vel = camera.global_transform.basis.inverse() * player.velocity
|
||||||
|
# We want the weapon to lag behind movement.
|
||||||
|
# If player moves left (-x), weapon should drift right (+x relative to camera, which means vm_camera shifts left -x to make weapon appear right)
|
||||||
|
# Wait, if vm_camera shifts left, the weapon (which is rendered by it) appears to shift RIGHT on the screen!
|
||||||
|
# So vm_camera shifts in the SAME direction as local velocity.
|
||||||
|
var drift_offset = Vector3(local_vel.x * 0.01, local_vel.y * 0.01, 0)
|
||||||
|
# Clamp the drift
|
||||||
|
drift_offset.x = clampf(drift_offset.x, -0.1, 0.1)
|
||||||
|
drift_offset.y = clampf(drift_offset.y, -0.1, 0.1)
|
||||||
|
vm_camera.translate_object_local(drift_offset)
|
||||||
|
# Slight rotation tilt as well
|
||||||
|
vm_camera.rotate_object_local(Vector3.UP, deg_to_rad(-local_vel.x * 0.5))
|
||||||
|
vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(local_vel.y * 0.5))
|
||||||
|
|
||||||
|
# Visual Recoil
|
||||||
|
_recoil_pitch = lerpf(_recoil_pitch, 0.0, 15.0 * _delta)
|
||||||
|
_recoil_yaw = lerpf(_recoil_yaw, 0.0, 15.0 * _delta)
|
||||||
|
|
||||||
|
vm_camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(_recoil_pitch))
|
||||||
|
vm_camera.rotate_object_local(Vector3.UP, deg_to_rad(_recoil_yaw))
|
||||||
|
# Add a slight backward kick along the local Z axis
|
||||||
|
vm_camera.translate_object_local(Vector3(0, 0, _recoil_pitch * 0.01))
|
||||||
|
|
||||||
vm_camera.fov = lerpf(vm_camera.fov, target_fov, 10.0 * _delta)
|
vm_camera.fov = lerpf(vm_camera.fov, target_fov, 10.0 * _delta)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user