feat: implement foundational weapon systems, including hitscan and projectile base classes, reload mechanics, and player movement/camera integration.
This commit is contained in:
@@ -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))
|
||||
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:
|
||||
|
||||
@@ -47,6 +47,7 @@ var grapple_swing_player: AudioStreamPlayer
|
||||
# UI
|
||||
var hit_marker: Control
|
||||
var _hit_marker_tween: Tween
|
||||
var reload_ring: Control
|
||||
|
||||
# Grapple
|
||||
var grapple_rope: MeshInstance3D
|
||||
@@ -214,6 +215,11 @@ func _setup_hit_marker() -> void:
|
||||
rect.rotation = angle
|
||||
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:
|
||||
grapple_rope = MeshInstance3D.new()
|
||||
var rope_mesh = CylinderMesh.new()
|
||||
@@ -770,6 +776,20 @@ func _process(delta: float) -> void:
|
||||
shield_bar.value = 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:
|
||||
# Continuously follow the ragdoll torso
|
||||
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 automatic: bool = true
|
||||
@export var spread_angle: float = 0.01
|
||||
@export var recoil_amplitude: float = 0.05
|
||||
|
||||
@export var penetration_count: int = 0
|
||||
@export var penetration_damage_penalty: float = 0.5
|
||||
@@ -46,15 +47,19 @@ func _process(delta: float) -> void:
|
||||
fire_cooldown -= delta
|
||||
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
var spin_speed = PI * 4.0
|
||||
model_root.rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
current_ammo = max_ammo
|
||||
reloading = false
|
||||
model_root.rotation.x = 0.0
|
||||
print(weapon_name, " Reloaded!")
|
||||
if reload_timer > 0.0:
|
||||
reload_timer -= delta
|
||||
else:
|
||||
# Continue the loop until facing normal direction (multiples of 2 PI)
|
||||
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:
|
||||
_fire()
|
||||
@@ -93,6 +98,9 @@ func _fire() -> void:
|
||||
_shoot_hitscan()
|
||||
_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:
|
||||
# Slight pitch variation
|
||||
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 automatic: bool = true
|
||||
@export var spread_angle: float = 0.01
|
||||
@export var recoil_amplitude: float = 0.05
|
||||
|
||||
@export var projectile_speed: float = 50.0
|
||||
@export var projectile_gravity: float = 0.0
|
||||
@@ -45,15 +46,19 @@ func _process(delta: float) -> void:
|
||||
fire_cooldown -= delta
|
||||
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
var spin_speed = PI * 4.0
|
||||
model_root.rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
current_ammo = max_ammo
|
||||
reloading = false
|
||||
model_root.rotation.x = 0.0
|
||||
print(weapon_name, " Reloaded!")
|
||||
if reload_timer > 0.0:
|
||||
reload_timer -= delta
|
||||
else:
|
||||
# Continue the loop until facing normal direction (multiples of 2 PI)
|
||||
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:
|
||||
_fire()
|
||||
@@ -92,6 +97,9 @@ func _fire() -> void:
|
||||
_shoot_projectile()
|
||||
_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:
|
||||
fire_sound.pitch_scale = 1.0 + randf_range(-0.05, 0.05)
|
||||
fire_sound.play()
|
||||
|
||||
@@ -55,6 +55,14 @@ func _setup_viewmodel_viewport() -> void:
|
||||
dir_light.light_energy = 0.5
|
||||
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:
|
||||
if is_instance_valid(vm_camera) and is_instance_valid(camera):
|
||||
vm_camera.global_transform = camera.global_transform
|
||||
@@ -68,6 +76,41 @@ func _process(_delta: float) -> void:
|
||||
else:
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user