Загрузка данных


import mss
import numpy as np
import win32api
import win32con
import ctypes
import threading
import time
import tkinter as tk
from tkinter import ttk, messagebox
import uuid
import random
import sys
import math
import cv2
import queue

# Попытка импорта официальной библиотеки Makcu
try:
    from makcu import create_controller, MouseButton
    MAKCU_SDK_AVAILABLE = True
except ImportError:
    MAKCU_SDK_AVAILABLE = False
    class MouseButton:
        LEFT = 1; RIGHT = 2; MIDDLE = 3; MOUSE4 = 4; MOUSE5 = 5
    print("Библиотека makcu не найдена. Установите: pip install makcu")

# Попытка импорта pygrabber для реальных имен камер
try:
    from pygrabber.dshow_graph import FilterGraph
    PYGRABBER_AVAILABLE = True
except ImportError:
    PYGRABBER_AVAILABLE = False
    print("Библиотека pygrabber не найдена. Установите: pip install pygrabber")

# Попытка импорта Pillow для безопасного рендера видео
try:
    from PIL import Image, ImageTk
    PILLOW_AVAILABLE = True
except ImportError:
    PILLOW_AVAILABLE = False
    print("Библиотека Pillow не найдена. Установите: pip install Pillow")

# Повышаем точность системного таймера до 1 мс
try:
    ctypes.windll.winmm.timeBeginPeriod(1)
    ctypes.windll.user32.SetProcessDPIAware()
except:
    pass

MOUSEEVENTF_MOVE = 0x0001
MOUSEEVENTF_LEFTDOWN = 0x0002
MOUSEEVENTF_LEFTUP = 0x0004

def get_vk_name(vk_code):
    special_keys = {
        0x01: "Makcu/Mouse Left", 0x02: "Makcu/Mouse Right", 0x04: "Makcu/Mouse Middle",
        0x05: "Makcu/Mouse 4 (Back)", 0x06: "Makcu/Mouse 5 (Forward)", 0x08: "Backspace",
        0x09: "Tab", 0x10: "Shift", 0x11: "Ctrl", 0x12: "Alt",
        0x14: "Caps Lock", 0x1B: "Esc", 0x20: "Space"
    }
    if vk_code in special_keys:
        return special_keys[vk_code]
    if 0x30 <= vk_code <= 0x5A:
        return chr(vk_code)
    return f"Key [0x{vk_code:02X}]"

def set_random_console_title():
    random_name = str(uuid.uuid4())[:8]
    try:
        ctypes.windll.kernel32.SetConsoleTitleW(random_name)
    except:
        pass

set_random_console_title()

class CaptureManager:
    """Управление картой видеозахвата (USB HDMI) с правильной инициализацией."""
    def __init__(self):
        self.capture = None
        self.current_frame = None
        self.lock = threading.Lock()
        self.running = False
        self.thread = None
        self.device_index = 0

    def list_capture_devices(self):
        if PYGRABBER_AVAILABLE:
            try:
                graph = FilterGraph()
                devices = graph.get_input_devices()
                if devices:
                    return [f"{i}: {name}" for i, name in enumerate(devices)]
            except Exception as e:
                print(f"[Capture] Ошибка pygrabber: {e}")
        
        devices = []
        index = 0
        while index < 5:
            cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
            if cap.isOpened():
                devices.append(f"Device {index} (Generic)")
                cap.release()
            index += 1
        return devices

    def start_capture(self, device_index=0):
        self.stop_capture()
        self.device_index = device_index
        # Обязательное использование CAP_DSHOW для избежания конфликтов драйверов в Windows
        self.capture = cv2.VideoCapture(device_index, cv2.CAP_DSHOW)
        
        if not self.capture.isOpened():
            raise Exception(f"Не удалось открыть устройство {device_index}")
        
        # КРИТИЧЕСКИ ВАЖНО: FOURCC (Кодек MJPG) должен быть установлен ДО разрешения, 
        # иначе USB интерфейс зависнет из-за попытки прокинуть YUY2 формат в 1080p.
        self.capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
        self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
        self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
        self.capture.set(cv2.CAP_PROP_FPS, 60)
        self.capture.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Минимизация задержки (input lag)
        
        self.running = True
        self.thread = threading.Thread(target=self._capture_loop, daemon=True)
        self.thread.start()

    def _capture_loop(self):
        empty_frames = 0
        while self.running and self.capture and self.capture.isOpened():
            ret, frame = self.capture.read()
            if ret:
                empty_frames = 0
                with self.lock:
                    self.current_frame = frame
            else:
                empty_frames += 1
                if empty_frames > 100: # Защита от полного зависания карты
                    print("[Capture] Потерян сигнал от карты захвата.")
                    break
                time.sleep(0.005)
        self.running = False

    def get_frame(self):
        with self.lock:
            if self.current_frame is not None:
                return self.current_frame.copy()
            return None

    def stop_capture(self):
        self.running = False
        if self.thread and self.thread.is_alive():
            self.thread.join(timeout=2)
        if self.capture:
            self.capture.release()
            self.capture = None
        self.current_frame = None

    def is_active(self):
        return self.running and self.capture is not None and self.capture.isOpened()

class MakcuManager:
    """Интеграция Makcu для сетапа 2PC."""
    def __init__(self):
        self.controller = None
        self.connected = False
        self.button_mapping = {
            0x01: MouseButton.LEFT, 0x02: MouseButton.RIGHT,
            0x04: MouseButton.MIDDLE, 0x05: MouseButton.MOUSE4,
            0x06: MouseButton.MOUSE5
        }

    def auto_connect(self):
        if not MAKCU_SDK_AVAILABLE:
            return False
        try:
            self.controller = create_controller(debug=False, auto_reconnect=True)
            self.connected = self.controller.is_connected()
            if self.connected:
                self.controller.enable_button_monitoring(True)
            return self.connected
        except Exception as e:
            self.connected = False
            return False

    def disconnect(self):
        if self.controller and self.connected:
            try:
                self.controller.disconnect()
            except:
                pass
        self.connected = False
        self.controller = None

    def is_button_pressed(self, vk_code):
        makcu_btn = self.button_mapping.get(vk_code)
        # 1. Запрос по COM-порту (Физическая мышь на PC1/Makcu)
        if self.connected and self.controller and makcu_btn:
            try:
                if self.controller.is_pressed(makcu_btn):
                    return True
            except:
                pass
        
        # 2. Локальный Fallback (Клавиатура на PC2)
        return (ctypes.windll.user32.GetAsyncKeyState(vk_code) & 0x8000) != 0

    def send_mouse_move(self, dx, dy):
        if not self.connected or not self.controller:
            return False
        dx = max(-127, min(127, int(dx)))
        dy = max(-127, min(127, int(dy)))
        if dx == 0 and dy == 0:
            return True
        try:
            self.controller.move(dx, dy)
            return True
        except:
            return False

    def send_mouse_click(self, vk_code=0x01, down=True):
        if not self.connected or not self.controller:
            return False
        makcu_btn = self.button_mapping.get(vk_code)
        if not makcu_btn:
            return False
        try:
            if down:
                self.controller.press(makcu_btn)
            else:
                self.controller.release(makcu_btn)
            return True
        except:
            return False


