From 70debc2b0dbbdae5f6f2208d2bb7a3a8252af046 Mon Sep 17 00:00:00 2001 From: jobithu <95491220+jobithu@users.noreply.github.com> Date: Sun, 12 Jun 2022 21:44:54 +0200 Subject: [PATCH] Major feature: FOHdin, Chaos Sanctuary (#765) * Setting Up FOHdin for CS * replaced hammers with FOH, but not targeted yet * finish merging * demo * implemented all runs. avoid nihlatak. * replaced FOH with Hammer for Nihl * using hammers for dia now * reverted dia hammers to FOH, so pure FOH runs CS * added non mob-detection attacks to trav * some tweaks * tweaked hammer pindle for tele amu * improved pindle for walker FOHdin * Pindle, Eld, Shenk, CS tweaked. Trav needs work. * fix NoneType error if nodes aren't found * consolidate trav code * small adjustment * increase attack duration in loop condition rather than attack sequence * pindle optimizations * optimizations of pindle, eld, shenk * more optimizations, trav still kinda ghetto * update target detect to include closer targets * implement concentration with holy bolt * nihlathak and summoner * revised CS trash atk pattern * tweaked sealbosses! * CS ready for pressure tests * implement in-chaos pickit, match/case logic over if/elif, consolidate more functions, move some to i_char.py, patch health_manager.py to prevent it from closing inventory, update inspect_items() to allow items to be dropped and not kept to be sold * revert change * reserve hammerdin changes for a separate PR * get rid of extra attack sequence * disable target/attack logs for merge * code cleanup Co-authored-by: mgleed --- README.md | 6 +- config/game.ini | 1 + config/params.ini | 17 +- src/bot.py | 7 +- src/char/i_char.py | 50 +- src/char/paladin/__init__.py | 3 + src/char/paladin/fohdin.py | 762 ++++++++++++++++++++++++++++ src/char/{ => paladin}/hammerdin.py | 25 +- src/char/paladin/paladin.py | 108 ++++ src/config.py | 13 +- src/health_manager.py | 17 +- src/inventory/personal.py | 4 +- src/item/pickit.py | 3 +- src/pather.py | 2 +- src/run/diablo.py | 10 +- src/target_detect.py | 65 ++- src/town/town_manager.py | 2 +- src/utils/node_recorder.py | 2 +- 18 files changed, 1035 insertions(+), 62 deletions(-) create mode 100644 src/char/paladin/__init__.py create mode 100644 src/char/paladin/fohdin.py rename src/char/{ => paladin}/hammerdin.py (99%) create mode 100644 src/char/paladin/paladin.py diff --git a/README.md b/README.md index b86b2e4..952387a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Check out the [development.md](development.md) docu for infos on how to build fr ## Support this project 💓 -You can support this project by giving feedback, reporting bugs, or creating pull requests. +You can support this project by giving feedback, reporting bugs, or creating pull requests. Contributions are welcome, and we encourage you to contribute to this project if you would like to help out. Botty is a open source project and almost excessively maintained by contributors (there has been 50+ contributors! <3). In our [**discord**](https://discord.gg/Jf3J8cuXWg) there is a contributor role, and you can ping one of the admins and ask for the role to talk to other contributors! Though you don't need to be in the [**discord**](https://discord.gg/Jf3J8cuXWg) to contribute, we do encourage you to do so. ## Graphic Debugger @@ -49,7 +49,7 @@ All botty configuration files are located in the __config__ folder. To ease the name=MyCustomName [routes] -order=run_pindle, run_eldritch +order=run_pindle, run_eldritch ``` | [general] | Descriptions | @@ -108,7 +108,7 @@ order=run_pindle, run_eldritch | atk_len_diablo_infector | Attack length for hdin or number of attack sequences when fighting Sealboss C "Infector of Souls" in Chaos Sanctuary (Diablo) | | atk_len_diablo_infector | Attack length for hdin or number of attack sequences when fighting Diablo in Chaos Sanctuary | | kill_cs_trash | If 1, most Trash mob packs from Chaos Sancturay Entrance to Pentagram are cleared. If 0, the run starts at Pentagram and just kills Sealbosses & Diablo (default) | -| cs_town_visits | If 1, it will go to town, buy pots & stash items between clearing the seals, set 0 to deactivate (default) | +| cs_town_visits | CURRENTLY BROKEN, LEAVE AT 0 FOR NOW | | cs_mob_detect | If 1, it will attempt to use holy freeze from merc / conviction aura / poison to detect nearby mobs to help speed-up CS run. | take_health_potion | Health percentage when healing potion will be used. e.g. 0.6 = 60% helath | | take_mana_potion | Mana percentage when mana potion will be used | diff --git a/config/game.ini b/config/game.ini index df8abbc..64e269b 100644 --- a/config/game.ini +++ b/config/game.ini @@ -209,6 +209,7 @@ dia_a2y_sealfake_sealboss=1194, 57 dia_a2y_hop_622=1020,147 dia_b1s_layout2_seal=1125,102, 1125,102, 1125,102, 1171,576, 651,584 dia_b1s_seal_deseis=302,128, 267,193, 395,502 +dia_b1s_seal_deseis_foh=302,128, 267,193 dia_c1f_651_654=1250,328, 1250,328 dia_c1f_654_651=100,413, 100,413 dia_c1f_651_652=1261,402 diff --git a/config/params.ini b/config/params.ini index e193607..94e2bf8 100644 --- a/config/params.ini +++ b/config/params.ini @@ -157,15 +157,26 @@ hydra=f2 ; ========================= ; ==== Builds: Paladin ==== ; ========================= -[hammerdin] +[paladin] teleport= -concentration= holy_shield= -blessed_hammer= vigor= redemption= cleansing= +[fohdin] +; foh must be left skill, hotkey required +foh= +holy_bolt= +conviction= +concentration= +blessed_hammer= + +[hammerdin] +concentration= +blessed_hammer= + + ; ========================== ; ==== Builds: Assassin ==== ; ========================== diff --git a/src/bot.py b/src/bot.py index ee24ed3..4cc5dad 100644 --- a/src/bot.py +++ b/src/bot.py @@ -24,7 +24,8 @@ from item.pickit import PickIt from pather import Pather, Location from char.sorceress import LightSorc, BlizzSorc, NovaSorc,HydraSorc from char.trapsin import Trapsin -from char.hammerdin import Hammerdin +from char.paladin.hammerdin import Hammerdin +from char.paladin import FoHdin from char.barbarian import Barbarian from char.necro import Necro from char.poison_necro import Poison_Necro @@ -61,8 +62,10 @@ class Bot: self._char: IChar = NovaSorc(Config().nova_sorc, self._pather) case "hydra_sorc": self._char: IChar = HydraSorc(Config().hydra_sorc, self._pather) - case "hammerdin": + case "hammerdin" | "paladin": self._char: IChar = Hammerdin(Config().hammerdin, self._pather, self._pickit) #pickit added for diablo + case "fohdin": + self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit) #pickit added for diablo case "trapsin": self._char: IChar = Trapsin(Config().trapsin, self._pather) case "barbarian": diff --git a/src/char/i_char.py b/src/char/i_char.py index ca5ea85..3735317 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -29,6 +29,35 @@ class IChar: self._cast_duration = Config().char["casting_frames"] * 0.04 + 0.01 self.damage_scaling = float(Config().char.get("damage_scaling", 1.0)) self.capabilities = None + self._active_skill = { + "left": "", + "right": "" + } + + def _set_active_skill(self, mouse_click_type: str = "left", skill: str =""): + self._active_skill[mouse_click_type] = skill + + def _select_skill(self, skill: str, mouse_click_type: str = "left", delay: float | list | tuple = None): + if not ( + skill in self._skill_hotkeys and (hotkey := self._skill_hotkeys[skill]) + or (skill in Config().char and (hotkey := Config().char[skill])) + ): + Logger.warning(f"No hotkey for skill: {skill}") + self._set_active_skill(mouse_click_type, "") + return False + + if self._active_skill[mouse_click_type] != skill: + keyboard.send(hotkey) + self._set_active_skill(mouse_click_type, skill) + if delay: + try: + wait(*delay) + except: + try: + wait(delay) + except Exception as e: + Logger.warning(f"_select_skill: Failed to delay with delay: {delay}. Exception: {e}") + return True def _discover_capabilities(self) -> CharacterCapabilities: override = Config().advanced_options["override_capabilities"] @@ -148,12 +177,18 @@ class IChar: # if teleport hotkey is set and if teleport is not already selected if self.capabilities.can_teleport_natively: self.select_tp() + self._set_active_skill("right", "teleport") def move(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False): factor = Config().advanced_options["pathing_delay_factor"] - if self._skill_hotkeys["teleport"] and \ - (force_tp or (skills.is_right_skill_selected(["TELE_ACTIVE"]) and \ - skills.is_right_skill_active())): + if "teleport" in self._skill_hotkeys and self._skill_hotkeys["teleport"] and ( + force_tp + or ( + skills.is_right_skill_selected(["TELE_ACTIVE"]) + and skills.is_right_skill_active() + ) + ): + self._set_active_skill("right", "teleport") mouse.move(pos_monitor[0], pos_monitor[1], randomize=3, delay_factor=[factor*0.1, factor*0.14]) wait(0.012, 0.02) mouse.click(button="right") @@ -244,8 +279,7 @@ class IChar: while time.time() - start < 4: keyboard.send(Config().char["weapon_switch"]) wait(0.3, 0.35) - keyboard.send(Config().char["battle_command"]) - wait(0.1, 0.19) + self._select_skill(skill = "battle_command", mouse_click_type="right", delay=(0.1, 0.2)) if skills.is_right_skill_selected(["BC", "BO"]): switch_sucess = True break @@ -257,8 +291,7 @@ class IChar: # We switched succesfully, let's pre buff mouse.click(button="right") wait(self._cast_duration + 0.16, self._cast_duration + 0.18) - keyboard.send(Config().char["battle_orders"]) - wait(0.1, 0.19) + self._select_skill(skill = "battle_orders", mouse_click_type="right", delay=(0.1, 0.2)) mouse.click(button="right") wait(self._cast_duration + 0.16, self._cast_duration + 0.18) @@ -290,8 +323,7 @@ class IChar: if not self._skill_hotkeys[ability]: raise ValueError(f"You did not set {ability} hotkey!") keyboard.send(Config().char["stand_still"], do_release=False) - keyboard.send(self._skill_hotkeys[ability]) - wait(0.02, 0.08) + self._select_skill(skill = ability, mouse_click_type="right", delay=(0.02, 0.08)) target = self.vec_to_monitor(arc_spread(cast_pos_abs, spread_deg=spread_deg)) mouse.move(*target,delay_factor=[0.95, 1.05]) diff --git a/src/char/paladin/__init__.py b/src/char/paladin/__init__.py new file mode 100644 index 0000000..3ff9111 --- /dev/null +++ b/src/char/paladin/__init__.py @@ -0,0 +1,3 @@ +from .paladin import Paladin +from .fohdin import FoHdin +from .hammerdin import Hammerdin \ No newline at end of file diff --git a/src/char/paladin/fohdin.py b/src/char/paladin/fohdin.py new file mode 100644 index 0000000..99fe022 --- /dev/null +++ b/src/char/paladin/fohdin.py @@ -0,0 +1,762 @@ +import random +import keyboard +import time +import numpy as np + +from health_manager import get_panel_check_paused, set_panel_check_paused +from inventory.personal import inspect_items +from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen +from utils.custom_mouse import mouse +from char.paladin import Paladin +from logger import Logger +from config import Config +from utils.misc import wait +from pather import Location +from target_detect import get_visible_targets, TargetInfo, log_targets + +class FoHdin(Paladin): + def __init__(self, *args, **kwargs): + Logger.info("Setting up FoHdin") + super().__init__(*args, **kwargs) + self._pather.adapt_path((Location.A3_TRAV_START, Location.A3_TRAV_CENTER_STAIRS), [220, 221, 222, 903, 904, 905, 906]) + + + def _cast_foh(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "conviction"): + return self._cast_skill_with_aura(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura) + + def _cast_holy_bolt(self, cast_pos_abs: tuple[float, float], spray: int = 10, min_duration: float = 0, aura: str = "concentration"): + #if skill is bound : concentration, use concentration, otherwise move on with conviction. alternatively use redemption whilst holybolting. conviction does not help holy bolt (its magic damage) + return self._cast_skill_with_aura(skill_name = "holy_bolt", cast_pos_abs = cast_pos_abs, spray = spray, min_duration = min_duration, aura = aura) + + def _cast_hammers(self, min_duration: float = 0, aura: str = "concentration"): #for nihlathak + return self._cast_skill_with_aura(skill_name = "blessed_hammer", spray = 0, min_duration = min_duration, aura = aura) + + def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float, aura: str = "concentration"): #for nihalthak + pos_m = convert_abs_to_monitor(abs_move) + self.pre_move() + self.move(pos_m, force_move=True) + self._cast_hammers(atk_len, aura=aura) + + def _generic_foh_attack_sequence( + self, + default_target_abs: tuple[int, int] = (0, 0), + min_duration: float = 0, + max_duration: float = 15, + foh_to_holy_bolt_ratio: int = 3, + target_detect: bool = True, + default_spray: int = 50, + aura: str = "" + ) -> bool: + start = time.time() + target_check_count = 1 + foh_aura = aura if aura else "conviction" + holy_bolt_aura = aura if aura else "concentration" + while (elapsed := (time.time() - start)) <= max_duration: + cast_pos_abs = default_target_abs + spray = default_spray + # if targets are detected, switch to targeting with reduced spread rather than present default cast position and default spread + if target_detect and (targets := get_visible_targets()): + # log_targets(targets) + spray = 5 + cast_pos_abs = targets[0].center_abs + + # TODO: add delay between FOH casts--doesn't properly cast each FOH in sequence + + # cast foh to holy bolt with preset ratio (e.g. 3 foh followed by 1 holy bolt if foh_to_holy_bolt_ratio = 3) + if foh_to_holy_bolt_ratio > 0 and not target_check_count % (foh_to_holy_bolt_ratio + 1): + self._cast_holy_bolt(cast_pos_abs, spray=spray, aura=holy_bolt_aura) + else: + self._cast_foh(cast_pos_abs, spray=spray, aura=foh_aura) + + # if time > minimum and either targets aren't set or targets don't exist, exit loop + if elapsed > min_duration and (not target_detect or not targets): + break + target_check_count += 1 + return True + + #FOHdin Attack Sequence Optimized for trash + def _cs_attack_sequence(self, min_duration: float = Config().char["atk_len_cs_trashmobs"], max_duration: float = Config().char["atk_len_cs_trashmobs"] * 3): + self._generic_foh_attack_sequence(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=100, foh_to_holy_bolt_ratio=6) + self._activate_redemption_aura() + + def _cs_trash_mobs_attack_sequence(self, min_duration: float = 1.2, max_duration: float = Config().char["atk_len_cs_trashmobs"]): + self._cs_attack_sequence(min_duration = min_duration, max_duration = max_duration) + + def _cs_pickit(self, skip_inspect: bool = False): + new_items = self._pickit.pick_up_items(self) + self._picked_up_items |= new_items + if not skip_inspect and new_items: + set_panel_check_paused(True) + inspect_items(grab(), ignore_sell=True) + set_panel_check_paused(False) + + + def kill_pindle(self) -> bool: + atk_len_dur = float(Config().char["atk_len_pindle"]) + pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0]) + + if self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges: + # Slightly retreating, so the Merc gets charged + if not self._pather.traverse_nodes([102], self, timeout=1.0, do_pre_move=self._do_pre_move, force_move=True,force_tp=False, use_tp_charge=False): + return False + # Doing one Teleport to safe_dist to grab our Merc + Logger.debug("Teleporting backwards to let Pindle charge the MERC. Looks strange, but is intended!") #I would leave this message in, so users dont complain that there is a strange movement pattern. + if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=self._do_pre_move, force_tp=True, use_tp_charge=True): + return False + # Slightly retreating, so the Merc gets charged + if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=self._do_pre_move, force_move=True, force_tp=False, use_tp_charge=False): + return False + else: + if not self._do_pre_move: + keyboard.send(self._skill_hotkeys["conviction"]) + wait(0.05, 0.15) + self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=self._do_pre_move) + + cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9] + self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=11) + + if self.capabilities.can_teleport_natively: + self._pather.traverse_nodes_fixed("pindle_end", self) + else: + if not self._do_pre_move: + keyboard.send(self._skill_hotkeys["redemption"]) + wait(0.05, 0.15) + self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=self._do_pre_move) + + # Use target-based attack sequence one more time before pickit + self._generic_foh_attack_sequence(default_target_abs=cast_pos_abs, max_duration=atk_len_dur, default_spray=11) + self._activate_cleanse_redemption() + + return True + + + def kill_council(self) -> bool: + atk_len_dur = float(Config().char["atk_len_trav"]) + + # traverse to nodes and attack + nodes = [225, 226, 300] + for node in nodes: + self._pather.traverse_nodes([node], self, timeout=2, force_tp=True) + default_target_abs = self._pather.find_abs_node_pos(node, img := grab()) or self._pather.find_abs_node_pos(906, img) or (-50, -50) + self._generic_foh_attack_sequence(default_target_abs=default_target_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=80) + + # return to 226 and prepare for pickit + self._pather.traverse_nodes([226], self, timeout=2, force_tp=True) + default_target_abs = self._pather.find_abs_node_pos(226, img := grab()) or self._pather.find_abs_node_pos(906, img) or (-50, -50) + self._generic_foh_attack_sequence(default_target_abs=default_target_abs, max_duration=atk_len_dur*3, default_spray=80) + + self._activate_cleanse_redemption() + + return True + + def kill_eldritch(self) -> bool: + eld_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0]) + atk_len_dur = float(Config().char["atk_len_eldritch"]) + + self._generic_foh_attack_sequence(default_target_abs=eld_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, default_spray=70) + + # move to end node + pos_m = convert_abs_to_monitor((70, -200)) + self.pre_move() + self.move(pos_m, force_move=True) + self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.1) + + # check mobs one more time before pickit + self._generic_foh_attack_sequence(default_target_abs=eld_pos_abs, max_duration=atk_len_dur, default_spray=70) + self._activate_cleanse_redemption() + + return True + + + def kill_shenk(self): + atk_len_dur = float(Config().char["atk_len_shenk"]) + + # traverse to shenk + if not self._do_pre_move: + keyboard.send(self._skill_hotkeys["conviction"]) + wait(0.05, 0.15) + self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=self._do_pre_move, force_tp=True, use_tp_charge=True) + wait(0.05, 0.1) + + # bypass mob detect first + self._cast_foh((0, 0), spray=11, min_duration = 2, aura = "conviction") + # then do generic mob detect sequence + diff = atk_len_dur if atk_len_dur <= 2 else (atk_len_dur - 2) + self._generic_foh_attack_sequence(min_duration=atk_len_dur - diff, max_duration=atk_len_dur*3 - diff, default_spray=10, target_detect=False) + self._activate_cleanse_redemption() + + return True + + + def kill_nihlathak(self, end_nodes: list[int]) -> bool: + atk_len_dur = Config().char["atk_len_nihlathak"] + # Move close to nihlathak + self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False) + if self._select_skill("blessed_hammer"): + self._cast_hammers(atk_len_dur/4) + self._cast_hammers(2*atk_len_dur/4, "redemption") + self._move_and_attack((30, 15), atk_len_dur/4, "redemption") + else: + Logger.warning("FOHDin without blessed hammer is not very strong vs. Nihlathak!") + self._generic_foh_attack_sequence(min_duration=atk_len_dur/2, max_duration=atk_len_dur, default_spray=70, aura="redemption") + self._generic_foh_attack_sequence(min_duration=atk_len_dur/2, max_duration=atk_len_dur, default_spray=70, aura="redemption") + self._generic_foh_attack_sequence(max_duration=atk_len_dur*2, default_spray=70) + self._activate_cleanse_redemption() + return True + + def kill_summoner(self) -> bool: + # Attack + atk_len_dur = Config().char["atk_len_arc"] + self._generic_foh_attack_sequence(min_duration=atk_len_dur, max_duration=atk_len_dur*2, default_spray=80) + self._activate_cleanse_redemption() + return True + + + ######################################################################################## + # Chaos Sanctuary, Trash, Seal Bosses (a = Vizier, b = De Seis, c = Infector) & Diablo # + ######################################################################################## + + def kill_cs_trash(self, location:str) -> bool: + + ########### + # SEALDANCE + ########### + + #these locations have no traverses and are basically identical. + #if location in ("sealdance", "rof_01", "rof_02", "entrance_hall_01", "entrance_hall_02", "entrance1_01", "entrance1_02", "entrance1_03", "entrance1_04", "entrance2_01", "entrance2_03"): + + match location: + case "sealdance": #if seal opening fails & trash needs to be cleared -> used at ANY seal + ### APPROACH + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + + + ################ + # CLEAR CS TRASH + ################ + + case "rof_01": #node 603 - outside CS in ROF + ### APPROACH ### + if not self._pather.traverse_nodes([603], self, timeout=3): return False #calibrate after static path + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([603], self): return False #calibrate after looting + + + case "rof_02": #node 604 - inside ROF + ### APPROACH ### + if not self._pather.traverse_nodes([604], self, timeout=3): return False #threshold=0.8 (ex 601) + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + case "entrance_hall_01": ##static_path "diablo_entrance_hall_1", node 677, CS Entrance Hall1 + ### APPROACH ### + self._pather.traverse_nodes_fixed("diablo_entrance_hall_1", self) # 604 -> 671 Hall1 + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + case "entrance_hall_02": #node 670,671, CS Entrance Hall1, CS Entrance Hall1 + ### APPROACH ### + if not self._pather.traverse_nodes([670], self): return False # pull top mobs 672 to bottom 670 + self._pather.traverse_nodes_fixed("diablo_entrance_1_670_672", self) # 604 -> 671 Hall1 + if not self._pather.traverse_nodes([670], self): return False # pull top mobs 672 to bottom 670 + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([671], self): return False # calibrate before static path + self._pather.traverse_nodes_fixed("diablo_entrance_hall_2", self) # 671 -> LC Hall2 + + + + # TRASH LAYOUT A + + case "entrance1_01": #static_path "diablo_entrance_hall_2", Hall1 (before layout check) + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([673], self): return False # , timeout=3): # Re-adjust itself and continues to attack + + case "entrance1_02": #node 673 + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + self._pather.traverse_nodes_fixed("diablo_entrance_1_1", self) # Moves char to postion close to node 674 continues to attack + if not self._pather.traverse_nodes([674], self): return False#, timeout=3) + + case "entrance1_03": #node 674 + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([675], self): return False#, timeout=3) # Re-adjust itself + self._pather.traverse_nodes_fixed("diablo_entrance_1_1", self) #static path to get to be able to spot 676 + if not self._pather.traverse_nodes([676], self): return False#, timeout=3) + + case "entrance1_04": #node 676- Hall3 + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + # TRASH LAYOUT B + + case "entrance2_01": #static_path "diablo_entrance_hall_2" + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + case "entrance2_02": #node 682 + ### APPROACH ### + #if not self._pather.traverse_nodes([682], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + case "entrance2_03": #node 683 + ### APPROACH ### + #if not self._pather.traverse_nodes([682], self): return False # , timeout=3): + #self._pather.traverse_nodes_fixed("diablo_entrance2_1", self) + #if not self._pather.traverse_nodes([683], self): return False # , timeout=3): + self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_top1", self) #pull mobs from top + wait (0.2, 0.5) + self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_top2", self) #pull mobs from top + if not self._pather.traverse_nodes([605], self): return False#, timeout=3) + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + case "entrance2_04": #node 686 - Hall3 + ### APPROACH ### + if not self._pather.traverse_nodes([605], self): return False#, timeout=3) + #if not self._pather.traverse_nodes([683,684], self): return False#, timeout=3) + #self._pather.traverse_nodes_fixed("diablo_entrance2_2", self) + #if not self._pather.traverse_nodes([685,686], self): return False#, timeout=3) + self._pather.traverse_nodes_fixed("diablo_trash_b_hall2_605_hall3", self) + if not self._pather.traverse_nodes([609], self): return False#, timeout=3) + self._pather.traverse_nodes_fixed("diablo_trash_b_hall3_pull_609", self) + if not self._pather.traverse_nodes([609], self): return False#, timeout=3) + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit(skip_inspect=True) + if not self._pather.traverse_nodes([609], self): return False#, timeout=3) + self._cs_pickit() + if not self._pather.traverse_nodes([609], self): return False#, timeout=3) + + #################### + # PENT TRASH TO SEAL + #################### + + case "dia_trash_a" | "dia_trash_b" | "dia_trash_c": #trash before between Pentagramm and Seal A Layoutcheck + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + ############### + # LAYOUT CHECKS + ############### + + case "layoutcheck_a" | "layoutcheck_b" | "layoutcheck_c": #layout check seal A, node 619 A1-L, node 620 A2-Y + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + ################## + # PENT BEFORE SEAL + ################## + + case "pent_before_a": #node 602, pentagram, before CTA buff & depature to layout check - not needed when trash is skipped & seals run in right order + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.") + + case "pent_before_b" | "pent_before_c": #node 602, pentagram, before CTA buff & depature to layout check + ### APPROACH ### + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + ########### + # SEAL A1-L + ########### + + case "A1-L_01": #node 611 seal layout A1-L: safe_dist + ### APPROACH ### + if not self._pather.traverse_nodes([611], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + # we loot at boss + + case "A1-L_02": #node 612 seal layout A1-L: center + ### APPROACH ### + if not self._pather.traverse_nodes([612], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + # we loot at boss + + case "A1-L_03": #node 613 seal layout A1-L: fake_seal + ### APPROACH ### + if not self._pather.traverse_nodes([613], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + + case "A1-L_seal1": #node 613 seal layout A1-L: fake_seal + ### APPROACH ### + self._cs_pickit() + if not self._pather.traverse_nodes([614], self): return False + ### ATTACK ### + self._activate_redemption_aura() + ### LOOT ### + # we loot at boss + + case "A1-L_seal2": #node 614 seal layout A1-L: boss_seal + ### APPROACH ### + if not self._pather.traverse_nodes([613, 615], self): return False # , timeout=3): + ### ATTACK ### + self._activate_redemption_aura() + ### LOOT ### + # we loot at boss + + ########### + # SEAL A2-Y + ########### + + case "A2-Y_01": #node 622 seal layout A2-Y: safe_dist + ### APPROACH ### + if not self._pather.traverse_nodes_fixed("dia_a2y_hop_622", self): return False + Logger.debug("A2-Y: Hop!") + #if not self._pather.traverse_nodes([622], self): return False # , timeout=3): + if not self._pather.traverse_nodes([622], self): return False + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + # we loot at boss + + case "A2-Y_02": #node 623 seal layout A2-Y: center + ### APPROACH ### + # if not self._pather.traverse_nodes([623,624], self): return False # + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + # we loot at boss + + case "A2-Y_03": #skipped + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.") + + case "A2-Y_seal1": #node 625 seal layout A2-Y: fake seal + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + if not self._pather.traverse_nodes([625], self): return False # , timeout=3): + self._activate_redemption_aura() + + case "A2-Y_seal2": + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + self._pather.traverse_nodes_fixed("dia_a2y_sealfake_sealboss", self) #instead of traversing node 626 which causes issues + self._activate_redemption_aura() + + ########### + # SEAL B1-S + ########### + + case "B1-S_01" | "B1-S_02" | "B1-S_03": + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.") + + case "B1-S_seal2": #B only has 1 seal, which is the boss seal = seal2 + ### APPROACH ### + if not self._pather.traverse_nodes([634], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + + + ########### + # SEAL B2-U + ########### + + case "B2-U_01" | "B2-U_02" | "B2-U_03": + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.") + + case "B2-U_seal2": #B only has 1 seal, which is the boss seal = seal2 + ### APPROACH ### + self._pather.traverse_nodes_fixed("dia_b2u_bold_seal", self) + if not self._pather.traverse_nodes([644], self): return False # , timeout=3): + ### ATTACK ### + self._activate_redemption_aura() + ### LOOT ### + # we loot at boss + + + ########### + # SEAL C1-F + ########### + + case "C1-F_01" | "C1-F_02" | "C1-F_03": + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.") + + case "C1-F_seal1": + ### APPROACH ### + wait(0.1,0.3) + self._pather.traverse_nodes_fixed("dia_c1f_hop_fakeseal", self) + wait(0.1,0.3) + if not self._pather.traverse_nodes([655], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([655], self): return False # , timeout=3): + self._activate_redemption_aura() + + case "C1-F_seal2": + ### APPROACH ### + self._pather.traverse_nodes_fixed("dia_c1f_654_651", self) + if not self._pather.traverse_nodes([652], self): return False # , timeout=3): + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([652], self): return False # , timeout=3): + self._activate_redemption_aura() + + ########### + # SEAL C2-G + ########### + + case "C2-G_01" | "C2-G_02" | "C2-G_03": #skipped + ### APPROACH ### + ### ATTACK ### + ### LOOT ### + # we loot at boss + Logger.debug("No attack choreography available in fohdin.py for this node " + location + " - skipping to shorten run.") + + case "C2-G_seal1": + ### APPROACH ### + if not self._pather.traverse_nodes([663, 662], self) or not self._pather.traverse_nodes_fixed("dia_c2g_lc_661", self): + return False + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + if not self._pather.traverse_nodes([662], self): return False + self._activate_redemption_aura() + + + case "C2-G_seal2": + ### APPROACH ### + # Killing infector here, because for C2G its the only seal where a bossfight occures BETWEEN opening seals + seal_layout="C2-G" + if not self._pather.traverse_nodes([662], self) or not self._pather.traverse_nodes_fixed("dia_c2g_663", self): + return False + ### ATTACK ### + atk_dur_min = Config().char["atk_len_diablo_infector"] + atk_dur_max = atk_dur_min * 3 + Logger.debug(seal_layout + ": Attacking Infector at position 1/2") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + if not self._pather.traverse_nodes([663], self): return False # , timeout=3): + Logger.debug(seal_layout + ": Attacking Infector at position 2/2") + self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max) + ### LOOT ### + self._cs_pickit(skip_inspect=True) # inspect on other + if not self._pather.traverse_nodes([664, 665], self): return False # , timeout=3): + + case _: + ### APPROACH ### + Logger.error("No location argument given for kill_cs_trash(" + location + "), should not happen") + ### ATTACK ### + self._cs_trash_mobs_attack_sequence() + ### LOOT ### + self._cs_pickit() + return True + + + def kill_vizier(self, seal_layout:str) -> bool: + atk_dur_min = Config().char["atk_len_diablo_vizier"] + atk_dur_max = atk_dur_min * 3 + match seal_layout: + case "A1-L": + ### APPROACH ### + if not self._pather.traverse_nodes([612], self): return False # , timeout=3): + ### ATTACK ### + Logger.debug(seal_layout + ": Attacking Vizier at position 1/2") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + Logger.debug(seal_layout + ": Attacking Vizier at position 2/2") + self._pather.traverse_nodes([611], self, timeout=3) + self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max) + ### LOOT ### + self._cs_pickit(skip_inspect=True) + if not self._pather.traverse_nodes([612], self): return False # , timeout=3): + self._activate_cleanse_redemption() + self._cs_pickit() + if not self._pather.traverse_nodes([612], self): return False # , timeout=3): # recalibrate after loot + + case "A2-Y": + ### APPROACH ### + if not self._pather.traverse_nodes([627, 622], self): return False # , timeout=3): + ### ATTACK ### + Logger.debug(seal_layout + ": Attacking Vizier at position 1/3") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + Logger.debug(seal_layout + ": Attacking Vizier at position 2/3") + self._pather.traverse_nodes([623], self, timeout=3) + self._cs_attack_sequence(min_duration=1.5, max_duration=atk_dur_max) + Logger.debug(seal_layout + ": Attacking Vizier at position 3/3") + if not self._pather.traverse_nodes([624], self): return False + self._cs_attack_sequence(min_duration=1.5, max_duration=atk_dur_max) + ### LOOT ### + self._cs_pickit(skip_inspect=True) + if not self._pather.traverse_nodes([624], self): return False + if not self._pather.traverse_nodes_fixed("dia_a2y_hop_622", self): return False + Logger.debug(seal_layout + ": Hop!") + self._activate_cleanse_redemption() + if not self._pather.traverse_nodes([622], self): return False #, timeout=3): + self._activate_redemption_aura() + self._cs_pickit() + if not self._pather.traverse_nodes([622], self): return False # , timeout=3): #recalibrate after loot + + case _: + Logger.warning(seal_layout + ": Invalid location for kill_vizier("+ seal_layout +"), should not happen.") + return False + return True + + + def kill_deseis(self, seal_layout:str) -> bool: + atk_dur_min = Config().char["atk_len_diablo_deseis"] + atk_dur_max = atk_dur_min * 3 + match seal_layout: + case "B1-S": + ### APPROACH ### + self._pather.traverse_nodes_fixed("dia_b1s_seal_deseis_foh", self) + nodes = [631] + ### ATTACK ### + Logger.debug(f"{seal_layout}: Attacking De Seis at position 1/{len(nodes)+1}") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + for i, node in enumerate(nodes): + Logger.debug(f"{seal_layout}: Attacking De Seis at position {i+2}/{len(nodes)+1}") + self._pather.traverse_nodes([node], self, timeout=3) + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + self._activate_redemption_aura(delay=[1, 2]) + #if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_check_deseis_dead" + seal_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab()) + ### LOOT ### + self._cs_pickit() + + case "B2-U": + ### APPROACH ### + self._pather.traverse_nodes_fixed("dia_b2u_644_646", self) # We try to breaking line of sight, sometimes makes De Seis walk into the hammercloud. A better attack sequence here could make sense. + #self._pather.traverse_nodes_fixed("dia_b2u_seal_deseis", self) # We try to breaking line of sight, sometimes makes De Seis walk into the hammercloud. A better attack sequence here could make sense. + nodes = [646, 641] + ### ATTACK ### + Logger.debug(seal_layout + ": Attacking De Seis at position 1/{len(nodes)+1}") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + for i, node in enumerate(nodes): + Logger.debug(f"{seal_layout}: Attacking De Seis at position {i+2}/{len(nodes)+1}") + self._pather.traverse_nodes([node], self, timeout=3) + self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max) + #if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_check_deseis_dead" + seal_layout + "_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab()) + ### LOOT ### + self._cs_pickit(skip_inspect=True) + if not self._pather.traverse_nodes([641], self): return False # , timeout=3): + if not self._pather.traverse_nodes([646], self): return False # , timeout=3): + self._cs_pickit(skip_inspect=True) + if not self._pather.traverse_nodes([646], self): return False # , timeout=3): + if not self._pather.traverse_nodes([640], self): return False # , timeout=3): + self._cs_pickit() + + case _: + Logger.warning(seal_layout + ": Invalid location for kill_deseis("+ seal_layout +"), should not happen.") + return False + return True + + + def kill_infector(self, seal_layout:str) -> bool: + atk_dur_min = Config().char["atk_len_diablo_infector"] + atk_dur_max = atk_dur_min * 3 + match seal_layout: + case "C1-F": + ### APPROACH ### + ### ATTACK ### + Logger.debug(seal_layout + ": Attacking Infector at position 1/1") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + self._pather.traverse_nodes_fixed("dia_c1f_652", self) + Logger.debug(seal_layout + ": Attacking Infector at position 2/2") + self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max) + ### LOOT ### + self._cs_pickit() + + case "C2-G": + ### APPROACH ### + if not self._pather.traverse_nodes([665], self): return False # , timeout=3): + ### ATTACK ### + Logger.debug(seal_layout + ": Attacking Infector at position 1/2") + self._cs_attack_sequence(min_duration=atk_dur_min, max_duration=atk_dur_max) + if not self._pather.traverse_nodes([663], self): return False # , timeout=3): + Logger.debug(seal_layout + ": Attacking Infector at position 2/2") + self._cs_attack_sequence(min_duration=2, max_duration=atk_dur_max) + ### LOOT ### + self._cs_pickit() + + case _: + Logger.warning(seal_layout + ": Invalid location for kill_infector("+ seal_layout +"), should not happen.") + return False + return True + + + #no aura effect on dia. not good for mob detection + def kill_diablo(self) -> bool: + ### APPROACH ### + ### ATTACK ### + atk_len_dur = float(Config().char["atk_len_diablo"]) + Logger.debug("Attacking Diablo at position 1/1") + diablo_abs = [100,-100] #hardcoded dia pos. + self._generic_foh_attack_sequence(default_target_abs=diablo_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, aura="concentration", foh_to_holy_bolt_ratio=2) + self._activate_cleanse_redemption() + ### LOOT ### + #self._cs_pickit() + return True \ No newline at end of file diff --git a/src/char/hammerdin.py b/src/char/paladin/hammerdin.py similarity index 99% rename from src/char/hammerdin.py rename to src/char/paladin/hammerdin.py index bd56dcb..ce19cca 100644 --- a/src/char/hammerdin.py +++ b/src/char/paladin/hammerdin.py @@ -1,29 +1,26 @@ import keyboard -from ui import skills +from screen import convert_abs_to_monitor, convert_screen_to_abs, grab from utils.custom_mouse import mouse -from char import IChar, CharacterCapabilities -import template_finder +from char.paladin import Paladin from pather import Pather from logger import Logger -from screen import convert_abs_to_monitor from config import Config from utils.misc import wait import time +from pather import Location +import random + +from ui import skills +from char import IChar, CharacterCapabilities from pather import Pather, Location -import cv2 #for Diablo from item.pickit import PickIt #for Diablo import numpy as np from target_detect import get_visible_targets - -class Hammerdin(IChar): - def __init__(self, skill_hotkeys: dict, pather: Pather, pickit: PickIt): +class Hammerdin(Paladin): + def __init__(self, *args, **kwargs): Logger.info("Setting up Hammerdin") - super().__init__(skill_hotkeys) - self._pather = pather - self._do_pre_move = True - self._pickit = pickit #for Diablo - self._picked_up_items = False #for Diablo + super().__init__(*args, **kwargs) #hammerdin needs to be closer to shenk to reach it with hammers self._pather.offset_node(149, (70, 10)) @@ -1286,4 +1283,4 @@ if __name__ == "__main__": keyboard.wait("f11") from config import Config pather = Pather() - char = Hammerdin(Config().hammerdin, Config().char, pather) + char = Hammerdin(Config().hammerdin, Config().char, pather) \ No newline at end of file diff --git a/src/char/paladin/paladin.py b/src/char/paladin/paladin.py new file mode 100644 index 0000000..85a8645 --- /dev/null +++ b/src/char/paladin/paladin.py @@ -0,0 +1,108 @@ +import keyboard +from ui import skills +import time +import random +from utils.custom_mouse import mouse +from char import IChar, CharacterCapabilities +from pather import Pather +from logger import Logger +from config import Config +from utils.misc import wait +from screen import convert_abs_to_screen, convert_abs_to_monitor +from pather import Pather +#import cv2 #for Diablo +from item.pickit import PickIt #for Diablo + +class Paladin(IChar): + def __init__(self, skill_hotkeys: dict, pather: Pather, pickit: PickIt): + Logger.info("Setting up Paladin") + super().__init__(skill_hotkeys) + self._pather = pather + self._do_pre_move = True + self._pickit = pickit #for Diablo + self._picked_up_items = False #for Diablo + + def pre_buff(self): + if Config().char["cta_available"]: + self._pre_buff_cta() + keyboard.send(self._skill_hotkeys["holy_shield"]) + wait(0.04, 0.1) + mouse.click(button="right") + wait(self._cast_duration, self._cast_duration + 0.06) + + def on_capabilities_discovered(self, capabilities: CharacterCapabilities): + # In case we have a running pala, we want to switch to concentration when moving to the boss + # ass most likely we will click on some mobs and already cast hammers + if capabilities.can_teleport_natively: + self._do_pre_move = False + + def pre_move(self): + # select teleport if available + super().pre_move() + # in case teleport hotkey is not set or teleport can not be used, use vigor if set + should_cast_vigor = self._skill_hotkeys["vigor"] and not skills.is_right_skill_selected(["VIGOR"]) + can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active() + if should_cast_vigor and not can_teleport: + keyboard.send(self._skill_hotkeys["vigor"]) + wait(0.15, 0.25) + + def _log_cast(self, skill_name: str, cast_pos_abs: tuple[float, float], spray: int, min_duration: float, aura: str): + msg = f"Casting skill {skill_name}" + if cast_pos_abs: + msg += f" at screen coordinate {convert_abs_to_screen(cast_pos_abs)}" + if spray: + msg += f" with spray of {spray}" + if min_duration: + msg += f" for {round(min_duration, 1)}s" + if aura: + msg += f" with {aura} active" + Logger.debug(msg) + + def _click_cast(self, cast_pos_abs: tuple[float, float], spray: int, mouse_click_type: str = "left"): + if cast_pos_abs: + x = cast_pos_abs[0] + y = cast_pos_abs[1] + if spray: + x += (random.random() * 2 * spray - spray) + y += (random.random() * 2 * spray - spray) + pos_m = convert_abs_to_monitor((x, y)) + mouse.move(*pos_m, delay_factor=[0.1, 0.2]) + wait(0.06, 0.08) + mouse.press(button = mouse_click_type) + wait(0.06, 0.08) + mouse.release(button = mouse_click_type) + + def _cast_skill_with_aura(self, skill_name: str, cast_pos_abs: tuple[float, float] = None, spray: int = 0, min_duration: float = 0, aura: str = ""): + #self._log_cast(skill_name, cast_pos_abs, spray, min_duration, aura) + + # set aura if needed + if aura: + self._select_skill(aura, mouse_click_type = "right") + + # ensure character stands still + keyboard.send(Config().char["stand_still"], do_release=False) + + # set left hand skill + self._select_skill(skill_name, mouse_click_type = "left") + wait(0.05, 0.1) + + # cast left hand skill + start = time.time() + if min_duration: + while (time.time() - start) <= min_duration: + self._click_cast(cast_pos_abs, spray) + else: + self._click_cast(cast_pos_abs, spray) + + # release stand still key + keyboard.send(Config().char["stand_still"], do_press=False) + + def _activate_redemption_aura(self, delay = [0.6, 0.8]): + self._select_skill("redemption", delay=delay) + + def _activate_cleanse_aura(self, delay = [0.3, 0.4]): + self._select_skill("cleansing", delay=delay) + + def _activate_cleanse_redemption(self): + self._activate_cleanse_aura() + self._activate_redemption_aura() \ No newline at end of file diff --git a/src/config.py b/src/config.py index 26a8004..b7fafd4 100644 --- a/src/config.py +++ b/src/config.py @@ -43,6 +43,7 @@ class Config: nova_sorc = {} hydra_sorc = {} hammerdin = {} + fohdin = {} trapsin = {} barbarian = {} poison_necro = {} @@ -333,10 +334,20 @@ class Config: self.hydra_sorc.update(dict(self.configs["custom"]["parser"]["hydra_sorc"])) self.hydra_sorc.update(sorc_base_cfg) - # Palandin config + # Paladin base config + paladin_base_cfg = dict(self.configs["config"]["parser"]["paladin"]) + if "paladin" in self.configs["custom"]["parser"]: + paladin_base_cfg.update(dict(self.configs["custom"]["parser"]["paladin"])) + # Hammerdin config self.hammerdin = self.configs["config"]["parser"]["hammerdin"] if "hammerdin" in self.configs["custom"]["parser"]: + self.fohdin.update(self.configs["custom"]["parser"]["fohdin"]) self.hammerdin.update(self.configs["custom"]["parser"]["hammerdin"]) + # FoHdin config + self.fohdin = dict(self.configs["config"]["parser"]["fohdin"]) + if "fohdin" in self.configs["custom"]["parser"]: + self.fohdin.update(self.configs["custom"]["parser"]["fohdin"]) + self.fohdin.update(paladin_base_cfg) # Assasin config self.trapsin = self.configs["config"]["parser"]["trapsin"] diff --git a/src/health_manager.py b/src/health_manager.py index 19f8a67..8bd16a9 100644 --- a/src/health_manager.py +++ b/src/health_manager.py @@ -15,19 +15,30 @@ from ui_manager import ScreenObjects, is_visible from random import uniform pause_state = True +panel_check_paused = False def get_pause_state(): - global pause_state return pause_state def set_pause_state(state: bool): global pause_state prev = get_pause_state() if prev != state: - debug_str = "pausing" if state else "active" + debug_str = "paused" if state else "active" Logger.info(f"Health Manager is now {debug_str}") pause_state = state +def get_panel_check_paused(): + return panel_check_paused + +def set_panel_check_paused(state: bool): + global panel_check_paused + prev = get_panel_check_paused() + if prev != state: + debug_str = "pausing" if state else "activating" + Logger.info(f"Health Manager is now {debug_str} inventory panel check") + panel_check_paused = state + class HealthManager: def __init__(self): self._do_monitor = False @@ -124,7 +135,7 @@ class HealthManager: elif merc_health_percentage <= Config().char["heal_merc"] and last_drink > merc_hp_potion_delay: belt.drink_potion("health", merc=True, stats=[merc_health_percentage]) self._last_merc_heal = time.time() - if is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img): + if not get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)): Logger.warning(f"Found an open inventory / quest / skill / stats page. Close it.") self._count_panel_detects += 1 if self._count_panel_detects >= 2: diff --git a/src/inventory/personal.py b/src/inventory/personal.py index 2dcee90..36d81c8 100644 --- a/src/inventory/personal.py +++ b/src/inventory/personal.py @@ -270,7 +270,7 @@ def open(img: np.ndarray = None) -> np.ndarray: img = grab() return img -def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None) -> list[BoxInfo]: +def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_stats: GameStats = None, ignore_sell: bool = False) -> list[BoxInfo]: """ Iterate over all picked items in inventory--ID items and decide which to stash :param img: Image in which the item is searched (item details should be visible) @@ -377,6 +377,8 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st keep = keep_item(item_box, found_item) if found_item else False if keep: sell = need_id = False + if ignore_sell: + sell = False box = BoxInfo( img = item_box.data, diff --git a/src/item/pickit.py b/src/item/pickit.py index 7c4cf86..bc948ad 100644 --- a/src/item/pickit.py +++ b/src/item/pickit.py @@ -178,8 +178,7 @@ if __name__ == "__main__": import os from config import Config from char.sorceress import LightSorc - from char.hammerdin import Hammerdin - import template_finder + from char.paladin import Hammerdin from pather import Pather import keyboard diff --git a/src/pather.py b/src/pather.py index 13cba84..96c6cc1 100644 --- a/src/pather.py +++ b/src/pather.py @@ -749,7 +749,7 @@ if __name__ == "__main__": start_detecting_window() from config import Config from char.sorceress import LightSorc - from char.hammerdin import Hammerdin + from char.paladin.hammerdin import Hammerdin from item.pickit import PickIt pather = Pather() diff --git a/src/run/diablo.py b/src/run/diablo.py index 9dc2efd..b33dd22 100644 --- a/src/run/diablo.py +++ b/src/run/diablo.py @@ -464,14 +464,10 @@ class Diablo: #Arrive at and clear Pentagram if not self._cs_pentagram(): return False - """ NEW APPROACH ONLY HAS 50% SUCCESS RATE - #if Config().char["kill_cs_trash"]: if not self._trash_seals("A", "dia_trash_a", [606], "dia_trash_a_loop", 0.78): return False - #if Config().char["kill_cs_trash"]: if not self._trash_seals("B", "dia_trash_b", [607], "dia_trash_b_loop", 0.85): return False #high threshold needed to avoid chasing 607 ghosts - #if Config().char["kill_cs_trash"]: if not self._trash_seals("C", "dia_trash_c", [608], "dia_trash_c_loop", 0.78): return False - """ #OLD APPROACH HAS 80% SUCCESS RATE if Config().char["kill_cs_trash"]: self._trash_seals() + # Maintenance at Pentagram after Trash & clear Seal A: Vizier (to the left) if Config().char["kill_cs_trash"]: self._char.kill_cs_trash("pent_before_a") if not self._pather.traverse_nodes([602], self._char): return False @@ -492,13 +488,11 @@ class Diablo: if Config().char["cs_town_visits"]: self._cs_town_visit("C") if do_pre_buff: self._char.pre_buff() if not self._layoutcheck("C", "Infector", "dia_c_layout_bold", "layoutcheck_c", [650660], 0.83, None, ["DIA_C2G_BOSS_CLOSED_LAYOUTCHECK1", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK4", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK5", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK2", "DIA_C2G_BOSS_CLOSED_LAYOUTCHECK3",], ["DIA_C1F_LAYOUTCHECK1", "DIA_C1F_LAYOUTCHECK2", "DIA_C1F_LAYOUTCHECK3"]): return False - - # Kill Diablo Logger.info("Waiting for Diablo to spawn") if not self._pather.traverse_nodes([602], self._char): return False self._char.kill_diablo() if Config().general["info_screenshots"]: cv2.imwrite(f"./info_screenshots/info_dia_kill_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab()) - self._picked_up_items = self._pickit.pick_up_items(char=self._char) + self._picked_up_items |= self._pickit.pick_up_items(char=self._char) wait(0.5, 0.7) return (Location.A4_DIABLO_END, self._picked_up_items) diff --git a/src/target_detect.py b/src/target_detect.py index 653271c..0639a38 100644 --- a/src/target_detect.py +++ b/src/target_detect.py @@ -4,7 +4,7 @@ import time from screen import convert_screen_to_monitor, grab, convert_screen_to_abs, convert_abs_to_monitor from logger import Logger from math import dist -from utils.misc import color_filter +from utils.misc import color_filter, is_in_roi import json from dataclasses import dataclass @@ -17,26 +17,51 @@ FILTER_RANGES=[ class TargetInfo: roi: tuple = None center: tuple = None + center_abs: tuple = None center_monitor: tuple = None distance: int = 0 +def log_targets(targets: list[TargetInfo]): + if (num_targets := len(targets)) > 0: + if num_targets == 1: + msg = f"{num_targets} target detected" + elif num_targets > 1: + msg = f"{num_targets} targets detected, closest" + Logger.debug(f"{msg} at {targets[0].center}, distance: {round(targets[0].distance)}") + def _dist_to_center(pos): return dist(pos, (1280/2, 720/2)) -def get_visible_targets(img: np.ndarray = None, radius_min: int = 150, radius_max: int = 1280) -> list[TargetInfo]: +def get_visible_targets( + img: np.ndarray = None, + radius_min: int = 150, + radius_max: int = 1280, + ignore_roi: list[int] = [600, 300, (1280/2 - 600)*2, (720/2 - 300)*2], + use_radius: bool = False +) -> list[TargetInfo]: + """ + :param img: The image to find targets in + :param radius_min: The minimum radius of the target [Default: 150, Integer 0 - 1280] + :param radius_max: The maximum radius of the target [Default: 1280, Integer 0 - 1280] + :param ignore_roi: The region of interest to ignore [Default: [600, 300, (1280/2 - 600)*2, (720/2 - 300)*2]] + :param use_radius: Whether to use the radius of the target (True) or the ignore_roi parameter (False) + Returns a list of TargetInfo objects + """ img = grab() if img is None else img targets = [] for filter in FILTER_RANGES: - filterimage, threshz = _process_image(img, mask_char=True, mask_hud=True, info_ss=False, **filter) # HSV Filter for BLUE and GREEN (Posison Nova & Holy Freeze) - filterimage, rectangles, positions = _add_markers(filterimage, threshz, info_ss=False, rect_min_size=100, rect_max_size=200, marker=True) # rather large rectangles + filterimage, threshz = _process_image(img, mask_char=True, mask_hud=True, **filter) # HSV Filter for BLUE and GREEN (Posison Nova & Holy Freeze) + filterimage, rectangles, positions = _add_markers(filterimage, threshz, rect_min_size=100, rect_max_size=200, marker=True) # rather large rectangles if positions: for cnt, position in enumerate(positions): distance = _dist_to_center(position) - if radius_min <= _dist_to_center(position) <= radius_max: + condition = (radius_min <= distance <= radius_max) if use_radius else (not is_in_roi(ignore_roi, position)) + if condition: targets.append(TargetInfo( roi = rectangles[cnt], center = position, center_monitor = convert_screen_to_monitor(position), + center_abs = convert_screen_to_abs(position), distance = distance )) if targets: @@ -72,13 +97,12 @@ def _bright_contrast(img: np.ndarray, brightness: int = 255, contrast: int = 127 cal = cv2.addWeighted(cal, alpha, cal, 0, gamma) return cal -def _process_image(img, mask_char:bool=False, mask_hud:bool=True, info_ss:bool=False, erode:int=None, dilate:int=None, blur:int=None, lh:int=None, ls:int=None, lv:int=None, uh:int=None, us:int=None, uv:int=None, bright:int=None, contrast:int=None, thresh:int=None, invert:int=None): +def _process_image(img, mask_char:bool=False, mask_hud:bool=True, erode:int=None, dilate:int=None, blur:int=None, lh:int=None, ls:int=None, lv:int=None, uh:int=None, us:int=None, uv:int=None, bright:int=None, contrast:int=None, thresh:int=None, invert:int=None): """ Helper function that will apply HSV filters :param img: The image to which filters should be applied :param mask_char: apply a black rectangle to mask your char (e.g. filter out Set-Item glow effect or auras) [Default: False, Bool] :param hud_mask: removes the HUD from the returned image [Default: True, Bool] - :param info_ss: Save an image of the applied filters in folder INFO_SCREENSHOTS [Default: False, Bool] :param erode: erode (thin lines) in the picture [Default: None, Integer, no filter: 0, 0 - 36] :param dilate: dilate (thicken lines) in the picture [Default: None, Integer, no filter: 0, 0 - 36] :param blur: blur the picture [Default: None, no filter: 0, Integer, 0 - 30] @@ -95,7 +119,6 @@ def _process_image(img, mask_char:bool=False, mask_hud:bool=True, info_ss:bool=F Returns variables filterimage and threshz """ img = img - if info_ss: cv2.imwrite(f"./info_screenshots/info_apply_filter_input_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img) if mask_hud: hud_mask = cv2.imread(f"./assets/hud_mask.png", cv2.IMREAD_GRAYSCALE) img = cv2.bitwise_and(img, img, mask=hud_mask) @@ -115,16 +138,14 @@ def _process_image(img, mask_char:bool=False, mask_hud:bool=True, info_ss:bool=F if thresh: _, threshz = cv2.threshold(threshz, thresh, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) if invert: img = 255 - img filterimage = img - if info_ss: cv2.imwrite(f"./info_screenshots/info_apply_filter_output_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img) return filterimage, threshz # add rectangles and crosses - adapted from Nathan's Live-View -def _add_markers(img:str, threshz:str, info_ss:bool=False, rect_min_size:int=20, rect_max_size:int=50, line_color:str=(0, 255, 0), line_type:str=cv2.LINE_4, marker:bool=False, marker_color:str=(0, 255, 255), marker_type:str=cv2.MARKER_CROSS): +def _add_markers(img:str, threshz:str, rect_min_size:int=20, rect_max_size:int=50, line_color:str=(0, 255, 0), line_type:str=cv2.LINE_4, marker:bool=False, marker_color:str=(0, 255, 255), marker_type:str=cv2.MARKER_CROSS): """ Helper function that will add rectangles and crosshairs to allow object detection :param img: The image to which filters should be applied :param threshz: The image that had the threshold adjusted (obtained from _process_image()) - :param info_ss: Take a screenshot for each step [Default: False, Bool] :param rect_min_size: Minimum size for the rectangle to be drawn [Default: 20, Integer] :param rect_max_size: Minimum size for the rectangle to be drawn [Default: 50, Integer] :param line_color: Color of the rectangle line [Default: (0, 255, 0)] @@ -210,7 +231,7 @@ class LiveViewer: except cv2.error: return self.image, threshz = _process_image(self.image, **self.settings) - filterimage, _, _ = _add_markers(self.image, threshz, info_ss=False, rect_min_size=100, rect_max_size=200, marker=True) + filterimage, _, _ = _add_markers(self.image, threshz, rect_min_size=100, rect_max_size=200, marker=True) cv2.imshow("Results", filterimage) cv2.waitKey(1) @@ -229,4 +250,22 @@ if __name__ == "__main__": start_detecting_window() print("Move to d2r window and press f11") keyboard.wait("f11") - l = LiveViewer() \ No newline at end of file + # l = LiveViewer() + + masked_image = False + + def _toggle_masked_image(): + global masked_image + masked_image = not masked_image + + while 1: + img = grab() + targets = get_visible_targets() + + display_img = img.copy() + + for target in targets: + x, y = target.center + cv2.rectangle(display_img, target.roi[:2], (target.roi[0] + target.roi[2], target.roi[1] + target.roi[3]), (0, 0, 255), 1) + cv2.imshow('test', display_img) + key = cv2.waitKey(1) diff --git a/src/town/town_manager.py b/src/town/town_manager.py index 81d2b1c..bdb7fda 100644 --- a/src/town/town_manager.py +++ b/src/town/town_manager.py @@ -254,7 +254,7 @@ if __name__ == "__main__": 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 char.paladin.hammerdin import Hammerdin from pather import Pather pather = Pather() char = Hammerdin(Config().hammerdin, Config().char, pather) diff --git a/src/utils/node_recorder.py b/src/utils/node_recorder.py index 1e132bb..adb402e 100644 --- a/src/utils/node_recorder.py +++ b/src/utils/node_recorder.py @@ -1,4 +1,4 @@ -from screen import grab, convert_monitor_to_screen, convert_abs_to_screen +from screen import grab, convert_monitor_to_screen, convert_abs_to_screen, convert_screen_to_abs import cv2 from config import Config import template_finder