feat: add double-barrel shotgun weapon, player movement state machine, and procedural sound assets
This commit is contained in:
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
@@ -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!")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ─────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user