diff --git a/game/assets/viewmodel/scifi_hands.glb b/game/assets/viewmodel/scifi_hands.glb new file mode 100644 index 0000000..243efaf Binary files /dev/null and b/game/assets/viewmodel/scifi_hands.glb differ diff --git a/game/dev/probe_scifi.gd b/game/dev/probe_scifi.gd new file mode 100644 index 0000000..dc41590 --- /dev/null +++ b/game/dev/probe_scifi.gd @@ -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 diff --git a/game/scripts/ViewModel.gd b/game/scripts/ViewModel.gd index cf4d5d7..ca1ce42 100644 --- a/game/scripts/ViewModel.gd +++ b/game/scripts/ViewModel.gd @@ -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)