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 == "stop" and current_state != "stop": _brake_left = BRAKE_DURATION _brake_strength = clampf(maxf(previous_speed, speed) / 10.0, 0.55, 1.0) if absf(_locomotion_fwd) > 0.15: _brake_fwd = signf(_locomotion_fwd) elif 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", "stop"]: _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", "stop"]: _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 "stop": # Neutral base under the catching-step overlay below. Keeping the run # cycle out of this state prevents residual speed from accelerating it. t_root_rot.x = deg_to_rad(8.0) t_thigh_r_rot.x = deg_to_rad(-24.0) t_calf_r_rot.x = deg_to_rad(12.0) t_thigh_l_rot.x = deg_to_rad(8.0) t_calf_l_rot.x = deg_to_rad(14.0) "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 "air_alt": # Mirror the simple fallback silhouette to match the alternating # offline-authored variants used by skinned motion-lab characters. t_thigh_l_rot.x = 0.2 t_thigh_r_rot.x = -0.4 t_calf_l_rot.x = 0.1 t_calf_r_rot.x = 0.5 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", "stop"] 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) )