class StealthColorBot:
    def __init__(self):
        self.root = tk.Tk()
        self.root.overrideredirect(True)
        self.root.geometry("420x780")
        self.root.configure(bg="#050505")
        self.root.attributes("-topmost", True)
        self.root.attributes("-alpha", 0.97)

        try:
            hwnd = ctypes.windll.user32.GetParent(self.root.winfo_id())
            style = ctypes.windll.user32.GetWindowLongW(hwnd, -20)
            style = style | 0x00000080 | 0x00000008
            ctypes.windll.user32.SetWindowLongW(hwnd, -20, style)
        except:
            pass

        self.running = True
        self.active = False
        self.visible = True

        self.capture_manager = CaptureManager()
        self.makcu = MakcuManager()

        self.mouse_lock = threading.Lock()
        self.has_target = False
        self.target_rel_x = 0.0
        self.target_rel_y = 0.0

        # Настройки Аима
        self.tracking_enabled = tk.BooleanVar(value=True)
        self.tracking_key = 0x06  
        self.target_color = (255, 0, 0)
        self.target_color_bgr = (0, 0, 255)
        self.fov = 100
        self.tolerance = 15

        # Физика
        self.gravity = 0.15
        self.wind = 0.1
        self.min_speed = 1.0
        self.max_speed = 15.0

        self.noise_state_x = 0.0
        self.noise_state_y = 0.0
        self.noise_velocity_x = 0.0
        self.noise_velocity_y = 0.0

        self.velocity_x = 0.0
        self.velocity_y = 0.0
        self.remainder_x = 0.0
        self.remainder_y = 0.0

        self.smooth_target_x = None
        self.smooth_target_y = None
        self.smoothing_factor = 0.6

        self.last_target_center = None
        self.last_raw_target_x = None
        self.last_raw_target_y = None

        # Триггер
        self.trigger_enabled = tk.BooleanVar(value=False)
        self.trigger_key = 0x05  
        self.trigger_tolerance = 15
        self.trigger_color = (255, 0, 0)
        self.trigger_color_bgr = (0, 0, 255)
        self.is_clicking = False
        self.trigger_queue = queue.Queue()

        self.min_reaction = 0.0
        self.max_reaction = 0.05
        self.min_hold = 0.03
        self.max_hold = 0.07

        self.show_fov_overlay = tk.BooleanVar(value=True)
        self.show_preview_window = tk.BooleanVar(value=False)
        self._latest_preview_frame = None
        self.preview_tk_window = None
        self.preview_tk_label = None

        self.scan_interval = 0.005
        self.mouse_send_rate = 125
        self.mouse_send_interval = 1.0 / self.mouse_send_rate
        self.mouse_jitter = 0.03
        self.mouse_max_step = 20
        self.mouse_skip_probability = 0.02
        self.mouse_target_noise = 1.0

        self.pending_move_x = 0.0
        self.pending_move_y = 0.0
        self.last_mouse_send_time = 0.0

        self.last_aim_time = 0.0
        self.last_trigger_time = 0.0
        self.aim_interval = 0.008
        self.trigger_interval = 0.01

        # Метрики экрана для локального захвата
        self.screen_width = win32api.GetSystemMetrics(0)
        self.screen_height = win32api.GetSystemMetrics(1)
        self.local_center_x = self.screen_width // 2
        self.local_center_y = self.screen_height // 2

        self.capture_source_var = tk.StringVar(value="capture") # По умолчанию карта захвата
        self.capture_device_list = []
        self.capture_device_index = 0
        self.capture_status_text = tk.StringVar(value="Not connected")

        # Кэш масок
        self.aim_bounds = []
        self.trig_bounds = []

        self.setup_ui()
        self.setup_overlay()
        self._update_hsv_cache()

        threading.Thread(target=self.logic_loop, daemon=True).start()
        threading.Thread(target=self.mouse_movement_loop, daemon=True).start()
        threading.Thread(target=self.key_listener, daemon=True).start()
        threading.Thread(target=self.trigger_worker, daemon=True).start()
        threading.Thread(target=self._auto_connect_makcu, daemon=True).start()
        
        # Запуск цикла отрисовки окна превью в главном GUI потоке (безопасно)
        self.root.after(30, self.update_preview_ui_loop)

    def _auto_connect_makcu(self):
        attempts = 0
        while self.running and not self.makcu.connected:
            try:
                if self.makcu.auto_connect():
                    self.root.after(0, self._update_makcu_status_connected)
                    break
                else:
                    time.sleep(3)
                    attempts += 1
                    if attempts > 10:
                        break
            except:
                time.sleep(2)

    def _update_makcu_status_connected(self):
        self.makcu_status_label.config(text="Status: Connected (SDK)", fg="#00ff00")

    def _get_hsv_bounds(self, bgr_color, tol):
        bgr = np.uint8([[bgr_color]])
        hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)[0][0]
        h, s, v = int(hsv[0]), int(hsv[1]), int(hsv[2])
        
        lower_s, upper_s = max(0, s - tol), min(255, s + tol)
        lower_v, upper_v = max(0, v - tol), min(255, v + tol)
        
        h_low = h - tol
        h_high = h + tol
        
        bounds = []
        if h_low < 0:
            bounds.append((np.array([0, lower_s, lower_v], dtype=np.uint8), np.array([h_high, upper_s, upper_v], dtype=np.uint8)))
            bounds.append((np.array([179 + h_low, lower_s, lower_v], dtype=np.uint8), np.array([179, upper_s, upper_v], dtype=np.uint8)))
        elif h_high > 179:
            bounds.append((np.array([h_low, lower_s, lower_v], dtype=np.uint8), np.array([179, upper_s, upper_v], dtype=np.uint8)))
            bounds.append((np.array([0, lower_s, lower_v], dtype=np.uint8), np.array([h_high - 179, upper_s, upper_v], dtype=np.uint8)))
        else:
            bounds.append((np.array([h_low, lower_s, lower_v], dtype=np.uint8), np.array([h_high, upper_s, upper_v], dtype=np.uint8)))
        return bounds

    def _update_hsv_cache(self):
        self.aim_bounds = self._get_hsv_bounds(self.target_color_bgr, self.tolerance)
        self.trig_bounds = self._get_hsv_bounds(self.trigger_color_bgr, self.trigger_tolerance)

    def apply_mask(self, hsv_img, bounds):
        final_mask = None
        for lower, upper in bounds:
            m = cv2.inRange(hsv_img, lower, upper)
            if final_mask is None:
                final_mask = m
            else:
                final_mask = cv2.bitwise_or(final_mask, m)
        return final_mask

    # ---------- UI ----------
    def setup_ui(self):
        header = tk.Frame(self.root, bg="#12001f", height=30)
        header.pack(fill="x")
        self.make_draggable(header)
        title_lbl = tk.Label(header, text="angelpriv - Advanced 2PC", bg="#12001f", fg="#b042ff", font=("Consolas", 11, "bold"))
        title_lbl.pack(side="left", padx=10)
        self.make_draggable(title_lbl)
        close_btn = tk.Button(header, text="X", bg="#12001f", fg="#b042ff", bd=0, activebackground="#36005c", activeforeground="#ffffff", command=self.exit_app, font=("Candara", 10, "bold"))
        close_btn.pack(side="right", padx=5)

        tab_frame = tk.Frame(self.root, bg="#0a0011", height=30)
        tab_frame.pack(fill="x")
        self.btn_tab_aim = tk.Button(tab_frame, text="Tracking", bg="#36005c", fg="#e0e0e0", bd=0, font=("Candara", 9, "bold"), command=self.show_aim_tab)
        self.btn_tab_aim.pack(side="left", fill="both", expand=True)
        self.btn_tab_trig = tk.Button(tab_frame, text="Action", bg="#12001f", fg="#e0e0e0", bd=0, font=("Candara", 9, "bold"), command=self.show_trig_tab)
        self.btn_tab_trig.pack(side="left", fill="both", expand=True)
        self.btn_tab_capture = tk.Button(tab_frame, text="Capture", bg="#12001f", fg="#e0e0e0", bd=0, font=("Candara", 9, "bold"), command=self.show_capture_tab)
        self.btn_tab_capture.pack(side="left", fill="both", expand=True)
        self.btn_tab_makcu = tk.Button(tab_frame, text="Makcu", bg="#12001f", fg="#e0e0e0", bd=0, font=("Candara", 9, "bold"), command=self.show_makcu_tab)
        self.btn_tab_makcu.pack(side="left", fill="both", expand=True)

        self.content_container = tk.Frame(self.root, bg="#050505")
        self.content_container.pack(fill="both", expand=True, padx=15, pady=5)

        # === ВКЛАДКА AIM ===
        self.aim_frame = tk.Frame(self.content_container, bg="#050505")
        tk.Checkbutton(self.aim_frame, text="Enable Target Tracking", variable=self.tracking_enabled, bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#b042ff", font=("Candara", 10)).pack(anchor="w")
        tk.Checkbutton(self.aim_frame, text="Show FOV Overlay (Screen 2)", variable=self.show_fov_overlay, bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#b042ff", font=("Candara", 10)).pack(anchor="w")
        bind_frame_aim = tk.Frame(self.aim_frame, bg="#050505")
        bind_frame_aim.pack(fill="x", pady=5)
        tk.Label(bind_frame_aim, text="Aim Activation:", bg="#050505", fg="#e0e0e0").pack(side="left")
        self.bind_btn_aim = tk.Button(bind_frame_aim, text=f"Bind: {get_vk_name(self.tracking_key)}", bg="#1a1a1a", fg="#b042ff", bd=0, command=lambda: self.start_keybind('aim'))
        self.bind_btn_aim.pack(side="right", padx=5)
        self.create_inline_color_picker(self.aim_frame, "Target Base Color", "target_color")
        self.create_slider(self.aim_frame, "Color Tolerance", 0, 100, self.tolerance, "tolerance")
        self.create_slider(self.aim_frame, "Tracking FOV", 10, 800, self.fov, "fov")
        self.create_slider(self.aim_frame, "Smoothing Factor", 0.0, 0.95, self.smoothing_factor, "smoothing_factor", res=0.05)
        tk.Label(self.aim_frame, text="- Kinematics Physics -", bg="#050505", fg="#5A5A5A", font=("Candara", 9)).pack(pady=(5, 0))
        self.create_slider(self.aim_frame, "Gravity (Acceleration)", 0.05, 0.5, self.gravity, "gravity", res=0.01)
        self.create_slider(self.aim_frame, "Wind (Sway Smoothness)", 0.0, 0.5, self.wind, "wind", res=0.01)
        self.create_slider(self.aim_frame, "Max Speed", 1.0, 30.0, self.max_speed, "max_speed", res=0.1)

        # === ВКЛАДКА TRIGGER ===
        self.trig_frame = tk.Frame(self.content_container, bg="#050505")
        tk.Checkbutton(self.trig_frame, text="Enable Event Trigger", variable=self.trigger_enabled, bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#b042ff", font=("Candara", 10)).pack(anchor="w")
        bind_frame_trig = tk.Frame(self.trig_frame, bg="#050505")
        bind_frame_trig.pack(fill="x", pady=5)
        tk.Label(bind_frame_trig, text="Trigger Activation:", bg="#050505", fg="#e0e0e0").pack(side="left")
        self.bind_btn_trig = tk.Button(bind_frame_trig, text=f"Bind: {get_vk_name(self.trigger_key)}", bg="#1a1a1a", fg="#b042ff", bd=0, command=lambda: self.start_keybind('trig'))
        self.bind_btn_trig.pack(side="right", padx=5)
        self.create_inline_color_picker(self.trig_frame, "Action Base Color", "trigger_color")
        self.create_slider(self.trig_frame, "Trigger Tolerance", 0, 100, self.trigger_tolerance, "trigger_tolerance")
        tk.Label(self.trig_frame, text="- Humanized Constraints -", bg="#050505", fg="#5A5A5A", font=("Candara", 9)).pack(pady=(10, 0))
        self.create_slider(self.trig_frame, "Min Reaction (s)", 0.0, 0.1, self.min_reaction, "min_reaction", res=0.005)
        self.create_slider(self.trig_frame, "Max Reaction (s)", 0.0, 0.2, self.max_reaction, "max_reaction", res=0.005)
        self.create_slider(self.trig_frame, "Min Hold (s)", 0.01, 0.1, self.min_hold, "min_hold", res=0.01)
        self.create_slider(self.trig_frame, "Max Hold (s)", 0.02, 0.2, self.max_hold, "max_hold", res=0.01)

        # === ВКЛАДКА CAPTURE ===
        self.capture_frame = tk.Frame(self.content_container, bg="#050505")
        tk.Label(self.capture_frame, text="Video Source (2PC Capture)", bg="#050505", fg="#e0e0e0", font=("Candara", 10, "bold")).pack(anchor="w", pady=(5,0))
        tk.Radiobutton(self.capture_frame, text="Local Screen (mss) - For 1PC", variable=self.capture_source_var, value="screen", bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#b042ff", font=("Candara", 10)).pack(anchor="w")
        tk.Radiobutton(self.capture_frame, text="Capture Card (USB HDMI) - For 2PC", variable=self.capture_source_var, value="capture", bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#b042ff", font=("Candara", 10)).pack(anchor="w")
        
        list_frame = tk.Frame(self.capture_frame, bg="#050505")
        list_frame.pack(fill="x", pady=5)
        self.device_combo = ttk.Combobox(list_frame, state="readonly", width=35)
        self.device_combo.pack(side="left", padx=2)
        self.device_combo.bind("<<ComboboxSelected>>", self.on_device_selected)
        btn_refresh = tk.Button(list_frame, text="↻", bg="#1a1a1a", fg="#b042ff", bd=0, command=self.refresh_devices, width=3)
        btn_refresh.pack(side="left", padx=2)
        
        # Галочка превью теперь работает безопасно через Tkinter
        tk.Checkbutton(self.capture_frame, text="Show Live Capture Preview (Debug)", variable=self.show_preview_window, bg="#050505", fg="#00ff00", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#00ff00", font=("Candara", 10, "bold")).pack(anchor="w", pady=(10,0))
        
        btn_start_cap = tk.Button(self.capture_frame, text="Start Video Stream", bg="#1a1a1a", fg="#b042ff", bd=0, command=self.start_capture_card)
        btn_start_cap.pack(fill="x", pady=5)
        self.capture_status_label = tk.Label(self.capture_frame, textvariable=self.capture_status_text, bg="#050505", fg="#a0a0a0", font=("Candara", 9))
        self.capture_status_label.pack(anchor="w")
        self.refresh_devices()

        # === ВКЛАДКА MAKCU ===
        self.makcu_frame = tk.Frame(self.content_container, bg="#050505")
        tk.Label(self.makcu_frame, text="Makcu Device", bg="#050505", fg="#e0e0e0", font=("Candara", 10, "bold")).pack(anchor="w", pady=(5,0))
        self.makcu_status_label = tk.Label(self.makcu_frame, text="Status: Not connected", bg="#050505", fg="#a0a0a0", font=("Candara", 9))
        self.makcu_status_label.pack(anchor="w")
        btn_auto = tk.Button(self.makcu_frame, text="Auto-connect", bg="#1a1a1a", fg="#b042ff", bd=0, command=self.auto_connect_makcu)
        btn_auto.pack(fill="x", pady=5)
        btn_disconnect = tk.Button(self.makcu_frame, text="Disconnect Makcu", bg="#1a1a1a", fg="#b042ff", bd=0, command=self.disconnect_makcu)
        btn_disconnect.pack(fill="x", pady=2)
        self.makcu_note = tk.Label(self.makcu_frame, text="If Makcu not connected,\nmouse_event will be used (Local PC2)", bg="#050505", fg="#5A5A5A", font=("Candara", 8))
        self.makcu_note.pack(pady=(10,0))

        self.aim_frame.pack(fill="both", expand=True)

        bottom_frame = tk.Frame(self.root, bg="#050505")
        bottom_frame.pack(fill="x", side="bottom", padx=15, pady=10)
        self.toggle_btn = tk.Button(bottom_frame, text="START ENGINE", bg="#1a1a1a", fg="#b042ff",
                                    font=("Arial", 9, "bold"), bd=0,
                                    activebackground="#2a2a2a", activeforeground="#ffffff",
                                    command=self.toggle_active, height=2, cursor="hand2")
        self.toggle_btn.pack(fill="x")
        tk.Label(bottom_frame, text="Press [END] to Hide Menu\nRight-Click colors to Copy/Paste", bg="#050505", fg="#3D3D3D", font=("Segoe UI", 7)).pack(pady=(2,0))

    def make_draggable(self, widget):
        widget.bind("<Button-1>", self.on_drag_start)
        widget.bind("<B1-Motion>", self.on_drag_motion)

    def on_drag_start(self, event):
        self.root._drag_data = {"x": event.x, "y": event.y}

    def on_drag_motion(self, event):
        delta_x = event.x - self.root._drag_data["x"]
        delta_y = event.y - self.root._drag_data["y"]
        x = self.root.winfo_x() + delta_x
        y = self.root.winfo_y() + delta_y
        self.root.geometry(f"+{x}+{y}")

    def show_aim_tab(self):
        self.trig_frame.pack_forget(); self.capture_frame.pack_forget(); self.makcu_frame.pack_forget()
        self.aim_frame.pack(fill="both", expand=True)
        self.btn_tab_aim.config(bg="#36005c"); self.btn_tab_trig.config(bg="#12001f")
        self.btn_tab_capture.config(bg="#12001f"); self.btn_tab_makcu.config(bg="#12001f")

    def show_trig_tab(self):
        self.aim_frame.pack_forget(); self.capture_frame.pack_forget(); self.makcu_frame.pack_forget()
        self.trig_frame.pack(fill="both", expand=True)
        self.btn_tab_trig.config(bg="#36005c"); self.btn_tab_aim.config(bg="#12001f")
        self.btn_tab_capture.config(bg="#12001f"); self.btn_tab_makcu.config(bg="#12001f")

    def show_capture_tab(self):
        self.aim_frame.pack_forget(); self.trig_frame.pack_forget(); self.makcu_frame.pack_forget()
        self.capture_frame.pack(fill="both", expand=True)
        self.btn_tab_capture.config(bg="#36005c"); self.btn_tab_aim.config(bg="#12001f")
        self.btn_tab_trig.config(bg="#12001f"); self.btn_tab_makcu.config(bg="#12001f")

    def show_makcu_tab(self):
        self.aim_frame.pack_forget(); self.trig_frame.pack_forget(); self.capture_frame.pack_forget()
        self.makcu_frame.pack(fill="both", expand=True)
        self.btn_tab_makcu.config(bg="#36005c"); self.btn_tab_aim.config(bg="#12001f")
        self.btn_tab_trig.config(bg="#12001f"); self.btn_tab_capture.config(bg="#12001f")

    def refresh_devices(self):
        def _refresh():
            devices = self.capture_manager.list_capture_devices()
            self.capture_device_list = devices
            self.root.after(0, self._update_device_combo)
        threading.Thread(target=_refresh, daemon=True).start()

    def _update_device_combo(self):
        if self.capture_device_list:
            self.device_combo['values'] = self.capture_device_list
            self.device_combo.current(0)
            self._extract_device_index(self.capture_device_list[0])
        else:
            self.device_combo['values'] = []
            self.capture_status_text.set("No devices found")

    def on_device_selected(self, event):
        selection_idx = self.device_combo.current()
        if 0 <= selection_idx < len(self.capture_device_list):
            selected_str = self.capture_device_list[selection_idx]
            self._extract_device_index(selected_str)

    def _extract_device_index(self, selected_str):
        if ":" in selected_str:
            try: self.capture_device_index = int(selected_str.split(":")[0])
            except: pass
        elif "Device" in selected_str:
            try: self.capture_device_index = int(selected_str.split(" ")[1])
            except: pass

    def start_capture_card(self):
        def _start():
            if self.capture_source_var.get() == "screen":
                self.root.after(0, lambda: self.capture_status_text.set("Ready: Local Screen (mss)"))
            else:
                self.root.after(0, lambda: self.capture_status_text.set("Connecting to Capture Card (MJPG)..."))
                try:
                    self.capture_manager.start_capture(self.capture_device_index)
                    # Ждем чуть дольше, некоторым картам захвата нужно время на ресет порта
                    time.sleep(2.0) 
                    frame = self.capture_manager.get_frame()
                    if frame is not None:
                        h, w = frame.shape[:2]
                        self.root.after(0, lambda: self.capture_status_text.set(f"Stream Active: {w}x{h}"))
                    else:
                        self.capture_manager.stop_capture()
                        self.root.after(0, lambda: self.capture_status_text.set("Stream Error (Black Screen)"))
                        self.root.after(0, lambda: messagebox.showerror("Capture", "Не удалось получить кадр. Возможно карта занята OBS."))
                except Exception as e:
                    self.root.after(0, lambda: self.capture_status_text.set(f"Error: {e}"))
        threading.Thread(target=_start, daemon=True).start()

    def auto_connect_makcu(self):
        def _auto():
            try:
                if self.makcu.auto_connect():
                    self.root.after(0, self._update_makcu_status_connected)
                    self.root.after(0, lambda: messagebox.showinfo("Makcu", "Успешное подключение. Аппаратный обход включен."))
                else:
                    self.root.after(0, lambda: messagebox.showerror("Makcu", "Подключение не удалось. Проверьте кабель и драйвер CH343."))
            except Exception as e:
                self.root.after(0, lambda: messagebox.showerror("Makcu", str(e)))
        threading.Thread(target=_auto, daemon=True).start()

    def connect_makcu(self):
        self.auto_connect_makcu()

    def disconnect_makcu(self):
        self.makcu.disconnect()
        self.makcu_status_label.config(text="Status: Not connected", fg="#a0a0a0")

    def create_inline_color_picker(self, parent, label_text, target_attr):
        frame = tk.Frame(parent, bg="#0a0a0a", highlightthickness=1, highlightbackground="#1a1a1a")
        frame.pack(fill="x", pady=5)
        top_frame = tk.Frame(frame, bg="#0a0a0a")
        top_frame.pack(fill="x", padx=5, pady=5)
        tk.Label(top_frame, text=label_text, bg="#0a0a0a", fg="#e0e0e0", font=("Candara", 9, "bold")).pack(side="left")
        preview_canvas = tk.Canvas(top_frame, width=24, height=24, bg="#ffffff", highlightthickness=1, highlightbackground="#36005c", cursor="hand2")
        preview_canvas.pack(side="right")
        color_label = tk.Label(top_frame, text="", bg="#0a0a0a", fg="#a0a0a0", font=("Consolas", 8), cursor="hand2")
        color_label.pack(side="right", padx=10)
        sliders_frame = tk.Frame(frame, bg="#0a0a0a")
        sliders_frame.pack(fill="x", padx=5, pady=(0, 5))

        current_rgb = getattr(self, target_attr)
        r_var, g_var, b_var = tk.IntVar(value=current_rgb[0]), tk.IntVar(value=current_rgb[1]), tk.IntVar(value=current_rgb[2])

        def update_color(*args):
            r, g, b = r_var.get(), g_var.get(), b_var.get()
            setattr(self, target_attr, (r, g, b))
            setattr(self, target_attr + '_bgr', (b, g, r))
            self._update_hsv_cache()
            hex_color = '#%02x%02x%02x' % (r, g, b)
            color_label.config(text=f"({r},{g},{b}) {hex_color.upper()}")
            preview_canvas.config(bg=hex_color)

        def copy_color():
            hex_color = '#%02x%02x%02x' % (r_var.get(), g_var.get(), b_var.get())
            self.root.clipboard_clear()
            self.root.clipboard_append(hex_color.upper())

        def paste_color():
            try:
                cb = self.root.clipboard_get().strip().replace(' ', '')
                if len(cb) == 7 and cb.startswith('#'):
                    r, g, b = tuple(int(cb.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))
                elif ',' in cb:
                    parts = cb.split(',')
                    if len(parts) == 3:
                        r, g, b = int(parts[0]), int(parts[1]), int(parts[2])
                else: return
                r_var.set(r); g_var.set(g); b_var.set(b)
                update_color()
            except: pass

        c_menu = tk.Menu(self.root, tearoff=0, bg="#1a1a1a", fg="#e0e0e0", bd=0, activebackground="#36005c")
        c_menu.add_command(label="Copy Hex", command=copy_color)
        c_menu.add_command(label="Paste Color", command=paste_color)

        def show_menu(event): c_menu.tk_popup(event.x_root, event.y_root)
        preview_canvas.bind("<Button-3>", show_menu); color_label.bind("<Button-3>", show_menu)

        r_scale = tk.Scale(sliders_frame, from_=0, to=255, variable=r_var, orient="horizontal", bg="#0a0a0a", fg="#ff4d4d", troughcolor="#1a0000", highlightthickness=0, bd=0, activebackground="#330000", command=update_color, showvalue=0)
        r_scale.pack(fill="x")
        g_scale = tk.Scale(sliders_frame, from_=0, to=255, variable=g_var, orient="horizontal", bg="#0a0a0a", fg="#4dff4d", troughcolor="#001a00", highlightthickness=0, bd=0, activebackground="#003300", command=update_color, showvalue=0)
        g_scale.pack(fill="x")
        b_scale = tk.Scale(sliders_frame, from_=0, to=255, variable=b_var, orient="horizontal", bg="#0a0a0a", fg="#4d4dff", troughcolor="#00001a", highlightthickness=0, bd=0, activebackground="#000033", command=update_color, showvalue=0)
        b_scale.pack(fill="x")
        update_color()

    def create_slider(self, parent, label_text, min_val, max_val, default_val, attr_name, res=1):
        frame = tk.Frame(parent, bg="#050505")
        frame.pack(fill="x", pady=2)
        tk.Label(frame, text=label_text, bg="#050505", fg="#b5b5b5", font=("Candara", 9)).pack(anchor="w")
        slider = tk.Scale(frame, from_=min_val, to=max_val, resolution=res, orient="horizontal",
                          bg="#050505", fg="#b042ff", troughcolor="#1a1a1a", highlightthickness=0, bd=0, activebackground="#36005c")
        slider.set(default_val)
        slider.pack(fill="x")
        slider.config(command=lambda val, a=attr_name: self.update_attr(a, val))

    def update_attr(self, attr_name, val):
        setattr(self, attr_name, float(val) if '.' in str(val) else int(float(val)))
        if attr_name in ('tolerance', 'trigger_tolerance'):
            self._update_hsv_cache()

    def start_keybind(self, target='aim'):
        if getattr(self, f'_binding_{target}', False): return
        setattr(self, f'_binding_{target}', True)
        btn = self.bind_btn_aim if target == 'aim' else self.bind_btn_trig
        btn.config(text="Listening for input...")
        threading.Thread(target=self._wait_for_key, args=(target,), daemon=True).start()

    def _wait_for_key(self, target):
        time.sleep(0.4) 
        while self.running:
            if self.makcu.connected and self.makcu.controller:
                for vk_code, makcu_btn in self.makcu.button_mapping.items():
                    try:
                        if self.makcu.controller.is_pressed(makcu_btn):
                            self._apply_bind(target, vk_code)
                            return
                    except: pass
            
            for vk in range(1, 256):
                if ctypes.windll.user32.GetAsyncKeyState(vk) & 0x8000:
                    if vk != 0x1B:  
                        self._apply_bind(target, vk)
                        return
            time.sleep(0.01)

    def _apply_bind(self, target, vk_code):
        if target == 'aim': self.tracking_key = vk_code
        else: self.trigger_key = vk_code
        btn = self.bind_btn_aim if target == 'aim' else self.bind_btn_trig
        val = self.tracking_key if target == 'aim' else self.trigger_key
        self.root.after(0, lambda b=btn, v=val: b.config(text=f"Bind: {get_vk_name(v)}"))
        setattr(self, f'_binding_{target}', False)

    def toggle_active(self):
        self.active = not self.active
        if self.active:
            self.toggle_btn.config(text="STOP ENGINE", fg="#ff3333", bg="#260000", activebackground="#400000")
        else:
            self.toggle_btn.config(text="START ENGINE", fg="#b042ff", bg="#1a1a1a", activebackground="#2a2a2a")

    def exit_app(self):
        self.running = False
        self.capture_manager.stop_capture()
        self.makcu.disconnect()
        self.root.destroy()
        sys.exit()

    def key_listener(self):
        while self.running:
            if win32api.GetAsyncKeyState(win32con.VK_END) & 1:
                if self.visible: self.root.after(0, self.root.withdraw)
                else:
                    self.root.after(0, self.root.deiconify)
                    self.root.after(0, self.root.lift)
                    self.root.after(0, self.root.focus_force)
                self.visible = not self.visible
            time.sleep(0.1)

    # ---------- Overlay ----------
    def setup_overlay(self):
        self.overlay = tk.Toplevel(self.root)
        self.overlay.overrideredirect(True)
        self.overlay.attributes("-topmost", True)
        self.overlay.attributes("-transparentcolor", "black")
        self.overlay.config(bg="black")
        self.overlay.geometry(f"{self.screen_width}x{self.screen_height}+0+0")
        hwnd = ctypes.windll.user32.GetParent(self.overlay.winfo_id())
        style = ctypes.windll.user32.GetWindowLongW(hwnd, -20)
        ctypes.windll.user32.SetWindowLongW(hwnd, -20, style | 0x00080000 | 0x00000020)
        
        self.overlay_canvas = tk.Canvas(self.overlay, width=self.screen_width, height=self.screen_height, bg="black", highlightthickness=0)
        self.overlay_canvas.pack()
        self.overlay_circle = self.overlay_canvas.create_oval(0, 0, 0, 0, outline="white", width=1)
        self.last_fov = self.fov; self.last_show = self.show_fov_overlay.get()
        self.update_overlay_loop()

    def update_overlay_loop(self):
        current_show = self.show_fov_overlay.get()
        current_fov = self.fov
        if current_show != self.last_show or current_fov != self.last_fov:
            if current_show:
                self.overlay.deiconify()
                r = int(current_fov) // 2
                self.overlay_canvas.coords(self.overlay_circle, self.local_center_x - r, self.local_center_y - r, self.local_center_x + r, self.local_center_y + r)
            else:
                self.overlay.withdraw()
            self.last_show = current_show; self.last_fov = current_fov
        self.root.after(200, self.update_overlay_loop)

    # ---------- GUI Превью (безопасное для потоков) ----------
    def update_preview_ui_loop(self):
        if self.show_preview_window.get() and PILLOW_AVAILABLE:
            if self.preview_tk_window is None or not self.preview_tk_window.winfo_exists():
                self.preview_tk_window = tk.Toplevel(self.root)
                self.preview_tk_window.title("FOV Live Preview (2PC)")
                self.preview_tk_window.attributes("-topmost", True)
                self.preview_tk_window.geometry("300x300")
                self.preview_tk_window.configure(bg="black")
                self.preview_tk_label = tk.Label(self.preview_tk_window, bg="black")
                self.preview_tk_label.pack(fill="both", expand=True)

            if self._latest_preview_frame is not None:
                # Pillow требует RGB, а не BGR
                rgb_img = cv2.cvtColor(self._latest_preview_frame, cv2.COLOR_BGR2RGB)
                img = Image.fromarray(rgb_img)
                imgtk = ImageTk.PhotoImage(image=img)
                self.preview_tk_label.config(image=imgtk)
                self.preview_tk_label.image = imgtk # Защита от сборщика мусора
        else:
            if self.preview_tk_window is not None and self.preview_tk_window.winfo_exists():
                self.preview_tk_window.destroy()
                self.preview_tk_window = None
                self.preview_tk_label = None

        self.root.after(30, self.update_preview_ui_loop) # Обновление 30 раз в секунду

    # ---------- Физика движения ----------
    def _generate_noise(self):
        self.noise_velocity_x += random.uniform(-0.1, 0.1)
        self.noise_velocity_y += random.uniform(-0.1, 0.1)
        self.noise_velocity_x *= 0.98
        self.noise_velocity_y *= 0.98
        self.noise_state_x += self.noise_velocity_x
        self.noise_state_y += self.noise_velocity_y
        self.noise_state_x = max(-2.0, min(2.0, self.noise_state_x))
        self.noise_state_y = max(-2.0, min(2.0, self.noise_state_y))
        return self.noise_state_x, self.noise_state_y

    def calculate_smooth_step(self, target_dx, target_dy):
        distance = math.hypot(target_dx, target_dy)
        if distance < 1.0:
            self.velocity_x, self.velocity_y = 0.0, 0.0
            self.remainder_x, self.remainder_y = 0.0, 0.0
            return 0.0, 0.0

        noise_x, noise_y = self._generate_noise()
        noise_x *= self.wind * (distance ** 0.3)
        noise_y *= self.wind * (distance ** 0.3)

        force_scale = self.gravity
        if distance < 10: force_scale *= 0.6

        accel_x = (target_dx * force_scale) + noise_x
        accel_y = (target_dy * force_scale) + noise_y

        if self.velocity_x != 0 or self.velocity_y != 0:
            dot_product = (self.velocity_x * accel_x + self.velocity_y * accel_y)
            if dot_product < 0:
                self.velocity_x *= 0.3; self.velocity_y *= 0.3

        current_damping = 0.65 if distance < 15 else 0.85
        self.velocity_x = (self.velocity_x + accel_x) * current_damping
        self.velocity_y = (self.velocity_y + accel_y) * current_damping

        speed = math.hypot(self.velocity_x, self.velocity_y)
        if speed > self.max_speed:
            scale = self.max_speed / speed
            self.velocity_x *= scale; self.velocity_y *= scale

        return self.velocity_x, self.velocity_y

    def mouse_movement_loop(self):
        while self.running:
            if not self.active or not self.tracking_enabled.get():
                time.sleep(0.01)
                continue

            current_time = time.perf_counter()
            jitter_factor = 1.0 + random.uniform(-self.mouse_jitter, self.mouse_jitter)
            dynamic_interval = self.mouse_send_interval * jitter_factor

            if random.random() < self.mouse_skip_probability:
                time.sleep(random.uniform(0.005, 0.015))
                continue

            if current_time - self.last_mouse_send_time >= dynamic_interval:
                with self.mouse_lock:
                    has_target = self.has_target
                    rel_x = self.target_rel_x
                    rel_y = self.target_rel_y

                    if has_target:
                        noise_x = random.uniform(-self.mouse_target_noise, self.mouse_target_noise)
                        noise_y = random.uniform(-self.mouse_target_noise, self.mouse_target_noise)
                        vx, vy = self.calculate_smooth_step(rel_x + noise_x, rel_y + noise_y)
                        self.pending_move_x += vx
                        self.pending_move_y += vy
                    else:
                        self.velocity_x *= 0.7; self.velocity_y *= 0.7
                        self.pending_move_x += self.velocity_x
                        self.pending_move_y += self.velocity_y
                        if abs(self.velocity_x) < 0.1: self.velocity_x = 0.0
                        if abs(self.velocity_y) < 0.1: self.velocity_y = 0.0

                    current_speed = math.hypot(self.pending_move_x, self.pending_move_y)
                    if current_speed > self.mouse_max_step:
                        scale_factor = self.mouse_max_step / current_speed
                        self.pending_move_x *= scale_factor
                        self.pending_move_y *= scale_factor

                    step_x = int(round(self.pending_move_x + self.remainder_x))
                    step_y = int(round(self.pending_move_y + self.remainder_y))
                    self.remainder_x = (self.pending_move_x + self.remainder_x) - step_x
                    self.remainder_y = (self.pending_move_y + self.remainder_y) - step_y

                    final_step_x = step_x; final_step_y = step_y
                    self.pending_move_x = 0.0; self.pending_move_y = 0.0

                if final_step_x != 0 or final_step_y != 0:
                    if self.makcu.connected: self.makcu.send_mouse_move(final_step_x, final_step_y)
                    else: ctypes.windll.user32.mouse_event(MOUSEEVENTF_MOVE, final_step_x, final_step_y, 0, 0)

                self.last_mouse_send_time = current_time
            else:
                sleep_time = max(0, dynamic_interval - (current_time - self.last_mouse_send_time))
                if sleep_time > 0.002: time.sleep(sleep_time * 0.5)
                else: time.sleep(0.0001)

    # ---------- Триггер ----------
    def trigger_worker(self):
        while self.running:
            try:
                item = self.trigger_queue.get(timeout=0.1)
                if item is None: continue
                self._do_trigger()
                self.trigger_queue.task_done()
            except queue.Empty: pass

    def _do_trigger(self):
        reaction = random.lognormvariate(math.log((self.min_reaction + self.max_reaction) / 2), 0.5)
        reaction = max(self.min_reaction, min(self.max_reaction, reaction))
        if reaction > 0.002: time.sleep(reaction)

        hold = random.lognormvariate(math.log((self.min_hold + self.max_hold) / 2), 0.5)
        hold = max(self.min_hold, min(self.max_hold, hold))

        shots = random.choices([1, 2, 3], weights=[70, 20, 10])[0]
        for _ in range(shots):
            if self.makcu.connected:
                self.makcu.send_mouse_click(0x01, True)
                time.sleep(hold)
                self.makcu.send_mouse_click(0x01, False)
            else:
                ctypes.windll.user32.mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
                time.sleep(hold)
                ctypes.windll.user32.mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
            time.sleep(random.uniform(0.03, 0.08))
        self.is_clicking = False

    def execute_trigger(self):
        if self.is_clicking: return
        if random.random() < 0.03: return
        self.is_clicking = True
        self.trigger_queue.put(True)

    # ---------- Основной цикл распознавания ----------
    def logic_loop(self):
        sct = mss.mss()
        while self.running:
            
            if not self.active:
                time.sleep(0.05)
                continue

            loop_start = time.perf_counter()
            current_fov = int(self.fov)

            # --- 1. Оптимизированный захват ДО обработки ---
            if self.capture_source_var.get() == "screen":
                left = max(0, self.local_center_x - current_fov // 2)
                top = max(0, self.local_center_y - current_fov // 2)
                width = min(current_fov, self.screen_width - left)
                height = min(current_fov, self.screen_height - top)
                if width <= 0 or height <= 0: continue
                region = {"left": left, "top": top, "width": width, "height": height}
                try:
                    img_data = sct.grab(region)
                    img_bgra = np.frombuffer(img_data.bgra, dtype=np.uint8).reshape((height, width, 4))
                    img_bgr = img_bgra[:, :, :3]
                except: continue
                if width < current_fov or height < current_fov:
                    pad_x = (current_fov - width) // 2
                    pad_y = (current_fov - height) // 2
                    img_bgr = cv2.copyMakeBorder(img_bgr, pad_y, current_fov - height - pad_y, pad_x, current_fov - width - pad_x, cv2.BORDER_CONSTANT, value=(0,0,0))
            else:
                if not self.capture_manager.is_active():
                    time.sleep(0.05)
                    continue
                
                frame = self.capture_manager.get_frame()
                if frame is None:
                    continue
                    
                h, w = frame.shape[:2]
                cx, cy = w // 2, h // 2
                half_fov = current_fov // 2
                
                # Вырезаем зону FOV из центра карты захвата
                x1 = max(0, cx - half_fov)
                y1 = max(0, cy - half_fov)
                x2 = min(w, cx + half_fov)
                y2 = min(h, cy + half_fov)
                
                img_bgr = frame[y1:y2, x1:x2]
                
                if img_bgr.shape[0] != current_fov or img_bgr.shape[1] != current_fov:
                    img_bgr = cv2.copyMakeBorder(img_bgr, 0, current_fov - img_bgr.shape[0], 0, current_fov - img_bgr.shape[1], cv2.BORDER_CONSTANT, value=(0,0,0))

            if 'img_bgr' in locals() and img_bgr.size > 0:
                act_h, act_w = img_bgr.shape[:2]
                center_x, center_y = act_w // 2, act_h // 2
                hsv_fov = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)

                debug_target_pos = None
                trigger_zone_size = 4
                y1_t = max(0, center_y - trigger_zone_size)
                y2_t = min(act_h, center_y + trigger_zone_size)
                x1_t = max(0, center_x - trigger_zone_size)
                x2_t = min(act_w, center_x + trigger_zone_size)

                # --- 2. Триггер ---
                if self.trigger_enabled.get():
                    now = time.perf_counter()
                    if now - self.last_trigger_time >= self.trigger_interval:
                        self.last_trigger_time = now
                        if self.makcu.is_button_pressed(self.trigger_key):
                            hsv_area = hsv_fov[y1_t:y2_t, x1_t:x2_t]
                            trig_mask = self.apply_mask(hsv_area, self.trig_bounds)
                            if trig_mask is not None and cv2.countNonZero(trig_mask) > 4:
                                self.execute_trigger()

                # --- 3. Наводка ---
                if self.tracking_enabled.get():
                    now = time.perf_counter()
                    if now - self.last_aim_time >= self.aim_interval:
                        self.last_aim_time = now
                        if self.makcu.is_button_pressed(self.tracking_key):
                            mask = self.apply_mask(hsv_fov, self.aim_bounds)
                            if mask is not None and cv2.countNonZero(mask) > 0:
                                contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
                                best_score = -float('inf')
                                best_center = None

                                if self.last_target_center is not None:
                                    last_cx, last_cy = self.last_target_center

                                for contour in contours:
                                    area = cv2.contourArea(contour)
                                    if area < 2: continue
                                    M = cv2.moments(contour)
                                    if M["m00"] == 0: continue
                                    cx_exact = M["m10"] / M["m00"]
                                    cy_exact = M["m01"] / M["m00"]

                                    dist_to_center = math.hypot(cx_exact - center_x, cy_exact - center_y)
                                    score = -dist_to_center + 10.0 * math.log(area + 1)

                                    if self.last_target_center is not None:
                                        dist_to_last = math.hypot(cx_exact - last_cx, cy_exact - last_cy)
                                        if dist_to_last < 15: score += 30.0
                                        else: score -= min(20.0, dist_to_last * 0.5)

                                    if score > best_score:
                                        best_score = score
                                        best_center = (cx_exact, cy_exact)

                                if best_center is not None:
                                    target_x, target_y = best_center
                                    debug_target_pos = best_center
                                    self.last_raw_target_x = target_x
                                    self.last_raw_target_y = target_y
                                    self.last_target_center = (target_x, target_y)
                                else:
                                    self.last_target_center = None
                                    target_x, target_y = center_x, center_y
                            else:
                                self.last_target_center = None
                                self.last_raw_target_x = None
                                self.last_raw_target_y = None
                                with self.mouse_lock: self.has_target = False
                                
                                if self.show_preview_window.get():
                                    self._prepare_preview(img_bgr, center_x, center_y, x1_t, y1_t, x2_t, y2_t, None)
                                continue

                            if self.smooth_target_x is None:
                                self.smooth_target_x = target_x
                                self.smooth_target_y = target_y
                            else:
                                if self.last_raw_target_x is not None:
                                    jump_dist = math.hypot(target_x - self.last_raw_target_x, target_y - self.last_raw_target_y)
                                else: jump_dist = 0

                                jump_threshold = max(15.0, current_fov * 0.15)
                                if jump_dist > jump_threshold:
                                    self.smooth_target_x = target_x
                                    self.smooth_target_y = target_y
                                    self.velocity_x = 0.0; self.velocity_y = 0.0
                                    with self.mouse_lock:
                                        self.pending_move_x = 0.0; self.pending_move_y = 0.0
                                        self.remainder_x = 0.0; self.remainder_y = 0.0
                                else:
                                    alpha = self.smoothing_factor
                                    self.smooth_target_x = alpha * self.smooth_target_x + (1 - alpha) * target_x
                                    self.smooth_target_y = alpha * self.smooth_target_y + (1 - alpha) * target_y

                            with self.mouse_lock:
                                self.has_target = True
                                self.target_rel_x = self.smooth_target_x - center_x
                                self.target_rel_y = self.smooth_target_y - center_y
                        else:
                            with self.mouse_lock:
                                self.has_target = False
                                self.target_rel_x = 0.0; self.target_rel_y = 0.0
                            self.smooth_target_x = None; self.smooth_target_y = None
                            self.velocity_x = 0.0; self.velocity_y = 0.0
                            self.last_target_center = None
                            self.last_raw_target_x = None; self.last_raw_target_y = None

                if self.show_preview_window.get():
                    self._prepare_preview(img_bgr, center_x, center_y, x1_t, y1_t, x2_t, y2_t, debug_target_pos)

            elapsed = time.perf_counter() - loop_start
            sleep_time = self.scan_interval - elapsed
            if sleep_time > 0:
                time.sleep(sleep_time)

    def _prepare_preview(self, img_bgr, cx, cy, tx1, ty1, tx2, ty2, target_pos):
        """Подготавливает кадр для безопасного рендера в Tkinter."""
        debug_frame = img_bgr.copy()
        
        cv2.line(debug_frame, (cx, 0), (cx, debug_frame.shape[0]), (50, 50, 50), 1)
        cv2.line(debug_frame, (0, cy), (debug_frame.shape[1], cy), (50, 50, 50), 1)
        cv2.rectangle(debug_frame, (tx1, ty1), (tx2, ty2), (0, 255, 255), 1)
        
        if target_pos is not None:
            cv2.circle(debug_frame, (int(target_pos[0]), int(target_pos[1])), 4, (0, 255, 0), -1)
            cv2.line(debug_frame, (cx, cy), (int(target_pos[0]), int(target_pos[1])), (0, 255, 0), 1)
            
        scale = 3
        self._latest_preview_frame = cv2.resize(debug_frame, (debug_frame.shape[1]*scale, debug_frame.shape[0]*scale), interpolation=cv2.INTER_NEAREST)

    def run(self):
        self.root.mainloop()

if __name__ == "__main__":
    bot = StealthColorBot()
    bot.run()