refactor(botty): Add docs, clean up, put all hardcoded px values to .ini

This commit is contained in:
aeon0
2021-11-06 12:48:19 +01:00
parent b9ccc733df
commit 90869ebfbb
16 changed files with 305 additions and 178 deletions

View File

@@ -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:</br>
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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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"])

View File

@@ -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:

View File

@@ -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)

View File

@@ -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])

25
ui.ini
View File

@@ -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