diff --git a/assets/shop/claws/2_to_assa.png b/assets/shop/claws/2_to_assa.png new file mode 100644 index 0000000..b4f72de Binary files /dev/null and b/assets/shop/claws/2_to_assa.png differ diff --git a/assets/shop/claws/3_to_traps.png b/assets/shop/claws/3_to_traps.png new file mode 100644 index 0000000..a672193 Binary files /dev/null and b/assets/shop/claws/3_to_traps.png differ diff --git a/assets/shop/claws/claw1.png b/assets/shop/claws/claw1.png new file mode 100644 index 0000000..4aaeef1 Binary files /dev/null and b/assets/shop/claws/claw1.png differ diff --git a/assets/shop/claws/claw2.png b/assets/shop/claws/claw2.png new file mode 100644 index 0000000..f7b6611 Binary files /dev/null and b/assets/shop/claws/claw2.png differ diff --git a/assets/shop/claws/claw3.png b/assets/shop/claws/claw3.png new file mode 100644 index 0000000..533c5db Binary files /dev/null and b/assets/shop/claws/claw3.png differ diff --git a/assets/shop/claws/to_ds.png b/assets/shop/claws/to_ds.png new file mode 100644 index 0000000..2c82a06 Binary files /dev/null and b/assets/shop/claws/to_ds.png differ diff --git a/assets/shop/claws/to_light.png b/assets/shop/claws/to_light.png new file mode 100644 index 0000000..680e073 Binary files /dev/null and b/assets/shop/claws/to_light.png differ diff --git a/assets/shop/claws/to_traps.png b/assets/shop/claws/to_traps.png new file mode 100644 index 0000000..2bde5dd Binary files /dev/null and b/assets/shop/claws/to_traps.png differ diff --git a/assets/shop/claws/to_venom.png b/assets/shop/claws/to_venom.png new file mode 100644 index 0000000..0cb09c0 Binary files /dev/null and b/assets/shop/claws/to_venom.png differ diff --git a/assets/shop/claws/wb.png b/assets/shop/claws/wb.png new file mode 100644 index 0000000..920588c Binary files /dev/null and b/assets/shop/claws/wb.png differ diff --git a/assets/shop/gloves/g_gloves.png b/assets/shop/gloves/g_gloves.png new file mode 100644 index 0000000..7d7d786 Binary files /dev/null and b/assets/shop/gloves/g_gloves.png differ diff --git a/src/shopper.py b/src/shopper.py new file mode 100644 index 0000000..c306b4e --- /dev/null +++ b/src/shopper.py @@ -0,0 +1,310 @@ +import datetime +import logging +import os +import time +import math + +import keyboard +import numpy as np + +from screen import Screen +from config import Config +from logger import Logger +from npc_manager import NpcManager, Npc +from template_finder import TemplateFinder +from utils.custom_mouse import mouse +from utils.misc import wait, load_template, send_discord +import cv2 + + +class ExtendedTemplateFinder(TemplateFinder): + def __init__(self, screen): + super().__init__(screen) + custom_templates = { + "CLAW1": [load_template("assets/shop/claws/claw1.png", 1.0), 1.0], + "CLAW2": [load_template("assets/shop/claws/claw2.png", 1.0), 1.0], + "CLAW3": [load_template("assets/shop/claws/claw3.png", 1.0), 1.0], + "TO_TRAPS": [load_template("assets/shop/claws/to_traps.png", 1.0), 1.0], + "3_TO_TRAPS": [load_template("assets/shop/claws/3_to_traps.png", 1.0), 1.0], + "2_TO_ASSA": [load_template("assets/shop/claws/2_to_assa.png", 1.0), 1.0], + "TO_LIGHT": [load_template("assets/shop/claws/to_light.png", 1.0), 1.0], + "TO_WB": [load_template("assets/shop/claws/wb.png", 1.0), 1.0], + "TO_DS": [load_template("assets/shop/claws/to_ds.png", 1.0), 1.0], + "TO_VENOM": [load_template("assets/shop/claws/to_venom.png", 1.0), 1.0], + } + self._templates.update(custom_templates) + + +def exit(run_obj): + run_time = str(datetime.timedelta(seconds=round(time.time() - run_obj.start_time))) + Logger.info("Exiting shopping mall...") + print( + "STATS \truns \t\ttime \t\tias_gloves_seen \tgloves_bought \tclaws_evaluated \tclaws_bought\n" + f"\t{run_obj.run_count} \t\t{run_time}" + f"\t\t{run_obj.ias_gloves_seen} \t\t\t{run_obj.gloves_bought} \t\t{run_obj.claws_evaluated} \t\t\t{run_obj.claws_bought}" + ) + os._exit(0) + + +def wait_for_loading_screen(screen: Screen, time_out): + start = time.time() + while time.time() - start < time_out: + img = screen.grab() + is_loading_black_roi = np.average(img[:700, 0:250]) < 4.0 + if is_loading_black_roi: + return True + return False + + +class AnyaShopper: + """ + Shop at Anya for 20 ias, +3 java skill gloves and more... + + In order to start the shopping bot: + 1.) Run this this file in Python. + 2.) Be ingame in Harrogath (act 5 town). + 3.) Stand close to Anya and the red portal. + 4.) While being ingame, press resume_key (default F11) to start the shopping, and exit_key (default F12) to stop it. + """ + + def __init__(self, config: Config): + + # Configurable part, dont touch anything above + # Set look_for variables to False if you dont like your personal shopper to look for these + # Obviously something need to be set to True, or your shopper will be very confused + # For the trap claw scores use: + # 7 if you are happy with any + to traps or +2 assassin + # 9 if you want at least +2 assassin or two useful trap stats + # 11 if you want at least +3 traps or +2 and a sentry bonus + # Similar for melee claws but not really worth keeping any less that 11 here since you really want both +2 assassin and a useful other stat, feedback needed + + self.look_for_plus_2_gloves = True + self.look_for_plus_3_gloves = True + self.look_for_trap_claws = True + self.trap_claw_min_score = 11 + self.look_for_melee_claws = True + self.melee_claw_min_score = 11 + + # Dont touch anything below here + self._config = config + self._screen = Screen(config.general["monitor"]) + self._template_finder = ExtendedTemplateFinder(self._screen) + self._npc_manager = NpcManager( + screen=self._screen, template_finder=self._template_finder + ) + self.run_count = 0 + self.start_time = time.time() + self.ias_gloves_seen = 0 + self.gloves_bought = 0 + + # Claws config + self.roi_claw_stats = [0, 0, config.ui_pos["screen_width"] // 2, config.ui_pos["screen_height"] - 100] + self.roi_vendor = config.ui_roi["vendor_stash"] + self.rx, self.ry, _, _ = self.roi_vendor + self.sb_x, self.sb_y = self._screen.convert_screen_to_monitor((180, 77)) + self.c_x, self.c_y = self._screen.convert_screen_to_monitor((config.ui_pos["center_x"], config.ui_pos["center_y"])) + self.claws_evaluated = 0 + self.claws_bought = 0 + + def run(self): + Logger.info("Personal Anya Shopper at your service! Hang on, running some errands...") + self.reset_shop() + self.shop_loop() + + def shop_loop(self): + + asset_folder = "assets/shop/gloves/" + + while True: + + self._npc_manager.open_npc_menu(Npc.ANYA) + self._npc_manager.press_npc_btn(Npc.ANYA, "trade") + time.sleep(0.1) + img = self._screen.grab() + + # 20 IAS gloves have a unique color so we can skip all others + ias_glove = self._template_finder.search( + ref=load_template(asset_folder + "ias_gloves.png", 1.0), + inp_img=img, + threshold=0.96, + roi=self._config.ui_roi["vendor_stash"], + normalize_monitor=True, + ) + if ias_glove.valid: + self.ias_gloves_seen += 1 + mouse.move(*ias_glove.position) + time.sleep(0.1) + img = self._screen.grab() + + if self.look_for_plus_3_gloves is True: + gg_gloves = self._template_finder.search( + ref=load_template( + asset_folder + "gg_gloves.png", 1.0 # assets for javazon gloves are mixed up, this one need +3 as in the 1080p version + ), + inp_img=img, + threshold=0.80, + normalize_monitor=True, + ) + if gg_gloves.valid: + mouse.click(button="right") + send_discord(f"Bought awesome IAS/+3 gloves!", self._config.general["custom_discord_hook"]) + Logger.info("IAS/+3 gloves bought!") + self.gloves_bought += 1 + time.sleep(1) + + else: + if self.look_for_plus_2_gloves is True: + g_gloves = self._template_finder.search( + ref=load_template( + asset_folder + "g_gloves.png", 1.0 + ), + inp_img=img, + threshold=0.80, + normalize_monitor=True, + ) + if g_gloves.valid: + mouse.click(button="right") + send_discord(f"Bought some decent IAS/+2 gloves", self._config.general["custom_discord_hook"]) + Logger.info("IAS/+2 gloves bought!") + self.gloves_bought += 1 + time.sleep(1) + + # Select Weapons section + if self.look_for_trap_claws is True or self.look_for_melee_claws is True: + mouse.move(self.sb_x, self.sb_y, randomize=3, delay_factor=[0.6, 0.8]) + wait(0.05, 0.1) + mouse.press(button="left") + wait(0.3, 0.4) + # Search for claws + claw_pos = [] + img = self._screen.grab().copy() + claw_keys = ["CLAW1", "CLAW2", "CLAW3"] + for ck in claw_keys: + template_match = self._template_finder.search(ck, img, roi=self.roi_vendor) + if template_match.valid: + (y, x) = np.where(self._template_finder.last_res >= 0.6) + for (x, y) in zip(x, y): + new_pos = [x + self.rx + 16, y + self.ry + 50] + # check if pos already exists in claw_pos + exists_already = False + for pos in claw_pos: + dist = math.dist(new_pos, pos) + if dist < 10: + exists_already = True + if not exists_already: + claw_pos.append(new_pos) + # check out each claw + for pos in claw_pos: + # cv2.circle(img, pos, 3, (0, 255, 0), 2) + x_m, y_m = self._screen.convert_screen_to_monitor(pos) + mouse.move(x_m, y_m, randomize=3, delay_factor=[0.5, 0.6]) + wait(0.5, 0.6) + img_stats = self._screen.grab() + trap_score = 0 + melee_score = 0 + if self._template_finder.search("3_TO_TRAPS", img_stats, roi=self.roi_claw_stats, threshold=0.94).valid: + trap_score += 12 + elif self._template_finder.search("TO_TRAPS", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid: + trap_score += 8 + + if self._template_finder.search("2_TO_ASSA", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid: + trap_score += 10 + melee_score += 10 + if self._template_finder.search("TO_VENOM", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid: + melee_score += 6 + if self._template_finder.search("TO_LIGHT", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid: + trap_score += 6 + if self._template_finder.search("TO_WB", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid: + melee_score += 2 + if self._template_finder.search("TO_DS", img_stats, roi=self.roi_claw_stats, threshold=0.9).valid: + trap_score += 4 + + self.claws_evaluated += 1 + + if trap_score > 11 and self.look_for_trap_claws is True: + # pick it up + mouse.click(button="right") + send_discord(f"Bought some terrific trap Claws (score: {trap_score})", self._config.general["custom_discord_hook"]) + Logger.info(f"Trap Claws (score: {trap_score}) bought!") + self.claws_bought += 1 + time.sleep(1) + + if melee_score > 11 and self.look_for_melee_claws is True: + # pick it up + mouse.click(button="right") + send_discord(f"Bought some mad melee Claws (score: {melee_score})", self._config.general["custom_discord_hook"]) + Logger.info(f"Melee Claws (score: {melee_score}) bought!") + self.claws_bought += 1 + time.sleep(1) + + # Done with this shopping round + self.reset_shop() + self.run_count += 1 + + def reset_shop(self): + while 1: + success = self.select_by_template("A5_RED_PORTAL") + success &= wait_for_loading_screen(self._screen, 2) + if success: + break + else: + mouse.move(800, 450, randomize=50, delay_factor=[0.7, 0.7]) + time.sleep(3) + while 1: + success = self.select_by_template("A5_RED_PORTAL") + success &= wait_for_loading_screen(self._screen, 2) + if success: + break + else: + mouse.move(800, 450, randomize=50, delay_factor=[0.7, 0.7]) + + def select_by_template(self, template_type: str) -> bool: + Logger.debug(f"Select {template_type}") + template_match = self._template_finder.search_and_wait(template_type, time_out=10) + if template_match.valid: + x_m, y_m = self._screen.convert_screen_to_monitor(template_match.position) + mouse.move(x_m, y_m) + wait(0.1, 0.2) + mouse.click(button="left") + return True + return False + +def main(): + config = Config() + print("Anya Shopper running, click resume key (Default F11) when close to Anya to start shopping") + + anyashopper = AnyaShopper(config) + keyboard.add_hotkey(config.general["exit_key"], lambda: exit(run_obj=anyashopper)) + + while True: + if keyboard.is_pressed(config.general["resume_key"]): + try: + anyashopper.run() + except Exception as e: + Logger.logger.exception(e) + exit(anyashopper) + break + if keyboard.is_pressed(config.general["graphic_debugger_key"]): + screen = Screen(config.general["monitor"]) + tf = ExtendedTemplateFinder(screen) + search_templates = ["CLAW1", "CLAW2", "CLAW3", "TO_TRAPS", "3_TO_TRAPS", "2_TO_ASSA", "TO_LIGHT", "TO_WB", "TO_DS", "TO_VENOM"] + scores = {} + while True: + img = screen.grab() + display_img = img.copy() + for template_name in search_templates: + template_match = tf.search(template_name, img) + scores[template_name] = template_match.score + if template_match.valid: + pos = template_match.position + cv2.putText(display_img, str(template_name), pos, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA) + cv2.circle(display_img, pos, 7, (255, 0, 0), thickness=5) + display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_NEAREST) + print(scores) + cv2.imshow('test', display_img) + cv2.waitKey(1) + time.sleep(0.05) + + +if __name__ == "__main__": + main() diff --git a/src/template_finder.py b/src/template_finder.py index 84b8896..7b4421e 100644 --- a/src/template_finder.py +++ b/src/template_finder.py @@ -26,6 +26,7 @@ class TemplateFinder: self._screen = screen self._config = Config() self._scale_factor = 1.0 + self.last_res = None self._templates = { # Templates for node in A5 Town "A5_TOWN_0": [load_template(f"assets/templates/a5_town/a5_town_0.png", self._scale_factor), self._scale_factor], @@ -195,8 +196,8 @@ class TemplateFinder: rh *= scale if img.shape[0] > template.shape[0] and img.shape[1] > template.shape[1]: - res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED) - _, max_val, _, max_pos = cv2.minMaxLoc(res) + self.last_res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED) + _, max_val, _, max_pos = cv2.minMaxLoc(self.last_res) if max_val > threshold: ref_point = (max_pos[0] + int(template.shape[1] * 0.5) + rx, max_pos[1] + int(template.shape[0] * 0.5) + ry) ref_point = (int(ref_point[0] * (1.0 / scale)), int(ref_point[1] * (1.0 / scale)))