class_name ImportedOperator extends Node3D static var scenes: Dictionary = {} var skeleton: Skeleton3D var weapon_mount := Node3D.new() var pose := OperatorPose.new() var current := "pro_rifle__idle" var clock := 0.0 var action := "" var action_time := 0.0 var action_weight := 0.0 var previous_life := -1 var previous_shot := -1 var previous_grenades := -1 var previous_hp := 100.0 var previous_yaw := 0.0 var was_grounded := true var was_reloading := false var dead := false var hand := -1 var socket := Transform3D.IDENTITY var variant := "steve" func setup(model: String, arms_only: bool = false) -> void: variant = model if model in ["steve", "gas_mask", "pro_rifle", "basic_shooter", "slim_shooter"] else "steve" var suffix := "_arms" if arms_only else "" var key := variant + suffix if not scenes.has(key): scenes[key] = load("res://scenes/operators/" + key + ".scn") as PackedScene var packed: PackedScene = scenes[key] var body := packed.instantiate() as Node3D body.name = "Body" body.rotation.y = PI add_child(body) skeleton = body.find_child("Skeleton3D", true, false) as Skeleton3D pose.setup(skeleton) weapon_mount.name = "WeaponMount" add_child(weapon_mount) hand = skeleton.find_bone("mixamorig_RightHand") pose.apply("pro_rifle__idle_aiming", 0.2, true, "", 0, 0, 0, 0) var grip := pose.global_poses[hand] # Calibrate in model space once; thereafter the socket follows the hand, # including reloads, recoil and death, instead of floating at chest height. var gun_basis := Basis(Vector3.UP, PI) var origin := grip.origin - gun_basis * Vector3(0, -0.09, 0.04) socket = grip.affine_inverse() * Transform3D(gun_basis, origin) update_socket() func update_socket() -> void: var body := skeleton.get_parent() as Node3D weapon_mount.transform = body.transform * skeleton.transform * pose.global_poses[hand] * socket func choose(key: String, restart: bool = false) -> void: if key != current or restart: current = key clock = 0.0 func trigger(key: String) -> void: action = key action_time = 0 static func direction(velocity: Vector3) -> String: var angle := atan2(velocity.x, -velocity.z) var sector := posmod(roundi(angle / (PI / 4.0)), 8) return ["forward", "forward_right", "right", "backward_right", "backward", "backward_left", "left", "forward_left"][sector] func animate_state(delta: float, state: Dictionary) -> void: var velocity: Vector3 = state.get("velocity", Vector3.ZERO) var speed := Vector2(velocity.x, velocity.z).length() var crouch := bool(state.get("crouch", false)) var aiming := bool(state.get("aim", false)) var grounded := bool(state.get("grounded", true)) var hp := float(state.get("hp", 100.0)) var life := int(state.get("life", 0)) var shot := int(state.get("shot", 0)) var grenades := int(state.get("grenades", 2)) + int(state.get("flashes", 1)) var reload_time := float(state.get("reload", 0.0)) var yaw := float(state.get("yaw", 0.0)) if life != previous_life: dead = false action = "" action_weight = 0 previous_shot = shot previous_grenades = grenades previous_hp = hp previous_yaw = yaw was_reloading = false choose("pro_rifle__idle", true) if hp <= 0: if not dead: var deaths := ["death_from_the_front", "death_from_the_back", "death_from_right", "death_from_front_headshot", "death_from_back_headshot"] var death_key: String = "crouch_death" if crouch else deaths[posmod(int(state.get("id", 0)) + life, deaths.size())] choose("pro_rifle__" + death_key, true) dead = true action = "" elif float(state.get("slide", 0)) > 0: choose("slide__running_slide") clock = (1.0 - clampf(float(state.slide) / 0.65, 0, 1)) * pose.clip(current).length elif not grounded: choose("pro_rifle__jump_up" if velocity.y > 1 else "pro_rifle__jump_loop") elif not was_grounded: choose("pro_rifle__jump_down", true) elif current == "pro_rifle__jump_down" and clock < minf(0.25, pose.clip(current).length): pass elif speed > 0.2: var gait := "walk_crouching_" if crouch else ("sprint_" if speed > 7 else ("walk_" if speed < 3.3 or aiming else "run_")) choose("pro_rifle__" + gait + direction(velocity)) else: var turn := wrapf(yaw - previous_yaw, -PI, PI) if absf(turn) > delta * 1.1 and not aiming: choose("pro_rifle__" + ("crouching_" if crouch else "") + "turn_90_" + ("left" if turn > 0 else "right")) else: choose("pro_rifle__idle" + ("_crouching" if crouch else "") + ("_aiming" if aiming else "")) if not dead: if reload_time > 0: if not was_reloading: trigger("pro_rifle__reload") var duration := maxf(0.1, float(state.get("reload_duration", 2.3))) action_time = (1.0 - clampf(reload_time / duration, 0, 1)) * pose.clip(action).length elif was_reloading: action = "" elif grenades < previous_grenades: trigger("basic_shooter__toss_grenade") elif hp < previous_hp: trigger("basic_shooter__hit_reaction") elif shot != previous_shot: trigger("pro_rifle__firing_rifle_walking" if speed > 0.2 else "pro_rifle__firing_rifle_standing") if not action.is_empty(): if reload_time <= 0: action_time += delta if action_time >= pose.clip(action).length: action = "" action_weight = move_toward(action_weight, 0.0 if action.is_empty() else 1.0, delta * 16) clock += delta var looping := not dead and not "jump" in current and not "slide" in current pose.apply(current, clock, looping, action, action_time, action_weight, delta, 0 if dead else float(state.get("pitch", 0))) update_socket() if not dead and action.is_empty() and state.has("weapon"): align_support(int(state.weapon)) previous_life = life previous_shot = shot previous_grenades = grenades previous_hp = hp previous_yaw = yaw was_grounded = grounded was_reloading = reload_time > 0 func preview(key: String, time: float) -> void: pose.apply(key, time, false, "", 0, 0, 0, 0) update_socket() static func support_point(weapon: int) -> Vector3: if Arsenal.FAMILIES[weapon] == 4: return Vector3(-0.045, -0.145, 0.08) return Vector3(-0.055, -0.095, -0.17 if Arsenal.FAMILIES[weapon] == 1 else -0.255) func align_support(weapon: int) -> void: var body := skeleton.get_parent() as Node3D var target := (body.transform * skeleton.transform).affine_inverse() * weapon_mount.transform * support_point(weapon) pose.reach_arm("Left", target)