Files
Papay-Shooter/characters/humanoid_model.gd
T
2026-08-02 02:20:02 -04:00

607 lines
20 KiB
GDScript

extends Node3D
class_name HumanoidModel
@export var color: Color = Color.WHITE
@export var shadows_only: bool = false
# Internal Pivots
var root_pivot: Node3D
var torso: MeshInstance3D
var head_pivot: Node3D
var head: MeshInstance3D
var upper_arm_l_pivot: Node3D
var upper_arm_l: MeshInstance3D
var lower_arm_l_pivot: Node3D
var lower_arm_l: MeshInstance3D
var upper_arm_r_pivot: Node3D
var upper_arm_r: MeshInstance3D
var lower_arm_r_pivot: Node3D
var lower_arm_r: MeshInstance3D
var hand_r_pivot: Node3D
var thigh_l_pivot: Node3D
var thigh_l: MeshInstance3D
var calf_l_pivot: Node3D
var calf_l: MeshInstance3D
var thigh_r_pivot: Node3D
var thigh_r: MeshInstance3D
var calf_r_pivot: Node3D
var calf_r: MeshInstance3D
# Animation State Variables
var current_state: String = "idle"
var movement_speed: float = 0.0
var _anim_time: float = 0.0
var _locomotion_strafe: float = 0.0
var _locomotion_fwd: float = 0.0
var _brake_left: float = 0.0
var _brake_fwd: float = 1.0
var _brake_strength: float = 0.0
var _brake_weight: float = 0.0
var _brake_armed := true
const BRAKE_DURATION := 0.46
func _ready() -> void:
rotation_degrees.y = 180
var mat := StandardMaterial3D.new()
mat.albedo_color = color
mat.roughness = 0.8
var shadow_setting = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY if shadows_only else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
root_pivot = Node3D.new()
add_child(root_pivot)
# Torso
torso = MeshInstance3D.new()
var t_mesh = BoxMesh.new()
t_mesh.size = Vector3(0.4, 0.7, 0.25)
t_mesh.material = mat
torso.mesh = t_mesh
torso.position = Vector3(0, 1.15, 0)
torso.cast_shadow = shadow_setting
root_pivot.add_child(torso)
# Head
head_pivot = Node3D.new()
head_pivot.position = Vector3(0, 1.5, 0)
root_pivot.add_child(head_pivot)
head = MeshInstance3D.new()
var h_mesh = BoxMesh.new()
h_mesh.size = Vector3(0.25, 0.25, 0.25)
h_mesh.material = mat
head.mesh = h_mesh
head.position = Vector3(0, 0.15, 0) # Offset from neck
head.cast_shadow = shadow_setting
head_pivot.add_child(head)
# Arms (Upper: 0.35 length, Lower: 0.35 length)
var ua_mesh = BoxMesh.new()
ua_mesh.size = Vector3(0.12, 0.35, 0.12)
ua_mesh.material = mat
var la_mesh = BoxMesh.new()
la_mesh.size = Vector3(0.1, 0.35, 0.1)
la_mesh.material = mat
# Left Arm
upper_arm_l_pivot = Node3D.new()
upper_arm_l_pivot.position = Vector3(-0.28, 1.45, 0) # Shoulder
root_pivot.add_child(upper_arm_l_pivot)
upper_arm_l = MeshInstance3D.new()
upper_arm_l.mesh = ua_mesh
upper_arm_l.position = Vector3(0, -0.175, 0)
upper_arm_l.cast_shadow = shadow_setting
upper_arm_l_pivot.add_child(upper_arm_l)
lower_arm_l_pivot = Node3D.new()
lower_arm_l_pivot.position = Vector3(0, -0.35, 0) # Elbow
upper_arm_l_pivot.add_child(lower_arm_l_pivot)
lower_arm_l = MeshInstance3D.new()
lower_arm_l.mesh = la_mesh
lower_arm_l.position = Vector3(0, -0.175, 0)
lower_arm_l.cast_shadow = shadow_setting
lower_arm_l_pivot.add_child(lower_arm_l)
# Right Arm
upper_arm_r_pivot = Node3D.new()
upper_arm_r_pivot.position = Vector3(0.28, 1.45, 0) # Shoulder
root_pivot.add_child(upper_arm_r_pivot)
upper_arm_r = MeshInstance3D.new()
upper_arm_r.mesh = ua_mesh
upper_arm_r.position = Vector3(0, -0.175, 0)
upper_arm_r.cast_shadow = shadow_setting
upper_arm_r_pivot.add_child(upper_arm_r)
lower_arm_r_pivot = Node3D.new()
lower_arm_r_pivot.position = Vector3(0, -0.35, 0) # Elbow
upper_arm_r_pivot.add_child(lower_arm_r_pivot)
lower_arm_r = MeshInstance3D.new()
lower_arm_r.mesh = la_mesh
lower_arm_r.position = Vector3(0, -0.175, 0)
lower_arm_r.cast_shadow = shadow_setting
lower_arm_r_pivot.add_child(lower_arm_r)
# Right hand: the wrist joint at the far end of the forearm. The
# third-person weapon hangs here so it travels with the arm instead of
# floating in front of the chest.
hand_r_pivot = Node3D.new()
hand_r_pivot.position = Vector3(0, -0.35, 0)
lower_arm_r_pivot.add_child(hand_r_pivot)
# Legs (Thigh: 0.45 length, Calf: 0.45 length)
var t_leg_mesh = BoxMesh.new()
t_leg_mesh.size = Vector3(0.15, 0.45, 0.15)
t_leg_mesh.material = mat
var c_leg_mesh = BoxMesh.new()
c_leg_mesh.size = Vector3(0.13, 0.45, 0.13)
c_leg_mesh.material = mat
# Left Leg
thigh_l_pivot = Node3D.new()
thigh_l_pivot.position = Vector3(-0.12, 0.85, 0) # Hip (raised slightly for 0.45+0.45=0.9 total)
root_pivot.add_child(thigh_l_pivot)
thigh_l = MeshInstance3D.new()
thigh_l.mesh = t_leg_mesh
thigh_l.position = Vector3(0, -0.225, 0)
thigh_l.cast_shadow = shadow_setting
thigh_l_pivot.add_child(thigh_l)
calf_l_pivot = Node3D.new()
calf_l_pivot.position = Vector3(0, -0.45, 0) # Knee
thigh_l_pivot.add_child(calf_l_pivot)
calf_l = MeshInstance3D.new()
calf_l.mesh = c_leg_mesh
calf_l.position = Vector3(0, -0.225, 0)
calf_l.cast_shadow = shadow_setting
calf_l_pivot.add_child(calf_l)
# Right Leg
thigh_r_pivot = Node3D.new()
thigh_r_pivot.position = Vector3(0.12, 0.85, 0) # Hip
root_pivot.add_child(thigh_r_pivot)
thigh_r = MeshInstance3D.new()
thigh_r.mesh = t_leg_mesh
thigh_r.position = Vector3(0, -0.225, 0)
thigh_r.cast_shadow = shadow_setting
thigh_r_pivot.add_child(thigh_r)
calf_r_pivot = Node3D.new()
calf_r_pivot.position = Vector3(0, -0.45, 0) # Knee
thigh_r_pivot.add_child(calf_r_pivot)
calf_r = MeshInstance3D.new()
calf_r.mesh = c_leg_mesh
calf_r.position = Vector3(0, -0.225, 0)
calf_r.cast_shadow = shadow_setting
calf_r_pivot.add_child(calf_r)
# Cel-shade + ink outline the blocky fallback body so it matches skinned
# player models (shadows-only bodies skip it: outlines would cast shadows)
if not shadows_only:
LevelMaterials.apply_toon_recursive(root_pivot, 0.004)
## Skin System
var current_skin: PlayerSkin
var skin_model: Node3D
func apply_skin(skin: PlayerSkin) -> void:
current_skin = skin
if skin.model_path != "" and skin.model_path != null:
var loaded = load(skin.model_path)
if loaded:
_set_procedural_visible(false)
if skin_model:
skin_model.queue_free()
skin_model = null
skin_model = loaded.instantiate()
if skin_model:
skin_model.name = "SkinModel"
# Fix orientation: Blender Z-up to Godot Y-up
skin_model.rotation_degrees = Vector3(0, 0, 0)
# Scale down if needed (Blender models can be large)
skin_model.scale = Vector3(1, 1, 1)
add_child(skin_model)
print("HumanoidModel: applied skin '%s'" % skin.skin_name)
return
_set_procedural_visible(true)
if skin_model:
skin_model.queue_free()
skin_model = null
_apply_color(skin.color_tint)
func _set_procedural_visible(on: bool) -> void:
for part in [torso, head, upper_arm_l, lower_arm_l, upper_arm_r, lower_arm_r, thigh_l, calf_l, thigh_r, calf_r]:
if part:
part.visible = on
## Show/hide the model to its OWNER (third-person toggle). off = shadows-only so
## the local first-person camera doesn't see the body; on = fully visible.
func set_owner_visible(on: bool) -> void:
var mode := GeometryInstance3D.SHADOW_CASTING_SETTING_ON if on \
else GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
_set_owner_shadow_recursive(self, mode)
func _set_owner_shadow_recursive(node: Node, mode: int) -> void:
if node is GeometryInstance3D:
node.cast_shadow = mode
for child in node.get_children():
_set_owner_shadow_recursive(child, mode)
func _apply_color(col: Color) -> void:
var mat = StandardMaterial3D.new()
mat.albedo_color = col
mat.roughness = 0.8
for part in [torso, upper_arm_l, lower_arm_l, upper_arm_r, lower_arm_r, thigh_l, calf_l, thigh_r, calf_r]:
if part and part.mesh:
part.mesh.material = mat
if head and head.mesh:
var hmat = StandardMaterial3D.new()
hmat.albedo_color = Color(0.95, 0.85, 0.78)
hmat.roughness = 0.8
head.mesh.material = hmat
# Re-toon after the material swap (fresh StandardMaterials above)
if not shadows_only:
LevelMaterials.apply_toon_recursive(root_pivot, 0.004)
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
var previous_speed := movement_speed
var slowdown := previous_speed - speed
if state in ["ground", "idle"]:
if speed > 4.5 and slowdown < 0.05:
_brake_armed = true
if _brake_armed and previous_speed > 4.5 and slowdown > 0.12:
_brake_left = BRAKE_DURATION
_brake_strength = clampf(previous_speed / 10.0, 0.55, 1.0)
_brake_armed = false
if absf(_locomotion_fwd) > 0.15:
_brake_fwd = signf(_locomotion_fwd)
elif state not in ["ground", "idle"]:
_brake_left = 0.0
_brake_armed = true
current_state = state
movement_speed = speed
if is_crouching and current_state == "ground":
current_state = "crouch"
func set_locomotion(strafe: float, fwd: float, _ads: float) -> void:
_locomotion_strafe = clampf(strafe, -1.0, 1.0)
_locomotion_fwd = clampf(fwd, -1.0, 1.0)
func get_jet_socket_world_positions() -> Array[Vector3]:
if not root_pivot:
return []
return [
upper_arm_l_pivot.global_position \
+ root_pivot.global_basis * Vector3(-0.04, -0.02, -0.16),
upper_arm_r_pivot.global_position \
+ root_pivot.global_basis * Vector3(0.04, -0.02, -0.16),
calf_l_pivot.to_global(Vector3(0.0, -0.35, -0.045)),
calf_r_pivot.to_global(Vector3(0.0, -0.35, -0.045)),
]
var is_holding_weapon: bool = false
func set_weapon(script_path: String) -> void:
# Clear existing weapons (older builds parented them to root_pivot)
for parent in [root_pivot, hand_r_pivot]:
if not parent:
continue
for child in parent.get_children():
if child.has_meta("is_third_person_weapon"):
child.queue_free()
if script_path == "":
is_holding_weapon = false
return
is_holding_weapon = true
var script = load(script_path)
if not script:
return
var w = script.new()
w.name = "ThirdPersonWeapon"
w.set_meta("is_third_person_weapon", true)
# Connect to ready so we can override the weapon's own _ready() calls
w.ready.connect(func():
w.set_process(false)
w.set_process_input(false)
if shadows_only:
_set_shadows_recursive(w)
else:
# Toon shading only — FBX weapon normals tear inverted-hull outlines
LevelMaterials.apply_toon_recursive(w, 0.0)
# Undo the first-person viewmodel placement from _build_model()
_seat_weapon_in_hand(w)
)
# Riding the hand keeps the gun with the arm; _process re-aims it forward.
(hand_r_pivot if hand_r_pivot else root_pivot).add_child(w)
## The muzzle of the gun actually in this character's hand — see
## SkinnedPlayerModel.get_muzzle_node() for why world effects must use it.
func get_muzzle_node() -> Node3D:
var holder := hand_r_pivot if hand_r_pivot else root_pivot
if not holder:
return null
for child in holder.get_children():
if child.has_meta("is_third_person_weapon"):
if "muzzle_flash" in child and child.muzzle_flash:
return child.muzzle_flash
return child as Node3D
return null
## Slide the weapon along its own barrel axis so a plausible grip point — not
## whatever origin the artist left the model at — ends up in the fist, and turn
## it to face the way the body faces (the model is yawed 180° in _ready, so the
## gun's -Z muzzle axis has to point along the model's local +Z).
func _seat_weapon_in_hand(w: Node3D) -> void:
w.rotation_degrees = Vector3(0, 180, 0)
w.position = Vector3.ZERO
var min_t := INF # most negative along the barrel = stock end
var max_t := -INF # most positive = muzzle end
for mi in w.find_children("*", "MeshInstance3D", true, false):
if not mi.mesh:
continue
# mi.get_aabb(), NOT mi.mesh.get_aabb(): the FBX gun parts are skinned and
# the Mesh resource still carries huge bind-pose bounds.
var xf: Transform3D = w.global_transform.affine_inverse() * mi.global_transform
var aabb: AABB = mi.get_aabb()
for i in 8:
var t: float = (xf * aabb.get_endpoint(i)).dot(Vector3(0, 0, -1))
min_t = minf(min_t, t)
max_t = maxf(max_t, t)
if min_t > max_t:
return
# ~a third back from the muzzle is where a pistol grip sits on every gun in
# the set, which leaves real length of weapon behind the hand.
var grip_at := min_t + (max_t - min_t) * 0.32
w.position = Vector3(0, 0, -grip_at)
func _set_shadows_recursive(node: Node) -> void:
if node is GeometryInstance3D:
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
for child in node.get_children():
_set_shadows_recursive(child)
func _process(delta: float) -> void:
var brake_target := 0.0
if _brake_left > 0.0 and current_state in ["ground", "idle"]:
_brake_left = maxf(0.0, _brake_left - delta)
var progress := 1.0 - _brake_left / BRAKE_DURATION
var envelope := smoothstep(0.0, 1.0, progress / 0.30) \
if progress < 0.30 else \
1.0 - smoothstep(0.0, 1.0, (progress - 0.30) / 0.70)
brake_target = envelope * _brake_strength
_brake_weight = lerpf(
_brake_weight, brake_target,
1.0 - exp((-22.0 if brake_target > _brake_weight else -10.0) * delta))
var anim_speed = 1.0
if current_state == "ground" and movement_speed > 1.0:
anim_speed = movement_speed * 0.4
_anim_time += delta * anim_speed
# Target values
var t_root_pos := Vector3.ZERO
var t_root_rot := Vector3.ZERO
var t_upper_arm_l_rot := Vector3.ZERO
var t_upper_arm_r_rot := Vector3.ZERO
var t_lower_arm_l_rot := Vector3.ZERO
var t_lower_arm_r_rot := Vector3.ZERO
var t_thigh_l_rot := Vector3.ZERO
var t_thigh_r_rot := Vector3.ZERO
var t_calf_l_rot := Vector3.ZERO
var t_calf_r_rot := Vector3.ZERO
match current_state:
"ground", "idle":
if movement_speed > 1.0:
# Run cycle
var reverse_cycle := -1.0 if _locomotion_fwd < -0.12 else 1.0
var swing = sin(_anim_time * 5.0) * reverse_cycle
# Hips
t_thigh_l_rot.x = -swing * 1.6
t_thigh_r_rot.x = swing * 1.6
# Knees bend backwards (positive X rotation) when leg swings back
# We add a bit of bend even when moving forward so it's not stiff
t_calf_l_rot.x = max(0.1, swing * 1.6 + 0.3)
t_calf_r_rot.x = max(0.1, -swing * 1.6 + 0.3)
# Arms
t_upper_arm_l_rot.x = swing * 1.0
t_upper_arm_r_rot.x = -swing * 1.0
# Elbows bend forwards (negative X rotation)
t_lower_arm_l_rot.x = -1.2 + max(0.0, -swing * 0.5)
t_lower_arm_r_rot.x = -1.2 + max(0.0, swing * 0.5)
# Bounce slightly
t_root_pos.y = abs(cos(_anim_time * 5.0)) * 0.1
# Directional silhouette: reverse travel moves the shoulders
# behind the hips instead of reusing the forward sprint lean.
t_root_rot.x = deg_to_rad(10.0) if _locomotion_fwd < -0.12 \
else deg_to_rad(-8.0)
t_root_rot.z = deg_to_rad(-7.0) * _locomotion_strafe
else:
# Idle
var breath = sin(_anim_time * 2.0)
t_root_pos.y = breath * 0.02
t_upper_arm_l_rot.z = 0.05 + breath * 0.01
t_upper_arm_r_rot.z = -0.05 - breath * 0.01
t_lower_arm_l_rot.x = -0.1
t_lower_arm_r_rot.x = -0.1
t_calf_l_rot.x = 0.05
t_calf_r_rot.x = 0.05
"crouch":
t_root_pos.y = -0.5
# Thighs forward, calves back
t_thigh_l_rot.x = -1.0
t_thigh_r_rot.x = -1.0
t_calf_l_rot.x = 1.0
t_calf_r_rot.x = 1.0
# Arms bent defensively
t_upper_arm_l_rot.x = -0.5
t_upper_arm_r_rot.x = -0.5
t_lower_arm_l_rot.x = -1.2
t_lower_arm_r_rot.x = -1.2
"slide":
t_root_pos.y = -0.6
t_root_rot.x = deg_to_rad(-45) # Lean back
# Legs forward
t_thigh_l_rot.x = deg_to_rad(-60)
t_thigh_r_rot.x = deg_to_rad(-30)
t_calf_l_rot.x = 0.1
t_calf_r_rot.x = 0.1
# Arms back
t_upper_arm_l_rot.x = deg_to_rad(60)
t_upper_arm_r_rot.x = deg_to_rad(60)
t_lower_arm_l_rot.x = -0.2
t_lower_arm_r_rot.x = -0.2
"air":
# Falling / Jumping
t_thigh_l_rot.x = -0.4
t_thigh_r_rot.x = 0.2
t_calf_l_rot.x = 0.5
t_calf_r_rot.x = 0.1
t_upper_arm_l_rot.x = -0.8
t_upper_arm_r_rot.x = -0.8
t_upper_arm_l_rot.z = 0.5
t_upper_arm_r_rot.z = -0.5
t_lower_arm_l_rot.x = -0.5
t_lower_arm_r_rot.x = -0.5
"wall_run":
t_root_rot.z = deg_to_rad(-15)
var cycle = sin(_anim_time * 25.0)
t_thigh_l_rot.x = cycle * 0.8
t_thigh_r_rot.x = -cycle * 0.8
t_calf_l_rot.x = max(0.1, -cycle * 1.0 + 0.4)
t_calf_r_rot.x = max(0.1, cycle * 1.0 + 0.4)
t_upper_arm_l_rot.x = -cycle * 0.8
t_upper_arm_r_rot.x = cycle * 0.8
t_lower_arm_l_rot.x = -1.0
t_lower_arm_r_rot.x = -1.0
"wall_cling":
# Spiderman hanging
t_thigh_l_rot.x = deg_to_rad(-80)
t_thigh_r_rot.x = deg_to_rad(-40)
t_calf_l_rot.x = deg_to_rad(80)
t_calf_r_rot.x = deg_to_rad(40)
t_upper_arm_l_rot.x = deg_to_rad(-140)
t_upper_arm_r_rot.x = deg_to_rad(-140)
t_lower_arm_l_rot.x = deg_to_rad(-40)
t_lower_arm_r_rot.x = deg_to_rad(-40)
"grapple":
t_upper_arm_l_rot.x = deg_to_rad(-160)
t_lower_arm_l_rot.x = 0.0
t_upper_arm_r_rot.x = deg_to_rad(20)
t_lower_arm_r_rot.x = deg_to_rad(-90)
t_thigh_l_rot.x = deg_to_rad(-30)
t_calf_l_rot.x = deg_to_rad(30)
t_thigh_r_rot.x = deg_to_rad(20)
t_calf_r_rot.x = deg_to_rad(10)
"dash":
t_root_rot.x = deg_to_rad(-38)
t_upper_arm_l_rot.x = deg_to_rad(-170)
t_lower_arm_l_rot.x = 0.0
t_upper_arm_r_rot.x = deg_to_rad(-170)
t_lower_arm_r_rot.x = 0.0
t_thigh_l_rot.x = deg_to_rad(20)
t_calf_l_rot.x = 0.0
t_thigh_r_rot.x = deg_to_rad(20)
t_calf_r_rot.x = 0.0
"death":
t_root_rot.x = deg_to_rad(-90)
t_root_pos.y = -0.5
t_upper_arm_l_rot.x = deg_to_rad(170)
t_lower_arm_l_rot.x = 0.0
t_upper_arm_r_rot.x = deg_to_rad(170)
t_lower_arm_r_rot.x = 0.0
t_thigh_l_rot.x = deg_to_rad(-10)
t_calf_l_rot.x = 0.0
t_thigh_r_rot.x = deg_to_rad(10)
t_calf_r_rot.x = 0.0
# Blend a single catching step over the locomotion result. It rises quickly
# and releases over the same span as the run-to-idle crossfade, preserving
# momentum without freezing the whole mannequin in an exaggerated plant.
if current_state in ["ground", "idle"] and _brake_weight > 0.001:
var travel_sign := -1.0 if _brake_fwd < 0.0 else 1.0
t_root_pos.y = lerpf(t_root_pos.y, -0.025, _brake_weight)
t_root_rot.x = lerp_angle(
t_root_rot.x, deg_to_rad(8.0) * travel_sign, _brake_weight)
t_thigh_r_rot.x = lerp_angle(
t_thigh_r_rot.x, deg_to_rad(-24.0) * travel_sign, _brake_weight)
t_calf_r_rot.x = lerp_angle(
t_calf_r_rot.x, deg_to_rad(12.0) * travel_sign, _brake_weight)
t_thigh_l_rot.x = lerp_angle(
t_thigh_l_rot.x, deg_to_rad(8.0) * travel_sign, _brake_weight)
t_calf_l_rot.x = lerp_angle(
t_calf_l_rot.x, deg_to_rad(14.0), _brake_weight)
# Override right arm (and left) if holding a weapon
if is_holding_weapon and current_state != "death":
# Right arm holds the grip: elbow tucked at the ribs, forearm level so
# the fist (and the weapon parented to it) sits chest-high just in
# front of the torso rather than out at arm's length.
t_upper_arm_r_rot.x = -0.15
t_upper_arm_r_rot.z = -0.10
t_lower_arm_r_rot.x = -1.45
# Left arm crosses over to the handguard: up, in, and a little further
# forward than the grip hand.
t_upper_arm_l_rot.x = -0.55
t_upper_arm_l_rot.z = 0.55
t_lower_arm_l_rot.x = -1.0
var lerp_speed := 1.0 - exp(-14.0 * delta)
root_pivot.position = root_pivot.position.lerp(t_root_pos, lerp_speed)
root_pivot.rotation = _lerp_vec3(root_pivot.rotation, t_root_rot, lerp_speed)
upper_arm_l_pivot.rotation = _lerp_vec3(upper_arm_l_pivot.rotation, t_upper_arm_l_rot, lerp_speed)
upper_arm_r_pivot.rotation = _lerp_vec3(upper_arm_r_pivot.rotation, t_upper_arm_r_rot, lerp_speed)
lower_arm_l_pivot.rotation = _lerp_vec3(lower_arm_l_pivot.rotation, t_lower_arm_l_rot, lerp_speed)
lower_arm_r_pivot.rotation = _lerp_vec3(lower_arm_r_pivot.rotation, t_lower_arm_r_rot, lerp_speed)
thigh_l_pivot.rotation = _lerp_vec3(thigh_l_pivot.rotation, t_thigh_l_rot, lerp_speed)
thigh_r_pivot.rotation = _lerp_vec3(thigh_r_pivot.rotation, t_thigh_r_rot, lerp_speed)
calf_l_pivot.rotation = _lerp_vec3(calf_l_pivot.rotation, t_calf_l_rot, lerp_speed)
calf_r_pivot.rotation = _lerp_vec3(calf_r_pivot.rotation, t_calf_r_rot, lerp_speed)
# The weapon rides the hand for position, but its AIM belongs to the body:
# cancel the arm chain's rotation so the barrel stays pointed where the
# player faces instead of swinging around with the arm animation.
if hand_r_pivot:
hand_r_pivot.global_basis = root_pivot.global_basis
func _lerp_vec3(a: Vector3, b: Vector3, t: float) -> Vector3:
return Vector3(
lerp_angle(a.x, b.x, t),
lerp_angle(a.y, b.y, t),
lerp_angle(a.z, b.z, t)
)