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
@@ -49,6 +49,7 @@ const CLIP_FALLBACKS := {
|
||||
"PistolIdle": ["PistolIdle", "Idle"],
|
||||
"PistolShoot": ["PistolShoot"],
|
||||
"PistolReload": ["PistolReload"],
|
||||
"Throw": ["Throw", "Hit"],
|
||||
}
|
||||
|
||||
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.
|
||||
const BLEND_TIMES := {
|
||||
"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,
|
||||
}
|
||||
|
||||
## 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 animation_player: AnimationPlayer
|
||||
var loaded: bool = false
|
||||
@@ -222,6 +231,24 @@ func set_dancing(on: bool) -> void:
|
||||
_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
|
||||
## controller each frame with either local or network-synced state.
|
||||
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 slide: float = 0.0 # 0 .. 1 slide blend
|
||||
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 weapon_held: bool = false
|
||||
|
||||
@@ -476,12 +505,42 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_resolve()
|
||||
|
||||
_apply_lean(skel)
|
||||
if absf(aim_pitch) > 0.01:
|
||||
_apply_aim_pitch(skel)
|
||||
if slide > 0.01:
|
||||
_apply_slide(skel)
|
||||
if absf(wall) > 0.01:
|
||||
_apply_wall_lean(skel)
|
||||
if weapon_held:
|
||||
_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.
|
||||
func _apply_lean(skel: Skeleton3D) -> void:
|
||||
@@ -527,6 +586,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
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)
|
||||
# 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 (attached to the hand) at the side, so we leave it alone. On ADS we
|
||||
|
||||
Reference in New Issue
Block a user