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


import math

import os

import threading

import tkinter as tk

from tkinter import font as tkfont



import customtkinter as ctk

from PIL import Image, ImageDraw, ImageTk



from bot_logic import BrawlBot



BASE_DIR = os.path.dirname(os.path.abspath(__file__))

BRAWLERS_DIR = os.path.join(BASE_DIR, "assets", "brawlers")

UI_DIR = os.path.join(BASE_DIR, "assets", "ui")



COLUMNS = 10

MAX_ICON = 82

MIN_ICON = 30

# Доля каждой стороны иконки, которая обрезается (убирает тёмную рамку,

# нарисованную прямо в PNG). 0.0 = не обрезать, 0.08 = по 8% с каждого края.

ICON_CROP = 0.08

WINDOW_BLUE = "#004cd0"

ENTRY_HEIGHT = 32

LOGO_HEIGHT = 140     # было 70 — логотип увеличен в 2 раза

BG_TOLERANCE = 40     # насколько цвет может отличаться от фона, чтобы считаться фоном



# Шрифты по приоритету: берётся первый установленный в системе

FONT_CANDIDATES = ["Segoe UI", "Nunito", "Trebuchet MS", "Verdana", "Arial"]

FONT_FAMILY = "Arial"





def pick_font_family():

  installed = set(tkfont.families())

  for family in FONT_CANDIDATES:

    if family in installed:

      return family

  return "Arial"





def remove_background(image, tolerance=BG_TOLERANCE):

  """Убирает однотонный фон (белый/любой по цвету углов) и обрезает картинку

  по границам содержимого. Фон ищется заливкой от краёв, поэтому светлые

  детали внутри самого изображения не пострадают."""

  image = image.convert("RGBA")

  width, height = image.size

  transparent = (0, 0, 0, 0)



  for corner in ((0, 0), (width - 1, 0), (0, height - 1), (width - 1, height - 1)):

    if image.getpixel(corner)[3] == 0:

      continue # этот угол уже прозрачный

    ImageDraw.floodfill(image, corner, transparent, thresh=tolerance)



  # Сглаживание: убираем светлую «окантовку» вдоль границы с прозрачностью

  pixels = image.load()

  to_clear = []

  for y in range(height):

    for x in range(width):

      r, g, b, a = pixels[x, y]

      if a == 0 or min(r, g, b) < 200:

        continue

      for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):

        if 0 <= nx < width and 0 <= ny < height and pixels[nx, ny][3] == 0:

          to_clear.append((x, y))

          break

  for x, y in to_clear:

    pixels[x, y] = transparent



  bbox = image.getbbox() # учитывает альфа-канал

  if bbox:

    image = image.crop(bbox)

  return image





def load_ui_image(filename, remove_bg=False):

  """Загружает картинку из assets/ui или возвращает None, если файла нет.

  remove_bg=True — вырезает фон и обрезает пустые поля."""

  path = os.path.join(UI_DIR, filename)

  if not os.path.exists(path):

    print(f"Файл не найден: {path}")

    return None

  image = Image.open(path).convert("RGBA")

  if remove_bg:

    image = remove_background(image)

  return image





