feat: player animation quality pass — aim follow, reload/throw, recoil
Third-person player characters now animate every action with readable intent (all networked, cel style preserved): - Look-around: upper body follows the owner's camera pitch — distributed over spine/neck/head so aiming up/down reads on the whole silhouette (driven by the already-synced HeadPivot rotation on remotes) - Reload: the baked PistolReload clip plays as a one-shot whenever the equipped weapon starts reloading (local edge-detect poll -> new synced action counter replays it on every peer) - Grenade throw: new Throw clip baked from the library's overhead swing (Sword_Attack retarget); triggers on throw, synced the same way - Shot recoil: visible kick on the shooter's model (shoulders snap back, forearms rise, fast decay) driven by the existing fire-effects RPC — remote players' shots now look like shots - Slide: trailing arm braces against the ground for balance on top of the feet-first pose - Armed idle confirmed in capture: weapon held at ready, not arms-down Plumbing: synced_action/synced_action_seq added to all three player spawners' replication configs; ACTIONS table on the model maps names to clips + lock times. Verified: 18 clips resolve (incl Throw), smoke 0 failures, movement 11/11, acoustics PASSED; third-person run + armed idle captures clean. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5224a455a6
commit
7920aecec5
@@ -39,6 +39,13 @@ var footstep_player: AudioStreamPlayer
|
||||
var footstep_streams: Array = []
|
||||
var land_player: AudioStreamPlayer
|
||||
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 recent_attackers: Dictionary = {} # attacker_id: timestamp
|
||||
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):
|
||||
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:
|
||||
# Spawn a standalone tracer directly into the scene
|
||||
_spawn_remote_tracer(origin, target_or_dir)
|
||||
@@ -983,6 +995,13 @@ func _process(delta: float) -> void:
|
||||
visual.set_wall_side(synced_wall_side)
|
||||
if visual.has_method("set_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
|
||||
if synced_weapon_path != "" and synced_weapon_path != visual.get_meta("current_weapon_path", ""):
|
||||
visual.set_weapon(synced_weapon_path)
|
||||
@@ -1002,6 +1021,18 @@ func _process(delta: float) -> void:
|
||||
_remote_footstep_timer = 0.0
|
||||
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
|
||||
if is_instance_valid(health_bar):
|
||||
health_bar.value = health
|
||||
@@ -1311,9 +1342,29 @@ func _draw_trajectory() -> void:
|
||||
current_pos = next_pos
|
||||
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:
|
||||
if grenades <= 0: return
|
||||
grenades -= 1
|
||||
_trigger_action("throw")
|
||||
|
||||
var throw_speed = 20.0
|
||||
var throw_up_speed = 5.0
|
||||
|
||||
Reference in New Issue
Block a user