Feat/2 weapons foundation #6
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 camera: Camera3D
|
||||
var muzzle_flash: OmniLight3D
|
||||
var fire_sound: AudioStreamPlayer3D
|
||||
var reload_sound: AudioStreamPlayer3D
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
@@ -62,6 +64,16 @@ func _build_model() -> void:
|
||||
muzzle_flash.position = Vector3(0, 0.03, -0.45)
|
||||
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:
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
@@ -87,8 +99,13 @@ func _input(event: InputEvent) -> void:
|
||||
|
||||
if event.is_action_pressed("reload"):
|
||||
if shells < 2 and not reloading:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
_start_reload()
|
||||
|
||||
func _start_reload() -> void:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
if reload_sound:
|
||||
reload_sound.play()
|
||||
|
||||
func _try_fire() -> void:
|
||||
if shells > 0 and not reloading:
|
||||
@@ -97,16 +114,16 @@ func _try_fire() -> void:
|
||||
_apply_impulse()
|
||||
_shoot_hitscan()
|
||||
_play_muzzle_flash()
|
||||
if fire_sound:
|
||||
fire_sound.play()
|
||||
|
||||
# Start reload timer if we just shot the LAST shell
|
||||
if shells == 0:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
_start_reload()
|
||||
|
||||
elif shells == 0 and not reloading:
|
||||
# Empty click, force reload
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
_start_reload()
|
||||
|
||||
func _apply_impulse() -> void:
|
||||
if not player or not camera:
|
||||
|
||||
Reference in New Issue
Block a user