class BrawlBotApp(ctk.CTk):

  def __init__(self):

    super().__init__()



    global FONT_FAMILY

    FONT_FAMILY = pick_font_family()



    self.title("Brawl Bot Controller")

    self.geometry("1140x700")

    self.minsize(900, 620)

    self.configure(fg_color=WINDOW_BLUE)



    self.bot = None      # создаётся в фоне (EasyOCR грузится долго)

    self.busy = False

    self.selected_brawler = None

    self.brawlers = []     # список (имя, PIL-картинка)

    self.icon_photos = []   # PhotoImage текущего размера

    self.icon_size = 0

    self.scroll_offset = 0

    self.max_offset = 0

    self.grid_w = 0

    self.grid_h = 0

    self.cell_w = 1

    self.cell_h = 1

    self.bg_pil = None

    self.bg_resized = None

    self._resize_job = None



    self.load_brawlers()

    self.setup_canvas()

    self.setup_grid()

    self.setup_inputs()

    self.init_bot_async()



    self.bind("<Configure>", self.on_configure)

    self.after(100, self.relayout)



  # ------------------------------------------------------------------

  # Загрузка бойцов

  # ------------------------------------------------------------------

  def load_brawlers(self):

    if not os.path.isdir(BRAWLERS_DIR):

      print(f"Папка не найдена: {BRAWLERS_DIR}")

      return



    files = sorted(

      f for f in os.listdir(BRAWLERS_DIR) if f.lower().endswith(".png")

    )

    for filename in files:

      name = os.path.splitext(filename)[0]

      path = os.path.join(BRAWLERS_DIR, filename)

      try:

        image = Image.open(path).convert("RGBA")

      except Exception as error:

        print(f"Не удалось загрузить {filename}: {error}")

        continue



      width, height = image.size

      margin = int(min(width, height) * ICON_CROP)

      if margin > 0:

        image = image.crop((margin, margin, width - margin, height - margin))



      self.brawlers.append((name, image))



  # ------------------------------------------------------------------

  # Главный холст: фон, текст и звезда рисуются прямо на нём

  # ------------------------------------------------------------------

  def setup_canvas(self):

    bg_path = os.path.join(UI_DIR, "background.png")

    if os.path.exists(bg_path):

      self.bg_pil = Image.open(bg_path).convert("RGB")



    self.canvas = tk.Canvas(

      self, highlightthickness=0, bd=0, bg=WINDOW_BLUE

    )

    self.canvas.place(x=0, y=0, relwidth=1, relheight=1)



    self.bg_item = self.canvas.create_image(0, 0, anchor="nw")



    self.title_item = self.canvas.create_text(

      0, 0,

      text="BRAWL BOT CONTROLLER",

      font=(FONT_FAMILY, -36, "bold"),

      fill="#ffcc00",

    )

    self.subtitle_item = self.canvas.create_text(

      0, 0,

      text="ВЫБЕРИТЕ БОЙЦА ДЛЯ ПРОКАЧКИ:",

      font=(FONT_FAMILY, -18, "bold"),

      fill="#ffffff",

    )

    self.status_item = self.canvas.create_text(

      0, 0,

      text="Боец не выбран",

      font=(FONT_FAMILY, -16, "italic"),

      fill="#ff4500",

    )



    # Логотип вместо текстового заголовка

    self.logo_pil = load_ui_image("logo.png", remove_bg=True)

    self.logo_item = self.canvas.create_image(0, 0, anchor="center")

    if self.logo_pil is not None:

      self.canvas.itemconfigure(self.title_item, state="hidden")



    # Кубки рядом с полями ввода

    self.trophy_pil = load_ui_image("trophy.png", remove_bg=True)

    self.trophy2_pil = load_ui_image("2trophy.png", remove_bg=True)

    self.trophy_item = self.canvas.create_image(0, 0, anchor="e")

    self.trophy2_item = self.canvas.create_image(0, 0, anchor="w")



    self.star_item = None

    star_path = os.path.join(UI_DIR, "star.png")

    if os.path.exists(star_path):

      star_pil = Image.open(star_path).convert("RGBA")

      star_pil = star_pil.resize((60, 60), Image.LANCZOS)

      self.star_photo = ImageTk.PhotoImage(star_pil)

      self.star_item = self.canvas.create_image(

        0, 0, anchor="ne", image=self.star_photo

      )



  # ------------------------------------------------------------------

  # Сетка бойцов (свой холст с кусочком фона, прокрутка колесом мыши)

  # ------------------------------------------------------------------

  def setup_grid(self):

    self.grid_canvas = tk.Canvas(

      self, highlightthickness=0, bd=0, bg=WINDOW_BLUE

    )

    self.grid_bg_item = self.grid_canvas.create_image(0, 0, anchor="nw")



    self.grid_canvas.bind("<Button-1>", self.on_grid_click)

    self.grid_canvas.bind("<MouseWheel>", self.on_mouse_wheel)

    self.grid_canvas.bind("<Button-4>", lambda e: self.scroll_by(-40))

    self.grid_canvas.bind("<Button-5>", lambda e: self.scroll_by(40))



  def render_grid(self):

    gw, gh = self.grid_w, self.grid_h

    if gw <= 0 or gh <= 0:

      return



    usable_w = gw - 12

    cell_w = usable_w / COLUMNS

    icon = max(MIN_ICON, min(MAX_ICON, int(cell_w) - 10))

    cell_h = icon + 12



    if icon != self.icon_size:

      self.icon_size = icon

      self.icon_photos = [

        ImageTk.PhotoImage(img.resize((icon, icon), Image.LANCZOS))

        for _, img in self.brawlers

      ]



    rows = math.ceil(len(self.brawlers) / COLUMNS)

    total_h = rows * cell_h

    self.cell_w = cell_w

    self.cell_h = cell_h

    self.max_offset = max(0, total_h - gh)

    self.scroll_offset = max(0, min(self.scroll_offset, self.max_offset))



    canvas = self.grid_canvas

    canvas.delete("icons")



    for index, (name, _) in enumerate(self.brawlers):

      row = index // COLUMNS

      col = index % COLUMNS

      cx = col * cell_w + cell_w / 2

      cy = row * cell_h + cell_h / 2 - self.scroll_offset



      if cy + cell_h / 2 < 0 or cy - cell_h / 2 > gh:

        continue



      canvas.create_image(

        cx, cy, image=self.icon_photos[index], tags="icons"

      )



      if name == self.selected_brawler:

        half = icon / 2 + 2

        canvas.create_rectangle(

          cx - half, cy - half, cx + half, cy + half,

          outline="#ffcc00", width=3, tags="icons",

        )



    # Тонкий индикатор прокрутки справа

    if self.max_offset > 0:

      thumb_h = max(30, gh * gh / total_h)

      thumb_y = (self.scroll_offset / self.max_offset) * (gh - thumb_h)

      canvas.create_rectangle(

        gw - 7, thumb_y, gw - 3, thumb_y + thumb_h,

        fill="#ffffff", outline="", tags="icons",

      )



  def scroll_by(self, delta):

    if self.max_offset <= 0:

      return

    self.scroll_offset = max(0, min(self.scroll_offset + delta, self.max_offset))

    self.render_grid()



  def on_mouse_wheel(self, event):

    direction = -1 if event.delta > 0 else 1

    self.scroll_by(direction * 40)



  def on_grid_click(self, event):

    if event.x >= self.grid_w - 12:

      return

    col = int(event.x // self.cell_w)

    row = int((event.y + self.scroll_offset) // self.cell_h)

    if col < 0 or col >= COLUMNS or row < 0:

      return

    index = row * COLUMNS + col

    if index < len(self.brawlers):

      self.select_brawler(self.brawlers[index][0])



  def select_brawler(self, name):

    self.selected_brawler = name

    self.canvas.itemconfigure(

      self.status_item, text=f"Выбран: {name}", fill="#00ff88"

    )

    self.render_grid()



  # ------------------------------------------------------------------

  # Поля ввода и кнопка старта (стоят прямо на окне)

  # ------------------------------------------------------------------

  def setup_inputs(self):

    entry_font = ctk.CTkFont(family=FONT_FAMILY, size=14)

    button_font = ctk.CTkFont(family=FONT_FAMILY, size=15, weight="bold")



    self.current_cups_entry = ctk.CTkEntry(

      self, placeholder_text="Текущие кубки", width=150,

      height=ENTRY_HEIGHT, font=entry_font,

    )

    self.current_cups_entry.place(relx=0.5, rely=1.0, x=-85, y=-115, anchor="s")



    self.target_cups_entry = ctk.CTkEntry(

      self, placeholder_text="Цель кубков", width=150,

      height=ENTRY_HEIGHT, font=entry_font,

    )

    self.target_cups_entry.place(relx=0.5, rely=1.0, x=85, y=-115, anchor="s")



    # Зелёная кнопка: поиск и выбор бойца по тексту

    self.start_button = ctk.CTkButton(

      self,

      text="ПОДТВЕРДИТЬ ВЫБРАННЫЕ НАСТРОЙКИ",

      font=ctk.CTkFont(family=FONT_FAMILY, size=16, weight="bold"),

      fg_color="#00cc66",

      hover_color="#00994d",

      height=40,

      command=self.click_start,

    )

    self.start_button.place(relx=0.5, rely=1.0, y=-60, anchor="s")



    # Нижний ряд: BRAWLERS (сине-фиолетовая) и PLAY (жёлто-оранжевая)

    self.brawlers_button = ctk.CTkButton(

      self,

      text="BRAWLERS",

      font=button_font,

      fg_color="#6a3de8",

      hover_color="#4f2bb8",

      width=170,

      height=38,

      command=self.click_brawlers,

    )

    self.brawlers_button.place(relx=0.5, rely=1.0, x=-95, y=-14, anchor="s")



    self.play_button = ctk.CTkButton(

      self,

      text="PLAY",

      font=button_font,

      fg_color="#ffa500",

      hover_color="#e68a00",

      text_color="#1a1a1a",

      width=170,

      height=38,

      command=self.click_play,

    )

    self.play_button.place(relx=0.5, rely=1.0, x=95, y=-14, anchor="s")



  # ------------------------------------------------------------------

  # Адаптивная раскладка

  # ------------------------------------------------------------------

  def on_configure(self, event):

    if event.widget is not self:

      return

    if self._resize_job is not None:

      self.after_cancel(self._resize_job)

    self._resize_job = self.after(30, self.relayout)



  def update_decor(self, w, h):

    self.update_idletasks()



    # Логотип по центру сверху

    if self.logo_pil is not None:

      max_h = LOGO_HEIGHT

      max_w = max(w - 240, 100)

      scale = min(

        max_h / self.logo_pil.height, max_w / self.logo_pil.width

      )

      size = (

        max(1, int(self.logo_pil.width * scale)),

        max(1, int(self.logo_pil.height * scale)),

      )

      self.logo_photo = ImageTk.PhotoImage(

        self.logo_pil.resize(size, Image.LANCZOS)

      )

      self.canvas.itemconfigure(self.logo_item, image=self.logo_photo)

      self.canvas.coords(self.logo_item, w / 2, 15 + LOGO_HEIGHT / 2)



    # Кубки по высоте строки ввода

    entry1 = self.current_cups_entry

    entry2 = self.target_cups_entry

    entry_h = entry1.winfo_height()

    if entry_h < 10:

      return

    center_y = entry1.winfo_y() + entry_h / 2

    gap = 8



    if self.trophy_pil is not None:

      ratio = self.trophy_pil.width / self.trophy_pil.height

      size = (max(1, int(entry_h * ratio)), entry_h)

      self.trophy_photo = ImageTk.PhotoImage(

        self.trophy_pil.resize(size, Image.LANCZOS)

      )

      self.canvas.itemconfigure(self.trophy_item, image=self.trophy_photo)

      self.canvas.coords(self.trophy_item, entry1.winfo_x() - gap, center_y)



    if self.trophy2_pil is not None:

      ratio = self.trophy2_pil.width / self.trophy2_pil.height

      size = (max(1, int(entry_h * ratio)), entry_h)

      self.trophy2_photo = ImageTk.PhotoImage(

        self.trophy2_pil.resize(size, Image.LANCZOS)

      )

      self.canvas.itemconfigure(self.trophy2_item, image=self.trophy2_photo)

      self.canvas.coords(

        self.trophy2_item,

        entry2.winfo_x() + entry2.winfo_width() + gap,

        center_y,

      )



  def relayout(self):

    self._resize_job = None

    w = self.winfo_width()

    h = self.winfo_height()

    if w < 50 or h < 50:

      return



    if self.bg_pil is not None:

      self.bg_resized = self.bg_pil.resize((w, h), Image.LANCZOS)

      self.bg_photo = ImageTk.PhotoImage(self.bg_resized)

      self.canvas.itemconfigure(self.bg_item, image=self.bg_photo)



    self.canvas.coords(self.title_item, w / 2, 15 + LOGO_HEIGHT / 2)

    self.canvas.coords(self.subtitle_item, w / 2, 15 + LOGO_HEIGHT + 22)

    self.canvas.coords(self.status_item, w / 2, 15 + LOGO_HEIGHT + 52)

    if self.star_item is not None:

      self.canvas.coords(self.star_item, w - 30, 20)

    self.update_decor(w, h)



    gx = 20

    gy = 15 + LOGO_HEIGHT + 75

    gw = max(w - 40, 50)

    gh = max(h - gy - 160, 50)

    self.grid_w = gw

    self.grid_h = gh

    self.grid_canvas.place(x=gx, y=gy, width=gw, height=gh)



    if self.bg_resized is not None:

      crop = self.bg_resized.crop((gx, gy, gx + gw, gy + gh))

      self.grid_bg_photo = ImageTk.PhotoImage(crop)

      self.grid_canvas.itemconfigure(self.grid_bg_item, image=self.grid_bg_photo)



    self.render_grid()



  # ------------------------------------------------------------------

  # Старт и ADB

  # ------------------------------------------------------------------

  def set_status(self, text, color="#00ff88"):

    self.canvas.itemconfigure(self.status_item, text=text, fill=color)



  def set_error(self, text):

    self.set_status(text, "#ff4500")



  def log_status(self, text):

    """Вызывается из потоков бота: выводит сообщение в статус и в консоль."""

    print(text)

    self.after(0, self.set_status, text, "#ffffff")



  # ------------------------------------------------------------------

  # Бот: создание и запуск действий в фоновых потоках

  # ------------------------------------------------------------------

  def init_bot_async(self):

    self.set_status("Загрузка распознавания текста (EasyOCR)...", "#ffcc00")

    threading.Thread(target=self._init_bot, daemon=True).start()



  def _init_bot(self):

    try:

      self.bot = BrawlBot(log=self.log_status)

    except Exception as error:

      print(f"Ошибка запуска бота: {error}")

      self.after(0, self.set_error, f"Ошибка запуска бота: {error}")

      return

    self.after(0, self.show_ready_status)



  def show_ready_status(self):

    if self.selected_brawler:

      self.set_status(f"Выбран: {self.selected_brawler}")

    else:

      self.set_status("Бот готов. Выберите бойца", "#00ff88")



  def run_bot_task(self, method_name, *args):

    if self.bot is None:

      self.set_error("Бот ещё загружается, подождите...")

      return

    if self.busy:

      self.set_error("Предыдущее действие ещё выполняется...")

      return



    self.busy = True

    task = getattr(self.bot, method_name)



    def worker():

      try:

        task(*args)

      except Exception as error:

        print(f"Ошибка: {error}")

        short = str(error).splitlines()[0][:80]

        self.after(0, self.set_error, f"Ошибка: {short}")

      finally:

        self.busy = False



    threading.Thread(target=worker, daemon=True).start()



  def click_brawlers(self):

    self.run_bot_task("open_brawlers_menu")



  def click_play(self):

    self.run_bot_task("start_game_match")



  def click_start(self):

    if not self.selected_brawler:

      self.set_error("Сначала выберите бойца!")

      return



    current_text = self.current_cups_entry.get().strip()

    target_text = self.target_cups_entry.get().strip()



    # Кубки необязательны, но если заполнены — должны быть числами

    for text in (current_text, target_text):

      if text and not text.isdigit():

        self.set_error("Введите кубки числами!")

        return



    if self.bot is not None:

      self.bot.current_cups = int(current_text) if current_text else None

      self.bot.target_cups = int(target_text) if target_text else None



    print(f"Боец: {self.selected_brawler}")

    self.run_bot_task("select_brawler_and_play", self.selected_brawler)





if __name__ == "__main__":

  ctk.set_appearance_mode("dark")

  app = BrawlBotApp()

  app.mainloop()