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
+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