Feat/visual audio overhaul #21

Merged
Dotts merged 31 commits from feat/visual-audio-overhaul into main 2026-07-21 14:09:18 -07:00
37 changed files with 87 additions and 22 deletions
Showing only changes of commit a9b0775de3 - Show all commits
Binary file not shown.
+5
View File
@@ -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.
+43 -8
View File
@@ -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
+8
View File
@@ -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
+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.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)
+5
View File
@@ -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",
} }
+5 -5
View File
@@ -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
+7 -3
View File
@@ -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()
+5 -3
View File
@@ -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()
+5 -3
View File
@@ -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()