refactor(botty): Add docs, clean up, put all hardcoded px values to .ini
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
25
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
|
||||
|
||||
Reference in New Issue
Block a user