635 lines
21 KiB
GDScript
635 lines
21 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 == "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)
|
|
)
|