game: sci-fi clip arms skin the fists slot — FAB Sci Fi Hands rig (Idle_Fight stance, Attack_01-04 punches, Walk/Run locomotion) shown instead of the procedural mixamo rig when bare hands equipped; other weapons unchanged. Headless probe: dev/probe_scifi.gd

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-30 12:27:23 +10:00
parent c2fad7eb53
commit 43c8c99b1f
3 changed files with 118 additions and 0 deletions

Binary file not shown.

44
game/dev/probe_scifi.gd Normal file
View File

@ -0,0 +1,44 @@
extends SceneTree
## Verify the sci-fi clip arms wire-up: found, animated, toggled by the fists slot.
## godot --headless --path game --script dev/probe_scifi.gd
func _initialize() -> void:
var main: Node = (load("res://main.tscn") as PackedScene).instantiate()
get_root().add_child(main)
await process_frame
await process_frame
var p := _find(main, "Player") as Player
if p == null:
print("FAIL no player"); quit(); return
var vm := p.viewmodel
var arms: Node3D = p.camera.get_node_or_null("SciFiArms")
print("arms found: ", arms != null)
if arms == null: quit(); return
var ap := _anim(arms)
print("anims: ", ap.get_animation_list() if ap else [])
vm.equip(p.loadout()[0]) # fists
await process_frame
print("fists -> arms visible: ", arms.visible, " rig visible: ", vm.get_node("Rig").visible, " playing: ", ap.current_animation)
vm.start_swing()
await process_frame
print("swing -> playing: ", ap.current_animation)
vm.equip(p.loadout()[1]) # a weapon
await process_frame
print("weapon -> arms visible: ", arms.visible, " rig visible: ", vm.get_node("Rig").visible)
print("PROBE OK")
quit()
func _anim(n: Node) -> AnimationPlayer:
if n is AnimationPlayer: return n
for c in n.get_children():
var r := _anim(c)
if r != null: return r
return null
func _find(n: Node, name_: String) -> Node:
if (name_ == "Player" and n is Player) or n.name == name_: return n
for c in n.get_children():
var r := _find(c, name_)
if r != null: return r
return null

View File

