feat: implement double barrel shotgun weapon with procedural sound generation
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,116 @@
|
|||||||
|
import math
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import wave
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.makedirs('assets/sounds', exist_ok=True)
|
||||||
|
|
||||||
|
def write_wav(filename, samples, sample_rate=44100):
|
||||||
|
with wave.open(filename, 'w') as wav_file:
|
||||||
|
wav_file.setnchannels(1)
|
||||||
|
wav_file.setsampwidth(2)
|
||||||
|
wav_file.setframerate(sample_rate)
|
||||||
|
for s in samples:
|
||||||
|
# Clamp and convert to 16-bit
|
||||||
|
val = int(max(-1.0, min(1.0, s)) * 32767)
|
||||||
|
data = struct.pack('<h', val)
|
||||||
|
wav_file.writeframesraw(data)
|
||||||
|
|
||||||
|
# 1. Shotgun Fire Sound (Bang + Boom + Decay)
|
||||||
|
sample_rate = 44100
|
||||||
|
duration = 0.8
|
||||||
|
num_samples = int(sample_rate * duration)
|
||||||
|
fire_samples = []
|
||||||
|
|
||||||
|
# Low-pass filter states
|
||||||
|
prev_val1 = 0.0
|
||||||
|
prev_val2 = 0.0
|
||||||
|
|
||||||
|
for i in range(num_samples):
|
||||||
|
t = i / sample_rate
|
||||||
|
|
||||||
|
# Base broadband noise
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
|
||||||
|
# 1. The Crack (Initial explosion, very sharp envelope)
|
||||||
|
crack_env = math.exp(-t * 50.0)
|
||||||
|
crack = noise * crack_env * 1.5
|
||||||
|
|
||||||
|
# 2. The Body (Deep explosive boom, slower decay)
|
||||||
|
body_env = math.exp(-t * 6.0)
|
||||||
|
|
||||||
|
# Dynamic low-pass filter (closes rapidly to muffle the tail into a boom)
|
||||||
|
alpha = max(0.01, 0.2 * math.exp(-t * 15.0))
|
||||||
|
filtered1 = prev_val1 + alpha * (noise - prev_val1)
|
||||||
|
prev_val1 = filtered1
|
||||||
|
|
||||||
|
# Second pass for deeper muffle
|
||||||
|
filtered2 = prev_val2 + alpha * (filtered1 - prev_val2)
|
||||||
|
prev_val2 = filtered2
|
||||||
|
|
||||||
|
body = filtered2 * body_env * 10.0 # Boost the bass significantly
|
||||||
|
|
||||||
|
# Mix crack and body
|
||||||
|
raw_sample = crack + body
|
||||||
|
|
||||||
|
# Soft clipping (tanh) to simulate a microphone peaking from a loud blast
|
||||||
|
sample = math.tanh(raw_sample * 2.5)
|
||||||
|
|
||||||
|
fire_samples.append(sample)
|
||||||
|
|
||||||
|
write_wav('assets/sounds/shotgun_fire.wav', fire_samples)
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Shotgun Reload Sound (Break, Insert, Snap)
|
||||||
|
duration = 1.0
|
||||||
|
num_samples = int(sample_rate * duration)
|
||||||
|
reload_samples = []
|
||||||
|
|
||||||
|
for i in range(num_samples):
|
||||||
|
t = i / sample_rate
|
||||||
|
sample = 0.0
|
||||||
|
noise = random.uniform(-1.0, 1.0)
|
||||||
|
|
||||||
|
# Phase 1: Break open (t=0.0)
|
||||||
|
if t < 0.2:
|
||||||
|
env_click = math.exp(-t * 60.0)
|
||||||
|
click = noise * env_click * 0.5
|
||||||
|
clunk_freq = 300.0 * math.exp(-t * 20.0) + 100.0
|
||||||
|
clunk = math.sin(t * clunk_freq * 2 * math.pi) * math.exp(-t * 15.0) * 0.4
|
||||||
|
sample += click + clunk
|
||||||
|
|
||||||
|
# Phase 2: Insert shells (t=0.3 to 0.6)
|
||||||
|
if 0.3 <= t < 0.6:
|
||||||
|
t2 = t - 0.3
|
||||||
|
slide_env = math.sin(t2 * math.pi / 0.3) * 0.15
|
||||||
|
slide = noise * slide_env
|
||||||
|
if t2 > 0.15:
|
||||||
|
seat_env = math.exp(-(t2 - 0.15) * 40.0)
|
||||||
|
seat_click = noise * seat_env * 0.4
|
||||||
|
sample += seat_click
|
||||||
|
sample += slide
|
||||||
|
|
||||||
|
# Phase 3: Snap shut (t=0.7)
|
||||||
|
if t >= 0.7:
|
||||||
|
t3 = t - 0.7
|
||||||
|
snap_env = math.exp(-t3 * 40.0)
|
||||||
|
snap = noise * snap_env * 0.8
|
||||||
|
ring_env = math.exp(-t3 * 10.0)
|
||||||
|
ring1 = math.sin(t3 * 3500.0 * 2 * math.pi)
|
||||||
|
ring2 = math.sin(t3 * 4200.0 * 2 * math.pi)
|
||||||
|
ring = (ring1 + ring2) * ring_env * 0.1
|
||||||
|
thud = math.sin(t3 * 80.0 * 2 * math.pi) * math.exp(-t3 * 15.0) * 0.5
|
||||||
|
sample += snap + ring + thud
|
||||||
|
|
||||||
|
reload_samples.append(sample)
|
||||||
|
|
||||||
|
# Global low-pass to smooth out the harsh digital noise
|
||||||
|
prev = 0.0
|
||||||
|
for i in range(len(reload_samples)):
|
||||||
|
prev = prev + 0.5 * (reload_samples[i] - prev)
|
||||||
|
reload_samples[i] = max(-1.0, min(1.0, prev * 1.5))
|
||||||
|
|
||||||
|
write_wav('assets/sounds/shotgun_reload.wav', reload_samples)
|
||||||
|
|
||||||
|
print("Generated shotgun sounds!")
|
||||||
@@ -10,6 +10,8 @@ var reload_timer: float = 0.0
|
|||||||
var player: CharacterBody3D
|
var player: CharacterBody3D
|
||||||
var camera: Camera3D
|
var camera: Camera3D
|
||||||
var muzzle_flash: OmniLight3D
|
var muzzle_flash: OmniLight3D
|
||||||
|
var fire_sound: AudioStreamPlayer3D
|
||||||
|
var reload_sound: AudioStreamPlayer3D
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
set_process_input(true)
|
set_process_input(true)
|
||||||
@@ -62,6 +64,16 @@ func _build_model() -> void:
|
|||||||
muzzle_flash.position = Vector3(0, 0.03, -0.45)
|
muzzle_flash.position = Vector3(0, 0.03, -0.45)
|
||||||
add_child(muzzle_flash)
|
add_child(muzzle_flash)
|
||||||
|
|
||||||
|
# Audio Players
|
||||||
|
fire_sound = AudioStreamPlayer3D.new()
|
||||||
|
fire_sound.stream = load("res://assets/sounds/shotgun_fire.wav")
|
||||||
|
fire_sound.position = muzzle_flash.position
|
||||||
|
add_child(fire_sound)
|
||||||
|
|
||||||
|
reload_sound = AudioStreamPlayer3D.new()
|
||||||
|
reload_sound.stream = load("res://assets/sounds/shotgun_reload.wav")
|
||||||
|
add_child(reload_sound)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if reloading:
|
if reloading:
|
||||||
reload_timer -= delta
|
reload_timer -= delta
|
||||||
@@ -87,8 +99,13 @@ func _input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
if event.is_action_pressed("reload"):
|
if event.is_action_pressed("reload"):
|
||||||
if shells < 2 and not reloading:
|
if shells < 2 and not reloading:
|
||||||
reloading = true
|
_start_reload()
|
||||||
reload_timer = reload_time
|
|
||||||
|
func _start_reload() -> void:
|
||||||
|
reloading = true
|
||||||
|
reload_timer = reload_time
|
||||||
|
if reload_sound:
|
||||||
|
reload_sound.play()
|
||||||
|
|
||||||
func _try_fire() -> void:
|
func _try_fire() -> void:
|
||||||
if shells > 0 and not reloading:
|
if shells > 0 and not reloading:
|
||||||
@@ -97,16 +114,16 @@ func _try_fire() -> void:
|
|||||||
_apply_impulse()
|
_apply_impulse()
|
||||||
_shoot_hitscan()
|
_shoot_hitscan()
|
||||||
_play_muzzle_flash()
|
_play_muzzle_flash()
|
||||||
|
if fire_sound:
|
||||||
|
fire_sound.play()
|
||||||
|
|
||||||
# Start reload timer if we just shot the LAST shell
|
# Start reload timer if we just shot the LAST shell
|
||||||
if shells == 0:
|
if shells == 0:
|
||||||
reloading = true
|
_start_reload()
|
||||||
reload_timer = reload_time
|
|
||||||
|
|
||||||
elif shells == 0 and not reloading:
|
elif shells == 0 and not reloading:
|
||||||
# Empty click, force reload
|
# Empty click, force reload
|
||||||
reloading = true
|
_start_reload()
|
||||||
reload_timer = reload_time
|
|
||||||
|
|
||||||
func _apply_impulse() -> void:
|
func _apply_impulse() -> void:
|
||||||
if not player or not camera:
|
if not player or not camera:
|
||||||
|
|||||||
Reference in New Issue
Block a user