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
18 changed files with 282 additions and 3 deletions
Showing only changes of commit 67bd5b4302 - Show all commits
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+149 -1
View File
@@ -113,4 +113,152 @@ for i in range(len(reload_samples)):
write_wav('assets/sounds/shotgun_reload.wav', reload_samples)
print("Generated shotgun sounds!")
# 3. Footstep Sound (Quiet Thud + Scrape)
footstep_duration = 0.15
footstep_samples = []
for i in range(int(sample_rate * footstep_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
# Dull Thud (lower frequency, less metallic)
thud_env = math.exp(-t * 25.0)
thud = math.sin(t * 50.0 * 2 * math.pi) * thud_env * 0.15
# Scrape (very quiet, low-passed)
scrape_env = math.exp(-t * 50.0)
scrape = noise * scrape_env * 0.03
footstep_samples.append(thud + scrape)
# Filter footstep to ensure no tin-can high frequencies
prev = 0.0
for i in range(len(footstep_samples)):
prev = prev + 0.3 * (footstep_samples[i] - prev)
footstep_samples[i] = prev
write_wav('assets/sounds/footstep.wav', footstep_samples)
# 4. Dash Sound (Whoosh)
dash_duration = 0.5
dash_samples = []
prev_dash = 0.0
for i in range(int(sample_rate * dash_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
env = math.sin(t * math.pi / dash_duration) ** 2 # Smooth bell curve
alpha = 0.1
filtered = prev_dash + alpha * (noise - prev_dash)
prev_dash = filtered
dash_samples.append(filtered * env * 0.8)
write_wav('assets/sounds/dash.wav', dash_samples)
# 5. Hit Confirm (Tink/Chirp)
hit_duration = 0.2
hit_samples = []
for i in range(int(sample_rate * hit_duration)):
t = i / sample_rate
env = math.exp(-t * 30.0)
# High pitch chirp sweeping slightly
freq = 2000.0 + math.exp(-t * 20.0) * 1000.0
tink = math.sin(t * freq * 2 * math.pi) * env * 0.3
hit_samples.append(tink)
write_wav('assets/sounds/hit_confirm.wav', hit_samples)
# 6. Wallrun (Continuous scrape)
# We want this to seamlessly loop
wallrun_duration = 1.0
wallrun_samples = []
for i in range(int(sample_rate * wallrun_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
# Give it a slight 4/4 rhythm
rhythm = 0.8 + 0.2 * math.sin(t * 8.0 * math.pi)
# Low pass to keep it as a dull scrape
alpha = 0.05
sample = noise * alpha * rhythm * 0.4
wallrun_samples.append(sample)
write_wav('assets/sounds/wallrun.wav', wallrun_samples)
# 7. Slide (Continuous deep friction)
slide_duration = 1.0
slide_samples = []
for i in range(int(sample_rate * slide_duration)):
noise = random.uniform(-1.0, 1.0)
# Deeper, steadier scrape than wallrun
alpha = 0.02
sample = noise * alpha * 0.5
slide_samples.append(sample)
write_wav('assets/sounds/slide.wav', slide_samples)
# 8. Wind (Subtle rushing air)
wind_duration = 2.0 # Generate 2 seconds
sample_count = int(sample_rate * wind_duration)
raw_wind = []
prev_wind = 0.0
# Generate extra long to stabilize filter
for i in range(sample_count + int(sample_rate * 0.5)):
noise = random.uniform(-1.0, 1.0)
alpha = 0.015
filtered = prev_wind + alpha * (noise - prev_wind)
prev_wind = filtered
if i >= int(sample_rate * 0.5):
raw_wind.append(filtered * 0.5)
# Crossfade the last 0.1s into the first 0.1s to make it perfectly loop
crossfade_samples = int(sample_rate * 0.1)
loop_wind = raw_wind[:-crossfade_samples] # remove the tail
for i in range(crossfade_samples):
# Blend the tail into the start
tail_sample = raw_wind[-(crossfade_samples - i)]
start_sample = loop_wind[i]
t = i / float(crossfade_samples)
loop_wind[i] = start_sample * t + tail_sample * (1.0 - t)
write_wav('assets/sounds/wind.wav', loop_wind)
# 9. Jump Sound (Quick exertion / push)
jump_duration = 0.2
jump_samples = []
for i in range(int(sample_rate * jump_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
# Lower frequency push (less tin can)
push_env = math.exp(-t * 25.0)
push = math.sin(t * 60.0 * 2 * math.pi) * push_env * 0.3
# High frequency clothing ruffle (quieter)
ruffle_env = math.exp(-t * 30.0)
ruffle = noise * ruffle_env * 0.05
jump_samples.append(push + ruffle)
# Filter jump to ensure no tin-can high frequencies
prev = 0.0
for i in range(len(jump_samples)):
prev = prev + 0.4 * (jump_samples[i] - prev)
jump_samples[i] = prev
write_wav('assets/sounds/jump.wav', jump_samples)
# 10. Double Jump Sound (Airy, cloudy whoosh)
dj_duration = 0.4
dj_samples = []
prev_dj = 0.0
for i in range(int(sample_rate * dj_duration)):
t = i / sample_rate
noise = random.uniform(-1.0, 1.0)
# Airy filter
alpha = 0.05
filtered = prev_dj + alpha * (noise - prev_dj)
prev_dj = filtered
# Envelope: soft attack, long decay
env = (math.exp(-t * 8.0) - math.exp(-t * 40.0)) * 1.5
# Slight pitch movement to give it a "lifting" feel
lift = math.sin(t * 150.0 * 2 * math.pi * (1.0 + t)) * env * 0.1
dj_samples.append((filtered * 0.4 + lift) * env)
write_wav('assets/sounds/double_jump.wav', dj_samples)
print("Generated all sounds!")
+89 -2
View File
@@ -10,6 +10,19 @@ var head_pivot: Node3D = null # FPSCameraRig node
var camera: Camera3D = null
var _damage_layer: CanvasLayer = null
# Audio
var footstep_player: AudioStreamPlayer
var dash_player: AudioStreamPlayer
var hit_player: AudioStreamPlayer
var slide_player: AudioStreamPlayer
var wallrun_player: AudioStreamPlayer
var wind_player: AudioStreamPlayer
var jump_player: AudioStreamPlayer
var double_jump_player: AudioStreamPlayer
# UI
var hit_marker: Control
func _ready() -> void:
var sm := _ensure_machine()
@@ -25,10 +38,72 @@ func _ready() -> void:
_damage_layer = CanvasLayer.new()
add_child(_damage_layer)
_setup_audio()
_setup_hit_marker()
set_process(true)
set_physics_process(true)
func _setup_audio() -> void:
footstep_player = AudioStreamPlayer.new()
footstep_player.stream = load("res://assets/sounds/footstep.wav")
add_child(footstep_player)
jump_player = AudioStreamPlayer.new()
jump_player.stream = load("res://assets/sounds/jump.wav")
add_child(jump_player)
double_jump_player = AudioStreamPlayer.new()
double_jump_player.stream = load("res://assets/sounds/double_jump.wav")
add_child(double_jump_player)
dash_player = AudioStreamPlayer.new()
dash_player.stream = load("res://assets/sounds/dash.wav")
dash_player.volume_db = -5.0
add_child(dash_player)
hit_player = AudioStreamPlayer.new()
hit_player.stream = load("res://assets/sounds/hit_confirm.wav")
add_child(hit_player)
slide_player = AudioStreamPlayer.new()
slide_player.stream = load("res://assets/sounds/slide.wav")
slide_player.volume_db = -10.0
add_child(slide_player)
wallrun_player = AudioStreamPlayer.new()
wallrun_player.stream = load("res://assets/sounds/wallrun.wav")
wallrun_player.volume_db = -10.0
add_child(wallrun_player)
wind_player = AudioStreamPlayer.new()
wind_player.stream = load("res://assets/sounds/wind.wav")
wind_player.volume_db = -80.0 # Start silent
add_child(wind_player)
wind_player.play()
func _setup_hit_marker() -> void:
hit_marker = Control.new()
hit_marker.set_anchors_preset(Control.PRESET_CENTER)
hit_marker.modulate.a = 0.0 # Hidden by default
_damage_layer.add_child(hit_marker)
# Draw an X perfectly centered
var length = 12
var thickness = 2
for angle in [PI/4, 3*PI/4, 5*PI/4, 7*PI/4]:
var rect = ColorRect.new()
rect.color = Color.WHITE
rect.size = Vector2(length, thickness)
rect.pivot_offset = rect.size / 2.0
# Center the rect itself at (0,0) before offset
var center_pos = -rect.size / 2.0
# Move it outward along the angle so it doesn't cover the exact center dot
rect.position = center_pos + Vector2(cos(angle), sin(angle)) * (length / 2.0)
rect.rotation = angle
hit_marker.add_child(rect)
func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
if not camera or not _damage_layer: return
@@ -45,7 +120,12 @@ func spawn_damage_number(amount: float, hit_pos: Vector3) -> void:
label.camera = camera
_damage_layer.add_child(label)
# Play sound and flash hit marker
hit_player.play()
hit_marker.modulate.a = 1.0
var tween = create_tween()
tween.tween_property(hit_marker, "modulate:a", 0.0, 0.4)
func _ensure_machine() -> MovementStateMachine:
if is_instance_valid(_machine):
@@ -58,8 +138,15 @@ func _ensure_machine() -> MovementStateMachine:
_machine = null
return null
func _physics_process(_delta: float) -> void:
var speed = velocity.length()
var wind_factor = clampf((speed - 10.0) / 25.0, 0.0, 1.0)
# Decibel scale is non-linear, so lerping from -80 means it stays inaudible for a long time.
# -40 db is practically silent for a background loop, -15 db is our max quiet wind.
wind_player.volume_db = lerpf(-40.0, -15.0, wind_factor)
if not wind_player.playing and wind_factor > 0.0:
wind_player.play()
var sm := _ensure_machine()
if not sm:
return
+4
View File
@@ -81,6 +81,8 @@ func update(delta: float) -> void:
machine.on_ground = false
machine.jump_buffer_time = 0.0
machine.register_chain_mechanic("bunny_hop")
if player.jump_player:
player.jump_player.play()
# Stay in air state
return
@@ -92,6 +94,8 @@ func update(delta: float) -> void:
player.velocity.y = params.double_jump_velocity
machine.current_jump_count += 1
machine.register_chain_mechanic("double_jump")
if player.double_jump_player:
player.double_jump_player.play()
return
# ── Wall run transition ───────────────────────────────────────────────
+4
View File
@@ -24,6 +24,10 @@ func enter(_data: Dictionary = {}) -> void:
elapsed = 0.0
_last_dash_time = now
var player := machine.player
if player.dash_player:
player.dash_player.play()
# Direction: use wish direction if available, else player forward
var wish := machine.wish_dir_world
if wish.length_squared() > 0.01:
+15
View File
@@ -5,6 +5,7 @@ var machine: MovementStateMachine
var params: MovementParams:
get: return machine.params
var _footstep_timer: float = 0.0
func enter(_data: Dictionary = {}) -> void:
machine.on_ground = true
@@ -33,6 +34,8 @@ func update(delta: float) -> void:
machine.on_ground = false
machine.coyote_timer = 0.0
machine.register_chain_mechanic("jump")
if player.jump_player:
player.jump_player.play()
machine.switch_to("air")
return
@@ -43,6 +46,8 @@ func update(delta: float) -> void:
machine.on_ground = false
machine.jump_buffer_time = 0.0
machine.register_chain_mechanic("jump")
if player.jump_player:
player.jump_player.play()
machine.switch_to("air")
return
@@ -86,6 +91,16 @@ func update(delta: float) -> void:
_update_capsule_height()
var current_hspeed = Vector2(player.velocity.x, player.velocity.z).length()
if current_hspeed > 1.0:
_footstep_timer -= delta
if _footstep_timer <= 0.0:
if player.footstep_player:
player.footstep_player.play()
_footstep_timer = max(0.2, 3.0 / current_hspeed)
else:
_footstep_timer = 0.0
# ── Update ground status ──────────────────────────────────────────────
if player.is_on_floor():
machine.on_ground = true
+11
View File
@@ -14,6 +14,9 @@ func enter(_data: Dictionary = {}) -> void:
elapsed = 0.0
machine.register_chain_mechanic("slide")
if machine.player.slide_player and not machine.player.slide_player.playing:
machine.player.slide_player.play()
# Save original capsule height and halve it
var shape: CapsuleShape3D = _get_capsule()
if shape:
@@ -38,6 +41,10 @@ func enter(_data: Dictionary = {}) -> void:
func exit() -> void:
machine.slide_cooldown_timer = params.slide_cooldown
if machine.player.slide_player:
machine.player.slide_player.stop()
# Restore original capsule height
var shape: CapsuleShape3D = _get_capsule()
if shape and _saved_capsule_height > 0.0:
@@ -47,6 +54,10 @@ func exit() -> void:
func update(delta: float) -> void:
elapsed += delta
var player := machine.player
if player.slide_player and not player.slide_player.playing:
player.slide_player.play()
var vel: Vector3 = player.velocity
# ── Apply friction to horizontal velocity ─────────────────────────────
+9
View File
@@ -22,6 +22,9 @@ func enter(_data: Dictionary = {}) -> void:
machine.on_ground = false
_last_vel = machine.player.velocity
if machine.player.wallrun_player and not machine.player.wallrun_player.playing:
machine.player.wallrun_player.play()
var hvel := Vector3(machine.player.velocity.x, 0.0, machine.player.velocity.z)
_current_tangent = machine.wall_normal.cross(Vector3.UP).normalized()
if hvel.dot(_current_tangent) < 0.0:
@@ -43,6 +46,9 @@ func exit() -> void:
var rig = _get_camera_rig()
if rig:
rig.clear_wall_tilt()
if machine.player.wallrun_player:
machine.player.wallrun_player.stop()
func update(delta: float) -> void:
@@ -57,6 +63,9 @@ func update(delta: float) -> void:
var player := machine.player
var vel: Vector3 = player.velocity
if player.wallrun_player and not player.wallrun_player.playing:
player.wallrun_player.play()
if vel.distance_squared_to(_last_vel) > 100.0:
machine.wall_normal = Vector3.ZERO
machine.wall_side = 0.0
+1
View File
@@ -68,6 +68,7 @@ func _build_model() -> void:
fire_sound = AudioStreamPlayer3D.new()
fire_sound.stream = load("res://assets/sounds/shotgun_fire.wav")
fire_sound.position = muzzle_flash.position
fire_sound.volume_db = -20.0
add_child(fire_sound)
reload_sound = AudioStreamPlayer3D.new()