@ -24,6 +24,8 @@ class_name ViewModel
## Godot 4.7 GDScript 2.0.
const ARMS_GLB := "res://assets/viewmodel/fps_arms.glb"
const SCIFI_GLB := "res://assets/viewmodel/scifi_hands.glb"
const SCIFI_ATTACKS := ["Attack_01", "Attack_02", "Attack_03", "Attack_04"]
const BONE_HAND_R := "mixamorig_RightHand"
const BONE_HAND_L := "mixamorig_LeftHand"
@ -63,6 +65,10 @@ var _palm_local_r := Vector3.ZERO # wrist->middle knuckle, in hand-bo
var _palm_local_l := Vector3.ZERO
var _weapon_node: Node3D = null
var _weapon: Weapon = null
var _cam: Camera3D = null
var _clip_arms: Node3D = null # sci-fi FAB hands (fists slot only)
var _clip_anim: AnimationPlayer = null
var _clip_active := false
# ---------------------------------------------------------------- motion state
var _sway := Vector2.ZERO # smoothed look-delta the rig lags by
@ -132,6 +138,7 @@ const IDLE_OFF_SHAFT := Vector3(0.35, 0.55, -0.75) # a virtual "shaft" for the
func setup(cam: Camera3D) -> void:
if cam == null:
return
_cam = cam
cam.add_child(self)
scale = Vector3.ONE * vm_scale
_rig = Node3D.new()
@ -147,6 +154,7 @@ func setup(cam: Camera3D) -> void:
_arm_r = _spawn_arm(true)
_arm_l = _spawn_arm(false)
_init_clip_arms()
## One instance of the arms rig, with the other side's meshes hidden. Returns null if
## the asset is missing so the game still runs (you just get no hands).
@ -291,6 +299,7 @@ func equip(w: Weapon) -> void:
for mi in _mesh_nodes(_weapon_node):
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_pose_hands()
_set_clip_mode(w != null and w.id == "fists")
## Start a swap: the current weapon lowers, then `w` is equipped and raised.
func request_swap(w: Weapon) -> void:
@ -410,6 +419,8 @@ func _curl_fingers(skel: Skeleton3D, prefixes: Array, thumb_prefix: String,
# ---------------------------------------------------------------- driving
## Player calls this every frame with its state.
func drive(look_delta: Vector2, planar_speed: float, on_floor: bool, dt: float) -> void:
if _clip_active:
_clip_locomotion(planar_speed)
var heft: float = _weapon.heft if _weapon != null else 1.0
# --- sway: the rig lags the camera, then springs back ---
@ -490,6 +501,10 @@ func _sample(kf: Array, t: float) -> Array:
# ---------------------------------------------------------------- events
func start_swing() -> void:
_swing_t = 0.0
if _clip_active and _clip_anim != null:
var pick: String = SCIFI_ATTACKS[randi() % SCIFI_ATTACKS.size()]
if _clip_anim.has_animation(pick):
_clip_anim.play(pick, 0.05)
func is_swinging() -> bool:
return _swing_t >= 0.0
@ -499,3 +514,62 @@ func is_swapping() -> bool:
func land(force: float) -> void:
_dip = clampf(_dip + force * 0.06, 0.0, 0.11)
# ------------------------------------------------- sci-fi clip arms (fists skin)
# The FAB "Sci Fi Hands" rig, clip-driven (Idle_Fight stance, Attack_01-04 punches,
# Walk/Run locomotion baked in the pack). Shown INSTEAD of the procedural rig when
# bare hands are equipped; every other weapon uses the mixamo arms as before.
func _init_clip_arms() -> void:
if not ResourceLoader.exists(SCIFI_GLB):
return
var packed := load(SCIFI_GLB) as PackedScene
if packed == null:
return
_clip_arms = packed.instantiate()
_clip_arms.name = "SciFiArms"
_cam.add_child(_clip_arms)
_clip_arms.position = Vector3(0.0, -1.55, 0.25)
_clip_arms.rotate_y(PI)
_clip_anim = _find_anim_player(_clip_arms)
if _clip_anim != null:
for n in ["Idle", "Idle_Fight", "Walk_01", "Run_01"]:
if _clip_anim.has_animation(n):
_clip_anim.get_animation(n).loop_mode = Animation.LOOP_LINEAR
_clip_anim.animation_finished.connect(func(anim: StringName) -> void:
if _clip_active and String(anim).begins_with("Attack"):
_clip_anim.play("Idle_Fight", 0.15))
for mi in _mesh_nodes(_clip_arms):
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_clip_arms.visible = false
func _find_anim_player(n: Node) -> AnimationPlayer:
if n is AnimationPlayer:
return n
for c in n.get_children():
var r := _find_anim_player(c)
if r != null:
return r
return null
func _set_clip_mode(on: bool) -> void:
_clip_active = on and _clip_arms != null
if _clip_arms != null:
_clip_arms.visible = _clip_active
if _rig != null:
_rig.visible = not _clip_active
if _clip_active and _clip_anim != null and _clip_anim.has_animation("Idle_Fight"):
_clip_anim.play("Idle_Fight", 0.15)
func _clip_locomotion(planar_speed: float) -> void:
if _clip_anim == null:
return
if _clip_anim.current_animation.begins_with("Attack"):
return
var want := "Idle_Fight"
if planar_speed > 4.0 and _clip_anim.has_animation("Run_01"):
want = "Run_01"
elif planar_speed > 0.4 and _clip_anim.has_animation("Walk_01"):
want = "Walk_01"
if _clip_anim.current_animation != String(want):
_clip_anim.play(want, 0.2)