Feat/visual audio overhaul #21
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
uid://dwst4d2srljqj
|
||||||
@@ -49,6 +49,7 @@ const CLIP_FALLBACKS := {
|
|||||||
"PistolIdle": ["PistolIdle", "Idle"],
|
"PistolIdle": ["PistolIdle", "Idle"],
|
||||||
"PistolShoot": ["PistolShoot"],
|
"PistolShoot": ["PistolShoot"],
|
||||||
"PistolReload": ["PistolReload"],
|
"PistolReload": ["PistolReload"],
|
||||||
|
"Throw": ["Throw", "Hit"],
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
||||||
@@ -59,10 +60,18 @@ const BLEND_TIME := 0.15
|
|||||||
## Per-clip blend overrides: snappy moves cut fast, locomotion cross-fades.
|
## Per-clip blend overrides: snappy moves cut fast, locomotion cross-fades.
|
||||||
const BLEND_TIMES := {
|
const BLEND_TIMES := {
|
||||||
"Dash": 0.06, "Jump": 0.08, "Hit": 0.05, "Land": 0.08,
|
"Dash": 0.06, "Jump": 0.08, "Hit": 0.05, "Land": 0.08,
|
||||||
"Slide": 0.1, "Death": 0.1,
|
"Slide": 0.1, "Death": 0.1, "Throw": 0.06, "PistolReload": 0.12,
|
||||||
"Idle": 0.25, "PistolIdle": 0.25, "Walk": 0.2, "Run": 0.2, "Sprint": 0.2,
|
"Idle": 0.25, "PistolIdle": 0.25, "Walk": 0.2, "Run": 0.2, "Sprint": 0.2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## Named gameplay actions -> (clip, lock seconds). Networked via the
|
||||||
|
## controller's synced action counter.
|
||||||
|
const ACTIONS := {
|
||||||
|
"reload": ["PistolReload", 1.15],
|
||||||
|
"throw": ["Throw", 0.55],
|
||||||
|
"shoot": ["PistolShoot", 0.2],
|
||||||
|
}
|
||||||
|
|
||||||
var skeleton: Skeleton3D
|
var skeleton: Skeleton3D
|
||||||
var animation_player: AnimationPlayer
|
var animation_player: AnimationPlayer
|
||||||
var loaded: bool = false
|
var loaded: bool = false
|
||||||
@@ -222,6 +231,24 @@ func set_dancing(on: bool) -> void:
|
|||||||
_dancing = on
|
_dancing = on
|
||||||
|
|
||||||
|
|
||||||
|
## Play a named gameplay action (reload / throw / shoot) as a one-shot.
|
||||||
|
func play_action(action: String) -> void:
|
||||||
|
if ACTIONS.has(action):
|
||||||
|
play_oneshot(ACTIONS[action][0], ACTIONS[action][1])
|
||||||
|
|
||||||
|
|
||||||
|
## Aim pitch in radians (up positive) — the upper body follows the camera.
|
||||||
|
func set_aim_pitch(pitch: float) -> void:
|
||||||
|
if _pose_mod:
|
||||||
|
_pose_mod.aim_pitch = clampf(pitch, -1.2, 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
## Kick the pose recoil (fires on every shot, local echo or remote replay).
|
||||||
|
func add_gun_recoil(strength: float = 1.0) -> void:
|
||||||
|
if _pose_mod:
|
||||||
|
_pose_mod.recoil = minf(_pose_mod.recoil + strength, 1.5)
|
||||||
|
|
||||||
|
|
||||||
## Same contract as HumanoidModel.update_state(). Called by the movement
|
## Same contract as HumanoidModel.update_state(). Called by the movement
|
||||||
## controller each frame with either local or network-synced state.
|
## controller each frame with either local or network-synced state.
|
||||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||||
@@ -437,6 +464,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var ads: float = 0.0 # 0 hip .. 1 aiming
|
var ads: float = 0.0 # 0 hip .. 1 aiming
|
||||||
var slide: float = 0.0 # 0 .. 1 slide blend
|
var slide: float = 0.0 # 0 .. 1 slide blend
|
||||||
var wall: float = 0.0 # -1 wall left .. +1 wall right (wall-run lean)
|
var wall: float = 0.0 # -1 wall left .. +1 wall right (wall-run lean)
|
||||||
|
var aim_pitch: float = 0.0 # radians, up positive — upper body follows aim
|
||||||
|
var recoil: float = 0.0 # decaying shot kick
|
||||||
var state: String = "idle"
|
var state: String = "idle"
|
||||||
var weapon_held: bool = false
|
var weapon_held: bool = false
|
||||||
|
|
||||||
@@ -476,12 +505,42 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
_resolve()
|
_resolve()
|
||||||
|
|
||||||
_apply_lean(skel)
|
_apply_lean(skel)
|
||||||
|
if absf(aim_pitch) > 0.01:
|
||||||
|
_apply_aim_pitch(skel)
|
||||||
if slide > 0.01:
|
if slide > 0.01:
|
||||||
_apply_slide(skel)
|
_apply_slide(skel)
|
||||||
if absf(wall) > 0.01:
|
if absf(wall) > 0.01:
|
||||||
_apply_wall_lean(skel)
|
_apply_wall_lean(skel)
|
||||||
if weapon_held:
|
if weapon_held:
|
||||||
_apply_weapon(skel)
|
_apply_weapon(skel)
|
||||||
|
if recoil > 0.01:
|
||||||
|
_apply_recoil(skel)
|
||||||
|
recoil = lerpf(recoil, 0.0, 0.25)
|
||||||
|
|
||||||
|
|
||||||
|
# Upper body follows the camera pitch: distributed over spine/neck/head
|
||||||
|
# so looking up/down reads on the whole silhouette, not just the head.
|
||||||
|
func _apply_aim_pitch(skel: Skeleton3D) -> void:
|
||||||
|
# Positive camera pitch (looking up) arches the torso back.
|
||||||
|
var per := Quaternion.IDENTITY.slerp(
|
||||||
|
Quaternion(Vector3(1, 0, 0), -aim_pitch * 0.55), 1.0 / SPINE.size())
|
||||||
|
for n in SPINE:
|
||||||
|
_add_space(skel, _idx.get(n, -1), per)
|
||||||
|
var head_q := Quaternion(Vector3(1, 0, 0), -aim_pitch * 0.45)
|
||||||
|
_add_space(skel, _idx.get("DEF-neck", -1), Quaternion.IDENTITY.slerp(head_q, 0.5))
|
||||||
|
_add_space(skel, _idx.get("DEF-head", -1), Quaternion.IDENTITY.slerp(head_q, 0.5))
|
||||||
|
|
||||||
|
|
||||||
|
# Shot kick: shoulders snap back and up, forearms rise; decays fast.
|
||||||
|
func _apply_recoil(skel: Skeleton3D) -> void:
|
||||||
|
var k := recoil
|
||||||
|
var back := Quaternion(Vector3(1, 0, 0), -0.12 * k)
|
||||||
|
for n in ["DEF-spine.002", "DEF-spine.003"]:
|
||||||
|
_add_space(skel, _idx.get(n, -1), back)
|
||||||
|
var arm_up := Quaternion(Vector3(1, 0, 0), -0.3 * k)
|
||||||
|
_add_space(skel, _idx.get("DEF-upper_arm.R", -1), arm_up)
|
||||||
|
_add_space(skel, _idx.get("DEF-upper_arm.L", -1), arm_up)
|
||||||
|
_add_space(skel, _idx.get("DEF-forearm.R", -1), Quaternion(Vector3(1, 0, 0), -0.22 * k))
|
||||||
|
|
||||||
# Distribute a skeleton-space lean across the spine bones.
|
# Distribute a skeleton-space lean across the spine bones.
|
||||||
func _apply_lean(skel: Skeleton3D) -> void:
|
func _apply_lean(skel: Skeleton3D) -> void:
|
||||||
@@ -527,6 +586,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var straighten := Quaternion(Vector3(1, 0, 0), SLIDE_KNEE * slide)
|
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.R", -1), straighten)
|
||||||
_add_space(skel, _idx.get("DEF-shin.L", -1), straighten)
|
_add_space(skel, _idx.get("DEF-shin.L", -1), straighten)
|
||||||
|
# Trailing arm braces back-and-down against the ground for balance.
|
||||||
|
var brace := Quaternion(Vector3(1, 0, 0), 0.9 * slide) \
|
||||||
|
* Quaternion(Vector3(0, 0, 1), -0.5 * slide)
|
||||||
|
_add_space(skel, _idx.get("DEF-upper_arm.L", -1), brace)
|
||||||
|
_add_space(skel, _idx.get("DEF-forearm.L", -1), Quaternion(Vector3(1, 0, 0), 0.35 * slide))
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -295,6 +295,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_loadout_sp")
|
client_rep_config.add_property(":synced_loadout_sp")
|
||||||
client_rep_config.add_property(":synced_loadout_melee")
|
client_rep_config.add_property(":synced_loadout_melee")
|
||||||
client_rep_config.add_property(":synced_loadout_ready")
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
|
client_rep_config.add_property(":synced_action")
|
||||||
|
client_rep_config.add_property(":synced_action_seq")
|
||||||
client_rep_config.add_property(":synced_is_targeting")
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
client_rep_config.add_property(":synced_homing_target_pos")
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
client_sync.replication_config = client_rep_config
|
client_sync.replication_config = client_rep_config
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ func _process(_delta: float) -> bool:
|
|||||||
if p.has_method("set_third_person") and p.is_multiplayer_authority():
|
if p.has_method("set_third_person") and p.is_multiplayer_authority():
|
||||||
p.set_third_person(true)
|
p.set_third_person(true)
|
||||||
Input.action_press("move_forward")
|
Input.action_press("move_forward")
|
||||||
elif _frames == 320:
|
elif _frames == 300:
|
||||||
_shot("tp_run")
|
_shot("tp_run")
|
||||||
Input.action_release("move_forward")
|
Input.action_release("move_forward")
|
||||||
|
elif _frames == 338:
|
||||||
|
_shot("tp_idle")
|
||||||
# Aerial overview of the whole map
|
# Aerial overview of the whole map
|
||||||
var cam := root.get_camera_3d()
|
var cam := root.get_camera_3d()
|
||||||
if cam:
|
if cam:
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ var footstep_player: AudioStreamPlayer
|
|||||||
var footstep_streams: Array = []
|
var footstep_streams: Array = []
|
||||||
var land_player: AudioStreamPlayer
|
var land_player: AudioStreamPlayer
|
||||||
var _remote_footstep_timer: float = 0.0
|
var _remote_footstep_timer: float = 0.0
|
||||||
|
|
||||||
|
# Networked one-shot animation actions (reload/throw): the owner bumps the
|
||||||
|
# sequence; remote peers play the named action when it changes.
|
||||||
|
@export var synced_action: String = ""
|
||||||
|
@export var synced_action_seq: int = 0
|
||||||
|
var _last_action_seq: int = 0
|
||||||
|
var _was_reloading: bool = false
|
||||||
var dash_player: AudioStreamPlayer
|
var dash_player: AudioStreamPlayer
|
||||||
var recent_attackers: Dictionary = {} # attacker_id: timestamp
|
var recent_attackers: Dictionary = {} # attacker_id: timestamp
|
||||||
var hit_player: AudioStreamPlayer
|
var hit_player: AudioStreamPlayer
|
||||||
@@ -509,6 +516,11 @@ func rpc_play_fire_effects(origin: Vector3, target_or_dir: Vector3, _weapon_name
|
|||||||
if sid != "" and am.has_sound(sid):
|
if sid != "" and am.has_sound(sid):
|
||||||
am.play_3d(sid, origin)
|
am.play_3d(sid, origin)
|
||||||
|
|
||||||
|
# Visible shot kick on the remote shooter's model
|
||||||
|
var rv = get_visual_model()
|
||||||
|
if rv and rv.has_method("add_gun_recoil"):
|
||||||
|
rv.add_gun_recoil()
|
||||||
|
|
||||||
if is_hitscan:
|
if is_hitscan:
|
||||||
# Spawn a standalone tracer directly into the scene
|
# Spawn a standalone tracer directly into the scene
|
||||||
_spawn_remote_tracer(origin, target_or_dir)
|
_spawn_remote_tracer(origin, target_or_dir)
|
||||||
@@ -983,6 +995,13 @@ func _process(delta: float) -> void:
|
|||||||
visual.set_wall_side(synced_wall_side)
|
visual.set_wall_side(synced_wall_side)
|
||||||
if visual.has_method("set_dancing"):
|
if visual.has_method("set_dancing"):
|
||||||
visual.set_dancing(synced_is_dancing)
|
visual.set_dancing(synced_is_dancing)
|
||||||
|
# Upper body follows the owner's synced camera pitch
|
||||||
|
if visual.has_method("set_aim_pitch") and head_pivot:
|
||||||
|
visual.set_aim_pitch(head_pivot.rotation.x)
|
||||||
|
# Replayed one-shot actions (reload, grenade throw)
|
||||||
|
if visual.has_method("play_action") and synced_action_seq != _last_action_seq:
|
||||||
|
_last_action_seq = synced_action_seq
|
||||||
|
visual.play_action(synced_action)
|
||||||
# Check for weapon changes
|
# Check for weapon changes
|
||||||
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
||||||
visual.set_weapon(synced_weapon_path)
|
visual.set_weapon(synced_weapon_path)
|
||||||
@@ -1002,6 +1021,18 @@ func _process(delta: float) -> void:
|
|||||||
_remote_footstep_timer = 0.0
|
_remote_footstep_timer = 0.0
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Local third-person model: aim pitch + reload one-shot (also broadcast
|
||||||
|
# to remote peers via the synced action counter)
|
||||||
|
var lvisual = get_visual_model()
|
||||||
|
if lvisual:
|
||||||
|
if lvisual.has_method("set_aim_pitch") and head_pivot:
|
||||||
|
lvisual.set_aim_pitch(head_pivot.rotation.x)
|
||||||
|
var lw = _active_weapon()
|
||||||
|
var now_reloading: bool = lw != null and "reloading" in lw and lw.reloading
|
||||||
|
if now_reloading and not _was_reloading:
|
||||||
|
_trigger_action("reload")
|
||||||
|
_was_reloading = now_reloading
|
||||||
|
|
||||||
# Update UI
|
# Update UI
|
||||||
if is_instance_valid(health_bar):
|
if is_instance_valid(health_bar):
|
||||||
health_bar.value = health
|
health_bar.value = health
|
||||||
@@ -1311,9 +1342,29 @@ func _draw_trajectory() -> void:
|
|||||||
current_pos = next_pos
|
current_pos = next_pos
|
||||||
throw_vel = next_vel
|
throw_vel = next_vel
|
||||||
|
|
||||||
|
## The local player's currently equipped weapon node (null if none).
|
||||||
|
func _active_weapon():
|
||||||
|
if not is_instance_valid(camera):
|
||||||
|
return null
|
||||||
|
var wman = camera.get_node_or_null("WeaponManager")
|
||||||
|
if wman and "active_slot" in wman and wman.weapons.has(wman.active_slot):
|
||||||
|
return wman.weapons[wman.active_slot]
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Play a named one-shot on the local model and broadcast it to peers.
|
||||||
|
func _trigger_action(action: String) -> void:
|
||||||
|
synced_action = action
|
||||||
|
synced_action_seq += 1
|
||||||
|
var v = get_visual_model()
|
||||||
|
if v and v.has_method("play_action"):
|
||||||
|
v.play_action(action)
|
||||||
|
|
||||||
|
|
||||||
func _throw_grenade() -> void:
|
func _throw_grenade() -> void:
|
||||||
if grenades <= 0: return
|
if grenades <= 0: return
|
||||||
grenades -= 1
|
grenades -= 1
|
||||||
|
_trigger_action("throw")
|
||||||
|
|
||||||
var throw_speed = 20.0
|
var throw_speed = 20.0
|
||||||
var throw_up_speed = 5.0
|
var throw_up_speed = 5.0
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_loadout_sp")
|
client_rep_config.add_property(":synced_loadout_sp")
|
||||||
client_rep_config.add_property(":synced_loadout_melee")
|
client_rep_config.add_property(":synced_loadout_melee")
|
||||||
client_rep_config.add_property(":synced_loadout_ready")
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
|
client_rep_config.add_property(":synced_action")
|
||||||
|
client_rep_config.add_property(":synced_action_seq")
|
||||||
client_rep_config.add_property(":synced_is_targeting")
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
client_rep_config.add_property(":synced_homing_target_pos")
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
client_sync.replication_config = client_rep_config
|
client_sync.replication_config = client_rep_config
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_loadout_sp")
|
client_rep_config.add_property(":synced_loadout_sp")
|
||||||
client_rep_config.add_property(":synced_loadout_melee")
|
client_rep_config.add_property(":synced_loadout_melee")
|
||||||
client_rep_config.add_property(":synced_loadout_ready")
|
client_rep_config.add_property(":synced_loadout_ready")
|
||||||
|
client_rep_config.add_property(":synced_action")
|
||||||
|
client_rep_config.add_property(":synced_action_seq")
|
||||||
client_rep_config.add_property(":synced_is_targeting")
|
client_rep_config.add_property(":synced_is_targeting")
|
||||||
client_rep_config.add_property(":synced_homing_target_pos")
|
client_rep_config.add_property(":synced_homing_target_pos")
|
||||||
client_sync.replication_config = client_rep_config
|
client_sync.replication_config = client_rep_config
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ LIBRARY_CLIP_MAP = {
|
|||||||
"Pistol_Idle_Loop": "PistolIdle", # armed idle (weapon actually held up)
|
"Pistol_Idle_Loop": "PistolIdle", # armed idle (weapon actually held up)
|
||||||
"Pistol_Shoot": "PistolShoot",
|
"Pistol_Shoot": "PistolShoot",
|
||||||
"Pistol_Reload": "PistolReload",
|
"Pistol_Reload": "PistolReload",
|
||||||
|
"Sword_Attack": "Throw", # overhead swing reads as a grenade throw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user