diff --git a/README.md b/README.md index fa6a127..916ca8f 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ run_shenk=0 | show_items | Hotkey for "show | | inventory_screen | Hotkey to open up | | stand_still | Hotkey for "stand still". Note this can not be the default shift key as it would interfere with the merc healing routine. | +| force_move | Hotkey for "force move" | | tp | Hotkey for using a town | | belt_rows | Integer value of how many rows the char's belt has | | show_belt | Hotkey for "show | diff --git a/assets/templates/a3_stash.png b/assets/templates/a3_stash.png new file mode 100644 index 0000000..17dcfd5 Binary files /dev/null and b/assets/templates/a3_stash.png differ diff --git a/assets/templates/a3_wp.png b/assets/templates/a3_wp.png new file mode 100644 index 0000000..7233b75 Binary files /dev/null and b/assets/templates/a3_wp.png differ diff --git a/assets/templates/a4_wp.png b/assets/templates/a4_wp.png new file mode 100644 index 0000000..f68a463 Binary files /dev/null and b/assets/templates/a4_wp.png differ diff --git a/game.ini b/game.ini index 926f7aa..f7908d8 100644 --- a/game.ini +++ b/game.ini @@ -96,6 +96,7 @@ go_btn=533,627,213,67 difficulty_select=556,278,168,142 gold_btn=927,491,267,100 inventory_gold=980,510,150,40 +gold_btn_stash=150,480,40,65 health_globe=160,580,240,140 mana_globe=887,580,240,140 cut_skill_bar=0,0,1280,653 diff --git a/params.ini b/params.ini index 0783257..fb212c3 100644 --- a/params.ini +++ b/params.ini @@ -42,6 +42,7 @@ show_items=alt inventory_screen=i ;note: stand_still can not be the default "shift" as it would interfere with merc healing stand_still=capslock +force_move=e tp=f9 belt_rows=4 ;note: this is different from the default hotkey as "~" is for many keyboards not reachable without also pressing altgr diff --git a/src/bot.py b/src/bot.py index e9b06c1..66c706b 100644 --- a/src/bot.py +++ b/src/bot.py @@ -5,6 +5,7 @@ from template_finder import TemplateFinder from item_finder import ItemFinder from screen import Screen from ui_manager import UiManager +from npc_manager import NpcManager from belt_manager import BeltManager from pather import Pather, Location from logger import Logger @@ -13,7 +14,7 @@ from char.i_char import IChar from config import Config from health_manager import HealthManager from death_manager import DeathManager -from npc_manager import NpcManager, Npc +from town import TownManager, A3, A4, A5 from pickit import PickIt from game_stats import GameStats from utils.misc import wait @@ -34,7 +35,6 @@ class Bot: self._ui_manager = UiManager(self._screen, self._template_finder) self._belt_manager = BeltManager(self._screen, self._template_finder) self._pather = Pather(self._screen, self._template_finder) - self._npc_manager = NpcManager(self._screen, self._template_finder) self._pickit = PickIt(self._screen, self._item_finder, self._ui_manager, self._belt_manager, self._game_stats) if self._config.char["type"] == "sorceress": self._char: IChar = Sorceress(self._config.sorceress, self._config.char, self._screen, self._template_finder, self._ui_manager, self._pather) @@ -43,6 +43,11 @@ class Bot: else: Logger.error(f'{self._config.char["type"]} is not supported! Closing down bot.') os._exit(1) + npc_manager = NpcManager(screen, self._template_finder) + a5 = A5(self._screen, self._template_finder, self._pather, self._char, npc_manager) + a4 = A4(self._screen, self._template_finder, self._pather, self._char, npc_manager) + a3 = A3(self._screen, self._template_finder, self._pather, self._char, npc_manager) + self._town_manager = TownManager(self._template_finder, self._ui_manager, a3, a4, a5) self._route_config = self._config.routes if self._route_config["run_shenk"] and not self._route_config["run_eldritch"]: Logger.error("Running shenk without eldtritch is not supported. Either run none, both or eldritch only.") @@ -54,8 +59,7 @@ class Bot: if self._config.general["randomize_runs"]: self.shuffle_runs() self._picked_up_items = False - self._tp_is_up = False - self._curr_location: Location = None + self._curr_loc: Location = None self._tps_left = 20 self._pre_buffed = 0 self._stopping = False @@ -64,17 +68,17 @@ class Bot: self._no_stash_counter = 0 self._ran_no_pickup = False - self._states=['hero_selection', 'a5_town', 'pindle', 'shenk'] + self._states=['hero_selection', 'town', 'pindle', 'shenk'] self._transitions = [ - { 'trigger': 'create_game', 'source': 'hero_selection', 'dest': 'a5_town', 'before': "on_create_game"}, + { 'trigger': 'create_game', 'source': 'hero_selection', 'dest': 'town', 'before': "on_create_game"}, # Tasks within town - { 'trigger': 'maintenance', 'source': 'a5_town', 'dest': 'a5_town', 'before': "on_maintenance"}, + { 'trigger': 'maintenance', 'source': 'town', 'dest': 'town', 'before': "on_maintenance"}, # Different runs - { 'trigger': 'run_pindle', 'source': 'a5_town', 'dest': 'pindle', 'before': "on_run_pindle"}, - { 'trigger': 'run_shenk', 'source': 'a5_town', 'dest': 'shenk', 'before': "on_run_shenk"}, + { 'trigger': 'run_pindle', 'source': 'town', 'dest': 'pindle', 'before': "on_run_pindle"}, + { 'trigger': 'run_shenk', 'source': 'town', 'dest': 'shenk', 'before': "on_run_shenk"}, # End run / game - { 'trigger': 'end_run', 'source': ['shenk', 'pindle'], 'dest': 'a5_town', 'before': "on_end_run"}, - { 'trigger': 'end_game', 'source': ['a5_town', 'shenk', 'pindle', 'end_run'], 'dest': 'hero_selection', 'before': "on_end_game"}, + { 'trigger': 'end_run', 'source': ['shenk', 'pindle'], 'dest': 'town', 'before': "on_end_run"}, + { 'trigger': 'end_game', 'source': ['town', 'shenk', 'pindle', 'end_run'], 'dest': 'hero_selection', 'before': "on_end_game"}, ] self.machine = Machine(model=self, states=self._states, initial="hero_selection", transitions=self._transitions, queued=True) @@ -85,7 +89,7 @@ class Bot: self.machine.get_graph().draw('my_state_diagram.png', prog='dot') def get_curr_location(self): - return self._curr_location + return self._curr_loc def start(self): self.trigger('create_game') @@ -127,96 +131,64 @@ class Bot: return not found_unfinished_run def on_create_game(self): + keyboard.release(self._config.char["stand_still"]) + # Start a game from hero selection self._game_stats.log_start_game() template_match = self._template_finder.search_and_wait("D2_LOGO_HS", roi=self._config.ui_roi["hero_selection_logo"]) if template_match.valid: Logger.debug(f"Found {template_match.name}") - if not self._ui_manager.start_game(): - return - self._template_finder.search_and_wait(["A5_TOWN_1", "A5_TOWN_0"]) - self._tp_is_up = False - self._curr_location = Location.A5_TOWN_START - # Make sure these keys are released - keyboard.release(self._config.char["stand_still"]) - wait(0.05, 0.15) - keyboard.release(self._config.char["show_items"]) - wait(0.05, 0.15) + if not self._ui_manager.start_game(): return + self._curr_loc = self._town_manager.wait_for_town_spawn() + # Run /nopickup command to avoid picking up stuff on accident if not self._ran_no_pickup: if self._ui_manager.enable_no_pickup(): self._ran_no_pickup = True - Logger.info("Enabled No Item Pickup.") + Logger.info("Enabled No Item Pickup") self.trigger_or_stop("maintenance") def on_maintenance(self): if self._pick_corpse: self._pick_corpse = False time.sleep(0.6) + # TODO: Test corpse pickup in A3 (and A4) DeathManager.pick_up_corpse(self._config, self._screen) wait(1.2, 1.5) self._belt_manager.fill_up_belt_from_inventory(self._config.char["num_loot_columns"]) - - self._belt_manager.update_pot_needs() - - # Check if healing is needed, TODO: add shoping e.g. for potions - img = self._screen.grab() - # TODO: If tp is up we always go back into the portal... - if not self._tp_is_up and (HealthManager.get_health(self._config, img) < 0.6 or HealthManager.get_mana(self._config, img) < 0.3): - Logger.info("Need some healing first. Going to Malah.") - if not self._pather.traverse_nodes(self._curr_location, Location.A5_MALAH, self._char): - self.trigger_or_stop("end_game") - return - self._curr_location = Location.A5_MALAH - self._npc_manager.open_npc_menu(Npc.MALAH) - if not self._pather.traverse_nodes(self._curr_location, Location.A5_TOWN_START, self._char): - self.trigger_or_stop("end_game") - return - self._curr_location = Location.A5_TOWN_START - - # Stash stuff, either when item was picked up or after 4 runs without stashing (so unwanted loot will not cause inventory full) - # but should not happen much with /nopickup set - self._no_stash_counter += 1 - if self._picked_up_items or (self._no_stash_counter > 4 and self._ui_manager.should_stash(self._config.char["num_loot_columns"])): - self._no_stash_counter = 0 - Logger.info("Stashing picked up items.") - if not self._pather.traverse_nodes(self._curr_location, Location.A5_STASH, self._char): - self.trigger_or_stop("end_game") - return - self._curr_location = Location.A5_STASH - time.sleep(0.3) - stash_is_open_func = lambda: self._template_finder.search("INVENTORY_GOLD_BTN", self._screen.grab(), roi=self._config.ui_roi["gold_btn"]).valid - if self._char.select_by_template("A5_STASH", stash_is_open_func): - self._ui_manager.stash_all_items(self._config.char["num_loot_columns"], self._item_finder) - self._picked_up_items = False - time.sleep(1.2) # otherwise next grab of screen will still have inventory - else: - Logger.warning("Could not find stash, continue...") - - if self._tps_left < 3 or (self._config.char["tp"] and not self._ui_manager.has_tps()): - Logger.info("Repairing and buying TPs at Larzuk.") - if not self._pather.traverse_nodes(self._curr_location, Location.A5_LARZUK, self._char): - self.trigger_or_stop("end_game") - return - self._curr_location = Location.A5_LARZUK - self._npc_manager.open_npc_menu(Npc.LARZUK) - self._npc_manager.press_npc_btn(Npc.LARZUK, "trade_repair") - if self._ui_manager.repair_and_fill_up_tp(): - wait(0.1, 0.2) - self._ui_manager.close_vendor_screen() - self._tps_left = 20 wait(0.5) - + self._belt_manager.update_pot_needs() + # Do some healing TODO: If tp is up we always go back into the portal... could use force move here? + img = self._screen.grab() + hp = HealthManager.get_health(self._config, img) + mp = HealthManager.get_mana(self._config, img) + if hp < 0.6 or mp < 0.2: + Logger.info("Healing at next possible Vendor") + self._curr_loc = self._town_manager.heal(self._curr_loc) + if not self._curr_loc: + return self.trigger_or_stop("end_game") + # Stash stuff, either when item was picked up or after 4 runs without stashing (so unwanted loot will not cause inventory full) + self._no_stash_counter += 1 + force_stash = self._no_stash_counter > 4 and self._ui_manager.should_stash(self._config.char["num_loot_columns"]) + if self._picked_up_items or force_stash: + self._no_stash_counter = 0 + Logger.info("Stashing items") + self._curr_loc = self._town_manager.stash(self._curr_loc) + if not self._curr_loc: + return self.trigger_or_stop("end_game") + wait(1.0) + # Check if we are out of tps + if self._tps_left < 3 or (self._config.char["tp"] and not self._ui_manager.has_tps()): + Logger.info("Repairing and buying TPs at next Vendor") + self._curr_loc = self._town_manager.repair_and_fill_tps(self._curr_loc) + if not self._curr_loc: + return self.trigger_or_stop("end_game") + wait(1.0) # Check if merc needs to be revived merc_alive = self._template_finder.search("MERC", self._screen.grab(), threshold=0.9, roi=[0, 0, 200, 200]).valid if not merc_alive and self._config.char["use_merc"]: - Logger.info("Reviving merc.") - if not self._pather.traverse_nodes(self._curr_location, Location.A5_QUAL_KEHK, self._char): - self.trigger_or_stop("end_game") - return - self._curr_location = Location.A5_QUAL_KEHK - if self._npc_manager.open_npc_menu(Npc.QUAL_KEHK): - self._npc_manager.press_npc_btn(Npc.QUAL_KEHK, "resurrect") - time.sleep(1.2) - + Logger.info("Resurrect merc") + self._curr_loc = self._town_manager.resurrect(self._curr_loc) + if not self._curr_loc: + return self.trigger_or_stop("end_game") # Start a new run started_run = False for key in self._do_runs: @@ -230,12 +202,15 @@ class Bot: def on_run_pindle(self): def do_it() -> bool: Logger.info("Run Pindle") - if not self._pather.traverse_nodes(self._curr_location, Location.A5_NIHLATHAK_PORTAL, self._char): return False - self._curr_location = Location.A5_NIHLATHAK_PORTAL - wait(0.3, 0.4) # otherwise will often missclick because still moving + self._curr_loc = self._town_manager.go_to_act(5, self._curr_loc) + if not self._curr_loc: + return False + if not self._pather.traverse_nodes(self._curr_loc, Location.A5_NIHLATHAK_PORTAL, self._char): return False + self._curr_loc = Location.A5_NIHLATHAK_PORTAL + wait(0.4) found_loading_screen_func = lambda: self._ui_manager.wait_for_loading_screen(2.0) if not self._char.select_by_template(["A5_RED_PORTAL", "A5_RED_PORTAL_TEXT"], found_loading_screen_func): return False - self._curr_location = Location.PINDLE_START + self._curr_loc = Location.A5_PINDLE_START if not self._template_finder.search_and_wait(["PINDLE_0", "PINDLE_1"], threshold=0.65, time_out=20).valid: return False if not self._pre_buffed: self._char.pre_buff() @@ -243,9 +218,10 @@ class Bot: if self._config.char["static_path_pindle"]: self._pather.traverse_nodes_fixed("pindle_save_dist", self._char) else: - if not self._pather.traverse_nodes(Location.PINDLE_START, Location.PINDLE_SAVE_DIST, self._char): return False + if not self._pather.traverse_nodes(Location.A5_PINDLE_START, Location.A5_PINDLE_SAVE_DIST, self._char): return False self._char.kill_pindle() self._picked_up_items = self._pickit.pick_up_items(self._char) + self._curr_loc = Location.A5_PINDLE_END return True self._do_runs["run_pindle"] = False @@ -258,14 +234,11 @@ class Bot: def on_run_shenk(self): def do_it(): Logger.info("Run Eldritch") - if not self._pather.traverse_nodes(self._curr_location, Location.A5_WP, self._char): return False - self._curr_location = Location.A5_WP - wait(0.5, 0.7) # otherwise will missclick because still moving - found_wp_func = lambda: self._template_finder.search("WAYPOINT_MENU", self._screen.grab()).valid - if not self._char.select_by_template("A5_WP", found_wp_func): return False - self._ui_manager.use_wp(4, 1) + self._curr_loc = self._town_manager.open_wp(self._curr_loc) + wait(0.4) + self._ui_manager.use_wp(5, 1) # eldritch - self._curr_location = Location.ELDRITCH_START + self._curr_loc = Location.A5_ELDRITCH_START if not self._template_finder.search_and_wait(["ELDRITCH_0", "ELDRITCH_START"], threshold=0.65, time_out=20).valid: return False if not self._pre_buffed: self._char.pre_buff() @@ -273,17 +246,19 @@ class Bot: if self._config.char["static_path_eldritch"]: self._pather.traverse_nodes_fixed("eldritch_save_dist", self._char) else: - if not self._pather.traverse_nodes(Location.ELDRITCH_START, Location.ELDRITCH_SAVE_DIST, self._char): return False + if not self._pather.traverse_nodes(Location.A5_ELDRITCH_START, Location.A5_ELDRITCH_SAVE_DIST, self._char): return False self._char.kill_eldritch() + self._curr_loc = Location.A5_ELDRITCH_END self._picked_up_items = self._pickit.pick_up_items(self._char) # shenk if self._route_config["run_shenk"]: Logger.info("Run Shenk") - self._curr_location = Location.SHENK_START - if not self._pather.traverse_nodes(Location.SHENK_START, Location.SHENK_SAVE_DIST, self._char): return False + self._curr_loc = Location.A5_SHENK_START + if not self._pather.traverse_nodes(Location.A5_SHENK_START, Location.A5_SHENK_SAVE_DIST, self._char): return False self._char.kill_shenk() - wait(1.9, 2.4) # sometimes merc needs some time to kill shenk... + wait(1.9, 2.4) # sometimes merc needs some more time to kill shenk... self._picked_up_items |= self._pickit.pick_up_items(self._char) + self._curr_loc = Location.A5_SHENK_END return True self._do_runs["run_shenk"] = False @@ -310,10 +285,8 @@ class Bot: success = self._char.tp_town() if success: self._tps_left -= 1 - success = self._template_finder.search_and_wait(["A5_TOWN_1", "A5_TOWN_0"], time_out=10).valid - if success: - self._tp_is_up = True - self._curr_location = Location.A5_TOWN_START + self._curr_loc = self._town_manager.wait_for_tp(self._curr_loc) + if self._curr_loc: self.trigger_or_stop("maintenance") else: self.trigger_or_stop("end_game") @@ -329,6 +302,6 @@ if __name__ == "__main__": config = Config() screen = Screen(config.general["monitor"]) bot = Bot(screen) - bot.state = "a5_town" - bot._curr_location = Location.A5_TOWN_START + bot.state = "town" + bot._curr_loc = Location.A5_TOWN_START bot.on_maintenance() diff --git a/src/char/hammerdin.py b/src/char/hammerdin.py index 3c5308a..6723694 100644 --- a/src/char/hammerdin.py +++ b/src/char/hammerdin.py @@ -65,10 +65,10 @@ class Hammerdin(IChar): if not self._do_pre_move: keyboard.send(self._skill_hotkeys["concentration"]) wait(0.05, 0.15) - self._pather.traverse_nodes(Location.PINDLE_SAVE_DIST, Location.PINDLE_END, self, time_out=1.0, do_pre_move=self._do_pre_move) + self._pather.traverse_nodes(Location.A5_PINDLE_SAVE_DIST, Location.A5_PINDLE_END, self, time_out=1.0, do_pre_move=self._do_pre_move) self._cast_hammers(1) # pindle sometimes knocks back, get back in - self._pather.traverse_nodes(Location.PINDLE_SAVE_DIST, Location.PINDLE_END, self, time_out=0.1) + self._pather.traverse_nodes(Location.A5_PINDLE_SAVE_DIST, Location.A5_PINDLE_END, self, time_out=0.1) self._cast_hammers(max(1, self._char_config["atk_len_pindle"] - 1)) wait(0.1, 0.15) self._do_redemption() @@ -81,7 +81,7 @@ class Hammerdin(IChar): if not self._do_pre_move: keyboard.send(self._skill_hotkeys["concentration"]) wait(0.05, 0.15) - self._pather.traverse_nodes(Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END, self, time_out=1.0, do_pre_move=self._do_pre_move) + self._pather.traverse_nodes(Location.A5_ELDRITCH_SAVE_DIST, Location.A5_ELDRITCH_END, self, time_out=1.0, do_pre_move=self._do_pre_move) wait(0.05, 0.1) self._cast_hammers(self._char_config["atk_len_eldritch"]) wait(0.05, 0.15) @@ -92,7 +92,7 @@ class Hammerdin(IChar): if not self._do_pre_move: keyboard.send(self._skill_hotkeys["concentration"]) wait(0.05, 0.15) - self._pather.traverse_nodes(Location.SHENK_SAVE_DIST, Location.SHENK_END, self, time_out=1.0, do_pre_move=self._do_pre_move) + self._pather.traverse_nodes(Location.A5_SHENK_SAVE_DIST, Location.A5_SHENK_END, self, time_out=1.0, do_pre_move=self._do_pre_move) wait(0.05, 0.1) self._cast_hammers(self._char_config["atk_len_shenk"]) wait(0.05, 0.15) @@ -124,5 +124,5 @@ if __name__ == "__main__": ui_manager = UiManager(screen, t_finder) char = Hammerdin(config.hammerdin, config.char, screen, t_finder, ui_manager, pather) char.pre_buff() - pather.traverse_nodes(Location.ELDRITCH_START, Location.ELDRITCH_SAVE_DIST, char) + pather.traverse_nodes(Location.A5_ELDRITCH_START, Location.A5_ELDRITCH_SAVE_DIST, char) char.kill_eldritch() diff --git a/src/char/i_char.py b/src/char/i_char.py index 0ae24a0..277abca 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -72,7 +72,7 @@ class IChar: keyboard.send(self._skill_hotkeys["teleport"]) wait(0.15, 0.25) - def move(self, pos_monitor: Tuple[float, float], force_tp: bool = False): + def move(self, pos_monitor: Tuple[float, float], force_tp: bool = False, force_move: bool = False): factor = self._config.advanced_options["pathing_delay_factor"] if self._skill_hotkeys["teleport"] and (force_tp or self._ui_manager.is_right_skill_active()): mouse.move(pos_monitor[0], pos_monitor[1], randomize=3, delay_factor=[factor*0.1, factor*0.14]) @@ -91,7 +91,10 @@ class IChar: x, y = self._screen.convert_abs_to_monitor(pos_abs) mouse.move(x, y, randomize=5, delay_factor=[factor*0.1, factor*0.14]) wait(0.012, 0.02) - mouse.click(button="left") + if force_move: + keyboard.send(self._config.char["force_move"]) + else: + mouse.click(button="left") def tp_town(self): if not self._ui_manager.has_tps(): diff --git a/src/char/sorceress.py b/src/char/sorceress.py index b5c0a7d..211fe81 100644 --- a/src/char/sorceress.py +++ b/src/char/sorceress.py @@ -96,7 +96,7 @@ class Sorceress(IChar): if self._config.char["static_path_pindle"]: self._pather.traverse_nodes_fixed("pindle_end", self) else: - self._pather.traverse_nodes(Location.PINDLE_SAVE_DIST, Location.PINDLE_END, self, force_tp=True) + self._pather.traverse_nodes(Location.A5_PINDLE_SAVE_DIST, Location.A5_PINDLE_END, self, force_tp=True) return True return False @@ -117,7 +117,7 @@ class Sorceress(IChar): if self._config.char["static_path_eldritch"]: self._pather.traverse_nodes_fixed("eldritch_end", self) else: - self._pather.traverse_nodes(Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END, self, time_out=0.6, force_tp=True) + self._pather.traverse_nodes(Location.A5_ELDRITCH_SAVE_DIST, Location.A5_ELDRITCH_END, self, time_out=0.6, force_tp=True) return True return False @@ -135,7 +135,7 @@ class Sorceress(IChar): self._left_attack(cast_pos_abs, delay, 90) wait(self._cast_duration, self._cast_duration + 0.2) # Move to items - self._pather.traverse_nodes(Location.SHENK_SAVE_DIST, Location.SHENK_END, self, time_out=1.4, force_tp=True) + self._pather.traverse_nodes(Location.A5_SHENK_SAVE_DIST, Location.A5_SHENK_END, self, time_out=1.4, force_tp=True) return True return False diff --git a/src/config.py b/src/config.py index f185f0f..80bc174 100644 --- a/src/config.py +++ b/src/config.py @@ -51,6 +51,7 @@ class Config: "show_items": self._select_val("char", "show_items"), "inventory_screen": self._select_val("char", "inventory_screen"), "stand_still": self._select_val("char", "stand_still"), + "force_move": self._select_val("char", "force_move"), "num_loot_columns": int(self._select_val("char", "num_loot_columns")), "take_health_potion": float(self._select_val("char", "take_health_potion")), "take_mana_potion": float(self._select_val("char", "take_mana_potion")), diff --git a/src/death_manager.py b/src/death_manager.py index 79e0580..d0fcebd 100644 --- a/src/death_manager.py +++ b/src/death_manager.py @@ -84,6 +84,5 @@ if __name__ == "__main__": keyboard.wait("f11") config = Config() screen = Screen(config.general["monitor"]) - template_finder = TemplateFinder(screen) - manager = DeathManager(screen, template_finder) - manager.start_monitor(None) + manager = DeathManager(screen) + manager.pick_up_corpse(config, screen) diff --git a/src/health_manager.py b/src/health_manager.py index 9ba3707..6b18b7f 100644 --- a/src/health_manager.py +++ b/src/health_manager.py @@ -44,8 +44,8 @@ class HealthManager: self._pausing = True def update_location(self, loc: Location): - if loc is not None: - bosses = ["shenk", "eldritch", "pindle"] + if loc is not None and type(loc) == str: + bosses = ["shenk", "eldritch", "pindle", "trav"] self._pausing = not any(substring in loc for substring in bosses) @staticmethod diff --git a/src/pather.py b/src/pather.py index 559cdb2..7c81f78 100644 --- a/src/pather.py +++ b/src/pather.py @@ -20,23 +20,24 @@ class Location: A5_TOWN_START = "a5_town_start" A5_STASH = "a5_stash" A5_WP = "a5_wp" - A5_QUAL_KEHK = "qual_kehk" - A5_MALAH = "malah" - A5_NIHLATHAK_PORTAL = "nihlathak_portal" - A5_LARZUK = "larzuk" + A5_QUAL_KEHK = "a5_qual_kehk" + A5_MALAH = "a5_malah" + A5_NIHLATHAK_PORTAL = "a5_nihlathak_portal" + A5_LARZUK = "a5_larzuk" # Pindle - PINDLE_START = "pindle_start" - PINDLE_SAVE_DIST = "pindle_save_dist" - PINDLE_END = "pindle_end" + A5_PINDLE_START = "a5_pindle_start" + A5_PINDLE_SAVE_DIST = "a5_pindle_save_dist" + A5_PINDLE_END = "a5_pindle_end" # Eldritch - ELDRITCH_START = "eldritch_start" - ELDRITCH_SAVE_DIST = "eldritch_save_dist" - ELDRITCH_END = "eldritch_end" + A5_ELDRITCH_START = "a5_eldritch_start" + A5_ELDRITCH_SAVE_DIST = "a5_eldritch_save_dist" + A5_ELDRITCH_END = "a5_eldritch_end" # Shenk - SHENK_START = "shenk_start" - SHENK_SAVE_DIST = "shenk_save_dist" - SHENK_END = "shenk_end" + A5_SHENK_START = "a5_shenk_start" + A5_SHENK_SAVE_DIST = "a5_shenk_save_dist" + A5_SHENK_END = "a5_shenk_end" # A4 Town + A4_TOWN_START = "a4_town_start" A4_WP = "a4_town_wp" A4_TYRAEL_STASH = "a4_tyrael_stash" A4_VENDOR = "a4_vendor" @@ -44,11 +45,11 @@ class Location: A3_TOWN_START = "a3_town_start" A3_ORMUS = "a3_ormus" A3_STASH_WP = "a3_stash_wp" - A3_ASHEARA = "ar_asheara" + A3_ASHEARA = "a3_asheara" # Trav - TRAV_START = "trav_start" - TRAV_SAVE_DIST = "trav_save_dist" - TRAV_END = "trav_end" + A3_TRAV_START = "a3_trav_start" + A3_TRAV_SAVE_DIST = "a3_trav_save_dist" + A3_TRAV_END = "a3_trav_end" class Pather: """ @@ -145,32 +146,37 @@ class Pather: (Location.A5_STASH, Location.A5_QUAL_KEHK): [5, 6, 10, 11, 12], (Location.A5_STASH, Location.A5_LARZUK): [13, 14], (Location.A5_STASH, Location.A5_WP): [], + (Location.A5_WP, Location.A5_STASH): [], + (Location.A5_WP, Location.A5_NIHLATHAK_PORTAL): [6, 8, 9], (Location.A5_QUAL_KEHK, Location.A5_NIHLATHAK_PORTAL): [12, 11, 10, 6, 8, 9], (Location.A5_QUAL_KEHK, Location.A5_WP): [12, 11, 10, 6], (Location.A5_LARZUK, Location.A5_QUAL_KEHK): [13, 14, 5, 6, 10, 11, 12], (Location.A5_LARZUK, Location.A5_NIHLATHAK_PORTAL): [14, 13, 5, 6, 8, 9], (Location.A5_LARZUK, Location.A5_WP): [14, 13, 5], # Pindle - (Location.PINDLE_START, Location.PINDLE_SAVE_DIST): [100, 101, 102, 103], - (Location.PINDLE_SAVE_DIST, Location.PINDLE_END): [104], + (Location.A5_PINDLE_START, Location.A5_PINDLE_SAVE_DIST): [100, 101, 102, 103], + (Location.A5_PINDLE_SAVE_DIST, Location.A5_PINDLE_END): [104], # Eldritch - (Location.ELDRITCH_START, Location.ELDRITCH_SAVE_DIST): [120, 121, 122], - (Location.ELDRITCH_SAVE_DIST, Location.ELDRITCH_END): [123], + (Location.A5_ELDRITCH_START, Location.A5_ELDRITCH_SAVE_DIST): [120, 121, 122], + (Location.A5_ELDRITCH_SAVE_DIST, Location.A5_ELDRITCH_END): [123], # Shenk - (Location.SHENK_START, Location.SHENK_SAVE_DIST): [140, 141, 142, 143, 144, 145, 146, 147, 148], - (Location.SHENK_SAVE_DIST, Location.SHENK_END): [149], + (Location.A5_SHENK_START, Location.A5_SHENK_SAVE_DIST): [140, 141, 142, 143, 144, 145, 146, 147, 148], + (Location.A5_SHENK_SAVE_DIST, Location.A5_SHENK_END): [149], # A4 Town (Location.A4_WP, Location.A4_TYRAEL_STASH): [160, 161], (Location.A4_TYRAEL_STASH, Location.A4_WP): [161, 160], + (Location.A4_TOWN_START, Location.A4_WP): [162], # A3 Town (Location.A3_TOWN_START, Location.A3_STASH_WP): [180, 181, 182, 183, 184, 185, 186, 187, 188], (Location.A3_TOWN_START, Location.A3_ORMUS): [180, 181, 182, 183, 184, 185], (Location.A3_ORMUS, Location.A3_STASH_WP): [186, 187, 188], (Location.A3_ORMUS, Location.A3_ASHEARA): [189, 190, 191, 192], (Location.A3_ASHEARA, Location.A3_STASH_WP): [191, 190, 189, 185, 186, 187, 188], + (Location.A3_STASH_WP, Location.A3_STASH_WP): [188], + (Location.A3_STASH_WP, Location.A3_ORMUS): [187, 186, 185], # Trav - (Location.TRAV_START, Location.TRAV_SAVE_DIST): [220, 221, 222, 223, 224, 225, 226, 227], - (Location.TRAV_SAVE_DIST, Location.TRAV_END): [228], + (Location.A3_TRAV_START, Location.A3_TRAV_SAVE_DIST): [220, 221, 222, 223, 224, 225, 226, 227], + (Location.A3_TRAV_SAVE_DIST, Location.A3_TRAV_END): [228], } def _get_node(self, key: int, template: str): @@ -234,17 +240,32 @@ class Pather: return node_pos_abs return None - def traverse_nodes(self, start_location: Location, end_location: Location, char: IChar, time_out: float = 7, force_tp: bool = False, do_pre_move: bool = True) -> bool: + def traverse_nodes( + self, + start_location: Location, + end_location: Location, + char: IChar,# + time_out: float = 7, + force_tp: bool = False, + do_pre_move: bool = True, + force_move: 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 time_out: Timeout in second. If no more move was found in that time it will cancel traverse + :param force_move: Bool value if force move should be used for pathing :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)] + try: + path = self._paths[(start_location, end_location)] + except KeyError: + if start_location == end_location: + return True + Logger.error(f"Don't know how to traverse from {start_location} to {end_location}") + return False if do_pre_move: char.pre_move() for i, node_idx in enumerate(path): @@ -276,7 +297,7 @@ class Pather: else: # Move the char x_m, y_m = self._screen.convert_abs_to_monitor(node_pos_abs) - char.move((x_m, y_m), force_tp=force_tp) + char.move((x_m, y_m), force_tp=force_tp, force_move=force_move) last_move = time.time() return True @@ -331,6 +352,6 @@ if __name__ == "__main__": ui_manager = UiManager(screen, t_finder) char = Hammerdin(config.hammerdin, config.char, screen, t_finder, ui_manager, pather) # pather.traverse_nodes_fixed("pindle_save_dist", char) - pather.traverse_nodes(Location.TRAV_START, Location.TRAV_SAVE_DIST, char) - pather.traverse_nodes(Location.TRAV_SAVE_DIST, Location.TRAV_END, char) + # pather.traverse_nodes(Location.A3_TRAV_START, Location.A3_TRAV_SAVE_DIST, char) + pather.traverse_nodes(Location.A3_STASH_WP, Location.A3_STASH_WP, char) # display_all_nodes(pather, filter="TRAV") diff --git a/src/town/__init__.py b/src/town/__init__.py new file mode 100644 index 0000000..d38c745 --- /dev/null +++ b/src/town/__init__.py @@ -0,0 +1,5 @@ +from .a3 import A3 +from .a4 import A4 +from .a5 import A5 +from .i_act import IAct +from .town_manager import TownManager diff --git a/src/town/a3.py b/src/town/a3.py new file mode 100644 index 0000000..3d40739 --- /dev/null +++ b/src/town/a3.py @@ -0,0 +1,52 @@ +from char.i_char import IChar +from town.i_act import IAct +from screen import Screen +from config import Config +from npc_manager import NpcManager, Npc +from pather import Pather, Location +from typing import Union +from template_finder import TemplateFinder +from utils.misc import wait +from utils.custom_mouse import mouse + + +class A3(IAct): + def __init__(self, screen: Screen, template_finder: TemplateFinder, pather: Pather, char: IChar, npc_manager: NpcManager): + self._config = Config() + self._screen = screen + self._pather = pather + self._char = char + self._npc_manager = npc_manager + self._template_finder = template_finder + + def get_wp_location(self) -> Location: return Location.A3_STASH_WP + def can_heal(self) -> bool: return True + def can_stash(self) -> bool: return True + + def heal(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A3_ORMUS, self._char): return False + self._npc_manager.open_npc_menu(Npc.ORMUS) + return Location.A3_ORMUS + + def open_stash(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A3_STASH_WP, self._char): return False + wait(0.3) + def stash_is_open_func(): + found = self._template_finder.search("INVENTORY_GOLD_BTN", self._screen.grab(), roi=self._config.ui_roi["gold_btn"]).valid + found |= self._template_finder.search("INVENTORY_GOLD_BTN", self._screen.grab(), roi=self._config.ui_roi["gold_btn_stash"]).valid + return found + if not self._char.select_by_template("A3_STASH", stash_is_open_func): return False + return Location.A3_STASH_WP + + def open_wp(self, curr_loc: Location) -> bool: + if not self._pather.traverse_nodes(curr_loc, Location.A3_STASH_WP, self._char): return False + wait(0.5, 0.7) + found_wp_func = lambda: self._template_finder.search("WAYPOINT_MENU", self._screen.grab()).valid + return self._char.select_by_template("A3_WP", found_wp_func) + + def wait_for_tp(self) -> Union[Location, bool]: + template_match = self._template_finder.search_and_wait("A3_TOWN_10", time_out=20) + if template_match.valid: + self._pather.traverse_nodes(Location.A3_STASH_WP, Location.A3_STASH_WP, self._char, force_move=True) + return Location.A3_STASH_WP + return False diff --git a/src/town/a4.py b/src/town/a4.py new file mode 100644 index 0000000..1eb0b62 --- /dev/null +++ b/src/town/a4.py @@ -0,0 +1,41 @@ +from char.i_char import IChar +from town.i_act import IAct +from screen import Screen +from config import Config +from npc_manager import NpcManager, Npc +from pather import Pather, Location +from typing import Union +from template_finder import TemplateFinder +from utils.misc import wait + + +class A4(IAct): + def __init__(self, screen: Screen, template_finder: TemplateFinder, pather: Pather, char: IChar, npc_manager: NpcManager): + self._config = Config() + self._screen = screen + self._pather = pather + self._char = char + self._npc_manager = npc_manager + self._template_finder = template_finder + + def get_wp_location(self) -> Location: return Location.A4_WP + def can_resurrect(self) -> bool: return True + + def resurrect(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A4_TYRAEL_STASH, self._char): return False + if self._npc_manager.open_npc_menu(Npc.TYRAEL): + self._npc_manager.press_npc_btn(Npc.TYRAEL, "resurrect") + return Location.A4_TYRAEL_STASH + return False + + def open_wp(self, curr_loc: Location) -> bool: + if not self._pather.traverse_nodes(curr_loc, Location.A4_WP, self._char): return False + wait(0.5, 0.7) + found_wp_func = lambda: self._template_finder.search("WAYPOINT_MENU", self._screen.grab()).valid + return self._char.select_by_template("A4_WP", found_wp_func) + + def wait_for_tp(self) -> Union[Location, bool]: + success = self._template_finder.search_and_wait(["A4_TOWN_4", "A4_TOWN_5", "A4_TOWN_6"], time_out=20).valid + if success: + return Location.A4_TOWN_START + return False \ No newline at end of file diff --git a/src/town/a5.py b/src/town/a5.py new file mode 100644 index 0000000..f3bbc2f --- /dev/null +++ b/src/town/a5.py @@ -0,0 +1,67 @@ +from char.i_char import IChar +from town.i_act import IAct +from screen import Screen +from config import Config +from npc_manager import NpcManager, Npc +from pather import Pather, Location +from typing import Union +from template_finder import TemplateFinder +from utils.misc import wait + + +class A5(IAct): + def __init__(self, screen: Screen, template_finder: TemplateFinder, pather: Pather, char: IChar, npc_manager: NpcManager): + self._config = Config() + self._screen = screen + self._pather = pather + self._char = char + self._npc_manager = npc_manager + self._template_finder = template_finder + + def get_wp_location(self) -> Location: return Location.A5_WP + + def can_heal(self) -> bool: return True + def can_resurrect(self) -> bool: return True + def can_stash(self) -> bool: return True + def can_trade_and_repair(self) -> bool: return True + + def heal(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A5_MALAH, self._char, force_move=True): return False + self._npc_manager.open_npc_menu(Npc.MALAH) + if not self._pather.traverse_nodes(Location.A5_MALAH, Location.A5_TOWN_START, self._char, force_move=True): return False + return Location.A5_TOWN_START + + def resurrect(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A5_QUAL_KEHK, self._char): return False + if self._npc_manager.open_npc_menu(Npc.QUAL_KEHK): + self._npc_manager.press_npc_btn(Npc.QUAL_KEHK, "resurrect") + return Location.A5_QUAL_KEHK + return False + + def open_stash(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A5_STASH, self._char): return False + wait(0.3) + def stash_is_open_func(): + found = self._template_finder.search("INVENTORY_GOLD_BTN", self._screen.grab(), roi=self._config.ui_roi["gold_btn"]).valid + found |= self._template_finder.search("INVENTORY_GOLD_BTN", self._screen.grab(), roi=self._config.ui_roi["gold_btn_stash"]).valid + return found + if not self._char.select_by_template("A5_STASH", stash_is_open_func): return False + return Location.A5_STASH + + def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]: + if not self._pather.traverse_nodes(curr_loc, Location.A5_LARZUK, self._char): return + self._npc_manager.open_npc_menu(Npc.LARZUK) + self._npc_manager.press_npc_btn(Npc.LARZUK, "trade_repair") + return Location.A5_LARZUK + + def open_wp(self, curr_loc: Location) -> bool: + if not self._pather.traverse_nodes(curr_loc, Location.A5_WP, self._char): return False + wait(0.5, 0.7) + found_wp_func = lambda: self._template_finder.search("WAYPOINT_MENU", self._screen.grab()).valid + return self._char.select_by_template("A5_WP", found_wp_func) + + def wait_for_tp(self) -> Union[Location, bool]: + success = self._template_finder.search_and_wait(["A5_TOWN_1", "A5_TOWN_0"], time_out=20).valid + if success: + return Location.A5_TOWN_START + return False diff --git a/src/town/i_act.py b/src/town/i_act.py new file mode 100644 index 0000000..80e32de --- /dev/null +++ b/src/town/i_act.py @@ -0,0 +1,24 @@ +from typing import Union +from pather import Location + +class IAct: + # Open waypoint menu + def open_wp(self, curr_loc: Location) -> bool: return False + # Get Location that is closest to waypoint to continue pathing from there + def get_wp_location(self) -> Location: pass + # Wait until we arrive in town after using town portal (by searching for templates close it) + def wait_for_tp(self) -> Union[Location, bool]: return False + + # Is healing implemented for this Town? + def can_heal(self) -> bool: return False + # Is merc resurrection implemented for this Town? + def can_resurrect(self) -> bool: return False + # Is stashing implemented in this Town? + def can_stash(self) -> bool: return False + # Is trade/repair implemented in this Town? + def can_trade_and_repair(self) -> bool: return False + # If any of the above functions return True for the Town, the respective method must be implemented + def heal(self, curr_loc: Location) -> Union[Location, bool]: return False + def resurrect(self, curr_loc: Location) -> Union[Location, bool]: return False + def open_stash(self, curr_loc: Location) -> Union[Location, bool]: return False + def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]: return False diff --git a/src/town/town_manager.py b/src/town/town_manager.py new file mode 100644 index 0000000..c5201fd --- /dev/null +++ b/src/town/town_manager.py @@ -0,0 +1,149 @@ +from typing import Union +from item_finder import ItemFinder +from template_finder import TemplateFinder +from config import Config +from pather import Location +from logger import Logger +from ui_manager import UiManager +from town import IAct, A3, A4, A5 +from utils.misc import wait + + +class TownManager: + def __init__(self, template_finder: TemplateFinder, ui_manager: UiManager, a3: A3, a4: A4, a5: A5): + self._template_finder = template_finder + self._ui_manager = ui_manager + self._item_finder = ItemFinder() + self._config = Config() + self._acts: dict[Location, IAct] = { + Location.A3_TOWN_START: a3, + Location.A4_TOWN_START: a4, + Location.A5_TOWN_START: a5 + } + + @staticmethod + def get_act_from_location(loc: Location) -> Location: + location = None + if loc.upper().startswith("A5_"): + location = Location.A5_TOWN_START + elif loc.upper().startswith("A4_"): + location = Location.A4_TOWN_START + elif loc.upper().startswith("A3_"): + location = Location.A3_TOWN_START + return location + + def wait_for_town_spawn(self, time_out: float = None) -> Location: + """Wait for the char to spawn in town after starting a new game + :param time_out: Optional float value for time out in seconds, defaults to None + :return: Location of the town (e.g. Location.A4_TOWN_START) or None if nothing was found within time_out time + """ + template_match = self._template_finder.search_and_wait([ + "A5_TOWN_0", "A5_TOWN_1", + "A4_TOWN_0", "A4_TOWN_1", + "A3_TOWN_0", "A3_TOWN_1" + ], time_out=time_out) + if template_match.valid: + return TownManager.get_act_from_location(template_match.name) + return None + + def wait_for_tp(self, curr_loc: Location): + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + return self._acts[curr_act].wait_for_tp() + + def open_wp(self, curr_loc: Location): + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + return self._acts[curr_act].open_wp(curr_loc) + + def go_to_act(self, act_idx: int, curr_loc: Location) -> Union[Location, bool]: + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + # check if we already are in the desired act + if act_idx == 3: act = Location.A3_TOWN_START + elif act_idx == 4: act = Location.A4_TOWN_START + elif act_idx == 5: act = Location.A5_TOWN_START + else: + Logger.error(f"Act {act_idx} is not supported") + return False + if curr_act == act: + return curr_loc + # if not, move to the desired act via waypoint + if not self._acts[curr_act].open_wp(curr_loc): return False + self._ui_manager.use_wp(act_idx, 0) + return self._acts[act].get_wp_location() + + def heal(self, curr_loc: Location) -> Union[Location, bool]: + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + # check if we can heal in current act + if self._acts[curr_act].can_heal(): + return self._acts[curr_act].heal(curr_loc) + Logger.warning(f"Could not heal in {curr_act}. Continue without healing") + return curr_loc + + def resurrect(self, curr_loc: Location) -> Union[Location, bool]: + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + # check if we can resurrect in current act + if self._acts[curr_act].can_resurrect(): + return self._acts[curr_act].resurrect(curr_loc) + new_loc = self.go_to_act(4, curr_loc) + if not new_loc: return False + return self._acts[Location.A4_TOWN_START].resurrect(new_loc) + + def stash(self, curr_loc: Location) -> Union[Location, bool]: + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + # check if we can stash in current act + if self._acts[curr_act].can_stash(): + new_loc = self._acts[curr_act].open_stash(curr_loc) + if not new_loc: return False + self._ui_manager.stash_all_items(self._config.char["num_loot_columns"], self._item_finder) + return new_loc + new_loc = self.go_to_act(5, curr_loc) + if not new_loc: return False + new_loc = self._acts[Location.A5_TOWN_START].open_stash(new_loc) + if not new_loc: return False + self._ui_manager.stash_all_items(self._config.char["num_loot_columns"], self._item_finder) + return new_loc + + def repair_and_fill_tps(self, curr_loc: Location) -> Union[Location, bool]: + curr_act = TownManager.get_act_from_location(curr_loc) + if curr_act is None: return False + # check if we can rapair in current act + if self._acts[curr_act].can_trade_and_repair(): + new_loc = self._acts[curr_act].open_trade_and_repair_menu(curr_loc) + if not new_loc: return False + if self._ui_manager.repair_and_fill_up_tp(): + wait(0.1, 0.2) + self._ui_manager.close_vendor_screen() + return new_loc + # TODO: Got to Act 5 for repair and fill up tps + Logger.warning(f"Could not repair in {curr_act}. Continue without repairing") + return curr_loc + + +# Test: Move to desired location in d2r and run any town action you want to test from there +if __name__ == "__main__": + import keyboard + import os + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1)) + print("Move to d2r window and press f11") + keyboard.wait("f11") + from char.hammerdin import Hammerdin + from pather import Pather + from screen import Screen + from npc_manager import NpcManager + config = Config() + screen = Screen(config.general["monitor"]) + template_finder = TemplateFinder(screen) + npc_manager = NpcManager(screen, template_finder) + pather = Pather(screen, template_finder) + ui_manager = UiManager(screen, template_finder) + char = Hammerdin(config.hammerdin, config.char, screen, template_finder, ui_manager, pather) + a5 = A5(screen, template_finder, pather, char, npc_manager) + a4 = A4(screen, template_finder, pather, char, npc_manager) + a3 = A3(screen, template_finder, pather, char, npc_manager) + town_manager = TownManager(template_finder, ui_manager, a3, a4, a5) + print(town_manager.wait_for_tp(Location.A3_TRAV_END)) diff --git a/src/ui_manager.py b/src/ui_manager.py index d394c40..2306ae5 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -27,20 +27,25 @@ class UiManager(): 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. + :param act: Index of the desired act starting at 1 [A1 = 1, A2 = 2, A3 = 3, ...] + :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) - # mouse.move(x, y, randomize=8) - # mouse.click(button="left") - # wait(0.3, 0.4) + # TODO: Optimize by checking if wp already has desired act active + pos_act_btn = (self._config.ui_pos["wp_act_i_btn_x"] + self._config.ui_pos["wp_act_btn_width"] * (act - 1), self._config.ui_pos["wp_act_i_btn_y"]) + x, y = self._screen.convert_screen_to_monitor(pos_act_btn) + mouse.move(x, y, 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) - mouse.move(x, y, randomize=[110, 20], delay_factor=[0.9, 1.4]) + mouse.move(x, y, randomize=[60, 9], delay_factor=[0.9, 1.4]) wait(0.4, 0.5) mouse.click(button="left") + # wait till loading screen is over + if self.wait_for_loading_screen(): + while 1: + if not self.wait_for_loading_screen(0.2): + return def is_right_skill_active(self) -> bool: """ @@ -80,19 +85,20 @@ class UiManager(): return True return False - def wait_for_loading_screen(self, time_out: float) -> bool: + def wait_for_loading_screen(self, time_out: float = None) -> bool: """ Waits until loading screen apears :param time_out: Maximum time to search for a loading screen :return: True if loading screen was found within the timeout. False otherwise """ start = time.time() - while time.time() - start < time_out: + while True: img = self._screen.grab() is_loading_black_roi = np.average(img[:700, 0:250]) < 3.5 if is_loading_black_roi: return True - return False + if time_out is not None and time.time() - start > time_out: + return False def save_and_exit(self, does_chicken: bool = False) -> bool: """ @@ -265,6 +271,10 @@ class UiManager(): # 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...") + # Move cursor to center + x, y = self._screen.convert_abs_to_monitor((0, 0)) + mouse.move(x, y, randomize=[40, 200], delay_factor=[1.0, 1.5]) + # Wait till gold btn is found gold_btn = self._template_finder.search_and_wait("INVENTORY_GOLD_BTN", roi=self._config.ui_roi["gold_btn"], time_out=20) if not gold_btn.valid: Logger.error("Could not determine to be in stash menu. Continue...") @@ -318,9 +328,7 @@ class UiManager(): time.sleep(0.3) curr_pos = mouse.get_position() # move mouse away from inventory, for some reason it was sometimes included in the grabed img - top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"]) - move_to = (top_left_slot[0] - 300, top_left_slot[1] - 200) - x, y = self._screen.convert_screen_to_monitor(move_to) + x, y = self._screen.convert_abs_to_monitor((0, 0)) mouse.move(x, y, randomize=[40, 200], delay_factor=[1.0, 1.5]) item_check_img = self._screen.grab() mouse.move(*curr_pos, randomize=2) @@ -341,9 +349,7 @@ class UiManager(): Logger.debug("Check if stash is full") time.sleep(0.6) # move mouse away from inventory, for some reason it was sometimes included in the grabed img - top_left_slot = (self._config.ui_pos["inventory_top_left_slot_x"], self._config.ui_pos["inventory_top_left_slot_y"]) - move_to = (top_left_slot[0] - 300, top_left_slot[1] - 200) - x, y = self._screen.convert_screen_to_monitor(move_to) + x, y = self._screen.convert_abs_to_monitor((0, 0)) mouse.move(x, y, randomize=[40, 200], delay_factor=[1.0, 1.5]) img = self._screen.grab() if self._inventory_has_items(img, num_loot_columns):