diff --git a/development.md b/development.md index f689a08..1193b3f 100644 --- a/development.md +++ b/development.md @@ -14,7 +14,7 @@ python src/run.py ## Building from source If you want to build a .exe from source you will have to first add the cv2 path to your PYTHONPATH:
-Edit System environment variables -> Enfironment Vriables... -> PYTHONPATH -> C:\Users\$USER\miniconda3\envs\botty\lib\site-packages\cv2 +Edit System environment variables -> Enfironment Vriables... -> PYTHONPATH -> C:\Users\\$USER\miniconda3\envs\botty\lib\site-packages\cv2 ```python # building .exe and bundeling all needed resource into one folder python release.py diff --git a/src/bot.py b/src/bot.py index 8cfd0c9..e7b7c7e 100644 --- a/src/bot.py +++ b/src/bot.py @@ -34,9 +34,9 @@ class Bot: self._npc_manager = NpcManager(self._screen, self._template_finder) self._pickit = PickIt(self._screen, self._item_finder, self._ui_manager) if self._config.char["type"] == "sorceress": - self._char: IChar = Sorceress(self._config.sorceress, self._config.char, self._screen, self._template_finder, self._item_finder, self._ui_manager) + self._char: IChar = Sorceress(self._config.sorceress, self._config.char, self._screen, self._template_finder, self._ui_manager) elif self._config.char["type"] == "hammerdin": - self._char: IChar = Hammerdin(self._config.hammerdin, self._config.char, self._screen, self._template_finder, self._item_finder, self._ui_manager) + self._char: IChar = Hammerdin(self._config.hammerdin, self._config.char, self._screen, self._template_finder, self._ui_manager) else: Logger.error(f'{self._config.char["type"]} is not supported! Closing down bot.') os._exit(1) diff --git a/src/char/hammerdin.py b/src/char/hammerdin.py index f42f2a8..97256ac 100644 --- a/src/char/hammerdin.py +++ b/src/char/hammerdin.py @@ -11,12 +11,13 @@ from screen import Screen from utils.misc import wait import random import time +from typing import Tuple class Hammerdin(IChar): - def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager): + def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager): Logger.info("Setting up Hammerdin") - super().__init__(skill_hotkeys, char_config, screen, template_finder, item_finder, ui_manager) + super().__init__(skill_hotkeys, char_config, screen, template_finder, ui_manager) def pre_buff(self): keyboard.send(self._skill_hotkeys["holy_shield"]) @@ -37,7 +38,7 @@ class Hammerdin(IChar): keyboard.send(self._char_config["weapon_switch"]) wait(0.25, 0.3) - def _cast_hammers(self, time_in_s): + def _cast_hammers(self, time_in_s: float): keyboard.send(self._char_config["stand_still"], do_release=False) wait(0.05) keyboard.send(self._skill_hotkeys["blessed_hammer"]) @@ -55,7 +56,7 @@ class Hammerdin(IChar): keyboard.send(self._skill_hotkeys["redemption"]) wait(1.5, 2.0) - def kill_pindle(self, pindle_pos_screen): + def kill_pindle(self, pindle_pos_screen: Tuple[float, float]): pos_monitor = self._screen.convert_screen_to_monitor(pindle_pos_screen) keyboard.send(self._skill_hotkeys["teleport"]) custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) @@ -65,7 +66,7 @@ class Hammerdin(IChar): wait(0.1, 0.15) self._do_redemption() - def kill_shenk(self, shenk_pos_screen): + def kill_shenk(self, shenk_pos_screen: Tuple[float, float]): pos_monitor = self._screen.convert_screen_to_monitor(shenk_pos_screen) keyboard.send(self._skill_hotkeys["teleport"]) wait(0.05) @@ -77,7 +78,7 @@ class Hammerdin(IChar): wait(0.1, 0.15) self._do_redemption() - def kill_eldritch(self, eldritch_pos_screen): + def kill_eldritch(self, eldritch_pos_screen: Tuple[float, float]): pos_monitor = self._screen.convert_screen_to_monitor(eldritch_pos_screen) keyboard.send(self._skill_hotkeys["teleport"]) custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) diff --git a/src/char/i_char.py b/src/char/i_char.py index 8450f1c..b6c4df8 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -12,6 +12,7 @@ import math import keyboard from logger import Logger import time +from typing import Dict, Tuple def abstract(f): @@ -20,11 +21,10 @@ def abstract(f): return _decorator class IChar: - def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager): + def __init__(self, skill_hotkeys: Dict, char_config: Dict, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager): self._skill_hotkeys = skill_hotkeys self._char_config = char_config self._template_finder = template_finder - self._item_finder = item_finder self._ui_manager = ui_manager self._screen = screen @@ -33,14 +33,13 @@ class IChar: success, screen_loc = self._template_finder.search_and_wait(template_type, time_out=10) if success: x_m, y_m = self._screen.convert_screen_to_monitor(screen_loc) - # TODO: check if telekinse can be used - custom_mouse.move(x_m, y_m, duration=(random.random() * 0.1 + 0.2)) - wait(0.1, 0.2) + custom_mouse.move(x_m, y_m, duration=0.3) + wait(0.3, 0.4) mouse.click(button="left") return True return False - def move(self, pos_monitor): + def move(self, pos_monitor: Tuple[float, float]): if not self._ui_manager.is_teleport_selected(): keyboard.send(self._skill_hotkeys["teleport"]) wait(0.1, 0.2) @@ -81,14 +80,11 @@ class IChar: x, y = self._screen.convert_screen_to_monitor(pos) # Note: Template is top of portal, thus move the y-position a bit to the bottom # Also move a bit left and right to get rid of possibly highlight other things such as items - custom_mouse.move(x - 20 * random.random() * 3, y + random.random() * 5, duration=0.1) - custom_mouse.move(x + 20 * random.random() * 3, y + random.random() * 5, duration=0.02) - custom_mouse.move(x + random.random() * 3, y + random.random() * 5, duration=0.02) - wait(0.09, 0.11) + custom_mouse.move(x - 20, y, duration=0.13, randomize=5) + custom_mouse.move(x + 20, y, duration=0.05, randomize=5) + custom_mouse.move(x, y, duration=0.05, randomize=5) + wait(0.1, 0.14) mouse.click(button="left") - # # we sometimes pick up items instead... just to be safe - # wait(0.09, 0.11) - # mouse.click(button="left") return True return False diff --git a/src/char/sorceress.py b/src/char/sorceress.py index 7521b59..21ac08c 100644 --- a/src/char/sorceress.py +++ b/src/char/sorceress.py @@ -10,12 +10,13 @@ from logger import Logger from screen import Screen from utils.misc import wait import random +from typing import Tuple class Sorceress(IChar): - def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, item_finder: ItemFinder, ui_manager: UiManager): + def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager): Logger.info("Setting up Sorceress") - super().__init__(skill_hotkeys, char_config, screen, template_finder, item_finder, ui_manager) + super().__init__(skill_hotkeys, char_config, screen, template_finder, ui_manager) def pre_buff(self): keyboard.send(self._skill_hotkeys["frozen_armor"]) @@ -36,7 +37,7 @@ class Sorceress(IChar): keyboard.send(self._char_config["weapon_switch"]) wait(0.25, 0.3) - def _left_attack(self, cast_pos, delay, spray = 10): + def _left_attack(self, cast_pos: Tuple[float, float], delay: float, spray: int = 10): keyboard.send(self._char_config["stand_still"], do_release=False) custom_mouse.move(cast_pos[0], cast_pos[1], duration=(random.random() * 0.05 + 0.15)) keyboard.send(self._skill_hotkeys["skill_left"]) @@ -48,7 +49,7 @@ class Sorceress(IChar): wait(delay[0], delay[1]) keyboard.send(self._char_config["stand_still"], do_press=False) - def _main_attack(self, cast_pos, delay, spray = 10): + def _main_attack(self, cast_pos: Tuple[float, float], delay: float, spray: float = 10): keyboard.send(self._skill_hotkeys["skill_right"]) x = cast_pos[0] + (random.random() * 2*spray - spray) y = cast_pos[1] + (random.random() * 2*spray - spray) @@ -56,7 +57,7 @@ class Sorceress(IChar): mouse.click(button="right") wait(delay[0], delay[1]) - def kill_pindle(self, pindle_pos_screen): + def kill_pindle(self, pindle_pos_screen: Tuple[float, float]): delay = [0.2, 0.3] pindle_pos_abs = self._screen.convert_screen_to_abs(pindle_pos_screen) cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9] @@ -75,7 +76,7 @@ class Sorceress(IChar): blizzard_cast_pos = self._screen.convert_abs_to_monitor([0, 0]) self._main_attack(blizzard_cast_pos, delay) - def kill_shenk(self, shenk_pos_screen): + def kill_shenk(self, shenk_pos_screen: Tuple[float, float]): delay = [0.2, 0.3] pos_abs = self._screen.convert_screen_to_abs(shenk_pos_screen) cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9] @@ -95,7 +96,7 @@ class Sorceress(IChar): custom_mouse.move(pos_monitor[0], pos_monitor[1], duration=(random.random() * 0.05 + 0.15)) mouse.click(button="right") - def kill_eldritch(self, eldritch_pos_screen): + def kill_eldritch(self, eldritch_pos_screen: Tuple[float, float]): delay = [0.2, 0.3] pos_abs = self._screen.convert_screen_to_abs(eldritch_pos_screen) cast_pos_abs = [pos_abs[0] * 0.9, pos_abs[1] * 0.9] @@ -126,6 +127,6 @@ if __name__ == "__main__": t_finder = TemplateFinder(screen) pather = Pather(screen, t_finder) ui_manager = UiManager(screen, t_finder) - char = Sorceress(config.sorceress, config.char, screen, t_finder, None, ui_manager) + char = Sorceress(config.sorceress, config.char, screen, t_finder, ui_manager) # char.pre_buff() char.tp_town() diff --git a/src/config.py b/src/config.py index 70dcb3b..3745a56 100644 --- a/src/config.py +++ b/src/config.py @@ -71,17 +71,16 @@ class Config: self.colors = {} for key in self._ui_config["colors"]: - self.colors[key] = np.split(np.array([int(x) for x in self._ui_config["colors"][key].split(",")]), 2) - if "colors" in self._custom: - custom_colors = {} - for key in self._custom["colors"]: - custom_colors[key] = np.split(np.array([int(x) for x in self._custom["colors"][key].split(",")]), 2) - self.colors.update(custom_colors) + self.colors[key] = np.split(np.array([int(x) for x in self._select_val("colors", key).split(",")]), 2) self.ui_pos = {} for key in self._ui_config["ui_pos_1920_1080"]: self.ui_pos[key] = int(self._select_val("ui_pos_1920_1080", key)) + self.ui_roi = {} + for key in self._ui_config["ui_roi_1920_1080"]: + self.ui_roi[key] = np.array([int(x) for x in self._select_val("ui_roi_1920_1080", key).split(",")]) + if __name__ == "__main__": config = Config() diff --git a/src/death_manager.py b/src/death_manager.py index 78742fc..42a0e6f 100644 --- a/src/death_manager.py +++ b/src/death_manager.py @@ -9,6 +9,7 @@ import cv2 from logger import Logger import time import random +from threading import Thread class DeathManager: @@ -17,7 +18,6 @@ class DeathManager: self._screen = screen self._template_finder = template_finder _, self._you_have_died_filtered = color_filter(cv2.imread("assets/templates/you_have_died.png"), self._config.colors["red"]) - self._search_roi = [self._config.ui_pos["death_roi_left"], self._config.ui_pos["death_roi_top"], self._config.ui_pos["death_roi_width"], self._config.ui_pos["death_roi_height"]] self._died = False self._do_monitor = False @@ -34,11 +34,11 @@ class DeathManager: mouse.click(button="left") self._died = False - def start_monitor(self, run_thread): + def start_monitor(self, run_thread: Thread): self._do_monitor = True while self._do_monitor: time.sleep(1.0) # no need to do this too frequent, when we died we are not in a hurry... - roi_img = cut_roi(self._screen.grab(), self._search_roi) + roi_img = cut_roi(self._screen.grab(), self._config.ui_roi["death"]) _, filtered_roi_img = color_filter(roi_img, self._config.colors["red"]) res = cv2.matchTemplate(filtered_roi_img, self._you_have_died_filtered, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ = cv2.minMaxLoc(res) diff --git a/src/health_manager.py b/src/health_manager.py index ac1b3cb..2dc78da 100644 --- a/src/health_manager.py +++ b/src/health_manager.py @@ -9,6 +9,7 @@ from screen import Screen import numpy as np import time from config import Config +from threading import Thread class HealthManager: @@ -48,7 +49,7 @@ class HealthManager: keyboard.send(self._config.char[key]) break - def get_health(self, img): + def get_health(self, img: np.ndarray) -> float: health_rec = [self._config.ui_pos["health_left"], self._config.ui_pos["health_top"], self._config.ui_pos["health_width"], self._config.ui_pos["health_height"]] health_img = cut_roi(img, health_rec) # red mask @@ -61,14 +62,14 @@ class HealthManager: health_percentage_green = (float(np.sum(mask)) / mask.size) * (1/255.0) return max(health_percentage, health_percentage_green) - def get_mana(self, img): + def get_mana(self, img: np.ndarray) -> float: mana_rec = [self._config.ui_pos["mana_left"], self._config.ui_pos["mana_top"], self._config.ui_pos["mana_width"], self._config.ui_pos["mana_height"]] mana_img = cut_roi(img, mana_rec) mask, _ = color_filter(mana_img, [np.array([117, 120, 20]), np.array([121, 255, 255])]) mana_percentage = (float(np.sum(mask)) / mask.size) * (1/255.0) return mana_percentage - def get_merc_health(self, img): + def get_merc_health(self, img: np.ndarray) -> float: health_rec = [self._config.ui_pos["merc_health_left"], self._config.ui_pos["merc_health_top"], self._config.ui_pos["merc_health_width"], self._config.ui_pos["merc_health_height"]] merc_health_img = cut_roi(img, health_rec) merc_health_img = cv2.cvtColor(merc_health_img, cv2.COLOR_BGR2GRAY) @@ -76,15 +77,15 @@ class HealthManager: merc_health_percentage = (float(np.sum(health_tresh)) / health_tresh.size) * (1/255.0) return merc_health_percentage - def start_monitor(self, run_thread): + def start_monitor(self, run_thread: Thread): Logger.debug("Start health monitoring") self._do_monitor = True self._did_chicken = False + start = time.time() while self._do_monitor: time.sleep(0.1) img = self._screen.grab() - roi = [700, 650, 460, 250] - is_loading_black_roi = np.average(img[:, 0:500]) < 1.0 + is_loading_black_roi = np.average(img[:, 0:self._config.ui_roi["loading_left_black"][2]]) < 1.0 if not is_loading_black_roi: # check health health_percentage = self.get_health(img) @@ -92,7 +93,8 @@ class HealthManager: if health_percentage < self._config.char["take_health_potion"] and last_drink > 2.5: self._drink_poition(img, "health") self._last_health = time.time() - elif health_percentage < self._config.char["chicken"]: + # give the chicken a 4 sec delay to give time for a healing pot and avoid endless loop of chicken + elif health_percentage < self._config.char["chicken"] and (time.time() - start) > 4: Logger.warning("Trying to chicken!") cv2.imwrite("info_debug_chicken.png", img) self._ui_manager.save_and_exit() @@ -107,8 +109,7 @@ class HealthManager: self._drink_poition(img, "mana") self._last_mana = time.time() # check merc - roi = [0, 0, 150, 150] - merc_alive, _ = self._template_finder.search("MERC", img, roi=roi) + merc_alive, _ = self._template_finder.search("MERC", img, roi=self._config.ui_roi["merc_icon"]) if merc_alive: merc_health_percentage = self.get_merc_health(img) last_drink = time.time() - self._last_merc_healh diff --git a/src/npc_manager.py b/src/npc_manager.py index 80f4a81..a0011fc 100644 --- a/src/npc_manager.py +++ b/src/npc_manager.py @@ -39,22 +39,18 @@ class NpcManager: } def open_npc_menu(self, npc_key: Npc) -> bool: - # TODO: 1920x1080 specific. Cut off bottom skill bar - roi = [0, 0, 1920, 1000] + roi = self._config.ui_roi["cut_skill_bar"] start = time.time() - while (time.time() - start) < 40: + while (time.time() - start) < 35: img = self._screen.grab() for key in self._npcs[npc_key]["template_group"]: - # TODO: 1920x1080 specific params - res, pos = self._template_finder.search(key, img, threshold=0.35, roi=[0, 0, 1920, 1000]) + res, pos = self._template_finder.search(key, img, threshold=0.35, roi=roi) if res: x_m, y_m = self._screen.convert_screen_to_monitor(pos) custom_mouse.move(x_m, y_m, duration=0.05) time.sleep(0.2) _, filtered_inp_w = color_filter(self._screen.grab(), self._config.colors["white"]) _, filtered_inp_g = color_filter(self._screen.grab(), self._config.colors["gold"]) - # TODO: 1920x1080 specific params - # roi = [max(0, pos[0] - 200), max(0, pos[1] - 250), 400, 250] res_w, _ = self._template_finder.search(self._npcs[npc_key]["name_tag_white"], filtered_inp_w, 0.92, roi=roi) res_g, _ = self._template_finder.search(self._npcs[npc_key]["name_tag_gold"], filtered_inp_g, 0.92, roi=roi) if res_w: @@ -69,11 +65,8 @@ class NpcManager: return False def press_npc_btn(self, npc_key: Npc, action_btn_key: str): - # click resurrect btn _, filtered_inp = color_filter(self._screen.grab(), self._config.colors["white"]) - # TODO: 1920x1080 specific params - roi = [300, 0, 1400, 500] - res, pos = self._template_finder.search(self._npcs[npc_key]["action_btns"][action_btn_key], filtered_inp, 0.92, roi=roi) + res, pos = self._template_finder.search(self._npcs[npc_key]["action_btns"][action_btn_key], filtered_inp, 0.92, roi=self._config.ui_roi["cut_skill_bar"]) if res: x_m, y_m = self._screen.convert_screen_to_monitor(pos) custom_mouse.move(x_m, y_m, duration=0.1) @@ -85,7 +78,7 @@ class NpcManager: keyboard.send("esc") -# Testing: Stand close to Qual-Kehk and run +# Testing: Stand close to Qual-Kehk or Malah and run if __name__ == "__main__": from screen import Screen from config import Config diff --git a/src/pather.py b/src/pather.py index 709c4ca..11df575 100644 --- a/src/pather.py +++ b/src/pather.py @@ -7,10 +7,11 @@ import keyboard import time import os import random -from typing import Tuple +from typing import Tuple, List import cv2 from config import Config -from utils.misc import wait +from utils.misc import wait, is_in_roi +import numpy as np class Location: @@ -23,22 +24,24 @@ class Location: class Pather: + """ + Traverses 'dynamic' pathes with reference templates and relative coordinates or statically recorded pathes. + Check utils/node_recorder.py to generate templates/refpoints and relative coordinates to nodes. Once you have refpoints and + nodes you can specify in which order this nodes should be traversed in self._paths. + """ + def __init__(self, screen: Screen, template_finder: TemplateFinder): self._config = Config() self._screen = screen self._template_finder = template_finder # TODO: params based on 1920x1080 (in rel coordinates to ref point) - self._range_x = [-950, 950] - self._range_y = [-530, 440] - # health/mana globe coordinates: - self._hg_rect = [245, 245 + 320] # s-left, x-right - self._mg_rect = [1350, 1350 + 320] # x-left, x-right - self._globe_top_abs_pos = 345 + self._range_x = [-self._config.ui_pos["center_x"] + 10, self._config.ui_pos["center_x"] - 10] + self._range_y = [-self._config.ui_pos["center_y"] + 10, self._config.ui_pos["center_y"] - self._config.ui_pos["skill_bar_height"] - 10] self._nodes = { # A5 town 0: {"A5_TOWN_0": (110-70, 373), "A5_TOWN_1": (-68-70, -205)}, 1: {"A5_TOWN_0": (-466, 287), "A5_TOWN_1": (-644, -291), "A5_TOWN_0.5": (717, 349)}, - 2: {"A5_TOWN_0": (-552, 42), "A5_TOWN_0.5": (659-25, 90+20)}, + 2: {"A5_TOWN_0": (-552, 42-100), "A5_TOWN_0.5": (659-25, 90+20-100)}, 3: {"A5_TOWN_1": (-414, 141), "A5_TOWN_2": (728, -90)}, 4: {"A5_TOWN_1": (-701, 400), "A5_TOWN_2": (440, 169), "A5_TOWN_3": (-400, -208), "A5_TOWN_4": (243, -244)}, 5: {"A5_TOWN_2": (555-100, 429-100), "A5_TOWN_3": (-285-100, 51-100), "A5_TOWN_4": (358-100, 15-100)}, @@ -75,7 +78,7 @@ class Pather: def get_fixed_path(self, key: str): return self._fixed_tele_path[key] - def _draw_debug(self, img, node_pos_abs_list, ref_pos_abs_list): + def _draw_debug(self, img: np.ndarray, node_pos_abs_list: List, ref_pos_abs_list: List): for node_pos_abs in node_pos_abs_list: pos_screen = self._screen.convert_abs_to_screen(node_pos_abs) cv2.circle(img, pos_screen, 10, (255, 0, 0), 10) @@ -88,7 +91,7 @@ class Pather: cv2.imshow("x", img) cv2.waitKey(1) - def _display_all_nodes(self): + def _display_all_nodes_debug(self): while 1: img = self._screen.grab() for node_idx in self._nodes: @@ -110,8 +113,8 @@ class Pather: cv2.imshow("debug", img) cv2.waitKey(1) - @staticmethod - def _convert_rel_to_abs(rel_loc, pos_abs): + @staticmethod + def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]: return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1]) def traverse_nodes_fixed(self, key: str, char: IChar): @@ -123,25 +126,42 @@ class Pather: char.move((x_m, y_m)) def _adjust_abs_range_to_screen(self, abs_pos: Tuple[float, float]) -> Tuple[float, float]: + """ + Adjust an absolute coordinate so it will not go out of screen or click on any ui which will not move the char + :param abs_pos: Absolute position of the desired position to move to + :return: Absolute position of a valid position that can be clicked on + """ f = 1.0 + # Check for x-range if abs_pos[0] > self._range_x[1]: f = min(f, abs(self._range_x[1] / float(abs_pos[0]))) elif abs_pos[0] < self._range_x[0]: f = min(f, abs(self._range_x[0] / float(abs_pos[0]))) + # Check top y-range if abs_pos[1] < self._range_y[0]: f = min(f, abs(self._range_y[0] / float(abs_pos[1]))) - # also accout for globes + # check bottom y-range + globe roi which will also not allow a movement range_y_bottom = self._range_y[1] screen_pos = self._screen.convert_abs_to_screen(abs_pos) - if self._hg_rect[0] < screen_pos[0] < self._hg_rect[1] or self._mg_rect[0] < screen_pos[0] < self._mg_rect[1]: - range_y_bottom = self._globe_top_abs_pos + if is_in_roi(self._config.ui_roi["mana_globe"], screen_pos) or is_in_roi(self._config.ui_roi["health_globe"], screen_pos): + # convert any of health or mana roi top coordinate to abs (x-coordinate is just a dummy 0 value) + range_y_bottom = self._screen.convert_screen_to_abs((0, self._config.ui_roi["mana_globe"][1]))[1] if abs_pos[1] > range_y_bottom: - f = min(f, abs(self._range_y[1] / float(abs_pos[1]))) + f = min(f, abs(range_y_bottom / float(abs_pos[1]))) + # Scale the position by the factor f if f < 1.0: abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f)) return abs_pos def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, debug: bool = False) -> bool: + """ + Traverse from one location to another + :param start_location: Location the char is starting at + :param end_location: Location the char is supposed to end up + :param char: Char that is traversing the nodes + :param debug: Debug mode will display some images and stuff + :return: Bool if traversed succesfull or False if it got stuck + """ Logger.debug(f"Traverse from {start_location} to {end_location}") path = self._paths[(start_location, end_location)] for i, node_idx in enumerate(path): @@ -158,7 +178,7 @@ class Pather: last_move = time.time() else: cv2.imwrite("info_pather_got_stuck.png", img) - Logger.error("Got stuck exit") + Logger.error("Got stuck exit pather") return False _debug_node_pos_abs_list = [] _debug_ref_pos_abs_list = [] @@ -177,8 +197,7 @@ class Pather: self._draw_debug(img, _debug_node_pos_abs_list, _debug_ref_pos_abs_list) node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs) dist = math.dist(node_pos_abs, (0, 0)) - # TODO: param based on 1920x1080 - if dist < 150: + if dist < self._config.ui_pos["reached_node_dist"]: continue_to_next_node = True else: # Move the char @@ -204,7 +223,7 @@ if __name__ == "__main__": t_finder = TemplateFinder(screen) pather = Pather(screen, t_finder) ui_manager = UiManager(screen, t_finder) - char = Sorceress(config.sorceress, config.char, screen, t_finder, None, ui_manager) + char = Sorceress(config.sorceress, config.char, screen, t_finder, ui_manager) # pather.traverse_nodes_fixed("PINDLE", char) - pather.traverse_nodes(Location.A5_TOWN_START, Location.NIHLATHAK_PORTAL, char, debug=True) + pather.traverse_nodes(Location.A5_TOWN_START, Location.MALAH, char, debug=True) # pather._display_all_nodes() diff --git a/src/pickit.py b/src/pickit.py index e40dac5..deab728 100644 --- a/src/pickit.py +++ b/src/pickit.py @@ -19,6 +19,11 @@ class PickIt: self._config = Config() def pick_up_items(self, char: IChar) -> bool: + """ + Pick up all items with specified char + :param char: The character used to pick up the item + :return: Bool if any items were picked up or not. (Does not account for picking up scrolls and pots) + """ found_items = False keyboard.send(self._config.char["show_items"], do_press=True, do_release=False) time.sleep(1.0) # sleep needed here to give d2r time to display items on screen on keypress @@ -41,16 +46,15 @@ class PickIt: if closest_item.dist > item.dist: closest_item = item x_m, y_m = self._screen.convert_screen_to_monitor(closest_item.center) - # TODO: 1920x1080 specific param - if closest_item.dist < 400: + if closest_item.dist < self._config.ui_pos["item_dist"]: # no need to stash poitions and scrolls if "potion" not in closest_item.name and "tp_scroll" != closest_item.name: found_items = True Logger.info(f"Picking up {closest_item.name}") - custom_mouse.move(x_m, y_m, duration=(random.random() * 0.01 + 0.02)) + custom_mouse.move(x_m, y_m, duration=(random.random() * 0.03 + 0.08)) time.sleep(0.1) mouse.click(button="left") - time.sleep(0.6) + time.sleep(0.5) if self._ui_manager.is_overburdened(): Logger.warning("Inventory full, skipping pickit!") # TODO: should go back to town and stash stuff then go back to picking up more stuff diff --git a/src/screen.py b/src/screen.py index 84121df..ca55a8c 100644 --- a/src/screen.py +++ b/src/screen.py @@ -5,10 +5,13 @@ import cv2 import time from logger import Logger from typing import Tuple +from config import Config import os class Screen: + """Grabs images from screen and converts differnt coordinate systems to each other""" + def __init__(self, monitor: int = 0): self._sct = mss() monitor_idx = monitor + 1 # sct saves the whole screen (including both monitors if available at index 0, then monitor 1 at 1 and 2 at 2) @@ -18,7 +21,12 @@ class Screen: if monitor_idx >= len(self._sct.monitors): Logger.warning("Monitor index not available! Choose a smaller number for 'monitor' in the param.ini. Forcing value to 0 for now.") monitor_idx = 1 + config = Config() self._monitor_roi = self._sct.monitors[monitor_idx] + # For windowed screens it is expected to always have them at the top left edge and adjust offset_top then + self._monitor_roi["top"] += config.ui_pos["offset_top"] + self._monitor_roi["width"] = config.ui_pos["screen_width"] + self._monitor_roi["height"] = config.ui_pos["screen_height"] def convert_monitor_to_screen(self, screen_coord: Tuple[float, float]) -> Tuple[float, float]: return (screen_coord[0] - self._monitor_roi["left"], screen_coord[1] - self._monitor_roi["top"]) diff --git a/src/template_finder.py b/src/template_finder.py index d2d4bb0..a2e62c7 100644 --- a/src/template_finder.py +++ b/src/template_finder.py @@ -14,6 +14,10 @@ def load_template(path, scale_factor): class TemplateFinder: def __init__(self, screen: Screen, scale_factor: float = 0.5): + """ + :param screen: Screen object + :param scale_factor: Scale factor that is used for templates. Note: UI and NPC templates will always have scale of 1.0 + """ self.debug_last_score = -1.0 self._screen = screen self._scale_factor = scale_factor @@ -79,6 +83,14 @@ class TemplateFinder: return self._templates[key][0] def search(self, ref: Union[str, np.ndarray], inp_img: np.ndarray, threshold: float = 0.7, roi: List[float] = None) -> Tuple[bool, Tuple[float, float]]: + """ + Search for a template in an image + :param ref: Either key of a already loaded template or a image which is used as template + :param inp_img: Image in which the template will be searched + :param threshold: Threshold which determines if a template is found or not + :param roi: Region of Interest of the inp_img to restrict search area. Format [left, top, width, height] + :return: Returns found flag and the position as [bool, [x, y]]. If not found, position will be None. Position in image space. + """ if roi is None: # if no roi is provided roi = full inp_img roi = [0, 0, inp_img.shape[1], inp_img.shape[0]] @@ -109,6 +121,12 @@ class TemplateFinder: return False, None def search_and_wait(self, ref: str, roi: List[float] = None, time_out: float = None, threshold: float = 0.7) -> Tuple[bool, Tuple[float, float]]: + """ + Helper function that will loop and keep searching for a template + :param ref: Key of template which has been loaded beforehand + :param time_out: After this amount of time the search will stop and it will return [False, None] + Rest of params same as TemplateFinder.search() + """ Logger.debug(f"Waiting for Template {ref}") start = time.time() while 1: diff --git a/src/ui_manager.py b/src/ui_manager.py index 60d99e7..3d43793 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -1,8 +1,9 @@ from screen import Screen from template_finder import TemplateFinder import mouse -from utils import custom_mouse, custom_keyboard -import keyboard # currently needed for a workaround +from typing import Tuple +from utils import custom_mouse +import keyboard import random import time import cv2 @@ -16,36 +17,50 @@ from utils.misc import color_filter class UiManager(): + """Everything that is clicking on some static 2D UI or is checking anything in regard to it should be placed here.""" + def __init__(self, screen: Screen, template_finder: TemplateFinder): self._config = Config() self._template_finder = template_finder self._screen = screen self._curr_stash = 0 # 0: personal, 1: shared1, 2: shared2, 3: shared3 - def use_wp(self, act, idx): - pos_act_btn = (self._config.ui_pos["wp_act_i_btn_x"] + self._config.ui_pos["wp_act_btn_width"] * act, self._config.ui_pos["wp_act_i_btn_y"]) - x, y = self._screen.convert_screen_to_monitor(pos_act_btn) - custom_mouse.move(x, y, duration=0.4, randomize=5) - mouse.click(button="left") - wait(0.35, 0.4) + def use_wp(self, act: int, idx: int): + """ + Use Waypoint. The menu must be opened when calling the function. + :param act: Index of the act from left. Note that it start at 0. e.g. Act5 -> act=4 + :param idx: Index of the waypoint from top. Note that it start at 0. + """ + # Note: We are currently only in act 5, thus no need to click here. + # pos_act_btn = (self._config.ui_pos["wp_act_i_btn_x"] + self._config.ui_pos["wp_act_btn_width"] * act, self._config.ui_pos["wp_act_i_btn_y"]) + # x, y = self._screen.convert_screen_to_monitor(pos_act_btn) + # custom_mouse.move(x, y, duration=0.4, randomize=8) + # mouse.click(button="left") + # wait(0.3, 0.4) pos_wp_btn = (self._config.ui_pos["wp_first_btn_x"], self._config.ui_pos["wp_first_btn_y"] + self._config.ui_pos["wp_btn_height"] * idx) x, y = self._screen.convert_screen_to_monitor(pos_wp_btn) - custom_mouse.move(x, y, duration=0.4, randomize=5) + custom_mouse.move(x, y, duration=0.4, randomize=12) + wait(0.3, 0.4) mouse.click(button="left") - def can_teleport(self): - img = self._screen.grab() + def can_teleport(self) -> bool: + """ + :return: Bool if teleport is red/available or not. Teleport skill must be selected on right skill slot when calling the function. + """ roi = [ self._config.ui_pos["skill_right_x"] - (self._config.ui_pos["skill_width"] // 2), self._config.ui_pos["skill_y"] - (self._config.ui_pos["skill_height"] // 2), self._config.ui_pos["skill_width"], self._config.ui_pos["skill_height"] ] - img = cut_roi(img, roi) + img = cut_roi(self._screen.grab(), roi) avg = np.average(img) return avg > 75.0 - def is_teleport_selected(self): + def is_teleport_selected(self) -> bool: + """ + :return: Bool if teleport is currently the selected skill on the right skill slot. + """ roi = [ self._config.ui_pos["skill_right_x"] - (self._config.ui_pos["skill_width"] // 2), self._config.ui_pos["skill_y"] - (self._config.ui_pos["skill_height"] // 2), @@ -58,11 +73,11 @@ class UiManager(): return True return False - def is_overburdened(self): - #TODO: 1920x1080 specific roi - roi = [17, 765, 470, 90] - img = self._screen.grab() - img = cut_roi(img, roi) + def is_overburdened(self) -> bool: + """ + :return: Bool if the last pick up overburdened your char. Must be called right after picking up an item. + """ + img = cut_roi(self._screen.grab(), self._config.ui_roi["is_overburdened"]) _, filtered_img = color_filter(img, self._config.colors["gold"]) templates = [cv2.imread("assets/templates/inventory_full_msg_0.png"), cv2.imread("assets/templates/inventory_full_msg_1.png")] for template in templates: @@ -74,7 +89,12 @@ class UiManager(): return False @staticmethod - def potion_type(img): + def potion_type(img: np.ndarray) -> str: + """ + Based on cut out image from belt, determines what type of potion it is. TODO: Add rejuv support + :param img: Cut out image of a belt slot + :return: Any of ["empty", "health", "mana"] + """ avg_brightness = np.average(img) if avg_brightness < 47: return "empty" @@ -89,7 +109,10 @@ class UiManager(): return "empty" def check_free_belt_spots(self) -> bool: - # currently only checks if a whole row is free + """ + Check if any column in the belt is free (only checking the bottom row) + :return: Bool if any column in belt is free + """ img = self._screen.grab() for i in range(4): roi = [ @@ -104,111 +127,146 @@ class UiManager(): return True return False - def save_and_exit(self): + def save_and_exit(self) -> bool: + """ + Performes save and exit action from within game + :return: Bool if action was successful + """ start = time.time() - while (time.time() - start) < 10: + while (time.time() - start) < 15: keyboard.send("esc") - wait(0.05) + wait(0.1) exit_btn_pos = (self._config.ui_pos["save_and_exit_x"], self._config.ui_pos["save_and_exit_y"]) - found, _ = self._template_finder.search_and_wait("SAVE_AND_EXIT", roi=[750, 350, 410, 260], time_out=2) + found, _ = self._template_finder.search_and_wait("SAVE_AND_EXIT", roi=self._config.ui_roi["save_and_exit"], time_out=3) if found: x_m, y_m = self._screen.convert_screen_to_monitor(exit_btn_pos) - custom_mouse.move(x_m, y_m, duration=random.random()*0.05 + 0.15, randomize=10) + custom_mouse.move(x_m, y_m, duration=0.2, randomize=12) + wait(0.1) mouse.click(button="left") - break + return True + return False - - def start_hell_game(self): - # expects to be in hero selection screen + def start_hell_game(self) -> bool: + """ + Starting a game in hell mode. Will wait and retry on server connection issue. + :return: Bool if action was successful + """ Logger.debug(f"Searching for Play Btn...") - # TODO: roi is with respect to 1920x1080 - roi = [390, 760, 1150, 300] - while 1: - _, pos = self._template_finder.search_and_wait("PLAY_BTN", roi=roi) - x_range_offline = [self._config.ui_pos["play_x_offline"] - 50, self._config.ui_pos["play_x_offline"] + 50] - x_range_online = [self._config.ui_pos["play_x_online"] - 50, self._config.ui_pos["play_x_online"] + 50] - y_range = [self._config.ui_pos["play_y"] - 50, self._config.ui_pos["play_y"] + 50] - in_offline_range = x_range_offline[0] < pos[0] < x_range_offline[1] - in_online_range = x_range_online[0] < pos[0] < x_range_online[1] - mode_info = "online mode" if in_online_range else "offline mode" - if (in_offline_range or in_online_range) and y_range[0] < pos[1] < y_range[1]: - pos = [pos[0], self._config.ui_pos["play_y"]] - x, y = self._screen.convert_screen_to_monitor(pos) - Logger.debug(f"Found Play Btn ({mode_info}) -> clicking it") - if mode_info == "online mode": - Logger.warning("You are currently creating a game in online mode!") - custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5) - mouse.click(button="left") - break + found, pos = self._template_finder.search_and_wait("PLAY_BTN", roi=self._config.ui_roi["play_btn"], time_out=8) + if not found: + return False + # sanity x, y check and determine if offline or online + x_range_offline = [self._config.ui_pos["play_x_offline"] - 50, self._config.ui_pos["play_x_offline"] + 50] + x_range_online = [self._config.ui_pos["play_x_online"] - 50, self._config.ui_pos["play_x_online"] + 50] + y_range = [self._config.ui_pos["play_y"] - 50, self._config.ui_pos["play_y"] + 50] + in_offline_range = x_range_offline[0] < pos[0] < x_range_offline[1] + in_online_range = x_range_online[0] < pos[0] < x_range_online[1] + mode_info = "online mode" if in_online_range else "offline mode" + if (in_offline_range or in_online_range) and y_range[0] < pos[1] < y_range[1]: + pos = [pos[0], self._config.ui_pos["play_y"]] + x, y = self._screen.convert_screen_to_monitor(pos) + Logger.debug(f"Found Play Btn ({mode_info}) -> clicking it") + if mode_info == "online mode": + Logger.warning("You are creating a game in online mode!") + custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5) + mouse.click(button="left") + else: + Logger.debug("Sanity position check on play btn failed") + return False + Logger.debug("Searching for Hell Btn...") - # TODO: roi is with respect to 1920x1080 - roi = [550, 100, 1000, 800] - while 1: - _, pos = self._template_finder.search_and_wait("HELL_BTN", roi=roi) - # sanity x y check based on 1920x1080 - # note: not checking y range as it often detects nightmare button as hell btn, not sure why - x_range = [self._config.ui_pos["hell_x"] - 50, self._config.ui_pos["hell_x"] + 50] - if x_range[0] < pos[0] < x_range[1]: - x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["hell_x"], self._config.ui_pos["hell_y"])) - Logger.debug("Found Hell Btn -> clicking it") - custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5) - mouse.click(button="left") - break + found, pos = self._template_finder.search_and_wait("HELL_BTN", roi=self._config.ui_roi["hell_btn"], time_out=8) + if not found: + return False + # sanity x y check. Note: not checking y range as it often detects nightmare button as hell btn, not sure why + x_range = [self._config.ui_pos["hell_x"] - 50, self._config.ui_pos["hell_x"] + 50] + if x_range[0] < pos[0] < x_range[1]: + x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["hell_x"], self._config.ui_pos["hell_y"])) + Logger.debug("Found Hell Btn -> clicking it") + custom_mouse.move(x, y, duration=(random.random() * 0.2 + 0.5), randomize=5) + mouse.click(button="left") + else: + Logger.debug("Sanity position check on hell btn failed") + return False + # check for server issue - time.sleep(1.0) + wait(2.0) server_issue, _ = self._template_finder.search("SERVER_ISSUES", self._screen.grab()) if server_issue: - Logger.warning("Server issue. waiting 20s") + Logger.warning("Server connection issue. waiting 20s") x, y = self._screen.convert_screen_to_monitor((self._config.ui_pos["issue_occured_ok_x"], self._config.ui_pos["issue_occured_ok_y"])) custom_mouse.move(x, y, duration=(random.random() * 0.4 + 0.5), randomize=5) mouse.click(button="left") wait(1, 2) keyboard.send("esc") - wait(16, 18) - self.start_hell_game() + wait(18, 22) + return self.start_hell_game() + else: + return True @staticmethod def _slot_has_item(slot_img: np.ndarray) -> bool: + """ + Check if a specific slot in the inventory has an item or not based on color + :param slot_img: Image of the slot + :return: Bool if there is an item or not + """ slot_img = cv2.cvtColor(slot_img, cv2.COLOR_BGR2HSV) avg_brightness = np.average(slot_img[:, :, 2]) - # TODO: magic param, move to param file (Could go as low as 11.0, but better be save, otherwise bot will stop cause "stash is full" if fails) return avg_brightness > 16.0 - def _get_slot_pos_and_img(self, img: np.ndarray, column: int, row: int): + def _get_slot_pos_and_img(self, img: np.ndarray, column: int, row: int) -> Tuple[Tuple[int, int], np.ndarray]: + """ + Get the pos and img of a specific slot position in Inventory. Inventory must be open in the image. + :param img: Image from screen.grab() not cut + :param column: Column in the Inventory + :param row: Row in the Inventory + :return: Returns position and image of the cut area as such: [[x, y], img] + """ top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"]) slot_width = self._config.ui_pos["slot_width"] slot_height= self._config.ui_pos["slot_height"] slot = (top_left_slot[0] + slot_width * column, top_left_slot[1] + slot_height * row) - min_x = slot[0] + 7 - max_x = slot[0] + slot_width - 7 - min_y = slot[1] + 7 - max_y = slot[1] + slot_height - 7 + # decrease size to make sure not to have any borders of the slot in the image + offset_w = int(slot_width * 0.12) + offset_h = int(slot_height * 0.12) + min_x = slot[0] + offset_w + max_x = slot[0] + slot_width - offset_w + min_y = slot[1] + offset_h + max_y = slot[1] + slot_height - offset_h slot_img = img[min_y:max_y, min_x:max_x] center_pos = (int(slot[0] + (slot_width // 2)), int(slot[1] + (slot_height // 2))) return center_pos, slot_img - def _inventory_has_items(self, img, num_loot_columns) -> bool: + def _inventory_has_items(self, img, num_loot_columns: int) -> bool: + """ + Check if Inventory has any items + :param img: Img from screen.grab() with inventory open + :param num_loot_columns: Number of columns to check from left + :return: Bool if inventory still has items or not + """ for column, row in itertools.product(range(num_loot_columns), range(4)): _, slot_img = self._get_slot_pos_and_img(img, column, row) if self._slot_has_item(slot_img): return True return False - def stash_all_items(self, num_loot_columns): + def stash_all_items(self, num_loot_columns: int): + """ + Stashing all items in inventory. Stash UI must be open when calling the function. + :param num_loot_columns: Number of columns used for loot from left + """ # TODO: Do not stash portal scrolls and potions but throw them out of inventory on the ground! # then the pickit check for potions and belt free can also be removed Logger.debug("Searching for inventory gold btn...") - #TODO: 1920x1080 specific params - gold_btn_pos = [self._config.ui_pos["inventory_gold_btn_x"], self._config.ui_pos["inventory_gold_btn_y"]] - inventory_roi = [gold_btn_pos[0] - 120, gold_btn_pos[1] - 60, 400, 150] - self._template_finder.search_and_wait("INVENTORY_GOLD_BTN", roi=inventory_roi) + self._template_finder.search_and_wait("INVENTORY_GOLD_BTN", roi=self._config.ui_roi["gold_btn"]) Logger.debug("Found inventory gold btn") # select the start stash personal_stash_pos = (self._config.ui_pos["stash_personal_btn_x"], self._config.ui_pos["stash_personal_btn_y"]) stash_btn_width = self._config.ui_pos["stash_btn_width"] next_stash_pos = (personal_stash_pos[0] + stash_btn_width * self._curr_stash, personal_stash_pos[1]) x_m, y_m = self._screen.convert_screen_to_monitor(next_stash_pos) - custom_mouse.move(x_m, y_m, duration=(random.random() * 0.2 + 0.6), randomize=15) + custom_mouse.move(x_m, y_m, duration=0.7, randomize=15) mouse.click(button="left") wait(0.3, 0.4) # stash stuff @@ -218,7 +276,7 @@ class UiManager(): slot_pos, slot_img = self._get_slot_pos_and_img(img, column, row) if self._slot_has_item(slot_img): x_m, y_m = self._screen.convert_screen_to_monitor(slot_pos) - custom_mouse.move(x_m, y_m, duration=(random.random() * 0.2 + 0.3), randomize=6) + custom_mouse.move(x_m, y_m, duration=(random.random() * 0.2 + 0.3), randomize=5) wait(0.1, 0.15) mouse.click(button="left") wait(0.4, 0.6) @@ -244,11 +302,14 @@ class UiManager(): return self.stash_all_items(num_loot_columns) Logger.debug("Done stashing") - wait(0.5, 0.6) + wait(0.4, 0.5) keyboard.send("esc") - def fill_up_belt_from_inventory(self, num_loot_columns): - # Find all pots in the inventory + def fill_up_belt_from_inventory(self, num_loot_columns: int): + """ + Fill up your belt with pots from the inventory e.g. after death. It will open and close invetory by itself! + :param num_loot_columns: Number of columns used for loot from left + """ keyboard.send(self._config.char["inventory_screen"]) wait(0.7, 1.0) img = self._screen.grab() @@ -262,8 +323,8 @@ class UiManager(): keyboard.press("shift") for pos in pot_positions: x, y = self._screen.convert_screen_to_monitor(pos) - custom_mouse.move(x, y, duration=0.15, randomize=3) - wait(0.1) + custom_mouse.move(x, y, duration=0.15, randomize=5) + wait(0.2, 0.3) mouse.click(button="left") wait(0.3, 0.4) keyboard.release("shift") @@ -281,6 +342,6 @@ if __name__ == "__main__": screen = Screen(config.general["monitor"]) template_finder = TemplateFinder(screen) ui_manager = UiManager(screen, template_finder) - # ui_manager.stash_all_items(6) + ui_manager.stash_all_items(6) # ui_manager.use_wp(4, 1) - ui_manager.fill_up_belt_from_inventory(10) + # ui_manager.fill_up_belt_from_inventory(10) diff --git a/src/utils/misc.py b/src/utils/misc.py index 9dc0330..f170400 100644 --- a/src/utils/misc.py +++ b/src/utils/misc.py @@ -3,7 +3,7 @@ import random import ctypes from logger import Logger import cv2 -import uuid +from typing import List, Tuple def wait(min_seconds, max_seconds = None): @@ -12,11 +12,6 @@ def wait(min_seconds, max_seconds = None): time.sleep(random.random() * (max_seconds - min_seconds) + min_seconds) return -def get_mac(): - mac_num = hex(uuid.getnode()).replace('0x', '').upper() - mac = ':'.join(mac_num[i: i + 2] for i in range(0, 11, 2)) - return mac - def kill_thread(thread): thread_id = thread.ident res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit)) @@ -28,6 +23,12 @@ def cut_roi(img, roi): x, y, width, height = roi return img[y:y+height, x:x+width] +def is_in_roi(roi: List[float], pos: Tuple[float, float]): + x, y, w, h = roi + is_in_x_range = x < pos[0] < x + w + is_in_y_range = y < pos[1] < y + h + return is_in_x_range and is_in_y_range + def color_filter(img, color_range): hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) color_mask = cv2.inRange(hsv_img, color_range[0], color_range[1]) diff --git a/ui.ini b/ui.ini index fba946d..aade3f6 100644 --- a/ui.ini +++ b/ui.ini @@ -11,6 +11,13 @@ orange=20,251,251,23,255,255 red=4,251,206,7,255,213 [ui_pos_1920_1080] +screen_width=1920 +screen_height=1080 +offset_top=0 +center_x=960 +center_y=540 +; some distance +skill_bar_height=100 ; game creation / ending buttons play_x_online=800 play_x_offline=960 @@ -69,3 +76,21 @@ wp_act_btn_width=100 wp_first_btn_x=340 wp_first_btn_y=193 wp_btn_height=70 +; pickit +item_dist=400 +; pather +reached_node_dist=150 + +[ui_roi_1920_1080] +; all rois are in [left, top, width, height] format +is_overburdened=17,765,470,90 +save_and_exit=750,350,410,260 +play_btn=390,760,1150,300 +hell_btn=550,100,1000,800 +gold_btn=1390,736,400,150 +health_globe=260,890,320,190 +mana_globe=1350,890,320,190 +cut_skill_bar=0,0,1920,980 +merc_icon=0,0,150,150 +loading_left_black=0,0,500,1080 +death=770,290,380,75