feat: real gunshots, localized 3D audio, seamless music, better animations

Audio:
- All ballistic weapons now fire real firearm recordings (The Free Firearm
  Sound Library, CC0): AK-47 uses actual AK-47 shots, M4=AR-15, MP7=PPSh,
  AWP=Mosin Nagant, DMR=Savage 10, shotgun=Mossberg/Nova, nailgun=Ruger .22,
  rocket/mortar layered with real booms. Plasma keeps its energy sound.
- Reload is real lever-action foley instead of interface clicks
- Projectile flight loops localized: unit_size 50->6, max_db 6->0,
  max_distance 45 — no more mortar/rocket drone in every player's ear;
  loops also got proper loop_end (was a zero-length loop)
- Flight loops lowpassed into soft whooshes instead of sci-fi engine drones
- Menu music converted to OGG with native full-track looping (47s seamless,
  replaces the broken manual WAV loop points)

Animations (movement-shooter pass):
- Rebaked miku.glb with 4 new clips from the CC0 library: Grapple (swim
  reach — reads as a swing), PistolIdle (armed idle with weapon up),
  PistolShoot, PistolReload
- Armed idle: standing with a weapon now uses PistolIdle instead of
  arms-at-sides
- Slide pose overhaul: legs kick out front (lead/trail thighs, knees
  straighten) — feet-first slide instead of "sitting in a crouch"
- Wall run: forward drive pitch + inner arm reaches out to the wall
- Per-transition blend times: dash/hit/jump cut fast (0.05-0.08s),
  locomotion cross-fades smoothly (0.2-0.25s); CrouchWalk speed-scales

Fixes:
- Third-person camera no longer renders the first-person viewmodel layer
  (the gun/arms floated in front of the third-person view)

