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


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

# Оптимизация OpenCV для максимальной производительности на слабых CPU
cv2.setUseOptimized(True)
cv2.setNumThreads(max(1, cv2.getNumberOfCPUs() - 1))
cv2.ocl.setUseOpenCL(False) # Отключение OpenCL предотвращает bottleneck на шине PCI-E у ноутбуков

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

try:
    from pygrabber.dshow_graph import FilterGraph
    PYGRABBER_AVAILABLE = True
except ImportError:
    PYGRABBER_AVAILABLE = False

try:
    from PIL import Image, ImageTk
    PILLOW_AVAILABLE = True
except ImportError:
    PILLOW_AVAILABLE = False

# Повышение точности таймеров Windows для высокочастотного Mouse Loop
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 Left", 0x02: "Makcu Right", 0x04: "Makcu Middle",
        0x05: "Makcu Mouse 4", 0x06: "Makcu Mouse 5", 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():
    try: ctypes.windll.kernel32.SetConsoleTitleW(str(uuid.uuid4())[:8])
    except: pass
set_random_console_title()

class CaptureManager:
    """
    Оптимизированный менеджер захвата видеосигнала.
    Реализует концепцию Zero-Copy и Drop-Stale для устранения Input Lag на USB-картах.
    """
    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: pass
        
        devices = []
        for index in range(5):
            cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
            if cap.isOpened():
                devices.append(f"Device {index} (Generic)")
                cap.release()
        return devices

    def start_capture(self, device_index, width=1920, height=1080):
        self.stop_capture()
        self.device_index = device_index
        
        # Инстанцирование захвата с приоритетом на минимальную задержку (Latency optimization)
        self.capture = cv2.VideoCapture(device_index, cv2.CAP_DSHOW)
        if not self.capture.isOpened():
            raise Exception(f"Не удалось открыть устройство {device_index}")
        
        self.capture.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
        self.capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
        self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, width)
        self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
        self.capture.set(cv2.CAP_PROP_FPS, 60)
        
        # Критическая оптимизация: Жесткое ограничение буфера DirectShow
        self.capture.set(cv2.CAP_PROP_BUFFERSIZE, 1) 
        
        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():
            # Механизм Drop-Stale: Перехват кадров без декодирования для очистки буфера
            self.capture.grab()
            ret, frame = self.capture.retrieve()
            
            if ret:
                empty_frames = 0
                with self.lock:
                    self.current_frame = frame
            else:
                empty_frames += 1
                if empty_frames > 500:
                    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=1)
        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:
    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:
            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)
        if self.connected and self.controller and makcu_btn:
            try:
                if self.controller.is_pressed(makcu_btn): return True
            except: pass
        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 AdvancedKinematics:
    """
    Математическая модель ПИД-регулятора (Proportional-Integral-Derivative Controller).
    Включает фильтр нижних частот (Low-Pass Filter) для подавления производного удара (Derivative Kick)
    и защиту от интегрального насыщения (Integral Windup Clamp).
    """
    def __init__(self):
        # Настройки калибровки (Ziegler-Nichols baseline)
        self.Kp = 2.8    # Пропорциональный коэффициент (определяет агрессивность отклика)
        self.Ki = 0.08   # Интегральный коэффициент (компенсирует статические ошибки и deadzone)
        self.Kd = 0.45   # Дифференциальный коэффициент (создает демпфирование, эффект Sticky Aim)
        
        self.integral_x = 0.0
        self.integral_y = 0.0
        self.prev_error_x = 0.0
        self.prev_error_y = 0.0
        self.prev_derivative_x = 0.0
        self.prev_derivative_y = 0.0
        
        self.lpf_alpha = 0.25 # Коэффициент экспоненциального сглаживания (EWMA) для фильтрации высокочастотных шумов
        self.max_integral = 40.0 # Предел интегрального накопления (Anti-Windup)
        
    def reset(self):
        self.integral_x = 0.0
        self.integral_y = 0.0
        self.prev_error_x = 0.0
        self.prev_error_y = 0.0
        self.prev_derivative_x = 0.0
        self.prev_derivative_y = 0.0

    def update(self, error_x, error_y, dt):
        if dt <= 0.0001: return 0.0, 0.0

        # Вычисление интеграла ошибки с жестким ограничением (Clamping) для предотвращения фазового сдвига
        self.integral_x += error_x * dt
        self.integral_y += error_y * dt
        self.integral_x = max(-self.max_integral, min(self.max_integral, self.integral_x))
        self.integral_y = max(-self.max_integral, min(self.max_integral, self.integral_y))

        # Вычисление производной с применением IIR-фильтра нижних частот 1-го порядка
        raw_derivative_x = (error_x - self.prev_error_x) / dt
        raw_derivative_y = (error_y - self.prev_error_y) / dt
        
        derivative_x = (self.lpf_alpha * raw_derivative_x) + ((1.0 - self.lpf_alpha) * self.prev_derivative_x)
        derivative_y = (self.lpf_alpha * raw_derivative_y) + ((1.0 - self.lpf_alpha) * self.prev_derivative_y)

        # Регистрация исторических состояний системы
        self.prev_error_x = error_x
        self.prev_error_y = error_y
        self.prev_derivative_x = derivative_x
        self.prev_derivative_y = derivative_y

        # Формирование управляющего воздействия (вектор скорости мыши в пикселях/сек)
        out_x = (self.Kp * error_x) + (self.Ki * self.integral_x) + (self.Kd * derivative_x)
        out_y = (self.Kp * error_y) + (self.Ki * self.integral_y) + (self.Kd * derivative_y)

        return out_x, out_y

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

        # Сокрытие процесса окна от поверхностного захвата и панели задач
        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.kinematics = AdvancedKinematics()
        
        self.mouse_lock = threading.Lock()
        
        # Асинхронная диспетчеризация: Vision Loop обновляет скорость, Mouse Loop интегрирует координаты
        self.target_velocity_x = 0.0
        self.target_velocity_y = 0.0
        self.remainder_x = 0.0
        self.remainder_y = 0.0

        self.tracking_enabled = tk.BooleanVar(value=True)
        self.tracking_key = 0x06  
        self.target_color = (250, 100, 250)
        self.target_color_bgr = (250, 100, 250)
        self.fov = 120
        self.tolerance = 15

        self.target_bone_var = tk.StringVar(value="HEAD")
        self.head_offset_y = 5 

        # Кинематические параметры экстернализированы для тонкой настройки
        self.pid_kp = 2.8
        self.pid_kd = 0.45
        self.max_speed = 900.0 # Предел баллистической скорости (пикселей/секунду)
        self.x_multiplier = 0.75  
        self.y_multiplier = 1.0
        self.deadzone = 2.0 

        self.trigger_enabled = tk.BooleanVar(value=False)
        self.trigger_key = 0x05  
        self.trigger_zone = 5 
        self.trigger_tolerance = 15
        self.is_clicking = False
        self.trigger_queue = queue.Queue()

        self.min_reaction = 0.015
        self.max_reaction = 0.045
        self.min_hold = 0.035
        self.max_hold = 0.075

        self.color_amp = 1
        self.min_area = 5 

        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.001
        self.mouse_send_rate = 500 # Частота дискретизации Polling Loop (500 Гц имитирует стандартный USB Rate)
        self.mouse_send_interval = 1.0 / self.mouse_send_rate
        self.last_mouse_send_time = time.perf_counter()
        
        self.last_aim_time = time.perf_counter()
        self.last_trigger_time = time.perf_counter()

        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.cap_res_width = tk.IntVar(value=1920)
        self.cap_res_height = tk.IntVar(value=1080)

        self.aim_bounds = []
        self.trig_bounds = []
        
        self.last_raw_target_x = None
        self.last_raw_target_y = None
        self.target_lost_frames = 0

        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()
        
        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, lambda: self.makcu_status_label.config(text="Status: Connected (SDK)", fg="#00ff00"))
                    break
                else:
                    time.sleep(3)
                    attempts += 1
                    if attempts > 10: break
            except: time.sleep(2)

    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_high = h - tol, 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.target_color_bgr, self.trigger_tolerance)

    def apply_mask(self, hsv_img, bounds, amplification):
        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)
                
        # Оптимизация морфологических ядер: 2x2 значительно снижает вычислительную сложность O(N*K^2)
        if amplification > 0 and final_mask is not None:
            kernel = np.ones((2, 2), np.uint8)
            final_mask = cv2.dilate(final_mask, kernel, iterations=int(amplification))
        return final_mask

    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="Kinematic Control (PID Enabled)", bg="#12001f", fg="#b042ff", font=("Consolas", 11, "bold"))
        title_lbl.pack(side="left", padx=10)
        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)

        style = ttk.Style()
        style.theme_use('default')
        style.configure('TNotebook', background='#050505', borderwidth=0)
        style.configure('TNotebook.Tab', background='#12001f', foreground='#e0e0e0', padding=[10, 2], font=("Candara", 9, "bold"))
        style.map('TNotebook.Tab', background=[('selected', '#36005c')], foreground=[('selected', '#ffffff')])

        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill="both", expand=True, padx=5, pady=5)

        self.aim_frame = tk.Frame(self.notebook, bg="#050505")
        self.trig_frame = tk.Frame(self.notebook, bg="#050505")
        self.vis_frame = tk.Frame(self.notebook, bg="#050505")
        self.capture_frame = tk.Frame(self.notebook, bg="#050505")
        self.makcu_frame = tk.Frame(self.notebook, bg="#050505")

        self.notebook.add(self.aim_frame, text="Aim")
        self.notebook.add(self.trig_frame, text="Trigger")
        self.notebook.add(self.vis_frame, text="Vision")
        self.notebook.add(self.capture_frame, text="Capture")
        self.notebook.add(self.makcu_frame, text="Makcu")

        tk.Checkbutton(self.aim_frame, text="Enable Target Acquisition", 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="Render Tracking Overlay", 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=2)
        tk.Label(bind_frame_aim, text="Acquisition Bind:", bg="#050505", fg="#e0e0e0").pack(side="left")
        self.bind_btn_aim = tk.Button(bind_frame_aim, text=f"Bound: {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_slider(self.aim_frame, "Global FOV Size (Pixels)", 10, 800, self.fov, "fov")
        
        bone_frame = tk.Frame(self.aim_frame, bg="#050505")
        bone_frame.pack(fill="x", pady=2)
        tk.Label(bone_frame, text="Target Bone:", bg="#050505", fg="#e0e0e0").pack(side="left")
        tk.Radiobutton(bone_frame, text="HEAD", variable=self.target_bone_var, value="HEAD", bg="#050505", fg="#00ff00", selectcolor="#1a1a1a").pack(side="left")
        tk.Radiobutton(bone_frame, text="CENTER", variable=self.target_bone_var, value="CENTER", bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a").pack(side="left")
        self.create_slider(self.aim_frame, "Z-Axis Offset (Pixels Down)", 0, 30, self.head_offset_y, "head_offset_y", res=1)

        tk.Label(self.aim_frame, text="- PID Tuning Parameters -", bg="#050505", fg="#5A5A5A", font=("Candara", 9, "bold")).pack(pady=(5, 0))
        self.create_slider(self.aim_frame, "Kp (Proportional Gain / Speed)", 0.1, 10.0, self.pid_kp, "pid_kp", res=0.1)
        self.create_slider(self.aim_frame, "Kd (Derivative Gain / Dampening)", 0.0, 2.0, self.pid_kd, "pid_kd", res=0.05)
        self.create_slider(self.aim_frame, "Static Friction Deadzone (Px)", 0.0, 10.0, self.deadzone, "deadzone", res=0.5)
        self.create_slider(self.aim_frame, "Velocity Limit (Px/Sec)", 100.0, 3000.0, self.max_speed, "max_speed", res=50.0)
        
        tk.Label(self.aim_frame, text="- Asymmetric Resolution Fix -", bg="#050505", fg="#5A5A5A", font=("Candara", 9)).pack(pady=(5, 0))
        self.create_slider(self.aim_frame, "X-Axis Scalar", 0.1, 2.0, self.x_multiplier, "x_multiplier", res=0.05)
        self.create_slider(self.aim_frame, "Y-Axis Scalar", 0.1, 2.0, self.y_multiplier, "y_multiplier", res=0.05)

        tk.Checkbutton(self.trig_frame, text="Enable Trigger Protocol", 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=2)
        tk.Label(bind_frame_trig, text="Trigger Bind:", bg="#050505", fg="#e0e0e0").pack(side="left")
        self.bind_btn_trig = tk.Button(bind_frame_trig, text=f"Bound: {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_slider(self.trig_frame, "Hitbox Trigger Area (Px)", 1, 20, self.trigger_zone, "trigger_zone", res=1)
        tk.Label(self.trig_frame, text="- Behavioral Obfuscation -", bg="#050505", fg="#5A5A5A", font=("Candara", 9)).pack(pady=(5, 0))
        self.create_slider(self.trig_frame, "Min Latency (s)", 0.0, 0.1, self.min_reaction, "min_reaction", res=0.005)
        self.create_slider(self.trig_frame, "Max Latency (s)", 0.0, 0.2, self.max_reaction, "max_reaction", res=0.005)

        tk.Label(self.vis_frame, text="Computer Vision Constraints", bg="#050505", fg="#e0e0e0", font=("Candara", 10, "bold")).pack(anchor="w", pady=(2,0))
        preset_frame = tk.Frame(self.vis_frame, bg="#050505")
        preset_frame.pack(fill="x", pady=2)
        tk.Label(preset_frame, text="Engine Preset:", bg="#050505", fg="#e0e0e0").pack(side="left")
        self.preset_combo = ttk.Combobox(preset_frame, state="readonly", values=["Purple (Tritanopia)", "Red (Default)", "Yellow"])
        self.preset_combo.current(0)
        self.preset_combo.pack(side="right", padx=5)
        self.preset_combo.bind("<<ComboboxSelected>>", self.apply_color_preset)

        self.create_inline_color_picker(self.vis_frame, "Target Hex/RGB", "target_color")
        self.create_slider(self.vis_frame, "HSV Tolerance Threshold", 0, 100, self.tolerance, "tolerance")
        self.create_slider(self.vis_frame, "Morphological Dilation (Amp)", 0, 10, self.color_amp, "color_amp", res=1)
        self.create_slider(self.vis_frame, "Area Filter (Noise Suppression)", 1, 50, self.min_area, "min_area", res=1)

        tk.Label(self.capture_frame, text="Data Stream Source (2PC Architecture)", bg="#050505", fg="#e0e0e0", font=("Candara", 10, "bold")).pack(anchor="w", pady=(2,0))
        tk.Radiobutton(self.capture_frame, text="Internal Screen (mss) - 1PC", variable=self.capture_source_var, value="screen", bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a").pack(anchor="w")
        tk.Radiobutton(self.capture_frame, text="Hardware Capture (PCIe/USB)", variable=self.capture_source_var, value="capture", bg="#050505", fg="#e0e0e0", selectcolor="#1a1a1a").pack(anchor="w")
        
        list_frame = tk.Frame(self.capture_frame, bg="#050505")
        list_frame.pack(fill="x", pady=2)
        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)

        res_frame = tk.Frame(self.capture_frame, bg="#050505")
        res_frame.pack(fill="x", pady=2)
        tk.Label(res_frame, text="Matrix Res:", bg="#050505", fg="#e0e0e0").pack(side="left")
        tk.Entry(res_frame, textvariable=self.cap_res_width, width=5, bg="#1a1a1a", fg="#e0e0e0", bd=0).pack(side="left", padx=2)
        tk.Label(res_frame, text="x", bg="#050505", fg="#e0e0e0").pack(side="left")
        tk.Entry(res_frame, textvariable=self.cap_res_height, width=5, bg="#1a1a1a", fg="#e0e0e0", bd=0).pack(side="left", padx=2)
        
        tk.Checkbutton(self.capture_frame, text="Enable Diagnostic Preview UI", variable=self.show_preview_window, bg="#050505", fg="#00ff00", selectcolor="#1a1a1a", activebackground="#050505", activeforeground="#00ff00", font=("Candara", 10, "bold")).pack(anchor="w", pady=(5,0))
        
        btn_start_cap = tk.Button(self.capture_frame, text="Initialize Video Pipeline", 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()

        tk.Label(self.makcu_frame, text="Hardware HID Interface", bg="#050505", fg="#e0e0e0", font=("Candara", 10, "bold")).pack(anchor="w", pady=(2,0))
        self.makcu_status_label = tk.Label(self.makcu_frame, text="Status: Disconnected", bg="#050505", fg="#a0a0a0", font=("Candara", 9))
        self.makcu_status_label.pack(anchor="w")
        btn_auto = tk.Button(self.makcu_frame, text="Establish Serial Handshake", 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="Terminate HID Connection", bg="#1a1a1a", fg="#b042ff", bd=0, command=self.disconnect_makcu)
        btn_disconnect.pack(fill="x", pady=2)

        bottom_frame = tk.Frame(self.root, bg="#050505")
        bottom_frame.pack(fill="x", side="bottom", padx=15, pady=5)
        self.toggle_btn = tk.Button(bottom_frame, text="START KINEMATIC ENGINE", bg="#1a1a1a", fg="#b042ff", font=("Arial", 10, "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="[END] Toggle UI | Right-Click colors to Copy", 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):
        x = self.root.winfo_x() + (event.x - self.root._drag_data["x"])
        y = self.root.winfo_y() + (event.y - self.root._drag_data["y"])
        self.root.geometry(f"+{x}+{y}")

    def apply_color_preset(self, event=None):
        val = self.preset_combo.get()
        if "Purple" in val:
            self.r_var.set(250); self.g_var.set(100); self.b_var.set(250); self.update_attr("tolerance", 15)
        elif "Red" in val:
            self.r_var.set(255); self.g_var.set(50); self.b_var.set(50); self.update_attr("tolerance", 15)
        elif "Yellow" in val:
            self.r_var.set(255); self.g_var.set(255); self.b_var.set(0); self.update_attr("tolerance", 15)
        self._sync_color_picker()

    def refresh_devices(self):
        threading.Thread(target=lambda: self.root.after(0, self._update_device_combo_task), daemon=True).start()

    def _update_device_combo_task(self):
        devices = self.capture_manager.list_capture_devices()
        self.capture_device_list = devices
        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):
            self._extract_device_index(self.capture_device_list[selection_idx])

    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:
                w = self.cap_res_width.get(); h = self.cap_res_height.get()
                self.root.after(0, lambda: self.capture_status_text.set(f"Connecting ({w}x{h}). Please wait..."))
                try:
                    self.capture_manager.start_capture(self.capture_device_index, width=w, height=h)
                    time.sleep(2.5) 
                    frame = self.capture_manager.get_frame()
                    if frame is not None:
                        real_h, real_w = frame.shape[:2]
                        self.root.after(0, lambda: self.capture_status_text.set(f"Stream Active: {real_w}x{real_h}"))
                    else:
                        self.capture_manager.stop_capture()
                        self.root.after(0, lambda: self.capture_status_text.set("Stream Error (Black Screen / Timeout)"))
                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, lambda: self.makcu_status_label.config(text="Status: Connected (SDK)", fg="#00ff00"))
                else:
                    self.root.after(0, lambda: messagebox.showerror("Makcu", "Подключение не удалось."))
            except Exception as e:
                self.root.after(0, lambda: messagebox.showerror("Makcu", str(e)))
        threading.Thread(target=_auto, daemon=True).start()

    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=2)
        top_frame = tk.Frame(frame, bg="#0a0a0a")
        top_frame.pack(fill="x", padx=5, pady=2)
        tk.Label(top_frame, text=label_text, bg="#0a0a0a", fg="#e0e0e0", font=("Candara", 9, "bold")).pack(side="left")
        self.preview_canvas = tk.Canvas(top_frame, width=24, height=24, bg="#ffffff", highlightthickness=1, highlightbackground="#36005c", cursor="hand2")
        self.preview_canvas.pack(side="right")
        self.color_label = tk.Label(top_frame, text="", bg="#0a0a0a", fg="#a0a0a0", font=("Consolas", 8), cursor="hand2")
        self.color_label.pack(side="right", padx=10)
        sliders_frame = tk.Frame(frame, bg="#0a0a0a")
        sliders_frame.pack(fill="x", padx=5, pady=0)

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

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

    def _on_color_slide(self, *args):
        self._sync_color_picker()

    def _sync_color_picker(self):
        r, g, b = self.r_var.get(), self.g_var.get(), self.b_var.get()
        setattr(self, self.target_attr_ref, (r, g, b))
        setattr(self, self.target_attr_ref + '_bgr', (b, g, r))
        self._update_hsv_cache()
        hex_color = '#%02x%02x%02x' % (r, g, b)
        self.color_label.config(text=f"({r},{g},{b}) {hex_color.upper()}")
        self.preview_canvas.config(bg=hex_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=0)
        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")
        if attr_name == "tolerance": self.slider_tol = slider 
        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'):
            if hasattr(self, 'slider_tol') and attr_name == "tolerance":
                self.slider_tol.set(self.tolerance)
            self._update_hsv_cache()
        elif attr_name.startswith('pid_'):
            setattr(self.kinematics, attr_name.replace('pid_', 'K'), float(val))

    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...")
        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"Bound: {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="HALT KINEMATIC ENGINE", fg="#ff3333", bg="#260000", activebackground="#400000")
            self.kinematics.reset()
        else: 
            self.toggle_btn.config(text="START KINEMATIC ENGINE", fg="#b042ff", bg="#1a1a1a", activebackground="#2a2a2a")
            with self.mouse_lock:
                self.target_velocity_x = 0.0
                self.target_velocity_y = 0.0

    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)

    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)

    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("Diagnostic Pipeline Preview")
                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:
                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)

    def mouse_movement_loop(self):
        """
        Изолированный высокочастотный цикл поллинга (Polling Loop).
        Обеспечивает интеграцию ПИД-скоростей независимо от времени обработки кадра,
        формируя непрерывную кривую движения (аналог Bezier-аппроксимации).
        """
        while self.running:
            if not self.active or not self.tracking_enabled.get():
                time.sleep(0.01)
                self.last_mouse_send_time = time.perf_counter()
                continue

            current_time = time.perf_counter()
            dt = current_time - self.last_mouse_send_time

            if dt >= self.mouse_send_interval:
                with self.mouse_lock:
                    v_x = self.target_velocity_x
                    v_y = self.target_velocity_y

                if v_x != 0.0 or v_y != 0.0:
                    # Интеграция скорости по дифференциалу времени (v * dt = дистанция)
                    move_px_x = v_x * dt
                    move_px_y = v_y * dt
                    
                    # Накопление субпиксельных остатков для предотвращения погрешности квантования
                    step_x = int(round(move_px_x + self.remainder_x))
                    step_y = int(round(move_px_y + self.remainder_y))
                    
                    self.remainder_x = (move_px_x + self.remainder_x) - step_x
                    self.remainder_y = (move_px_y + self.remainder_y) - step_y

                    if step_x != 0 or step_y != 0:
                        if self.makcu.connected:
                            self.makcu.send_mouse_move(step_x, step_y)
                        else:
                            ctypes.windll.user32.mouse_event(MOUSEEVENTF_MOVE, step_x, step_y, 0, 0)
                else:
                    self.remainder_x = 0.0
                    self.remainder_y = 0.0

                self.last_mouse_send_time = current_time
            else:
                # Пассивный spinlock для микросекундной точности синхронизации
                pass 

    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):
        # Рандомизация латентности посредством лог-нормального распределения (Humanization)
        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
        self.is_clicking = True
        self.trigger_queue.put(True)

    def logic_loop(self):
        """
        Вычислительный пайплайн компьютерного зрения (Vision Loop).
        Выполняет локализацию цели и обновление вектора ПИД-скоростей.
        """
        sct = mss.mss()
        current_roi_size = self.fov 
        
        while self.running:
            if not self.active:
                time.sleep(0.05)
                self.last_aim_time = time.perf_counter()
                continue

            loop_start = time.perf_counter()
            max_fov = int(self.fov)
            
            if current_roi_size > max_fov:
                current_roi_size = max_fov

            offset_x, offset_y = 0, 0
            micro_roi_active = False

            # --- ЭТАП ЗАХВАТА ВИДЕОПОТОКА ---
            if self.capture_source_var.get() == "screen":
                left = max(0, self.local_center_x - max_fov // 2)
                top = max(0, self.local_center_y - max_fov // 2)
                width = min(max_fov, self.screen_width - left)
                height = min(max_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))
                    full_bgr = img_bgra[:, :, :3]
                except: continue
                
            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 = max_fov // 2
                
                x1 = max(0, cx - half_fov)
                y1 = max(0, cy - half_fov)
                x2 = min(w, cx + half_fov)
                y2 = min(h, cy + half_fov)
                
                full_bgr = frame[y1:y2, x1:x2]

            if 'full_bgr' in locals() and full_bgr.size > 0:
                act_h, act_w = full_bgr.shape[:2]
                center_x, center_y = act_w // 2, act_h // 2
                
                # --- ДИНАМИЧЕСКИЙ ROI (Оптимизация матричных вычислений) ---
                if self.tracking_enabled.get() and self.target_lost_frames < 10 and self.last_raw_target_x is not None:
                    target_x_int = int(self.last_raw_target_x)
                    target_y_int = int(self.last_raw_target_y)
                    
                    half_roi = current_roi_size // 2
                    mx1 = max(0, target_x_int - half_roi)
                    my1 = max(0, target_y_int - half_roi)
                    mx2 = min(act_w, target_x_int + half_roi)
                    my2 = min(act_h, target_y_int + half_roi)
                    
                    search_bgr = full_bgr[my1:my2, mx1:mx2]
                    offset_x, offset_y = mx1, my1
                    micro_roi_active = True
                else:
                    search_bgr = full_bgr

                hsv_search = cv2.cvtColor(search_bgr, cv2.COLOR_BGR2HSV)
                debug_target_pos = None

                # --- ТРИГГЕР-БОТ ---
                if self.trigger_enabled.get():
                    if self.makcu.is_button_pressed(self.trigger_key):
                        trigger_zone_size = int(self.trigger_zone)
                        local_cx = center_x - offset_x
                        local_cy = center_y - offset_y
                        
                        if 0 <= local_cx <= search_bgr.shape[1] and 0 <= local_cy <= search_bgr.shape[0]:
                            y1_t = max(0, local_cy - trigger_zone_size)
                            y2_t = min(search_bgr.shape[0], local_cy + trigger_zone_size)
                            x1_t = max(0, local_cx - trigger_zone_size)
                            x2_t = min(search_bgr.shape[1], local_cx + trigger_zone_size)
                            
                            hsv_area = hsv_search[y1_t:y2_t, x1_t:x2_t]
                            trig_mask = self.apply_mask(hsv_area, self.trig_bounds, self.color_amp)
                            
                            if trig_mask is not None and cv2.countNonZero(trig_mask) > 1:
                                self.execute_trigger()

                # --- СИСТЕМА НАВЕДЕНИЯ И ПИД-УПРАВЛЕНИЕ ---
                if self.tracking_enabled.get():
                    now = time.perf_counter()
                    dt_vision = now - self.last_aim_time
                    self.last_aim_time = now
                    
                    if self.makcu.is_button_pressed(self.tracking_key):
                        mask = self.apply_mask(hsv_search, self.aim_bounds, self.color_amp)
                        
                        target_found_this_frame = False
                        if mask is not None and cv2.countNonZero(mask) > 0:
                            contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
                            best_dist = float('inf')
                            best_contour = None

                            for c in contours:
                                x, y, w_box, h_box = cv2.boundingRect(c)
                                if (w_box * h_box) < self.min_area: continue
                                
                                cx_box = x + w_box / 2 + offset_x 
                                cy_box = y + h_box / 2 + offset_y
                                
                                dist_x = (cx_box - center_x) * self.x_multiplier
                                dist_y = (cy_box - center_y) * self.y_multiplier
                                dist = math.hypot(dist_x, dist_y)
                                
                                # Трекинг-персистентность: отдача приоритета последней захваченной цели
                                if self.last_raw_target_x is not None:
                                    dist_to_last = math.hypot(cx_box - self.last_raw_target_x, cy_box - self.last_raw_target_y)
                                    if dist_to_last < 20:
                                        dist -= 50.0  

                                if dist < best_dist:
                                    best_dist = dist
                                    best_contour = c

                            if best_contour is not None:
                                target_found_this_frame = True
                                self.target_lost_frames = 0
                                
                                if self.target_bone_var.get() == "HEAD":
                                    topmost_point = tuple(best_contour[best_contour[:, :, 1].argmin()][0])
                                    target_x = topmost_point[0] + offset_x
                                    target_y = topmost_point[1] + offset_y + self.head_offset_y 
                                else:
                                    M = cv2.moments(best_contour)
                                    if M["m00"] != 0:
                                        target_x = (M["m10"] / M["m00"]) + offset_x
                                        target_y = (M["m01"] / M["m00"]) + offset_y
                                    else:
                                        x, y, w_box, h_box = cv2.boundingRect(best_contour)
                                        target_x = x + w_box / 2 + offset_x
                                        target_y = y + h_box / 2 + offset_y
                                        
                                debug_target_pos = (target_x, target_y)
                                
                                # Расчет вектора ошибки для ПИД-регулятора
                                error_x = (target_x - center_x) * self.x_multiplier
                                error_y = (target_y - center_y) * self.y_multiplier
                                
                                if math.hypot(error_x, error_y) < self.deadzone:
                                    error_x, error_y = 0.0, 0.0

                                # Генерация целевой скорости на основе кинематической модели
                                vel_x, vel_y = self.kinematics.update(error_x, error_y, dt_vision)
                                
                                # Экстремальное ограничение баллистической скорости
                                speed = math.hypot(vel_x, vel_y)
                                if speed > self.max_speed:
                                    scale = self.max_speed / speed
                                    vel_x *= scale
                                    vel_y *= scale
                                
                                # Асинхронная передача скорости в поток диспетчеризации мыши
                                with self.mouse_lock:
                                    self.target_velocity_x = vel_x
                                    self.target_velocity_y = vel_y

                                self.last_raw_target_x = target_x
                                self.last_raw_target_y = target_y
                                current_roi_size = max(80, int(max_fov * 0.4))

                        if not target_found_this_frame:
                            self.target_lost_frames += 1
                            with self.mouse_lock: 
                                self.target_velocity_x = 0.0
                                self.target_velocity_y = 0.0
                            if self.target_lost_frames > 10:
                                self.kinematics.reset()
                                current_roi_size = max_fov
                    else:
                        self.target_lost_frames += 1
                        with self.mouse_lock: 
                            self.target_velocity_x = 0.0
                            self.target_velocity_y = 0.0
                        self.kinematics.reset()
                        current_roi_size = max_fov

                if self.show_preview_window.get():
                    m_rect = (offset_x, offset_y, search_bgr.shape[1], search_bgr.shape[0]) if micro_roi_active else None
                    self._prepare_preview(full_bgr, center_x, center_y, debug_target_pos, m_rect)

            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, target_pos, micro_rect):
        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)
        
        if micro_rect is not None:
            mx, my, mw, mh = micro_rect
            cv2.rectangle(debug_frame, (mx, my), (mx + mw, my + mh), (255, 100, 0), 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()