feat: implement foundational weapon systems, including hitscan and projectile base classes, reload mechanics, and player movement/camera integration.

This commit is contained in:
DottsGit
2026-06-09 22:33:40 -04:00
parent 5102b770c8
commit f63187e493
7 changed files with 116 additions and 15 deletions
-1
View File
@@ -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:
+20
View File
@@ -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):
+22
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://c5eanbqnnm4kn
+15 -7
View File
@@ -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)
+15 -7
View File
@@ -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()
+43
View File
@@ -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)