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") )