Files
Brachistochrone/look_loader.gd
T
scorpion21211-cmd 72ca74c67f Initial commit
2026-10-03 23:29:41 -04:00

647 lines
19 KiB
GDScript

extends Node3D
## Player/Body look loader. PASTE 2026-09-12 22:54 — 1P always gun-out eye ride
## 8-way = yaw CreatedBody toward the stick + Walk/Run.
## No AnimationTree. Waist fold AFTER clip via SkeletonModifier3D.
const MALE_FBX := "res://character assets/All together/FBX/Humans_Master.fbx"
const FEMALE_FBX := "res://character assets/All together/FBX women/All together.fbx"
const CLIP_ALIASES := {
"Idle": ["Idle"],
"Walk": ["Walk"],
"Run": ["Run"],
"Run_Back": ["Run_Back", "RunBack"],
"Run_Left": ["Run_Left", "RunLeft"],
"Run_Right": ["Run_Right", "RunRight"],
"Jump": ["Jump"],
"Interact": ["Interact", "Pickup", "PickUp"],
"Idle_Gun": ["Idle_Gun", "Idle_Gun_Pointing", "Gun_Idle"],
"AimIdle": ["Idle_Gun_Pointing", "Idle_Gun", "Gun_Idle", "Aim_Idle"],
"AimWalk": ["Walk_Shoot", "WalkShoot", "Gun_Walk", "Walk_Gun", "Walk"],
"Walk_Shoot": ["Walk_Shoot", "WalkShoot", "Gun_Walk", "Walk"],
"AimRun": ["Run_Shoot", "RunShoot", "Gun_Run", "Run_Gun", "Run"],
"Run_Shoot": ["Run_Shoot", "RunShoot", "Run"],
"Gun_Shoot": ["Gun_Shoot", "Pistol_Shoot", "Shoot", "Idle_Gun_Shoot"],
"Idle_Gun_Shoot": ["Idle_Gun_Shoot", "Gun_Shoot", "Idle_Gun_Pointing"],
}
var current: Node3D = null
var anim: AnimationPlayer = null
var last_keep: Array[String] = []
var _logged_names: bool = false
var hide_head: bool = false
var show_pistol: bool = false
var _skel: Skeleton3D = null
var _bone_abdomen: int = -1
var _bone_torso: int = -1
var _bone_chest: int = -1
var _bone_head: int = -1
var _bone_rarm: int = -1
var _bone_larm: int = -1
var _bone_rfore: int = -1
var _bone_lfore: int = -1
var _aim_pitch: float = 0.0
var _aim_enabled: bool = false
var _logged_bones: bool = false
var _mod: SkeletonModifier3D = null
var _dbg: float = 0.0
var tree: AnimationTree = null
var _walk_bs: AnimationNodeBlendSpace2D = null
var _run_bs: AnimationNodeBlendSpace2D = null
var _aim_walk_bs: AnimationNodeBlendSpace2D = null
var _aim_run_bs: AnimationNodeBlendSpace2D = null
var _mode: String = ""
var _blend_pos: Vector2 = Vector2.ZERO
var _run_amt: float = 0.0
var _aiming: bool = false
var _fire_lock: float = 0.0
var _ride_fp: bool = false
var _fp_cam: Camera3D = null
var _fp_pivot: Node3D = null
var arm_axis_id: int = 2
class AimBend extends SkeletonModifier3D:
var host: Node
func _process_modification() -> void:
if host and host.has_method("_apply_waist_now"):
host._apply_waist_now()
func apply_look(appearance: Dictionary) -> AnimationPlayer:
_clear_old()
var sex := str(appearance.get("sex", "male"))
var path := MALE_FBX if sex == "male" else FEMALE_FBX
if not ResourceLoader.exists(path):
push_warning("Look file missing: " + path)
return null
var packed = load(path)
if packed == null:
return null
var inst: Node = packed.instantiate()
if not (inst is Node3D):
return null
current = inst as Node3D
current.name = "CreatedBody"
current.position = Vector3.ZERO
current.rotation_degrees = Vector3.ZERO
current.scale = Vector3.ONE * 1.3
add_child(current)
last_keep = [
str(appearance.get("head", "Casual_Head")),
str(appearance.get("body", "Casual_Body")),
str(appearance.get("legs", "Casual_Legs")),
str(appearance.get("feet", "Casual_Feet")),
]
anim = current.find_child("AnimationPlayer", true, false) as AnimationPlayer
if anim:
anim.speed_scale = 1.0
anim.root_motion_track = NodePath()
_loop_locomotion(anim)
print("Look clips: ", anim.get_animation_list())
_cache_skeleton()
_build_loco_tree()
_apply_keep(current, last_keep)
call_deferred("_apply_keep_deferred")
return anim
func _resolve_clip(logical: String) -> String:
if anim == null:
return ""
var names: Array = CLIP_ALIASES.get(logical, [logical])
var candidates: Array[String] = []
for n in names:
candidates.append("CharacterArmature|%s" % n)
candidates.append(str(n))
for clip in candidates:
if anim.has_animation(clip):
return clip
for clip in anim.get_animation_list():
for n in names:
if clip.ends_with(str(n)) or clip.contains(str(n)):
return clip
return ""
func play_clip(logical: String, speed_scale: float = 1.0) -> void:
if anim == null:
return
if tree:
tree.active = false
var clip := _resolve_clip(logical)
if clip.is_empty():
return
var a: Animation = anim.get_animation(clip)
if a:
var looped := logical in [
"Idle", "Walk", "Run", "Run_Back", "Run_Left", "Run_Right",
"Idle_Gun", "AimIdle", "AimWalk", "AimRun", "Walk_Shoot", "Run_Shoot"
]
a.loop_mode = Animation.LOOP_LINEAR if looped else Animation.LOOP_NONE
anim.speed_scale = speed_scale
if anim.current_animation != clip:
anim.play(clip)
elif not anim.is_playing():
anim.play(clip)
_apply_keep(current, last_keep)
func _loop_locomotion(player: AnimationPlayer) -> void:
for logical in [
"Idle", "Walk", "Run", "Run_Back", "Run_Left", "Run_Right",
"Idle_Gun", "AimIdle", "AimWalk", "AimRun", "Walk_Shoot", "Run_Shoot"
]:
var clip := _resolve_clip(logical)
if clip.is_empty():
continue
var a: Animation = player.get_animation(clip)
if a:
a.loop_mode = Animation.LOOP_LINEAR
func _clip_node(logical: String) -> AnimationNodeAnimation:
var n := AnimationNodeAnimation.new()
var clip := _resolve_clip(logical)
if clip.is_empty():
clip = _resolve_clip("Idle")
if clip.is_empty():
clip = logical
n.animation = StringName(clip)
n.resource_name = logical
return n
func _add_pt(bs: AnimationNodeBlendSpace2D, logical: String, pos: Vector2, label: String) -> void:
var node := _clip_node(logical)
bs.add_blend_point(node, pos)
var idx: int = bs.get_blend_point_count() - 1
if bs.has_method("set_blend_point_name"):
bs.call("set_blend_point_name", idx, StringName(label))
func _make_space(_idle: String, fwd: String, back: String, left: String, right: String) -> AnimationNodeBlendSpace2D:
var bs := AnimationNodeBlendSpace2D.new()
# Discrete = one whole clip. Interpolating Walk with Run_Left is clown shoes.
bs.blend_mode = AnimationNodeBlendSpace2D.BLEND_MODE_DISCRETE_CARRY
bs.min_space = Vector2(-1.0, -1.0)
bs.max_space = Vector2(1.0, 1.0)
bs.snap = Vector2(0.0, 0.0)
_add_pt(bs, fwd, Vector2(0.0, -1.0), "fwd")
_add_pt(bs, back, Vector2(0.0, 1.0), "back")
_add_pt(bs, left, Vector2(-1.0, 0.0), "left")
_add_pt(bs, right, Vector2(1.0, 0.0), "right")
_add_pt(bs, fwd, Vector2(-0.71, -0.71), "fwd_left")
_add_pt(bs, fwd, Vector2(0.71, -0.71), "fwd_right")
_add_pt(bs, back, Vector2(-0.71, 0.71), "back_left")
_add_pt(bs, back, Vector2(0.71, 0.71), "back_right")
return bs
func _build_loco_tree() -> void:
# No AnimationTree. Quaternius has no Walk_Left/Walk_Right — blending
# Walk with Run_Left is clown shoes. One clip per octant instead.
if tree and is_instance_valid(tree):
tree.queue_free()
tree = null
_walk_bs = null
_run_bs = null
_aim_walk_bs = null
_aim_run_bs = null
_mode = "idle"
_blend_pos = Vector2.ZERO
_run_amt = 0.0
print("Loco clips walk=", _resolve_clip("Walk"),
" left=", _resolve_clip("Run_Left"),
" right=", _resolve_clip("Run_Right"),
" back=", _resolve_clip("Run_Back"),
" run=", _resolve_clip("Run"),
" aimIdle=", _resolve_clip("AimIdle"))
func play_fire(aiming_now: bool = false) -> void:
var logical: String = "Idle_Gun_Shoot" if aiming_now else "Gun_Shoot"
play_clip(logical)
var clip := _resolve_clip(logical)
var dur: float = 0.26
if anim and not clip.is_empty() and anim.has_animation(clip):
dur = maxf(0.16, anim.get_animation(clip).length * 0.82)
_fire_lock = dur
func set_loco(strafe: float, back: float, run_amount: float, delta: float = 0.016, aiming: bool = false) -> void:
_aiming = aiming
if tree and tree.active:
tree.active = false
if _fire_lock > 0.0:
_fire_lock = maxf(_fire_lock - delta, 0.0)
if aiming:
_face_move(0.0, delta, 14.0)
return
if aiming:
_face_move(0.0, delta, 14.0)
var aim_stick := Vector2(clampf(strafe, -1.0, 1.0), clampf(back, -1.0, 1.0))
_blend_pos = aim_stick.normalized() if aim_stick.length() >= 0.16 else Vector2.ZERO
_run_amt = run_amount if aim_stick.length() >= 0.16 else 0.0
if aim_stick.length() < 0.16:
_mode = "aim_idle"
play_clip("AimIdle")
elif run_amount > 0.5:
_mode = "aim_run"
play_clip("AimRun")
else:
_mode = "aim_walk"
play_clip("AimWalk")
return
var target := Vector2(clampf(strafe, -1.0, 1.0), clampf(back, -1.0, 1.0))
if target.length() < 0.16:
_face_move(0.0, delta, 10.0)
_blend_pos = Vector2.ZERO
_run_amt = 0.0
if show_pistol or hide_head:
_mode = "idle_gun"
play_clip("Idle_Gun")
else:
_mode = "idle"
play_clip("Idle")
return
_blend_pos = target.normalized()
_run_amt = 1.0 if run_amount > 0.5 else 0.0
# S / SA / SD — backpedal, no 180 spin
if _blend_pos.y > 0.2:
_face_move(0.0, delta, 12.0)
_mode = "Run_Back"
play_clip("Run_Back")
return
# Pure A / D — sidestep, keep facing camera-forward
if absf(_blend_pos.x) >= 0.62 and absf(_blend_pos.y) <= 0.42:
_face_move(0.0, delta, 14.0)
var side: String = "Run_Left" if _blend_pos.x < 0.0 else "Run_Right"
_mode = side
play_clip(side)
return
# W / WA / WD — face the stick + Walk/Run
var yaw := atan2(-_blend_pos.x, maxf(-_blend_pos.y, 0.001))
yaw = clampf(yaw, deg_to_rad(-90.0), deg_to_rad(90.0))
_face_move(yaw, delta, 16.0)
var logical: String = "Run" if _run_amt > 0.5 else "Walk"
_mode = logical
var pace: float = 0.92 + _run_amt * 0.18 if logical == "Run" else 0.88 + target.length() * 0.14
play_clip(logical, pace)
func _face_move(yaw: float, delta: float, rate: float) -> void:
if current == null or not is_instance_valid(current):
return
current.rotation.y = lerp_angle(current.rotation.y, yaw, clampf(rate * delta, 0.0, 1.0))
func set_first_person(fp: bool) -> void:
hide_head = fp
print("Look 1P=", fp, " idle=Idle_Gun" if fp else " idle=Idle")
if current != null and is_instance_valid(current):
_apply_keep(current, last_keep)
func set_pistol_drawn(drawn: bool) -> void:
show_pistol = drawn
if current != null and is_instance_valid(current):
_apply_keep(current, last_keep)
func set_meshes_visible(v: bool) -> void:
if current == null:
return
if v:
_apply_keep(current, last_keep)
else:
_hide_all_visuals(current)
func set_aim_pitch(radians: float) -> void:
if not _aim_enabled:
_aim_pitch = 0.0
return
_aim_pitch = clampf(radians, deg_to_rad(-50.0), deg_to_rad(65.0))
func get_head_global_pos() -> Vector3:
if _skel == null or not is_instance_valid(_skel) or _bone_head < 0:
return Vector3.INF
var pose: Transform3D = _skel.get_bone_global_pose(_bone_head)
return _skel.to_global(pose.origin)
func set_fp_ride(cam: Camera3D, pivot: Node3D, on: bool) -> void:
_fp_cam = cam
_fp_pivot = pivot
_ride_fp = on and cam != null
func set_aim_enabled(on: bool) -> void:
_aim_enabled = on
if not on:
_aim_pitch = 0.0
if _mod:
_mod.active = on
func _process(delta: float) -> void:
_dbg += delta
if _dbg < 4.0:
return
_dbg = 0.0
print("Loco dbg mode=", _mode,
" pos=", snapped(_blend_pos, Vector2(0.01, 0.01)),
" run=", snappedf(_run_amt, 0.01),
" aim=", _aiming,
" player=", anim.current_animation if anim else "?")
if _aim_enabled:
print("AimFold on pitch=", snappedf(rad_to_deg(_aim_pitch), 0.1),
" abd=", _bone_name(_bone_abdomen),
" torso=", _bone_name(_bone_torso),
" chest=", _bone_name(_bone_chest),
" mod=", _mod != null, " active=", _mod.active if _mod else false)
func _cache_skeleton() -> void:
_skel = null
_bone_abdomen = -1
_bone_torso = -1
_bone_chest = -1
_bone_head = -1
_bone_rarm = -1
_bone_larm = -1
_bone_rfore = -1
_bone_lfore = -1
_mod = null
if current == null:
return
_skel = current.find_child("Skeleton3D", true, false) as Skeleton3D
if _skel == null:
push_warning("Look: no Skeleton3D on CreatedBody")
return
_bone_abdomen = _find_exact(["Abdomen", "mixamorig_Spine", "mixamorig:Spine", "Spine"])
_bone_torso = _find_exact(["Torso", "mixamorig_Spine1", "mixamorig:Spine1", "Spine1"])
_bone_chest = _find_exact(["Chest", "mixamorig_Spine2", "mixamorig:Spine2", "Spine2", "UpperChest"])
_bone_head = _find_exact(["Head", "mixamorig_Head", "mixamorig:Head"])
_bone_rarm = _find_limb(["mixamorig_RightArm", "mixamorig:RightArm", "RightArm", "Arm.R"])
_bone_larm = _find_limb(["mixamorig_LeftArm", "mixamorig:LeftArm", "LeftArm", "Arm.L"])
_bone_rfore = _find_limb(["mixamorig_RightForeArm", "mixamorig:RightForeArm", "RightForeArm", "ForeArm.R"])
_bone_lfore = _find_limb(["mixamorig_LeftForeArm", "mixamorig:LeftForeArm", "LeftForeArm", "ForeArm.L"])
_mod = AimBend.new()
_mod.name = "AimBend"
_mod.host = self
_mod.active = false
_skel.add_child(_mod)
if not _logged_bones:
_logged_bones = true
print("Look skeleton=", _skel.name,
" abd=", _bone_name(_bone_abdomen),
" torso=", _bone_name(_bone_torso),
" chest=", _bone_name(_bone_chest),
" head=", _bone_name(_bone_head),
" rarm=", _bone_name(_bone_rarm),
" larm=", _bone_name(_bone_larm),
" modifier=AimBend")
func _find_exact(names: Array[String]) -> int:
if _skel == null:
return -1
for bone_key in names:
var idx: int = _skel.find_bone(bone_key)
if idx >= 0:
var low := _skel.get_bone_name(idx).to_lower()
var want_head := "head" in bone_key.to_lower()
if "shoulder" in low or "arm" in low or "neck" in low:
continue
if "head" in low and not want_head:
continue
return idx
return -1
func _find_limb(names: Array[String]) -> int:
if _skel == null:
return -1
for bone_key in names:
var idx: int = _skel.find_bone(bone_key)
if idx >= 0:
return idx
for i in range(_skel.get_bone_count()):
var raw: String = _skel.get_bone_name(i)
var low: String = raw.to_lower()
for bone_key in names:
var want: String = bone_key.to_lower()
if low == want or low.contains(want):
return i
return -1
func _bone_name(idx: int) -> String:
if _skel == null or idx < 0:
return str(idx)
return "%s(%d)" % [_skel.get_bone_name(idx), idx]
func _apply_waist_now() -> void:
if _skel == null:
return
if _aim_enabled:
var pitch: float = -_aim_pitch
if hide_head:
# 1P: lean the whole torso so the gun and shoes enter the lens.
_set_fold(_bone_abdomen, pitch * 0.40)
_set_fold(_bone_torso, pitch * 0.22)
_set_fold(_bone_chest, pitch * 0.08)
var axis: Vector3 = Vector3.FORWARD
match arm_axis_id:
0:
axis = Vector3.RIGHT
1:
axis = Vector3.UP
_:
axis = Vector3.FORWARD
var arm_amt: float = 0.22
if _aim_pitch > 0.0:
arm_amt = 0.36
_set_fold_axis(_bone_rarm, pitch * arm_amt, axis)
_set_fold_axis(_bone_larm, pitch * arm_amt * 0.55, axis)
else:
_set_fold(_bone_abdomen, pitch * 0.42)
_set_fold(_bone_torso, pitch * 0.38)
_set_fold(_bone_chest, pitch * 0.28)
_snap_fp_eye()
func _snap_fp_eye() -> void:
if not _ride_fp or _fp_pivot == null or _skel == null or _bone_head < 0:
return
var pose: Transform3D = _skel.get_bone_global_pose(_bone_head)
var xf: Transform3D = _skel.global_transform * pose
var body: Node3D = _fp_pivot.get_parent() as Node3D
var face := Vector3.FORWARD
var up := Vector3.UP
if body:
face = -body.global_transform.basis.z
face.y = 0.0
if face.length_squared() < 0.0001:
face = -body.global_transform.basis.z
face = face.normalized()
up = body.global_transform.basis.y.normalized()
else:
face = xf.basis.z.normalized()
up = xf.basis.y.normalized()
_fp_pivot.global_position = xf.origin + face * 0.03 + up * 0.12
func _set_fold(idx: int, angle: float) -> void:
_set_fold_axis(idx, angle, Vector3.RIGHT)
func _set_fold_axis(idx: int, angle: float, axis: Vector3) -> void:
if idx < 0 or _skel == null:
return
if absf(angle) < 0.0005:
return
var pose: Quaternion = _skel.get_bone_pose_rotation(idx)
_skel.set_bone_pose_rotation(idx, pose * Quaternion(axis.normalized(), angle))
func cycle_arm_axis() -> void:
arm_axis_id = (arm_axis_id + 1) % 3
print("ARM AXIS ", ["X (RIGHT)", "Y (UP)", "Z (FORWARD)"][arm_axis_id])
func _apply_keep_deferred() -> void:
if current != null and is_instance_valid(current):
_apply_keep(current, last_keep)
if _skel == null:
_cache_skeleton()
func _clear_old() -> void:
current = null
anim = null
if tree and is_instance_valid(tree):
tree.queue_free()
tree = null
_walk_bs = null
_run_bs = null
_aim_walk_bs = null
_aim_run_bs = null
_mode = ""
_blend_pos = Vector2.ZERO
_run_amt = 0.0
_aiming = false
_fire_lock = 0.0
_skel = null
_mod = null
_bone_abdomen = -1
_bone_torso = -1
_bone_chest = -1
_bone_head = -1
_bone_rarm = -1
_bone_larm = -1
_bone_rfore = -1
_bone_lfore = -1
_logged_bones = false
_aim_enabled = false
_aim_pitch = 0.0
_ride_fp = false
_fp_cam = null
_fp_pivot = null
for child in get_children():
child.hide()
child.queue_free()
func _hide_all_visuals(root: Node) -> void:
var stack: Array[Node] = [root]
while stack.size() > 0:
var n: Node = stack.pop_back()
if n is GeometryInstance3D:
n.visible = false
for c in n.get_children():
stack.append(c)
func _apply_keep(root: Node, keep: Array[String]) -> void:
if root == null or keep.is_empty():
return
var shown: PackedStringArray = []
var hidden: PackedStringArray = []
var stack: Array[Node] = [root]
while stack.size() > 0:
var n: Node = stack.pop_back()
if n is GeometryInstance3D:
var lab := _label(n)
var keep_this := _matches_keep(n, keep) and not _is_banned(lab)
if show_pistol and _is_pistol_label(lab):
keep_this = true
if hide_head and _is_head_label(lab):
keep_this = false
n.visible = keep_this
if keep_this:
shown.append(_label(n))
else:
hidden.append(_label(n))
for c in n.get_children():
stack.append(c)
if not _logged_names:
_logged_names = true
print("Look keep=", keep)
print("Look SHOWN (", shown.size(), "): ", shown)
print("Look HIDDEN (", hidden.size(), "): ", hidden)
func _label(n: Node) -> String:
var label := n.name
if n is MeshInstance3D:
var mi := n as MeshInstance3D
if mi.mesh and not mi.mesh.resource_name.is_empty():
label = "%s|%s" % [n.name, mi.mesh.resource_name]
return label
func _matches_keep(n: Node, keep: Array[String]) -> bool:
var labels: Array[String] = [n.name]
if n is MeshInstance3D:
var mi := n as MeshInstance3D
if mi.mesh and not mi.mesh.resource_name.is_empty():
labels.append(mi.mesh.resource_name)
for label in labels:
for k in keep:
if k.is_empty():
continue
if label == k or label.begins_with(k):
return true
return false
func _is_head_label(mesh_name: String) -> bool:
var low := mesh_name.to_lower()
if "pistol" in low:
return false
return ("_head" in low) or low.ends_with("head") or ("hair" in low) or ("hat" in low) or ("neck" in low)
func _is_pistol_label(mesh_name: String) -> bool:
return mesh_name.contains("Pistol") or mesh_name.contains("pistol")
func _is_banned(mesh_name: String) -> bool:
var n := mesh_name
return (
n.begins_with("Horse")
or n.contains("Horse")
or n.begins_with("King")
or n.begins_with("Witch")
or n.begins_with("Medieval")
or n.begins_with("Beach")
or n.begins_with("Sword")
)