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:
co-authored by
Claude Fable 5
parent
6c8b0ddd28
commit
a9b0775de3
Binary file not shown.
@@ -3,6 +3,11 @@
|
|||||||
All current WAVs are forged (pitch/layer/trim via ffmpeg — see repo history)
|
All current WAVs are forged (pitch/layer/trim via ffmpeg — see repo history)
|
||||||
from these CC0 packs, no attribution required:
|
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 Impact Sounds — https://kenney.nl/assets/impact-sounds (CC0)
|
||||||
- Kenney Sci-Fi Sounds — https://kenney.nl/assets/sci-fi-sounds (CC0)
|
- Kenney Sci-Fi Sounds — https://kenney.nl/assets/sci-fi-sounds (CC0)
|
||||||
- Kenney Interface Sounds — https://kenney.nl/assets/interface-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.
@@ -46,12 +46,22 @@ const CLIP_FALLBACKS := {
|
|||||||
"Death": ["Death", "Fall"],
|
"Death": ["Death", "Fall"],
|
||||||
"Hit": ["Hit", "Idle"],
|
"Hit": ["Hit", "Idle"],
|
||||||
"Dance": ["Dance", "Idle"],
|
"Dance": ["Dance", "Idle"],
|
||||||
|
"PistolIdle": ["PistolIdle", "Idle"],
|
||||||
|
"PistolShoot": ["PistolShoot"],
|
||||||
|
"PistolReload": ["PistolReload"],
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
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
|
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 skeleton: Skeleton3D
|
||||||
var animation_player: AnimationPlayer
|
var animation_player: AnimationPlayer
|
||||||
@@ -246,6 +256,9 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
clip = "Run"
|
clip = "Run"
|
||||||
elif speed > 0.5:
|
elif speed > 0.5:
|
||||||
clip = "Walk"
|
clip = "Walk"
|
||||||
|
elif is_holding_weapon and _resolved_clips.has("PistolIdle"):
|
||||||
|
# Armed idle: weapon actually held ready instead of arms-down
|
||||||
|
clip = "PistolIdle"
|
||||||
"air":
|
"air":
|
||||||
# Rising = jump, falling = the fall loop.
|
# Rising = jump, falling = the fall loop.
|
||||||
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
|
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.
|
# Scale locomotion playback so feet keep up with actual movement speed.
|
||||||
match clip:
|
match clip:
|
||||||
"Walk":
|
"Walk", "CrouchWalk":
|
||||||
animation_player.speed_scale = clampf(speed / walk_anim_reference_speed, 0.7, 1.6)
|
animation_player.speed_scale = clampf(speed / walk_anim_reference_speed, 0.7, 1.6)
|
||||||
"Run", "Sprint", "WallRun":
|
"Run", "Sprint", "WallRun":
|
||||||
animation_player.speed_scale = clampf(speed / run_anim_reference_speed, 0.7, 1.8)
|
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
|
return
|
||||||
if restart:
|
if restart:
|
||||||
animation_player.stop()
|
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
|
_current_clip = clip_name
|
||||||
|
|
||||||
|
|
||||||
@@ -432,6 +445,10 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
const LEAN_PITCH := 0.18
|
const LEAN_PITCH := 0.18
|
||||||
const SLIDE_BACK := 0.75 # torso lean-back during slide
|
const SLIDE_BACK := 0.75 # torso lean-back during slide
|
||||||
const SLIDE_HEAD_UP := 0.7 # head pitch to keep looking forward
|
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_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_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
|
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 skel := get_skeleton()
|
||||||
var names := SPINE + ["DEF-neck", "DEF-head",
|
var names := SPINE + ["DEF-neck", "DEF-head",
|
||||||
"DEF-upper_arm.R", "DEF-forearm.R", "DEF-hand.R",
|
"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:
|
for n in names:
|
||||||
_idx[n] = skel.find_bone(n)
|
_idx[n] = skel.find_bone(n)
|
||||||
_resolved = true
|
_resolved = true
|
||||||
@@ -476,14 +494,23 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
for n in SPINE:
|
for n in SPINE:
|
||||||
_add_space(skel, _idx.get(n, -1), per)
|
_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:
|
func _apply_wall_lean(skel: Skeleton3D) -> void:
|
||||||
var roll := Quaternion(Vector3(0, 0, 1), wall * 0.35)
|
var q := Quaternion(Vector3(0, 0, 1), wall * 0.35) \
|
||||||
var per := Quaternion.IDENTITY.slerp(roll, 1.0 / SPINE.size())
|
* Quaternion(Vector3(1, 0, 0), WALL_PITCH * absf(wall))
|
||||||
|
var per := Quaternion.IDENTITY.slerp(q, 1.0 / SPINE.size())
|
||||||
for n in SPINE:
|
for n in SPINE:
|
||||||
_add_space(skel, _idx.get(n, -1), per)
|
_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:
|
func _apply_slide(skel: Skeleton3D) -> void:
|
||||||
var back := Quaternion(Vector3(1, 0, 0), -SLIDE_BACK * slide)
|
var back := Quaternion(Vector3(1, 0, 0), -SLIDE_BACK * slide)
|
||||||
var per := Quaternion.IDENTITY.slerp(back, 1.0 / SPINE.size())
|
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)
|
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-neck", -1), Quaternion.IDENTITY.slerp(up, 0.5))
|
||||||
_add_space(skel, _idx.get("DEF-head", -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 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
|
# weapon (attached to the hand) at the side, so we leave it alone. On ADS we
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ func _process(_delta: float) -> bool:
|
|||||||
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
|
||||||
elif _frames == 240:
|
elif _frames == 240:
|
||||||
_shot("level")
|
_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 true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c3fpowkoyt67f
|
||||||
@@ -174,6 +174,9 @@ func _setup_third_person_camera() -> void:
|
|||||||
_tp_camera.name = "ThirdPersonCamera"
|
_tp_camera.name = "ThirdPersonCamera"
|
||||||
_tp_camera.fov = camera.fov
|
_tp_camera.fov = camera.fov
|
||||||
_tp_camera.current = false
|
_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)
|
boom.add_child(_tp_camera)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ LIBRARY_CLIP_MAP = {
|
|||||||
"Death01": "Death",
|
"Death01": "Death",
|
||||||
"Hit_Chest": "Hit",
|
"Hit_Chest": "Hit",
|
||||||
"Dance_Loop": "Dance",
|
"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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ func _ready() -> void:
|
|||||||
# Comic/cel theme for every Control in the game (root window inherits down)
|
# Comic/cel theme for every Control in the game (root window inherits down)
|
||||||
UITheme.apply_global(get_tree())
|
UITheme.apply_global(get_tree())
|
||||||
|
|
||||||
# Menu music (scene-owned: stops automatically when a level loads)
|
# Menu music (scene-owned: stops automatically when a level loads).
|
||||||
var music_path := "res://assets/sounds/music_menu.wav"
|
# 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):
|
if ResourceLoader.exists(music_path):
|
||||||
var music := AudioStreamPlayer.new()
|
var music := AudioStreamPlayer.new()
|
||||||
music.stream = load(music_path)
|
music.stream = load(music_path)
|
||||||
if music.stream is AudioStreamWAV:
|
if music.stream is AudioStreamOggVorbis:
|
||||||
music.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
music.stream.loop = true
|
||||||
music.stream.loop_end = music.stream.data.size() / 4 # 16-bit stereo frames
|
|
||||||
music.bus = "Music"
|
music.bus = "Music"
|
||||||
music.volume_db = -8.0
|
music.volume_db = -8.0
|
||||||
music.autoplay = true
|
music.autoplay = true
|
||||||
|
|||||||
@@ -65,9 +65,13 @@ func _setup_effects() -> void:
|
|||||||
flight_sound.autoplay = true
|
flight_sound.autoplay = true
|
||||||
if flight_sound.stream is AudioStreamWAV:
|
if flight_sound.stream is AudioStreamWAV:
|
||||||
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||||
flight_sound.unit_size = 50.0
|
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
|
||||||
flight_sound.max_db = 6.0
|
# Localized: audible nearby, falls off with distance instead of droning
|
||||||
flight_sound.volume_db = 6.0
|
# 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)
|
add_child(flight_sound)
|
||||||
flight_sound.play()
|
flight_sound.play()
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ func _setup_effects() -> void:
|
|||||||
flight_sound.autoplay = true
|
flight_sound.autoplay = true
|
||||||
if flight_sound.stream is AudioStreamWAV:
|
if flight_sound.stream is AudioStreamWAV:
|
||||||
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||||
flight_sound.unit_size = 50.0
|
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
|
||||||
flight_sound.max_db = 6.0
|
flight_sound.unit_size = 6.0
|
||||||
flight_sound.volume_db = 0.0
|
flight_sound.max_db = 0.0
|
||||||
|
flight_sound.max_distance = 45.0
|
||||||
|
flight_sound.volume_db = -10.0
|
||||||
add_child(flight_sound)
|
add_child(flight_sound)
|
||||||
flight_sound.play()
|
flight_sound.play()
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ func _start_homing_effects() -> void:
|
|||||||
flight_sound.autoplay = true
|
flight_sound.autoplay = true
|
||||||
if flight_sound.stream is AudioStreamWAV:
|
if flight_sound.stream is AudioStreamWAV:
|
||||||
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
flight_sound.stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||||
flight_sound.unit_size = 50.0
|
flight_sound.stream.loop_end = flight_sound.stream.data.size() / 2 # mono 16-bit frames
|
||||||
flight_sound.max_db = 6.0
|
flight_sound.unit_size = 6.0
|
||||||
flight_sound.volume_db = 6.0
|
flight_sound.max_db = 0.0
|
||||||
|
flight_sound.max_distance = 45.0
|
||||||
|
flight_sound.volume_db = -10.0
|
||||||
add_child(flight_sound)
|
add_child(flight_sound)
|
||||||
flight_sound.play()
|
flight_sound.play()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user