Feat/2 weapons foundation #6

Merged
Dotts merged 30 commits from feat/2-weapons-foundation into main 2026-06-04 22:22:16 -07:00
4 changed files with 139 additions and 6 deletions
Showing only changes of commit 13ca2f46e9 - Show all commits
Binary file not shown.
Binary file not shown.
+116
View File
@@ -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!")
+23 -6
View File
@@ -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)
@@ -61,6 +63,16 @@ func _build_model() -> void:
muzzle_flash.omni_range = 4.0
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:
@@ -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: