Im going to give you a detection thread and Trigger bot thread can you make the TriggerBot thread to work like this: When the center of the FOV is on a target the trigger bot will shoot because right now the Trigger bot only shoot when im right in the center of the detected object so that mean that i would need to be right in the center just for it to shoot and i dont want that i want to shoot when im on the detected object like top right or down left anywher so long the detected object is in the Crosshair(center of the FOV)

import sys
import os
import json
import time
import threading
import logging
import numpy as np
import cv2
import mss
import mouse
import ctypes
import keyboard
import webbrowser
import sort
from ultralytics import YOLO
from PyQt5 import QtCore, QtGui, QtWidgets

# --- LOGIN DIALOG (unchanged) ---
class LoginDialog(QtWidgets.QDialog):
    def __init__(self, correct_key, parent=None):
        super().__init__(parent)
        self.correct_key = correct_key
        self.setWindowTitle("Enter Key")
        self.setModal(True)
        self.resize(300, 100)
        self.layout = QtWidgets.QVBoxLayout(self)

        self.label = QtWidgets.QLabel("Enter your license key or password:", self)
        self.layout.addWidget(self.label)

        self.input = QtWidgets.QLineEdit(self)
        self.input.setEchoMode(QtWidgets.QLineEdit.Password)
        self.layout.addWidget(self.input)

        self.button = QtWidgets.QPushButton("Unlock", self)
        self.button.clicked.connect(self.check_key)
        self.layout.addWidget(self.button)

        self.result = False

    def check_key(self):
        if self.input.text() == self.correct_key:
            self.result = True
            self.accept()
        else:
            QtWidgets.QMessageBox.warning(self, "Error", "Wrong key or password!")

    @staticmethod
    def get_login(correct_key):
        app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
        dialog = LoginDialog(correct_key)
        result = dialog.exec_()
        return dialog.result

CORRECT_KEY = "1"

if not LoginDialog.get_login(CORRECT_KEY):
    sys.exit(0)

# --- SETTINGS AND LOGGING SETUP ---
APPNAME = "UD Premium"
SETTINGS_FILE = "ud_premium_settings.json"
LOG_FILE = "ud_premium.log"
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
SETTINGS_PATH = os.path.join(BASE_PATH, SETTINGS_FILE)
LOG_PATH = os.path.join(BASE_PATH, LOG_FILE)
YOLO_PT_PATH = os.path.join(BASE_PATH, "yolo11n.pt")

if not os.path.exists(LOG_PATH):
    with open(LOG_PATH, "w"): pass

logging.basicConfig(
    filename=LOG_PATH,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s: %(message)s"
)

DEFAULT_SETTINGS = {
    "fov_radius": 200,
    "fov_enabled": True,
    "fov_color": "#B366FF",
    "fov_thickness": 0.1,
    "fov_opacity": 0.0,    # Default see-through for FOV
    "aim_enabled": True,
    "aim_hotkey": "Right Mouse Button",
    "aim_hotkey_code": None,
    "aim_move_speed": 0.8,
    "aim_smoothing": 0.45,
    "aim_sticky_max": 35,
    "aim_sticky_decay": 0.85,
    "aim_sticky_boost": 1.55,
    "aim_prediction_factor": 0.1,
    "aim_prioritize_head": True,
    "aim_max_move": 35,
    "silent_aim_enabled": False,
    "silent_aim_hotkey": "Q",
    "silent_aim_speed": 1.0,
    "esp_enabled": True,
    "esp_box_color": "#B366FF",
    "esp_fill_color": "#7E3FF2",
    "esp_line_color": "#B366FF",
    "esp_box_style": "Box",
    "esp_show_lines": True,
    "esp_font_size": 12,
    "esp_box_thickness": 1,
    "esp_line_thickness": 1,
    "confidence": 0.5,
    "imgsz": 416,
    "detection_fps": 60,
    "ui_theme": "dark",
    "ui_font": "Segoe UI",
    "ui_font_size": 11,
    "window_transparency": 0.87,   # Main window opacity (see-through)
    "trigger_enabled": False,
    "trigger_auto_fire": False,
    "trigger_fire_speed": 150,
    "trigger_tolerance": 2,
    "trigger_hotkey": "Left Mouse Button",
    "trigger_burst": False,
    "trigger_burst_count": 3,
    "trigger_burst_delay": 60,
    "trigger_mouse_button": "left",
    "menu_shortcut": "F8",
    # --- RECOIL SETTINGS ---
    "recoil_enabled": False,
    "recoil_strength": 50,
    "recoil_jitter": 5,
    "recoil_pattern": "Default",
    "recoil_activation_key": "Left Mouse Button",
    "recoil_delay": 12,
    "recoil_humanize": 6,
    "recoil_side": 0,
    "recoil_custom_pattern": [],
}

def load_settings():
    if os.path.exists(SETTINGS_PATH):
        try:
            with open(SETTINGS_PATH, "r") as f:
                data = json.load(f)
            for k, v in DEFAULT_SETTINGS.items():
                if k not in data:
                    data[k] = v
            return data
        except Exception as e:
            logging.error(f"Failed to load settings: {e}")
    return DEFAULT_SETTINGS.copy()

def save_settings(settings):
    try:
        with open(SETTINGS_PATH, "w") as f:
            json.dump(settings, f, indent=2)
    except Exception as e:
        logging.error(f"Failed to save settings: {e}")

def export_settings(settings, parent=None):
    fname, _ = QtWidgets.QFileDialog.getSaveFileName(parent, "Export Settings", "", "JSON Files (*.json)")
    if fname:
        try:
            with open(fname, "w") as f:
                json.dump(settings, f, indent=2)
            QtWidgets.QMessageBox.information(parent, "Exported", "Settings exported successfully.")
        except Exception as e:
            QtWidgets.QMessageBox.warning(parent, "Error", f"Could not export settings:\n{e}")

def import_settings(parent=None):
    fname, _ = QtWidgets.QFileDialog.getOpenFileName(parent, "Import Settings", "", "JSON Files (*.json)")
    if fname:
        try:
            with open(fname, "r") as f:
                new_settings = json.load(f)
            for k, v in DEFAULT_SETTINGS.items():
                if k not in new_settings:
                    new_settings[k] = v
            QtWidgets.QMessageBox.information(parent, "Imported", "Settings imported. Restart app for all changes.")
            return new_settings
        except Exception as e:
            QtWidgets.QMessageBox.warning(parent, "Error", f"Could not import settings:\n{e}")
    return None

# --- UTILS ---
def parse_qcolor(colorstr, fallback="#B366FF"):
    try:
        if isinstance(colorstr, str) and colorstr.startswith("#") and len(colorstr) == 7:
            return QtGui.QColor(colorstr)
        else:
            return QtGui.QColor(fallback)
    except Exception:
        return QtGui.QColor(fallback)

def clamp(val, minv, maxv):
    return max(minv, min(val, maxv))

def move_mouse_relative(dx, dy):
    import ctypes
    INPUT_MOUSE = 0
    MOUSEEVENTF_MOVE = 0x0001
    class MOUSEINPUT(ctypes.Structure):
        _fields_ = [("dx", ctypes.c_long), ("dy", ctypes.c_long), ("mouseData", ctypes.c_ulong),
                    ("dwFlags", ctypes.c_ulong), ("time", ctypes.c_ulong), ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong))]
    class INPUT(ctypes.Structure):
        _fields_ = [("type", ctypes.c_ulong), ("mi", MOUSEINPUT)]
    extra = ctypes.c_ulong(0)
    mi = MOUSEINPUT(dx, dy, 0, MOUSEEVENTF_MOVE, 0, ctypes.pointer(extra))
    inp = INPUT(INPUT_MOUSE, mi)
    ctypes.windll.user32.SendInput(1, ctypes.byref(inp), ctypes.sizeof(inp))

def mouse_click_down_up(button='left'):
    mouse.press(button=button)
    time.sleep(0.01)
    mouse.release(button=button)

def mouse_press_down(button='left'):
    mouse.press(button=button)

def mouse_release_up(button='left'):
    mouse.release(button=button)

# --- YOLO SINGLETON ---
class YOLOModelSingleton:
    _instance = None
    _lock = threading.Lock()
    def __init__(self, model_path, imgsz):
        self.model_path = model_path
        self.imgsz = imgsz
        self.model = None
        self.names = []
        self._load_model()
    def _load_model(self):
        try:
            self.model = YOLO(self.model_path)
            self.names = self.model.names if hasattr(self.model, "names") else []
            dummy = np.zeros((self.imgsz, self.imgsz, 3), dtype=np.uint8)
            self.model.predict(source=dummy, imgsz=self.imgsz, device="cpu")
        except Exception as e:
            logging.error(f"YOLOModelSingleton load error: {e}")
            self.model = None
    @classmethod
    def get_instance(cls, model_path, imgsz):
        with cls._lock:
            if cls._instance is None or cls._instance.model_path != model_path or cls._instance.imgsz != imgsz:
                cls._instance = YOLOModelSingleton(model_path, imgsz)
            return cls._instance

class DetectionThread(QtCore.QThread):
    # Now emits track_ids as a 5th list
    detection_signal = QtCore.pyqtSignal(list, list, list, list, list)  # boxes, labels, confidences, out_classes, track_ids

    def __init__(self, settings):
        super().__init__()
        self.settings = settings
        self.running = False
        self.yolo_singleton = None
        self.frame_lock = threading.Lock()
        self.tracker = Sort(max_age=30, min_hits=3, iou_threshold=0.3)  # SORT params you can tune

    def nms(self, boxes, scores, iou_threshold=0.5):
        if len(boxes) == 0:
            return []

        boxes = np.array(boxes, dtype=float)
        scores = np.array(scores)

        x1 = boxes[:, 0]
        y1 = boxes[:, 1]
        x2 = boxes[:, 2]
        y2 = boxes[:, 3]

        areas = (x2 - x1) * (y2 - y1)
        indices = np.argsort(scores)[::-1]  # Sort by scores descending
        selected_indices = []

        while len(indices) > 0:
            current_index = indices[0]
            selected_indices.append(current_index)

            xx1 = np.maximum(x1[current_index], x1[indices[1:]])
            yy1 = np.maximum(y1[current_index], y1[indices[1:]])
            xx2 = np.minimum(x2[current_index], x2[indices[1:]])
            yy2 = np.minimum(y2[current_index], y2[indices[1:]])

            w = np.maximum(0, xx2 - xx1)
            h = np.maximum(0, yy2 - yy1)
            intersection = w * h

            iou = intersection / (areas[current_index] + areas[indices[1:]] - intersection)
            indices = indices[np.where(iou <= iou_threshold)[0] + 1]  # +1 skips current

        return selected_indices

    def run(self):
        self.running = True
        model_path = YOLO_PT_PATH
        imgsz = clamp(self.settings.get("imgsz", 416), 320, 640)
        self.yolo_singleton = YOLOModelSingleton.get_instance(model_path, imgsz)
        if self.yolo_singleton.model is None:
            logging.error("YOLO model failed to load.")
            return

        with mss.mss() as sct:
            mon = sct.monitors[1]
            sw, sh = mon['width'], mon['height']

            while self.running:
                try:
                    # FOV radius & center
                    fov_radius = clamp(self.settings.get('fov_radius', 200), 50, min(sw, sh) // 2)
                    cx, cy = sw // 2, sh // 2
                    x1 = max(cx - fov_radius, 0)
                    y1 = max(cy - fov_radius, 0)
                    x2 = min(cx + fov_radius, sw)
                    y2 = min(cy + fov_radius, sh)

                    bbox = {'top': int(y1), 'left': int(x1), 'width': int(x2 - x1), 'height': int(y2 - y1)}

                    with self.frame_lock:
                        frame = np.array(sct.grab({**mon, **bbox}))

                    frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)

                    conf_thres = clamp(self.settings.get('confidence', 0.5), 0.1, 0.95)

                    results = self.yolo_singleton.model.predict(
                        source=frame,
                        save=False,
                        imgsz=imgsz,
                        conf=conf_thres,
                        iou=0.45,
                        device="cpu"
                    )[0]

                    dets, classes = [], []
                    for i, box in enumerate(results.boxes.xyxy):
                        conf = float(results.boxes.conf[i]) if hasattr(results.boxes, "conf") else 0.0
                        if conf < conf_thres:
                            continue
                        x1b, y1b, x2b, y2b = map(int, box)
                        class_id = int(results.boxes.cls[i]) if hasattr(results.boxes, "cls") else -1
                        class_name = self.yolo_singleton.names[class_id] if 0 <= class_id < len(self.yolo_singleton.names) else "?"

                        # Map to screen coordinates (offset by cropped bbox)
                        x1b += x1
                        x2b += x1
                        y1b += y1
                        y2b += y1

                        # Filter detections outside circular FOV
                        bx, by = (x1b + x2b) // 2, (y1b + y2b) // 2
                        if np.hypot(bx - cx, by - cy) > fov_radius:
                            continue

                        dets.append((x1b, y1b, x2b, y2b, conf, class_name))
                        classes.append(class_name)

                    # Sort by confidence descending
                    dets = sorted(dets, key=lambda x: x[4], reverse=True)
                    boxes = [(x1b, y1b, x2b, y2b) for (x1b, y1b, x2b, y2b, _, _) in dets]
                    confidences = [cf for (_, _, _, _, cf, _) in dets]

                    # NMS filtering
                    if boxes:
                        selected_indices = self.nms(boxes, confidences, iou_threshold=0.5)
                        boxes = [boxes[i] for i in selected_indices]
                        labels = [classes[i] for i in selected_indices]
                        confidences = [confidences[i] for i in selected_indices]
                    else:
                        labels = []

                    out_classes = labels  # Use post-NMS labels

                    # Prepare detections for SORT: [x1, y1, x2, y2, score]
                    if boxes:
                        dets_for_sort = np.array([[*box, conf] for box, conf in zip(boxes, confidences)])
                    else:
                        dets_for_sort = np.empty((0, 5))

                    tracked_objects = self.tracker.update(dets_for_sort)

                    # tracked_objects format: np.array([[x1, y1, x2, y2, track_id], ...])
                    tracked_boxes = []
                    track_ids = []
                    for obj in tracked_objects:
                        x1t, y1t, x2t, y2t, track_id = obj
                        tracked_boxes.append((int(x1t), int(y1t), int(x2t), int(y2t)))
                        track_ids.append(int(track_id))

                    # Emit detection + tracking info
                    self.detection_signal.emit(tracked_boxes, labels, confidences, out_classes, track_ids)

                    del frame

                except Exception as e:
                    logging.error(f"DetectionThread error: {e}")

    def stop(self):
        self.running = False

class AimAssistThread(QtCore.QThread):
    aim_active_signal = QtCore.pyqtSignal(bool)

    def __init__(self, settings, get_targets_func, get_fov_center_func):
        super().__init__()
        self.settings = settings
        self.get_targets = get_targets_func
        self.get_fov_center = get_fov_center_func
        self.running = False
        self.aim_active = False
        self.lock = threading.Lock()
        
        # Ultra sticky tracking variables
        self.locked_track_id = None
        self.locked_target_center = None
        self.target_acquired = False
        self.last_known_position = None
        self.acquisition_time = None
        self.error_count = 0
        
        # Anti-wiggle variables
        self.movement_history = []
        self.last_move_time = 0
        self.settled = False
        
        # Sync variables
        self.last_target_update = 0
        self.target_cache = []

    def parse_targets_with_tracking(self, raw_targets):
        """Parse targets to ensure we have tracking info - this is the key fix!"""
        parsed_targets = []
        
        # Handle different possible target formats from get_targets()
        if not raw_targets:
            return []
            
        for target in raw_targets:
            try:
                # Format should be: [x1, y1, x2, y2, confidence, track_id]
                # But sometimes it might come as nested tuples or different formats
                
                if isinstance(target, (tuple, list)) and len(target) >= 4:
                    if len(target) == 4:
                        # Missing confidence and track_id - this is the problem!
                        x1, y1, x2, y2 = target
                        conf = 0.5  # Default confidence
                        track_id = None  # No tracking - this breaks sticky aim
                    elif len(target) == 5:
                        # Missing track_id
                        x1, y1, x2, y2, conf = target
                        track_id = None  # No tracking - this breaks sticky aim
                    elif len(target) >= 6:
                        # Full format with tracking
                        x1, y1, x2, y2, conf, track_id = target[:6]
                    else:
                        continue
                        
                    # Ensure coordinates are integers
                    x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
                    
                    # Add to parsed targets
                    parsed_targets.append([x1, y1, x2, y2, float(conf), track_id])
                    
            except Exception as e:
                logging.warning(f"Failed to parse target {target}: {e}")
                continue
                
        return parsed_targets

    def get_target_by_track_id(self, targets, track_id):
        """Get exact target by track ID"""
        if track_id is None:
            return None, None
            
        for target in targets:
            if len(target) >= 6 and target[5] is not None:
                x1, y1, x2, y2, conf, curr_track_id = target[:6]
                if int(curr_track_id) == int(track_id):
                    center_x = int((x1 + x2) // 2)
                    center_y = int((y1 + y2) // 2)
                    return (center_x, center_y), target
        return None, None

    def find_closest_target_with_tracking(self, targets, fc_x, fc_y, fov_radius):
        """Find the closest target to crosshair center, prioritizing tracked targets"""
        if not targets:
            return None, None
            
        closest_dist = float('inf')
        closest_target = None
        closest_pos = None
        
        # First pass: look for targets with tracking IDs (these are more reliable)
        tracked_candidates = []
        untracked_candidates = []
        
        for target in targets:
            if len(target) < 4:
                continue
                
            x1, y1, x2, y2 = target[:4]
            center_x = int((x1 + x2) // 2)
            center_y = int((y1 + y2) // 2)
            
            # Distance from crosshair center
            dist = np.hypot(center_x - fc_x, center_y - fc_y)
            
            # Only consider targets within FOV
            if dist < fov_radius:
                conf = target[4] if len(target) > 4 else 0.5
                track_id = target[5] if len(target) > 5 else None
                
                candidate = (dist, (center_x, center_y), target, conf)
                
                if track_id is not None:
                    tracked_candidates.append(candidate)
                else:
                    untracked_candidates.append(candidate)
        
        # Prefer tracked targets, then fall back to untracked
        all_candidates = tracked_candidates + untracked_candidates
        
        if all_candidates:
            # Sort by distance, then by confidence
            all_candidates.sort(key=lambda x: (x[0], -x[3]))
            closest_dist, closest_pos, closest_target, _ = all_candidates[0]
        
        return closest_pos, closest_target

    def get_current_target_position(self, targets, fc_x, fc_y, fov_radius):
        """Get the current target position using track ID lock or closest target"""
        now = time.time()
        
        # Parse targets to ensure proper format
        parsed_targets = self.parse_targets_with_tracking(targets)
        
        if not parsed_targets:
            return None
        
        # If we have a locked track ID, try to find it first
        if self.locked_track_id is not None:
            locked_pos, locked_target = self.get_target_by_track_id(parsed_targets, self.locked_track_id)
            if locked_pos:
                # Update last known position
                self.last_known_position = locked_pos
                return locked_pos
            else:
                # Target lost by ID, try proximity matching
                if self.last_known_position:
                    for target in parsed_targets:
                        x1, y1, x2, y2 = target[:4]
                        center_x = int((x1 + x2) // 2)
                        center_y = int((y1 + y2) // 2)
                        
                        # Check if any target is close to last known position
                        dist_from_last = np.hypot(center_x - self.last_known_position[0], 
                                                center_y - self.last_known_position[1])
                        if dist_from_last < 60:  # Reduced threshold for better stickiness
                            # Lock onto this target if it has a track ID
                            if len(target) >= 6 and target[5] is not None:
                                self.locked_track_id = target[5]
                                logging.info(f"Reacquired target with new ID: {self.locked_track_id}")
                            self.last_known_position = (center_x, center_y)
                            return (center_x, center_y)
        
        # No locked target or target lost, find closest with tracking preference
        closest_pos, closest_target = self.find_closest_target_with_tracking(parsed_targets, fc_x, fc_y, fov_radius)
        
        if closest_pos and closest_target:
            # Lock onto new target
            old_track_id = self.locked_track_id
            if len(closest_target) >= 6 and closest_target[5] is not None:
                self.locked_track_id = closest_target[5]
                if old_track_id != self.locked_track_id:
                    logging.info(f"Locked onto new target ID: {self.locked_track_id}")
            else:
                self.locked_track_id = None
                logging.warning("Target found but no track ID available")
            
            self.last_known_position = closest_pos
            self.target_acquired = True
            self.acquisition_time = now
            # Reset anti-wiggle state for new target
            self.settled = False
            self.movement_history = []
            
        return closest_pos

    def is_oscillating(self):
        """Check if we're oscillating/wiggling around the target"""
        if len(self.movement_history) < 6:
            return False
            
        # Check last 6 movements for oscillation pattern
        recent_moves = self.movement_history[-6:]
        
        # Look for back-and-forth movement patterns
        x_changes = [move[0] for move in recent_moves if abs(move[0]) > 0]
        y_changes = [move[1] for move in recent_moves if abs(move[1]) > 0]
        
        # Check for sign changes (indicating back-and-forth movement)
        x_oscillating = False
        y_oscillating = False
        
        if len(x_changes) >= 4:
            sign_changes = sum(1 for i in range(1, len(x_changes)) 
                             if (x_changes[i] > 0) != (x_changes[i-1] > 0))
            x_oscillating = sign_changes >= 2
            
        if len(y_changes) >= 4:
            sign_changes = sum(1 for i in range(1, len(y_changes)) 
                             if (y_changes[i] > 0) != (y_changes[i-1] > 0))
            y_oscillating = sign_changes >= 2
            
        return x_oscillating or y_oscillating

    def calculate_smooth_move(self, target_pos, fc_x, fc_y):
        """Calculate smooth incremental movement with anti-wiggle logic"""
        if not target_pos:
            return 0, 0
            
        # Calculate difference to target
        dx = target_pos[0] - fc_x
        dy = target_pos[1] - fc_y
        distance = np.hypot(dx, dy)
        
        # Enhanced deadzone that grows when settled
        base_deadzone = 4  # Reduced for better precision
        if self.settled:
            deadzone = base_deadzone * 2.5  # Larger deadzone when settled
        else:
            deadzone = base_deadzone
            
        if distance <= deadzone:
            # Mark as settled if we stay in deadzone
            if distance <= base_deadzone:
                self.settled = True
            return 0, 0
        
        # Reset settled state if we move far from target
        if distance > deadzone * 2:
            self.settled = False
        
        # Check for oscillation
        if self.is_oscillating():
            # Reduce movement aggressiveness when oscillating
            move_factor = 0.08  # Very slow when oscillating
            deadzone *= 3  # Much larger deadzone
            if distance <= deadzone:
                self.settled = True
                return 0, 0
        else:
            # Normal movement calculation - more aggressive for better tracking
            if distance < 8:
                move_factor = 0.7  # More aggressive when very close
            elif distance < 15:
                move_factor = 0.5  # Medium speed for close targets
            elif distance < 30:
                move_factor = 0.35  # Normal speed
            else:
                move_factor = 0.25  # Slower for distant targets
            
        mdx = int(dx * move_factor)
        mdy = int(dy * move_factor)
        
        # Ensure minimum movement if target is far enough
        if distance > 10:
            if abs(mdx) < 1 and abs(dx) > 3:
                mdx = 1 if dx > 0 else -1
            if abs(mdy) < 1 and abs(dy) > 3:
                mdy = 1 if dy > 0 else -1
        
        # Cap maximum movement per frame
        max_move = 15  # Increased for better responsiveness
        mdx = max(-max_move, min(mdx, max_move))
        mdy = max(-max_move, min(mdy, max_move))
        
        # Store movement in history for oscillation detection
        now = time.time()
        self.movement_history.append((mdx, mdy, now))
        
        # Keep only recent history (last 1.5 seconds)
        self.movement_history = [(x, y, t) for x, y, t in self.movement_history 
                               if now - t < 1.5]
        
        return mdx, mdy

    def run(self):
        self.running = True
        
        while self.running:
            try:
                # Get hotkey setting
                hotkey = self.settings.get('aim_hotkey', "Right Mouse Button").lower()
                hotkey_pressed = False
                
                # Check hotkey state
                if "mouse" in hotkey:
                    if "right" in hotkey:
                        hotkey_pressed = mouse.is_pressed(button="right")
                    elif "left" in hotkey:
                        hotkey_pressed = mouse.is_pressed(button="left")
                    elif "middle" in hotkey:
                        hotkey_pressed = mouse.is_pressed(button="middle")
                else:
                    try:
                        hotkey_pressed = keyboard.is_pressed(hotkey)
                    except Exception:
                        hotkey_pressed = False

                aim_on = False

                # MAIN LOGIC: Ultra sticky aim assist with proper target sync
                if hotkey_pressed and self.settings.get('aim_enabled', True):
                    with self.lock:
                        targets = self.get_targets()
                    
                    fov_center = self.get_fov_center()
                    
                    if targets and fov_center:
                        # Get FOV center coordinates
                        fc_x, fc_y = (
                            (fov_center.x(), fov_center.y())
                            if hasattr(fov_center, "x") else fov_center
                        )
                        
                        fov_radius = self.settings.get('fov_radius', 200)
                        
                        # Get current target position with improved sync
                        target_pos = self.get_current_target_position(targets, fc_x, fc_y, fov_radius)
                        
                        if target_pos:
                            # Calculate smooth movement with anti-wiggle
                            mdx, mdy = self.calculate_smooth_move(target_pos, fc_x, fc_y)
                            
                            # Execute movement if needed
                            if abs(mdx) > 0 or abs(mdy) > 0:
                                try:
                                    move_mouse_relative(mdx, mdy)
                                    aim_on = True
                                    self.last_move_time = time.time()
                                except Exception as e:
                                    logging.error(f"Mouse movement failed: {e}")
                        else:
                            # No targets found
                            if self.locked_track_id is not None:
                                logging.debug(f"Lost track of target ID: {self.locked_track_id}")
                            
                else:
                    # Hotkey released - reset all tracking
                    if self.locked_track_id is not None:
                        logging.info(f"Released tracking of target ID: {self.locked_track_id}")
                    self.locked_track_id = None
                    self.locked_target_center = None
                    self.target_acquired = False
                    self.last_known_position = None
                    self.acquisition_time = None
                    self.settled = False
                    self.movement_history = []

                # Emit aim state change
                if aim_on != self.aim_active:
                    self.aim_active = aim_on
                    self.aim_active_signal.emit(self.aim_active)
                
                # Optimal update rate for smooth tracking
                time.sleep(0.005)  # ~200 FPS for responsive tracking
                
            except Exception as e:
                self.error_count += 1
                if self.error_count < 5:
                    logging.exception(f"AimAssistThread error: {e}")
                elif self.error_count == 5:
                    logging.error("AimAssistThread: Too many errors, suppressing further logs.")

    def stop(self):
        self.running = False



class TriggerBotThread(QtCore.QThread):
    def __init__(self, settings, get_targets_func, get_fov_center_func):
        super().__init__()
        self.settings = settings
        self.get_targets = get_targets_func
        self.get_fov_center = get_fov_center_func
        self.running = False
        self.is_firing = False
        self.last_fired = 0
        self.last_triggered = False
        self.lock = threading.Lock()
        self.burst_shots_left = 0
        self.last_burst_time = 0
        self.trigger_hotkey = self.settings.get("trigger_hotkey", "Left Mouse Button").lower()

    def is_in_crosshair(self, tx, ty, fc_x, fc_y, tolerance):
        return abs(tx - fc_x) <= tolerance and abs(ty - fc_y) <= tolerance

    def run(self):
        self.running = True
        while self.running:
            try:
                if self.settings.get("trigger_enabled", False):
                    fire_speed_ms = int(self.settings.get("trigger_fire_speed", 150))
                    auto_fire = self.settings.get("trigger_auto_fire", False)
                    tolerance = int(self.settings.get("trigger_tolerance", 2))
                    burst_enabled = self.settings.get("trigger_burst", False)
                    burst_count = int(self.settings.get("trigger_burst_count", 3))
                    burst_delay = int(self.settings.get("trigger_burst_delay", 60))
                    button = self.settings.get("trigger_mouse_button", "left")

                    with self.lock:
                        targets = self.get_targets()
                    fov_center = self.get_fov_center()
                    if not fov_center:
                        time.sleep(0.01)
                        continue
                    fc_x, fc_y = (
                        (fov_center.x(), fov_center.y())
                        if hasattr(fov_center, "x") else fov_center
                    )
                    fired_this_loop = False
                    now = time.time()

                    for (x1, y1, x2, y2, *_) in targets:
                        tx, ty = (x1 + x2) // 2, (y1 + y2) // 2
                        if self.is_in_crosshair(tx, ty, fc_x, fc_y, tolerance):
                            if burst_enabled:
                                if self.burst_shots_left == 0 or (now - self.last_burst_time)*1000 >= fire_speed_ms:
                                    self.burst_shots_left = burst_count
                                    self.last_burst_time = now
                                if self.burst_shots_left > 0:
                                    if (now - self.last_fired)*1000 >= burst_delay:
                                        mouse_click_down_up(button)
                                        self.last_fired = now
                                        self.burst_shots_left -= 1
                                        self.last_triggered = True
                                fired_this_loop = True
                            elif auto_fire:
                                if not self.is_firing:
                                    mouse_press_down(button)
                                    self.is_firing = True
                                fired_this_loop = True
                            else:
                                if not self.last_triggered or (now - self.last_fired)*1000 >= fire_speed_ms:
                                    mouse_click_down_up(button)
                                    self.last_fired = now
                                    self.last_triggered = True
                            break
                    else:
                        self.last_triggered = False
                        self.burst_shots_left = 0
                        if self.is_firing:
                            mouse_release_up(button)
                            self.is_firing = False

                    if auto_fire and not fired_this_loop and self.is_firing:
                        mouse_release_up(button)
                        self.is_firing = False
                else:
                    self.burst_shots_left = 0
                    if self.is_firing:
                        mouse_release_up(self.settings.get("trigger_mouse_button", "left"))
                        self.is_firing = False
                time.sleep(0.008)
            except Exception as e:
                logging.error(f"TriggerBotThread error: {e}")

    def stop(self):
        self.running = False
        if self.is_firing:
            mouse_release_up(self.settings.get("trigger_mouse_button", "left"))
            self.is_firing = False

# --- RECOIL THREAD (with fixes & customization) ---
class RecoilControlThread(QtCore.QThread):
    def __init__(self, settings):
        super().__init__()
        self.settings = settings
        self.running = False
        self.patterns = {
            "Default": [(0, 7), (0, 7), (0, 7), (0, 8), (0, 8), (0, 9), (0, 10), (0, 11)],
            "Pattern 1": [(1, 8), (0, 8), (-1, 8), (2, 8), (-2, 9), (1, 10), (0, 11)],
            "Pattern 2": [(0, 7), (1, 7), (2, 8), (2, 8), (1, 9), (0, 10), (-1, 11)],
        }
        self._terminate = threading.Event()
        self.shot_idx = 0

    def run(self):
        import random
        import keyboard
        self.running = True
        while not self._terminate.is_set():
            try:
                if self.settings.get('recoil_enabled', False):
                    key = self.settings.get('recoil_activation_key', 'Left Mouse Button')
                    if not key:
                        time.sleep(0.02)
                        continue
                    key_std = key.lower().replace(" ", "")
                    pressed = False
                    if "mouse" in key_std or "button" in key_std:
                        if "right" in key_std:
                            pressed = mouse.is_pressed(button="right")
                        elif "middle" in key_std:
                            pressed = mouse.is_pressed(button="middle")
                        else:
                            pressed = mouse.is_pressed(button="left")
                    else:
                        try:
                            pressed = keyboard.is_pressed(key)
                        except Exception:
                            pressed = False
                    if pressed:
                        pattern_name = self.settings.get('recoil_pattern', 'Default')
                        pattern = self.patterns.get(pattern_name, self.patterns["Default"])
                        strength = self.settings.get('recoil_strength', 50)
                        jitter = self.settings.get('recoil_jitter', 5)
                        delay = self.settings.get('recoil_delay', 12)
                        humanize = self.settings.get('recoil_humanize', 6)
                        side = self.settings.get('recoil_side', 0)
                        if pattern_name == "Custom" and self.settings.get("recoil_custom_pattern"):
                            pattern = self.settings.get("recoil_custom_pattern")
                        if self.shot_idx >= len(pattern):
                            self.shot_idx = 0
                        move_x, move_y = pattern[self.shot_idx]
                        move_x += side
                        move_y = int(move_y * (strength / 50.0))
                        move_x += random.randint(-jitter, jitter)
                        move_y += random.randint(-humanize, humanize)
                        move_mouse_relative(move_x, move_y)
                        self.shot_idx += 1
                        time.sleep(delay / 1000.0)
                    else:
                        self.shot_idx = 0
                        time.sleep(0.01)
                else:
                    self.shot_idx = 0
                    time.sleep(0.02)
            except Exception as ex:
                logging.error(f"RecoilControlThread error: {ex}")
                time.sleep(0.04)
        self.running = False

    def stop(self):
        self._terminate.set()

# --- UI utility classes: HotkeyButton, ColorButton, SidebarButton, ScrollAreaNoBar, OverlayWindow ---

class HotkeyButton(QtWidgets.QPushButton):
    def __init__(self, label, setting_key, main_window):
        super().__init__(label)
        self.setting_key = setting_key
        self.main_window = main_window
        self.setText(self.main_window.settings.get(setting_key, label))
        self.clicked.connect(self.listen_for_hotkey)
        self.listening = False

    def listen_for_hotkey(self):
        self.setText("Press any key or mouse button...")
        self.listening = True
        self.grabKeyboard()
        self.grabMouse()

    def keyPressEvent(self, event):
        if self.listening:
            key = event.text() or event.key()
            if isinstance(key, int):
                key = QtGui.QKeySequence(key).toString()
            self.main_window.update_setting(self.setting_key, key)
            self.setText(key)
            self.listening = False
            self.releaseKeyboard()
            self.releaseMouse()
        else:
            super().keyPressEvent(event)

    def mousePressEvent(self, event):
        if self.listening:
            if event.button() == QtCore.Qt.LeftButton:
                btn = "Left Mouse Button"
            elif event.button() == QtCore.Qt.RightButton:
                btn = "Right Mouse Button"
            elif event.button() == QtCore.Qt.MiddleButton:
                btn = "Middle Mouse Button"
            else:
                btn = f"Mouse Button {event.button()}"
            self.main_window.update_setting(self.setting_key, btn)
            self.setText(btn)
            self.listening = False
            self.releaseKeyboard()
            self.releaseMouse()
        else:
            super().mousePressEvent(event)

class ColorButton(QtWidgets.QPushButton):
    def __init__(self, color, slot):
        super().__init__()
        self.setMaximumWidth(48)
        self.color = color
        self.slot = slot
        self.setStyleSheet(f"background-color: {color}; border: 1px solid #888;")
        self.clicked.connect(self.pick_color)

    def pick_color(self):
        col = QtWidgets.QColorDialog.getColor(QtGui.QColor(self.color))
        if col.isValid():
            self.color = col.name()
            self.setStyleSheet(f"background-color: {self.color}; border: 1px solid #888;")
            self.slot(self.color)

class SidebarButton(QtWidgets.QPushButton):
    def __init__(self, icon, page_index=None, parent=None):
        super().__init__(parent)
        self.page_index = page_index
        self.setFixedSize(56, 56)
        self.setIcon(icon)
        self.setIconSize(QtCore.QSize(50, 50))
        self.setCheckable(True)
        self.setFlat(True)
        self.setStyleSheet("""
            QPushButton {
                background-color: transparent;
                border: none;
            }
            QPushButton:checked, QPushButton:hover {
                background-color: #292B2E;
                border-radius: 12px;
            }
        """)

class ScrollAreaNoBar(QtWidgets.QScrollArea):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.setWidgetResizable(True)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setStyleSheet("""
            QScrollBar:vertical, QScrollBar:horizontal { width: 0px; height: 0px; }
        """)

    def wheelEvent(self, event):
        return super().wheelEvent(event)

class OverlayWindow(QtWidgets.QWidget):
    def __init__(self, settings, get_classes, main_window=None):
        super().__init__()
        self.settings = settings
        self.get_classes = get_classes
        self.main_window = main_window  # <-- now accepted!
        self.setWindowFlags(
            QtCore.Qt.WindowStaysOnTopHint |
            QtCore.Qt.FramelessWindowHint |
            QtCore.Qt.Tool
        )
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
        self.screen = QtWidgets.QApplication.primaryScreen().geometry()
        self.setGeometry(self.screen)
        self.fov_center = self.screen.center()
        self.boxes = []
        self.labels = []
        self.confidences = []
        self.classes = []
        self.aim_active = False
        self.opacity = self.settings.get('window_transparency', 0.87)
        self.setWindowOpacity(self.opacity)
        self.frame_times = []
        self.show_fps = True
        self.show()

    def set_opacity(self, opacity):
        self.opacity = opacity
        self.setWindowOpacity(opacity)
        self.repaint()

    def update_overlay(self, boxes, labels, confidences, classes):
        self.boxes = boxes
        self.labels = labels
        self.confidences = confidences
        self.classes = classes
        self.repaint()

    def set_aim_active(self, active):
        self.aim_active = active
        self.repaint()

    def paintEvent(self, event):
        import time
        now = time.time()
        self.frame_times.append(now)
        if len(self.frame_times) > 60:
            self.frame_times.pop(0)

        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setOpacity(self.opacity)
        # --- Draw FOV ---
        if self.settings.get('fov_enabled', True):
            pen = QtGui.QPen(parse_qcolor(self.settings.get('fov_color', "#B366FF")))
            pen.setWidth(1)
            pen.setCosmetic(True)
            painter.setPen(pen)
            r = clamp(self.settings.get('fov_radius', 200), 50, 600)
            center = self.fov_center
            painter.setBrush(QtCore.Qt.NoBrush)
            painter.drawEllipse(center, r, r)
        # --- Draw ESP Boxes ---
        if self.settings.get('esp_enabled', True):
            style = self.settings.get('esp_box_style', "Box")
            box_color = parse_qcolor(self.settings.get('esp_box_color', "#B366FF"))
            fill_color = parse_qcolor(self.settings.get('esp_fill_color', "#7E3FF2"))
            line_color = parse_qcolor(self.settings.get('esp_line_color', "#B366FF"))
            label_font = QtGui.QFont(self.settings.get('ui_font', "Segoe UI"), self.settings.get('esp_font_size', 12))
            painter.setFont(label_font)
            for i, box in enumerate(self.boxes):
                x1, y1, x2, y2 = box
                if style == "Box":
                    pen = QtGui.QPen(box_color)
                    pen.setWidth(1)
                    pen.setCosmetic(True)
                    painter.setPen(pen)
                    painter.setBrush(QtCore.Qt.NoBrush)
                    painter.drawRect(x1, y1, x2 - x1, y2 - y1)
                elif style == "Corner":
                    pen = QtGui.QPen(box_color)
                    pen.setWidth(1)
                    pen.setCosmetic(True)
                    painter.setPen(pen)
                    painter.setBrush(QtCore.Qt.NoBrush)
                    l = 18
                    painter.drawLine(x1, y1, x1+l, y1)
                    painter.drawLine(x1, y1, x1, y1+l)
                    painter.drawLine(x2, y1, x2-l, y1)
                    painter.drawLine(x2, y1, x2, y1+l)
                    painter.drawLine(x1, y2, x1+l, y2)
                    painter.drawLine(x1, y2, x1, y2-l)
                    painter.drawLine(x2, y2, x2-l, y2)
                    painter.drawLine(x2, y2, x2, y2-l)
                elif style == "Filled":
                    painter.setPen(QtCore.Qt.NoPen)
                    painter.setBrush(fill_color)
                    painter.drawRect(x1, y1, x2 - x1, y2 - y1)
                # --- Draw Snap Lines ---
                if self.settings.get('esp_show_lines', True):
                    pen = QtGui.QPen(line_color)
                    pen.setWidth(1)
                    pen.setCosmetic(True)
                    pen.setStyle(QtCore.Qt.DashLine)
                    painter.setPen(pen)
                    cx, cy = self.fov_center.x(), self.fov_center.y()
                    painter.drawLine(cx, cy, (x1 + x2) // 2, (y1 + y2) // 2)
                # --- Draw label with thin outline ---
                pen = QtGui.QPen(QtGui.QColor("#FFFFFF"), 1)
                painter.setPen(pen)
                if i < len(self.labels):
                    painter.drawText(x1, y1-8, self.labels[i])
        # --- FPS DISPLAY ---
        y_fps = 28
        if self.show_fps and len(self.frame_times) > 1:
            fps = len(self.frame_times) / (self.frame_times[-1] - self.frame_times[0])
            fps_text = f"Overlay FPS: {fps:.1f}"
            painter.setFont(QtGui.QFont("Segoe UI", 14, QtGui.QFont.Bold))
            painter.setPen(QtGui.QPen(QtGui.QColor("#00FF00"), 1))
            painter.drawText(12, y_fps, fps_text)
            y_fps += 22

        # --- Detection FPS DISPLAY (TRUE FPS) ---
        if self.main_window is not None and hasattr(self.main_window, "detection_fps_times"):
            times = self.main_window.detection_fps_times
            if len(times) > 1:
                fps = len(times) / (times[-1] - times[0])
                painter.setFont(QtGui.QFont("Segoe UI", 14, QtGui.QFont.Bold))
                painter.setPen(QtGui.QPen(QtGui.QColor("#00FFFF"), 1))
                painter.drawText(12, y_fps, f"Detection FPS: {fps:.1f}")

# --- ModernMainWindow (frameless, all tabs, clean thread stop), and main entry point ---

class ModernMainWindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.settings = load_settings()
        self.setWindowTitle(f"{APPNAME} — Melvin .cheats YOLO11n UD Detection by Melvin")
        self.setGeometry(120, 120, 900, 650)
        self.setMinimumSize(750, 500)
        # Frameless window, drag from anywhere without controls/widgets
        self.setWindowFlags(
            QtCore.Qt.FramelessWindowHint |
            QtCore.Qt.Window
        )
        self._drag_pos = None
        self._dragging = False
        self.apply_theme()
        self.detection_results = ([], [], [], [])
        self.detection_fps_times = []  # For true detection FPS display!
        self.overlay = OverlayWindow(self.settings, self.get_detection_classes, main_window=self)
        self.detection_thread = DetectionThread(self.settings)
        self.detection_thread.detection_signal.connect(self.overlay.update_overlay)
        self.aim_assist_thread = AimAssistThread(self.settings, self.get_detection_targets, self.get_fov_center)
        self.aim_assist_thread.aim_active_signal.connect(self.set_aim_active)
        self.trigger_bot_thread = TriggerBotThread(self.settings, self.get_detection_targets, self.get_fov_center)
        self.recoil_thread = RecoilControlThread(self.settings)
        self.threads = [self.detection_thread, self.aim_assist_thread, self.trigger_bot_thread, self.recoil_thread]
        self.init_ui()
        self.start_detection()
        self.menu_shortcut_key = self.settings.get("menu_shortcut", "F8")
        self.shortcut_listener_thread = threading.Thread(target=self.listen_for_menu_shortcut, daemon=True)
        self.shortcut_listener_thread.start()
        # Rounded corners
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        self.radius = 22  # You can tweak this for more/less rounding

    # --- Frameless Dragging ---
    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.LeftButton and not self.childAt(event.pos()):
            self._drag_pos = event.globalPos() - self.frameGeometry().topLeft()
            self._dragging = True
            event.accept()
        else:
            super().mousePressEvent(event)
    def mouseMoveEvent(self, event):
        if self._dragging and event.buttons() == QtCore.Qt.LeftButton:
            self.move(event.globalPos() - self._drag_pos)
            event.accept()
        else:
            super().mouseMoveEvent(event)
    def mouseReleaseEvent(self, event):
        self._dragging = False
        super().mouseReleaseEvent(event)

    def listen_for_menu_shortcut(self):
        while True:
            try:
                if keyboard.is_pressed(self.menu_shortcut_key):
                    self.showNormal()
                    self.activateWindow()
                    self.raise_()
                    time.sleep(0.8)
            except Exception:
                pass
            time.sleep(0.15)

    def apply_theme(self):
        purple = "#B366FF"
        font = self.settings.get('ui_font', "Segoe UI")
        font_size = self.settings.get('ui_font_size', 11)
        opacity = self.settings.get('window_transparency', 0.87)
        self.setWindowOpacity(opacity)
        self.setStyleSheet(f"""
            QWidget {{
                background-color: rgba(24,26,27,220);
                color: #FFFFFF;
                font-family: "{font}";
                font-size: {font_size}pt;
                border-radius: 18px;
            }}
            QTabBar::tab:selected {{ background: {purple}; color: #181A1B; border-radius: 10px; }}
            QTabWidget::pane {{ border: 2px solid {purple}; border-radius: 10px; }}
            QPushButton, QSlider::groove:horizontal {{
                background: #222;
                color: {purple};
                border-radius: 8px;
                padding: 3px;
            }}
            QSlider::handle:horizontal {{
                background: {purple};
                width: 15px;
                border-radius: 8px;
            }}
            QLabel {{ color: #FFFFFF; }}
        """)

    def set_aim_active(self, active):
        self.overlay.set_aim_active(active)

    def update_setting(self, key, value):
        self.settings[key] = value
        save_settings(self.settings)
        self.overlay.settings = self.settings
        self.detection_thread.settings = self.settings
        self.aim_assist_thread.settings = self.settings
        self.trigger_bot_thread.settings = self.settings
        self.recoil_thread.settings = self.settings
        self.apply_theme()
        self.repaint()

    def get_detection_targets(self):
        return self.detection_results[0]

    def get_detection_classes(self):
        return self.detection_results[3]

    def get_fov_center(self):
        g = QtWidgets.QApplication.primaryScreen().geometry()
        return (g.width()//2, g.height()//2)

    def start_detection(self):
        for t in self.threads:
            if not t.isRunning():
                t.start()
        self.detection_thread.detection_signal.connect(self.handle_detection_results)

    def handle_detection_results(self, boxes, labels, confidences, classes):
        self.detection_results = (boxes, labels, confidences, classes)
        # --- Track true detection FPS ---
        import time
        now = time.time()
        self.detection_fps_times.append(now)
        if len(self.detection_fps_times) > 60:
            self.detection_fps_times.pop(0)

    def closeEvent(self, event):
        for t in self.threads:
            try:
                t.stop()
            except Exception: pass
        for t in self.threads:
            try:
                t.wait(1000)
            except Exception: pass
        event.accept()

    def init_ui(self):
        main_layout = QtWidgets.QHBoxLayout(self)
        sidebar = QtWidgets.QVBoxLayout()
        sidebar.setSpacing(6)
        sidebar.setContentsMargins(18, 18, 6, 18)

        icon_paths = [
            QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\aim.png"),
            QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\fov.png"),
            QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\esp.png"),
            QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\trigger.png"),
            QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\settings.png"),
            QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\recoil.png"),
        ]
        self.sidebar_buttons = []
        for i in range(6):
            btn = SidebarButton(icon_paths[i], i)
            btn.setIconSize(QtCore.QSize(50, 50))
            btn.clicked.connect(lambda checked, idx=i: self.set_tab(idx))
            self.sidebar_buttons.append(btn)
            sidebar.addWidget(btn)

        discord_icon = QtGui.QIcon(r"C:\Users\belze\OneDrive\Desktop\UD Premium Lite\icons\discord.png")
        discord_btn = QtWidgets.QPushButton()
        discord_btn.setIcon(discord_icon)
        discord_btn.setIconSize(QtCore.QSize(50, 50))
        discord_btn.setFixedSize(56, 56)
        discord_btn.setStyleSheet("""
            QPushButton {
                background-color: transparent;
                border: none;
            }
            QPushButton:hover {
                background-color: #5865F2;
                border-radius: 14px;
            }
        """)
        discord_btn.clicked.connect(lambda: webbrowser.open_new_tab("https://discord.gg/q7hXusbe"))
        sidebar.addStretch(1)
        sidebar.addWidget(discord_btn)

        self.tabs = QtWidgets.QTabWidget()
        self.tabs.setStyleSheet("QTabBar::tab { min-width: 120px; font-size: 14pt; border-radius: 10px; }")
        self.tabs.addTab(self.make_aim_tab(), "AIM")
        self.tabs.addTab(self.make_fov_tab(), "FOV")
        self.tabs.addTab(self.make_esp_tab(), "ESP")
        self.tabs.addTab(self.make_trigger_tab(), "TRIGGER")
        self.tabs.addTab(self.make_settings_tab(), "SETTINGS")
        self.tabs.addTab(self.make_recoil_tab(), "RECOIL")
        self.tabs.tabBar().hide()
        container = QtWidgets.QWidget()
        content_layout = QtWidgets.QVBoxLayout(container)
        content_layout.setContentsMargins(10, 18, 18, 18)
        content_layout.addWidget(self.tabs)
        main_layout.addLayout(sidebar, 0)
        main_layout.addWidget(container, 1)
        main_layout.setSpacing(0)
        self.setLayout(main_layout)
        self.set_tab(0)

    def set_tab(self, idx):
        self.tabs.setCurrentIndex(idx)
        for i, btn in enumerate(self.sidebar_buttons):
            btn.setChecked(i == idx)

    def make_percent_slider(self, minv, maxv, value, label, slot, decimals=2, suffix='%'):
        hbox = QtWidgets.QHBoxLayout()
        slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        slider.setMinimum(int(minv*100))
        slider.setMaximum(int(maxv*100))
        slider.setValue(int(value*100))
        percent_label = QtWidgets.QLabel(f"{int(value*100)}{suffix}")
        def update_label(v):
            percent_label.setText(f"{v}{suffix}")
            slot(round(v/100, decimals))
        slider.valueChanged.connect(lambda v: update_label(v))
        hbox.addWidget(QtWidgets.QLabel(label))
        hbox.addWidget(slider)
        hbox.addWidget(percent_label)
        w = QtWidgets.QWidget()
        w.setLayout(hbox)
        return w

    # ---- UI Tab Methods ----

    def make_aim_tab(self):
        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(w)
        aim_enable = QtWidgets.QCheckBox("Enable Aim Assist")
        aim_enable.setChecked(self.settings['aim_enabled'])
        aim_enable.stateChanged.connect(lambda s: self.update_setting('aim_enabled', bool(s)))
        layout.addWidget(aim_enable)
        aim_hotkey_btn = HotkeyButton("Set AIM Hotkey", "aim_hotkey", self)
        layout.addWidget(QtWidgets.QLabel("Aim Assist Hotkey:"))
        layout.addWidget(aim_hotkey_btn)
        move_speed = self.make_percent_slider(0.1, 1.5, self.settings.get('aim_move_speed', 0.8), "Move Speed", lambda v: self.update_setting('aim_move_speed', v), decimals=2)
        layout.addWidget(move_speed)
        aim_smoothing = self.make_percent_slider(0.0, 1.0, self.settings.get('aim_smoothing', 0.45), "Aim Smoothing", lambda v: self.update_setting('aim_smoothing', v), decimals=2)
        layout.addWidget(aim_smoothing)
        sticky_max = self.make_percent_slider(5, 80, self.settings.get('aim_sticky_max', 35)/80, "Sticky Max", lambda v: self.update_setting('aim_sticky_max', int(v*80)), decimals=0, suffix="px")
        layout.addWidget(sticky_max)
        sticky_decay = self.make_percent_slider(0.5, 1.0, self.settings.get('aim_sticky_decay', 0.85), "Sticky Decay", lambda v: self.update_setting('aim_sticky_decay', v), decimals=2)
        layout.addWidget(sticky_decay)
        sticky_boost = self.make_percent_slider(1.0, 2.5, self.settings.get('aim_sticky_boost', 1.55), "Sticky Boost", lambda v: self.update_setting('aim_sticky_boost', v), decimals=2)
        layout.addWidget(sticky_boost)
        pred_factor = self.make_percent_slider(0.0, 1.0, self.settings.get('aim_prediction_factor', 0.1), "Prediction Factor", lambda v: self.update_setting('aim_prediction_factor', v), decimals=2)
        layout.addWidget(pred_factor)
        # --- Silent Aim ---
        silent_enable = QtWidgets.QCheckBox("Enable Silent Aim (hold key to flick to target)")
        silent_enable.setChecked(self.settings.get('silent_aim_enabled', False))
        silent_enable.stateChanged.connect(lambda s: self.update_setting('silent_aim_enabled', bool(s)))
        layout.addWidget(silent_enable)
        silent_hotkey_btn = HotkeyButton("Set Silent Aim Hotkey", "silent_aim_hotkey", self)
        layout.addWidget(QtWidgets.QLabel("Silent Aim Hotkey:"))
        layout.addWidget(silent_hotkey_btn)
        silent_speed = self.make_percent_slider(0.2, 5.0, self.settings.get('silent_aim_speed', 1.0)/5, "Silent Aim Flick Speed", lambda v: self.update_setting('silent_aim_speed', v*5), decimals=2)
        layout.addWidget(silent_speed)
        layout.addStretch(1)
        scroll = ScrollAreaNoBar()
        scroll.setWidgetResizable(True)
        w.setLayout(layout)
        scroll.setWidget(w)
        return scroll

    def make_fov_tab(self):
        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(w)
        fov_enable = QtWidgets.QCheckBox("Show FOV Circle")
        fov_enable.setChecked(self.settings['fov_enabled'])
        fov_enable.stateChanged.connect(lambda s: self.update_setting('fov_enabled', bool(s)))
        layout.addWidget(fov_enable)
        fov_radius = self.make_percent_slider(0.1, 0.8, self.settings['fov_radius']/800, "FOV Circle Size", lambda v: self.update_setting('fov_radius', int(v*800)), decimals=2)
        layout.addWidget(fov_radius)
        fov_thick_slider = self.make_percent_slider(1, 8, self.settings.get('fov_thickness', 1)/8, "FOV Circle Thickness", lambda v: self.update_setting('fov_thickness', int(v*8)), decimals=0, suffix="px")
        layout.addWidget(fov_thick_slider)
        fov_opacity = self.make_percent_slider(0.05, 1.0, self.settings.get('fov_opacity', 0.23), "FOV Opacity", lambda v: self.update_setting('fov_opacity', v), decimals=2)
        layout.addWidget(fov_opacity)
        fov_color = ColorButton(self.settings.get('fov_color', "#B366FF"), lambda c: self.update_setting('fov_color', c))
        layout.addWidget(QtWidgets.QLabel("FOV Circle Color:"))
        layout.addWidget(fov_color)
        layout.addStretch(1)
        scroll = ScrollAreaNoBar()
        scroll.setWidgetResizable(True)
        w.setLayout(layout)
        scroll.setWidget(w)
        return scroll

    def make_esp_tab(self):
        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(w)
        esp_enable = QtWidgets.QCheckBox("Enable ESP")
        esp_enable.setChecked(self.settings['esp_enabled'])
        esp_enable.stateChanged.connect(lambda s: self.update_setting('esp_enabled', bool(s)))
        layout.addWidget(esp_enable)
        box_color = ColorButton(self.settings.get('esp_box_color', "#B366FF"), lambda c: self.update_setting('esp_box_color', c))
        fill_color = ColorButton(self.settings.get('esp_fill_color', "#7E3FF2"), lambda c: self.update_setting('esp_fill_color', c))
        line_color = ColorButton(self.settings.get('esp_line_color', "#B366FF"), lambda c: self.update_setting('esp_line_color', c))
        layout.addWidget(QtWidgets.QLabel("ESP Box Color:"))
        layout.addWidget(box_color)
        layout.addWidget(QtWidgets.QLabel("ESP Fill Color:"))
        layout.addWidget(fill_color)
        layout.addWidget(QtWidgets.QLabel("ESP Line Color:"))
        layout.addWidget(line_color)
        esp_style = QtWidgets.QComboBox()
        esp_style.addItems(["Box", "Corner", "Filled"])
        esp_style.setCurrentText(self.settings.get('esp_box_style', "Box"))
        esp_style.currentTextChanged.connect(lambda v: self.update_setting('esp_box_style', v))
        layout.addWidget(QtWidgets.QLabel("ESP Box Style:"))
        layout.addWidget(esp_style)
        esp_lines = QtWidgets.QCheckBox("Show Lines to ESP Boxes")
        esp_lines.setChecked(self.settings.get('esp_show_lines', True))
        esp_lines.stateChanged.connect(lambda s: self.update_setting('esp_show_lines', bool(s)))
        layout.addWidget(esp_lines)
        font_size = self.make_percent_slider(8, 22, self.settings.get('esp_font_size', 12)/22, "ESP Font Size", lambda v: self.update_setting('esp_font_size', int(v*22)), decimals=0, suffix="px")
        layout.addWidget(font_size)
        esp_box_slider = self.make_percent_slider(1, 8, self.settings.get('esp_box_thickness', 1)/8, "ESP Box Thickness", lambda v: self.update_setting('esp_box_thickness', int(v*8)), decimals=0, suffix="px")
        layout.addWidget(esp_box_slider)
        esp_line_slider = self.make_percent_slider(1, 8, self.settings.get('esp_line_thickness', 1)/8, "ESP Line Thickness", lambda v: self.update_setting('esp_line_thickness', int(v*8)), decimals=0, suffix="px")
        layout.addWidget(esp_line_slider)
        layout.addStretch(1)
        scroll = ScrollAreaNoBar()
        scroll.setWidgetResizable(True)
        w.setLayout(layout)
        scroll.setWidget(w)
        return scroll

    def make_trigger_tab(self):
        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(w)
        trigger_enable = QtWidgets.QCheckBox("Enable Trigger Bot")
        trigger_enable.setChecked(self.settings.get("trigger_enabled", False))
        trigger_enable.stateChanged.connect(lambda s: self.update_setting("trigger_enabled", bool(s)))
        layout.addWidget(trigger_enable)
        trigger_auto = QtWidgets.QCheckBox("Auto Fire (hold button down while target in center)")
        trigger_auto.setChecked(self.settings.get("trigger_auto_fire", False))
        trigger_auto.stateChanged.connect(lambda s: self.update_setting("trigger_auto_fire", bool(s)))
        layout.addWidget(trigger_auto)
        burst_enable = QtWidgets.QCheckBox("Enable Burst Mode")
        burst_enable.setChecked(self.settings.get("trigger_burst", False))
        burst_enable.stateChanged.connect(lambda s: self.update_setting("trigger_burst", bool(s)))
        layout.addWidget(burst_enable)
        hbox = QtWidgets.QHBoxLayout()
        slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        slider.setMinimum(50)
        slider.setMaximum(1000)
        slider.setValue(self.settings.get("trigger_fire_speed", 150))
        fire_label = QtWidgets.QLabel(f"{self.settings.get('trigger_fire_speed', 150)} ms")
        def set_fire_speed(v):
            fire_label.setText(f"{v} ms")
            self.update_setting("trigger_fire_speed", int(v))
        slider.valueChanged.connect(set_fire_speed)
        hbox.addWidget(QtWidgets.QLabel("Fire Speed (ms between bursts):"))
        hbox.addWidget(slider)
        hbox.addWidget(fire_label)
        layout.addLayout(hbox)
        hbox2 = QtWidgets.QHBoxLayout()
        tol_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        tol_slider.setMinimum(0)
        tol_slider.setMaximum(10)
        tol_slider.setValue(self.settings.get("trigger_tolerance", 2))
        tol_label = QtWidgets.QLabel(f"{self.settings.get('trigger_tolerance', 2)} px")
        def set_tol(v):
            tol_label.setText(f"{v} px")
            self.update_setting('trigger_tolerance', int(v))
        tol_slider.valueChanged.connect(set_tol)
        hbox2.addWidget(QtWidgets.QLabel("Trigger Center Tolerance:"))
        hbox2.addWidget(tol_slider)
        hbox2.addWidget(tol_label)
        layout.addLayout(hbox2)
        hbox3 = QtWidgets.QHBoxLayout()
        burst_count_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        burst_count_slider.setMinimum(2)
        burst_count_slider.setMaximum(8)
        burst_count_slider.setValue(self.settings.get("trigger_burst_count", 3))
        burst_count_label = QtWidgets.QLabel(f"{self.settings.get('trigger_burst_count', 3)} shots")
        def set_burst_count(v):
            burst_count_label.setText(f"{v} shots")
            self.update_setting("trigger_burst_count", int(v))
        burst_count_slider.valueChanged.connect(set_burst_count)
        hbox3.addWidget(QtWidgets.QLabel("Burst Count:"))
        hbox3.addWidget(burst_count_slider)
        hbox3.addWidget(burst_count_label)
        layout.addLayout(hbox3)
        hbox4 = QtWidgets.QHBoxLayout()
        burst_delay_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        burst_delay_slider.setMinimum(20)
        burst_delay_slider.setMaximum(200)
        burst_delay_slider.setValue(self.settings.get("trigger_burst_delay", 60))
        burst_delay_label = QtWidgets.QLabel(f"{self.settings.get('trigger_burst_delay', 60)} ms")
        def set_burst_delay(v):
            burst_delay_label.setText(f"{v} ms")
            self.update_setting("trigger_burst_delay", int(v))
        burst_delay_slider.valueChanged.connect(set_burst_delay)
        hbox4.addWidget(QtWidgets.QLabel("Burst Delay (between shots):"))
        hbox4.addWidget(burst_delay_slider)
        hbox4.addWidget(burst_delay_label)
        layout.addLayout(hbox4)
        layout.addStretch(1)
        scroll = ScrollAreaNoBar()
        scroll.setWidgetResizable(True)
        w.setLayout(layout)
        scroll.setWidget(w)
        return scroll

    def make_settings_tab(self):
        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(w)
        conf_slider = self.make_percent_slider(0.1, 0.9, self.settings.get('confidence', 0.5), "Detection Confidence", lambda v: self.update_setting('confidence', v), decimals=2)
        layout.addWidget(conf_slider)
        window_opacity_slider = self.make_percent_slider(0.1, 1.0, self.settings.get('window_transparency', 0.87), "Menu/Background Opacity", lambda v: self.update_setting('window_transparency', v), decimals=2)
        layout.addWidget(window_opacity_slider)
        hbox = QtWidgets.QHBoxLayout()
        import_btn = QtWidgets.QPushButton("Import Settings")
        export_btn = QtWidgets.QPushButton("Export Settings")
        hbox.addWidget(import_btn)
        hbox.addWidget(export_btn)
        layout.addLayout(hbox)
        import_btn.clicked.connect(lambda: self.handle_import_settings())
        export_btn.clicked.connect(lambda: export_settings(self.settings, self))
        shortcut_hbox = QtWidgets.QHBoxLayout()
        shortcut_label = QtWidgets.QLabel("Menu Shortcut Key:")
        shortcut_edit = QtWidgets.QLineEdit(self.settings.get("menu_shortcut", "F8"))
        shortcut_edit.setMaximumWidth(60)
        def set_shortcut():
            key = shortcut_edit.text().strip()
            if key:
                self.update_setting("menu_shortcut", key)
                self.menu_shortcut_key = key
        shortcut_edit.editingFinished.connect(set_shortcut)
        shortcut_hbox.addWidget(shortcut_label)
        shortcut_hbox.addWidget(shortcut_edit)
        layout.addLayout(shortcut_hbox)
        layout.addStretch(1)
        scroll = ScrollAreaNoBar()
        scroll.setWidgetResizable(True)
        w.setLayout(layout)
        scroll.setWidget(w)
        return scroll

    def make_recoil_tab(self):
        w = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(w)
        recoil_enable = QtWidgets.QCheckBox("Enable Recoil Control")
        recoil_enable.setChecked(self.settings.get('recoil_enabled', False))
        recoil_enable.stateChanged.connect(lambda s: self.update_setting('recoil_enabled', bool(s)))
        layout.addWidget(recoil_enable)
        recoil_strength_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        recoil_strength_slider.setMinimum(0)
        recoil_strength_slider.setMaximum(100)
        recoil_strength_slider.setValue(self.settings.get('recoil_strength', 50))
        recoil_strength_label = QtWidgets.QLabel(f"{self.settings.get('recoil_strength', 50)}%")
        def set_strength(v):
            recoil_strength_label.setText(f"{v}%")
            self.update_setting('recoil_strength', v)
        recoil_strength_slider.valueChanged.connect(set_strength)
        layout.addWidget(QtWidgets.QLabel("Recoil Strength"))
        layout.addWidget(recoil_strength_slider)
        layout.addWidget(recoil_strength_label)
        recoil_jitter_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
        recoil_jitter_slider.setMinimum(0)
        recoil_jitter_slider.setMaximum(20)
        recoil_jitter_slider.setValue(self.settings.get('recoil_jitter', 5))
        recoil_jitter_label = QtWidgets.QLabel(f"{self.settings.get('recoil_jitter', 5)} px")
        def set_jitter(v):
            recoil_jitter_label.setText(f"{v} px")
            self.update_setting('recoil_jitter', v)
        recoil_jitter_slider.valueChanged.connect(set_jitter)
        layout.addWidget(QtWidgets.QLabel("Jitter (humanization)"))
        layout.addWidget(recoil_jitter_slider)
        layout.addWidget(recoil_jitter_label)
        pattern_combo = QtWidgets.QComboBox()
        pattern_combo.addItems(["Default", "Pattern 1", "Pattern 2"])
        pattern_combo.setCurrentText(self.settings.get('recoil_pattern', "Default"))
        pattern_combo.currentTextChanged.connect(lambda v: self.update_setting('recoil_pattern', v))
        layout.addWidget(QtWidgets.QLabel("Recoil Pattern"))
        layout.addWidget(pattern_combo)
        activation_key = HotkeyButton("Set Recoil Activation Key", "recoil_activation_key", self)
        layout.addWidget(QtWidgets.QLabel("Activation Key:"))
        layout.addWidget(activation_key)
        layout.addStretch(1)
        scroll = ScrollAreaNoBar()
        scroll.setWidgetResizable(True)
        w.setLayout(layout)
        scroll.setWidget(w)
        return scroll

    def handle_import_settings(self):
        new_settings = import_settings(self)
        if new_settings:
            self.settings = new_settings
            save_settings(self.settings)
            QtWidgets.QMessageBox.information(self, "Imported", "Settings imported. Restart app for all changes.")
            
    def paintEvent(self, event):
        path = QtGui.QPainterPath()
        path.addRoundedRect(QtCore.QRectF(self.rect()), self.radius, self.radius)
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setClipPath(path)
        painter.setPen(QtCore.Qt.NoPen)
        painter.setBrush(QtGui.QColor(24, 26, 27, 220))
        painter.drawPath(path)
        super().paintEvent(event)
        
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    settings = load_settings()
    main_win = ModernMainWindow()
    main_win.show()
    sys.exit(app.exec_())