Files
Papay-Shooter/weapons/base_projectile_weapon.gd
2026-08-02 02:20:02 -04:00

190 lines
5.7 KiB
GDScript

extends Node3D
class_name BaseProjectileWeapon
@export var weapon_name: String = "Base Projectile"
@export var fire_rate: float = 0.1
@export var max_ammo: int = 30
@export var reload_time: float = 1.5
@export var base_damage: float = 10.0
@export var min_damage: float = 5.0
@export var falloff_start: float = 10.0
@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
## Reload choreography style (see ViewmodelAnim): mag, boltmag, break,
## tube, cell, none.
@export var reload_style: String = "tube"
var current_ammo: int = 30
var reloading: bool = false
var reload_timer: float = 0.0
var fire_cooldown: float = 0.0
var is_firing: bool = false
var _vm_kick: float = 0.0 # per-shot viewmodel recoil, decays in _process
var player: CharacterBody3D
var camera: Camera3D
var muzzle_flash: OmniLight3D
var fire_sound: AudioStreamPlayer3D
var reload_sound: AudioStreamPlayer3D
var model_root: Node3D
func _ready() -> void:
current_ammo = max_ammo
set_process_input(true)
set_process(true)
model_root = Node3D.new()
add_child(model_root)
_build_model()
if fire_sound:
AudioManager.configure_player(fire_sound,
AudioManager.weapon_sound_id(weapon_name))
if reload_sound:
AudioManager.configure_player(reload_sound, "shotgun_reload")
func _build_model() -> void:
pass
func _process(delta: float) -> void:
if fire_cooldown > 0.0:
fire_cooldown -= delta
if reloading:
# The manual-of-arms animation runs via ViewmodelAnim; here we just
# track the timer and hand the ammo over at the end.
reload_timer -= delta
if reload_timer <= 0.0:
current_ammo = max_ammo
reloading = false
elif automatic and is_firing and fire_cooldown <= 0.0 and current_ammo > 0:
_fire()
# Per-shot recoil kick (launchers shove hard; see recoil_amplitude).
if not reloading and _vm_kick > 0.001:
_vm_kick = lerpf(_vm_kick, 0.0, 1.0 - exp(-14.0 * delta))
ViewmodelAnim.apply_kick(self, _vm_kick, recoil_amplitude)
func _input(event: InputEvent) -> void:
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
if event.is_action_pressed("fire"):
is_firing = true
if not automatic and fire_cooldown <= 0.0 and current_ammo > 0 and not reloading:
_fire()
elif current_ammo == 0 and not reloading:
_start_reload()
elif event.is_action_released("fire"):
is_firing = false
if event.is_action_pressed("reload"):
if current_ammo < max_ammo and not reloading:
_start_reload()
func _start_reload() -> void:
reloading = true
reload_timer = reload_time
is_firing = false
_vm_kick = 0.0
ViewmodelAnim.play_reload(self, reload_style, reload_time)
if reload_sound:
reload_sound.play()
## Called by WeaponManager when this weapon is holstered mid-anything.
func unequip() -> void:
ViewmodelAnim.stop(self)
_vm_kick = 0.0
func _fire() -> void:
if current_ammo <= 0 or reloading:
return
current_ammo -= 1
fire_cooldown = fire_rate
_vm_kick = minf(_vm_kick + 1.0, 1.4)
_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.015, 0.015)
fire_sound.play()
if current_ammo == 0:
_start_reload()
## See BaseHitscanWeapon.world_muzzle(): in third person the viewmodel's muzzle
## sits inside the player's head, so world effects must come off the gun the
## character is actually holding.
func world_muzzle() -> Node3D:
if player and "third_person" in player and player.third_person \
and player.has_method("get_visual_model"):
var vis = player.get_visual_model()
if vis and vis.has_method("get_muzzle_node"):
var m: Node3D = vis.get_muzzle_node()
if m:
return m
return muzzle_flash
## Where OTHER clients should see this shot leave from — always the held gun.
func remote_muzzle_position() -> Vector3:
if player and player.has_method("get_visual_model"):
var vis = player.get_visual_model()
if vis and vis.has_method("get_muzzle_node"):
var m: Node3D = vis.get_muzzle_node()
if m:
return m.global_position
var own := world_muzzle()
return own.global_position if own else global_position
func _play_muzzle_flash() -> void:
if not muzzle_flash:
return
var m := world_muzzle()
ExplosionVFX.muzzle_flash(m.get_parent(), m.position)
if m is OmniLight3D:
var lit: OmniLight3D = m
lit.light_energy = 8.0
create_tween().tween_property(lit, "light_energy", 0.0, 0.05)
if m != muzzle_flash:
muzzle_flash.light_energy = 8.0
create_tween().tween_property(muzzle_flash, "light_energy", 0.0, 0.05)
func _shoot_projectile() -> void:
if not camera: return
var origin = camera.global_position
var forward = -camera.global_transform.basis.z.normalized()
var right = camera.global_transform.basis.x.normalized()
var up = camera.global_transform.basis.y.normalized()
# Calculate spread
var r = sqrt(randf()) * spread_angle
var theta = randf() * PI * 2.0
var offset_dir = (right * cos(theta) + up * sin(theta)) * r
var fire_dir = (forward + offset_dir).normalized()
_spawn_custom_projectile(origin, fire_dir)
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
# Remote viewers see our character, so the projectile must appear to
# leave the held gun rather than our camera.
player.server_play_fire_effects.rpc_id(1, remote_muzzle_position(),
fire_dir, weapon_name, false)
func _spawn_custom_projectile(_origin: Vector3, _fire_dir: Vector3) -> void:
# Virtual method for custom projectiles (e.g., bouncing, homing)
pass