From be6e3a267dfe210ad35f4b77a750ec177ca26b40 Mon Sep 17 00:00:00 2001 From: Legit <96157236+D2RLegit@users.noreply.github.com> Date: Thu, 30 Jun 2022 01:59:57 -0500 Subject: [PATCH 1/5] Poisonmancer Overhaul with mob detect Fixed poisonmancer by doing a complete overhaul with mob detection --- src/char/poison_necro.py | 803 +++++++++++++++++++++------------------ 1 file changed, 435 insertions(+), 368 deletions(-) diff --git a/src/char/poison_necro.py b/src/char/poison_necro.py index 72ae64cac..cf950dfc0 100644 --- a/src/char/poison_necro.py +++ b/src/char/poison_necro.py @@ -14,6 +14,8 @@ import time import os from ui_manager import get_closest_non_hud_pixel +from screen import convert_abs_to_monitor, convert_screen_to_abs, grab +from target_detect import get_visible_green_targets, get_visible_targets, get_visible_blue_targets class Poison_Necro(IChar): def __init__(self, skill_hotkeys: dict, pather: Pather): @@ -41,56 +43,6 @@ def __init__(self, skill_hotkeys: dict, pather: Pather): self._golem_count="none" self._revive_count=0 - - def _check_shenk_death(self): - ''' make sure shenk is dead checking for fireballs so we can exit combat sooner ''' - - roi = [640,0,640,720] - img = grab() - - template_match = template_finder.search( - ['SHENK_DEATH_1','SHENK_DEATH_2','SHENK_DEATH_3','SHENK_DEATH_4'], - img, - threshold=0.6, - roi=roi, - use_grayscale = False - ) - if template_match.valid: - self._shenk_dead=1 - Logger.info('\33[31m'+"Shenks Dead, looting..."+'\033[0m') - else: - return True - - def _count_revives(self): - roi = [15,14,400,45] - img = grab() - max_rev = 13 - - template_match = template_finder.search( - ['REV_BASE'], - img, - threshold=0.6, - roi=roi - ) - if template_match.valid: - self._revive_count=max_rev - else: - self._revive_count=0 - return True - - for count in range(1,max_rev): - rev_num = "REV_"+str(count) - template_match = template_finder.search( - [rev_num], - img, - threshold=0.66, - roi=roi, - use_grayscale = False - ) - if template_match.valid: - self._revive_count=count - - def poison_nova(self, time_in_s: float): if not self._skill_hotkeys["poison_nova"]: raise ValueError("You did not set poison nova hotkey!") @@ -102,148 +54,7 @@ def poison_nova(self, time_in_s: float): mouse.press(button="right") wait(0.12, 0.2) mouse.release(button="right") - - def _count_skeletons(self): - roi = [15,14,400,45] - img = grab() - max_skeles = 13 - - template_match = template_finder.search( - ['SKELE_BASE'], - img, - threshold=0.6, - roi=roi - ) - if template_match.valid: - self._skeletons_count=max_skeles - else: - self._skeletons_count=0 - return True - - for count in range(1,max_skeles): - skele_num = "SKELE_"+str(count) - template_match = template_finder.search( - [skele_num], - img, - threshold=0.66, - roi=roi, - use_grayscale = False - ) - if template_match.valid: - self._skeletons_count=count - - def _count_gol(self): - roi = [15,14,400,45] - img = grab() - - template_match = template_finder.search( - ['CLAY'], - img, - threshold=0.6, - roi=roi - ) - if template_match.valid: - self._golem_count="clay gol" - else: - self._golem_count="none" - return True - - def _summon_count(self): - ''' see how many summons and which golem are out ''' - - self._count_skeletons() - self._count_revives() - self._count_gol() - def _summon_stat(self): - ''' print counts for summons ''' - Logger.info('\33[31m'+"Summon status | "+str(self._skeletons_count)+"skele | "+str(self._revive_count)+" rev | "+self._golem_count+" |"+'\033[0m') - - def _revive(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=12): - Logger.info('\033[94m'+"raise revive"+'\033[0m') - keyboard.send(Config().char["stand_still"], do_release=False) - for _ in range(cast_count): - if self._skill_hotkeys["raise_revive"]: - keyboard.send(self._skill_hotkeys["raise_revive"]) - #Logger.info("revive -> cast") - x = cast_pos_abs[0] + (random.random() * 2*spray - spray) - y = cast_pos_abs[1] + (random.random() * 2*spray - spray) - cast_pos_monitor = screen.convert_abs_to_monitor((x, y)) - - nx = cast_pos_monitor[0] - ny = cast_pos_monitor[1] - if(nx>1280): - nx=1275 - if(ny>720): - ny=715 - if(nx<0): - nx=0 - if(ny<0): - ny=0 - clamp = [nx,ny] - - mouse.move(*clamp) - mouse.press(button="right") - wait(0.075, 0.1) - mouse.release(button="right") - keyboard.send(Config().char["stand_still"], do_press=False) - - def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=16): - Logger.info('\033[94m'+"raise skeleton"+'\033[0m') - keyboard.send(Config().char["stand_still"], do_release=False) - for _ in range(cast_count): - if self._skill_hotkeys["raise_skeleton"]: - keyboard.send(self._skill_hotkeys["raise_skeleton"]) - #Logger.info("raise skeleton -> cast") - x = cast_pos_abs[0] + (random.random() * 2*spray - spray) - y = cast_pos_abs[1] + (random.random() * 2*spray - spray) - cast_pos_monitor = screen.convert_abs_to_monitor((x, y)) - - nx = cast_pos_monitor[0] - ny = cast_pos_monitor[1] - if(nx>1280): - nx=1279 - if(ny>720): - ny=719 - if(nx<0): - nx=0 - if(ny<0): - ny=0 - clamp = [nx,ny] - - mouse.move(*clamp) - mouse.press(button="right") - wait(0.02, 0.05) - mouse.release(button="right") - keyboard.send(Config().char["stand_still"], do_press=False) - - def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=16): - Logger.info('\033[94m'+"raise mage"+'\033[0m') - keyboard.send(Config().char["stand_still"], do_release=False) - for _ in range(cast_count): - if self._skill_hotkeys["raise_mage"]: - keyboard.send(self._skill_hotkeys["raise_mage"]) - #Logger.info("raise skeleton -> cast") - x = cast_pos_abs[0] + (random.random() * 2*spray - spray) - y = cast_pos_abs[1] + (random.random() * 2*spray - spray) - cast_pos_monitor = screen.convert_abs_to_monitor((x, y)) - - nx = cast_pos_monitor[0] - ny = cast_pos_monitor[1] - if(nx>1280): - nx=1279 - if(ny>720): - ny=719 - if(nx<0): - nx=0 - if(ny<0): - ny=0 - clamp = [nx,ny] - - mouse.move(*clamp) - mouse.press(button="right") - wait(0.02, 0.05) mouse.release(button="right") - keyboard.send(Config().char["stand_still"], do_press=False) def pre_buff(self): @@ -270,6 +81,17 @@ def _clay_golem(self): mouse.click(button="right") wait(self._cast_duration) + def amp_dmg(self, hork_time: int): + wait(0.5) + if self._skill_hotkeys["amp_dmg"]: + keyboard.send(self._skill_hotkeys["amp_dmg"]) + wait(0.5, 0.15) + pos_m = convert_abs_to_monitor((0, -20)) + mouse.move(*pos_m) + wait(0.5, 0.15) + mouse.press(button="right") + wait(hork_time) + mouse.release(button="right") def bone_armor(self): if self._skill_hotkeys["bone_armor"]: @@ -322,223 +144,468 @@ def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: int = 10 keyboard.send(Config().char["stand_still"], do_press=False) - def _amp_dmg(self, cast_pos_abs: tuple[float, float], spray: float = 10): - if self._skill_hotkeys["amp_dmg"]: - keyboard.send(self._skill_hotkeys["amp_dmg"]) - - x = cast_pos_abs[0] + (random.random() * 2*spray - spray) - y = cast_pos_abs[1] + (random.random() * 2*spray - spray) - cast_pos_monitor = screen.convert_abs_to_monitor((x, y)) - mouse.move(*cast_pos_monitor) + def _do_curse(self, hork_time: int): + wait(0.5) + if self._skill_hotkeys["curse"]: + keyboard.send(self._skill_hotkeys["curse"]) + wait(0.05, 0.15) + pos_m = convert_abs_to_monitor((0, -20)) + mouse.move(*pos_m) + wait(0.05, 0.15) mouse.press(button="right") - wait(0.25, 0.35) - mouse.release(button="right") - - def _lower_res(self, cast_pos_abs: tuple[float, float], spray: float = 10): - if self._skill_hotkeys["lower_res"]: - keyboard.send(self._skill_hotkeys["lower_res"]) - - x = cast_pos_abs[0] + (random.random() * 2*spray - spray) - y = cast_pos_abs[1] + (random.random() * 2*spray - spray) - cast_pos_monitor = screen.convert_abs_to_monitor((x, y)) + wait(hork_time) + mouse.release(button="right") + + def _force_teleport(self, cast_pos_abs: tuple[float, float], spray: float = 10): + if not self._skill_hotkeys["force_teleport"]: + raise ValueError("You did not set a hotkey for force_teleport!") + keyboard.send(self._skill_hotkeys["force_teleport"]) + x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) + y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) + cast_pos_monitor = convert_abs_to_monitor((x, y)) mouse.move(*cast_pos_monitor) - mouse.press(button="right") - wait(0.25, 0.35) - mouse.release(button="right") - - def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: int = 10,cast_count: int = 8): - keyboard.send(Config().char["stand_still"], do_release=False) - Logger.info('\033[93m'+"corpse explosion~> random cast"+'\033[0m') - for _ in range(cast_count): - if self._skill_hotkeys["corpse_explosion"]: - keyboard.send(self._skill_hotkeys["corpse_explosion"]) - x = cast_pos_abs[0] + (random.random() * 2*spray - spray) - y = cast_pos_abs[1] + (random.random() * 2*spray - spray) - cast_pos_monitor = screen.convert_abs_to_monitor((x, y)) - mouse.move(*cast_pos_monitor) - mouse.press(button="right") - wait(0.075, 0.1) - mouse.release(button="right") - keyboard.send(Config().char["stand_still"], do_press=False) + click_tries = random.randint(2, 4) + for _ in range(click_tries): + mouse.press(button="right") + wait(0.09, 0.12) + mouse.release(button="right") + + def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: float = 10): + if not self._skill_hotkeys["raise_skeleton"]: + raise ValueError("You did not set a hotkey for raise_skeleton!") + keyboard.send(self._skill_hotkeys["raise_skeleton"]) + x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) + y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) + cast_pos_monitor = convert_abs_to_monitor((x, y)) + mouse.move(*cast_pos_monitor) + click_tries = random.randint(2, 4) + for _ in range(click_tries): + mouse.press(button="right") + wait(0.09, 0.12) + mouse.release(button="right") - def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, cast_end_angle: float=90.0,cast_div: int = 10,cast_v_div: int=4,cast_spell: str='raise_skeleton',delay: float=1.0,offset: float=1.0): - Logger.info('\033[93m'+"circle cast ~>"+cast_spell+'\033[0m') - keyboard.send(Config().char["stand_still"], do_release=False) - keyboard.send(self._skill_hotkeys[cast_spell]) - mouse.press(button="right") + def corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: float = 10): + if not self._skill_hotkeys["corpse_explosion"]: + raise ValueError("You did not set a hotkey for corpse_explosion!") + keyboard.send(self._skill_hotkeys["corpse_explosion"]) + x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) + y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) + cast_pos_monitor = convert_abs_to_monitor((x, y)) + mouse.move(*cast_pos_monitor) + click_tries = random.randint(2, 4) + for _ in range(click_tries): + mouse.press(button="right") + wait(0.09, 0.12) + mouse.release(button="right") + + def _raise_mage(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.2, 0.3), spray: float = 10): + if not self._skill_hotkeys["raise_mage"]: + raise ValueError("You did not set raise_mage hotkey!") + keyboard.send(self._skill_hotkeys["raise_mage"]) + for _ in range(3): + x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) + y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) + cast_pos_monitor = convert_abs_to_monitor((x, y)) + mouse.move(*cast_pos_monitor, delay_factor=[0.3, 0.6]) + mouse.press(button="right") + wait(delay[0], delay[1]) + mouse.release(button="right") - for i in range(cast_div): - angle = self._lerp(cast_start_angle,cast_end_angle,float(i)/cast_div) - target = unit_vector(rotate_vec(cast_dir, angle)) - #Logger.info("current angle ~> "+str(angle)) - for j in range(cast_v_div): - circle_pos_abs = get_closest_non_hud_pixel(pos = ((target*120.0*float(j+1.0))*offset), pos_type="abs") - circle_pos_monitor = screen.convert_abs_to_monitor(circle_pos_abs) - mouse.move(*circle_pos_monitor,delay_factor=[0.3*delay, .6*delay]) + def _raise_revive(self, cast_pos_abs: tuple[float, float], spray: float = 10): + if not self._skill_hotkeys["raise_revive"]: + raise ValueError("You did not set a hotkey for raise_revive!") + keyboard.send(self._skill_hotkeys["raise_revive"]) + x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) + y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) + cast_pos_monitor = convert_abs_to_monitor((x, y)) + mouse.move(*cast_pos_monitor) + click_tries = random.randint(2, 4) + for _ in range(click_tries): + mouse.press(button="right") + wait(0.09, 0.12) + mouse.release(button="right") + + def _pn_attack_sequence( + self, + default_target_abs: tuple[int, int] = (-50, -50), + min_duration: float = 0, + max_duration: float = 15, + blizz_to_ice_blast_ratio: int = 3, + target_detect: bool = True, + default_spray: int = 20, + #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 + atk_len = Config().char["atk_len_cs_trashmobs"] * .50 + # 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_green_targets()): + # log_targets(targets) + spray = 5 + cast_pos_abs = targets[0].center_abs + target_check_count += 1 + closest_target_position_monitor = targets[0].center_monitor + + + + # 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 + + targets = get_visible_green_targets() + if targets and len(targets) > 0: + # 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) + Logger.info("Mob detected, Attacking mobs!") + self.poison_nova(atk_len) + self._raise_skeleton(cast_pos_abs, spray=spray) + #self._raise_mage(cast_pos_abs, spray=spray) + self._raise_revive(cast_pos_abs, spray=spray) + targets = get_visible_green_targets() + if targets: + closest_target_position_monitor = targets[0].center_monitor + self.pre_move() + self.move(closest_target_position_monitor, force_move=True) + pos_m = screen.convert_abs_to_monitor((50, 0)) + self.walk(pos_m, force_move=True) + self.poison_nova(atk_len) + targets = get_visible_green_targets() + self._raise_revive(cast_pos_abs, spray=spray) + self._raise_mage(cast_pos_abs, spray=spray) + self._raise_skeleton(cast_pos_abs, spray=spray) + + else: + Logger.info("No minions detected yet, Default attack sequence") + targets = get_visible_green_targets() + self.poison_nova(atk_len) + targets = get_visible_green_targets() + self._raise_skeleton((-25, -75), spray=50) + pos_m = screen.convert_abs_to_monitor((0, 30)) + self.walk(pos_m, force_move=True) + self._raise_revive((-35, -50), spray=50) + self._raise_mage((0, -100), spray=50) + self._raise_skeleton(cast_pos_abs, spray=spray) + + + target_check_count += 1 + return True - #Logger.info("circle move") - mouse.release(button="right") - keyboard.send(Config().char["stand_still"], do_press=False) - + def _trav_attack( + self, + default_target_abs: tuple[int, int] = (-50, -50), + min_duration: float = 0, + max_duration: float = 15, + blizz_to_ice_blast_ratio: int = 3, + target_detect: bool = True, + default_spray: int = 20, + #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 + atk_len = Config().char["atk_len_cs_trashmobs"] * .50 + # 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 + target_check_count += 1 + closest_target_position_monitor = targets[0].center_monitor + + + + # 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 + + targets = get_visible_targets() + if targets and len(targets) > 0: + # 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) + Logger.info("Mob detected, Attacking mobs!") + self.poison_nova(atk_len) + #self._raise_revive(cast_pos_abs, spray=spray) + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + if targets: + Logger.info("Teleporting to mob!") + closest_target_position_monitor = targets[0].center_monitor + self.pre_move() + self.move(closest_target_position_monitor, force_move=True) + pos_m = screen.convert_abs_to_monitor((50, 0)) + self.walk(pos_m, force_move=True) + self.poison_nova(atk_len) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + if targets: + closest_target_position_monitor = targets[0].center_monitor + self.pre_move() + self.move(closest_target_position_monitor, force_move=True) + pos_m = screen.convert_abs_to_monitor((-50, 0)) + self.walk(pos_m, force_move=True) + self.corpse_explosion(cast_pos_abs, spray=spray) + self._raise_revive(cast_pos_abs, spray=spray) + #self._raise_mage(cast_pos_abs, spray=spray) + self._raise_skeleton(cast_pos_abs, spray=spray) + self.corpse_explosion(cast_pos_abs, spray=spray) + + else: + Logger.info("No minions detected yet, Default attack sequence") + targets = get_visible_targets() + self.poison_nova(atk_len) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + #self._raise_revive(cast_pos_abs, spray=spray) + self._raise_mage(cast_pos_abs, spray=spray) + self._raise_skeleton(cast_pos_abs, spray=spray) + self.corpse_explosion(cast_pos_abs, spray=spray) + + target_check_count += 1 + return True + + def _nihl_attack( + self, + default_target_abs: tuple[int, int] = (-50, -50), + min_duration: float = 0, + max_duration: float = 15, + blizz_to_ice_blast_ratio: int = 3, + target_detect: bool = True, + default_spray: int = 20, + #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 + atk_len = Config().char["atk_len_nihlathak"] * .50 + # 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 + target_check_count += 1 + closest_target_position_monitor = targets[0].center_monitor + + + + # 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 + + targets = get_visible_targets() + if targets and len(targets) > 0: + # 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) + Logger.info("Mob detected, Attacking mobs!") + self.poison_nova(1.5) + #self._raise_revive(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + if targets: + Logger.info("Teleporting to mob!") + closest_target_position_monitor = targets[0].center_monitor + self.pre_move() + self.move(closest_target_position_monitor, force_move=True) + self.poison_nova(1.5) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + else: + Logger.info("No minions detected yet, Default attack sequence") + targets = get_visible_targets() + self.poison_nova(1.5) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + + target_check_count += 1 + return True + + def _pindle_attack( + self, + default_target_abs: tuple[int, int] = (-50, -50), + min_duration: float = 0, + max_duration: float = 15, + blizz_to_ice_blast_ratio: int = 3, + target_detect: bool = True, + default_spray: int = 20, + #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 + atk_len = Config().char["atk_len_nihlathak"] * .50 + # 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_blue_targets()): + # log_targets(targets) + spray = 5 + cast_pos_abs = targets[0].center_abs + target_check_count += 1 + closest_target_position_monitor = targets[0].center_monitor + + + + # 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 + + targets = get_visible_blue_targets() + if targets and len(targets) > 0: + # 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) + Logger.info("Mob detected, Attacking mobs!") + self.poison_nova(atk_len) + #self._raise_revive(cast_pos_abs, spray=spray) + self.corpse_explosion(cast_pos_abs, spray=spray) + targets = get_visible_blue_targets() + if targets: + closest_target_position_monitor = targets[0].center_monitor + self.pre_move() + self.move(closest_target_position_monitor, force_move=True) + pos_m = screen.convert_abs_to_monitor((-50, 0)) + self.walk(pos_m, force_move=True) + self.poison_nova(atk_len) + self._raise_revive(cast_pos_abs, spray=spray) + self._raise_mage(cast_pos_abs, spray=spray) + self._raise_skeleton(cast_pos_abs, spray=spray) + self.corpse_explosion(cast_pos_abs, spray=spray) + + else: + Logger.info("No minions detected yet, Default attack sequence") + targets = get_visible_blue_targets() + self.poison_nova(atk_len) + targets = get_visible_blue_targets() + self.corpse_explosion(cast_pos_abs, spray=spray) + self._raise_revive(cast_pos_abs, spray=spray) + self._raise_mage(cast_pos_abs, spray=spray) + self._raise_skeleton(cast_pos_abs, spray=spray) + self.corpse_explosion(cast_pos_abs, spray=spray) + + target_check_count += 1 + return True + + def _attack_sequence(self, min_duration: float = Config().char["atk_len_cs_trashmobs"], max_duration: float = Config().char["atk_len_cs_trashmobs"] * 2): + self._pn_attack_sequence(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=10, blizz_to_ice_blast_ratio=2) + + def _trav_attack_sequence(self, min_duration: float = Config().char["atk_len_trav"], max_duration: float = Config().char["atk_len_trav"] * 4): + self._trav_attack(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=10, blizz_to_ice_blast_ratio=2) + + def _nihl_attack_sequence(self, min_duration: float = Config().char["atk_len_nihlathak"], max_duration: float = Config().char["atk_len_nihlathak"] * 4): + self._nihl_attack(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=10, blizz_to_ice_blast_ratio=2) + + def _pindle_attack_sequence(self, min_duration: float = Config().char["atk_len_nihlathak"], max_duration: float = Config().char["atk_len_nihlathak"] * 4): + self._pindle_attack(default_target_abs=(20,20), min_duration = min_duration, max_duration = max_duration, default_spray=10, blizz_to_ice_blast_ratio=2) def kill_pindle(self) -> bool: - pos_m = screen.convert_abs_to_monitor((0, 30)) - self.walk(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - self.poison_nova(3.0) - pos_m = screen.convert_abs_to_monitor((0, -50)) - self.pre_move() - self.move(pos_m, force_move=True) - pos_m = screen.convert_abs_to_monitor((50, 0)) - self.walk(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=1.1,offset=1.8) - self.poison_nova(3.0) + self.bone_armor() + self._do_curse(.5) + self._pindle_attack_sequence() + self._pindle_attack_sequence() + self._pindle_attack_sequence() + # Move to items + self._pather.traverse_nodes_fixed("pindle_end", self) return True def kill_eldritch(self) -> bool: - pos_m = screen.convert_abs_to_monitor((0, -100)) + pos_m = screen.convert_abs_to_monitor((0, -60)) self.pre_move() self.move(pos_m, force_move=True) self.bone_armor() - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - self.poison_nova(2.0) - self._summon_stat() + self._do_curse(.5) + self._attack_sequence() + self._attack_sequence() # move a bit back - pos_m = screen.convert_abs_to_monitor((0, 50)) - self.pre_move() - self.move(pos_m, force_move=True) - self.poison_nova(2.0) - self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True) - pos_m = screen.convert_abs_to_monitor((0, 170)) - self.pre_move() - self.move(pos_m, force_move=True) - #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=8,cast_v_div=4,cast_spell='raise_revive',delay=1.2,offset=.8) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.8) - pos_m = screen.convert_abs_to_monitor((0, -50)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_mage',delay=1.1,offset=1.0) - pos_m = screen.convert_abs_to_monitor((-75, 0)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=8,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.5) - self._summon_count() - self._summon_stat() - self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True) return True def kill_shenk(self) -> bool: self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0) - #pos_m = self._screen.convert_abs_to_monitor((50, 0)) - #self.walk(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - self.poison_nova(3.0) - pos_m = screen.convert_abs_to_monitor((0, -50)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.1,offset=.8) pos_m = screen.convert_abs_to_monitor((50, 0)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=720,cast_div=10,cast_v_div=4,cast_spell='raise_revive',delay=1.1,offset=.8) - pos_m = screen.convert_abs_to_monitor((-20, -20)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_skeleton',delay=1.1,offset=.8) - self._summon_count() + self.walk(pos_m, force_move=True) + self._do_curse(.5) + self._attack_sequence() + self._attack_sequence() #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8) return True def kill_council(self) -> bool: - pos_m = screen.convert_abs_to_monitor((0, -200)) - self.pre_move() - self.move(pos_m, force_move=True) - self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True) - pos_m = screen.convert_abs_to_monitor((50, 0)) - self.walk(pos_m, force_move=True) - #self._lower_res((-50, 0), spray=10) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - self.poison_nova(2.0) - #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8) - pos_m = screen.convert_abs_to_monitor((200, 50)) - self.pre_move() - self.move(pos_m, force_move=True) - pos_m = screen.convert_abs_to_monitor((30, -50)) + self.bone_armor() + # Check out the node screenshot in assets/templates/trav/nodes to see where each node is at + atk_len = Config().char["atk_len_trav"] + # Go inside and hammer a bit + self._pather.traverse_nodes([228, 229], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + pos_m = screen.convert_abs_to_monitor((50, 50)) self.walk(pos_m, force_move=True) - self.poison_nova(2.0) - #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8) - #wait(self._cast_duration, self._cast_duration +.2) - pos_m = screen.convert_abs_to_monitor((-200, 200)) - self.pre_move() - self.move(pos_m, force_move=True) - pos_m = screen.convert_abs_to_monitor((-100, 200)) - self.pre_move() - self.move(pos_m, force_move=True) - self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True, use_tp_charge=True) - pos_m = screen.convert_abs_to_monitor((0, 30)) + # Move a bit back and another round + self._do_curse(.5) + self._trav_attack_sequence() + self._pather.traverse_nodes([229], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + # Here we have two different attack sequences depending if tele is available or not + # Back to center stairs and more hammers + #self._pather.traverse_nodes([228], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes([300], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + pos_m = screen.convert_abs_to_monitor((50, -50)) self.walk(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - wait(0.5) - self.poison_nova(4.0) - #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=2,cast_v_div=1,cast_spell='corpse_explosion',delay=3.0,offset=1.8) - #wait(self._cast_duration, self._cast_duration +.2) - #self.poison_nova(2.0) - pos_m = screen.convert_abs_to_monitor((50, 0)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=0.5,offset=1.8) - #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_skeleton',delay=1.2,offset=.8) - #self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=9,cast_v_div=3,cast_spell='raise_mage',delay=1.2,offset=.8) - pos_m = screen.convert_abs_to_monitor((-200, -200)) - self.pre_move() - self.move(pos_m, force_move=True) - self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True, use_tp_charge=True) - pos_m = screen.convert_abs_to_monitor((20, -50)) + self._do_curse(.5) + self._trav_attack_sequence() + self._pather.traverse_nodes([300], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + #self._pather.traverse_nodes([228], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes([228], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes([226], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + pos_m = screen.convert_abs_to_monitor((50, -50)) self.walk(pos_m, force_move=True) - self.poison_nova(2.0) - pos_m = screen.convert_abs_to_monitor((50, 0)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=120,cast_div=5,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8) - pos_m = screen.convert_abs_to_monitor((-30, -20)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_skeleton',delay=1.2,offset=.8) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8) + self._do_curse(.5) + self._trav_attack_sequence() + # move a bit to the top + return True def kill_nihlathak(self, end_nodes: list[int]) -> bool: + self.bone_armor() # Move close to nihlathak self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=True) - pos_m = screen.convert_abs_to_monitor((20, 20)) - self.walk(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - self.poison_nova(3.0) - pos_m = screen.convert_abs_to_monitor((50, 0)) - self.pre_move() - self.move(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=7200,cast_div=2,cast_v_div=2,cast_spell='corpse_explosion',delay=3.0,offset=1.8) - wait(self._cast_duration, self._cast_duration +.2) - self.poison_nova(3.0) + self._do_curse(.5) + self._nihl_attack_sequence() return True def kill_summoner(self) -> bool: # Attack - pos_m = screen.convert_abs_to_monitor((0, 30)) - self.walk(pos_m, force_move=True) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=4,cast_v_div=3,cast_spell='lower_res',delay=1.0) - wait(0.5) - self.poison_nova(3.0) - pos_m = screen.convert_abs_to_monitor((50, 0)) - self.pre_move() - self.move(pos_m, force_move=True) - wait(self._cast_duration, self._cast_duration + 0.2) - self._cast_circle(cast_dir=[-1,1],cast_start_angle=0,cast_end_angle=360,cast_div=10,cast_v_div=4,cast_spell='raise_mage',delay=1.2,offset=.8) + pos_m = convert_abs_to_monitor((0, 20)) + mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7]) + # Attack + self._do_curse(.5) + self.poison_nova(Config().char["atk_len_arc"]) + # Move a bit back and another round return True From 119e05990ae92bd3dae19797489cdaa31da17cfe Mon Sep 17 00:00:00 2001 From: Legit <96157236+D2RLegit@users.noreply.github.com> Date: Thu, 30 Jun 2022 02:01:38 -0500 Subject: [PATCH 2/5] option for only poison or only cold mob detect added optional only poison or only cold mob detect --- src/target_detect.py | 81 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/target_detect.py b/src/target_detect.py index 914bd7143..78573eb77 100644 --- a/src/target_detect.py +++ b/src/target_detect.py @@ -14,6 +14,14 @@ {"erode": 1, "blur": 3, "lh": 110, "ls": 169, "lv": 50, "uh": 120, "us": 255, "uv": 255} # frozen ] +FILTER_GREEN_RANGES=[ + {"erode": 1, "blur": 3, "lh": 38, "ls": 169, "lv": 50, "uh": 70, "us": 255, "uv": 255}, # poison +] + +FILTER_BLUE_RANGES=[ + {"erode": 1, "blur": 3, "lh": 110, "ls": 169, "lv": 50, "uh": 120, "us": 255, "uv": 255} # frozen +] + @dataclass class TargetInfo: roi: tuple = None @@ -69,6 +77,79 @@ def get_visible_targets( targets = sorted(targets, key=lambda obj: obj.distance) return targets +def get_visible_green_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_GREEN_RANGES: + filterimage, threshz = _process_image(img, mask_char=True, mask_hud=True, **filter) # HSV Filter for BLUE Only (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) + 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: + targets = sorted(targets, key=lambda obj: obj.distance) + return targets + +def get_visible_blue_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_BLUE_RANGES: + filterimage, threshz = _process_image(img, mask_char=True, mask_hud=True, **filter) # HSV Filter for BLUE Only (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) + 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: + targets = sorted(targets, key=lambda obj: obj.distance) + return targets + + def _bright_contrast(img: np.ndarray, brightness: int = 255, contrast: int = 127): """ :param img: The image to which filters should be applied From db940fa18d9fc6b4959e44d1c531015a5d975d9a Mon Sep 17 00:00:00 2001 From: Legit <96157236+D2RLegit@users.noreply.github.com> Date: Thu, 30 Jun 2022 02:07:57 -0500 Subject: [PATCH 3/5] cleanup of uneeded skills --- src/char/poison_necro.py | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/char/poison_necro.py b/src/char/poison_necro.py index cf950dfc0..3ae587896 100644 --- a/src/char/poison_necro.py +++ b/src/char/poison_necro.py @@ -81,18 +81,6 @@ def _clay_golem(self): mouse.click(button="right") wait(self._cast_duration) - def amp_dmg(self, hork_time: int): - wait(0.5) - if self._skill_hotkeys["amp_dmg"]: - keyboard.send(self._skill_hotkeys["amp_dmg"]) - wait(0.5, 0.15) - pos_m = convert_abs_to_monitor((0, -20)) - mouse.move(*pos_m) - wait(0.5, 0.15) - mouse.press(button="right") - wait(hork_time) - mouse.release(button="right") - def bone_armor(self): if self._skill_hotkeys["bone_armor"]: keyboard.send(self._skill_hotkeys["bone_armor"]) @@ -113,7 +101,6 @@ def _bone_armor(self): wait(self._cast_duration) - def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10): keyboard.send(Config().char["stand_still"], do_release=False) if self._skill_hotkeys["skill_left"]: @@ -154,21 +141,7 @@ def _do_curse(self, hork_time: int): wait(0.05, 0.15) mouse.press(button="right") wait(hork_time) - mouse.release(button="right") - - def _force_teleport(self, cast_pos_abs: tuple[float, float], spray: float = 10): - if not self._skill_hotkeys["force_teleport"]: - raise ValueError("You did not set a hotkey for force_teleport!") - keyboard.send(self._skill_hotkeys["force_teleport"]) - x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) - y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) - cast_pos_monitor = convert_abs_to_monitor((x, y)) - mouse.move(*cast_pos_monitor) - click_tries = random.randint(2, 4) - for _ in range(click_tries): - mouse.press(button="right") - wait(0.09, 0.12) - mouse.release(button="right") + mouse.release(button="right") def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: float = 10): if not self._skill_hotkeys["raise_skeleton"]: From aef8190e655ef4ecfed60f2851b263787bc962f0 Mon Sep 17 00:00:00 2001 From: Legit <96157236+D2RLegit@users.noreply.github.com> Date: Thu, 30 Jun 2022 02:09:40 -0500 Subject: [PATCH 4/5] Renamed lower_res to curse --- config/params.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/params.ini b/config/params.ini index 21d5949ee..fbaa5f01c 100644 --- a/config/params.ini +++ b/config/params.ini @@ -249,8 +249,8 @@ clay_golem= clear_pindle_pack= corpse_explosion= heart_of_wolverine= -# Can Use any curse -lower_res= +# Can Use any curse but will likely want Lower resist +curse= poison_nova= raise_mage= raise_revive= From 3b693281b7bbc0388ab28388d943e9c4ecad3778 Mon Sep 17 00:00:00 2001 From: Legit <96157236+D2RLegit@users.noreply.github.com> Date: Thu, 30 Jun 2022 12:03:15 -0500 Subject: [PATCH 5/5] Add files via upload --- assets/hud_mask.png | Bin 9552 -> 13450 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/hud_mask.png b/assets/hud_mask.png index 36d9c912dbd8172506a928cfb9a8164ce1e5d6d7..b6adcb563febfd624cd094b829718e2f49ca9f74 100644 GIT binary patch literal 13450 zcmeHOc|6ql|NoGR6n3lan`{kj(Q3xr7ZW9m3WcaGH0D5I#>}`CU90WTp;8oTmG5+r za_6k=SaRlAD@QqE3MEJ6_`N=qZ{PiW_gjzOciZppkLDpWX5OFo`*l2Duh;YS`b
f(Vc<>}_jGu?QrB39r$&^|UZ!OC;M^ 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 AivR!s literal 9552 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