Compare commits

..
15 Commits
Author SHA1 Message Date
aeon0 b33135f704 Bump version to v0.5.1 (#294) 2021-12-15 19:03:52 +01:00
aeon0 67e03fc938 Adjust template for nil on A (#293)
* Adjust template for nil on A

* change order
2021-12-15 18:05:53 +01:00
aeon0 3c39cfa5d9 Fix trav gold pick up only after latest changes + clean ups (#292) 2021-12-15 16:24:55 +01:00
tw1st40 be1f1ce08c Barbarian support (currently War Cry/singer only) with Horking (#288)
* Added Barbarian support (currently War Cry/singer only) with Horking (find item) - thanks Outburst.

* Changes as requested by aeon for pull request to be merged.
2021-12-15 15:49:09 +01:00
yhkinsar 7b7815a021 Support Greater Potion (#290) 2021-12-15 15:47:40 +01:00
EthanJian 5b70c24c7b Add-Mercenary-deaths-counter (#287)
1. Add a merc deaths counter in game_stats.py.

2. Trigger: put it in bot.py while Check if merc needs to be revived.
2021-12-15 08:42:24 +01:00
egut125 636745f47b Add location from the Run procedure (#283)
* Add location from the Run procedure and all Discord messages can leverage

* Removing old area calls

* remove logger

* Shenk Eld improvements

* Cleaning

* Cleaning
2021-12-14 23:04:33 +01:00
aeon0 4d083bee83 Use static for sorc at trav (#285)
* Sorc use static at trav

* Sorc use static at trav
2021-12-13 23:03:41 +01:00
aeon0 fe8c6d5de0 Trav for Sorc (#284) 2021-12-13 20:24:47 +01:00
aeon0 857052c75d Tele dancing sorc nil (#282)
* Implement simple "tele-dancing" for sorc nil

* increase trav atk len
2021-12-13 19:13:53 +01:00
aeon0 bf7ce99c5f Implement Nihlatak for Sorc (#281) 2021-12-13 18:14:03 +01:00
aeon0 b6260327b7 Add area for item pickup 2021-12-13 17:20:47 +01:00
aeon0 8a85f38819 Optional Redemption 2021-12-13 17:17:58 +01:00
aeon0 c2bd99af18 Fix gold stashing bug (#280) 2021-12-13 17:00:52 +01:00
aeon0 5e43da1f02 Bump version to v0.5.0 (#278) 2021-12-12 21:22:45 +01:00
35 changed files with 473 additions and 124 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+1
View File
@@ -109,6 +109,7 @@ repair_btn=318,473,90,80
inventory=860,343,394,165
vendor_stash=29,84,400,400
skill_right=664,673,41,41
skill_left=574,673,41,41
hero_selection_logo=0,0,367,553
window_ingame_ref=600,650,90,90
no_pickup=5,530,240,65
+47 -28
View File
@@ -1,20 +1,20 @@
; There is detailed documentation for each parameter in the README.md
[general]
name=Botty
saved_games_folder=
monitor=0
max_game_length_s=320
custom_message_hook=
logg_lvl=debug
max_game_length_s=380
resume_key=f11
exit_key=f12
auto_settings_key=f9
graphic_debugger_key=f10
logg_lvl=debug
randomize_runs=0
difficulty=hell
custom_message_hook=
discord_status_count=20
info_screenshots=1
loot_screenshots=0
saved_games_folder=
[routes]
run_trav=0
@@ -24,48 +24,56 @@ run_shenk=0
run_nihlatak=0
[char]
; ==========================
; ==== Mandatory Fields ====
; ==========================
; These configs have to be alligned with your d2r settings and char build
type=sorceress
casting_frames=11
stash_gold=1
gold_trav_only=0
use_merc=1
atk_len_trav=2.0
atk_len_pindle=3.0
atk_len_eldritch=3.0
atk_len_shenk=4.0
atk_len_nihlatak=4.0
num_loot_columns=5
take_health_potion=0.8
take_mana_potion=0.6
take_rejuv_potion_health=0.4
take_rejuv_potion_mana=0.1
heal_merc=0.5
heal_rejuv_merc=0.2
chicken=0.2
merc_chicken=0
show_items=alt
inventory_screen=i
;note: stand_still can not be the default "shift" as it would interfere with merc healing
stand_still=capslock
force_move=e
tp=f9
belt_rows=4
;note: stand_still can not be the default "shift" as it would interfere with merc healing
stand_still=capslock
;note: this is different from the default hotkey as "~" is for many keyboards not reachable without also pressing altgr
show_belt=k
potion1=1
potion2=2
potion3=3
potion4=4
belt_rejuv_columns=2
belt_hp_columns=1
belt_mp_columns=1
pre_buff_every_run=0
cta_available=0
weapon_switch=x
battle_orders=f7
battle_command=f8
; ==========================
; ==== Optional configs ====
; ==========================
stash_gold=1
gold_trav_only=0
use_merc=1
; Attack length for barbarians should be as high as 8-10 and even 10-12 for trav/shenk
atk_len_trav=3.0
atk_len_pindle=3.0
atk_len_eldritch=3.0
atk_len_shenk=4.0
atk_len_nihlatak=4.0
take_health_potion=0.8
take_mana_potion=0.6
take_rejuv_potion_health=0.42
take_rejuv_potion_mana=0.1
heal_merc=0.5
heal_rejuv_merc=0.2
chicken=0.35
merc_chicken=0
belt_rejuv_columns=2
belt_hp_columns=1
belt_mp_columns=1
pre_buff_every_run=0
; Supported runs: Pindle, Eldritch, Shenk
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlathak
[sorceress]
teleport=f1
skill_left=f2
@@ -74,8 +82,9 @@ frozen_armor=
energy_shield=
thunder_storm=
telekinesis=
static=
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlatak (tele only)
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlathak (tele only)
[hammerdin]
teleport=f1
concentration=f2
@@ -94,6 +103,16 @@ shadow_warrior=
lightning_sentry=f2
death_sentry=f3
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlatak (tele only)
; Make sure leap hotkey is set if you do not have Enigma
; Ensure Battle Order/Command hotkeys are set above in the [char] section
[barbarian]
teleport=
leap=f6
shout=f3
war_cry=f5
find_item=f4
[dclone]
; Normally have known region IP (that we didn't block) for ip hunting
; eg: EU Server expected IP would be 37.244.11.xxx or 37.244.48.xxx and US Server expected IP would be 158.115.222.xxx or 158.115.221.xxx
+47 -45
View File
@@ -9,10 +9,12 @@
;==================================================================;
;potions
misc_rejuvenation_potion=1
misc_rejuvenation_potion=0
misc_full_rejuvenation_potion=1
misc_super_healing_potion=1
misc_super_mana_potion=1
misc_greater_healing_potion=0
misc_greater_mana_potion=0
;other
misc_tp_scroll=0
@@ -83,9 +85,9 @@ rune_16_io=0
rune_17_lum=0
rune_18_ko=0
rune_19_fal=0
rune_20_lem=1
rune_20_lem=0
rune_21_pul=1
rune_22_um=2
rune_22_um=1
rune_23_mal=2
rune_24_ist=2
rune_25_gul=2
@@ -249,11 +251,11 @@ rare_griffon_headdress=0
rare_alpha_helm=0
;elite
rare_earth_spirit=1
rare_dream_spirit=1
rare_sky_spirit=1
rare_sun_spirit=1
rare_blood_spirit=1
rare_earth_spirit=0
rare_dream_spirit=0
rare_sky_spirit=0
rare_sun_spirit=0
rare_blood_spirit=0
;---------------------------------------;
@@ -317,11 +319,11 @@ rare_sharkskin_boots=0
rare_demonhide_boots=0
;elite
rare_myrmidon_greaves=1
rare_mirrored_boots=1
rare_boneweave_boots=1
rare_scarabshell_boots=1
rare_wyrmhide_boots=1
rare_myrmidon_greaves=0
rare_mirrored_boots=0
rare_boneweave_boots=0
rare_scarabshell_boots=0
rare_wyrmhide_boots=0
;---------------------------------------;
@@ -343,11 +345,11 @@ rare_battle_gauntlets=0
rare_war_gauntlets=0
;elite
rare_bramble_mitts=1
rare_vampirebone_gloves=1
rare_vambraces=1
rare_crusader_gauntlets=1
rare_ogre_gauntlets=1
rare_bramble_mitts=0
rare_vampirebone_gloves=0
rare_vambraces=0
rare_crusader_gauntlets=0
rare_ogre_gauntlets=0
;---------------------------------------;
@@ -440,11 +442,11 @@ white_superior_scarab_husk=0
white_superior_wyrmhide=0
;ethereal-socketed
eth_armor_archon_plate=1
eth_armor_dusk_shroud=1
eth_armor_archon_plate=0
eth_armor_dusk_shroud=0
eth_armor_lacquered_plate=0
eth_armor_light_plate=0
eth_armor_mage_plate=1
eth_armor_mage_plate=0
eth_armor_sacred_armor=0
eth_armor_shadow_plate=0
eth_armor_great_hauberk=0
@@ -457,14 +459,14 @@ eth_armor_kraken=0
;superior ethereal-socketed
eth_superior_archon_plate=1
eth_superior_dusk_shroud=1
eth_superior_lacquered_plate=1
eth_superior_lacquered_plate=3
eth_superior_light_plate=0
eth_superior_mage_plate=1
eth_superior_sacred_armor=1
eth_superior_shadow_plate=1
eth_superior_sacred_armor=3
eth_superior_shadow_plate=3
eth_superior_great_hauberk=0
eth_superior_wyrmhide=1
eth_superior_boneweave=1
eth_superior_wyrmhide=0
eth_superior_boneweave=0
;---------------------------------------;
@@ -488,10 +490,10 @@ white_monarch=0
;superior white
white_superior_crown_shield=0
white_superior_monarch=1
white_superior_monarch=0
;ethereal
eth_shield_monarch=1
eth_shield_monarch=0
eth_shield_troll_nest=0
@@ -531,10 +533,10 @@ eth_akaran_rondache=0
eth_royal_shield=0
;superior ethereal-socketed
eth_superior_vortex_shield=0
eth_superior_vortex_shield=1
eth_superior_royal_shield=0
eth_superior_sacred_targe=1
eth_superior_sacred_rondache=0
eth_superior_sacred_rondache=1
eth_superior_kurast_shield=0
eth_superior_zakarum_shield=0
eth_superior_akaran_targe=0
@@ -779,13 +781,13 @@ uniq_corpsemourn=0
uniq_the_gladiators_bane=0
uniq_leviathan=0
uniq_arkaines_valor=0
uniq_tyraels_might=1
uniq_armor_duriel_shell=1
uniq_tyraels_might=0
uniq_armor_duriel_shell=0
uniq_armor_guardian_angel=0
uniq_armor_ormus_robes=1
uniq_armor_shaftstop=0
uniq_armor_skin_of_the_vipermagi=1
uniq_armor_skullder_ire=1
uniq_armor_skin_of_the_vipermagi=0
uniq_armor_skullder_ire=0
;------------------------------;
;======== UNIQUE BELTS ========;
@@ -796,8 +798,8 @@ uniq_glooms_trap=0
uniq_belt_arachnid_mesh=2
uniq_belt_goldwrap=1
uniq_belt_razortail=0
uniq_belt_snowclash=1
uniq_belt_string_of_ears=1
uniq_belt_snowclash=0
uniq_belt_string_of_ears=0
uniq_belt_thundergod_vigor=0
uniq_belt_verdungo_hearty_cord=1
@@ -808,12 +810,12 @@ uniq_hotspur=0
uniq_infernostride=0
uniq_boots_goblin_toe=0
uniq_boots_gore_rider=0
uniq_boots_marrowwalk=1
uniq_boots_marrowwalk=0
uniq_boots_sandstorm_trek=1
uniq_boots_shadow_dancer=1
uniq_boots_silkweave=1
uniq_boots_shadow_dancer=0
uniq_boots_silkweave=0
uniq_boots_war_traveler=2
uniq_boots_waterwalk=1
uniq_boots_waterwalk=0
;------------------------------;
;======= UNIQUE GLOVES ========;
@@ -834,7 +836,7 @@ uniq_peasant_crown=0
uniq_crown_of_thieves=0
uniq_helm_andariel_visage=2
uniq_helm_crown_of_ages=2
uniq_helm_giant_skull=1
uniq_helm_giant_skull=0
uniq_helm_griffon_eye=2
uniq_helm_harlequin_crest=2
uniq_helm_kira_guardian=0
@@ -870,7 +872,7 @@ uniq_gheeds_fortune=0
;------------------------------;
;= UNIQUE NECROMANCER SHIELDS =;
;------------------------------;
uniq_offhand_boneflame=1
uniq_offhand_boneflame=0
uniq_offhand_darkforce_spawn=1
uniq_offhand_homunculus=1
@@ -879,8 +881,8 @@ uniq_offhand_homunculus=1
;------------------------------;
uniq_umes_lament=0
uniq_arm_of_king_leoric=0
uniq_suicide_branch=1
uniq_weapon_boneshade=1
uniq_suicide_branch=0
uniq_weapon_boneshade=0
uniq_weapon_death_web=2
;------------------------------;
@@ -925,7 +927,7 @@ uniq_weapon_gimmershred=0
;------------------------------;
uniq_weapon_death_fathom=2
uniq_weapon_eschuta_temper=2
uniq_weapon_the_oculus=2
uniq_weapon_the_oculus=0
;------------------------------;
;====== UNIQUE POLEARMS =======;
+9 -1
View File
@@ -24,6 +24,7 @@ from death_manager import DeathManager
from char.sorceress import Sorceress
from char.trapsin import Trapsin
from char.hammerdin import Hammerdin
from char.barbarian import Barbarian
from run import Pindle, ShenkEld, Trav, Nihlatak
from town import TownManager, A3, A4, A5
@@ -51,6 +52,8 @@ class Bot:
self._char: IChar = Hammerdin(self._config.hammerdin, self._config.char, self._screen, self._template_finder, self._ui_manager, self._pather)
elif self._config.char["type"] == "trapsin":
self._char: IChar = Trapsin(self._config.trapsin, self._config.char, self._screen, self._template_finder, self._ui_manager, self._pather)
elif self._config.char["type"] == "barbarian":
self._char: IChar = Barbarian(self._config.barbarian, self._config.char, self._screen, self._template_finder, self._ui_manager, self._pather)
else:
Logger.error(f'{self._config.char["type"]} is not supported! Closing down bot.')
os._exit(1)
@@ -234,6 +237,7 @@ class Bot:
merc_alive = self._template_finder.search(["MERC_A2","MERC_A1","MERC_A5","MERC_A3"], self._screen.grab(), threshold=0.9, roi=self._config.ui_roi["merc_icon"]).valid
if not merc_alive and self._config.char["use_merc"]:
Logger.info("Resurrect merc")
self._game_stats.log_merc_death()
self._curr_loc = self._town_manager.resurrect(self._curr_loc)
if not self._curr_loc:
return self.trigger_or_stop("end_game", failed=True)
@@ -294,6 +298,7 @@ class Bot:
def on_run_pindle(self):
res = False
self._do_runs["run_pindle"] = False
self._game_stats.update_location("Pindle")
self._curr_loc = self._pindle.approach(self._curr_loc)
if self._curr_loc:
res = self._pindle.battle(not self._pre_buffed)
@@ -302,14 +307,16 @@ class Bot:
def on_run_shenk(self):
res = False
self._do_runs["run_shenk"] = False
self._game_stats.update_location("Shenk")
self._curr_loc = self._shenk.approach(self._curr_loc)
if self._curr_loc:
res = self._shenk.battle(self._route_config["run_shenk"], not self._pre_buffed)
res = self._shenk.battle(self._route_config["run_shenk"], not self._pre_buffed, self._game_stats)
self._ending_run_helper(res)
def on_run_trav(self):
res = False
self._do_runs["run_trav"] = False
self._game_stats.update_location("Travincal")
self._curr_loc = self._trav.approach(self._curr_loc)
if self._curr_loc:
res = self._trav.battle(not self._pre_buffed)
@@ -318,6 +325,7 @@ class Bot:
def on_run_nihlatak(self):
res = False
self._do_runs["run_nihlatak"] = False
self._game_stats.update_location("Nihlatak")
self._curr_loc = self._nihlatak.approach(self._curr_loc)
if self._curr_loc:
res = self._nihlatak.battle(not self._pre_buffed)
+203
View File
@@ -0,0 +1,203 @@
import keyboard
from utils.custom_mouse import mouse
from char import IChar
from template_finder import TemplateFinder
from ui import UiManager
from pather import Pather
from logger import Logger
from screen import Screen
from utils.misc import wait, cut_roi, is_in_roi
import time
from pather import Pather, Location
class Barbarian(IChar):
def __init__(self, skill_hotkeys, char_config, screen: Screen, template_finder: TemplateFinder, ui_manager: UiManager, pather: Pather):
Logger.info("Setting up Barbarian")
super().__init__(skill_hotkeys, char_config, screen, template_finder, ui_manager)
self._pather = pather
self._do_pre_move = True
# offset shenk final position further to the right and bottom
self._pather.offset_node(149, [120, 70])
# In case we have a running barb, we want to switch to ???? when moving to the boss
# as most likely we will click on some mobs
if not self._skill_hotkeys["teleport"]:
self._do_pre_move = False
def _cast_war_cry(self, time_in_s: float):
# keyboard.send(self._skill_hotkeys["concentration"])
# wait(0.05, 0.1)
keyboard.send(self._char_config["stand_still"], do_release=False)
wait(0.05, 0.1)
if self._skill_hotkeys["war_cry"]:
keyboard.send(self._skill_hotkeys["war_cry"])
wait(0.05, 0.1)
start = time.time()
while (time.time() - start) < time_in_s:
wait(0.06, 0.08)
mouse.press(button="right")
wait(0.5, 0.7)
mouse.release(button="right")
wait(0.01, 0.05)
keyboard.send(self._char_config["stand_still"], do_press=False)
def _do_hork(self, hork_time: int):
# Outburst's fine work below
# save current skill img
wait(0.1)
skill_before = cut_roi(self._screen.grab(), self._config.ui_roi["skill_right"])
wait(0.5)
keyboard.send(self._char_config["weapon_switch"])
wait(0.5)
#hork
if self._skill_hotkeys["find_item"]:
keyboard.send(self._skill_hotkeys["find_item"])
wait(0.5, 0.15)
mouse.move(637, 354)
wait(0.5, 0.15)
mouse.press(button="right")
wait(hork_time)
mouse.release(button="right")
wait(1)
keyboard.send(self._char_config["weapon_switch"])
wait(0.5, 0.15)
# Make sure that we are back at the previous skill
skill_after = cut_roi(self._screen.grab(), self._config.ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val < 0.96:
Logger.warning("Failed to switch weapon, try again")
wait(1.2)
skill_after = cut_roi(self._screen.grab(), self._config.ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val < 0.96:
keyboard.send(self._char_config["weapon_switch"])
wait(0.4)
else:
Logger.warning("Turns out weapon switch just took a long time. You ever considered getting a new internet provider or to upgrade your pc?")
def pre_buff(self):
# keyboard.send(self._char_config["weapon_switch"])
# wait(0.3, 0.35)
keyboard.send(self._char_config["battle_command"])
wait(0.08, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
keyboard.send(self._char_config["battle_orders"])
wait(0.08, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
keyboard.send(self._skill_hotkeys["shout"])
wait(0.08, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.08, self._cast_duration + 0.1)
# keyboard.send(self._char_config["weapon_switch"])
wait(0.3, 0.35)
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 self._ui_manager.is_left_skill_selected(["LEAP"])
can_teleport = self._skill_hotkeys["teleport"] and self._ui_manager.is_right_skill_active()
if should_cast_leap and not can_teleport:
keyboard.send(self._skill_hotkeys["leap"])
wait(0.15, 0.25)
def _move_and_attack(self, abs_move: tuple[int, int], atk_len: float):
pos_m = self._screen.convert_abs_to_monitor(abs_move)
self.pre_move()
self.move(pos_m, force_move=True)
self._cast_war_cry(atk_len)
def kill_pindle(self) -> bool:
wait(0.1, 0.15)
if self.can_teleport():
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, time_out=1.0, do_pre_move=self._do_pre_move)
self._pather.traverse_nodes((Location.A5_PINDLE_SAFE_DIST, Location.A5_PINDLE_END), self, time_out=0.1)
self._cast_war_cry(self._char_config["atk_len_pindle"])
wait(0.1, 0.15)
self._do_hork(4)
return True
def kill_eldritch(self) -> bool:
if self.can_teleport():
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, time_out=1.0, do_pre_move=self._do_pre_move)
wait(0.05, 0.1)
self._cast_war_cry(self._char_config["atk_len_eldritch"])
wait(0.1, 0.15)
self._do_hork(4)
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, time_out=1.0, do_pre_move=self._do_pre_move)
wait(0.05, 0.1)
self._cast_war_cry(self._char_config["atk_len_shenk"])
wait(0.1, 0.15)
self._do_hork(7)
return True
def kill_council(self) -> bool:
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
atk_len = self._char_config["atk_len_trav"]
# Go inside and war cry a bit
self._pather.traverse_nodes([228, 229], self, time_out=2.5, force_tp=True)
self._cast_war_cry(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.can_teleport():
# Back to center stairs and more war cry
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
self._cast_war_cry(atk_len)
# move a bit to the top
self._move_and_attack((65, -30), atk_len)
else:
# Stay inside and cast war cry again moving forward
self._move_and_attack((40, 10), atk_len)
self._move_and_attack((-40, -20), atk_len)
self._do_hork(5)
return True
def kill_nihlatak(self, end_nodes: list[int]) -> bool:
# Move close to nilathak
self._pather.traverse_nodes(end_nodes, self, time_out=0.8, do_pre_move=False)
# move mouse to center (leftover from hammerdin)
pos_m = self._screen.convert_abs_to_monitor((0, 0))
mouse.move(*pos_m, randomize=80, delay_factor=[0.5, 0.7])
self._cast_war_cry(self._char_config["atk_len_nihlatak"] * 0.4)
self._cast_war_cry(0.8)
self._move_and_attack((30, 15), self._char_config["atk_len_nihlatak"] * 0.3)
self._cast_war_cry(0.8)
self._move_and_attack((-30, -15), self._char_config["atk_len_nihlatak"] * 0.4)
wait(0.1, 0.15)
self._cast_war_cry(1.2)
self._do_hork(5)
return True
if __name__ == "__main__":
import os
import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
from ui_manager import UiManager
config = Config()
screen = Screen(config.general["monitor"])
t_finder = TemplateFinder(screen)
pather = Pather(screen, t_finder)
ui_manager = UiManager(screen, t_finder)
char = Barbarian(config.barbarian, config.char, screen, t_finder, ui_manager, pather)
char.kill_council()
+1 -1
View File
@@ -23,7 +23,7 @@ class Hammerdin(IChar):
self._do_pre_move = False
def _cast_hammers(self, time_in_s: float, aura: str = "concentration"):
if aura in self._skill_hotkeys:
if aura in self._skill_hotkeys and self._skill_hotkeys[aura]:
keyboard.send(self._skill_hotkeys[aura])
wait(0.05, 0.1)
keyboard.send(self._char_config["stand_still"], do_release=False)
+4 -4
View File
@@ -149,13 +149,13 @@ class IChar:
keyboard.send(self._char_config["weapon_switch"])
wait(0.3, 0.35)
keyboard.send(self._char_config["battle_command"])
wait(0.08, 0.19)
wait(0.1, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.13, self._cast_duration + 0.16)
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
keyboard.send(self._char_config["battle_orders"])
wait(0.08, 0.19)
wait(0.1, 0.19)
mouse.click(button="right")
wait(self._cast_duration + 0.13, self._cast_duration + 0.16)
wait(self._cast_duration + 0.16, self._cast_duration + 0.18)
keyboard.send(self._char_config["weapon_switch"])
wait(0.3, 0.35)
# Make sure that we are back at the previous skill
+79 -8
View File
@@ -6,11 +6,12 @@ from ui import UiManager
from pather import Pather
from logger import Logger
from screen import Screen
from utils.misc import wait
from utils.misc import wait, rotate_vec, unit_vector
import random
import time
from typing import Tuple
from pather import Location, Pather
import numpy as np
class Sorceress(IChar):
@@ -55,11 +56,20 @@ class Sorceress(IChar):
mouse.click(button="right")
wait(self._cast_duration)
def _cast_static(self):
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 < 1.4:
mouse.click(button="right")
wait(self._cast_duration)
def _left_attack(self, cast_pos_abs: Tuple[float, float], delay: float, spray: int = 10):
keyboard.send(self._char_config["stand_still"], do_release=False)
if self._skill_hotkeys["skill_left"]:
keyboard.send(self._skill_hotkeys["skill_left"])
for _ in range(6):
for _ in range(5):
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
cast_pos_monitor = self._screen.convert_abs_to_monitor((x, y))
@@ -69,7 +79,7 @@ class Sorceress(IChar):
mouse.release(button="left")
keyboard.send(self._char_config["stand_still"], do_press=False)
def _main_attack(self, cast_pos_abs: Tuple[float, float], delay: float, spray: float = 10):
def _right_attack(self, cast_pos_abs: Tuple[float, float], delay: float, spray: float = 10):
keyboard.send(self._skill_hotkeys["skill_right"])
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
@@ -88,7 +98,7 @@ class Sorceress(IChar):
if pindle_pos_abs is not None:
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
for _ in range(int(self._char_config["atk_len_pindle"])):
self._main_attack(cast_pos_abs, delay, 11)
self._right_attack(cast_pos_abs, delay, 11)
self._left_attack(cast_pos_abs, delay, 11)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
@@ -109,7 +119,7 @@ class Sorceress(IChar):
if eld_pos_abs is not None:
cast_pos_abs = [eld_pos_abs[0] * 0.9, eld_pos_abs[1] * 0.9]
for _ in range(int(self._char_config["atk_len_eldritch"])):
self._main_attack(cast_pos_abs, delay, 90)
self._right_attack(cast_pos_abs, delay, 90)
self._left_attack(cast_pos_abs, delay, 90)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
@@ -130,7 +140,7 @@ class Sorceress(IChar):
if shenk_pos_abs is not None:
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(self._char_config["atk_len_shenk"])):
self._main_attack(cast_pos_abs, delay, 90)
self._right_attack(cast_pos_abs, delay, 90)
self._left_attack(cast_pos_abs, delay, 90)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
@@ -138,6 +148,67 @@ class Sorceress(IChar):
return True
return False
def kill_council(self) -> bool:
delay = [0.2, 0.3]
# Check out the node screenshot in assets/templates/trav/nodes to see where each node is at
# Go inside cast stuff in general direction
self._pather.traverse_nodes([228, 229], self, time_out=2.5, force_tp=True)
atk_pos_abs = self._pather.find_abs_node_pos(230, self._screen.grab())
if atk_pos_abs is None:
atk_pos_abs = [-300, -200]
cast_pos_abs = np.array([atk_pos_abs[0] * 0.9, atk_pos_abs[1] * 0.9])
self._right_attack(cast_pos_abs, delay, 80)
self._cast_static()
self._left_attack(cast_pos_abs, delay, 80)
self._right_attack((0, 0), delay, 30)
self._left_attack(cast_pos_abs, delay, 80)
# move a bit back
pos_m = self._screen.convert_abs_to_monitor((160, 30))
self.pre_move()
self.move(pos_m, force_move=True)
atk_pos_abs = self._pather.find_abs_node_pos(229, self._screen.grab())
if atk_pos_abs is None:
atk_pos_abs = [-200, -80]
self._left_attack(cast_pos_abs, delay, 60)
self._right_attack(cast_pos_abs, delay, 60)
self._right_attack((0, 0), delay, 60)
# Move outside
# Move a bit back and another round
self._pather.traverse_nodes([226], self, time_out=2.5, force_tp=True)
cast_pos_abs = np.array([-300, -100])
self._left_attack(cast_pos_abs, delay, 60)
self._right_attack(cast_pos_abs, delay, 60)
self._cast_static()
self._right_attack((0, 0), delay, 30)
wait(0.4)
# move a bit back
pos_m = self._screen.convert_abs_to_monitor((100, -100))
self.pre_move()
self.move(pos_m, force_move=True)
self._right_attack((0, 0), delay, 80)
return True
def kill_nihlatak(self, end_nodes: list[int]) -> bool:
# Find nilhlatak position
delay = [0.2, 0.3]
atk_sequences = int(self._char_config["atk_len_nihlatak"])
for i in range(atk_sequences):
nihlatak_pos_abs = self._pather.find_abs_node_pos(end_nodes[-1], self._screen.grab())
if nihlatak_pos_abs is not None:
cast_pos_abs = np.array([nihlatak_pos_abs[0] * 0.9, nihlatak_pos_abs[1] * 0.9])
self._right_attack(cast_pos_abs, delay, 90)
self._left_attack(cast_pos_abs, delay, 90)
# Do some tele "dancing" after each sequence
if i < atk_sequences - 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
pos_m = self._screen.convert_abs_to_monitor(tele_pos_abs)
self.pre_move()
self.move(pos_m)
# Move to items
self._pather.traverse_nodes(end_nodes, self, time_out=0.8)
return True
if __name__ == "__main__":
import os
@@ -152,5 +223,5 @@ if __name__ == "__main__":
pather = Pather(screen, t_finder)
ui_manager = UiManager(screen, t_finder)
char = Sorceress(config.sorceress, config.char, screen, t_finder, ui_manager, pather)
char.pre_buff()
pather.traverse_nodes_fixed("trav_safe_dist", char)
#char.pre_buff()
char.kill_council()
+4 -4
View File
@@ -52,7 +52,7 @@ class Trapsin(IChar):
keyboard.send(self._char_config["stand_still"], do_press=False)
def _main_attack(self, cast_pos_abs: Tuple[float, float], delay: float, spray: float = 10):
def _right_attack(self, cast_pos_abs: Tuple[float, float], delay: float, spray: float = 10):
keyboard.send(self._skill_hotkeys["lightning_sentry"])
x = cast_pos_abs[0] + (random.random() * 2*spray - spray)
y = cast_pos_abs[1] + (random.random() * 2*spray - spray)
@@ -77,7 +77,7 @@ class Trapsin(IChar):
if pindle_pos_abs is not None:
cast_pos_abs = [pindle_pos_abs[0] * 0.9, pindle_pos_abs[1] * 0.9]
for _ in range(int(self._char_config["atk_len_pindle"])):
self._main_attack(cast_pos_abs, delay, 11)
self._right_attack(cast_pos_abs, delay, 11)
self._left_attack(cast_pos_abs, delay, 11)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
@@ -98,7 +98,7 @@ class Trapsin(IChar):
if eld_pos_abs is not None:
cast_pos_abs = [eld_pos_abs[0] * 0.9, eld_pos_abs[1] * 0.9]
for _ in range(int(self._char_config["atk_len_eldritch"])):
self._main_attack(cast_pos_abs, delay, 90)
self._right_attack(cast_pos_abs, delay, 90)
self._left_attack(cast_pos_abs, delay, 90)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
@@ -119,7 +119,7 @@ class Trapsin(IChar):
if shenk_pos_abs is not None:
cast_pos_abs = [shenk_pos_abs[0] * 0.9, shenk_pos_abs[1] * 0.9]
for _ in range(int(self._char_config["atk_len_shenk"])):
self._main_attack(cast_pos_abs, delay, 90)
self._right_attack(cast_pos_abs, delay, 90)
self._left_attack(cast_pos_abs, delay, 90)
wait(self._cast_duration, self._cast_duration + 0.2)
# Move to items
+4
View File
@@ -112,6 +112,10 @@ class Config:
self.trapsin = self._config["trapsin"]
if "trapsin" in self._custom:
self.trapsin.update(self._custom["trapsin"])
self.barbarian = self._config["barbarian"]
if "barbarian" in self._custom:
self.barbarian.update(self._custom["barbarian"])
self.advanced_options = {
"pathing_delay_factor": min(max(int(self._select_val("advanced_options", "pathing_delay_factor")), 1), 10),
+24 -7
View File
@@ -21,8 +21,20 @@ class GameStats:
self._game_counter = 0
self._chicken_counter = 0
self._death_counter = 0
self._merc_death_counter = 0
self._runs_failed = 0
self._failed_game_time = 0
self._location = None
def update_location(self, loc: str):
if self._location != loc:
self._location = str(loc)
def get_location_msg(self):
if self._location is not None:
return f" at {self._location}"
else:
return ""
def _send_message_thread(self, msg: str):
if self._config.general["custom_message_hook"]:
@@ -33,21 +45,25 @@ class GameStats:
send_message_thread.daemon = True
send_message_thread.start()
def log_item_pickup(self, item_name: str, send_message: bool, area: str = None):
def log_item_pickup(self, item_name: str, send_message: bool):
self._picked_up_items.append(item_name)
if send_message:
msg = f"{self._config.general['name']}: Found {item_name}"
if area is not None:
msg += f" at {area}"
msg = f"{self._config.general['name']}: Found {item_name}{self.get_location_msg()}"
self._send_message_thread(msg)
def log_death(self):
self._death_counter += 1
self._send_message_thread(f"{self._config.general['name']}: You have died")
msg = f"{self._config.general['name']}: You have died{self.get_location_msg()}"
self._send_message_thread(msg)
def log_chicken(self):
self._chicken_counter += 1
self._send_message_thread(f"{self._config.general['name']}: You have chickened")
msg = f"{self._config.general['name']}: You have chickened{self.get_location_msg()}"
self._send_message_thread(msg)
def log_merc_death(self):
self._merc_death_counter += 1
self._send_message_thread(f"{self._config.general['name']}: Merc has died")
def log_start_game(self):
if self._game_counter > 0:
@@ -107,6 +123,7 @@ class GameStats:
Avg Game Length: {avg_length_str}
Chickens: {self._chicken_counter}
Deaths: {self._death_counter}
Merc deaths: {self._merc_death_counter}
Failed runs: {self._runs_failed}
''')
return msg
@@ -126,4 +143,4 @@ class GameStats:
if __name__ == "__main__":
game_stats = GameStats()
game_stats.log_item_pickup("rune_12", True, "shenk")
game_stats.log_item_pickup("rune_12", True)
+4 -5
View File
@@ -23,11 +23,11 @@ class PickIt:
self._config = Config()
self._last_closest_item: Item = None
def pick_up_items(self, char: IChar, area: str = None) -> bool:
def pick_up_items(self, char: IChar, is_at_trav: bool = False) -> bool:
"""
Pick up all items with specified char
:param char: The character used to pick up the item
:param area: Specify area where item was picked up for logging
:param is_at_trav: Dirty hack to reduce gold pickup only to trav area, should be removed once we can determine the amount of gold reliably
:return: Bool if any items were picked up or not. (Does not account for picking up scrolls and pots)
"""
found_nothing = 0
@@ -64,8 +64,7 @@ class PickIt:
item_list = [x for x in item_list if "rejuvenation_potion" not in x.name]
# TODO: Hacky solution for trav only gold pickup, hope we can soon read gold ammount and filter by that...
at_trav = area is not None and "trav" in area.lower()
if self._config.char["gold_trav_only"] and not at_trav:
if self._config.char["gold_trav_only"] and not is_at_trav:
item_list = [x for x in item_list if "misc_gold" not in x.name]
if len(item_list) == 0:
@@ -131,7 +130,7 @@ class PickIt:
# send log to discord
if found_items and closest_item.name not in picked_up_items:
Logger.info(f"Picking up: {closest_item.name} ({closest_item.score*100:.1f}% confidence)")
self._game_stats.log_item_pickup(closest_item.name, self._config.items[closest_item.name] == 2, area)
self._game_stats.log_item_pickup(closest_item.name, self._config.items[closest_item.name] == 2)
picked_up_items.append(closest_item.name)
else:
char.pre_move()
+1 -1
View File
@@ -244,7 +244,7 @@ class Pather:
if score > 0.15:
i += 1
else:
Logger.debug(f"Teleport cancel detected. Try same teleport action again. ({score})")
Logger.debug(f"Teleport cancel detected. Try same teleport action again. ({score:.4f})")
# if type(key) == str and ("_save_dist" in key or "_end" in key):
# cv2.imwrite(f"./info_screenshots/nil_path_{key}_" + time.strftime("%Y%m%d_%H%M%S") + ".png", self._screen.grab())
+2 -2
View File
@@ -57,7 +57,7 @@ class Nihlatak:
if not self._char.select_by_template(["NI1_STAIRS", "NI1_STAIRS_2", "NI1_STAIRS_3", "NI1_STAIRS_4"], found_loading_screen_func, threshold=0.63, time_out=4):
return False
# Wait until templates in lvl 2 entrance are found
if not self._template_finder.search_and_wait(["NI2_SEARCH_0", "NI2_SEARCH_1"], threshold=0.8, time_out=20).valid:
if not self._template_finder.search_and_wait(["NI2_SEARCH_0", "NI2_SEARCH_1", "NI2_SEARCH_2"], threshold=0.8, time_out=20).valid:
return False
@dataclass
class EyeCheckData:
@@ -67,7 +67,7 @@ class Nihlatak:
end_nodes: list[int]
check_arr = [
EyeCheckData(["NI2_A_SAFE_DIST", "NI2_A_NOATTACK"], "ni2_circle_a", [500], [501]),
EyeCheckData(["NI2_A_SAFE_DIST", "NI2_A_NOATTACK", "NI2_A_SAFE_DIST_2", "NI2_A_NOATTACK_2"], "ni2_circle_a", [500], [501]),
EyeCheckData(["NI2_B_SAFE_DIST", "NI2_B_NOATTACK", "NI2_B_NOATTACK_2"], "ni2_circle_b", [505], [506]),
EyeCheckData(["NI2_C_SAFE_DIST", "NI2_C_NOATTACK"], "ni2_circle_c", [510], [511]),
EyeCheckData(["NI2_D_SAFE_DIST", "NI2_D_NOATTACK"], "ni2_circle_d", [515], [516, 517]),
+1 -1
View File
@@ -56,5 +56,5 @@ class Pindle:
return False
self._char.kill_pindle()
wait(0.2, 0.3)
picked_up_items = self._pickit.pick_up_items(self._char, "Pindle")
picked_up_items = self._pickit.pick_up_items(self._char)
return (Location.A5_PINDLE_END, picked_up_items)
+5 -3
View File
@@ -37,8 +37,9 @@ class ShenkEld:
self._ui_manager.use_wp(5, 1)
return Location.A5_ELDRITCH_START
def battle(self, do_shenk: bool, do_pre_buff: bool) -> Union[bool, tuple[Location, bool]]:
def battle(self, do_shenk: bool, do_pre_buff: bool, game_stats) -> Union[bool, tuple[Location, bool]]:
# Eldritch
game_stats.update_location("Eldritch")
if not self._template_finder.search_and_wait(["ELDRITCH_0", "ELDRITCH_START"], threshold=0.65, time_out=20).valid:
return False
if do_pre_buff:
@@ -51,11 +52,12 @@ class ShenkEld:
self._char.kill_eldritch()
loc = Location.A5_ELDRITCH_END
wait(0.2, 0.3)
picked_up_items = self._pickit.pick_up_items(self._char, "Eldritch")
picked_up_items = self._pickit.pick_up_items(self._char)
# Shenk
if do_shenk:
Logger.info("Run Shenk")
game_stats.update_location("Shenk")
self._curr_loc = Location.A5_SHENK_START
# No force move, otherwise we might get stuck at stairs!
if not self._pather.traverse_nodes((Location.A5_SHENK_START, Location.A5_SHENK_SAFE_DIST), self._char):
@@ -63,6 +65,6 @@ class ShenkEld:
self._char.kill_shenk()
loc = Location.A5_SHENK_END
wait(1.9, 2.4) # sometimes merc needs some more time to kill shenk...
picked_up_items |= self._pickit.pick_up_items(self._char, "Shenk")
picked_up_items |= self._pickit.pick_up_items(self._char)
return (loc, picked_up_items)
+3 -3
View File
@@ -49,14 +49,14 @@ class Trav:
if not self._pather.traverse_nodes((Location.A3_TRAV_START, Location.A3_TRAV_CENTER_STAIRS), self._char, force_move=True):
return False
self._char.kill_council()
picked_up_items = self._pickit.pick_up_items(self._char, "Travincal")
picked_up_items = self._pickit.pick_up_items(self._char, is_at_trav=True)
wait(0.2, 0.3)
# If we can teleport we want to move back inside and also check loot there
if self._char.can_teleport():
if not self._pather.traverse_nodes([229], self._char, time_out=2.5):
self._pather.traverse_nodes([228, 229], self._char, time_out=2.5)
picked_up_items |= self._pickit.pick_up_items(self._char, "Travincal")
picked_up_items |= self._pickit.pick_up_items(self._char, is_at_trav=True)
# if we picked up items lets make sure we go back to the center to not hide the tp
if picked_up_items:
self._pather.traverse_nodes([229], self._char, time_out=2.5)
self._pather.traverse_nodes([230], self._char, time_out=2.5)
return (Location.A3_TRAV_CENTER_STAIRS, picked_up_items)
+2
View File
@@ -64,6 +64,8 @@ class Screen:
found_offsets = True
break
if not found_offsets:
if self._config.general["info_screenshots"]:
cv2.imwrite("./info_screenshots/error_d2r_window_now_found_" + time.strftime("%Y%m%d_%H%M%S") + ".png", self.grab())
Logger.error("Could not find D2R logo at hero selection or template for ingame, shutting down")
Logger.error(f"The max score that could be found was: ({debug_max_val*100:.1f}% confidence)")
raise RuntimeError("Could not determine window offset. Please make sure you have the D2R window " +
+1 -1
View File
@@ -156,4 +156,4 @@ if __name__ == "__main__":
a4 = A4(screen, template_finder, pather, char, npc_manager)
a3 = A3(screen, template_finder, pather, char, npc_manager)
town_manager = TownManager(template_finder, ui_manager, item_finder, a3, a4, a5)
print(town_manager.stash(Location.A3_STASH_WP))
print(town_manager.stash(Location.A5_STASH))
+4 -2
View File
@@ -24,7 +24,9 @@ class BeltManager:
"misc_rejuvenation_potion": "rejuv",
"misc_full_rejuvenation_potion": "rejuv",
"misc_super_healing_potion": "health",
"misc_super_mana_potion": "mana"
"misc_greater_healing_potion": "health",
"misc_super_mana_potion": "mana",
"misc_greater_mana_potion": "mana"
}
def get_pot_needs(self):
@@ -160,7 +162,7 @@ class BeltManager:
pot_positions = []
for column, row in itertools.product(range(num_loot_columns), range(4)):
center_pos, slot_img = UiManager.get_slot_pos_and_img(self._config, img, column, row)
found = self._template_finder.search(["SUPER_HEALING_POTION", "SUPER_MANA_POTION", "FULL_REJUV_POTION", "REJUV_POTION"], slot_img, threshold=0.9).valid
found = self._template_finder.search(["GREATER_HEALING_POTION", "GREATER_MANA_POTION", "SUPER_HEALING_POTION", "SUPER_MANA_POTION", "FULL_REJUV_POTION", "REJUV_POTION"], slot_img, threshold=0.9).valid
if found:
pot_positions.append(center_pos)
keyboard.press("shift")
+17 -7
View File
@@ -77,6 +77,15 @@ class UiManager():
return True
return False
def is_left_skill_selected(self, template_list: List[str]) -> bool:
"""
:return: Bool if skill is currently the selected skill on the left skill slot.
"""
for template in template_list:
if self._template_finder.search(template, self._screen.grab(), threshold=0.87, roi=self._config.ui_roi["skill_left"]).valid:
return True
return False
def is_overburdened(self) -> bool:
"""
:return: Bool if the last pick up overburdened your char. Must be called right after picking up an item.
@@ -157,19 +166,16 @@ class UiManager():
# it returns a bool value (True or False) if the button was found, and the position of it
# roi = Region of interest. It reduces the search area and can be adapted within game.ini
# by running >> python src/screen.py you can visualize all of the currently set region of interests
found_btn = self._template_finder.search(["PLAY_BTN","PLAY_BTN_GRAY"], img, roi=self._config.ui_roi["offline_btn"], threshold=0.8, best_match=True)
found_btn = self._template_finder.search(["PLAY_BTN", "PLAY_BTN_GRAY"], img, roi=self._config.ui_roi["offline_btn"], threshold=0.8, best_match=True)
if found_btn.name == "PLAY_BTN":
# We need to convert the position to monitor coordinates (e.g. if someone is using 2 monitors or windowed mode)
x, y = self._screen.convert_screen_to_monitor(found_btn.position)
Logger.debug(f"Found Play Btn")
# move the mouse to the play button and randomize the position a bit. +-35 pixel in x direction, +-7 pixel in y direction
mouse.move(x, y, randomize=[35, 7], delay_factor=[1.0, 1.8])
wait(0.1, 0.15)
# click!
mouse.click(button="left")
break
else:
# Might be in online mode?
found_btn = self._template_finder.search("PLAY_BTN", img, roi=self._config.ui_roi["online_btn"], threshold=0.8)
if found_btn.valid:
Logger.error("Botty only works for single player. Please switch to offline mode and restart botty!")
@@ -305,7 +311,7 @@ class UiManager():
Logger.debug("Found inventory gold btn")
# stash gold
if self._config.char["stash_gold"]:
inventory_no_gold = self._template_finder.search("INVENTORY_NO_GOLD", self._screen.grab(), roi=self._config.ui_roi["inventory_gold"], threshold=0.97)
inventory_no_gold = self._template_finder.search("INVENTORY_NO_GOLD", self._screen.grab(), roi=self._config.ui_roi["inventory_gold"], threshold=0.83)
if inventory_no_gold.valid:
Logger.debug("Skipping gold stashing")
else:
@@ -320,10 +326,14 @@ class UiManager():
wait(0.4, 0.6)
keyboard.send("enter") #if stash already full of gold just nothing happens -> gold stays on char -> no popup window
wait(1.0, 1.2)
inventory_no_gold = self._template_finder.search("INVENTORY_NO_GOLD", self._screen.grab(), roi=self._config.ui_roi["inventory_gold"], threshold=0.97)
# move cursor away from button to interfere with screen grab
mouse.move(-120, 0, absolute=False, randomize=15)
inventory_no_gold = self._template_finder.search("INVENTORY_NO_GOLD", self._screen.grab(), roi=self._config.ui_roi["inventory_gold"], threshold=0.83)
if not inventory_no_gold.valid:
Logger.info("Stash tab is full of gold, selecting next stash tab.")
self._curr_stash["gold"] += 1
if self._config.general["info_screenshots"]:
cv2.imwrite("./info_screenshots/info_gold_stash_full_" + time.strftime("%Y%m%d_%H%M%S") + ".png", self._screen.grab())
if self._curr_stash["gold"] > 3:
# turn of gold pickup
self._config.char["stash_gold"] = False
@@ -537,4 +547,4 @@ if __name__ == "__main__":
template_finder = TemplateFinder(screen)
item_finder = ItemFinder(config)
ui_manager = UiManager(screen, template_finder)
ui_manager.stash_all_items(5, item_finder)
ui_manager.start_game()
+9
View File
@@ -6,6 +6,7 @@ from logger import Logger
import cv2
from typing import List, Tuple
import os
from math import cos, sin, dist
def wait(min_seconds, max_seconds = None):
@@ -65,3 +66,11 @@ def list_files_in_folder(path: str):
for name in files:
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))
+1 -1
View File
@@ -1 +1 @@
__version__ = '0.4.4-dev'
__version__ = '0.5.1-dev'