extends Object class_name ViewmodelAnim ## Procedural first-person weapon choreography. ## ## Replaces the old "spin the gun 360° to reload" placeholder with per-style ## reload sequences that read like the real manual of arms, plus a per-shot ## viewmodel recoil kick. Everything is tween-driven on nodes the weapon ## already has: `model_root` (the gun) and the named arm pivots ("ArmL") ## built by WeaponManager, plus temporary prop meshes (magazine, shells, ## rocket) spawned for the duration. ## ## Styles: ## "mag" – box-mag rifles/SMGs: tilt gun, left hand pulls the mag, ## discards it, brings a fresh one, seats it, charges the bolt. ## "boltmag" – same, then a distinct bolt-cycle rock (AWP/DMR). ## "break" – break-action: gun hinges open, shells eject, reload, snap shut. ## "tube" – launchers: gun lowers, a rocket/shell is shoved into the tube. ## "cell" – energy weapons: glowing cell swapped on the side. ## "none" – no animation (ammo just refills). ## ## All state (original transforms, spawned props, the tween) is stored on the ## weapon via metadata and torn down by `stop()` — called on finish, unequip, ## or a new reload starting. const META_TWEEN := "vm_reload_tween" const META_PROPS := "vm_reload_props" const META_ARM_L_XFORM := "vm_arm_l_xform" static func play_reload(w: Node3D, style: String, T: float) -> void: if style == "none" or not is_instance_valid(w) or not ("model_root" in w): return stop(w) # clean slate; also restores transforms from any prior run var root: Node3D = w.model_root var arm_l: Node3D = w.get_node_or_null("ArmL") if arm_l and not w.has_meta(META_ARM_L_XFORM): w.set_meta(META_ARM_L_XFORM, arm_l.transform) var tw := w.create_tween() w.set_meta(META_TWEEN, tw) match style: "boltmag": _mag_sequence(w, root, arm_l, tw, T, true) "break": _break_sequence(w, root, arm_l, tw, T) "tube": _tube_sequence(w, root, arm_l, tw, T) "cell": _cell_sequence(w, root, arm_l, tw, T) _: _mag_sequence(w, root, arm_l, tw, T, false) tw.tween_callback(func(): stop(w)) ## Kill the running choreography and restore the resting pose. static func stop(w: Node3D) -> void: if not is_instance_valid(w): return if w.has_meta(META_TWEEN): var tw = w.get_meta(META_TWEEN) if tw is Tween and tw.is_valid(): tw.kill() w.remove_meta(META_TWEEN) if w.has_meta(META_PROPS): for p in w.get_meta(META_PROPS): if is_instance_valid(p): p.queue_free() w.remove_meta(META_PROPS) if "model_root" in w and is_instance_valid(w.model_root): w.model_root.transform = Transform3D.IDENTITY var arm_l: Node3D = w.get_node_or_null("ArmL") if arm_l and w.has_meta(META_ARM_L_XFORM): arm_l.transform = w.get_meta(META_ARM_L_XFORM) ## Per-shot viewmodel kick: the gun slides back into the grip and the muzzle ## flips up, scaled by the weapon's recoil so every gun has its own punch. ## Weapons call this from _process with their decaying kick value 0..1. static func apply_kick(w: Node3D, kick: float, amplitude: float) -> void: if not ("model_root" in w) or not is_instance_valid(w.model_root): return var root: Node3D = w.model_root root.position.z = kick * amplitude * 2.2 root.rotation.x = kick * amplitude * 5.0 # ── Sequences ──────────────────────────────────────────────────────────────── # Times are fractions of the weapon's reload_time so a 1.6 s M4 mag swap and # a 3 s AWP reload both fill their whole window. static func _mag_sequence(w: Node3D, root: Node3D, arm_l: Node3D, tw: Tween, T: float, bolt_cycle: bool) -> void: # Mag prop rides the left hand. Sits at the gun's mag well when stowed. var mag := _make_prop(w, arm_l, Vector3(0.07, 0.22, 0.11), Color(0.42, 0.44, 0.50), Vector3(0.02, -0.30, -0.30)) var mag_t := 0.55 if bolt_cycle else 0.78 # leave room for the bolt rock # 1. Bring the gun UP into view and roll it so the mag well faces the # player — real reloads happen at chest height, gun clearly on screen. tw.tween_property(root, "rotation", Vector3(0.05, 0.15, 0.45), 0.12 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3(-0.10, 0.16, 0.18), 0.12 * T) # Left hand leaves the foregrip for the mag well. if arm_l: tw.parallel().tween_method(_aim_arm.bind(arm_l), Vector3(-0.02, -0.02, -0.3), Vector3(0.02, -0.10, -0.34), 0.12 * T) # 2. Mag out: hand rips it down and back (mag visibly leaves the gun). tw.tween_callback(func(): mag.visible = true) if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(0.02, -0.10, -0.34), Vector3(-0.12, -0.40, -0.14), 0.18 * mag_t * T) \ .set_ease(Tween.EASE_IN_OUT) # 3. Discard, grab fresh mag low off-screen. tw.tween_callback(func(): mag.visible = false) tw.tween_interval(0.14 * mag_t * T) tw.tween_callback(func(): mag.visible = true) # 4. Fresh mag back up to the well. if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.12, -0.40, -0.14), Vector3(0.02, -0.10, -0.34), 0.22 * mag_t * T) \ .set_ease(Tween.EASE_IN_OUT) # 5. Seat it (small upward jolt on the gun), hide the prop. tw.tween_property(root, "position", Vector3(-0.10, 0.19, 0.18), 0.05 * T) \ .set_ease(Tween.EASE_OUT) tw.tween_callback(func(): mag.visible = false) tw.tween_property(root, "position", Vector3(-0.10, 0.16, 0.18), 0.06 * T) # 6. Hand back to the foregrip while the gun levels out. if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(0.0, -0.06, -0.34), Vector3(-0.02, -0.02, -0.3), 0.12 * T) tw.parallel().tween_property(root, "rotation", Vector3.ZERO, 0.12 * T) tw.parallel().tween_property(root, "position", Vector3.ZERO, 0.12 * T) # 7. Charge: sharp pull back, snap forward. Bolt guns rock the whole gun. if bolt_cycle: tw.tween_property(root, "rotation", Vector3(-0.14, 0.0, -0.08), 0.10 * T) tw.parallel().tween_property(root, "position", Vector3(0, 0.01, 0.10), 0.10 * T) tw.tween_interval(0.06 * T) tw.tween_property(root, "rotation", Vector3.ZERO, 0.08 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3.ZERO, 0.08 * T) else: tw.tween_property(root, "position", Vector3(0, 0, 0.06), 0.06 * T) tw.tween_property(root, "position", Vector3.ZERO, 0.05 * T) \ .set_ease(Tween.EASE_OUT) static func _break_sequence(w: Node3D, root: Node3D, arm_l: Node3D, tw: Tween, T: float) -> void: var shell_a := _make_prop(w, root, Vector3(0.028, 0.028, 0.07), Color(0.75, 0.25, 0.15), Vector3(-0.01, 0.03, -0.25)) var shell_b := _make_prop(w, root, Vector3(0.028, 0.028, 0.07), Color(0.75, 0.25, 0.15), Vector3(0.02, 0.03, -0.25)) # Hinge open — barrels drop forward-down, breech rises into view — and # flick the spent shells up and back. tw.tween_property(root, "rotation", Vector3(-0.42, 0.15, 0.06), 0.18 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3(-0.10, 0.10, 0.16), 0.18 * T) tw.tween_callback(func(): shell_a.visible = true shell_b.visible = true) tw.tween_property(shell_a, "position", shell_a.position + Vector3(-0.06, 0.28, 0.25), 0.2 * T) tw.parallel().tween_property(shell_b, "position", shell_b.position + Vector3(0.08, 0.24, 0.28), 0.2 * T) tw.parallel().tween_property(shell_a, "rotation", Vector3(2.5, 0, 1.0), 0.2 * T) tw.parallel().tween_property(shell_b, "rotation", Vector3(2.1, 0, -1.2), 0.2 * T) tw.tween_callback(func(): shell_a.visible = false shell_b.visible = false shell_a.position = Vector3(-0.01, 0.10, -0.25) shell_b.position = Vector3(0.02, 0.10, -0.25) shell_a.rotation = Vector3.ZERO shell_b.rotation = Vector3.ZERO) # Left hand brings two fresh shells and drops them in. if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.02, -0.02, -0.3), Vector3(-0.04, -0.5, -0.1), 0.14 * T) tw.tween_callback(func(): shell_a.visible = true shell_b.visible = true) if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.04, -0.5, -0.1), Vector3(0.0, -0.06, -0.3), 0.16 * T) tw.tween_property(shell_a, "position", Vector3(-0.01, 0.03, -0.25), 0.12 * T) tw.parallel().tween_property(shell_b, "position", Vector3(0.02, 0.03, -0.25), 0.12 * T) tw.tween_callback(func(): shell_a.visible = false shell_b.visible = false) # Snap shut with a little overshoot. if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(0.0, -0.06, -0.3), Vector3(-0.02, -0.02, -0.3), 0.1 * T) tw.parallel().tween_property(root, "rotation", Vector3(0.06, 0.0, 0.0), 0.10 * T) \ .set_ease(Tween.EASE_IN) tw.parallel().tween_property(root, "position", Vector3.ZERO, 0.10 * T) tw.tween_property(root, "rotation", Vector3.ZERO, 0.06 * T) \ .set_ease(Tween.EASE_OUT) static func _tube_sequence(w: Node3D, root: Node3D, arm_l: Node3D, tw: Tween, T: float) -> void: var rocket := _make_prop(w, arm_l if arm_l else root, Vector3(0.045, 0.045, 0.30), Color(0.35, 0.55, 0.35), Vector3(0, -0.1, -0.58)) # Bring the tube up across the chest and tip it toward the player. tw.tween_property(root, "rotation", Vector3(0.28, 0.35, 0.0), 0.18 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3(-0.10, 0.06, 0.16), 0.18 * T) # Left hand fetches a fresh rocket from below… if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.02, -0.02, -0.3), Vector3(-0.08, -0.55, -0.05), 0.16 * T) tw.tween_callback(func(): rocket.visible = true) # …lines it up with the muzzle and shoves it home. if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.08, -0.55, -0.05), Vector3(0.0, -0.12, -0.55), 0.24 * T) \ .set_ease(Tween.EASE_IN_OUT) tw.tween_method(_aim_arm.bind(arm_l), Vector3(0.0, -0.12, -0.55), Vector3(0.0, -0.05, -0.30), 0.16 * T) \ .set_ease(Tween.EASE_IN) tw.tween_callback(func(): rocket.visible = false) # Shoulder it again. tw.tween_property(root, "rotation", Vector3.ZERO, 0.16 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3.ZERO, 0.16 * T) if arm_l: tw.parallel().tween_method(_aim_arm.bind(arm_l), Vector3(0.0, -0.05, -0.30), Vector3(-0.02, -0.02, -0.3), 0.14 * T) static func _cell_sequence(w: Node3D, root: Node3D, arm_l: Node3D, tw: Tween, T: float) -> void: var cell := _make_prop(w, arm_l if arm_l else root, Vector3(0.05, 0.09, 0.05), Color(0.2, 0.95, 0.9), Vector3(0, -0.1, -0.58), true) # Raise across the chest and quarter-roll so the cell port faces up. tw.tween_property(root, "rotation", Vector3(0.08, 0.2, -0.35), 0.14 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3(-0.10, 0.08, 0.14), 0.14 * T) if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.02, -0.02, -0.3), Vector3(0.03, -0.10, -0.30), 0.12 * T) tw.tween_callback(func(): cell.visible = true) # Pull the spent cell, flick it away, slot a fresh one. if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(0.03, -0.10, -0.30), Vector3(-0.10, -0.45, -0.12), 0.2 * T) tw.tween_callback(func(): cell.visible = false) tw.tween_interval(0.12 * T) tw.tween_callback(func(): cell.visible = true) if arm_l: tw.tween_method(_aim_arm.bind(arm_l), Vector3(-0.10, -0.45, -0.12), Vector3(0.03, -0.10, -0.30), 0.2 * T) tw.tween_callback(func(): cell.visible = false) tw.tween_property(root, "rotation", Vector3.ZERO, 0.14 * T) \ .set_ease(Tween.EASE_OUT) tw.parallel().tween_property(root, "position", Vector3.ZERO, 0.14 * T) if arm_l: tw.parallel().tween_method(_aim_arm.bind(arm_l), Vector3(0.03, -0.10, -0.30), Vector3(-0.02, -0.02, -0.3), 0.14 * T) # ── Helpers ────────────────────────────────────────────────────────────────── ## Re-aims an arm pivot (shoulder stays put, hand tracks `target` in weapon ## space) — same math WeaponManager used to place the arm initially. static func _aim_arm(target: Vector3, arm: Node3D) -> void: if not is_instance_valid(arm): return var shoulder := arm.position var dir := target - shoulder if dir.length_squared() < 0.0001: return arm.transform = Transform3D(Basis.looking_at(dir.normalized(), Vector3.UP), shoulder) ## An invisible-until-needed prop (mag/shell/rocket/cell) parented so it moves ## with the left hand or the gun. Registered for cleanup on stop(). static func _make_prop(w: Node3D, parent: Node3D, size: Vector3, color: Color, pos: Vector3, emissive: bool = false) -> MeshInstance3D: var mi := MeshInstance3D.new() var mesh := BoxMesh.new() mesh.size = size var mat := StandardMaterial3D.new() mat.albedo_color = color mat.roughness = 0.9 if emissive: mat.emission_enabled = true mat.emission = color mat.emission_energy_multiplier = 1.6 mesh.material = mat mi.mesh = mesh mi.position = pos mi.visible = false mi.layers = 1 << 19 # viewmodel layer, same as the rest of the gun mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF (parent if parent else w).add_child(mi) var props: Array = w.get_meta(META_PROPS) if w.has_meta(META_PROPS) else [] props.append(mi) w.set_meta(META_PROPS, props) return mi