diff --git a/README.md b/README.md index 3f59402d3..a1d1e6d2b 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,11 @@ order=run_pindle, run_eldritch | ------------------ | -------------------------------------------------------------------------------------------------| | type | Build type. Currently only "sorceress" or "hammerdin" is supported | | belt_rows | Integer value of how many rows the char's belt has | -| casting_frames | Depending on your char and fcr you will have a specific casting frame count. Check it here: https://diablo2.diablowiki.net/Breakpoints and fill in the right number. Determines how much delay there is after each teleport for example. If your system has some delay e.g. on vms, you might have to increase this value above the suggest value in the table! | +| faster_cast_rate | Set to your character's faster cast rate, will calculate skill cooldowns | +| extra_casting_frames | This will cause skills to wait an additional `extra_casting_frames` after calculated cooldown period. Helpful for low-performance. | | cta_available | 0: no cta available, 1: cta is available and should be used during prebuff | -| safer_routines | Set to 1 to enable optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players) +| safer_routines | Set to 1 to enable optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players) | +| use_charged_teleport | 0: Character doesn't teleport or is able to teleport without charges. 1: Character depends on teleport charges to teleport. | | num_loot_columns | Number of columns in inventory used for loot (from left!). Remaining space can be used for charms | | force_move | Hotkey for "force move" | | inventory_screen | Hotkey to open inventory | diff --git a/assets/NTItemAlias.dbl b/assets/NTItemAlias.dbl index 282450f6c..14af6b9ee 100644 --- a/assets/NTItemAlias.dbl +++ b/assets/NTItemAlias.dbl @@ -939,7 +939,7 @@ NTIPAliasStat["skillpoisonoverridelength"] = 101; NTIPAliasStat["itemfasterblockrate"] = 102; NTIPAliasStat["fbr"] = 102; NTIPAliasStat["skillbypassundead"] = 103; NTIPAliasStat["skillbypassdemons"] = 104; -NTIPAliasStat["itemfastercastrate"] = 105; NTIPAliasStat["fcr"] = 105; +NTIPAliasStat["itemfastercastrate"] = 105; NTIPAliasStat["faster_cast_rate"] = 105; NTIPAliasStat["skillbypassbeasts"] = 106; NTIPAliasStat["itemsingleskill"] = 107; diff --git a/assets/d2r_settings.json b/assets/d2r_settings.json index 516dad448..4a49f7197 100644 --- a/assets/d2r_settings.json +++ b/assets/d2r_settings.json @@ -43,8 +43,8 @@ "Item Name Display": 1, "Chat Background": 0, "Always Run": 1, - "Quick Cast Enabled": 0, - "Display Active Skill Bindings": 0, + "Quick Cast Enabled": 1, + "Display Active Skill Bindings": 1, "Lobby Wide Item Drop Enabled": 1, "Item Tooltip Hotkey Appender": 1 } diff --git a/assets/templates/inventory/active_weapon_main.png b/assets/templates/inventory/active_weapon_main.png new file mode 100644 index 000000000..dbd9729d5 Binary files /dev/null and b/assets/templates/inventory/active_weapon_main.png differ diff --git a/assets/templates/inventory/active_weapon_offhand.png b/assets/templates/inventory/active_weapon_offhand.png new file mode 100644 index 000000000..617b3047b Binary files /dev/null and b/assets/templates/inventory/active_weapon_offhand.png differ diff --git a/assets/templates/ui/skills/bar_blessed_hammer_active.png b/assets/templates/ui/skills/bar_blessed_hammer_active.png new file mode 100644 index 000000000..7c53cf450 Binary files /dev/null and b/assets/templates/ui/skills/bar_blessed_hammer_active.png differ diff --git a/assets/templates/ui/skills/bar_blessed_hammer_inactive.png b/assets/templates/ui/skills/bar_blessed_hammer_inactive.png new file mode 100644 index 000000000..007af820d Binary files /dev/null and b/assets/templates/ui/skills/bar_blessed_hammer_inactive.png differ diff --git a/assets/templates/ui/skills/bar_charge_active.png b/assets/templates/ui/skills/bar_charge_active.png new file mode 100644 index 000000000..e24788567 Binary files /dev/null and b/assets/templates/ui/skills/bar_charge_active.png differ diff --git a/assets/templates/ui/skills/bar_charge_inactive.png b/assets/templates/ui/skills/bar_charge_inactive.png new file mode 100644 index 000000000..30dc42907 Binary files /dev/null and b/assets/templates/ui/skills/bar_charge_inactive.png differ diff --git a/assets/templates/ui/skills/bar_holy_shield.png b/assets/templates/ui/skills/bar_holy_shield.png new file mode 100644 index 000000000..9020f5cc1 Binary files /dev/null and b/assets/templates/ui/skills/bar_holy_shield.png differ diff --git a/assets/templates/ui/skills/bar_tele_active.png b/assets/templates/ui/skills/bar_tele_active.png new file mode 100644 index 000000000..5dc1b5abe Binary files /dev/null and b/assets/templates/ui/skills/bar_tele_active.png differ diff --git a/assets/templates/ui/skills/bar_tele_inactive.png b/assets/templates/ui/skills/bar_tele_inactive.png new file mode 100644 index 000000000..5c763d5f8 Binary files /dev/null and b/assets/templates/ui/skills/bar_tele_inactive.png differ diff --git a/assets/templates/ui/skills/bar_tp_active.png b/assets/templates/ui/skills/bar_tp_active.png new file mode 100644 index 000000000..a8c4cc14f Binary files /dev/null and b/assets/templates/ui/skills/bar_tp_active.png differ diff --git a/assets/templates/ui/skills/bar_tp_inactive.png b/assets/templates/ui/skills/bar_tp_inactive.png new file mode 100644 index 000000000..9acadfa0b Binary files /dev/null and b/assets/templates/ui/skills/bar_tp_inactive.png differ diff --git a/assets/templates/ui/skills/bar_vigor.png b/assets/templates/ui/skills/bar_vigor.png new file mode 100644 index 000000000..21d8513f7 Binary files /dev/null and b/assets/templates/ui/skills/bar_vigor.png differ diff --git a/config/game.ini b/config/game.ini index 3e6a2887b..29879211e 100644 --- a/config/game.ini +++ b/config/game.ini @@ -107,7 +107,7 @@ repair_btn=318,473,90,80 left_inventory=35,92,378,378 right_inventory=866,348,379,152 skill_right=664,673,41,41 -skill_right_expanded=655,375,385,255 +skill_speed_bar=655,375,385,255 skill_left=574,673,41,41 main_menu_top_left=0,0,100,100 gamebar_anchor=600,650,90,90 @@ -136,6 +136,8 @@ tab_indicator=31,82,385,14 deposit_btn=454,365,186,43 equipped_inventory_area=861,59,387,287 inventory_bg_pattern=21,622,1236,75 +active_skills_bar=372,605,538,47 +active_weapon_tabs=864,65,93,30 [path] ; static pathes in format: x0,y0, x1,y1, x2,y2, ... diff --git a/config/params.ini b/config/params.ini index 21d5949ee..94f08ec5e 100644 --- a/config/params.ini +++ b/config/params.ini @@ -46,10 +46,14 @@ order=run_pindle, run_eldritch_shenk ; These configs have to be alligned with your d2r settings and char build type=light_sorc belt_rows=4 -casting_frames=10 +faster_cast_rate=105 +faster_cast_rate_offhand=105 +extra_casting_frames=1 cta_available=0 ; safer_routines: enable for optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players) safer_routines=0 +; use_charged_teleport: set to 1 if your character depends on teleport charges to teleport +use_charged_teleport=0 ; num_loot_columns: Number of empty columns from left to right of inventory to be used for looting. ; Store charms, etc. to the right of the inventory. @@ -313,8 +317,6 @@ logg_lvl=debug message_body_template={{"content": "{msg}"}} message_headers= ocr_during_pickit=0 -;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly -override_capabilities= -pathing_delay_factor=4 +pathing_delay_factor=0 ;If you want to control Hyper-V window from host use 0,51 here window_client_area_offset=0,0 diff --git a/src/bot.py b/src/bot.py index 01f8ee969..bde961340 100644 --- a/src/bot.py +++ b/src/bot.py @@ -264,11 +264,12 @@ def on_start_from_town(self): view.pickup_corpse() wait_until_hidden(ScreenObjects.Corpse) belt.fill_up_belt_from_inventory(Config().char["num_loot_columns"]) - self._char.discover_capabilities() - if corpse_present and self._char.capabilities.can_teleport_with_charges and not self._char.select_tp(): + if self._char.capabilities is None or not self._char.capabilities.can_teleport_natively: + self._char.discover_capabilities() + if corpse_present and self._char.capabilities.can_teleport_with_charges: keybind = Config().char["teleport"] Logger.info(f"Teleport keybind is lost upon death. Rebinding teleport to '{keybind}'") - self._char.remap_right_skill_hotkey("TELE_ACTIVE", Config().char["teleport"]) + skills.remap_right_skill_hotkey("TELE_ACTIVE", keybind) # Run /nopickup command to avoid picking up stuff on accident if Config().char["enable_no_pickup"] and (not self._ran_no_pickup and not self._game_stats._nopickup_active): @@ -301,6 +302,9 @@ def on_maintenance(self): need_inspect |= (self._game_stats._run_counter - 1) % Config().char["runs_per_stash"] == 0 if need_inspect: img = personal.open() + # Check which weapon is bound (main vs offhand) + if self._char.main_weapon_equipped is None: + self._char.main_weapon_equipped = personal.is_main_weapon_active(img) # Update TP, ID, key needs if self._game_stats._game_counter == 1: self._use_id_tome = common.tome_state(img, 'id')[0] is not None @@ -368,7 +372,7 @@ def on_maintenance(self): # Check if we are out of tps or need repairing need_repair = is_visible(ScreenObjects.NeedRepair) need_routine_repair = False if not Config().char["runs_per_repair"] else self._game_stats._run_counter % Config().char["runs_per_repair"] == 0 - need_refill_teleport = self._char.capabilities.can_teleport_with_charges and (not self._char.select_tp() or self._char.is_low_on_teleport_charges()) + need_refill_teleport = self._char.capabilities.can_teleport_with_charges and (not self._char.select_teleport() or self._char.is_low_on_teleport_charges()) if need_repair or need_routine_repair or need_refill_teleport or sell_items: if need_repair: Logger.info("Repair needed. Gear is about to break") diff --git a/src/char/__init__.py b/src/char/__init__.py index b60068a00..02390943f 100644 --- a/src/char/__init__.py +++ b/src/char/__init__.py @@ -1,2 +1 @@ -from .i_char import IChar -from .capabilities import CharacterCapabilities \ No newline at end of file +from .i_char import IChar \ No newline at end of file diff --git a/src/char/barbarian.py b/src/char/barbarian.py index 50f3f0332..d8b6e03a2 100644 --- a/src/char/barbarian.py +++ b/src/char/barbarian.py @@ -1,7 +1,8 @@ import keyboard from ui import skills from utils.custom_mouse import mouse -from char import IChar, CharacterCapabilities +from char import IChar +from char.tools.capabilities import CharacterCapabilities import template_finder from pather import Pather from logger import Logger @@ -17,7 +18,6 @@ def __init__(self, skill_hotkeys: dict, pather: Pather): Logger.info("Setting up Barbarian") super().__init__(skill_hotkeys) self._pather = pather - self._do_pre_move = True # offset shenk final position further to the right and bottom def _cast_war_cry(self, time_in_s: float): @@ -70,12 +70,10 @@ def pre_buff(self): wait(self._cast_duration + 0.08, self._cast_duration + 0.1) 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 leap if set should_cast_leap = self._skill_hotkeys["leap"] and not skills.is_left_skill_selected(["LEAP"]) - can_teleport = self.capabilities.can_teleport_natively and skills.is_right_skill_active() - if should_cast_leap and not can_teleport: + if should_cast_leap and not self.can_teleport(): keyboard.send(self._skill_hotkeys["leap"]) wait(0.15, 0.25) @@ -90,11 +88,7 @@ def kill_pindle(self) -> bool: 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["concentration"]) - # 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) - self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=0.1) + self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0) self._cast_war_cry(Config().char["atk_len_pindle"]) wait(0.1, 0.15) self._do_hork(4) @@ -104,7 +98,7 @@ def kill_eldritch(self) -> bool: if self.capabilities.can_teleport_natively: self._pather.traverse_nodes_fixed("eldritch_end", self) else: - self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, do_pre_move=self._do_pre_move) + self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0) wait(0.05, 0.1) self._cast_war_cry(Config().char["atk_len_eldritch"]) wait(0.1, 0.15) @@ -112,7 +106,7 @@ def kill_eldritch(self) -> bool: return True def kill_shenk(self): - self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=self._do_pre_move) + self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0) wait(0.05, 0.1) self._cast_war_cry(Config().char["atk_len_shenk"]) wait(0.1, 0.15) diff --git a/src/char/basic.py b/src/char/basic.py index 6703094f9..dbd5f2a73 100644 --- a/src/char/basic.py +++ b/src/char/basic.py @@ -1,7 +1,8 @@ import keyboard from ui import skills from utils.custom_mouse import mouse -from char import IChar,CharacterCapabilities +from char import IChar +from char.tools.capabilities import CharacterCapabilities import template_finder from pather import Pather from logger import Logger @@ -17,7 +18,6 @@ def __init__(self, skill_hotkeys: dict, pather: Pather): Logger.info("Setting up Basic Character") super().__init__(skill_hotkeys) self._pather = pather - self._do_pre_move = True def on_capabilities_discovered(self, capabilities: CharacterCapabilities): # offset shenk final position further to the right and bottom @@ -43,8 +43,7 @@ def _cast_attack_pattern(self, time_in_s: float): keyboard.send(Config().char["stand_still"], do_press=False) def pre_buff(self): - if Config().char["cta_available"]: - self._pre_buff_cta() + self._pre_buff_cta() if self._skill_hotkeys["buff_1"]: keyboard.send(self._skill_hotkeys["buff_1"]) wait(0.5, 0.15) @@ -74,11 +73,7 @@ def kill_pindle(self) -> bool: 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["concentration"]) - # 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) - self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=0.1) + self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0) self._cast_attack_pattern(Config().char["atk_len_pindle"]) wait(0.1, 0.15) return True @@ -87,19 +82,13 @@ def kill_eldritch(self) -> bool: if self.capabilities.can_teleport_natively: self._pather.traverse_nodes_fixed("eldritch_end", self) else: - if not self._do_pre_move: - # keyboard.send(self._skill_hotkeys["concentration"]) - # wait(0.05, 0.15) - self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, do_pre_move=self._do_pre_move) + self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0) wait(0.05, 0.1) self._cast_attack_pattern(Config().char["atk_len_eldritch"]) return True def kill_shenk(self): - # if not self._do_pre_move: - # keyboard.send(self._skill_hotkeys["concentration"]) - # 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) + self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0) wait(0.05, 0.1) self._cast_attack_pattern(Config().char["atk_len_shenk"]) wait(0.1, 0.15) diff --git a/src/char/basic_ranged.py b/src/char/basic_ranged.py index 50215ad5f..4e0b4bf48 100644 --- a/src/char/basic_ranged.py +++ b/src/char/basic_ranged.py @@ -19,9 +19,8 @@ def __init__(self, skill_hotkeys: dict, pather: Pather): Logger.info("Setting up Basic Ranged Character") super().__init__(skill_hotkeys) self._pather = pather - self._do_pre_move = True - def _left_attack(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.2, 0.3), spray: int = 10): + def _left_attack(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.2, 0.3), spray: float = 10): if self._skill_hotkeys["left_attack"]: keyboard.send(self._skill_hotkeys["left_attack"]) for _ in range(4): @@ -43,8 +42,7 @@ def _right_attack(self, cast_pos_abs: tuple[float, float], delay: tuple[float, f mouse.click(button="right") def pre_buff(self): - if Config().char["cta_available"]: - self._pre_buff_cta() + self._pre_buff_cta() if self._skill_hotkeys["buff_1"]: keyboard.send(self._skill_hotkeys["buff_1"]) wait(0.5, 0.15) diff --git a/src/char/bone_necro.py b/src/char/bone_necro.py index b45f97b0e..a512fc586 100644 --- a/src/char/bone_necro.py +++ b/src/char/bone_necro.py @@ -6,7 +6,8 @@ from logger import Logger from screen import grab, convert_abs_to_monitor, convert_screen_to_abs from config import Config -from utils.misc import wait, rotate_vec, unit_vector +from utils.misc import wait +from char.tools import calculations import random from pather import Location, Pather import screen as screen @@ -29,7 +30,7 @@ def move_to(self, x, y): self.pre_move() self.move(pos_m, force_move=True) - def bone_wall(self, cast_pos_abs: tuple[float, float], spray: int): + def bone_wall(self, cast_pos_abs: tuple[float, float], spray: float): if not self._skill_hotkeys["bone_wall"]: raise ValueError("You did not set bone_wall hotkey!") keyboard.send(Config().char["stand_still"], do_release=False) @@ -47,8 +48,7 @@ def bone_wall(self, cast_pos_abs: tuple[float, float], spray: int): def pre_buff(self): self.bone_armor() #only CTA if pre trav - if Config().char["cta_available"]: - self._pre_buff_cta() + self._pre_buff_cta() Logger.info("prebuff/cta") def _clay_golem(self): @@ -77,7 +77,7 @@ def _bone_armor(self): mouse.click(button="right") wait(self._cast_duration) - def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: int = 10,cast_count: int = 8): + def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: float = 10,cast_count: int = 8): keyboard.send(Config().char["stand_still"], do_release=False) Logger.debug(f'casting corpse explosion {cast_count} times with spray = {spray}') for _ in range(cast_count): @@ -104,8 +104,8 @@ def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, mouse.press(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)) + angle = calculations.lerp(cast_start_angle,cast_end_angle,float(i)/cast_div) + target = calculations.unit_vector(calculations.rotate_vec(cast_dir, angle)) Logger.debug(f"Circle cast - current angle: {angle}º") circle_pos_abs = get_closest_non_hud_pixel(pos = target*radius, pos_type="abs") circle_pos_monitor = convert_abs_to_monitor(circle_pos_abs) @@ -130,9 +130,9 @@ def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, def kill_pindle(self) -> bool: for pos in [[200,-100], [-150,100] ]: self.bone_wall(pos, spray=10) - self.cast_in_arc(ability='bone_spear', cast_pos_abs=[110,-50], spread_deg=15, time_in_s=5) + self._cast_in_arc(skill_name='bone_spear', cast_pos_abs=[110,-50], spread_deg=15, time_in_s=5) self._corpse_explosion([165,-75], spray=100, cast_count=5) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[110,-50], spread_deg=15, time_in_s=2.5) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[110,-50], spread_deg=15, time_in_s=2.5) self._pather.traverse_nodes_fixed("pindle_end", self) return True @@ -140,10 +140,10 @@ def kill_eldritch(self) -> bool: #build an arc of bone walls for pos in [[50,-200], [-200,-175], [-350,50]]: self.bone_wall(pos, spray=10) - self.cast_in_arc(ability='teeth', cast_pos_abs=[-20,-150], spread_deg=15, time_in_s=3) - self.cast_in_arc(ability='bone_spear', cast_pos_abs=[-20,-150], spread_deg=15, time_in_s=2) + self._cast_in_arc(skill_name='teeth', cast_pos_abs=[-20,-150], spread_deg=15, time_in_s=3) + self._cast_in_arc(skill_name='bone_spear', cast_pos_abs=[-20,-150], spread_deg=15, time_in_s=2) self._corpse_explosion([-20,-240], spray=100, cast_count=5) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[0,-80], spread_deg=60, time_in_s=2.5) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[0,-80], spread_deg=60, time_in_s=2.5) self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=0.6, force_tp=True) self.bone_armor() return True @@ -151,12 +151,12 @@ def kill_eldritch(self) -> bool: def kill_shenk(self) -> bool: self._cast_circle(cast_dir=[1,1],cast_start_angle=0,cast_end_angle=360,cast_div=5,cast_spell='bone_wall',delay=.8,radius=100, hold=False) - self.cast_in_arc(ability='teeth', cast_pos_abs=[160,75], spread_deg=360, time_in_s=6) - self.cast_in_arc(ability='teeth', cast_pos_abs=[160,75], spread_deg=30, time_in_s=2) + self._cast_in_arc(skill_name='teeth', cast_pos_abs=[160,75], spread_deg=360, time_in_s=6) + self._cast_in_arc(skill_name='teeth', cast_pos_abs=[160,75], spread_deg=30, time_in_s=2) self._corpse_explosion([0,0], spray=200, cast_count=4) - self.cast_in_arc(ability='bone_spear', cast_pos_abs=[160,75], spread_deg=30, time_in_s=3) + self._cast_in_arc(skill_name='bone_spear', cast_pos_abs=[160,75], spread_deg=30, time_in_s=3) self._corpse_explosion([240,112], spray=200, cast_count=8) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[80,37], spread_deg=60, time_in_s=3) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[80,37], spread_deg=60, time_in_s=3) self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0) return True @@ -169,13 +169,13 @@ def kill_council(self) -> bool: #moat on right side, encircle with bone walls on the other 3 sides for pos in [[100,-100], [-125,-25], [-50,100]]: self.bone_wall(pos, spray=10) - self.cast_in_arc(ability='teeth', cast_pos_abs=[40,-100], spread_deg=180, time_in_s=5) - self.cast_in_arc(ability='bone_spear', cast_pos_abs=[40,-100], spread_deg=120, time_in_s=8) + self._cast_in_arc(skill_name='teeth', cast_pos_abs=[40,-100], spread_deg=180, time_in_s=5) + self._cast_in_arc(skill_name='bone_spear', cast_pos_abs=[40,-100], spread_deg=120, time_in_s=8) self._corpse_explosion([40,-100], spray=200, cast_count=8) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[20,-50], spread_deg=180, time_in_s=5) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[20,-50], spread_deg=180, time_in_s=5) self._corpse_explosion([40,-100], spray=200, cast_count=8) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[20,-50], spread_deg=360, time_in_s=4) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[20,-50], spread_deg=360, time_in_s=4) return True @@ -189,14 +189,14 @@ def kill_nihlathak(self, end_nodes: list[int]) -> bool: cast_pos_abs = np.array(nihlathak_pos_abs)*.2 self._cast_circle(cast_dir=[1,1],cast_start_angle=0,cast_end_angle=360,cast_div=5,cast_spell='bone_wall',delay=.8,radius=100, hold=False) self._bone_armor() - self.cast_in_arc(ability='teeth', cast_pos_abs=cast_pos_abs, spread_deg=150, time_in_s=5) + self._cast_in_arc(skill_name='teeth', cast_pos_abs=cast_pos_abs, spread_deg=150, time_in_s=5) self._bone_armor() self._corpse_explosion(cast_pos_abs, spray=200, cast_count=8) - self.cast_in_arc(ability='bone_spear', cast_pos_abs=cast_pos_abs, spread_deg=10, time_in_s=5) + self._cast_in_arc(skill_name='bone_spear', cast_pos_abs=cast_pos_abs, spread_deg=10, time_in_s=5) self._bone_armor() self._corpse_explosion(np.array(nihlathak_pos_abs)*.75, spray=200, cast_count=10) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=cast_pos_abs, spread_deg=30, time_in_s=2.5) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=cast_pos_abs, spread_deg=30, time_in_s=2.5) # Move to items wait(self._cast_duration, self._cast_duration + 0.2) @@ -205,10 +205,10 @@ def kill_nihlathak(self, end_nodes: list[int]) -> bool: def kill_summoner(self) -> bool: # Attack - self.cast_in_arc(ability='teeth', cast_pos_abs=[30,30], spread_deg=360, time_in_s=3) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[30,30], spread_deg=360, time_in_s=2) + self._cast_in_arc(skill_name='teeth', cast_pos_abs=[30,30], spread_deg=360, time_in_s=3) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[30,30], spread_deg=360, time_in_s=2) self._corpse_explosion([0,0], spray=200, cast_count=8) - self.cast_in_arc(ability='bone_spirit', cast_pos_abs=[30,30], spread_deg=360, time_in_s=2) + self._cast_in_arc(skill_name='bone_spirit', cast_pos_abs=[30,30], spread_deg=360, time_in_s=2) return True diff --git a/src/char/i_char.py b/src/char/i_char.py index 4374b43ef..f0bacaf76 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -1,84 +1,158 @@ from typing import Callable +import keyboard +import math import random import time -import cv2 -import math +from functools import cached_property + +from char.tools.capabilities import CharacterCapabilities +from char.tools.skill_data import get_cast_wait_time +from char.tools import calculations +from config import Config from item import consumables -import keyboard -import numpy as np -from char.capabilities import CharacterCapabilities -from ui_manager import is_visible, wait_until_visible +from logger import Logger +from screen import grab, convert_monitor_to_screen, convert_screen_to_abs, convert_abs_to_monitor, convert_abs_to_screen from ui import skills +from ui_manager import ScreenObjects, get_closest_non_hud_pixel, is_visible, wait_until_visible from utils.custom_mouse import mouse -from utils.misc import wait, cut_roi, is_in_roi, color_filter, arc_spread -from logger import Logger -from config import Config -from screen import grab, convert_monitor_to_screen, convert_screen_to_abs, convert_abs_to_monitor, convert_screen_to_monitor +from utils.misc import wait, is_in_roi import template_finder -from ui_manager import detect_screen_object, ScreenObjects, get_closest_non_hud_pixel +from target_detect import get_visible_targets class IChar: _CrossGameCapabilities: None | CharacterCapabilities = None def __init__(self, skill_hotkeys: dict): + self._active_aura = "" + self._mouse_click_held = { + "left": False, + "right": False + } + self._key_held = dict.fromkeys([v[0] for v in keyboard._winkeyboard.official_virtual_keys.values()], False) self._skill_hotkeys = skill_hotkeys - self._last_tp = time.time() - # Add a bit to be on the save side - self._cast_duration = Config().char["casting_frames"] * 0.04 + 0.01 + self._standing_still = False + self.default_move_skill = "" self.damage_scaling = float(Config().char.get("damage_scaling", 1.0)) - self.capabilities = None - self._active_skill = { - "left": "", - "right": "" - } self._use_safer_routines = Config().char["safer_routines"] + self._base_class = "" + self.capabilities = None + self.main_weapon_equipped = None + self._last_cast_time = 0 + self._last_cast_skill = "" + self._current_fcr = Config().char["faster_cast_rate"] + + """ + MOUSE AND KEYBOARD METHODS + """ + + @staticmethod + def _handle_delay(delay: float | list | tuple | None = None): + if delay is None: + return + if isinstance(delay, (list, tuple)): + wait(*delay) + else: + try: + wait(delay) + except Exception as e: + Logger.warning(f"Failed to delay with delay: {delay}. Exception: {e}") + + def _key_press(self, key: str, hold_time: float | list | tuple | None = 0.04): + self._key_held[key] = True + keyboard.send(key, do_release=False) + self._handle_delay(hold_time) + keyboard.send(key, do_press=False) + Logger.debug(f"Pressed key: {key} for {hold_time}s at {round(time.time(),3)}") + self._key_held[key] = False + + def _key_hold(self, key: str, enable: bool = True): + if enable and not self._key_held[key]: + self._key_held[key] = True + keyboard.send(key, do_release=False) + elif not enable: + self._key_held[key] = False + keyboard.send(key, do_press=False) + + def _click(self, mouse_click_type: str = "left", hold_time: float | list | tuple | None = None): + if not hold_time: + mouse.click(button = mouse_click_type) + else: + mouse.press(button = mouse_click_type) + self._handle_delay(hold_time) + mouse.release(button = mouse_click_type) + + def _click_hold(self, mouse_click_type: str = "left", enable: bool = True): + if enable: + if not self._mouse_click_held[mouse_click_type]: + self._mouse_click_held[mouse_click_type] = True + mouse.press(button = mouse_click_type) + else: + if self._mouse_click_held[mouse_click_type]: + self._mouse_click_held[mouse_click_type] = False + mouse.release(button = mouse_click_type) + + def _click_left(self, hold_time: float | list | tuple | None = None): + self._click("left", hold_time = hold_time) - def _set_active_skill(self, mouse_click_type: str = "left", skill: str =""): - self._active_skill[mouse_click_type] = skill + def _click_right(self, hold_time: float | list | tuple | None = None): + self._click("right", hold_time = hold_time) - def _select_skill(self, skill: str, mouse_click_type: str = "left", delay: float | list | tuple = None): + def _get_hotkey(self, skill: str) -> str | None: if not ( - skill in self._skill_hotkeys and (hotkey := self._skill_hotkeys[skill]) + (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 + # Logger.warning(f"No hotkey for skill: {skill}") + return None + return hotkey - 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 + """ + CAPABILITIES METHODS + """ + + def _get_teleport_type(self) -> str: + """ + 1. player can teleport natively + a. and has teleport bound and is visible + b. and does not have teleport hotkey bound + 2. player can teleport with charges + a. and has teleport bound and is visible + b. and does not have teleport hotkey bound + c. and has run out of teleport charges + 3. player can't teleport + """ + + # 3. player can't teleport + if Config().char["use_charged_teleport"] and not self._get_hotkey("teleport"): + Logger.error("No hotkey for teleport even though param.ini 'use_charged_teleport' is set to True") + return "walk" + if not self._get_hotkey("teleport"): + return "walk" + # 2. player can teleport with charges + if Config().char["use_charged_teleport"]: + if not skills.is_skill_bound(["BAR_TP_ACTIVE", "BAR_TP_INACTIVE"]): + # 2c. + Logger.debug("can_teleport: player can teleport with charges, but has no teleport bound. Likely needs repair.") + # 2a. + return "charges" + # 1. player can teleport natively + if not Config().char["use_charged_teleport"] and skills.is_skill_bound(["BAR_TP_ACTIVE", "BAR_TP_INACTIVE"]): + return "native" + return "walk" + + @staticmethod + def _teleport_active(): + return skills.is_teleport_active() + + def can_teleport(self): + return (self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges) and self._teleport_active() def _discover_capabilities(self) -> CharacterCapabilities: - override = Config().advanced_options["override_capabilities"] - if override is None: - if Config().char["teleport"]: - if self.select_tp(): - if self.skill_is_charged(): - return CharacterCapabilities(can_teleport_natively=False, can_teleport_with_charges=True) - else: - return CharacterCapabilities(can_teleport_natively=True, can_teleport_with_charges=False) - return CharacterCapabilities(can_teleport_natively=False, can_teleport_with_charges=True) - else: - return CharacterCapabilities(can_teleport_natively=False, can_teleport_with_charges=False) - else: - Logger.debug(f"override_capabilities is set to {override}") - return CharacterCapabilities( - can_teleport_natively="can_teleport_natively" in override, - can_teleport_with_charges="can_teleport_with_charges" in override - ) - - def discover_capabilities(self, force = False): - if IChar._CrossGameCapabilities is None or force: + type = self._get_teleport_type() + return CharacterCapabilities(can_teleport_natively=(type == "native"), can_teleport_with_charges=(type == "charges")) + + def discover_capabilities(self): + if IChar._CrossGameCapabilities is None: capabilities = self._discover_capabilities() self.capabilities = capabilities Logger.info(f"Capabilities: {self.capabilities}") @@ -87,131 +161,256 @@ def discover_capabilities(self, force = False): def on_capabilities_discovered(self, capabilities: CharacterCapabilities): pass - def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0): - mouse.move(pos[0], pos[1]) - time.sleep(0.1) - mouse.click(button="left") - wait(0.25, 0.3) - return prev_cast_start + """ + SKILL / CASTING METHODS + """ + + @staticmethod + def _log_cast(skill_name: str, cast_pos_abs: tuple[float, float], spray: float, spread_deg: float, min_duration: float, max_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 spread_deg: + msg += f" with spread of {spread_deg}" + if min_duration or max_duration: + msg += f" for " + if min_duration: + msg += f"{round(min_duration, 1)}" + if min_duration and max_duration: + msg += f" to " + if max_duration: + msg += f"{round(min_duration, 1)}" + if min_duration or max_duration: + msg += f" sec" + if aura: + msg += f" with {aura} active" + Logger.debug(msg) + + def _randomize_position(self, pos_abs: tuple[float, float], spray: float = 0, spread_deg: float = 0) -> tuple[int, int]: + if spread_deg: + pos_abs = calculations.spread(pos_abs = pos_abs, spread_deg = spread_deg) + if spray: + pos_abs = calculations.spray(pos_abs = pos_abs, r = spray) + return get_closest_non_hud_pixel(pos_abs, "abs") + + def _wait_for_cooldown(self): + min_wait = get_cast_wait_time(class_base = self._base_class, skill_name = self._last_cast_skill, fcr = self._current_fcr) + # if there's still time remaining in cooldown, wait + while (time.time() - self._last_cast_time) < (min_wait): + wait(0.02) + + def _send_skill(self, skill_name: str, cooldown: bool = True, hold_time: float | list | tuple | None = None): + if cooldown: + self._wait_for_cooldown() + self._key_press(self._get_hotkey(skill_name), hold_time = hold_time) + self._last_cast_time = time.time() + self._last_cast_skill = skill_name + + def _activate_aura(self, skill_name: str, delay: float | list | tuple | None = (0.04, 0.08)): + if not self._get_hotkey(skill_name): + return False + if self._active_aura != skill_name: # if aura is already active, don't activate it again + self._active_aura = skill_name + Logger.debug(f"Switch to aura {skill_name}") + self._send_skill(skill_name = skill_name, cooldown = True, hold_time = delay) + return True - def select_by_template( + def _cast_simple(self, skill_name: str, duration: float | list | tuple | None = None, cooldown = True) -> bool: + """ + Casts a skill + """ + if not (hotkey := self._get_hotkey(skill_name)): + return False + if not self._key_held[hotkey]: # if skill is already active, don't activate it again + if not duration: + self._send_skill(skill_name = skill_name, cooldown = cooldown) + else: + self._stand_still(True) + self._send_skill(skill_name = skill_name, cooldown = cooldown, hold_time = duration) + self._stand_still(False) + return True + + def _cast_at_position( self, - template_type: str | list[str], - success_func: Callable = None, - timeout: float = 8, - threshold: float = 0.68, - telekinesis: bool = False + skill_name: str, + cast_pos_abs: tuple[float, float] = (0, 0), + spray: float = 0, + spread_deg: float = 0, + min_duration: float = 0, + max_duration: float = 0, + teleport_frequency: float = 0, + use_target_detect = False, + aura: str = None, ) -> bool: """ - Finds any template from the template finder and interacts with it - :param template_type: Strings or list of strings of the templates that should be searched for - :param success_func: Function that will return True if the interaction is successful e.g. return True when loading screen is reached, defaults to None - :param timeout: Timeout for the whole template selection, defaults to None - :param threshold: Threshold which determines if a template is found or not. None will use default form .ini files - :return: True if success. False otherwise + Casts a skill toward a given target. + :param skill_name: name of skill to cast + :param cast_pos_abs: default absolute position to cast at. Defaults to origin (0, 0) + :param spray: apply randomization within circle of radius 'spray' centered at cast_pos_abs + :param spread_deg: apply randomization of cast position distributed along arc between theta of spread_deg + :param min_duration: hold down skill key for minimum 'duration' seconds + :param max_duration: hold down skill key for maximum 'duration' seconds + :param teleport_frequency: teleport to origin every 'teleport_frequency' seconds + :param use_target_detect: override cast_pos_abs with closest target position + :param aura: name of aura to attempt to keep active while casting + :return: True if function finished, False otherwise """ - if type(template_type) == list and "A5_STASH" in template_type: - # sometimes waypoint is opened and stash not found because of that, check for that - if is_visible(ScreenObjects.WaypointLabel): - keyboard.send("esc") - start = time.time() - while timeout is None or (time.time() - start) < timeout: - template_match = template_finder.search(template_type, grab(), threshold=threshold) - if template_match.valid: - Logger.debug(f"Select {template_match.name} ({template_match.score*100:.1f}% confidence)") - mouse.move(*template_match.center_monitor) - wait(0.2, 0.3) - mouse.click(button="left") - # check the successfunction for 2 sec, if not found, try again - check_success_start = time.time() - while time.time() - check_success_start < 2: - if success_func is None or success_func(): - return True - Logger.error(f"Wanted to select {template_type}, but could not find it") - return False + if not self._get_hotkey(skill_name): + return False + self._log_cast(skill_name, cast_pos_abs, spray, spread_deg, min_duration, max_duration, aura) + if aura: + self._activate_aura(aura) + + mouse_move_delay = [0.4, 0.6] + if min_duration > max_duration: + max_duration = min_duration + + if max_duration: + self._stand_still(True) + start = time_of_last_tp = time.perf_counter() + # cast while time is less than max duration + while (elapsed_time := time.perf_counter() - start) < max_duration: + targets = None + # if target detection is enabled, use it to get target position + if use_target_detect: + targets = get_visible_targets() + pos_abs = targets[0].center_abs + pos_abs = self._randomize_position(pos_abs = pos_abs, spray = 5, spread_deg = 0) + # otherwise, use the given position with randomization parameters + else: + pos_abs = self._randomize_position(pos_abs = cast_pos_abs, spray = spray, spread_deg = spread_deg) + pos_m = convert_abs_to_monitor(pos_abs) + mouse.move(*pos_m, delay_factor=mouse_move_delay) + # start keyhold, if not already started + self._key_hold(self._get_hotkey(skill_name), True) + # if teleport frequency is set, teleport every teleport_frequency seconds + if teleport_frequency and (elapsed_time - time_of_last_tp) >= teleport_frequency: + self._key_hold(self._get_hotkey(skill_name), False) + wait(0.04, 0.08) + self._teleport_to_origin() + time_of_last_tp = elapsed_time + # if target detection is enabled and minimum time has elapsed and no targets remain, end casting + if use_target_detect and (elapsed_time > min_duration) and not targets: + break + self._key_hold(self._get_hotkey(skill_name), False) + self._last_cast_time = time.time() + self._last_cast_skill = skill_name + self._stand_still(False) + else: + random_abs = self._randomize_position(pos_abs = cast_pos_abs, spray = spray, spread_deg = spread_deg) + pos_m = convert_abs_to_monitor(random_abs) + mouse.move(*pos_m, delay_factor = [x/2 for x in mouse_move_delay]) + self._cast_simple(skill_name) - def skill_is_charged(self, img: np.ndarray = None) -> bool: - if img is None: - img = grab() - skill_img = cut_roi(img, Config().ui_roi["skill_right"]) - charge_mask, _ = color_filter(skill_img, Config().colors["blue"]) - if np.sum(charge_mask) > 0: - return True - return False + return True - def is_low_on_teleport_charges(self): - img = grab() - charges_remaining = skills.get_skill_charges(img) - if charges_remaining: - Logger.debug(f"{charges_remaining} teleport charges remain") - return charges_remaining <= 3 - else: - charges_present = self.skill_is_charged(img) - if charges_present: - Logger.error("is_low_on_teleport_charges: unable to determine skill charges, assume zero") - return True - - def _remap_skill_hotkey(self, skill_asset, hotkey, skill_roi, expanded_skill_roi): - x, y, w, h = skill_roi - x, y = convert_screen_to_monitor((x, y)) - mouse.move(x + w/2, y + h / 2) - mouse.click("left") - wait(0.3) - match = template_finder.search(skill_asset, grab(), threshold=0.84, roi=expanded_skill_roi) - if match.valid: - mouse.move(*match.center_monitor) - wait(0.3) - keyboard.send(hotkey) - wait(0.3) - mouse.click("left") - wait(0.3) - - def remap_right_skill_hotkey(self, skill_asset, hotkey): - return self._remap_skill_hotkey(skill_asset, hotkey, Config().ui_roi["skill_right"], Config().ui_roi["skill_right_expanded"]) - - def select_tp(self): - return skills.select_tp(Config().char["teleport"]) + """ + TODO: Update this fn + + def _cast_in_arc(self, skill_name: str, cast_pos_abs: tuple[float, float] = [0,-100], time_in_s: float = 3, spread_deg: float = 10, hold=True): + #scale cast time by damage_scaling + time_in_s *= self.damage_scaling + Logger.debug(f'Casting {skill_name} for {time_in_s:.02f}s at {cast_pos_abs} with {spread_deg}°') + if not self._skill_hotkeys[skill_name]: + raise ValueError(f"You did not set {skill_name} hotkey!") + self._stand_still(True) + + target = convert_abs_to_monitor(calculations.arc_spread(cast_pos_abs, spread_deg=spread_deg)) + mouse.move(*target,delay_factor=[0.95, 1.05]) + if hold: + self._hold_click("right", True) + start = time.time() + while (time.time() - start) < time_in_s: + target = convert_abs_to_monitor(calculations.arc_spread(cast_pos_abs, spread_deg=spread_deg)) + if hold: + mouse.move(*target, delay_factor=[3, 8]) + if not hold: + mouse.move(*target, delay_factor=[.2, .4]) + self._click_right(0.04) + wait(self._cast_duration, self._cast_duration) + + if hold: + self._hold_click("right", False) + self._stand_still(False) + """ + + """ + GLOBAL SKILLS + """ + + def _cast_teleport(self, cooldown: bool = True) -> bool: + return self._cast_simple(skill_name="teleport", cooldown=cooldown) + + def _cast_battle_orders(self) -> bool: + return self._cast_simple(skill_name="battle_orders") + + def _cast_battle_command(self) -> bool: + return self._cast_simple(skill_name="battle_command") + + def _cast_town_portal(self) -> bool: + if res := self._cast_simple(skill_name="town_portal"): + consumables.increment_need("tp", 1) + return res + + """ + CHARACTER ACTIONS AND MOVEMENT METHODS + """ + + def _weapon_switch(self): + if self.main_weapon_equipped is not None: + self.main_weapon_equipped = not self.main_weapon_equipped + equipped = "main" if self.main_weapon_equipped else "offhand" + Logger.debug(f"Switch to {equipped} weapon") + return self._send_skill("weapon_switch", cooldown=True) + + def _switch_to_main_weapon(self): + if self.main_weapon_equipped == False: + self._current_fcr = Config().char["faster_cast_rate"] + self._weapon_switch() + wait(0.04, 0.08) + + def _switch_to_offhand_weapon(self): + if self.main_weapon_equipped: + self._current_fcr = Config().char["faster_cast_rate_offhand"] + self._weapon_switch() + wait(0.04, 0.08) + + def _force_move(self): + self._send_skill("force_move", cooldown=False) + + def _stand_still(self, enable: bool): + if enable and not self._standing_still: + keyboard.send(self._get_hotkey("stand_still"), do_release=False) + self._standing_still = True + elif not enable and self._standing_still: + keyboard.send(self._get_hotkey("stand_still"), do_press=False) + self._standing_still = False + + def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0) -> float: + mouse.move(*pos) + self._click_left() + wait(0.25, 0.35) + return prev_cast_start def pre_move(self): - # 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") + pass + + def _teleport_to_origin(self): + random_abs = self._randomize_position(pos_abs = (0,0), spray = 5) + pos_m = convert_abs_to_monitor(random_abs) + self._teleport_to_position(pos_monitor = pos_m) - def move(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False): + def _teleport_to_position(self, pos_monitor: tuple[float, float]): factor = Config().advanced_options["pathing_delay_factor"] - if "teleport" in Config().char and Config().char["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") - wait(self._cast_duration, self._cast_duration + 0.02) - else: - # in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk" - pos_screen = convert_monitor_to_screen(pos_monitor) - pos_abs = convert_screen_to_abs(pos_screen) - dist = math.dist(pos_abs, (0, 0)) - min_wd = max(10, Config().ui_pos["min_walk_dist"]) - max_wd = random.randint(int(Config().ui_pos["max_walk_dist"] * 0.65), Config().ui_pos["max_walk_dist"]) - adjust_factor = max(max_wd, min(min_wd, dist - 50)) / max(min_wd, dist) - pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)] - x, y = convert_abs_to_monitor(pos_abs) - mouse.move(x, y, randomize=5, delay_factor=[factor*0.1, factor*0.14]) - wait(0.012, 0.02) - if force_move: - keyboard.send(Config().char["force_move"]) - else: - mouse.click(button="left") + mouse.move(*pos_monitor, randomize=3, delay_factor=[(1+factor)/25, (2+factor)/25]) + wait(0.04) + self._cast_teleport() - def walk(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_move: bool = False): + def _walk_to_position(self, pos_monitor: tuple[float, float], force_move: bool = False): factor = Config().advanced_options["pathing_delay_factor"] - # in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk" + # in case we want to walk we actually want to move a bit before the point cause d2r will always "overwalk" pos_screen = convert_monitor_to_screen(pos_monitor) pos_abs = convert_screen_to_abs(pos_screen) dist = math.dist(pos_abs, (0, 0)) @@ -220,27 +419,46 @@ def walk(self, pos_monitor: tuple[float, float], force_tp: bool = False, force_m adjust_factor = max(max_wd, min(min_wd, dist - 50)) / max(min_wd, dist) pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)] x, y = convert_abs_to_monitor(pos_abs) - mouse.move(x, y, randomize=5, delay_factor=[factor*0.1, factor*0.14]) + mouse.move(x, y, randomize=3, delay_factor=[(2+factor)/25, (4+factor)/25]) wait(0.012, 0.02) if force_move: - keyboard.send(Config().char["force_move"]) + self._force_move() + else: + self._click_left() + + def move( + self, + pos_monitor: tuple[float, float], + use_tp: bool = False, + force_move: bool = False, + ) -> float: + """ + Moves character to position. + :param pos_monitor: Position to move to (screen coordinates) + :param use_tp: Use teleport if able to + :param force_move: Use force_move hotkey to move if not teleporting + :return: Time of move completed. + """ + if use_tp and self.can_teleport(): # can_teleport() activates teleport hotkey if True + self._teleport_to_position(pos_monitor) else: - mouse.click(button="left") + self._walk_to_position(pos_monitor = pos_monitor, force_move=force_move) + return time.time() - def tp_town(self): + def tp_town(self) -> bool: # will check if tp is available and select the skill if not skills.has_tps(): - return False - mouse.click(button="right") - consumables.increment_need("tp", 1) + pos_m = convert_abs_to_monitor((0, Config().ui_pos["screen_height"]/2 - 5)) + mouse.move(*pos_m) + if not skills.has_tps(): + return False + self._cast_town_portal() roi_mouse_move = [ - int(Config().ui_pos["screen_width"] * 0.3), + round(Config().ui_pos["screen_width"] * 0.3), 0, - int(Config().ui_pos["screen_width"] * 0.4), - int(Config().ui_pos["screen_height"] * 0.7) + round(Config().ui_pos["screen_width"] * 0.4), + round(Config().ui_pos["screen_height"] * 0.7) ] - pos_away = convert_abs_to_monitor((-167, -30)) - wait(0.8, 1.3) # takes quite a while for tp to be visible start = time.time() retry_count = 0 while (time.time() - start) < 8: @@ -251,103 +469,85 @@ def tp_town(self): self.pre_move() self.move(pos_m) if skills.has_tps(): - mouse.click(button="right") - consumables.increment_need("tp", 1) - wait(0.8, 1.3) # takes quite a while for tp to be visible - if (template_match := detect_screen_object(ScreenObjects.TownPortal)).valid: + self._cast_town_portal() + else: + pos_m = convert_abs_to_monitor((0, Config().ui_pos["screen_height"]/2 - 5)) + mouse.move(*pos_m) + if skills.has_tps(): + self._cast_town_portal() + else: + return False + if (template_match := wait_until_visible(ScreenObjects.TownPortal, timeout=3)).valid: pos = template_match.center_monitor pos = (pos[0], pos[1] + 30) # Note: Template is top of portal, thus move the y-position a bit to the bottom - mouse.move(*pos, randomize=6, delay_factor=[0.9, 1.1]) - wait(0.08, 0.15) - mouse.click(button="left") + mouse.move(*pos, randomize=6, delay_factor=[0.4, 0.6]) + self._click_left() if wait_until_visible(ScreenObjects.Loading, 2).valid: return True # move mouse away to not overlay with the town portal if mouse is in center pos_screen = convert_monitor_to_screen(mouse.get_position()) if is_in_roi(roi_mouse_move, pos_screen): + pos_away = convert_abs_to_monitor((-167, -30)) mouse.move(*pos_away, randomize=40, delay_factor=[0.8, 1.4]) return False - def _pre_buff_cta(self): - # Save current skill img - skill_before = cut_roi(grab(), Config().ui_roi["skill_right"]) - # Try to switch weapons and select bo until we find the skill on the right skill slot - start = time.time() - switch_sucess = False - while time.time() - start < 4: - keyboard.send(Config().char["weapon_switch"]) - wait(0.3, 0.35) - 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 - - if not switch_sucess: - Logger.warning("You dont have Battle Command bound, or you do not have CTA. ending CTA buff") - Config().char["cta_available"] = 0 - else: - # We switched succesfully, let's pre buff - mouse.click(button="right") - wait(self._cast_duration + 0.16, self._cast_duration + 0.18) - 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) - - # Make sure the switch back to the original weapon is good - start = time.time() - while time.time() - start < 4: - keyboard.send(Config().char["weapon_switch"]) - wait(0.3, 0.35) - skill_after = cut_roi(grab(), Config().ui_roi["skill_right"]) - _, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED)) - if max_val > 0.9: - break - else: - Logger.warning("Failed to switch weapon, try again") - wait(0.5) - - - def vec_to_monitor(self, target): - circle_pos_abs = get_closest_non_hud_pixel(pos = target, pos_type="abs") - return convert_abs_to_monitor(circle_pos_abs) + def _pre_buff_cta(self) -> bool: + if not self._get_hotkey("cta_available"): + return False + if self.main_weapon_equipped: + self._switch_to_offhand_weapon() + self._cast_battle_command() + self._cast_battle_orders() + self._wait_for_cooldown() + self._switch_to_main_weapon() - def _lerp(self,a: float,b: float, f:float): - return a + f * (b - a) + def pre_buff(self): + pass - def cast_in_arc(self, ability: str, cast_pos_abs: tuple[float, float] = [0,-100], time_in_s: float = 3, spread_deg: float = 10, hold=True): - #scale cast time by damage_scaling - time_in_s *= self.damage_scaling - Logger.debug(f'Casting {ability} for {time_in_s:.02f}s at {cast_pos_abs} with {spread_deg}°') - if not self._skill_hotkeys[ability]: - raise ValueError(f"You did not set {ability} hotkey!") - keyboard.send(Config().char["stand_still"], do_release=False) - self._select_skill(skill = ability, mouse_click_type="right", delay=(0.02, 0.08)) + """ + OTHER METHODS + """ - target = self.vec_to_monitor(arc_spread(cast_pos_abs, spread_deg=spread_deg)) - mouse.move(*target,delay_factor=[0.95, 1.05]) - if hold: - mouse.press(button="right") + def select_by_template( + self, + template_type: str | list[str], + success_func: Callable = None, + timeout: float = 8, + threshold: float = 0.68, + telekinesis: bool = False + ) -> bool: + """ + Finds any template from the template finder and interacts with it + :param template_type: Strings or list of strings of the templates that should be searched for + :param success_func: Function that will return True if the interaction is successful e.g. return True when loading screen is reached, defaults to None + :param timeout: Timeout for the whole template selection, defaults to None + :param threshold: Threshold which determines if a template is found or not. None will use default form .ini files + :return: True if success. False otherwise + """ + if type(template_type) == list and "A5_STASH" in template_type: + # sometimes waypoint is opened and stash not found because of that, check for that + if is_visible(ScreenObjects.WaypointLabel): + keyboard.send("esc") start = time.time() - while (time.time() - start) < time_in_s: - target = self.vec_to_monitor(arc_spread(cast_pos_abs, spread_deg=spread_deg)) - if hold: - mouse.move(*target,delay_factor=[3, 8]) - if not hold: - mouse.move(*target,delay_factor=[.2, .4]) - wait(0.02, 0.04) - mouse.press(button="right") - wait(0.02, 0.06) - mouse.release(button="right") - wait(self._cast_duration, self._cast_duration) - - if hold: - mouse.release(button="right") - keyboard.send(Config().char["stand_still"], do_press=False) - + while timeout is None or (time.time() - start) < timeout: + template_match = template_finder.search(template_type, grab(), threshold=threshold) + if template_match.valid: + Logger.debug(f"Select {template_match.name} ({template_match.score*100:.1f}% confidence)") + mouse.move(*template_match.center_monitor) + wait(0.2, 0.3) + self._click_left() + # check the successfunction for 2 sec, if not found, try again + check_success_start = time.time() + while time.time() - check_success_start < 2: + if success_func is None or success_func(): + return True + Logger.error(f"Wanted to select {template_type}, but could not find it") + return False - def pre_buff(self): - pass + """ + KILL ROUTINES + """ def kill_pindle(self) -> bool: raise ValueError("Pindle is not implemented!") diff --git a/src/char/necro.py b/src/char/necro.py index 02535d7fa..1492d32f1 100644 --- a/src/char/necro.py +++ b/src/char/necro.py @@ -7,7 +7,8 @@ from logger import Logger from screen import grab, convert_abs_to_monitor, convert_screen_to_abs from config import Config -from utils.misc import wait, rotate_vec, unit_vector +from utils.misc import wait +from char.tools import calculations import random from pather import Location, Pather import numpy as np @@ -150,7 +151,7 @@ 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): + def _revive(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -179,7 +180,7 @@ def _revive(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count 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): + def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -208,7 +209,7 @@ def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: int = 10, ca 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): + def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -240,8 +241,7 @@ def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_c def pre_buff(self): #only CTA if pre trav - if Config().char["cta_available"]: - self._pre_buff_cta() + self._pre_buff_cta() if self._shenk_dead==1: Logger.info("trav buff?") #self._heart_of_wolverine() @@ -284,7 +284,7 @@ def _bone_armor(self): - def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10): + def _left_attack(self, cast_pos_abs: tuple[float, float], spray: float = 10): keyboard.send(Config().char["stand_still"], do_release=False) if self._skill_hotkeys["skill_left"]: keyboard.send(self._skill_hotkeys["skill_left"]) @@ -299,7 +299,7 @@ def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10): keyboard.send(Config().char["stand_still"], do_press=False) - def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=6): + def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: float = 10, cast_count: int=6): keyboard.send(Config().char["stand_still"], do_release=False) if self._skill_hotkeys["skill_left"]: keyboard.send(self._skill_hotkeys["skill_left"]) @@ -326,7 +326,7 @@ def _amp_dmg(self, cast_pos_abs: tuple[float, float], spray: float = 10): 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): + def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -348,8 +348,8 @@ def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, mouse.press(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)) + angle = calculations.lerp(cast_start_angle,cast_end_angle,float(i)/cast_div) + target = calculations.unit_vector(calculations.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") @@ -425,12 +425,12 @@ def kill_pindle(self) -> bool: for _ in range(2): - corpse_pos = unit_vector(rotate_vec(cast_pos_abs, rot_deg)) * 200 + corpse_pos = calculations.unit_vector(calculations.rotate_vec(cast_pos_abs, rot_deg)) * 200 self._corpse_explosion(pc,40,cast_count=2) rot_deg-=7 rot_deg=0 for _ in range(2): - corpse_pos = unit_vector(rotate_vec(cast_pos_abs, rot_deg)) * 200 + corpse_pos = calculations.unit_vector(calculations.rotate_vec(cast_pos_abs, rot_deg)) * 200 self._corpse_explosion(pc,40,cast_count=2) rot_deg+=7 diff --git a/src/char/paladin/fohdin.py b/src/char/paladin/fohdin.py index b2fcc186e..0d39d59ce 100644 --- a/src/char/paladin/fohdin.py +++ b/src/char/paladin/fohdin.py @@ -1,18 +1,15 @@ -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 health_manager import set_panel_check_paused +from inventory.personal import inspect_items +from logger import Logger from pather import Location -from target_detect import get_visible_targets, TargetInfo, log_targets +from screen import convert_abs_to_monitor, convert_screen_to_abs, grab +from target_detect import get_visible_targets +from utils.misc import wait class FoHdin(Paladin): def __init__(self, *args, **kwargs): @@ -20,73 +17,101 @@ def __init__(self, *args, **kwargs): 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 + def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float, aura: str = "concentration"): 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) + self._cast_hammers(atk_len) + + def _cast_hammers( + self, + max_duration: float = 0, + aura: str = "concentration" + ): #for nihlathak + return self._cast_at_position(skill_name = "blessed_hammer", spray = 0, spread_deg=0, max_duration = max_duration, aura = aura) + + def _cast_foh( + self, + cast_pos_abs: tuple[float, float], + spray: float = 10, + spread_deg: float = 10, + min_duration: float = 0, + max_duration: float = 0, + teleport_frequency: float = 0, + use_target_detect: bool = False, + aura: str = "conviction", + ): + return self._cast_at_position(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, spread_deg = spread_deg, min_duration = min_duration, max_duration = max_duration, use_target_detect = use_target_detect, teleport_frequency = teleport_frequency, aura = aura) + + def _cast_holy_bolt( + self, + cast_pos_abs: tuple[float, float], + spray: float = 10, + spread_deg: float = 10, + max_duration: float = 0, + aura: str = "concentration", + ): + return self._cast_at_position(skill_name = "holy_bolt", cast_pos_abs = cast_pos_abs, spray = spray, spread_deg = spread_deg, max_duration = max_duration, aura = aura) def _generic_foh_attack_sequence( self, - default_target_abs: tuple[int, int] = (0, 0), + cast_pos_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 = "" + spray: float = 20, + spread_deg: float = 10, + aura: str = "conviction" ) -> 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 - - # 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 - else: - - # 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) + # custom FOH alternating with holy bolt routine + if Config().char["faster_cast_rate"] >= 75: + self._activate_conviction_aura() + self._stand_still(True) + start = time_of_last_tp = time.perf_counter() + # cast while time is less than max duration + while (elapsed_time := time.perf_counter() - start) < max_duration: + targets = get_visible_targets() + if targets: + pos_abs = targets[0].center_abs + pos_abs = self._randomize_position(pos_abs = pos_abs, spray = 5, spread_deg = 0) + # otherwise, use the given position with randomization parameters else: - self._cast_foh(cast_pos_abs, spray=spray, aura=foh_aura) - - target_check_count += 1 + pos_abs = self._randomize_position(cast_pos_abs, spray = spray, spread_deg = spread_deg) + pos_m = convert_abs_to_monitor(pos_abs) + mouse.move(*pos_m, delay_factor= [0.4, 0.6]) + # at frame 0, press key + # frame 1-6 startup of FOH + # frame 6 can cast HB + # frame 15 FOH able to be cast again + # frame 16 FOH startup + self._send_skill("foh", hold_time=3/25, cooldown=False) + time.sleep(2/25) #total of 5 frame startup + self._send_skill("holy_bolt", hold_time=8/25, cooldown=False) # now at frame 6 + time.sleep(2/25) # now at frame 15 + # if teleport frequency is set, teleport every teleport_frequency seconds + if (elapsed_time - time_of_last_tp) >= 3.5: + self._teleport_to_origin() + time_of_last_tp = elapsed_time + # if target detection is enabled and minimum time has elapsed and no targets remain, end casting + if (elapsed_time > min_duration) and not targets: + break + self._stand_still(False) + else: + self._cast_at_position(skill_name = "foh", cast_pos_abs = cast_pos_abs, spray = spray, spread_deg = spread_deg, min_duration = min_duration, max_duration = max_duration, teleport_frequency = 3.5, use_target_detect = True, aura = aura) 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) + def _cs_attack_sequence(self, min_duration: float, max_duration: float): + self._generic_foh_attack_sequence(cast_pos_abs=(0, 0), min_duration = min_duration, max_duration = max_duration, spread_deg=100) 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_trash_mobs_attack_sequence(self): + self._cs_attack_sequence(min_duration = Config().char["atk_len_cs_trashmobs"], max_duration = Config().char["atk_len_cs_trashmobs"]*3) 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: + wait(1) set_panel_check_paused(True) inspect_items(grab(), ignore_sell=True) set_panel_check_paused(False) @@ -98,33 +123,29 @@ def kill_pindle(self) -> bool: if (self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges) and self._use_safer_routines: # Slightly retreating, so the Merc gets charged - if not self._pather.traverse_nodes([102], self, timeout=1.0, do_pre_move=False, force_move=True,force_tp=False, use_tp_charge=False): + if not self._pather.traverse_nodes([102], self, timeout=1.0, force_move=True, force_tp=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=False, force_tp=True, use_tp_charge=True): + if not self._pather.traverse_nodes([103], self, timeout=1.0, force_tp=True): return False # Slightly retreating, so the Merc gets charged - if not self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False, force_move=True, force_tp=False, use_tp_charge=False): + if not self._pather.traverse_nodes([103], self, timeout=1.0, force_move=True, force_tp=False): return False else: - keyboard.send(self._skill_hotkeys["conviction"]) - wait(0.15) - self._pather.traverse_nodes([103], self, timeout=1.0, do_pre_move=False) + self._pather.traverse_nodes([103], self, timeout=1.0, active_skill="conviction") - 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) + cast_pos_abs = (pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9) + self._generic_foh_attack_sequence(cast_pos_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, spray = 20) if self.capabilities.can_teleport_natively: self._pather.traverse_nodes_fixed("pindle_end", self) else: - - keyboard.send(self._skill_hotkeys["redemption"]) - wait(0.15) - self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=False) + self._activate_redemption_aura() + self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0) # 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._generic_foh_attack_sequence(cast_pos_abs=cast_pos_abs, max_duration=atk_len_dur, spray=11) self._activate_cleanse_redemption() return True @@ -133,19 +154,17 @@ def kill_pindle(self) -> bool: def kill_council(self) -> bool: atk_len_dur = float(Config().char["atk_len_trav"]) - keyboard.send(self._skill_hotkeys["conviction"]) - wait(.15) # traverse to nodes and attack nodes = [225, 226, 300] for i, node in enumerate(nodes): - self._pather.traverse_nodes([node], self, timeout=2.2, do_pre_move = False, force_tp=(self.capabilities.can_teleport_natively or i > 0), use_tp_charge=(self.capabilities.can_teleport_natively or i > 0)) - 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) + self._pather.traverse_nodes([node], self, timeout=2.2, do_pre_move = False, force_tp=(i > 0)) + cast_pos_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(cast_pos_abs=cast_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, spray=80) # return to 226 and prepare for pickit - self._pather.traverse_nodes([226], self, timeout=2.2, do_pre_move = False, force_tp=True, use_tp_charge=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._pather.traverse_nodes([226], self, timeout=2.2, do_pre_move = False, force_tp=True) + cast_pos_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(cast_pos_abs=cast_pos_abs, max_duration=atk_len_dur*3, spray=80) self._activate_cleanse_redemption() @@ -154,17 +173,10 @@ def kill_council(self) -> bool: 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._generic_foh_attack_sequence(cast_pos_abs=eld_pos_abs, min_duration=atk_len_dur, max_duration=atk_len_dur*3, spray=70) 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._generic_foh_attack_sequence(cast_pos_abs=eld_pos_abs, max_duration=atk_len_dur, spray=70) self._activate_cleanse_redemption() return True @@ -174,16 +186,14 @@ def kill_shenk(self): atk_len_dur = float(Config().char["atk_len_shenk"]) # traverse to shenk - keyboard.send(self._skill_hotkeys["conviction"]) - wait(0.15) - self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, force_tp=True, active_skill="conviction") wait(0.05, 0.1) # bypass mob detect first - self._cast_foh((0, 0), spray=11, min_duration = 2, aura = "conviction") + self._cast_foh((0, 0), spray=11, min_duration = 2, aura = "conviction", use_target_detect=False) # 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._generic_foh_attack_sequence(min_duration=atk_len_dur - diff, max_duration=atk_len_dur*3 - diff, spray=10) self._activate_cleanse_redemption() return True @@ -192,23 +202,23 @@ def kill_shenk(self): 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._pather.traverse_nodes(end_nodes, self, timeout=0.8) + if self._get_hotkey("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._generic_foh_attack_sequence(min_duration=atk_len_dur/2, max_duration=atk_len_dur, spray=70, aura="redemption") + self._generic_foh_attack_sequence(min_duration=atk_len_dur/2, max_duration=atk_len_dur, spray=70, aura="redemption") + self._generic_foh_attack_sequence(max_duration=atk_len_dur*2, 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._generic_foh_attack_sequence(min_duration=atk_len_dur, max_duration=atk_len_dur*2, spray=80) self._activate_cleanse_redemption() return True @@ -756,8 +766,43 @@ def kill_diablo(self) -> bool: 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._generic_foh_attack_sequence(cast_pos_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 + return True + +if __name__ == "__main__": + import os + from config import Config + from char.paladin import FoHdin + from pather import Pather + from item.pickit import PickIt + import keyboard + from logger import Logger + from screen import start_detecting_window, stop_detecting_window + + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1)) + start_detecting_window() + print("Move to d2r window and press f11") + print("Press F9 to test attack sequence") + keyboard.wait("f11") + + pather = Pather() + pickit = PickIt() + char = FoHdin(Config().fohdin, pather, pickit) + + char.discover_capabilities() + + def routine(): + char._key_press(char._get_hotkey("foh"), hold_time=4/25) + time.sleep(1/25) #total of 5 frame startup + char._key_press(char._get_hotkey("holy_bolt"), hold_time=(1/25)) + char._key_press(char._get_hotkey("foh"), hold_time=(3)) + # for _ in range(0, 10): + # char._key_press(char._get_hotkey("foh"), hold_time=(0.1)) + # time.sleep(0.3) + + keyboard.add_hotkey('f9', lambda: routine()) + while True: + wait(0.01) diff --git a/src/char/paladin/hammerdin.py b/src/char/paladin/hammerdin.py index 8fa88ed1b..9819350f2 100644 --- a/src/char/paladin/hammerdin.py +++ b/src/char/paladin/hammerdin.py @@ -1,17 +1,12 @@ import keyboard -import random -import time -from char import CharacterCapabilities from char.paladin import Paladin from config import Config from logger import Logger -from pather import Location from pather import Pather from pather import Pather, Location from screen import convert_abs_to_monitor, convert_screen_to_abs, grab from target_detect import get_visible_targets -from ui import skills from utils.custom_mouse import mouse from utils.misc import wait @@ -22,41 +17,8 @@ def __init__(self, *args, **kwargs): #hammerdin needs to be closer to shenk to reach it with hammers self._pather.offset_node(149, (70, 10)) - def _cast_hammers(self, time_in_s: float, aura: str = "concentration"): - if aura in self._skill_hotkeys and self._skill_hotkeys[aura]: - keyboard.send(self._skill_hotkeys[aura]) - wait(0.05, 0.1) - keyboard.send(Config().char["stand_still"], do_release=False) - wait(0.05, 0.1) - if self._skill_hotkeys["blessed_hammer"]: - keyboard.send(self._skill_hotkeys["blessed_hammer"]) - wait(0.05, 0.1) - start = time.time() - while (time.time() - start) < time_in_s: - wait(0.06, 0.08) - mouse.press(button="left") - wait(0.1, 0.2) - mouse.release(button="left") - wait(0.01, 0.05) - keyboard.send(Config().char["stand_still"], do_press=False) - - 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 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 _cast_hammers(self, duration: float = 0, aura: str = "concentration"): #for nihlathak + return self._cast_at_position(skill_name = "blessed_hammer", spray = 0, duration = duration, aura = aura) def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float): pos_m = convert_abs_to_monitor(abs_move) @@ -67,15 +29,13 @@ def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float): def kill_pindle(self) -> bool: wait(0.1, 0.15) if self.capabilities.can_teleport_with_charges: - if not self._pather.traverse_nodes([104], self, timeout=1.0, force_tp=True, use_tp_charge=True): + if not self._pather.traverse_nodes([104], self, timeout=1.0, force_tp=True): return False elif self.capabilities.can_teleport_natively: if not self._pather.traverse_nodes_fixed("pindle_end", self): return False else: - keyboard.send(self._skill_hotkeys["concentration"]) - wait(0.15) - self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, timeout=1.0, active_skill="concentration") self._cast_hammers(Config().char["atk_len_pindle"]) wait(0.1, 0.15) self._cast_hammers(1.6, "redemption") @@ -86,10 +46,7 @@ def kill_eldritch(self) -> bool: # Custom eld position for teleport that brings us closer to eld self._pather.traverse_nodes_fixed([(675, 30)], self) else: - keyboard.send(self._skill_hotkeys["concentration"]) - wait(0.15) - # Traverse without pre_move, because we don't want to activate vigor when walking! - self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes((Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END), self, timeout=1.0, force_tp=True, active_skill="concentration") wait(0.05, 0.1) self._cast_hammers(Config().char["atk_len_eldritch"]) wait(0.1, 0.15) @@ -97,9 +54,7 @@ def kill_eldritch(self) -> bool: return True def kill_shenk(self): - keyboard.send(self._skill_hotkeys["concentration"]) - wait(0.15) - self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0, force_tp=True, active_skill="concentration") wait(0.05, 0.1) self._cast_hammers(Config().char["atk_len_shenk"]) wait(0.1, 0.15) @@ -107,18 +62,16 @@ def kill_shenk(self): return True def kill_council(self) -> bool: - keyboard.send(self._skill_hotkeys["concentration"]) - wait(.15) - # 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) + self._pather.traverse_nodes([228, 229], self, timeout=2.5, force_tp=True, active_skill="concentration") + self._cast_hammers(atk_len) # Move a bit back and another round self._move_and_attack((40, 20), atk_len) # Here we have two different attack sequences depending if tele is available or not - if self.capabilities.can_teleport_natively or self.capabilities.can_teleport_with_charges: + if self.can_teleport(): # Back to center stairs and more hammers - self._pather.traverse_nodes([226], self, timeout=2.2, do_pre_move=False, force_tp=True, use_tp_charge=True) + self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True) self._cast_hammers(atk_len) # move a bit to the top self._move_and_attack((65, -30), atk_len) @@ -131,7 +84,7 @@ def kill_council(self) -> bool: def kill_nihlathak(self, end_nodes: list[int]) -> bool: # Move close to nihlathak - self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False) + self._pather.traverse_nodes(end_nodes, self, timeout=0.8) # move mouse to center, otherwise hammers sometimes dont fly, not sure why pos_m = convert_abs_to_monitor((0, 0)) mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7]) diff --git a/src/char/paladin/paladin.py b/src/char/paladin/paladin.py index 283cc9baa..6c0604edd 100644 --- a/src/char/paladin/paladin.py +++ b/src/char/paladin/paladin.py @@ -1,101 +1,39 @@ -import keyboard -from ui import skills -import time -import random -from utils.custom_mouse import mouse -from char import IChar, CharacterCapabilities +from char import IChar +from item.pickit import PickIt #for Diablo 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") + # Logger.info("Setting up Paladin") super().__init__(skill_hotkeys) self._pather = pather self._pickit = pickit #for Diablo self._picked_up_items = False #for Diablo + self.default_move_skill = "vigor" 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 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") + self._pre_buff_cta() + self._cast_holy_shield() - # ensure character stands still - keyboard.send(Config().char["stand_still"], do_release=False) + def _activate_concentration_aura(self, delay=None) -> bool: + return self._activate_aura("concentration", delay=delay) - # set left hand skill - self._select_skill(skill_name, mouse_click_type = "left") - wait(0.04) + def _activate_redemption_aura(self, delay = None) -> bool: + return self._activate_aura("redemption", delay=delay) - # 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) + def _activate_cleansing_aura(self, delay = None) -> bool: + return self._activate_aura("cleansing", delay=delay) - # release stand still key - keyboard.send(Config().char["stand_still"], do_press=False) + def _activate_conviction_aura(self, delay = None) -> bool: + return self._activate_aura("conviction", delay=delay) - def _activate_redemption_aura(self, delay = [0.6, 0.8]): - self._select_skill("redemption", delay=delay) + def _activate_vigor_aura(self, delay = None) -> bool: + return self._activate_aura("vigor", delay=delay) - def _activate_cleanse_aura(self, delay = [0.3, 0.4]): - self._select_skill("cleansing", delay=delay) + def _cast_holy_shield(self) -> bool: + return self._cast_simple(skill_name="holy_shield") - def _activate_cleanse_redemption(self): - self._activate_cleanse_aura() - self._activate_redemption_aura() \ No newline at end of file + def _activate_cleanse_redemption(self) -> bool: + self._activate_cleansing_aura([0.3, 0.5]) + return self._activate_redemption_aura([0.3, 0.5]) diff --git a/src/char/poison_necro.py b/src/char/poison_necro.py index 72ae64cac..e90f8f5c5 100644 --- a/src/char/poison_necro.py +++ b/src/char/poison_necro.py @@ -6,11 +6,11 @@ from logger import Logger from screen import grab from config import Config -from utils.misc import wait, rotate_vec, unit_vector +from utils.misc import wait +from char.tools import calculations import random from pather import Location, Pather import screen as screen -import numpy as np import time import os from ui_manager import get_closest_non_hud_pixel @@ -158,7 +158,7 @@ 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): + def _revive(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -187,7 +187,7 @@ def _revive(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count 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): + def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -216,7 +216,7 @@ def _raise_skeleton(self, cast_pos_abs: tuple[float, float], spray: int = 10, ca 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): + def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -248,8 +248,7 @@ def _raise_mage(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_c def pre_buff(self): #only CTA if pre trav - if Config().char["cta_available"]: - self._pre_buff_cta() + self._pre_buff_cta() if self._shenk_dead==1: Logger.info("trav buff?") #self._heart_of_wolverine() @@ -292,7 +291,7 @@ def _bone_armor(self): - def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10): + def _left_attack(self, cast_pos_abs: tuple[float, float], spray: float = 10): keyboard.send(Config().char["stand_still"], do_release=False) if self._skill_hotkeys["skill_left"]: keyboard.send(self._skill_hotkeys["skill_left"]) @@ -307,7 +306,7 @@ def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10): keyboard.send(Config().char["stand_still"], do_press=False) - def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: int = 10, cast_count: int=6): + def _left_attack_single(self, cast_pos_abs: tuple[float, float], spray: float = 10, cast_count: int=6): keyboard.send(Config().char["stand_still"], do_release=False) if self._skill_hotkeys["skill_left"]: keyboard.send(self._skill_hotkeys["skill_left"]) @@ -346,7 +345,7 @@ def _lower_res(self, cast_pos_abs: tuple[float, float], spray: float = 10): 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): + def _corpse_explosion(self, cast_pos_abs: tuple[float, float], spray: float = 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): @@ -369,8 +368,8 @@ def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, mouse.press(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)) + angle = calculations.lerp(cast_start_angle,cast_end_angle,float(i)/cast_div) + target = calculations.unit_vector(calculations.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") @@ -385,14 +384,14 @@ def _cast_circle(self, cast_dir: tuple[float,float],cast_start_angle: float=0.0, def kill_pindle(self) -> bool: pos_m = screen.convert_abs_to_monitor((0, 30)) - self.walk(pos_m, force_move=True) + self._walk_to_position(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._walk_to_position(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) return True @@ -434,7 +433,7 @@ def kill_eldritch(self) -> bool: 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._walk_to_position(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)) @@ -458,9 +457,9 @@ 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) + self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True) pos_m = screen.convert_abs_to_monitor((50, 0)) - self.walk(pos_m, force_move=True) + self._walk_to_position(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) @@ -469,7 +468,7 @@ def kill_council(self) -> bool: self.pre_move() self.move(pos_m, force_move=True) pos_m = screen.convert_abs_to_monitor((30, -50)) - self.walk(pos_m, force_move=True) + self._walk_to_position(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) @@ -479,9 +478,9 @@ def kill_council(self) -> bool: 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) + self._pather.traverse_nodes([226], self, timeout=2.5, force_tp=True) pos_m = screen.convert_abs_to_monitor((0, 30)) - self.walk(pos_m, force_move=True) + self._walk_to_position(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) @@ -497,9 +496,9 @@ def kill_council(self) -> bool: 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) + self._pather.traverse_nodes([229], self, timeout=2.5, force_tp=True) pos_m = screen.convert_abs_to_monitor((20, -50)) - self.walk(pos_m, force_move=True) + self._walk_to_position(pos_m, force_move=True) self.poison_nova(2.0) pos_m = screen.convert_abs_to_monitor((50, 0)) self.pre_move() @@ -514,9 +513,9 @@ def kill_council(self) -> bool: def kill_nihlathak(self, end_nodes: list[int]) -> bool: # Move close to nihlathak - self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=True) + self._pather.traverse_nodes(end_nodes, self, timeout=0.8) pos_m = screen.convert_abs_to_monitor((20, 20)) - self.walk(pos_m, force_move=True) + self._walk_to_position(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)) @@ -530,7 +529,7 @@ def kill_nihlathak(self, end_nodes: list[int]) -> bool: def kill_summoner(self) -> bool: # Attack pos_m = screen.convert_abs_to_monitor((0, 30)) - self.walk(pos_m, force_move=True) + self._walk_to_position(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) diff --git a/src/char/sorceress/blizz_sorc.py b/src/char/sorceress/blizz_sorc.py index 39cc7bd5d..de0f25af5 100644 --- a/src/char/sorceress/blizz_sorc.py +++ b/src/char/sorceress/blizz_sorc.py @@ -2,10 +2,11 @@ from char.sorceress import Sorceress from utils.custom_mouse import mouse from logger import Logger -from utils.misc import wait, rotate_vec, unit_vector +from utils.misc import wait import random from pather import Location import numpy as np +from char.tools import calculations from screen import convert_abs_to_monitor, grab, convert_screen_to_abs from config import Config import template_finder diff --git a/src/char/sorceress/hydra_sorc.py b/src/char/sorceress/hydra_sorc.py index dd555ea17..921f18298 100644 --- a/src/char/sorceress/hydra_sorc.py +++ b/src/char/sorceress/hydra_sorc.py @@ -13,7 +13,7 @@ class HydraSorc(Sorceress): def __init__(self, *args, **kwargs): Logger.info("Setting up HydraSorc Sorc") super().__init__(*args, **kwargs) - self._hydra_time = None + self._hydra_time = None def _alt_attack(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.16, 0.23), spray: float = 10): keyboard.send(Config().char["stand_still"], do_release=False) @@ -34,14 +34,14 @@ def _hydra(self, cast_pos_abs: tuple[float, float], spray: float = 10): if not self._skill_hotkeys["hydra"]: raise ValueError("You did not set a hotkey for hydra!") keyboard.send(self._skill_hotkeys["hydra"]) - self._hydra_time = time.time() + self._hydra_time = time.time() 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(2,3) - mouse.release(button="right") + mouse.release(button="right") def kill_pindle(self) -> bool: pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0]) diff --git a/src/char/sorceress/light_sorc.py b/src/char/sorceress/light_sorc.py index 10abcf1ae..530fb0e8a 100644 --- a/src/char/sorceress/light_sorc.py +++ b/src/char/sorceress/light_sorc.py @@ -2,7 +2,8 @@ from char.sorceress import Sorceress from utils.custom_mouse import mouse from logger import Logger -from utils.misc import wait, rotate_vec, unit_vector +from utils.misc import wait +from char.tools import calculations import random from pather import Location import numpy as np @@ -15,52 +16,26 @@ def __init__(self, *args, **kwargs): Logger.info("Setting up Light Sorc") super().__init__(*args, **kwargs) - def _chain_lightning(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.2, 0.3), spray: int = 10): - keyboard.send(Config().char["stand_still"], do_release=False) - if self._skill_hotkeys["chain_lightning"]: - keyboard.send(self._skill_hotkeys["chain_lightning"]) - for _ in range(4): - x = cast_pos_abs[0] + (random.random() * 2 * spray - spray) - y = cast_pos_abs[1] + (random.random() * 2 * spray - spray) - pos_m = convert_abs_to_monitor((x, y)) - mouse.move(*pos_m, delay_factor=[0.3, 0.6]) - mouse.press(button="left") - wait(delay[0], delay[1]) - mouse.release(button="left") - keyboard.send(Config().char["stand_still"], do_press=False) - - def _lightning(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.2, 0.3), spray: float = 10): - if not self._skill_hotkeys["lightning"]: - raise ValueError("You did not set lightning hotkey!") - keyboard.send(self._skill_hotkeys["lightning"]) - 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") - - def _frozen_orb(self, cast_pos_abs: tuple[float, float], delay: tuple[float, float] = (0.2, 0.3), spray: float = 10): - if self._skill_hotkeys["frozen_orb"]: - keyboard.send(self._skill_hotkeys["frozen_orb"]) - 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) - mouse.press(button="right") - wait(delay[0], delay[1]) - mouse.release(button="right") + def _cast_chain_lightning(self, cast_pos_abs: tuple[float, float], spray: float = 20, duration: float = 0) -> bool: + return self._cast_at_position(skill_name="chain_lightning", spray = spray, cast_pos_abs = cast_pos_abs, duration = duration) + + def _cast_lightning(self, cast_pos_abs: tuple[float, float], spray: float = 20, duration: float = 0) -> bool: + return self._cast_at_position(skill_name="lightning", spray = spray, cast_pos_abs = cast_pos_abs, duration = duration) + + def _cast_frozen_orb(self, cast_pos_abs: tuple[float, float], spray: float = 10, duration: float = 0) -> bool: + return self._cast_at_position("frozen_orb", cast_pos_abs, spray = spray, duration = duration) + + def _generic_light_sorc_attack_sequence(self, cast_pos_abs: tuple[float, float], chain_spray: float = 20, duration: float = 0): + self._cast_lightning(cast_pos_abs, spray=5) + wait(self._cast_duration, self._cast_duration + 0.2) + self._cast_chain_lightning(cast_pos_abs, spray=chain_spray, duration=duration) + wait(self._cast_duration, self._cast_duration + 0.2) def kill_pindle(self) -> bool: pindle_pos_abs = convert_screen_to_abs(Config().path["pindle_end"][0]) cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9] - self._lightning(cast_pos_abs, spray=11) - for _ in range(int(Config().char["atk_len_pindle"])): - self._chain_lightning(cast_pos_abs, spray=11) - wait(self._cast_duration, self._cast_duration + 0.2) + atk_len_dur = Config().char["atk_len_pindle"] + self._generic_light_sorc_attack_sequence(cast_pos_abs, chain_spray=11, duration=atk_len_dur) # Move to items self._pather.traverse_nodes_fixed("pindle_end", self) return True @@ -68,14 +43,12 @@ def kill_pindle(self) -> bool: def kill_eldritch(self) -> bool: eld_pos_abs = convert_screen_to_abs(Config().path["eldritch_end"][0]) cast_pos_abs = [eld_pos_abs[0] * 0.9, eld_pos_abs[1] * 0.9] - self._lightning(cast_pos_abs, spray=50) - for _ in range(int(Config().char["atk_len_eldritch"])): - self._chain_lightning(cast_pos_abs, spray=90) + atk_len_dur = Config().char["atk_len_eldritch"] + self._generic_light_sorc_attack_sequence(cast_pos_abs, chain_spray=90, duration=atk_len_dur) # Move to items - wait(self._cast_duration, self._cast_duration + 0.2) pos_m = convert_abs_to_monitor((70, -200)) self.pre_move() - self.move(pos_m, force_move=True) + self.move(pos_m, force_move=True) self._pather.traverse_nodes(Location.A5_ELDRITCH_SAFE_DIST, Location.A5_ELDRITCH_END) return True @@ -84,25 +57,20 @@ def kill_shenk(self) -> bool: if shenk_pos_abs is None: shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0]) cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9] - self._lightning(cast_pos_abs, spray=60) - for _ in range(int(Config().char["atk_len_shenk"] * 0.5)): - self._chain_lightning(cast_pos_abs, spray=90) + atk_len_dur = Config().char["atk_len_shenk"] + self._generic_light_sorc_attack_sequence(cast_pos_abs, chain_spray=90, duration=atk_len_dur/2) pos_m = convert_abs_to_monitor((150, 50)) self.pre_move() self.move(pos_m, force_move=True) shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0]) cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9] - self._lightning(cast_pos_abs, spray=60) - for _ in range(int(Config().char["atk_len_shenk"] * 0.5)): - self._chain_lightning(cast_pos_abs, spray=90) + self._generic_light_sorc_attack_sequence(cast_pos_abs, chain_spray=90, duration=atk_len_dur/2) pos_m = convert_abs_to_monitor((150, 50)) self.pre_move() self.move(pos_m, force_move=True) shenk_pos_abs = convert_screen_to_abs(Config().path["shenk_end"][0]) cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9] - self._lightning(cast_pos_abs, spray=60) - for _ in range(int(Config().char["atk_len_shenk"])): - self._chain_lightning(cast_pos_abs, spray=90) + self._generic_light_sorc_attack_sequence(cast_pos_abs, chain_spray=90, duration=atk_len_dur) self.pre_move() self.move(pos_m, force_move=True) # Move to items @@ -117,9 +85,9 @@ def kill_council(self) -> bool: self._pather.traverse_nodes([300], self, timeout=1.0, force_tp=True) self._pather.offset_node(300, (-80, 110)) wait(0.5) - self._frozen_orb((-150, -10), spray=10) - self._lightning((-150, 0), spray=10) - self._chain_lightning((-150, 15), spray=10) + self._cast_frozen_orb((-150, -10), spray=10) + self._cast_lightning((-150, 0), spray=10) + self._cast_chain_lightning((-150, 15), spray=10) wait(0.5) pos_m = convert_abs_to_monitor((-50, 200)) self.pre_move() @@ -133,9 +101,9 @@ def kill_council(self) -> bool: self._pather.traverse_nodes([226], self, timeout=1.0, force_tp=True) self._pather.offset_node(226, (80, -60)) wait(0.5) - self._frozen_orb((-150, -130), spray=10) - self._chain_lightning((200, -185), spray=20) - self._chain_lightning((-170, -150), spray=20) + self._cast_frozen_orb((-150, -130), spray=10) + self._cast_chain_lightning((200, -185), spray=20) + self._cast_chain_lightning((-170, -150), spray=20) wait(0.5) self._pather.traverse_nodes_fixed([(1110, 15)], self) self._pather.traverse_nodes([300], self, timeout=1.0, force_tp=True) @@ -143,10 +111,10 @@ def kill_council(self) -> bool: self.pre_move() self.move(pos_m, force_move=True) wait(0.5) - self._frozen_orb((-170, -100), spray=40) - self._chain_lightning((-300, -100), spray=10) - self._chain_lightning((-300, -90), spray=10) - self._lightning((-300, -110), spray=10) + self._cast_frozen_orb((-170, -100), spray=40) + self._cast_chain_lightning((-300, -100), spray=10) + self._cast_chain_lightning((-300, -90), spray=10) + self._cast_lightning((-300, -110), spray=10) wait(0.5) # Move back outside and attack pos_m = convert_abs_to_monitor((-430, 230)) @@ -156,10 +124,10 @@ def kill_council(self) -> bool: self._pather.traverse_nodes([304], self, timeout=1.0, force_tp=True) self._pather.offset_node(304, (0, 80)) wait(0.5) - self._frozen_orb((175, -170), spray=40) - self._chain_lightning((-170, -150), spray=20) - self._chain_lightning((300, -200), spray=20) - self._chain_lightning((-170, -150), spray=20) + self._cast_frozen_orb((175, -170), spray=40) + self._cast_chain_lightning((-170, -150), spray=20) + self._cast_chain_lightning((300, -200), spray=20) + self._cast_chain_lightning((-170, -150), spray=20) wait(0.5) # Move back inside and attack pos_m = convert_abs_to_monitor((350, -350)) @@ -170,9 +138,9 @@ def kill_council(self) -> bool: self.move(pos_m, force_move=True) wait(0.5) # Attack sequence center - self._frozen_orb((0, 20), spray=40) - self._lightning((-50, 50), spray=30) - self._lightning((50, 50), spray=30) + self._cast_frozen_orb((0, 20), spray=40) + self._cast_lightning((-50, 50), spray=30) + self._cast_lightning((50, 50), spray=30) wait(0.5) # Move inside pos_m = convert_abs_to_monitor((40, -30)) @@ -180,9 +148,9 @@ def kill_council(self) -> bool: self.move(pos_m, force_move=True) # Attack sequence to center wait(0.5) - self._chain_lightning((-150, 100), spray=20) - self._chain_lightning((150, 200), spray=40) - self._chain_lightning((-150, 0), spray=20) + self._cast_chain_lightning((-150, 100), spray=20) + self._cast_chain_lightning((150, 200), spray=40) + self._cast_chain_lightning((-150, 0), spray=20) wait(0.5) pos_m = convert_abs_to_monitor((-200, 240)) self.pre_move() @@ -208,17 +176,17 @@ def kill_nihlathak(self, end_nodes: list[int]) -> bool: if nihlathak_pos_abs is not None: cast_pos_abs = np.array([nihlathak_pos_abs[0] * 0.9, nihlathak_pos_abs[1] * 0.9]) - self._lightning(cast_pos_abs, spray=60) - self._chain_lightning(cast_pos_abs, delay, 90) + self._cast_lightning(cast_pos_abs, spray=60) + self._cast_chain_lightning(cast_pos_abs, delay, 90) # Do some tele "dancing" after each sequence if i < atk_len - 1: rot_deg = random.randint(-10, 10) if i % 2 == 0 else random.randint(170, 190) - tele_pos_abs = unit_vector(rotate_vec(cast_pos_abs, rot_deg)) * 100 + tele_pos_abs = calculations.unit_vector(calculations.rotate_vec(cast_pos_abs, rot_deg)) * 100 pos_m = convert_abs_to_monitor(tele_pos_abs) self.pre_move() self.move(pos_m) else: - self._lightning(cast_pos_abs, spray=60) + self._cast_lightning(cast_pos_abs, spray=60) else: Logger.warning(f"Casting static as the last position isn't known. Skipping attack sequence") self._cast_static(duration=2) @@ -231,12 +199,10 @@ def kill_nihlathak(self, end_nodes: list[int]) -> bool: def kill_summoner(self) -> bool: # Attack cast_pos_abs = np.array([0, 0]) - pos_m = convert_abs_to_monitor((-20, 20)) - mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7]) - for _ in range(int(Config().char["atk_len_arc"])): - self._lightning(cast_pos_abs, spray=11) - self._chain_lightning(cast_pos_abs, spray=11) - wait(self._cast_duration, self._cast_duration + 0.2) + atk_len_dur = Config().char["atk_len_arc"] + # pos_m = convert_abs_to_monitor((-20, 20)) + # mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7]) + self._generic_light_sorc_attack_sequence(cast_pos_abs, chain_spray=11, duration=atk_len_dur) return True if __name__ == "__main__": diff --git a/src/char/sorceress/nova_sorc.py b/src/char/sorceress/nova_sorc.py index 88a06749a..fe72adb37 100644 --- a/src/char/sorceress/nova_sorc.py +++ b/src/char/sorceress/nova_sorc.py @@ -17,40 +17,31 @@ def __init__(self, *args, **kwargs): # we want to change positions a bit of end points self._pather.offset_node(149, (70, 10)) - def _nova(self, time_in_s: float): - if not self._skill_hotkeys["nova"]: - raise ValueError("You did not set nova hotkey!") - keyboard.send(self._skill_hotkeys["nova"]) - wait(0.05, 0.1) - start = time.time() - while (time.time() - start) < time_in_s: - wait(0.03, 0.04) - mouse.press(button="right") - wait(0.12, 0.2) - mouse.release(button="right") + def _cast_nova(self, min_duration: float = 0) -> bool: + return self._cast_simple(skill_name = "nova", mouse_click_type = "right", duration = min_duration) def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float): pos_m = convert_abs_to_monitor(abs_move) self.pre_move() self.move(pos_m, force_move=True) - self._nova(atk_len) + self._cast_nova(atk_len) def kill_pindle(self) -> bool: self._pather.traverse_nodes_fixed("pindle_end", self) self._cast_static(0.6) - self._nova(Config().char["atk_len_pindle"]) + self._cast_nova(Config().char["atk_len_pindle"]) return True def kill_eldritch(self) -> bool: self._pather.traverse_nodes_fixed([(675, 30)], self) self._cast_static(0.6) - self._nova(Config().char["atk_len_eldritch"]) + self._cast_nova(Config().char["atk_len_eldritch"]) return True def kill_shenk(self) -> bool: self._pather.traverse_nodes((Location.A5_SHENK_SAFE_DIST, Location.A5_SHENK_END), self, timeout=1.0) self._cast_static(0.6) - self._nova(Config().char["atk_len_shenk"]) + self._cast_nova(Config().char["atk_len_shenk"]) return True def kill_council(self) -> bool: @@ -62,13 +53,13 @@ def kill_council(self) -> bool: def clear_inside(): self._pather.traverse_nodes_fixed([(1110, 120)], self) self._pather.traverse_nodes([229], self, timeout=0.8, force_tp=True) - self._nova(atk_len) + self._cast_nova(atk_len) self._move_and_attack((-40, -20), atk_len) self._move_and_attack((40, 20), atk_len) self._move_and_attack((40, 20), atk_len) def clear_outside(): self._pather.traverse_nodes([226], self, timeout=0.8, force_tp=True) - self._nova(atk_len) + self._cast_nova(atk_len) self._move_and_attack((45, -20), atk_len) self._move_and_attack((-45, 20), atk_len) clear_inside() @@ -82,12 +73,12 @@ def clear_outside(): def kill_nihlathak(self, end_nodes: list[int]) -> bool: atk_len = Config().char["atk_len_nihlathak"] * 0.3 # Move close to nihlathak - self._pather.traverse_nodes(end_nodes, self, timeout=0.8, do_pre_move=False) + self._pather.traverse_nodes(end_nodes, self, timeout=0.8) # move mouse to center pos_m = convert_abs_to_monitor((0, 0)) mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7]) self._cast_static(0.6) - self._nova(atk_len) + self._cast_nova(atk_len) self._move_and_attack((50, 25), atk_len) self._move_and_attack((-70, -35), atk_len) return True @@ -97,11 +88,11 @@ def kill_summoner(self) -> bool: pos_m = convert_abs_to_monitor((0, 20)) mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7]) # Attack - self._nova(Config().char["atk_len_arc"]) + self._cast_nova(Config().char["atk_len_arc"]) # Move a bit back and another round self._move_and_attack((0, 80), Config().char["atk_len_arc"] * 0.5) wait(0.1, 0.15) - self._nova(Config().char["atk_len_arc"] * 0.5) + self._cast_nova(Config().char["atk_len_arc"] * 0.5) return True diff --git a/src/char/sorceress/sorceress.py b/src/char/sorceress/sorceress.py index 0737d7fe6..8a60f636f 100644 --- a/src/char/sorceress/sorceress.py +++ b/src/char/sorceress/sorceress.py @@ -10,6 +10,7 @@ from pather import Pather from config import Config from ui_manager import ScreenObjects, is_visible +from screen import convert_screen_to_abs class Sorceress(IChar): def __init__(self, skill_hotkeys: dict, pather: Pather): @@ -17,12 +18,8 @@ def __init__(self, skill_hotkeys: dict, pather: Pather): self._pather = pather def pick_up_item(self, pos: tuple[float, float], item_name: str = None, prev_cast_start: float = 0): - if self._skill_hotkeys["telekinesis"] and any(x in item_name for x in ['potion', 'misc_gold', 'tp_scroll']): - keyboard.send(self._skill_hotkeys["telekinesis"]) - wait(0.1, 0.2) - mouse.move(pos[0], pos[1]) - wait(0.1, 0.2) - mouse.click(button="right") + if any(x in item_name for x in ['potion', 'misc_gold', 'tp_scroll']) and self._set_active_skill(mouse_click_type="right", skill="telekinesis"): + self._cast_telekinesis(*pos) # need about 0.4s delay before next capture for the item not to persist on screen cast_start = time.time() interval = (cast_start - prev_cast_start) @@ -42,7 +39,7 @@ def select_by_template( telekinesis: bool = False ) -> bool: # In case telekinesis is False or hotkey is not set, just call the base implementation - if not self._skill_hotkeys["telekinesis"] or not telekinesis: + if not (telekinesis and self._get_hotkey("telekinesis")): return super().select_by_template(template_type, success_func, timeout, threshold) if type(template_type) == list and "A5_STASH" in template_type: # sometimes waypoint is opened and stash not found because of that, check for that @@ -52,11 +49,8 @@ def select_by_template( while timeout is None or (time.time() - start) < timeout: template_match = template_finder.search(template_type, grab(), threshold=threshold) if template_match.valid: - keyboard.send(self._skill_hotkeys["telekinesis"]) - wait(0.1, 0.2) - mouse.move(*template_match.center_monitor) - wait(0.2, 0.3) - mouse.click(button="right") + pos_abs = convert_screen_to_abs(template_match.center) + self._cast_telekinesis(*pos_abs) # check the successfunction for 2 sec, if not found, try again check_success_start = time.time() while time.time() - check_success_start < 2: @@ -66,29 +60,26 @@ def select_by_template( return super().select_by_template(template_type, success_func, timeout, threshold) def pre_buff(self): - if Config().char["cta_available"]: - self._pre_buff_cta() - if self._skill_hotkeys["energy_shield"]: - keyboard.send(self._skill_hotkeys["energy_shield"]) - wait(0.1, 0.13) - mouse.click(button="right") - wait(self._cast_duration) - if self._skill_hotkeys["thunder_storm"]: - keyboard.send(self._skill_hotkeys["thunder_storm"]) - wait(0.1, 0.13) - mouse.click(button="right") - wait(self._cast_duration) - if self._skill_hotkeys["frozen_armor"]: - keyboard.send(self._skill_hotkeys["frozen_armor"]) - wait(0.1, 0.13) - mouse.click(button="right") - wait(self._cast_duration) + if self._pre_buff_cta(): + wait(self._cast_duration + 0.1) + if self._cast_energy_shield(): + wait(self._cast_duration + 0.1) + if self._cast_thunder_storm(): + wait(self._cast_duration + 0.1) + if self._cast_frozen_armor(): + wait(self._cast_duration + 0.1) - def _cast_static(self, duration: float = 1.4): - if self._skill_hotkeys["static_field"]: - keyboard.send(self._skill_hotkeys["static_field"]) - wait(0.1, 0.13) - start = time.time() - while time.time() - start < duration: - mouse.click(button="right") - wait(self._cast_duration) + def _cast_static(self, duration: float = 1.4) -> bool: + return self._cast_simple(skill_name="static_field", mouse_click_type = "right", duration=duration) + + def _cast_telekinesis(self, cast_pos_abs: tuple[float, float]) -> bool: + return self._cast_at_position(skill_name="telekinesis", cast_pos_abs = cast_pos_abs, spray = 0, mouse_click_type = "right") + + def _cast_thunder_storm(self) -> bool: + return self._cast_simple(skill_name="thunder_storm", mouse_click_type="right") + + def _cast_energy_shield(self) -> bool: + return self._cast_simple(skill_name="energy_shield", mouse_click_type="right") + + def _cast_frozen_armor(self) -> bool: + return self._cast_simple(skill_name="frozen_armor", mouse_click_type="right") \ No newline at end of file diff --git a/src/char/tools/__init__.py b/src/char/tools/__init__.py new file mode 100644 index 000000000..768facb81 --- /dev/null +++ b/src/char/tools/__init__.py @@ -0,0 +1 @@ +from .capabilities import CharacterCapabilities \ No newline at end of file diff --git a/src/char/tools/calculations.py b/src/char/tools/calculations.py new file mode 100644 index 000000000..32e47c078 --- /dev/null +++ b/src/char/tools/calculations.py @@ -0,0 +1,45 @@ +import random +import numpy as np +from math import cos, sin, dist, pi, radians, degrees + +# return location of a point on a circle +def point_on_circle(radius: float, theta_deg: float, center: np.ndarray = (0, 0)) -> tuple[int, int]: + theta = radians(theta_deg) + res = center + radius * np.array([cos(theta), sin(theta)]) + return tuple([round(i) for i in res]) + +# return location of a point equidistant from origin randomly distributed between theta of spread_deg +def spread(pos_abs: tuple[float, float], spread_deg: float) -> tuple[int, int]: + x1, y1 = pos_abs + start_deg = degrees(np.arctan2(y1, x1)) + random_theta_deg = random.uniform(start_deg-spread_deg/2, start_deg+spread_deg/2) + return point_on_circle(radius = dist(pos_abs, (0, 0)), theta_deg = random_theta_deg) + +# return random point within circle centered at x1, y1 with radius r +def spray(pos_abs: tuple[float, float], r: float) -> tuple[int, int]: + x1, y1 = pos_abs + x2 = random.uniform(x1-r, x1+r) + y2 = random.uniform(y1-r, y1+r) + return tuple([round(i) for i in (x2, y2)]) + +# rotate a vector by angle degrees +def rotate_vec(vec: np.ndarray, deg: float) -> np.ndarray: + theta = np.deg2rad(deg) + rot_matrix = np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]]) + return np.dot(rot_matrix, vec) + +def unit_vector(vec: np.ndarray) -> np.ndarray: + return vec / dist(vec, (0, 0)) + +def arc_spread(cast_dir: tuple[float,float], spread_deg: float=10, radius_spread: tuple[float, float] = [.95, 1.05]): + """ + Given an x,y vec (target), generate a new target that is the same vector but rotated by +/- spread_deg/2 + """ + cast_dir = np.array(cast_dir) + length = dist(cast_dir, (0, 0)) + adj = (radius_spread[1] - radius_spread[0])*random.random() + radius_spread[0] + rot = spread_deg*(random.random() - .5) + return rotate_vec(unit_vector(cast_dir)*(length+adj), rot) + +def lerp(a: float, b: float, f:float) -> float: + return a + f * (b - a) \ No newline at end of file diff --git a/src/char/capabilities.py b/src/char/tools/capabilities.py similarity index 100% rename from src/char/capabilities.py rename to src/char/tools/capabilities.py diff --git a/src/char/tools/skill_data.py b/src/char/tools/skill_data.py new file mode 100644 index 000000000..38988e47d --- /dev/null +++ b/src/char/tools/skill_data.py @@ -0,0 +1,95 @@ +import math +from functools import cache +from config import Config + +BASE_FRAMES = { + "amazon": 20, + "assassin": 17, + "barbarian": 14, + "druid": 15, + "necromancer": 16, + "paladin": 16, + "sorceress": 14, + "lightning_skills": 19, + "default": 20 +} + +ANIMATION_SPEED = { + "default": 256, + "werewolf": 228, + "werebear": 229, +} + +AURAS = { + "blessed_aim", + "cleansing", + "concentration", + "conviction" + "defiance", + "fanaticism", + "holy_fire", + "holy_freeze", + "holy_shock", + "meditation", + "might", + "prayer", + "redemption", + "resist_cold", + "resist_fire", + "resist_lightning", + "salvation", + "sanctuary", + "thorns", + "vigor" +} + +CHANNELED_SKILLS = { + "armageddon": 0, # 2.4: removed + "blade_sentinel": 2, + "blizzard": 1.8, + "dragon_flight": 1, + "fire_wall": 1.4, + "firestorm": 0.6, + "fissure": 2, + "fist_of_the_heavens": 1, # 2.4: 1 -> 0.4 + "frozen_orb": 1, + "hurricane": 0, # 2.4: removed + "hydra": 0, # 2.4: removed + "immolation_arrow": 1, + "meteor": 1.2, + "molten_boulder": 1, # 2.4: 2 -> 1 + "plague_javelin": 1, # 2.4: 4 -> 1 + "poison_javelin": 0.6, + "shadow_master": 0.6, # 2.4: 6 -> 0.6 + "shadow_warrior": 0.6, # 2.4: 6 -> 0.6 + "shock_web": 0.6, + "valkyrie": 0.6, # 2.4: 6 -> 0.6 + "volcano": 4, + "werebear": 1, + "werewolf": 1, +} + +def _get_base_frames(class_base: str, skill_name: str): + if "lightning" in skill_name.lower() and class_base == "sorceress": + class_base = "lightning_skills" + if class_base not in BASE_FRAMES: + class_base = "default" + return BASE_FRAMES[class_base] + +def _get_animation_speed(class_base: str): + return ANIMATION_SPEED[class_base] if class_base in ANIMATION_SPEED else ANIMATION_SPEED["default"] + +def _efcr(fcr: int) -> float: + return math.floor(fcr * 120 / (fcr + 120)) + +@cache +def get_casting_frames(class_base: str, skill_name: str, fcr: int): + if "lightning" in skill_name.lower() and class_base == "sorceress": + return math.ceil(256 * _get_base_frames(class_base, skill_name) / math.floor(256 * (100 + _efcr(fcr)) / 100)) + else: + return math.ceil(256 * _get_base_frames(class_base, skill_name) / math.floor(_get_animation_speed(class_base) * (100 + _efcr(fcr)) / 100)) - 1 + +def get_cast_wait_time(class_base: str, skill_name: str, fcr: int): + if skill_name in CHANNELED_SKILLS: + return (math.ceil(get_casting_frames(class_base, skill_name, fcr)/2) + Config().char["extra_casting_frames"]) * (1/25) + return (get_casting_frames(class_base, skill_name, fcr) + Config().char["extra_casting_frames"]) * (1/25) \ No newline at end of file diff --git a/src/char/trapsin.py b/src/char/trapsin.py index f7117058f..c0987f016 100644 --- a/src/char/trapsin.py +++ b/src/char/trapsin.py @@ -5,7 +5,8 @@ from logger import Logger from screen import convert_abs_to_monitor, convert_screen_to_abs, grab from config import Config -from utils.misc import wait, rotate_vec, unit_vector +from utils.misc import wait +from char.tools import calculations import random from pather import Location, Pather import numpy as np @@ -18,8 +19,7 @@ def __init__(self, skill_hotkeys: dict, pather: Pather): self._pather = pather def pre_buff(self): - if Config().char["cta_available"]: - self._pre_buff_cta() + self._pre_buff_cta() if self._skill_hotkeys["fade"]: keyboard.send(self._skill_hotkeys["fade"]) wait(0.1, 0.13) @@ -36,7 +36,7 @@ def pre_buff(self): mouse.click(button="right") wait(self._cast_duration) - def _left_attack(self, cast_pos_abs: tuple[float, float], spray: int = 10): + def _left_attack(self, cast_pos_abs: tuple[float, float], spray: float = 10): keyboard.send(Config().char["stand_still"], do_release=False) if self._skill_hotkeys["skill_left"]: keyboard.send(self._skill_hotkeys["skill_left"]) @@ -124,7 +124,7 @@ def kill_nihlathak(self, end_nodes: list[int]) -> bool: # Do some tele "dancing" after each sequence if i < atk_len - 1: rot_deg = random.randint(-10, 10) if i % 2 == 0 else random.randint(170, 190) - tele_pos_abs = unit_vector(rotate_vec(cast_pos_abs, rot_deg)) * 100 + tele_pos_abs = calculations.unit_vector(calculations.rotate_vec(cast_pos_abs, rot_deg)) * 100 pos_m = convert_abs_to_monitor(tele_pos_abs) self.pre_move() self.move(pos_m) diff --git a/src/config.py b/src/config.py index daccc72c7..8b4d8cddb 100644 --- a/src/config.py +++ b/src/config.py @@ -160,6 +160,7 @@ def load_data(self): "inventory_screen": self._select_val("char", "inventory_screen"), "teleport": self._select_val("char", "teleport"), "stand_still": self._select_val("char", "stand_still"), + "use_charged_teleport": bool(int(self._select_val("char", "use_charged_teleport"))), "force_move": self._select_val("char", "force_move"), "num_loot_columns": int(self._select_val("char", "num_loot_columns")), "take_health_potion": float(self._select_val("char", "take_health_potion")), @@ -189,7 +190,9 @@ def load_data(self): "weapon_switch": self._select_val("char", "weapon_switch"), "battle_orders": self._select_val("char", "battle_orders"), "battle_command": self._select_val("char", "battle_command"), - "casting_frames": int(self._select_val("char", "casting_frames")), + "extra_casting_frames": int(self._select_val("char", "extra_casting_frames")), + "faster_cast_rate": int(self._select_val("char", "faster_cast_rate")), + "faster_cast_rate_offhand": int(self._select_val("char", "faster_cast_rate_offhand")), "atk_len_arc": float(self._select_val("char", "atk_len_arc")), "atk_len_trav": float(self._select_val("char", "atk_len_trav")), "atk_len_pindle": float(self._select_val("char", "atk_len_pindle")), @@ -303,7 +306,6 @@ def load_data(self): "window_client_area_offset": tuple(map(int, Config()._select_val("advanced_options", "window_client_area_offset").split(","))), "ocr_during_pickit": bool(int(self._select_val("advanced_options", "ocr_during_pickit"))), "launch_options": self._select_val("advanced_options", "launch_options").replace("", only_lowercase_letters(self.general["name"].lower())), - "override_capabilities": _default_iff(Config()._select_optional("advanced_options", "override_capabilities"), ""), } self.colors = {} diff --git a/src/d2r_image/d2data_data.py b/src/d2r_image/d2data_data.py index b3fe05de5..8e990548e 100644 --- a/src/d2r_image/d2data_data.py +++ b/src/d2r_image/d2data_data.py @@ -35563,7 +35563,7 @@ "cast1", "item_fastercastrate", "itemfastercastrate", - "fcr", + "faster_cast_rate", "fastercastrate", "cast2", "cast3" diff --git a/src/inventory/personal.py b/src/inventory/personal.py index ca007bbf2..969ce192f 100644 --- a/src/inventory/personal.py +++ b/src/inventory/personal.py @@ -70,6 +70,16 @@ def inventory_has_items(img: np.ndarray = None, close_window = False) -> bool: return True return False +def is_main_weapon_active(img: np.ndarray = None) -> bool | None: + # inventory must be open + img = grab() if img is None else img + if (res := detect_screen_object(ScreenObjects.ActiveWeaponBound, img)).valid: + return "main" in res.name.lower() + if not is_visible(ScreenObjects.InventoryBackground): + Logger.warning("is_main_weapon_active(): Failed to detect active weapon, is inventory not open?") + else: + Logger.warning('fail') + return None def stash_all_items(items: list = None): """ diff --git a/src/item/pickit.py b/src/item/pickit.py index 94375d286..090785577 100644 --- a/src/item/pickit.py +++ b/src/item/pickit.py @@ -187,6 +187,7 @@ def pick_up_items(self, char: IChar) -> bool: if __name__ == "__main__": import os from config import Config + from screen import start_detecting_window, stop_detecting_window from char.sorceress import LightSorc from char.paladin import Hammerdin from pather import Pather diff --git a/src/pather.py b/src/pather.py index 98db4eb7d..45ae506bb 100644 --- a/src/pather.py +++ b/src/pather.py @@ -6,14 +6,13 @@ import cv2 import numpy as np from utils.custom_mouse import mouse -from utils.misc import wait # for stash/shrine tele cancel detection in traverse node -from utils.misc import is_in_roi +from utils.misc import wait, image_diff from config import Config from logger import Logger from screen import convert_screen_to_monitor, convert_abs_to_screen, convert_abs_to_monitor, convert_screen_to_abs, grab, stop_detecting_window import template_finder from char import IChar -from ui_manager import detect_screen_object, ScreenObjects, is_visible, select_screen_object_match, get_closest_non_hud_pixel +from ui_manager import detect_screen_object, ScreenObjects, get_hud_mask, is_visible, select_screen_object_match, get_closest_non_hud_pixel class Location: # A5 Town @@ -93,6 +92,12 @@ class Pather: def __init__(self): self._range_x = [-Config().ui_pos["center_x"] + 7, Config().ui_pos["center_x"] - 7] self._range_y = [-Config().ui_pos["center_y"] + 7, Config().ui_pos["center_y"] - Config().ui_pos["skill_bar_height"] - 33] + self._roi_middle_half = [round(x) for x in [ + Config().ui_pos["screen_width"]/4, + Config().ui_pos["screen_height"]/4, + Config().ui_pos["screen_width"]/2, + Config().ui_pos["screen_height"]/2 + ]] self._nodes = { # A5 town 0: {'A5_TOWN_0': (27, 249), 'A5_TOWN_1': (-92, -137), 'A5_TOWN_11': (-313, -177)}, @@ -500,8 +505,29 @@ def _get_node(self, key: int, template: str): def _convert_rel_to_abs(rel_loc: tuple[float, float], pos_abs: tuple[float, float]) -> tuple[float, float]: return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1]) - def traverse_nodes_fixed(self, key: str | list[tuple[float, float]], char: IChar) -> bool: - if not char.capabilities.can_teleport_natively: + @staticmethod + def _wait_for_screen_update(img_pre: np.ndarray, roi: list = None, timeout: float = 1.5, score_threshold: float = 0.15) -> tuple[np.ndarray, float, bool]: + """ + Waits for the screen to update. + :param img_pre: Image before the update + :param roi: Region of interest to be checked. If None, the whole screen is checked. + :param timeout: Timeout in seconds. + :param score_threshold: Threshold for the score. If the score is below this threshold, assume the screen has not updated. + :return: The image after the update, the score, and a boolean indicating if successful. + """ + start = time.perf_counter() + success = True + while (score := image_diff(img_pre, (img_post := grab(force_new = True)), roi = roi)) < score_threshold: + wait(0.02) + if (time.perf_counter() - start) > timeout: + success=False + break + # print(f"spent {time.perf_counter() - start} seconds waiting for window change") + return img_post, score, success + + def traverse_nodes_fixed(self, key: str | list[tuple[float, float]], char: IChar, require_teleport: bool = False) -> bool: + # this will check if character can teleport. for charged or native teleporters, it'll select teleport + if require_teleport and not (char.capabilities.can_teleport_natively and char.can_teleport()): error_msg = "Teleport is required for static pathing" Logger.error(error_msg) raise ValueError(error_msg) @@ -516,15 +542,9 @@ def traverse_nodes_fixed(self, key: str | list[tuple[float, float]], char: IChar x_m, y_m = convert_screen_to_monitor(path[i]) x_m += int(random.random() * 6 - 3) y_m += int(random.random() * 6 - 3) - t0 = grab(force_new=True) - char.move((x_m, y_m)) - t1 = grab(force_new=True) - # check difference between the two frames to determine if tele was good or not - diff = cv2.absdiff(t0, t1) - diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) - _, mask = cv2.threshold(diff, 13, 255, cv2.THRESH_BINARY) - score = (float(np.sum(mask)) / mask.size) * (1/255.0) - if score > 0.15: + char.move((x_m, y_m), use_tp=True) + _, score, _ = self._wait_for_screen_update(img_pre = grab(force_new = True), roi = self._roi_middle_half) + if score >= 0.15: i += 1 else: stuck_count += 1 @@ -556,6 +576,7 @@ def find_abs_node_pos(self, node_idx: int, img: np.ndarray, threshold: float = 0 return node_pos_abs return None + def traverse_nodes( self, path: tuple[Location, Location] | list[int], @@ -565,13 +586,17 @@ def traverse_nodes( do_pre_move: bool = True, force_move: bool = False, threshold: float = 0.68, - use_tp_charge: bool = False + active_skill: str = "", ) -> bool: """Traverse from one location to another :param path: Either a list of node indices or a tuple with (start_location, end_location) :param char: Char that is traversing the nodes :param timeout: Timeout in second. If no more move was found in that time it will cancel traverse :param force_move: Bool value if force move should be used for pathing + :param force_tp: Bool value if teleport should be used for pathing for charged characters + :param do_pre_move: Bool value if pre-move function should be used prior to moving + :param threshold: Threshold for template matching + :param active_skill: Name of the skill/aura that should be active during the traverse for walking chars or charged TP chars not using TP :return: Bool if traversed successful or False if it got stuck """ if len(path) == 0: @@ -591,23 +616,29 @@ def traverse_nodes( else: Logger.debug(f"Traverse: {path}") - if use_tp_charge and char.select_tp(): - # this means we want to use tele charge and we were able to select it - pass - elif do_pre_move: - # we either want to tele charge but have no charges or don't wanna use the charge falling back to default pre_move handling + use_tp = (char.capabilities.can_teleport_natively or (char.capabilities.can_teleport_with_charges and force_tp)) and char.can_teleport() + if do_pre_move: char.pre_move() + if not use_tp: + if active_skill == "": + active_skill = char.default_move_skill + if active_skill is not None: + char._activate_aura(active_skill) last_direction = None - for i, node_idx in enumerate(path): + last_move = time.time() + img = None + last_node_pos_abs = None + for _, node_idx in enumerate(path): continue_to_next_node = False - last_move = time.time() did_force_move = False teleport_count = 0 + identical_count = 0 while not continue_to_next_node: - img = grab(force_new=True) + if img is None or not use_tp: + img = grab(force_new=True) # Handle timeout - if (time.time() - last_move) > timeout: + if (elapsed := time.time() - last_move) > timeout: if is_visible(ScreenObjects.WaypointLabel, img): # sometimes bot opens waypoint menu, close it to find templates again Logger.debug("Opened wp, closing it again") @@ -624,7 +655,7 @@ def traverse_nodes( return False # Sometimes we get stuck at rocks and stuff, after a few seconds force a move into the last known direction - if not did_force_move and time.time() - last_move > 3.1: + if not did_force_move and elapsed > 3.1: if last_direction is not None: pos_abs = last_direction else: @@ -633,9 +664,8 @@ def traverse_nodes( pos_abs = get_closest_non_hud_pixel(pos = pos_abs, pos_type="abs") Logger.debug(f"Pather: taking a random guess towards " + str(pos_abs)) x_m, y_m = convert_abs_to_monitor(pos_abs) - char.move((x_m, y_m), force_move=True) + last_move = char.move((x_m, y_m), use_tp=use_tp, force_move=True) did_force_move = True - last_move = time.time() # Sometimes we get stuck at a Shrine or Stash, after a few seconds check if the screen was different, if force a left click. if (teleport_count + 1) % 30 == 0: @@ -655,14 +685,22 @@ def traverse_nodes( if node_pos_abs is not None: dist = math.dist(node_pos_abs, (0, 0)) if dist < Config().ui_pos["reached_node_dist"]: + Logger.debug(f"Continue to next node") continue_to_next_node = True + # if relative node position is roughly identical to previous attempt, try another screengrab + elif last_node_pos_abs is not None and math.dist(node_pos_abs, last_node_pos_abs) < 5 and identical_count <= 2: + img = grab(force_new=True) + Logger.debug(f"Identical node position {node_pos_abs} compared to previous {last_node_pos_abs}, trying another screengrab") + identical_count += 1 else: # Move the char x_m, y_m = convert_abs_to_monitor(node_pos_abs) - char.move((x_m, y_m), force_tp=force_tp, force_move=force_move) + last_move = char.move((x_m, y_m), use_tp=use_tp, force_move=force_move) last_direction = node_pos_abs - last_move = time.time() - + # wait until there's a change on screen + img, score, _ = self._wait_for_screen_update(img, roi = self._roi_middle_half, score_threshold=0.5) + Logger.debug(f"moved toward node {node_idx} at {node_pos_abs}, screen update score: {score}") + last_node_pos_abs = node_pos_abs return True diff --git a/src/run/nihlathak.py b/src/run/nihlathak.py index fc2f1d3ff..aaa2629e6 100644 --- a/src/run/nihlathak.py +++ b/src/run/nihlathak.py @@ -89,7 +89,7 @@ class EyeCheckData: # If it is found, move down that hallway if template_match.valid and template_match.name.endswith("_SAFE_DIST"): self._pather.traverse_nodes_fixed(data.destination_static_path_key, self._char) - self._pather.traverse_nodes(data.save_dist_nodes, self._char, timeout=2, do_pre_move=False) + self._pather.traverse_nodes(data.save_dist_nodes, self._char, timeout=2) end_nodes = data.end_nodes break @@ -97,7 +97,7 @@ class EyeCheckData: if end_nodes is None: self._pather.traverse_nodes_fixed("ni2_circle_back_to_a", self._char) self._pather.traverse_nodes_fixed(check_arr[0].destination_static_path_key, self._char) - self._pather.traverse_nodes(check_arr[0].save_dist_nodes, self._char, timeout=2, do_pre_move=False) + self._pather.traverse_nodes(check_arr[0].save_dist_nodes, self._char, timeout=2) end_nodes = check_arr[0].end_nodes # Attack & Pick items diff --git a/src/run/pindle.py b/src/run/pindle.py index 5eb6d07bf..da5d00410 100644 --- a/src/run/pindle.py +++ b/src/run/pindle.py @@ -31,7 +31,7 @@ def approach(self, start_loc: Location) -> bool | Location: loc = self._town_manager.go_to_act(5, start_loc) if not loc: return False - if not self._pather.traverse_nodes((loc, Location.A5_NIHLATHAK_PORTAL), self._char): + if not self._pather.traverse_nodes((loc, Location.A5_NIHLATHAK_PORTAL), self._char, force_move=True): return False wait(0.5, 0.6) found_loading_screen_func = lambda: loading.wait_for_loading_screen(2.0) diff --git a/src/run/trav.py b/src/run/trav.py index 3726e2bc3..c3798d936 100644 --- a/src/run/trav.py +++ b/src/run/trav.py @@ -52,8 +52,8 @@ def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]: wait(0.2, 0.3) # If we can teleport we want to move back inside and also check loot there if self._char.capabilities.can_teleport_natively or self._char.capabilities.can_teleport_with_charges: - if not self._pather.traverse_nodes([229], self._char, timeout=2.5, use_tp_charge=self._char.capabilities.can_teleport_natively): - self._pather.traverse_nodes([228, 229], self._char, timeout=2.5, use_tp_charge=True) + if not self._pather.traverse_nodes([229], self._char, timeout=2.5, force_tp=self._char.capabilities.can_teleport_natively): + self._pather.traverse_nodes([228, 229], self._char, timeout=2.5, force_tp=True) picked_up_items |= self._pickit.pick_up_items(self._char) # If travincal run is not the last run if self.name != self._runs[-1]: diff --git a/src/screen.py b/src/screen.py index 78b788f6c..68675b48f 100644 --- a/src/screen.py +++ b/src/screen.py @@ -122,4 +122,4 @@ def convert_abs_to_monitor(abs_coord: tuple[float, float]) -> tuple[float, float return None screen_coord = convert_abs_to_screen(abs_coord) monitor_coord = convert_screen_to_monitor(screen_coord) - return monitor_coord + return monitor_coord \ No newline at end of file diff --git a/src/target_detect.py b/src/target_detect.py index 914bd7143..2c8c67123 100644 --- a/src/target_detect.py +++ b/src/target_detect.py @@ -67,6 +67,7 @@ def get_visible_targets( )) if targets: targets = sorted(targets, key=lambda obj: obj.distance) + Logger.debug(f"{len(targets)} targets detected, closest at {targets[0].center_abs} {targets[0].distance} px away") return targets def _bright_contrast(img: np.ndarray, brightness: int = 255, contrast: int = 127): @@ -248,22 +249,17 @@ def live_view(self): start_detecting_window() print("Move to d2r window and press f11") keyboard.wait("f11") - # l = LiveViewer() + 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) +# 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/ui/skills.py b/src/ui/skills.py index 1b0926743..0496f205b 100644 --- a/src/ui/skills.py +++ b/src/ui/skills.py @@ -2,69 +2,85 @@ from logger import Logger import cv2 import time +from utils.custom_mouse import mouse import numpy as np from utils.misc import cut_roi, color_filter, wait -from screen import grab +from screen import grab, convert_screen_to_monitor from config import Config import template_finder -from ui_manager import wait_until_visible, ScreenObjects +from ui_manager import is_visible, wait_until_visible, ScreenObjects from d2r_image import ocr -def is_left_skill_selected(template_list: list[str]) -> bool: - """ - :return: Bool if skill is currently the selected skill on the left skill slot. - """ - skill_left_ui_roi = Config().ui_roi["skill_left"] - for template in template_list: - if template_finder.search(template, grab(), threshold=0.84, roi=skill_left_ui_roi).valid: - return True - return False - -def has_tps() -> bool: - """ - :return: Returns True if botty has town portals available. False otherwise - """ - if Config().char["town_portal"]: - keyboard.send(Config().char["town_portal"]) - if not (tps_remain := wait_until_visible(ScreenObjects.TownPortalSkill, timeout=4).valid): - Logger.warning("You are out of tps") - if Config().general["info_screenshots"]: - cv2.imwrite("./log/screenshots/info/debug_out_of_tps_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab()) - return tps_remain - else: - return False +LEFT_SKILL_ROI = [ + Config().ui_pos["skill_left_x"] - (Config().ui_pos["skill_width"] // 2), + Config().ui_pos["skill_y"] - (Config().ui_pos["skill_height"] // 2), + Config().ui_pos["skill_width"], + Config().ui_pos["skill_height"] +] -def select_tp(tp_hotkey): - if tp_hotkey and not is_right_skill_selected( - ["TELE_ACTIVE", "TELE_INACTIVE"]): - keyboard.send(tp_hotkey) - wait(0.1, 0.2) - return is_right_skill_selected(["TELE_ACTIVE", "TELE_INACTIVE"]) -def is_right_skill_active() -> bool: - """ - :return: Bool if skill is red/available or not. Skill must be selected on right skill slot when calling the function. - """ - roi = [ +RIGHT_SKILL_ROI = [ Config().ui_pos["skill_right_x"] - (Config().ui_pos["skill_width"] // 2), Config().ui_pos["skill_y"] - (Config().ui_pos["skill_height"] // 2), Config().ui_pos["skill_width"], Config().ui_pos["skill_height"] - ] +] + + +def _is_skill_active(roi: list[int]) -> bool: + """ + :return: Bool if skill is currently active in the desired ROI + """ img = cut_roi(grab(), roi) avg = np.average(img) return avg > 75.0 -def is_right_skill_selected(template_list: list[str]) -> bool: +def is_skill_active(roi: list[int]) -> bool: + return _is_skill_active(roi) + +def is_right_skill_active() -> bool: + return _is_skill_active(roi = RIGHT_SKILL_ROI) + +def is_left_skill_active() -> bool: + return _is_skill_active(roi = LEFT_SKILL_ROI) + + +def _is_skill_bound(template_list: list[str] | str, roi: list[int]) -> bool: """ :return: Bool if skill is currently the selected skill on the right skill slot. """ - skill_right_ui_roi = Config().ui_roi["skill_right"] + if isinstance(template_list, str): + template_list = [template_list] for template in template_list: - if template_finder.search(template, grab(), threshold=0.84, roi=skill_right_ui_roi).valid: + if template_finder.search(template, grab(), threshold=0.84, roi=roi).valid: return True return False +def is_skill_bound(template_list: list[str] | str, roi: list[int] = Config().ui_roi["active_skills_bar"]) -> bool: + """ + :return: Bool if skill is visible in ROI, defaults to active skills bar. + """ + return _is_skill_bound(template_list, roi) + +def is_right_skill_bound(template_list: list[str] | str) -> bool: + return _is_skill_bound(template_list, RIGHT_SKILL_ROI) + +def is_left_skill_bound(template_list: list[str] | str) -> bool: + return _is_skill_bound(template_list, LEFT_SKILL_ROI) + +def is_teleport_active(img: np.ndarray = None) -> bool: + img = grab() if img is None else img + if (match := template_finder.search(["BAR_TP_ACTIVE", "BAR_TP_INACTIVE"], img, roi=Config().ui_roi["active_skills_bar"], best_match=True)).valid: + return not "inactive" in match.name.lower() + return False + +def has_tps() -> bool: + if not (tps_remain := is_visible(ScreenObjects.BarTownPortalSkill)): + Logger.warning("You are out of tps") + if Config().general["info_screenshots"]: + cv2.imwrite("./log/screenshots/info/debug_out_of_tps_" + time.strftime("%Y%m%d_%H%M%S") + ".png", grab()) + return tps_remain + def get_skill_charges(img: np.ndarray = None): if img is None: img = grab() @@ -93,3 +109,47 @@ def get_skill_charges(img: np.ndarray = None): return int(ocr_result.text) except: return None + +def skill_is_charged(img: np.ndarray = None) -> bool: + img = grab() if img is None else img + skill_img = cut_roi(img, Config().ui_roi["skill_right"]) + charge_mask, _ = color_filter(skill_img, Config().colors["blue"]) + if np.sum(charge_mask) > 0: + return True + return False + +def is_low_on_teleport_charges(img: np.ndarray = None) -> bool: + img = grab() if img is None else img + charges_remaining = get_skill_charges(img) + if charges_remaining: + Logger.debug(f"{charges_remaining} teleport charges remain") + return charges_remaining <= 3 + else: + charges_present = skill_is_charged(img) + if charges_present: + Logger.error("is_low_on_teleport_charges: unable to determine skill charges, assume zero") + return True + +def _remap_skill_hotkey(skill_assets: list[str] | str, hotkey: str, skill_roi: list[int], expanded_skill_roi: list[int]) -> bool: + x, y, w, h = skill_roi + x, y = convert_screen_to_monitor((x, y)) + mouse.move(x + w/2, y + h / 2) + mouse.click("left") + wait(0.3) + + if isinstance(skill_assets, str): + skill_assets = [skill_assets] + for skill_asset in skill_assets: + match = template_finder.search(skill_asset, grab(), threshold=0.84, roi=expanded_skill_roi) + if match.valid: + mouse.move(*match.center_monitor) + wait(0.3) + keyboard.send(hotkey) + wait(0.3) + mouse.click("left") + wait(0.3) + return True + return False + +def remap_right_skill_hotkey(skill_assets: list[str] | str, hotkey: str) -> bool: + return _remap_skill_hotkey(skill_assets, hotkey, Config().ui_roi["skill_right"], Config().ui_roi["skill_speed_bar"]) \ No newline at end of file diff --git a/src/ui_manager.py b/src/ui_manager.py index 18ba1a465..6484e2c24 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -161,6 +161,12 @@ class ScreenObjects: best_match=True, threshold=0.79 ) + BarTownPortalSkill=ScreenObject( + ref=["BAR_TP_ACTIVE", "BAR_TP_INACTIVE"], + roi="active_skills_bar", + best_match=True, + threshold=0.79 + ) RepairBtn=ScreenObject( ref="REPAIR_BTN", roi="repair_btn", @@ -264,6 +270,13 @@ class ScreenObjects: roi="inventory_bg_pattern", threshold=0.8, ) + ActiveWeaponBound=ScreenObject( + ref=["ACTIVE_WEAPON_MAIN", "ACTIVE_WEAPON_OFFHAND"], + #roi="active_weapon_tabs", + threshold=0.8, + use_grayscale=True, + best_match=True, + ) def detect_screen_object(screen_object: ScreenObject, img: np.ndarray = None) -> TemplateMatch: roi = Config().ui_roi[screen_object.roi] if screen_object.roi else None diff --git a/src/utils/live-view-last-settings.json b/src/utils/live-view-last-settings.json index c119f0f25..b16461b7d 100644 --- a/src/utils/live-view-last-settings.json +++ b/src/utils/live-view-last-settings.json @@ -1 +1 @@ -{"erode": 0, "dilate": 0, "blur": 3, "lh": 110, "ls": 169, "lv": 50, "uh": 120, "us": 255, "uv": 255, "bright": 255, "contrast": 127, "thresh": 0, "invert": 0} \ No newline at end of file +{"erode": 0, "dilate": 0, "blur": 3, "lh": 0, "ls": 0, "lv": 0, "uh": 180, "us": 255, "uv": 255, "bright": 255, "contrast": 127, "thresh": 0, "invert": 0} \ No newline at end of file diff --git a/src/utils/misc.py b/src/utils/misc.py index 68827647e..4411d1c12 100644 --- a/src/utils/misc.py +++ b/src/utils/misc.py @@ -13,7 +13,7 @@ from logger import Logger import cv2 import os -from math import cos, sin, dist +from math import cos, sin, dist, pi import subprocess from win32con import HWND_TOPMOST, SWP_NOMOVE, SWP_NOSIZE, HWND_NOTOPMOST from win32gui import GetWindowText, SetWindowPos, EnumWindows, GetClientRect, ClientToScreen @@ -94,11 +94,11 @@ def kill_thread(thread): ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0) Logger.error('Exception raise failure') -def cut_roi(img, roi): +def cut_roi(img: np.ndarray, roi: list) -> np.ndarray: x, y, w, h = roi return img[y:y+h, x:x+w] -def mask_by_roi(img, roi, type: str = "regular"): +def mask_by_roi(img: np.ndarray, roi: list, type: str = "regular"): x, y, w, h = roi if type == "regular": masked = np.zeros(img.shape, dtype=np.uint8) @@ -209,14 +209,6 @@ def list_files_in_folder(path: str): r.append(os.path.join(root, name)) return r -def rotate_vec(vec: np.ndarray, deg: float) -> np.ndarray: - theta = np.deg2rad(deg) - rot_matrix = np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]]) - return np.dot(rot_matrix, vec) - -def unit_vector(vec: np.ndarray) -> np.ndarray: - return vec / dist(vec, (0, 0)) - def image_is_equal(img1: np.ndarray, img2: np.ndarray) -> bool: shape_equal = img1.shape == img2.shape if not shape_equal: @@ -224,16 +216,27 @@ def image_is_equal(img1: np.ndarray, img2: np.ndarray) -> bool: return False return not(np.bitwise_xor(img1, img2).any()) -def arc_spread(cast_dir: tuple[float,float], spread_deg: float=10, radius_spread: tuple[float, float] = [.95, 1.05]): - """ - Given an x,y vec (target), generate a new target that is the same vector but rotated by +/- spread_deg/2 - """ - cast_dir = np.array(cast_dir) - length = dist(cast_dir, (0, 0)) - adj = (radius_spread[1] - radius_spread[0])*random.random() + radius_spread[0] - rot = spread_deg*(random.random() - .5) - return rotate_vec(unit_vector(cast_dir)*(length+adj), rot) +def apply_mask(img: np.ndarray, mask: np.ndarray) -> np.ndarray: + if img.shape[:][:][0] == mask.shape[:][:][0]: + return cv2.bitwise_and(img, img, mask = mask) + Logger.warning("apply_mask: Image shape is not equal, failed to apply to img") + return img +def image_diff(img1: np.ndarray, img2: np.ndarray, roi: list = None, mask: np.ndarray = None, threshold: int = 13) -> float: + if img1.shape != img2.shape: + Logger.warning("image_diff: Image shape is not equal, failed to calculate diff") + return 0 + if mask is not None: + img1 = apply_mask(img1, mask) + img2 = apply_mask(img2, mask) + if roi is not None: + img1 = cut_roi(img1, roi) + img2 = cut_roi(img2, roi) + diff = cv2.absdiff(img1, img2) + diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) + _, diff_mask = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY) + score = (float(np.sum(diff_mask)) / diff_mask.size) * (1/255.0) + return score @dataclass class BestMatchResult: @@ -265,4 +268,4 @@ def slugify(value, allow_unicode=False): def only_lowercase_letters(value): if not (x := ''.join(filter( lambda x: x in 'abcdefghijklmnopqrstuvwxyz', value ))): x = "botty" - return x \ No newline at end of file + return x diff --git a/src/utils/static_run_recorder.py b/src/utils/static_run_recorder.py index 9ddef9843..ff189fce0 100644 --- a/src/utils/static_run_recorder.py +++ b/src/utils/static_run_recorder.py @@ -1,7 +1,17 @@ -from screen import convert_monitor_to_screen +from tracemalloc import start +from screen import convert_monitor_to_screen, start_detecting_window, stop_detecting_window import mouse +import keyboard +from logger import Logger +import os if __name__ == "__main__": + + keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1)) + Logger.debug("Start with F11") + start_detecting_window() + keyboard.wait("f11") + pos_list = [] while 1: mouse.wait(button=mouse.RIGHT, target_types=mouse.DOWN)