Verified: 11/11 movement tests, smoke test 0 failures (17 clips resolved),
third-person run screenshot shows grounded animated model, no floating gun.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-17 21:49:10 -04:00
co-authored by Claude Fable 5
parent 6c8b0ddd28
commit a9b0775de3
37 changed files with 87 additions and 22 deletions
Binary file not shown.
+5
View File
@@ -3,6 +3,11 @@
All current WAVs are forged (pitch/layer/trim via ffmpeg — see repo history)
from these CC0 packs, no attribution required:
- The Free Firearm Sound Library (Ben Jaszczak et al.) — CC0,
https://opengameart.org/content/the-free-firearm-sound-library
→ all ballistic weapon fire sounds (AK-47/AR-15/PPSh/Mosin/Savage/
Mossberg/Nova/Ruger/Carl Gustav recordings) + reload (Model 1894 lever)
- Kenney Impact Sounds — https://kenney.nl/assets/impact-sounds (CC0)
- Kenney Sci-Fi Sounds — https://kenney.nl/assets/sci-fi-sounds (CC0)
- Kenney Interface Sounds — https://kenney.nl/assets/interface-sounds (CC0)
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+43 -8
View File
@@ -46,12 +46,22 @@ const CLIP_FALLBACKS := {
"Death": ["Death", "Fall"],
"Hit": ["Hit", "Idle"],
"Dance": ["Dance", "Idle"],
"PistolIdle": ["PistolIdle", "Idle"],
"PistolShoot": ["PistolShoot"],
"PistolReload": ["PistolReload"],
}
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple", "Dance"]
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple",
"Dance", "PistolIdle"]
const BLEND_TIME := 0.15
## Per-clip blend overrides: snappy moves cut fast, locomotion cross-fades.
const BLEND_TIMES := {
"Dash": 0.06, "Jump": 0.08, "Hit": 0.05, "Land": 0.08,
"Slide": 0.1, "Death": 0.1,
"Idle": 0.25, "PistolIdle": 0.25, "Walk": 0.2, "Run": 0.2, "Sprint": 0.2,
}
var skeleton: Skeleton3D
var animation_player: AnimationPlayer
@@ -246,6 +256,9 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
clip = "Run"
elif speed > 0.5:
clip = "Walk"
elif is_holding_weapon and _resolved_clips.has("PistolIdle"):
# Armed idle: weapon actually held ready instead of arms-down
clip = "PistolIdle"
"air":
# Rising = jump, falling = the fall loop.
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
@@ -270,7 +283,7 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
# Scale locomotion playback so feet keep up with actual movement speed.
match clip:
"Walk":
"Walk", "CrouchWalk":
animation_player.speed_scale = clampf(speed / walk_anim_reference_speed, 0.7, 1.6)
"Run", "Sprint", "WallRun":
animation_player.speed_scale = clampf(speed / run_anim_reference_speed, 0.7, 1.8)
@@ -320,7 +333,7 @@ func _play_clip(canonical: String, restart: bool = false) -> void:
return
if restart:
animation_player.stop()
animation_player.play(clip_name, BLEND_TIME)
animation_player.play(clip_name, BLEND_TIMES.get(canonical, BLEND_TIME))
_current_clip = clip_name
@@ -432,6 +445,10 @@ class ShooterPoseModifier extends SkeletonModifier3D:
const LEAN_PITCH := 0.18
const SLIDE_BACK := 0.75 # torso lean-back during slide
const SLIDE_HEAD_UP := 0.7 # head pitch to keep looking forward
const SLIDE_LEG_FWD := 0.95 # thighs swing forward so feet lead the slide
const SLIDE_KNEE := 0.55 # shins straighten against the crouch clip's bend
const WALL_PITCH := 0.2 # forward drive lean during a wall run
const WALL_ARM_OUT := 0.9 # inner arm reaches out to touch the wall
const ADS_LIFT := 1.05 # upper-arm raise toward aim at full ADS
const ADS_SWING := 0.6 # swing arms in toward centre-front on ADS
const ADS_FOREARM := 0.55 # forearm bend to bring the weapon up on ADS
@@ -445,7 +462,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
var skel := get_skeleton()
var names := SPINE + ["DEF-neck", "DEF-head",
"DEF-upper_arm.R", "DEF-forearm.R", "DEF-hand.R",
"DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L"]
"DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L",
"DEF-thigh.R", "DEF-shin.R", "DEF-thigh.L", "DEF-shin.L"]
for n in names:
_idx[n] = skel.find_bone(n)
_resolved = true
@@ -476,14 +494,23 @@ class ShooterPoseModifier extends SkeletonModifier3D:
for n in SPINE:
_add_space(skel, _idx.get(n, -1), per)
# Wall run: roll the torso into the wall (+wall = wall on the right).
# Wall run: roll into the wall, drive forward, inner arm reaches the wall.
func _apply_wall_lean(skel: Skeleton3D) -> void:
var roll := Quaternion(Vector3(0, 0, 1), wall * 0.35)
var per := Quaternion.IDENTITY.slerp(roll, 1.0 / SPINE.size())
var q := Quaternion(Vector3(0, 0, 1), wall * 0.35) \
* Quaternion(Vector3(1, 0, 0), WALL_PITCH * absf(wall))
var per := Quaternion.IDENTITY.slerp(q, 1.0 / SPINE.size())
for n in SPINE:
_add_space(skel, _idx.get(n, -1), per)
# Reach the wall-side arm out sideways (character-right = -X, so a
# negative Z rotation swings the down arm toward the right side).
var reach := Quaternion(Vector3(0, 0, 1), -WALL_ARM_OUT * wall)
if wall > 0.0:
_add_space(skel, _idx.get("DEF-upper_arm.R", -1), reach)
else:
_add_space(skel, _idx.get("DEF-upper_arm.L", -1), reach)
# Slide: lean the whole torso back, then pitch the head up to look forward.
# Slide: torso leans back, head looks forward, legs kick out in front so
# it reads feet-first instead of "sitting in a crouch".
func _apply_slide(skel: Skeleton3D) -> void:
var back := Quaternion(Vector3(1, 0, 0), -SLIDE_BACK * slide)
var per := Quaternion.IDENTITY.slerp(back, 1.0 / SPINE.size())
@@ -492,6 +519,14 @@ class ShooterPoseModifier extends SkeletonModifier3D:
var up := Quaternion(Vector3(1, 0, 0), SLIDE_HEAD_UP * slide)
_add_space(skel, _idx.get("DEF-neck", -1), Quaternion.IDENTITY.slerp(up, 0.5))
_add_space(skel, _idx.get("DEF-head", -1), Quaternion.IDENTITY.slerp(up, 0.5))
# Legs: thighs swing forward (lead leg further), knees straighten.
var lead := Quaternion(Vector3(1, 0, 0), -SLIDE_LEG_FWD * slide)
var trail := Quaternion(Vector3(1, 0, 0), -SLIDE_LEG_FWD * 0.7 * slide)
_add_space(skel, _idx.get("DEF-thigh.R", -1), lead)
_add_space(skel, _idx.get("DEF-thigh.L", -1), trail)
var straighten := Quaternion(Vector3(1, 0, 0), SLIDE_KNEE * slide)
_add_space(skel, _idx.get("DEF-shin.R", -1), straighten)
_add_space(skel, _idx.get("DEF-shin.L", -1), straighten)
# Weapon hold. At the hip the base clip already keeps the arms down with the
# weapon (attached to the hand) at the side, so we leave it alone. On ADS we
+8
View File
@@ -26,6 +26,14 @@ func _process(_delta: float) -> bool:
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
elif _frames == 240:
_shot("level")
# Third person + run forward, to eyeball model grounding and animation
for p in root.find_children("*", "CharacterBody3D", true, false):
if p.has_method("set_third_person") and p.is_multiplayer_authority():
p.set_third_person(true)
Input.action_press("move_forward")
elif _frames == 320:
_shot("tp_run")
Input.action_release("move_forward")
return true
return false
+1
View File
@@ -0,0 +1 @@
uid://c3fpowkoyt67f
+3
View File
@@ -174,6 +174,9 @@ func _setup_third_person_camera() -> void:
_tp_camera.name = "ThirdPersonCamera"
_tp_camera.fov = camera.fov
_tp_camera.current = false
# Never render the first-person viewmodel layer (20) — otherwise the gun
# and arms float in front of the third-person view.
_tp_camera.cull_mask &= ~(1 << 19)
boom.add_child(_tp_camera)
+5
View File
@@ -61,6 +61,11 @@ LIBRARY_CLIP_MAP = {
"Death01": "Death",
"Hit_Chest": "Hit",
"Dance_Loop": "Dance",
# Movement-shooter extras:
"Swim_Fwd_Loop": "Grapple", # superman reach reads as a swing pose
"Pistol_Idle_Loop": "PistolIdle", # armed idle (weapon actually held up)
"Pistol_Shoot": "PistolShoot",
"Pistol_Reload": "PistolReload",
}
+5 -5
View File
@@ -13,14 +13,14 @@ func _ready() -> void:
# Comic/cel theme for every Control in the game (root window inherits down)
UITheme.apply_global(get_tree())
# Menu music (scene-owned: stops automatically when a level loads)
var music_path := "res://assets/sounds/music_menu.wav"
# Menu music (scene-owned: stops automatically when a level loads).
# OGG loops natively over the full track — no manual loop points.
var music_path := "res://assets/sounds/music_menu.ogg"
if ResourceLoader.exists(music_path):
var music := AudioStreamPlayer.new()
music.stream = load(music_path)
if music.stream is AudioStreamWAV:
music.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
music.stream.loop_end = music.stream.data.size() / 4 # 16-bit stereo frames
if music.stream is AudioStreamOggVorbis:
music.stream.loop = true
music.bus = "Music"
music.volume_db = -8.0
music.autoplay = true
+7 -3
View File
@@ -65,9 +65,13 @@ func _setup_effects() -> void:
flight_sound.autoplay = true
if flight_sound.stream is AudioStreamWAV:
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
flight_sound.unit_size = 50.0
flight_sound.max_db = 6.0
flight_sound.volume_db = 6.0
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
# Localized: audible nearby, falls off with distance instead of droning
# in every player's ear regardless of where the projectile is.
flight_sound.unit_size = 6.0
flight_sound.max_db = 0.0
flight_sound.max_distance = 45.0
flight_sound.volume_db = -8.0
add_child(flight_sound)
flight_sound.play()
+5 -3
View File
@@ -17,9 +17,11 @@ func _setup_effects() -> void:
flight_sound.autoplay = true
if flight_sound.stream is AudioStreamWAV:
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
flight_sound.unit_size = 50.0
flight_sound.max_db = 6.0
flight_sound.volume_db = 0.0
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
flight_sound.unit_size = 6.0
flight_sound.max_db = 0.0
flight_sound.max_distance = 45.0
flight_sound.volume_db = -10.0
add_child(flight_sound)
flight_sound.play()
+5 -3
View File
@@ -24,9 +24,11 @@ func _start_homing_effects() -> void:
flight_sound.autoplay = true
if flight_sound.stream is AudioStreamWAV:
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
flight_sound.unit_size = 50.0
flight_sound.max_db = 6.0
flight_sound.volume_db = 6.0
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
flight_sound.unit_size = 6.0
flight_sound.max_db = 0.0
flight_sound.max_distance = 45.0
flight_sound.volume_db = -10.0
add_child(flight_sound)
flight_sound.play()