Major feature: FOHdin, Chaos Sanctuary (#765)

* Setting Up FOHdin for CS

* replaced hammers with FOH, but not targeted yet

* finish merging

* demo

* implemented all runs. avoid nihlatak.

* replaced FOH with Hammer for Nihl

* using hammers for dia now

* reverted dia hammers to FOH, so pure FOH runs CS

* added non mob-detection attacks to trav

* some tweaks

* tweaked hammer pindle for tele amu

* improved pindle for walker FOHdin

* Pindle, Eld, Shenk, CS tweaked. Trav needs work.

* fix NoneType error if nodes aren't found

* consolidate trav code

* small adjustment

* increase attack duration in loop condition rather than attack sequence

* pindle optimizations

* optimizations of pindle, eld, shenk

* more optimizations, trav still kinda ghetto

* update target detect to include closer targets

* implement concentration with holy bolt

* nihlathak and summoner

* revised CS trash atk pattern

* tweaked sealbosses!

* CS ready for pressure tests

* implement in-chaos pickit, match/case logic over if/elif, consolidate more functions, move some to i_char.py, patch health_manager.py to prevent it from closing inventory, update inspect_items() to allow items to be dropped and not kept to be sold

* revert change

* reserve hammerdin changes for a separate PR

* get rid of extra attack sequence

* disable target/attack logs for merge

* code cleanup

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