Files
my-botty/src/pather.py
T

388 lines
23 KiB
Python

from char.i_char import IChar
from screen import Screen
from template_finder import TemplateFinder
from logger import Logger
from char.i_char import IChar
import math
import keyboard
import time
import os
import random
from typing import Tuple, Union, List
import cv2
from config import Config
from utils.misc import is_in_roi
import numpy as np
class Location:
# A5 Town
A5_TOWN_START = "a5_town_start"
A5_STASH = "a5_stash"
A5_WP = "a5_wp"
A5_QUAL_KEHK = "a5_qual_kehk"
A5_MALAH = "a5_malah"
A5_NIHLATHAK_PORTAL = "a5_nihlathak_portal"
A5_LARZUK = "a5_larzuk"
# Pindle
A5_PINDLE_START = "a5_pindle_start"
A5_PINDLE_SAVE_DIST = "a5_pindle_save_dist"
A5_PINDLE_END = "a5_pindle_end"
# Eldritch
A5_ELDRITCH_START = "a5_eldritch_start"
A5_ELDRITCH_SAVE_DIST = "a5_eldritch_save_dist"
A5_ELDRITCH_END = "a5_eldritch_end"
# Shenk
A5_SHENK_START = "a5_shenk_start"
A5_SHENK_SAVE_DIST = "a5_shenk_save_dist"
A5_SHENK_END = "a5_shenk_end"
# A4 Town
A4_TOWN_START = "a4_town_start"
A4_WP = "a4_town_wp"
A4_TYRAEL_STASH = "a4_tyrael_stash"
A4_VENDOR = "a4_vendor"
# A3 Town
A3_TOWN_START = "a3_town_start"
A3_ORMUS = "a3_ormus"
A3_STASH_WP = "a3_stash_wp"
A3_ASHEARA = "a3_asheara"
# Trav
A3_TRAV_START = "a3_trav_start"
A3_TRAV_CENTER_STAIRS = "a3_trav_center_stairs"
class Pather:
"""
Traverses 'dynamic' pathes with reference templates and relative coordinates or statically recorded pathes.
Check utils/node_recorder.py to generate templates/refpoints and relative coordinates to nodes. Once you have refpoints and
nodes you can specify in which order this nodes should be traversed in self._paths.
"""
def __init__(self, screen: Screen, template_finder: TemplateFinder):
self._config = Config()
self._screen = screen
self._template_finder = template_finder
self._range_x = [-self._config.ui_pos["center_x"] + 7, self._config.ui_pos["center_x"] - 7]
self._range_y = [-self._config.ui_pos["center_y"] + 7, self._config.ui_pos["center_y"] - self._config.ui_pos["skill_bar_height"] - 33]
self._nodes = {
# A5 town
0: {'A5_TOWN_0': (27, 249), 'A5_TOWN_1': (-92, -137)},
1: {'A5_TOWN_0': (-311, 191), 'A5_TOWN_1': (-429, -194), 'A5_TOWN_0.5': (478, 233)},
2: {'A5_TOWN_0': (-368, -39), 'A5_TOWN_0.5': (423, 7)},
3: {'A5_TOWN_1': (-276, 94), 'A5_TOWN_2': (485, -60)},
4: {'A5_TOWN_1': (-467, 267), 'A5_TOWN_2': (293, 113), 'A5_TOWN_3': (-267, -139), 'A5_TOWN_4': (162, -163)},
5: {'A5_TOWN_2': (303+60, 219+40), 'A5_TOWN_3': (-257+60, -33+40), 'A5_TOWN_4': (172+60, -57+40)},
6: {'A5_TOWN_3': (-583+80, 175+60), 'A5_TOWN_4': (-155+80, 151+60), 'A5_TOWN_5': (-13+80, -240+60), 'A5_TOWN_6': (307+80, 61+60)},
8: {'A5_TOWN_6': (127, 293), 'A5_TOWN_5': (-195, -5), 'A5_TOWN_7': (598, -87)},
9: {'A5_TOWN_5': (-407, 167), 'A5_TOWN_7': (386, 85)},
10: {'A5_TOWN_4': (-472, 58), 'A5_TOWN_6': (-11, -32), 'A5_TOWN_8': (321, 131)},
11: {'A5_TOWN_6': (-299, -215), 'A5_TOWN_8': (33, -52), 'A5_TOWN_9': (7, 231)},
12: {'A5_TOWN_8': (-139, -196), 'A5_TOWN_9': (-165, 87)},
13: {'A5_TOWN_3': (120, 120), 'A5_TOWN_10': (-533, 221), 'A5_TOWN_4': (548, 97)},
14: {'A5_TOWN_3': (447, 173), 'A5_TOWN_10': (-200, 280)},
# Pindle
100: {'PINDLE_7': (384, -92), 'PINDLE_0': (-97, -40), 'PINDLE_1': (-13, 223), 'PINDLE_2': (-366, 85)},
101: {'PINDLE_1': (371, -45), 'PINDLE_2': (18, -184), 'PINDLE_3': (-123, 261)},
102: {'PINDLE_3': (223, 88), 'PINDLE_4': (95, 215)},
103: {'PINDLE_3': (395, -75), 'PINDLE_4': (267, 52)},
104: {'PINDLE_4': (717, -117), 'PINDLE_3': (843, -244), 'PINDLE_5': (-187, 237), 'PINDLE_6': (-467, 89)},
# Eldritch
120: {'ELDRITCH_0': (293, 24), 'ELDRITCH_1': (-307, 76), 'ELDRITCH_5': (27, -164), 'ELDRITCH_6': (400, -50)},
121: {'ELDRITCH_6': (360, -244), 'ELDRITCH_1': (-329, -103), 'ELDRITCH_2': (411, 171), 'ELDRITCH_3': (-91, 198)},
122: {'ELDRITCH_2': (353, -145), 'ELDRITCH_3': (-149, -119)},
123: {'ELDRITCH_3': (-99, -252), 'ELDRITCH_2': (403, -279), 'ELDRITCH_4': (-62, -109)},
# Shenk
140: {'SHENK_0': (-149, -227), 'SHENK_1': (445, -161), 'SHENK_17': (-500, 235), 'SHENK_15': (80, 13)},
141: {'SHENK_0': (-129, 44), 'SHENK_1': (464, 107), 'SHENK_2': (-167, -34), 'SHENK_17': (-520, 528), 'SHENK_15': (77, 293)},
142: {'SHENK_1': (584, 376), 'SHENK_4': (-443, -103), 'SHENK_2': (-52, 235), 'SHENK_3': (357, -129)},
143: {'SHENK_4': (-251, 165), 'SHENK_2': (141, 505), 'SHENK_3': (549, 139), 'SHENK_6': (-339, -69)},
144: {'SHENK_6': (-108, 123), 'SHENK_7': (481, 151)},
145: {'SHENK_7': (803, 372), 'SHENK_12': (97, -133), 'SHENK_6': (209, 347), 'SHENK_8': (-245, 18)},
146: {'SHENK_12': (272, 111), 'SHENK_9': (-331, -144), 'SHENK_8': (-72, 258)},
147: {'SHENK_16': (317, -18), 'SHENK_9': (-67, 139), 'SHENK_10': (-431, 67)},
148: {'SHENK_16': (682, 103), 'SHENK_9': (301, 263), 'SHENK_10': (-65, 188), 'SHENK_11': (-306, 139)},
149: {'SHENK_11': (261, 395), 'SHENK_10': (495, 421), 'SHENK_13': (393, -9)},
# A4 town
160: {"A4_TOWN_4": (-100, -133), "A4_TOWN_3": (-117, 238), "A4_TOWN_0": (-364, 151), "A4_TOWN_6": (24, -425), "A4_TOWN_5": (-347, -277)},
161: {"A4_TOWN_3": (-289, 156), "A4_TOWN_4": (-272, -215), "A4_TOWN_2": (385, -92), "A4_TOWN_6": (-148, -507), "A4_TOWN_0": (-536, 69)},
162: {"A4_TOWN_4": (74, 66), "A4_TOWN_5": (-173, -78), "A4_TOWN_6": (198, -226), "A4_TOWN_0": (-190, 350), "A4_TOWN_7": (-101, -455)},
163: {"A4_TOWN_5": (-92, 143), "A4_TOWN_7": (-19, -233), "A4_TOWN_6": (280, -4), "A4_TOWN_4": (156, 288), "A4_TOWN_8": (-598, 5)},
164: {"A4_TOWN_7": (235, 39), "A4_TOWN_8": (-344, 277), "A4_TOWN_5": (163, 415), "A4_TOWN_6": (534, 268), "A4_TOWN_4": (409, 559)},
# A3 town
180: {"A3_TOWN_0": (-144, 170), "A3_TOWN_1": (-417, 59), "A3_TOWN_2": (-716, -161)},
181: {"A3_TOWN_1": (-113, 155), "A3_TOWN_0": (160, 266), "A3_TOWN_2": (-412, -65), "A3_TOWN_3": (-867, 133)},
182: {"A3_TOWN_2": (-101, -135), "A3_TOWN_1": (198, 85), "A3_TOWN_0": (471, 196), "A3_TOWN_3": (-556, 63), "A3_TOWN_12": (-500, 717)},
183: {"A3_TOWN_3": (-287, -80), "A3_TOWN_2": (168, -279), "A3_TOWN_1": (467, -58), "A3_TOWN_4": (-587, 86), "A3_TOWN_12": (-231, 574)},
184: {"A3_TOWN_4": (-109, -146), "A3_TOWN_5": (-81, 206), "A3_TOWN_3": (191, -311), "A3_TOWN_12": (247, 342), "A3_TOWN_13": (-323, 530)},
185: {"A3_TOWN_5": (223, 40), "A3_TOWN_13": (-19, 364), "A3_TOWN_4": (195, -312), "A3_TOWN_12": (551, 176), "A3_TOWN_20": (-549, 184)},
186: {"A3_TOWN_7": (-195, -118), "A3_TOWN_20": (-130, 237), "A3_TOWN_8": (-269, 308), "A3_TOWN_13": (400, 417), "A3_TOWN_5": (642, 93)},
187: {"A3_TOWN_8": (161, 207), "A3_TOWN_9": (-108, 265), "A3_TOWN_7": (236, -219), "A3_TOWN_20": (300, 136), "A3_TOWN_11": (-618, 150)},
188: {"A3_TOWN_9": (173, 157), "A3_TOWN_11": (-337, 42), "A3_TOWN_8": (442, 99), "A3_TOWN_20": (581, 28), "A3_TOWN_7": (517, -327)},
189: {"A3_TOWN_5": (10, -182), "A3_TOWN_13": (-232, 142), "A3_TOWN_14": (-7, 337), "A3_TOWN_12": (338, -46), "A3_TOWN_15": (722, 143)},
190: {"A3_TOWN_12": (27, -198), "A3_TOWN_16": (831, 258), "A3_TOWN_14": (-318, 185), "A3_TOWN_15": (411, -9), "A3_TOWN_5": (-301, -334), "A3_TOWN_13": (-543, -10), "A3_TOWN_17": (707, 541)},
191: {"A3_TOWN_15": (-79, -256), "A3_TOWN_16": (341, 11), "A3_TOWN_17": (217, 294), "A3_TOWN_18": (542, 408), "A3_TOWN_19": (779, 171)},
192: {"A3_TOWN_17": (-187, 78), "A3_TOWN_16": (-63, -205), "A3_TOWN_18": (138, 192), "A3_TOWN_19": (375, -45)},
# Trav
220: {"TRAV_0": (445, 384), "TRAV_20": (-259, 267), "TRAV_1": (-248, -139), "TRAV_2": (-682, 21), "TRAV_21": (25, 180)},
221: {"TRAV_2": (-153, -101), "TRAV_3": (-125, 201), "TRAV_20": (270, 145), "TRAV_1": (281, -261), "TRAV_4": (-459, 122)},
222: {"TRAV_5": (-218, 106), "TRAV_4": (120, 230), "TRAV_2": (426, 7), "TRAV_7": (-719, -166), "TRAV_1": (862, -153)},
223: {"TRAV_5": (344, 123), 'TRAV_4': (682, 247), "TRAV_8": (-353, -31), "TRAV_7": (-157, -149), "TRAV_22": (-368, -222), "TRAV_23": (-579, 116)},
224: {'TRAV_7': (411, -129), 'TRAV_27': (-363, 163), "TRAV_8": (214, -11), "TRAV_23": (-11, 136), "TRAV_10": (-130, -187), "TRAV_24": (-274, 15), "TRAV_22": (200, -202)},
225: {'TRAV_27': (96, 359), 'TRAV_8': (670, 187), 'TRAV_7': (867, 69), "TRAV_11": (10, 214), "TRAV_19": (-298, 539), "TRAV_24": (181, 213), "TRAV_12": (-408, -73), "TRAV_25": (-538, 132)},
226: {"TRAV_12": (-95, -182), "TRAV_25": (-225, 23), "TRAV_13": (-272, 185), "TRAV_11": (323, 105), 'TRAV_18': (-534, 363), 'TRAV_19': (15, 430), "TRAV_17": (-251, 232), 'TRAV_27': (408, 253)},
227: {"TRAV_11": (65, -42), "TRAV_24": (236, -43), "TRAV_19": (-243, 283), 'TRAV_18': (-792, 216), "TRAV_12": (-356, -330), "TRAV_25": (-483, -124), 'TRAV_27': (154, 104)},
228: {"TRAV_13": (8, 9), "TRAV_17": (29, 56), "TRAV_25": (58, -152), "TRAV_16": (-198, -110), "TRAV_18": (-251, 188)},
229: {"TRAV_18": (-220-70, 58), "TRAV_25": (89-70, -282), "TRAV_17": (60-70, -74), "TRAV_13": (39-70, -121), "TRAV_16": (-168-70, -241)},
230: {"TRAV_19": (157, 39), "TRAV_18": (-392, -28), "TRAV_17": (-112, -160), "TRAV_13": (-133, -207), "TRAV_25": (-83, -368)}
}
self._paths = {
# A5 Town
(Location.A5_TOWN_START, Location.A5_NIHLATHAK_PORTAL): [3, 4, 5, 6, 8, 9],
(Location.A5_TOWN_START, Location.A5_STASH): [3, 4, 5],
(Location.A5_TOWN_START, Location.A5_WP): [3, 4],
(Location.A5_TOWN_START, Location.A5_QUAL_KEHK): [3, 4, 5, 6, 10, 11, 12],
(Location.A5_TOWN_START, Location.A5_MALAH): [1, 2],
(Location.A5_TOWN_START, Location.A5_LARZUK): [3, 4, 5, 13, 14],
(Location.A5_MALAH, Location.A5_TOWN_START): [1, 0],
(Location.A5_STASH, Location.A5_NIHLATHAK_PORTAL): [6, 8, 9],
(Location.A5_STASH, Location.A5_QUAL_KEHK): [5, 6, 10, 11, 12],
(Location.A5_STASH, Location.A5_LARZUK): [13, 14],
(Location.A5_STASH, Location.A5_WP): [],
(Location.A5_WP, Location.A5_STASH): [],
(Location.A5_WP, Location.A5_LARZUK): [13, 14],
(Location.A5_WP, Location.A5_NIHLATHAK_PORTAL): [6, 8, 9],
(Location.A5_QUAL_KEHK, Location.A5_NIHLATHAK_PORTAL): [12, 11, 10, 6, 8, 9],
(Location.A5_QUAL_KEHK, Location.A5_WP): [12, 11, 10, 6],
(Location.A5_LARZUK, Location.A5_QUAL_KEHK): [13, 14, 5, 6, 10, 11, 12],
(Location.A5_LARZUK, Location.A5_NIHLATHAK_PORTAL): [14, 13, 5, 6, 8, 9],
(Location.A5_LARZUK, Location.A5_WP): [14, 13, 5],
# Pindle
(Location.A5_PINDLE_START, Location.A5_PINDLE_SAVE_DIST): [100, 101, 102, 103],
(Location.A5_PINDLE_SAVE_DIST, Location.A5_PINDLE_END): [104],
# Eldritch
(Location.A5_ELDRITCH_START, Location.A5_ELDRITCH_SAVE_DIST): [120, 121, 122],
(Location.A5_ELDRITCH_SAVE_DIST, Location.A5_ELDRITCH_END): [123],
# Shenk
(Location.A5_SHENK_START, Location.A5_SHENK_SAVE_DIST): [140, 141, 142, 143, 144, 145, 146, 147, 148],
(Location.A5_SHENK_SAVE_DIST, Location.A5_SHENK_END): [149],
# A4 Town
(Location.A4_WP, Location.A4_TYRAEL_STASH): [160, 161],
(Location.A4_TYRAEL_STASH, Location.A4_WP): [161, 160],
(Location.A4_TOWN_START, Location.A4_WP): [162],
(Location.A4_TOWN_START, Location.A4_TYRAEL_STASH): [160, 161],
# A3 Town
(Location.A3_TOWN_START, Location.A3_STASH_WP): [180, 181, 182, 183, 184, 185, 186, 187, 188],
(Location.A3_TOWN_START, Location.A3_ORMUS): [180, 181, 182, 183, 184, 185],
(Location.A3_ORMUS, Location.A3_STASH_WP): [186, 187, 188],
(Location.A3_ORMUS, Location.A3_ASHEARA): [189, 190, 191, 192],
(Location.A3_ASHEARA, Location.A3_STASH_WP): [191, 190, 189, 185, 186, 187, 188],
(Location.A3_STASH_WP, Location.A3_STASH_WP): [188],
(Location.A3_STASH_WP, Location.A3_ORMUS): [187, 186, 185],
# Trav
(Location.A3_TRAV_START, Location.A3_TRAV_CENTER_STAIRS): [220, 221, 222, 223, 224, 225, 226]
}
def _get_node(self, key: int, template: str):
return (
self._nodes[key][template][0],
self._nodes[key][template][1]
)
@staticmethod
def _convert_rel_to_abs(rel_loc: Tuple[float, float], pos_abs: Tuple[float, float]) -> Tuple[float, float]:
return (rel_loc[0] + pos_abs[0], rel_loc[1] + pos_abs[1])
def traverse_nodes_fixed(self, path: Union[str, List[Tuple[float, float]]], char: IChar):
if not char.can_teleport():
error_msg = "Teleport is requiered for static pathing"
Logger.error(error_msg)
raise ValueError(error_msg)
char.pre_move()
if type(path) == str:
path = self._config.path[path]
for pos in path:
x_m, y_m = self._screen.convert_screen_to_monitor(pos)
x_m += int(random.random() * 6 - 3)
y_m += int(random.random() * 6 - 3)
char.move((x_m, y_m))
def _adjust_abs_range_to_screen(self, abs_pos: Tuple[float, float]) -> Tuple[float, float]:
"""
Adjust an absolute coordinate so it will not go out of screen or click on any ui which will not move the char
:param abs_pos: Absolute position of the desired position to move to
:return: Absolute position of a valid position that can be clicked on
"""
f = 1.0
# Check for x-range
if abs_pos[0] > self._range_x[1]:
f = min(f, abs(self._range_x[1] / float(abs_pos[0])))
elif abs_pos[0] < self._range_x[0]:
f = min(f, abs(self._range_x[0] / float(abs_pos[0])))
# Check y-range
if abs_pos[1] > self._range_y[1]:
f = min(f, abs(self._range_y[1] / float(abs_pos[1])))
if abs_pos[1] < self._range_y[0]:
f = min(f, abs(self._range_y[0] / float(abs_pos[1])))
# Scale the position by the factor f
if f < 1.0:
abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f))
# Check if adjusted position is "inside globe"
screen_pos = self._screen.convert_abs_to_screen(abs_pos)
if is_in_roi(self._config.ui_roi["mana_globe"], screen_pos) or is_in_roi(self._config.ui_roi["health_globe"], screen_pos):
# convert any of health or mana roi top coordinate to abs (x-coordinate is just a dummy 0 value)
new_range_y_bottom = self._screen.convert_screen_to_abs((0, self._config.ui_roi["mana_globe"][1]))[1]
f = abs(new_range_y_bottom / float(abs_pos[1]))
abs_pos = (int(abs_pos[0] * f), int(abs_pos[1] * f))
return abs_pos
def find_abs_node_pos(self, node_idx: int, img: np.ndarray) -> Tuple[float, float]:
node = self._nodes[node_idx]
template_match = self._template_finder.search([*node], img, best_match=False, roi=self._config.ui_roi["cut_skill_bar"], use_grayscale=True)
if template_match.valid:
# Get reference position of template in abs coordinates
ref_pos_abs = self._screen.convert_screen_to_abs(template_match.position)
# Calc the abs node position with the relative coordinates (relative to ref)
node_pos_rel = self._get_node(node_idx, template_match.name)
node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs)
return node_pos_abs
return None
def traverse_nodes(
self,
path: Union[tuple[Location, Location], list[int]],
char: IChar,
time_out: float = 5,
force_tp: bool = False,
do_pre_move: bool = True,
force_move: bool = False
) -> bool:
"""Traverse from one location to another
:param path: Either a list of node indices or a tuple with (start_location, end_location)
:param char: Char that is traversing the nodes
:param time_out: Timeout in second. If no more move was found in that time it will cancel traverse
:param force_move: Bool value if force move should be used for pathing
:return: Bool if traversed succesfull or False if it got stuck
"""
if len(path) == 0:
Logger.error("Path must be a list of integers or a tuple with start and end location!")
return False
if type(path[0]) != int:
start_location = path[0]
end_location = path[1]
Logger.debug(f"Traverse from {start_location} to {end_location}")
try:
path = self._paths[(start_location, end_location)]
except KeyError:
if start_location == end_location:
return True
Logger.error(f"Don't know how to traverse from {start_location} to {end_location}")
return False
else:
Logger.debug(f"Traverse: {path}")
if do_pre_move:
char.pre_move()
last_direction = None
for i, node_idx in enumerate(path):
continue_to_next_node = False
last_move = time.time()
did_force_move = False
while not continue_to_next_node:
img = self._screen.grab()
# Handle timeout
if (time.time() - last_move) > time_out:
success = self._template_finder.search("WAYPOINT_MENU", img).valid
if success:
# sometimes bot opens waypoint menu, close it to find templates again
Logger.debug("Opened wp, closing it again")
keyboard.send("esc")
last_move = time.time()
else:
# This is a bit hacky, but for moving into a boss location we set time_out usually quite low
# because of all the spells and monsters it often can not determine the final template
# Don't want to spam the log with errors in this case because it most likely worked out just fine
if time_out > 3.1:
if self._config.general["info_screenshots"]:
cv2.imwrite("./info_screenshots/info_pather_got_stuck_" + time.strftime("%Y%m%d_%H%M%S") + ".png", img)
Logger.error("Got stuck exit pather")
return False
# Sometimes we get stuck at rocks and stuff, after 2.5 seconds force a move into the last know direction
if not did_force_move and time.time() - last_move > 3.1:
pos_abs = (0, 150)
if last_direction is not None:
pos_abs = last_direction
Logger.debug(f"Pather: taking a random guess for next position")
x_m, y_m = self._screen.convert_abs_to_monitor(pos_abs)
char.move((x_m, y_m), force_move=True)
did_force_move = True
last_move = time.time()
# Find any template and calc node position from it
node_pos_abs = self.find_abs_node_pos(node_idx, img)
if node_pos_abs is not None:
dist = math.dist(node_pos_abs, (0, 0))
if dist < self._config.ui_pos["reached_node_dist"]:
continue_to_next_node = True
else:
# Move the char
x_m, y_m = self._screen.convert_abs_to_monitor(node_pos_abs)
char.move((x_m, y_m), force_tp=force_tp, force_move=force_move)
last_direction = node_pos_abs
last_move = time.time()
return True
# Testing: Move char to whatever Location to start and run
if __name__ == "__main__":
# debug method to display all nodes
def display_all_nodes(pather: Pather, filter: str = None):
while 1:
img = pather._screen.grab()
display_img = img.copy()
template_map = {}
template_scores = {}
for template_type in pather._template_finder._templates:
if filter is None or filter in template_type:
template_match = pather._template_finder.search(template_type, img, use_grayscale=True)
if template_match.valid:
template_map[template_type] = template_match.position
template_scores[template_type] = template_match.score
print(template_scores)
for node_idx in pather._nodes:
for template_type in pather._nodes[node_idx]:
if template_type in template_map:
ref_pos_screen = template_map[template_type]
# Get reference position of template in abs coordinates
ref_pos_abs = pather._screen.convert_screen_to_abs(ref_pos_screen)
# Calc the abs node position with the relative coordinates (relative to ref)
node_pos_rel = pather._get_node(node_idx, template_type)
node_pos_abs = pather._convert_rel_to_abs(node_pos_rel, ref_pos_abs)
node_pos_abs = pather._adjust_abs_range_to_screen(node_pos_abs)
x, y = pather._screen.convert_abs_to_screen(node_pos_abs)
cv2.circle(display_img, (x, y), 5, (255, 0, 0), 3)
cv2.putText(display_img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
x, y = pather._screen.convert_abs_to_screen(ref_pos_abs)
cv2.circle(display_img, (x, y), 5, (0, 255, 0), 3)
cv2.putText(display_img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA)
# display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5)
cv2.imshow("debug", display_img)
cv2.waitKey(1)
import keyboard
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
keyboard.wait("f11")
from config import Config
from char.sorceress import Sorceress
from char.hammerdin import Hammerdin
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 = Hammerdin(config.hammerdin, config.char, screen, t_finder, ui_manager, pather)
# pather.traverse_nodes_fixed("trav_save_dist", char)
# pather.traverse_nodes((Location.A3_TRAV_START, Location.A3_TRAV_SAVE_DIST), char)
# pather.traverse_nodes((Location.A3_TRAV_SAVE_DIST, Location.A3_TRAV_END), char)
display_all_nodes(pather, filter="TRAV")