diff --git a/src/belt_manager.py b/src/belt_manager.py index e50dcec95..ca4fa5f58 100644 --- a/src/belt_manager.py +++ b/src/belt_manager.py @@ -144,7 +144,7 @@ def update_pot_needs(self) -> List[int]: elif current_column is not None and potion_type == "empty": self._pot_needs[current_column] += 1 wait(0.2) - Logger.debug(self._pot_needs) + Logger.debug(f"Will pickup: {self._pot_needs}") keyboard.send(self._config.char["show_belt"]) def fill_up_belt_from_inventory(self, num_loot_columns: int): diff --git a/src/bot.py b/src/bot.py index 31e4e55c5..58c7044aa 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,6 +1,7 @@ from transitions import Machine import time from char.hammerdin import Hammerdin +from run import Pindle, ShenkEld, Trav from template_finder import TemplateFinder from item_finder import ItemFinder from screen import Screen @@ -28,7 +29,6 @@ class Bot: def __init__(self, screen: Screen, game_stats: GameStats, pick_corpse: bool = False): self._screen = screen self._game_stats = game_stats - self._pick_corpse = pick_corpse self._config = Config() self._template_finder = TemplateFinder(self._screen) self._item_finder = ItemFinder() @@ -36,6 +36,8 @@ def __init__(self, screen: Screen, game_stats: GameStats, pick_corpse: bool = Fa self._belt_manager = BeltManager(self._screen, self._template_finder) self._pather = Pather(self._screen, self._template_finder) self._pickit = PickIt(self._screen, self._item_finder, self._ui_manager, self._belt_manager, self._game_stats) + + # Create Character 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) elif self._config.char["type"] == "hammerdin": @@ -43,12 +45,16 @@ def __init__(self, screen: Screen, game_stats: GameStats, pick_corpse: bool = Fa else: Logger.error(f'{self._config.char["type"]} is not supported! Closing down bot.') os._exit(1) + + # Create Town Manager 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 + + # Create runs 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 or both") os._exit(1) @@ -59,6 +65,12 @@ def __init__(self, screen: Screen, game_stats: GameStats, pick_corpse: bool = Fa } if self._config.general["randomize_runs"]: self.shuffle_runs() + self._pindle = Pindle(self._template_finder, self._pather, self._town_manager, self._ui_manager, self._char, self._pickit) + self._shenk = ShenkEld(self._template_finder, self._pather, self._town_manager, self._ui_manager, self._char, self._pickit) + self._trav = Trav(self._template_finder, self._pather, self._town_manager, self._ui_manager, self._char, self._pickit) + + # Create member variables + self._pick_corpse = pick_corpse self._picked_up_items = False self._curr_loc: Location = None self._tps_left = 20 @@ -69,6 +81,7 @@ def __init__(self, screen: Screen, game_stats: GameStats, pick_corpse: bool = Fa self._no_stash_counter = 0 self._ran_no_pickup = False + # Create State Machine self._states=['hero_selection', 'town', 'pindle', 'shenk', "trav"] self._transitions = [ { 'trigger': 'create_game', 'source': 'hero_selection', 'dest': 'town', 'before': "on_create_game"}, @@ -149,25 +162,26 @@ def on_create_game(self): self.trigger_or_stop("maintenance") def on_maintenance(self): + # Handle picking up corpse in case of death 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"]) wait(0.5) + # Look at belt to figure out how many pots need to be picked up 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? + + # Check if should need some healing 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: + if HealthManager.get_health(self._config, img) < 0.6 or HealthManager.get_mana(self._config, img) < 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) + + # Stash stuff, either when item was picked up or after X runs without stashing because of unwanted loot in inventory 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: @@ -177,6 +191,7 @@ def on_maintenance(self): 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") @@ -184,6 +199,7 @@ def on_maintenance(self): 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"]: @@ -191,6 +207,7 @@ def on_maintenance(self): 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: @@ -202,104 +219,37 @@ def on_maintenance(self): self.trigger_or_stop("end_game") def on_run_pindle(self): - def do_it() -> bool: - Logger.info("Run Pindle") - 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_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() - self._pre_buffed = 1 - 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.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 - success = do_it() - if self.is_last_run() or not success: + res = self._pindle.run(self._curr_loc, not self._pre_buffed) + if self.is_last_run() or not res: self.trigger_or_stop("end_game") else: self.trigger_or_stop("end_run") + self._curr_loc = res[0] + self._picked_up_items = res[1] def on_run_shenk(self): - def do_it(): - Logger.info("Run Eldritch") - self._curr_loc = self._town_manager.open_wp(self._curr_loc) - wait(0.4) - self._ui_manager.use_wp(5, 1) - # eldritch - 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() - self._pre_buffed = 1 - 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.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_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 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 - success = do_it() - if self.is_last_run() or not success: + res = self._shenk.run(self._curr_loc, self._route_config["run_shenk"], not self._pre_buffed) + if self.is_last_run() or not res: self.trigger_or_stop("end_game") else: self.trigger_or_stop("end_run") + self._curr_loc = res[0] + self._picked_up_items = res[1] def on_run_trav(self): - def do_it(): - Logger.info("Run Trav") - if not self._char.can_teleport(): - Logger.error("Trav is currently only supported for teleporting builds. Skipping trav") - return True - self._curr_loc = self._town_manager.open_wp(self._curr_loc) - wait(0.4) - self._ui_manager.use_wp(3, 7) - # eldritch - self._curr_loc = Location.A3_TRAV_START - if not self._template_finder.search_and_wait(["TRAV_0", "TRAV_1"], threshold=0.65, time_out=20).valid: return False - if not self._pre_buffed: - self._char.pre_buff() - self._pre_buffed = 1 - self._pather.traverse_nodes_fixed("trav_save_dist", self._char) - self._char.kill_council() - self._curr_loc = Location.A3_TRAV_END - self._picked_up_items = self._pickit.pick_up_items(self._char) - return True - self._do_runs["run_trav"] = False - success = do_it() - if self.is_last_run() or not success: + res = self._trav.run(self._curr_loc, not self._pre_buffed) + if self.is_last_run() or not res: self.trigger_or_stop("end_game") else: self.trigger_or_stop("end_run") + self._curr_loc = res[0] + self._picked_up_items = res[1] def on_end_game(self): - self._pre_buffed = 0 + self._pre_buffed = False self._ui_manager.save_and_exit() self._game_stats.log_end_game() self._do_runs = { @@ -313,6 +263,7 @@ def on_end_game(self): self.trigger_or_stop("create_game") def on_end_run(self): + self._pre_buffed = True success = self._char.tp_town() if success: self._tps_left -= 1 diff --git a/src/char/hammerdin.py b/src/char/hammerdin.py index 1534fdba2..c5df905ba 100644 --- a/src/char/hammerdin.py +++ b/src/char/hammerdin.py @@ -39,10 +39,10 @@ def _cast_hammers(self, time_in_s: float): wait(0.01, 0.05) keyboard.send(self._char_config["stand_still"], do_press=False) - def _do_redemption(self): + def _do_redemption(self, delay: tuple[float, float] = (1.5, 2.0)): if self._skill_hotkeys["redemption"]: keyboard.send(self._skill_hotkeys["redemption"]) - wait(1.5, 2.0) + wait(*delay) def pre_buff(self): if self._char_config["cta_available"]: @@ -71,10 +71,8 @@ def kill_pindle(self) -> bool: keyboard.send(self._skill_hotkeys["concentration"]) wait(0.05, 0.15) 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.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)) + self._cast_hammers(self._char_config["atk_len_pindle"]) wait(0.1, 0.15) self._do_redemption() return True diff --git a/src/char/i_char.py b/src/char/i_char.py index 237835a9e..8c7c7023d 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -102,7 +102,6 @@ def move(self, pos_monitor: Tuple[float, float], force_tp: bool = False, force_m def tp_town(self): if not self._ui_manager.has_tps(): - Logger.error("Wanted to TP but no TPs are available! Make sure your keybinding is correct and you have a tomb in your inventory.") return False mouse.click(button="right") # TODO: Add hardcoded coordinates to ini file diff --git a/src/pather.py b/src/pather.py index 7db650f50..cbbd89b64 100644 --- a/src/pather.py +++ b/src/pather.py @@ -177,6 +177,7 @@ def __init__(self, screen: Screen, template_finder: TemplateFinder): # Trav (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], + (Location.A3_TRAV_SAVE_DIST, Location.A3_TRAV_SAVE_DIST): [227] } def _get_node(self, key: int, template: str): @@ -250,10 +251,11 @@ def traverse_nodes( start_location: Location, end_location: Location, char: IChar,# - time_out: float = 7, + time_out: float = 5, force_tp: bool = False, do_pre_move: bool = True, - force_move: bool = False) -> bool: + force_move: bool = False + ) -> bool: """ Traverse from one location to another :param start_location: Location the char is starting at @@ -273,11 +275,14 @@ def traverse_nodes( return False if do_pre_move: char.pre_move() + last_direction = None for i, node_idx in enumerate(path): continue_to_next_node = False last_move = time.time() + did_force_move = False while not continue_to_next_node: img = self._screen.grab() + # Handle timeout if (time.time() - last_move) > time_out: success = self._template_finder.search("WAYPOINT_MENU", img).valid if success: @@ -294,6 +299,19 @@ def traverse_nodes( cv2.imwrite("./info_screenshots/info_pather_got_stuck_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img) Logger.error("Got stuck exit pather") return False + + # Sometimes we get stuck at rocks and stuff, after 2.5 seconds force a move into the last know direction + if not did_force_move and time.time() - last_move > 3.1: + pos_abs = (0, 150) + if last_direction is not None: + pos_abs = last_direction + print(pos_abs) + x_m, y_m = self._screen.convert_abs_to_monitor(pos_abs) + char.move((x_m, y_m), force_move=True) + did_force_move = True + last_move = time.time() + + # Find any template and calc node position from it node_pos_abs = self.find_abs_node_pos(node_idx, img) if node_pos_abs is not None: dist = math.dist(node_pos_abs, (0, 0)) @@ -303,6 +321,7 @@ def traverse_nodes( # 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, force_move=force_move) + last_direction = node_pos_abs last_move = time.time() return True @@ -358,5 +377,5 @@ def display_all_nodes(pather: Pather, filter: str = None): char = Hammerdin(config.hammerdin, config.char, screen, t_finder, ui_manager, pather) # pather.traverse_nodes_fixed("pindle_save_dist", 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) + pather.traverse_nodes(Location.A3_TOWN_START, Location.A3_STASH_WP, char) # display_all_nodes(pather, filter="TRAV") diff --git a/src/run/__init__.py b/src/run/__init__.py new file mode 100644 index 000000000..e0e4b3da7 --- /dev/null +++ b/src/run/__init__.py @@ -0,0 +1,3 @@ +from run.pindle import Pindle +from run.shenk_eld import ShenkEld +from run.trav import Trav diff --git a/src/run/pindle.py b/src/run/pindle.py new file mode 100644 index 000000000..e6d73714c --- /dev/null +++ b/src/run/pindle.py @@ -0,0 +1,57 @@ +from char.i_char import IChar +from config import Config +from logger import Logger +from pather import Location, Pather +from typing import Union +from pickit import PickIt +from template_finder import TemplateFinder +from town.town_manager import TownManager +from ui_manager import UiManager +from utils.misc import wait + + +class Pindle: + def __init__( + self, + template_finder: TemplateFinder, + pather: Pather, + town_manager: TownManager, + ui_manager: UiManager, + char: IChar, + pickit: PickIt + ): + self._config = Config() + self._template_finder = template_finder + self._pather = pather + self._town_manager = town_manager + self._ui_manager = ui_manager + self._char = char + self._pickit = pickit + + def run(self, start_loc: Location, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]: + # Go through Red Portal in A5 + Logger.info("Run Pindle") + loc = self._town_manager.go_to_act(5, start_loc) + if not loc: + return False + if not self._pather.traverse_nodes(loc, Location.A5_NIHLATHAK_PORTAL, self._char): + return False + wait(0.4) + found_loading_screen_func = lambda: self._ui_manager.wait_for_loading_screen(2.0) + + # Kill Pindle + if not self._char.select_by_template(["A5_RED_PORTAL", "A5_RED_PORTAL_TEXT"], found_loading_screen_func): + return False + if not self._template_finder.search_and_wait(["PINDLE_0", "PINDLE_1"], threshold=0.65, time_out=20).valid: + return False + if do_pre_buff: + self._char.pre_buff() + 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.A5_PINDLE_START, Location.A5_PINDLE_SAVE_DIST, self._char): + return False + self._char.kill_pindle() + wait(0.2, 0.3) + picked_up_items = self._pickit.pick_up_items(self._char) + return (Location.A5_PINDLE_END, picked_up_items) diff --git a/src/run/shenk_eld.py b/src/run/shenk_eld.py new file mode 100644 index 000000000..68e91cd57 --- /dev/null +++ b/src/run/shenk_eld.py @@ -0,0 +1,65 @@ +from char.i_char import IChar +from config import Config +from logger import Logger +from pather import Location, Pather +from typing import Union +from pickit import PickIt +from template_finder import TemplateFinder +from town.town_manager import TownManager +from ui_manager import UiManager +from utils.misc import wait + + +class ShenkEld: + def __init__( + self, + template_finder: TemplateFinder, + pather: Pather, + town_manager: TownManager, + ui_manager: UiManager, + char: IChar, + pickit: PickIt + ): + self._config = Config() + self._template_finder = template_finder + self._pather = pather + self._town_manager = town_manager + self._ui_manager = ui_manager + self._char = char + self._pickit = pickit + + def run(self, start_loc: Location, do_shenk: bool, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]: + Logger.info("Run Eldritch") + # Go to Frigid Highlands + if not self._town_manager.open_wp(start_loc): + return False + wait(0.4) + self._ui_manager.use_wp(5, 1) + + # Eldritch + if not self._template_finder.search_and_wait(["ELDRITCH_0", "ELDRITCH_START"], threshold=0.65, time_out=20).valid: + return False + if do_pre_buff: + self._char.pre_buff() + 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.A5_ELDRITCH_START, Location.A5_ELDRITCH_SAVE_DIST, self._char): + return False + self._char.kill_eldritch() + loc = Location.A5_ELDRITCH_END + wait(0.2, 0.3) + picked_up_items = self._pickit.pick_up_items(self._char) + + # Shenk + if do_shenk: + Logger.info("Run Shenk") + 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() + loc = Location.A5_SHENK_END + wait(1.9, 2.4) # sometimes merc needs some more time to kill shenk... + picked_up_items |= self._pickit.pick_up_items(self._char) + + return (loc, picked_up_items) diff --git a/src/run/trav.py b/src/run/trav.py new file mode 100644 index 000000000..d7da3724f --- /dev/null +++ b/src/run/trav.py @@ -0,0 +1,60 @@ +from char.i_char import IChar +from config import Config +from logger import Logger +from pather import Location, Pather +from typing import Union +from pickit import PickIt +from template_finder import TemplateFinder +from town.town_manager import TownManager +from ui_manager import UiManager +from utils.misc import wait + + +class Trav: + def __init__( + self, + template_finder: TemplateFinder, + pather: Pather, + town_manager: TownManager, + ui_manager: UiManager, + char: IChar, + pickit: PickIt + ): + self._config = Config() + self._template_finder = template_finder + self._pather = pather + self._town_manager = town_manager + self._ui_manager = ui_manager + self._char = char + self._pickit = pickit + + def run(self, start_loc: Location, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]: + # Go to Travincal via waypoint + Logger.info("Run Trav") + if not self._char.can_teleport(): + Logger.error("Trav is currently only supported for teleporting builds. Skipping trav") + return True + if not self._town_manager.open_wp(start_loc): + return False + wait(0.4) + self._ui_manager.use_wp(3, 7) + + # Kill Council + if not self._template_finder.search_and_wait(["TRAV_0", "TRAV_1"], threshold=0.65, time_out=20).valid: + return False + if do_pre_buff: + self._char.pre_buff() + self._pather.traverse_nodes_fixed("trav_save_dist", self._char) + self._char.kill_council() + picked_up_items = self._pickit.pick_up_items(self._char) + if not picked_up_items: + # in trav many items can drop that we might not see all, move to the left a bit + x_m, y_m = self._char._screen.convert_abs_to_monitor((-120, -70)) + self._char.pre_move() + self._char.move((x_m, y_m), force_move=True, force_tp=True) + picked_up_items = self._pickit.pick_up_items(self._char) + + # Move back to center to avoid hidden tps + self._pather.traverse_nodes(Location.A3_TRAV_SAVE_DIST, Location.A3_TRAV_SAVE_DIST, self._char, time_out=3) + + return (Location.A3_TRAV_END, picked_up_items) diff --git a/src/town/a3.py b/src/town/a3.py index 3d4073916..266f6a0f3 100644 --- a/src/town/a3.py +++ b/src/town/a3.py @@ -39,7 +39,7 @@ def stash_is_open_func(): 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 + if not self._pather.traverse_nodes(curr_loc, Location.A3_STASH_WP, self._char, force_move=True): 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) diff --git a/src/ui_manager.py b/src/ui_manager.py index df4f37c78..93bf4d9d7 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -449,6 +449,10 @@ def has_tps(self) -> bool: threshold=0.79, time_out=4 ) + if not template_match.valid: + Logger.warning("You are out of tps") + if self._config.general["info_screenshots"]: + cv2.imwrite("./info_screenshots/debug_out_of_tps_" + time.strftime("%Y%m%d_%H%M%S") + ".png", self._screen.grab()) return template_match.valid else: return False