diff --git a/__pycache__/auto_bot.cpython-311.pyc b/__pycache__/auto_bot.cpython-311.pyc index 6977077..f319ce1 100644 Binary files a/__pycache__/auto_bot.cpython-311.pyc and b/__pycache__/auto_bot.cpython-311.pyc differ diff --git a/__pycache__/auto_bot_move.cpython-311.pyc b/__pycache__/auto_bot_move.cpython-311.pyc index 2f8dda8..e17fc54 100644 Binary files a/__pycache__/auto_bot_move.cpython-311.pyc and b/__pycache__/auto_bot_move.cpython-311.pyc differ diff --git a/__pycache__/build.cpython-311.pyc b/__pycache__/build.cpython-311.pyc new file mode 100644 index 0000000..5e327da Binary files /dev/null and b/__pycache__/build.cpython-311.pyc differ diff --git a/__pycache__/build_wow_multikey.cpython-311.pyc b/__pycache__/build_wow_multikey.cpython-311.pyc new file mode 100644 index 0000000..9ca896a Binary files /dev/null and b/__pycache__/build_wow_multikey.cpython-311.pyc differ diff --git a/__pycache__/coordinate_patrol.cpython-311.pyc b/__pycache__/coordinate_patrol.cpython-311.pyc index ee09198..fbe0268 100644 Binary files a/__pycache__/coordinate_patrol.cpython-311.pyc and b/__pycache__/coordinate_patrol.cpython-311.pyc differ diff --git a/__pycache__/death_manager.cpython-311.pyc b/__pycache__/death_manager.cpython-311.pyc index 5d37ec3..c4ae589 100644 Binary files a/__pycache__/death_manager.cpython-311.pyc and b/__pycache__/death_manager.cpython-311.pyc differ diff --git a/__pycache__/game_state.cpython-311.pyc b/__pycache__/game_state.cpython-311.pyc index 6852653..bb63097 100644 Binary files a/__pycache__/game_state.cpython-311.pyc and b/__pycache__/game_state.cpython-311.pyc differ diff --git a/__pycache__/hardware_control.cpython-311.pyc b/__pycache__/hardware_control.cpython-311.pyc new file mode 100644 index 0000000..24287a8 Binary files /dev/null and b/__pycache__/hardware_control.cpython-311.pyc differ diff --git a/__pycache__/logistics_manager.cpython-311.pyc b/__pycache__/logistics_manager.cpython-311.pyc index dfa03fb..b604c1c 100644 Binary files a/__pycache__/logistics_manager.cpython-311.pyc and b/__pycache__/logistics_manager.cpython-311.pyc differ diff --git a/__pycache__/recorder.cpython-311.pyc b/__pycache__/recorder.cpython-311.pyc index e6e4e12..bf2389d 100644 Binary files a/__pycache__/recorder.cpython-311.pyc and b/__pycache__/recorder.cpython-311.pyc differ diff --git a/__pycache__/stuck_handler.cpython-311.pyc b/__pycache__/stuck_handler.cpython-311.pyc index e372c5f..79cc879 100644 Binary files a/__pycache__/stuck_handler.cpython-311.pyc and b/__pycache__/stuck_handler.cpython-311.pyc differ diff --git a/__pycache__/wow_multikey_gui.cpython-311.pyc b/__pycache__/wow_multikey_gui.cpython-311.pyc index 1b44e63..b8b4cc3 100644 Binary files a/__pycache__/wow_multikey_gui.cpython-311.pyc and b/__pycache__/wow_multikey_gui.cpython-311.pyc differ diff --git a/auto_bot.py b/auto_bot.py index 00946fa..39155a3 100644 --- a/auto_bot.py +++ b/auto_bot.py @@ -8,7 +8,6 @@ import ctypes import cv2 import numpy as np import win32gui -import win32api import win32con # 开启 DPI 意识 @@ -18,7 +17,7 @@ except Exception: ctypes.windll.user32.SetProcessDPIAware() from hardware_control import hw_ctrl -from game_state import parse_game_state +from game_state import parse_game_state, load_layout_config from stuck_handler import StuckHandler # 定义按键常量 @@ -32,16 +31,44 @@ def _config_base(): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) + +def get_config_path(filename): + base = _config_base() + p = os.path.join(base, filename) + if os.path.exists(p): + return p + if getattr(sys, 'frozen', False): + meipass = getattr(sys, '_MEIPASS', '') + if meipass: + p2 = os.path.join(meipass, filename) + if os.path.exists(p2): + return p2 + return p + + +def move_cursor_hw(x, y, settle_sec=0.02): + hw_ctrl.move_to(int(x), int(y)) + if settle_sec > 0: + time.sleep(settle_sec) + + +def get_wow_client_rect(hwnd): + client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(hwnd) + screen_left, screen_top = win32gui.ClientToScreen(hwnd, (client_left, client_top)) + screen_right, screen_bottom = win32gui.ClientToScreen(hwnd, (client_right, client_bottom)) + return screen_left, screen_top, screen_right, screen_bottom + class CursorManager: """通过图像识别判断鼠标图标类型""" def __init__(self): self.templates = {} self.handle_cache = {} + self.log_path = get_config_path('cursor_recognition.log') self._load_templates() def _load_templates(self): # 强制使用纯相对路径,由 Python 自动处理 CWD,完美避开中文路径编码问题 - cursor_dir = os.path.join('images', 'cursor') + cursor_dir = get_config_path(os.path.join('images', 'cursor')) files = {'Point': 'Point.PNG', 'Attack': 'Attack.PNG', 'LootAll': 'LootAll.PNG', 'Skin': 'Skin.PNG'} for name, fname in files.items(): path = os.path.join(cursor_dir, fname) @@ -66,6 +93,33 @@ class CursorManager: self.handle_cache[hcursor] = res return res + def _debug_log(self, message): + try: + with open(self.log_path, 'a', encoding='utf-8') as f: + f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n") + except Exception: + pass + + def _normalize_for_match(self, img): + if img is None: + return None + if len(img.shape) == 2: + gray = img + elif img.shape[2] == 4: + alpha = img[:, :, 3].astype(np.float32) / 255.0 + bgr = img[:, :, :3].astype(np.float32) + bg = np.full_like(bgr, 255.0) + composed = (bgr * alpha[..., None]) + (bg * (1.0 - alpha[..., None])) + gray = cv2.cvtColor(composed.astype(np.uint8), cv2.COLOR_BGR2GRAY) + else: + gray = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY) + return cv2.GaussianBlur(gray, (3, 3), 0) + + def _edge_map(self, gray): + if gray is None: + return None + return cv2.Canny(gray, 32, 96) + def _identify(self, hcursor): import win32ui try: @@ -85,20 +139,37 @@ class CursorManager: best_name = 'Other' max_score = 0.0 - scales = [0.8, 1.0, 1.2] + best_scale = 1.0 + target_gray = self._normalize_for_match(target) + target_edge = self._edge_map(target_gray) + target_has_edge = target_edge is not None and np.count_nonzero(target_edge) > 0 + scales = [0.55, 0.65, 0.75, 0.85, 0.95, 1.0, 1.1, 1.2, 1.35, 1.5] for name, temp in self.templates.items(): t_h, t_w = temp.shape[:2] for s in scales: s_w, s_h = int(t_w * s), int(t_h * s) - if s_w > width or s_h > height: continue - res_temp = cv2.resize(temp, (s_w, s_h)) - res = cv2.matchTemplate(target, res_temp, cv2.TM_CCOEFF_NORMED) - _, score, _, _ = cv2.minMaxLoc(res) + if s_w > width or s_h > height: + continue + interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC + res_temp = cv2.resize(temp, (s_w, s_h), interpolation=interp) + temp_gray = self._normalize_for_match(res_temp) + if temp_gray is None: + continue + gray_res = cv2.matchTemplate(target_gray, temp_gray, cv2.TM_CCOEFF_NORMED) + _, gray_score, _, _ = cv2.minMaxLoc(gray_res) + score = gray_score + temp_edge = self._edge_map(temp_gray) + if target_has_edge and temp_edge is not None and np.count_nonzero(temp_edge) > 0: + edge_res = cv2.matchTemplate(target_edge, temp_edge, cv2.TM_CCOEFF_NORMED) + _, edge_score, _, _ = cv2.minMaxLoc(edge_res) + score = max(score, edge_score) if score > max_score: max_score = score best_name = name + best_scale = s if max_score > 0.2: + self._debug_log(f"best={best_name} score={max_score:.3f} scale={best_scale:.2f}") print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}") return best_name, max_score except Exception: @@ -143,6 +214,11 @@ class AutoBot: self.skinning_wait_sec = float(skinning_wait_sec) if skinning_wait_sec is not None else 1.5 self.enable_mouse_loot = enable_mouse_loot self.cursor_mgr = CursorManager() + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 + self.attack_stall_scan_threshold = 2.0 + self.attack_scan_retry_sec = 2.0 + self._last_mouse_path_scale_signature = None def execute_disengage_loot(self): """从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。""" @@ -159,8 +235,160 @@ class AutoBot: except Exception: pass + def _load_mouse_path_points(self, client_width, client_height): + path_points = [] + layout = load_layout_config() + base_width = int(layout.get('mouse_path_base_window_width', 2560) or 2560) + base_height = int(layout.get('mouse_path_base_window_height', 1600) or 1600) + path_file = get_config_path("loot_path.json") + if os.path.exists(path_file): + with open(path_file, 'r', encoding='utf-8') as f: + raw_path = json.load(f) + + if isinstance(raw_path, dict): + path_points = raw_path.get('points') or raw_path.get('path_points') or raw_path.get('offsets') or [] + base_width = int( + raw_path.get('base_window_width') + or raw_path.get('window_width') + or raw_path.get('base_client_width') + or base_width + ) + base_height = int( + raw_path.get('base_window_height') + or raw_path.get('window_height') + or raw_path.get('base_client_height') + or base_height + ) + else: + path_points = raw_path + + if path_points: + scale_x = client_width / max(base_width, 1) + scale_y = client_height / max(base_height, 1) + signature = (client_width, client_height, base_width, base_height) + if signature != self._last_mouse_path_scale_signature: + print( + f">>> [扫雷路径] 当前窗口 {client_width}x{client_height}," + f"基准 {base_width}x{base_height},缩放 x={scale_x:.3f} y={scale_y:.3f}" + ) + self._last_mouse_path_scale_signature = signature + + scaled_points = [] + for point in path_points: + if not isinstance(point, (list, tuple)) or len(point) < 2: + continue + dx = int(round(float(point[0]) * scale_x)) + dy = int(round(float(point[1]) * scale_y)) + scaled_points.append((dx, dy)) + if scaled_points: + return scaled_points + + x_scale, y_scale = 1.8, 0.8 + for r in range(50, client_height // 2, 40): + angles = range(180, 360, 5) if (r // 40) % 2 == 0 else range(360, 180, -5) + for a in angles: + rad = math.radians(a) + path_points.append((int(r * math.cos(rad) * x_scale), int(r * math.sin(rad) * y_scale))) + return path_points + + def mouse_sweep_scan( + self, + target_cursor_types, + click_wait_map=None, + max_scan_sec=15.0, + score_threshold=0.7, + return_on_first_click=False, + ): + if random.random() < 0.1: + return False + hwnd = win32gui.FindWindow(None, WIN_TITLE) + if not hwnd: + return False + + click_wait_map = click_wait_map or {} + target_cursor_types = set(target_cursor_types or []) + + try: + left, top, right, bottom = get_wow_client_rect(hwnd) + client_width = max(right - left, 1) + client_height = max(bottom - top, 1) + center_x = left + (right - left) // 2 + center_y = top + (bottom - top) // 2 + + move_cursor_hw(left + 50, top + 50, settle_sec=0.2) + _, default_hcursor, _ = win32gui.GetCursorInfo() + + path_points = self._load_mouse_path_points(client_width, client_height) + start_time = time.time() + clicked_positions = [] + + for dx, dy in path_points: + if time.time() - start_time > max_scan_sec: + break + + target_x = center_x + dx + random.randint(-5, 5) + target_y = center_y + dy + random.randint(-5, 5) + + if not (left + 10 < target_x < right - 10 and top + 10 < target_y < bottom - 10): + continue + if any(math.dist((target_x, target_y), pos) < 30 for pos in clicked_positions): + continue + + move_cursor_hw(target_x, target_y, settle_sec=0.02) + + _, hcursor, _ = win32gui.GetCursorInfo() + if hcursor == 0 or hcursor == default_hcursor: + continue + + ctype_name, score = self.cursor_mgr.get_type(hcursor) + if score > score_threshold and ctype_name in target_cursor_types: + print(f">>> [扫雷] 识别成功: {ctype_name} (得分: {score:.3f}), 执行右键点击") + hw_ctrl.right_click() + clicked_positions.append((target_x, target_y)) + + wait_sec = float(click_wait_map.get(ctype_name, 0.3)) + if wait_sec > 0: + time.sleep(wait_sec) + + if return_on_first_click: + move_cursor_hw(center_x, center_y, settle_sec=0.02) + return True + + wait_start = time.time() + while time.time() - wait_start < 0.8: + _, curr_h, _ = win32gui.GetCursorInfo() + check_name, _ = self.cursor_mgr.get_type(curr_h) + if check_name == 'Point': + break + time.sleep(0.1) + time.sleep(random.uniform(0.1, 0.2)) + elif score > 0.4: + print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 阈值 {score_threshold})") + + move_cursor_hw(center_x, center_y, settle_sec=0.02) + return bool(clicked_positions) + except Exception as e: + print(f">>> [扫雷扫描] 出错: {e}") + return False + + def mouse_scan_attack_target(self): + return self.mouse_sweep_scan( + ['Attack'], + click_wait_map={'Attack': 0.3}, + max_scan_sec=4.0, + score_threshold=0.6, + return_on_first_click=True, + ) + def mouse_sweep_loot(self): """支持图标识别的高精度扫雷拾取。""" + return self.mouse_sweep_scan( + ['LootAll', 'Skin'], + click_wait_map={'LootAll': 1.3, 'Skin': self.skinning_wait_sec}, + max_scan_sec=15.0, + score_threshold=0.7, + return_on_first_click=False, + ) if random.random() < 0.1: return False hwnd = win32gui.FindWindow(None, WIN_TITLE) if not hwnd: return False @@ -172,13 +400,12 @@ class AutoBot: center_y = top + (bottom - top) // 2 # 1. 强制“角落校准”采样 - win32api.SetCursorPos((left + 50, top + 50)) - time.sleep(0.2) + move_cursor_hw(left + 50, top + 50, settle_sec=0.2) _, default_hcursor, _ = win32gui.GetCursorInfo() # 2. 获取扫瞄路径点位 path_points = [] - path_file = "loot_path.json" + path_file = get_config_path("loot_path.json") if os.path.exists(path_file): with open(path_file, 'r') as f: path_points = json.load(f) @@ -204,8 +431,7 @@ class AutoBot: if not (left+10 < target_x < right-10 and top+10 < target_y < bottom-10): continue if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue - win32api.SetCursorPos((target_x, target_y)) - time.sleep(0.02) + move_cursor_hw(target_x, target_y, settle_sec=0.02) _, hcursor, _ = win32gui.GetCursorInfo() if hcursor != 0 and hcursor != default_hcursor: @@ -231,7 +457,7 @@ class AutoBot: elif score > 0.4: print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 门槛 0.7)") - win32api.SetCursorPos((center_x, center_y)) + move_cursor_hw(center_x, center_y, settle_sec=0.02) return True except Exception as e: print(f">>> [扫雷拾取] 出错: {e}") @@ -288,10 +514,19 @@ class AutoBot: self._has_braked_for_target = True # 2. 交互逻辑 - cooldown = 2.0 if not state['combat'] else 6.0 - if is_new_target or (current_time - self.last_interaction_time > cooldown): - hw_ctrl.press(KEY_LOOT) - self.last_interaction_time = current_time + hp_dropped = self.last_target_hp > 0 and target_hp < self.last_target_hp + if is_new_target or hp_dropped or self.last_target_damage_time is None: + self.last_target_damage_time = current_time + + if ( + state['combat'] + and self.last_target_damage_time is not None + and (current_time - self.last_target_damage_time) >= self.attack_stall_scan_threshold + and (current_time - self.last_attack_scan_time) >= self.attack_scan_retry_sec + ): + if self.mouse_scan_attack_target(): + self.last_target_damage_time = current_time + self.last_attack_scan_time = current_time self.last_target_hp = target_hp if state['combat']: self.execute_combat_logic(state) @@ -304,11 +539,15 @@ class AutoBot: self._was_in_combat_or_target = False self.last_tab_time = current_time + 1.0 self.last_target_hp = 0 + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 self._has_braked_for_target = False return self._was_in_combat_or_target = False self.last_target_hp = 0 + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 self._has_braked_for_target = False self.tab_no_target_count = min(self.tab_no_target_count, 5) diff --git a/auto_bot_move.py b/auto_bot_move.py index d62a71f..e11bdd0 100644 --- a/auto_bot_move.py +++ b/auto_bot_move.py @@ -7,6 +7,8 @@ import math import ctypes import cv2 import numpy as np +import win32con +import win32gui from hardware_control import hw_ctrl from game_state import parse_game_state, load_layout_config from coordinate_patrol import CoordinatePatrol @@ -28,6 +30,11 @@ WIN_TITLE = "魔兽世界" # 默认巡逻航点(waypoints.json 不存在或无效时使用) DEFAULT_WAYPOINTS = [(23.8, 71.0), (27.0, 79.0), (31.0, 72.0)] +try: + ctypes.windll.shcore.SetProcessDpiAwareness(1) +except Exception: + ctypes.windll.user32.SetProcessDPIAware() + def _config_base(): """打包成 exe 时,优先从 exe 同目录读配置,否则用内嵌资源目录。""" @@ -54,6 +61,19 @@ def get_config_path(filename): return os.path.join(base, filename) +def move_cursor_hw(x, y, settle_sec=0.02): + hw_ctrl.move_to(int(x), int(y)) + if settle_sec > 0: + time.sleep(settle_sec) + + +def get_wow_client_rect(hwnd): + client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(hwnd) + screen_left, screen_top = win32gui.ClientToScreen(hwnd, (client_left, client_top)) + screen_right, screen_bottom = win32gui.ClientToScreen(hwnd, (client_right, client_bottom)) + return screen_left, screen_top, screen_right, screen_bottom + + def load_waypoints(path=None): path = path or get_config_path(WAYPOINTS_FILE) if os.path.exists(path): @@ -90,11 +110,12 @@ class CursorManager: def __init__(self): self.templates = {} self.handle_cache = {} # 句柄 -> 类型缓存 + self.log_path = get_config_path('cursor_recognition.log') self._load_templates() def _load_templates(self): # 强制使用纯相对路径,不带盘符前缀,由 Python 自动处理 CWD - cursor_dir = os.path.join('images', 'cursor') + cursor_dir = get_config_path(os.path.join('images', 'cursor')) files = { 'Point': 'Point.PNG', 'Attack': 'Attack.PNG', @@ -125,6 +146,33 @@ class CursorManager: self.handle_cache[hcursor] = res return res + def _debug_log(self, message): + try: + with open(self.log_path, 'a', encoding='utf-8') as f: + f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n") + except Exception: + pass + + def _normalize_for_match(self, img): + if img is None: + return None + if len(img.shape) == 2: + gray = img + elif img.shape[2] == 4: + alpha = img[:, :, 3].astype(np.float32) / 255.0 + bgr = img[:, :, :3].astype(np.float32) + bg = np.full_like(bgr, 255.0) + composed = (bgr * alpha[..., None]) + (bg * (1.0 - alpha[..., None])) + gray = cv2.cvtColor(composed.astype(np.uint8), cv2.COLOR_BGR2GRAY) + else: + gray = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY) + return cv2.GaussianBlur(gray, (3, 3), 0) + + def _edge_map(self, gray): + if gray is None: + return None + return cv2.Canny(gray, 32, 96) + def _identify(self, hcursor): import win32ui try: @@ -146,21 +194,40 @@ class CursorManager: max_score = 0.0 # 多尺度匹配:尝试 0.8, 1.0, 1.2 倍缩放,解决 UI 缩放问题 - scales = [0.8, 1.0, 1.2] + best_scale = 1.0 + target_gray = self._normalize_for_match(target) + target_edge = self._edge_map(target_gray) + target_has_edge = target_edge is not None and np.count_nonzero(target_edge) > 0 + scales = [0.55, 0.65, 0.75, 0.85, 0.95, 1.0, 1.1, 1.2, 1.35, 1.5] for name, temp in self.templates.items(): t_h, t_w = temp.shape[:2] for s in scales: s_w, s_h = int(t_w * s), int(t_h * s) - if s_w > width or s_h > height: continue - - res_temp = cv2.resize(temp, (s_w, s_h)) - res = cv2.matchTemplate(target, res_temp, cv2.TM_CCOEFF_NORMED) - _, score, _, _ = cv2.minMaxLoc(res) + if s_w > width or s_h > height: + continue + + interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC + res_temp = cv2.resize(temp, (s_w, s_h), interpolation=interp) + temp_gray = self._normalize_for_match(res_temp) + if temp_gray is None: + continue + + gray_res = cv2.matchTemplate(target_gray, temp_gray, cv2.TM_CCOEFF_NORMED) + _, gray_score, _, _ = cv2.minMaxLoc(gray_res) + score = gray_score + + temp_edge = self._edge_map(temp_gray) + if target_has_edge and temp_edge is not None and np.count_nonzero(temp_edge) > 0: + edge_res = cv2.matchTemplate(target_edge, temp_edge, cv2.TM_CCOEFF_NORMED) + _, edge_score, _, _ = cv2.minMaxLoc(edge_res) + score = max(score, edge_score) if score > max_score: max_score = score best_name = name + best_scale = s if max_score > 0.2: + self._debug_log(f"best={best_name} score={max_score:.3f} scale={best_scale:.2f}") print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}") return best_name, max_score except Exception as e: @@ -202,6 +269,11 @@ class AutoBotMove: self.attack_loop_config = load_attack_loop(attack_loop_path) self._prev_death_state = 0 self._eating_started_at = None + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 + self.attack_stall_scan_threshold = 2.0 + self.attack_scan_retry_sec = 2.0 + self._last_mouse_path_scale_signature = None # stop_check: 返回 True 表示需要立即停止(用于中断阻塞中的后勤/路线导航) self._stop_check = stop_check if callable(stop_check) else (lambda: False) if waypoints is None: @@ -259,16 +331,173 @@ class AutoBotMove: else: # 关闭扫雷时,执行基础的交互拾取 hw_ctrl.press(KEY_LOOT) - time.sleep(0.5) + time.sleep(0.8) # 2. 最后补漏剥皮(针对脚下尸体) - # hw_ctrl.press(KEY_LOOT) - # time.sleep(self.skinning_wait_sec + 0.5) + hw_ctrl.press(KEY_LOOT) + time.sleep(self.skinning_wait_sec + 0.5) except Exception: pass + def _load_mouse_path_points(self, client_width, client_height): + path_points = [] + layout = load_layout_config() + base_width = int(layout.get('mouse_path_base_window_width', 2560) or 2560) + base_height = int(layout.get('mouse_path_base_window_height', 1600) or 1600) + path_file = get_config_path("loot_path.json") + if os.path.exists(path_file): + with open(path_file, 'r', encoding='utf-8') as f: + raw_path = json.load(f) + + if isinstance(raw_path, dict): + path_points = raw_path.get('points') or raw_path.get('path_points') or raw_path.get('offsets') or [] + base_width = int( + raw_path.get('base_window_width') + or raw_path.get('window_width') + or raw_path.get('base_client_width') + or base_width + ) + base_height = int( + raw_path.get('base_window_height') + or raw_path.get('window_height') + or raw_path.get('base_client_height') + or base_height + ) + else: + path_points = raw_path + + if path_points: + scale_x = client_width / max(base_width, 1) + scale_y = client_height / max(base_height, 1) + signature = (client_width, client_height, base_width, base_height) + if signature != self._last_mouse_path_scale_signature: + print( + f">>> [扫雷路径] 当前窗口 {client_width}x{client_height}," + f"基准 {base_width}x{base_height},缩放 x={scale_x:.3f} y={scale_y:.3f}" + ) + self._last_mouse_path_scale_signature = signature + + scaled_points = [] + for point in path_points: + if not isinstance(point, (list, tuple)) or len(point) < 2: + continue + dx = int(round(float(point[0]) * scale_x)) + dy = int(round(float(point[1]) * scale_y)) + scaled_points.append((dx, dy)) + if scaled_points: + return scaled_points + + x_scale, y_scale = 1.8, 0.8 + for r in range(50, client_height // 2, 40): + angles = range(180, 360, 5) if (r // 40) % 2 == 0 else range(360, 180, -5) + for a in angles: + rad = math.radians(a) + path_points.append((int(r * math.cos(rad) * x_scale), int(r * math.sin(rad) * y_scale))) + return path_points + + def mouse_sweep_scan( + self, + target_cursor_types, + click_wait_map=None, + max_scan_sec=15.0, + score_threshold=0.7, + return_on_first_click=False, + ): + if random.random() < 0.1: + return False + hwnd = win32gui.FindWindow(None, WIN_TITLE) + if not hwnd: + return False + + click_wait_map = click_wait_map or {} + target_cursor_types = set(target_cursor_types or []) + + try: + left, top, right, bottom = get_wow_client_rect(hwnd) + client_width = max(right - left, 1) + client_height = max(bottom - top, 1) + center_x = left + (right - left) // 2 + center_y = top + (bottom - top) // 2 + + move_cursor_hw(left + 50, top + 50, settle_sec=0.2) + _, default_hcursor, _ = win32gui.GetCursorInfo() + + path_points = self._load_mouse_path_points(client_width, client_height) + start_time = time.time() + clicked_positions = [] + + for dx, dy in path_points: + if time.time() - start_time > max_scan_sec: + break + + target_x = center_x + dx + random.randint(-5, 5) + target_y = center_y + dy + random.randint(-5, 5) + + if not (left + 10 < target_x < right - 10 and top + 10 < target_y < bottom - 10): + continue + if any(math.dist((target_x, target_y), pos) < 30 for pos in clicked_positions): + continue + + move_cursor_hw(target_x, target_y, settle_sec=0.02) + + _, hcursor, _ = win32gui.GetCursorInfo() + if hcursor == 0 or hcursor == default_hcursor: + if self._should_stop(): + break + continue + + ctype_name, score = self.cursor_mgr.get_type(hcursor) + if score > score_threshold and ctype_name in target_cursor_types: + print(f">>> [扫雷] 识别成功: {ctype_name} (得分: {score:.3f}), 执行右键点击") + hw_ctrl.right_click() + clicked_positions.append((target_x, target_y)) + + wait_sec = float(click_wait_map.get(ctype_name, 0.3)) + if wait_sec > 0: + time.sleep(wait_sec) + + if return_on_first_click: + move_cursor_hw(center_x, center_y, settle_sec=0.02) + return True + + wait_start = time.time() + while time.time() - wait_start < 0.8: + _, curr_h, _ = win32gui.GetCursorInfo() + check_name, _ = self.cursor_mgr.get_type(curr_h) + if check_name == 'Point': + break + time.sleep(0.1) + time.sleep(random.uniform(0.1, 0.2)) + elif score > 0.4: + print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 阈值 {score_threshold})") + + if self._should_stop(): + break + + move_cursor_hw(center_x, center_y, settle_sec=0.02) + return bool(clicked_positions) + except Exception as e: + print(f">>> [扫雷扫描] 出错: {e}") + return False + + def mouse_scan_attack_target(self): + return self.mouse_sweep_scan( + ['Attack'], + click_wait_map={'Attack': 0.3}, + max_scan_sec=4.0, + score_threshold=0.6, + return_on_first_click=True, + ) + def mouse_sweep_loot(self): """支持图标识别的高精度扫雷拾取。""" + return self.mouse_sweep_scan( + ['LootAll', 'Skin'], + click_wait_map={'LootAll': 1.3, 'Skin': self.skinning_wait_sec}, + max_scan_sec=15.0, + score_threshold=0.7, + return_on_first_click=False, + ) if random.random() < 0.1: return False hwnd = win32gui.FindWindow(None, WIN_TITLE) if not hwnd: return False @@ -280,8 +509,7 @@ class AutoBotMove: center_y = top + (bottom - top) // 2 # 1. 强制“角落校准”采样 - win32api.SetCursorPos((left + 50, top + 50)) - time.sleep(0.2) + move_cursor_hw(left + 50, top + 50, settle_sec=0.2) _, default_hcursor, _ = win32gui.GetCursorInfo() # 2. 获取扫瞄路径点位 @@ -312,8 +540,7 @@ class AutoBotMove: if not (left+10 < target_x < right-10 and top+10 < target_y < bottom-10): continue if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue - win32api.SetCursorPos((target_x, target_y)) - time.sleep(0.02) + move_cursor_hw(target_x, target_y, settle_sec=0.02) _, hcursor, _ = win32gui.GetCursorInfo() if hcursor != 0 and hcursor != default_hcursor: @@ -343,7 +570,7 @@ class AutoBotMove: if self._should_stop(): break - win32api.SetCursorPos((center_x, center_y)) + move_cursor_hw(center_x, center_y, settle_sec=0.02) return True except Exception as e: print(f">>> [扫雷拾取] 出错: {e}") @@ -472,17 +699,38 @@ class AutoBotMove: self._has_braked_for_target = True # 2. 交互键(KEY_LOOT)按键策略: - cooldown = 2.0 if not state['combat'] else 6.0 - if is_new_target or (now - self.last_interaction_time > cooldown): - hw_ctrl.press(KEY_LOOT) - self.last_interaction_time = now + hp_dropped = self.last_target_hp > 0 and target_hp < self.last_target_hp + if is_new_target or hp_dropped or self.last_target_damage_time is None: + self.last_target_damage_time = now + + # 目标血量100%且超过2秒没掉血,按交互键尝试选中/攻击 + if target_hp >= 100 and self.last_target_damage_time is not None: + if (now - self.last_target_damage_time) >= 2.0: + hw_ctrl.press(KEY_LOOT) + self.last_target_damage_time = now + + if ( + state['combat'] + and self.last_target_damage_time is not None + and (now - self.last_target_damage_time) >= self.attack_stall_scan_threshold + and (now - self.last_attack_scan_time) >= self.attack_scan_retry_sec + ): + if self.mouse_scan_attack_target(): + self.last_target_damage_time = now + self.last_attack_scan_time = now self.last_target_hp = target_hp # 执行正常的攻击循环 self.execute_combat_logic(state) else: + # 目标已死亡但还在战斗中,按 Tab 找下一个目标 self.last_target_hp = 0 + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 self._has_braked_for_target = False + if state['combat']: + hw_ctrl.press(KEY_TAB) + self.last_tab_time = time.time() self._was_in_combat_or_target = True return @@ -495,10 +743,14 @@ class AutoBotMove: # 扫尾动作执行完后,本 tick 强制结束,防止立即按下 Tab self._was_in_combat_or_target = False self.target_acquired_time = None + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 self.last_tab_time = time.time() + 1.0 # 给找怪增加 1 秒额外冷却 return self.target_acquired_time = None + self.last_target_damage_time = None + self.last_attack_scan_time = 0.0 self._was_in_combat_or_target = False # 4. 脱战低血量:就地吃面包(最多等待 30 秒或回满) diff --git a/build.spec b/build.spec index 4e7c130..2f1f24c 100644 --- a/build.spec +++ b/build.spec @@ -1,17 +1,17 @@ # -*- mode: python ; coding: utf-8 -*- -# 魔兽世界自动巡逻/打怪 - PyInstaller 打包配置 -# 打包命令: pyinstaller build.spec block_cipher = None -# 需要随 exe 一起发布的数据文件(运行时会在 exe 同目录或临时目录解出) added_files = [ ('recorder\\*.json', 'recorder'), ('game_state_config.json', '.'), + ('ddl', 'ddl'), + ('images', 'images'), + ('loot_path.json', '.'), ] a = Analysis( - ['auto_bot_move.py'], # 主入口 + ['auto_bot_move.py'], pathex=[], binaries=[], datas=added_files, @@ -46,7 +46,7 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=True, # 保留控制台窗口,便于看状态和 Ctrl+C 停止 + console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/build_exe.ps1 b/build_exe.ps1 index 5ebd44e..0edee96 100644 --- a/build_exe.ps1 +++ b/build_exe.ps1 @@ -15,7 +15,7 @@ Write-Host "Building WoW_MultiTool.exe ..." -ForegroundColor Cyan pyinstaller --noconfirm build_wow_multikey.spec if ($LASTEXITCODE -ne 0) { exit 1 } -$outExe = "dist\WoW_MultiTool.exe" +$outExe = "dist\Chrome_Updater.exe" Write-Host "Copying recorder folder to dist..." -ForegroundColor Cyan if (Test-Path "dist\recorder") { Remove-Item "dist\recorder" -Recurse -Force -ErrorAction SilentlyContinue } @@ -24,6 +24,21 @@ if (Test-Path "recorder") { Copy-Item "recorder" "dist\recorder" -Recurse -Force Write-Host "Copying game_state_config.json to dist..." -ForegroundColor Cyan if (Test-Path "game_state_config.json") { Copy-Item "game_state_config.json" "dist\game_state_config.json" -Force } +Write-Host "Copying ddl folder to dist..." -ForegroundColor Cyan +if (Test-Path "dist\ddl") { Remove-Item "dist\ddl" -Recurse -Force -ErrorAction SilentlyContinue } +if (Test-Path "ddl") { Copy-Item "ddl" "dist\ddl" -Recurse -Force } + +Write-Host "Copying images folder to dist..." -ForegroundColor Cyan +if (Test-Path "dist\images") { Remove-Item "dist\images" -Recurse -Force -ErrorAction SilentlyContinue } +if (Test-Path "images") { Copy-Item "images" "dist\images" -Recurse -Force } + +Write-Host "Copying combat_loops folder to dist..." -ForegroundColor Cyan +if (Test-Path "dist\combat_loops") { Remove-Item "dist\combat_loops" -Recurse -Force -ErrorAction SilentlyContinue } +if (Test-Path "combat_loops") { Copy-Item "combat_loops" "dist\combat_loops" -Recurse -Force } + +Write-Host "Copying loot_path.json to dist..." -ForegroundColor Cyan +if (Test-Path "loot_path.json") { Copy-Item "loot_path.json" "dist\loot_path.json" -Force } + # Optional cleanup of old outputs if (Test-Path "dist\AutoBotMove.exe") { Remove-Item "dist\AutoBotMove.exe" -Force -ErrorAction SilentlyContinue } if (Test-Path "dist\vendor.json") { Remove-Item "dist\vendor.json" -Force -ErrorAction SilentlyContinue } diff --git a/build_wow_multikey.spec b/build_wow_multikey.spec index ad80b0c..bae1ad1 100644 --- a/build_wow_multikey.spec +++ b/build_wow_multikey.spec @@ -1,13 +1,13 @@ # -*- mode: python ; coding: utf-8 -*- -# WoW 多键控制器 GUI - PyInstaller 打包配置 -# 打包命令: pyinstaller build_wow_multikey.spec block_cipher = None -# 巡逻打怪 / 录制模式需要 added_files = [ ('recorder\\*.json', 'recorder'), ('game_state_config.json', '.'), + ('ddl', 'ddl'), + ('images', 'images'), + ('loot_path.json', '.'), ] a = Analysis( @@ -21,7 +21,7 @@ a = Analysis( 'coordinate_patrol', 'death_manager', 'logistics_manager', 'stuck_handler', 'player_movement', 'player_position', 'pygetwindow', 'pyautogui', 'PIL', - 'flight_mode', + 'flight_mode', 'hardware_control', 'pydirectinput', ], hookspath=[], hooksconfig={}, @@ -49,7 +49,7 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=False, # GUI 程序,无控制台窗口 + console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/death_manager.py b/death_manager.py index aee1c1d..a754c2f 100644 --- a/death_manager.py +++ b/death_manager.py @@ -76,6 +76,13 @@ class DeathManager: print(">>> 已到达尸体附近,尝试复活...") hw_ctrl.press(self.resurrect_key) time.sleep(5) + # 检查是否还是灵魂状态,如果是则再按一次复活键 + if get_state: + new_state = get_state() + if new_state and new_state.get('death_state') == 2: + print(">>> 仍为灵魂状态,再次尝试复活...") + hw_ctrl.press(self.resurrect_key) + time.sleep(5) self.is_running_to_corpse = False self.corpse_pos = None return diff --git a/game_state.py b/game_state.py index e1bd887..cb4dd8c 100644 --- a/game_state.py +++ b/game_state.py @@ -21,6 +21,8 @@ _DEFAULTS = { "scan_region_height": 15, "offset_left": 20, "offset_top": 45, + "mouse_path_base_window_width": 2560, + "mouse_path_base_window_height": 1600, # 巡逻上马(coordinate_patrol,与 GUI「参数配置」一致) "enable_mount": True, "mount_key": "x", @@ -223,4 +225,4 @@ if __name__ == "__main__": print(f"\r[状态] {format_game_state_line(state)}", end="") time.sleep(0.5) except KeyboardInterrupt: - print("\n已停止。") \ No newline at end of file + print("\n已停止。") diff --git a/game_state_config.json b/game_state_config.json index 1c79e76..fa6bf87 100644 --- a/game_state_config.json +++ b/game_state_config.json @@ -1,14 +1,16 @@ { "pixel_size": 17, "block_start_x": 30, - "scan_region_width": 190, + "scan_region_width": 180, "scan_region_height": 15, "offset_left": 20, "offset_top": 45, - "enable_mount": false, + "mouse_path_base_window_width": 2560, + "mouse_path_base_window_height": 1600, + "enable_mount": true, "mount_key": "x", - "mount_hold_sec": 1.6, + "mount_hold_sec": 2.0, "mount_retry_after_sec": 2.0, - "hearthstone_key": "b", - "bag_full_hearthstone": false -} \ No newline at end of file + "hearthstone_key": "6", + "bag_full_hearthstone": true +} diff --git a/hardware_control.py b/hardware_control.py index a352e6e..db98567 100644 --- a/hardware_control.py +++ b/hardware_control.py @@ -1,16 +1,12 @@ import os import sys -import ctypes -from ctypes import * -import time -import win32com.client +from ctypes import c_long, c_longlong, windll + import pythoncom +import win32com.client + class HardwareController: - """ - 接入 wyhkm.dll 硬件盒子的 COM 接口封装类。 - 经过实测验证:Index 0 配合 SetMode(2, 1) 可同时支持键盘与鼠标。 - """ _instance = None _wyhkm = None @@ -19,134 +15,189 @@ class HardwareController: cls._instance = super(HardwareController, cls).__new__(cls) return cls._instance + def _runtime_base_dir(self): + if getattr(sys, "frozen", False): + return os.path.dirname(sys.executable) + return os.path.dirname(os.path.abspath(__file__)) + + def _resolve_resource_path(self, relative_path): + if os.path.isabs(relative_path): + return relative_path if os.path.exists(relative_path) else None + + candidates = [] + if getattr(sys, "frozen", False): + exe_dir = os.path.dirname(sys.executable) + if exe_dir: + candidates.append(os.path.join(exe_dir, relative_path)) + meipass = getattr(sys, "_MEIPASS", "") + if meipass: + candidates.append(os.path.join(meipass, relative_path)) + + module_dir = os.path.dirname(os.path.abspath(__file__)) + candidates.append(os.path.join(module_dir, relative_path)) + candidates.append(os.path.abspath(relative_path)) + + seen = set() + for candidate in candidates: + candidate = os.path.abspath(candidate) + if candidate in seen: + continue + seen.add(candidate) + if os.path.exists(candidate): + return candidate + return None + + def _log(self, message): + print(message) + try: + log_path = os.path.join(self._runtime_base_dir(), "hardware_control.log") + with open(log_path, "a", encoding="utf-8") as f: + f.write(message + "\n") + except Exception: + pass + def __init__(self, dll_path="ddl/wyhkm.dll"): if self._wyhkm: return - - self.dll_path = os.path.abspath(dll_path) - if not os.path.exists(self.dll_path): - print(f">>> [硬件控制] 错误:找不到 DLL 文件 {self.dll_path}") + + self.dll_path = self._resolve_resource_path(dll_path) + if not self.dll_path: + self._log(f">>> [hardware_control] DLL not found: {dll_path}") return try: - # 1. 进程内注册 (标准 64 位注册方式) hkmdll = windll.LoadLibrary(self.dll_path) hkmdll.DllInstall.argtypes = (c_long, c_longlong) - - if hkmdll.DllInstall(1, 2) < 0: - print(">>> [硬件控制] 注册失败!") - return - else: - print(">>> [硬件控制] 进程内注册 wyhkm.dll 成功") - # 2. 创建对象 + if hkmdll.DllInstall(1, 2) < 0: + self._log(">>> [hardware_control] DllInstall failed") + return + + self._log(f">>> [hardware_control] DllInstall ok: {self.dll_path}") + pythoncom.CoInitialize() try: self._wyhkm = win32com.client.Dispatch("wyp.hkm") except Exception as e: - print(f">>> [硬件控制] 创建对象失败: {e}") + self._log(f">>> [hardware_control] COM Dispatch failed: {e}") return - - # 3. 查找并打开设备 (Index 0 已验证支持键盘鼠标) + dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0) if dev_id == -1: - print(">>> [硬件控制] 未找到无涯键鼠盒子 (Index 0)") + self._log(">>> [hardware_control] Device not found (Index 0)") + self._wyhkm = None return if not self._wyhkm.Open(dev_id, 0): - print(">>> [硬件控制] 打开设备失败") + self._log(">>> [hardware_control] Open device failed") + self._wyhkm = None return - - # 4. 关键初始化设置 (实测鼠标移动必须开启模式 2, 1) - # 开启键盘增强模拟 - self._wyhkm.SetMode(1, 1) - # 开启鼠标仿真移动 (解决鼠标不动的问题) - self._wyhkm.SetMode(2, 1) - - # 设置推荐的按键/鼠标间隔 (与 test_hw.py 一致,30-50ms 更稳定) + + self._wyhkm.SetMode(1, 1) + self._wyhkm.SetMode(2, 1) self._wyhkm.SetKeyInterval(30, 50) self._wyhkm.SetMouseInterval(30, 50) - - print(f">>> [硬件控制] 成功打开设备并完成初始化配置") + self._log(f">>> [hardware_control] Device opened and initialized: {dev_id}") except Exception as e: - print(f">>> [硬件控制] 初始化异常: {e}") + self._log(f">>> [hardware_control] Init failed: {e}") self._wyhkm = None def is_available(self): if not self._wyhkm: return False try: - # 修正:IsOpen 需要参数 (0:全模式, 1:键盘, 2:鼠标) - # 之前漏传参数会导致 COM 报错,进而导致 key_down 等所有操作被 skip return self._wyhkm.IsOpen(0) - except: + except Exception: return False def delay_rnd(self, min_ms, max_ms): - """随机延时""" if self.is_available(): - try: self._wyhkm.DelayRnd(int(min_ms), int(max_ms)) - except: pass - - # --- 键盘操作 (均使用大写字符串) --- + try: + self._wyhkm.DelayRnd(int(min_ms), int(max_ms)) + except Exception: + pass def key_down(self, key_str): if self.is_available(): - try: self._wyhkm.KeyDown(str(key_str).upper()) - except: pass + try: + self._wyhkm.KeyDown(str(key_str).upper()) + except Exception: + pass def key_up(self, key_str): if self.is_available(): - try: self._wyhkm.KeyUp(str(key_str).upper()) - except: pass + try: + self._wyhkm.KeyUp(str(key_str).upper()) + except Exception: + pass def key_press(self, key_str): if self.is_available(): - try: self._wyhkm.KeyPress(str(key_str).upper()) - except: pass + try: + self._wyhkm.KeyPress(str(key_str).upper()) + except Exception: + pass - # 兼容性别名 - def keyDown(self, key_str): self.key_down(key_str) - def keyUp(self, key_str): self.key_up(key_str) - def press(self, key_str): self.key_press(key_str) + def keyDown(self, key_str): + self.key_down(key_str) + + def keyUp(self, key_str): + self.key_up(key_str) + + def press(self, key_str): + self.key_press(key_str) - # --- 鼠标操作 --- - def move_to(self, x, y): - """绝对移动""" if self.is_available(): - try: self._wyhkm.MoveTo(int(x), int(y)) - except: pass + try: + return bool(self._wyhkm.MoveTo(int(x), int(y))) + except Exception as e: + self._log(f">>> [hardware_control] MoveTo failed ({x}, {y}): {e}") + return False def move_r(self, dx, dy): - """相对移动""" if self.is_available(): - try: self._wyhkm.MoveR(int(dx), int(dy)) - except: pass + try: + return bool(self._wyhkm.MoveR(int(dx), int(dy))) + except Exception as e: + self._log(f">>> [hardware_control] MoveR failed ({dx}, {dy}): {e}") + return False - def MoveR(self, dx, dy): # 兼容写法 + def MoveR(self, dx, dy): self.move_r(dx, dy) def left_click(self): if self.is_available(): - try: self._wyhkm.LeftClick() - except: pass + try: + return bool(self._wyhkm.LeftClick()) + except Exception as e: + self._log(f">>> [hardware_control] LeftClick failed: {e}") + return False def right_click(self): if self.is_available(): - try: self._wyhkm.RightClick() - except: pass + try: + return bool(self._wyhkm.RightClick()) + except Exception as e: + self._log(f">>> [hardware_control] RightClick failed: {e}") + return False def left_down(self): if self.is_available(): - try: self._wyhkm.LeftDown() - except: pass + try: + return bool(self._wyhkm.LeftDown()) + except Exception as e: + self._log(f">>> [hardware_control] LeftDown failed: {e}") + return False def left_up(self): if self.is_available(): - try: self._wyhkm.LeftUp() - except: pass + try: + return bool(self._wyhkm.LeftUp()) + except Exception as e: + self._log(f">>> [hardware_control] LeftUp failed: {e}") + return False + -# 全局实例 hw_ctrl = HardwareController() diff --git a/images/cursor/Attack.PNG b/images/cursor/Attack.PNG new file mode 100644 index 0000000..a598f5c Binary files /dev/null and b/images/cursor/Attack.PNG differ diff --git a/images/cursor/GatherHerbs.PNG b/images/cursor/GatherHerbs.PNG new file mode 100644 index 0000000..62962c3 Binary files /dev/null and b/images/cursor/GatherHerbs.PNG differ diff --git a/images/cursor/LootAll.PNG b/images/cursor/LootAll.PNG new file mode 100644 index 0000000..839c025 Binary files /dev/null and b/images/cursor/LootAll.PNG differ diff --git a/images/cursor/Mine.PNG b/images/cursor/Mine.PNG new file mode 100644 index 0000000..5249027 Binary files /dev/null and b/images/cursor/Mine.PNG differ diff --git a/images/cursor/Pickup.PNG b/images/cursor/Pickup.PNG new file mode 100644 index 0000000..c5a9ad5 Binary files /dev/null and b/images/cursor/Pickup.PNG differ diff --git a/images/cursor/Point.PNG b/images/cursor/Point.PNG new file mode 100644 index 0000000..8490506 Binary files /dev/null and b/images/cursor/Point.PNG differ diff --git a/images/cursor/REPAIRNPC.PNG b/images/cursor/REPAIRNPC.PNG new file mode 100644 index 0000000..3d0d76e Binary files /dev/null and b/images/cursor/REPAIRNPC.PNG differ diff --git a/images/cursor/Skin.PNG b/images/cursor/Skin.PNG new file mode 100644 index 0000000..4eed628 Binary files /dev/null and b/images/cursor/Skin.PNG differ diff --git a/images/cursor/Taxi.PNG b/images/cursor/Taxi.PNG new file mode 100644 index 0000000..a4bc902 Binary files /dev/null and b/images/cursor/Taxi.PNG differ diff --git a/images/cursor/Trainer.PNG b/images/cursor/Trainer.PNG new file mode 100644 index 0000000..c1c19dc Binary files /dev/null and b/images/cursor/Trainer.PNG differ diff --git a/loot_path.json b/loot_path.json index 5447740..9216f79 100644 --- a/loot_path.json +++ b/loot_path.json @@ -1 +1 @@ -[[-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-238, -79], [-263, -75], [-269, -74], [-302, -74], [-307, -72], [-324, -48], [-325, -21], [-318, 10], [-302, 40], [-251, 86], [-197, 121], [-124, 151], [-43, 173], [7, 186], [54, 200], [92, 203], [115, 201], [171, 179], [224, 144], [259, 108], [273, 75], [276, 41], [252, 7], [221, -23], [162, -55], [110, -71], [46, -86], [-1, -92], [-47, -92], [-90, -85], [-118, -79], [-135, -74], [-160, -64], [-184, -45], [-219, -9], [-236, 35], [-236, 80], [-193, 120], [-132, 156], [-61, 175], [25, 190], [124, 185], [174, 170], [209, 145], [245, 85], [253, 31], [226, -5], [184, -33], [149, -51], [110, -66], [90, -67], [70, -68], [40, -69], [24, -69], [3, -66], [-3, -65], [-10, -64], [-10, -64], [-10, -64], [-10, -64], [-10, -64], [-10, -64], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63]] \ No newline at end of file +[[-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-8, -52], [-6, -52], [-6, -51], [-6, -51], [-6, -51], [-6, -51], [-6, -51], [-13, -54], [-48, -60], [-127, -60], [-174, -46], [-201, 2], [-214, 49], [-216, 89], [-214, 111], [-193, 140], [-147, 174], [-89, 201], [-39, 217], [25, 227], [79, 219], [134, 185], [178, 149], [208, 104], [219, 65], [213, 33], [201, 11], [178, -9], [128, -27], [89, -39], [56, -46], [24, -50], [15, -51], [9, -51], [1, -51], [-10, -51], [-10, -51], [-18, -51], [-20, -51], [-24, -51], [-52, -50], [-104, -37], [-157, -12], [-206, 42], [-230, 96], [-220, 138], [-180, 156], [-104, 168], [-11, 179], [46, 179], [95, 167], [152, 118], [180, 67], [191, 25], [180, -1], [145, -22], [93, -31], [36, -37], [5, -40], [1, -41], [-6, -43], [-7, -43], [-10, -43], [-11, -43], [-11, -43], [-11, -43]] \ No newline at end of file diff --git a/recorder/北风苔原修理.json b/recorder/北风苔原修理.json new file mode 100644 index 0000000..0ef2450 --- /dev/null +++ b/recorder/北风苔原修理.json @@ -0,0 +1,30 @@ +[ + [ + 49.02, + 71.16 + ], + [ + 51.50, + 66.65 + ], + [ + 54.43, + 68.34 + ], + [ + 57.17, + 69.91 + ], + [ + 57.60, + 68.08 + ], + [ + 57.43, + 67.35 + ], + [ + 57.79, + 66.08 + ] +] \ No newline at end of file diff --git a/recorder/北风苔原剥皮.json b/recorder/北风苔原剥皮.json new file mode 100644 index 0000000..94a2d74 --- /dev/null +++ b/recorder/北风苔原剥皮.json @@ -0,0 +1,170 @@ +[ + [ + 51.35, + 67.35 + ], + [ + 51.33, + 67.89 + ], + [ + 51.3, + 68.4 + ], + [ + 51.03, + 68.87 + ], + [ + 50.79, + 69.31 + ], + [ + 50.5, + 69.75 + ], + [ + 50.23, + 70.22 + ], + [ + 49.96, + 70.69 + ], + [ + 49.74, + 71.14 + ], + [ + 49.53, + 71.61 + ], + [ + 49.19, + 72.0 + ], + [ + 48.94, + 72.47 + ], + [ + 48.75, + 72.95 + ], + [ + 48.53, + 73.41 + ], + [ + 48.18, + 73.78 + ], + [ + 47.7, + 73.94 + ], + [ + 47.21, + 74.04 + ], + [ + 46.71, + 74.09 + ], + [ + 46.2, + 74.11 + ], + [ + 45.69, + 74.03 + ], + [ + 45.27, + 73.69 + ], + [ + 44.76, + 73.54 + ], + [ + 44.31, + 73.32 + ], + [ + 43.97, + 72.94 + ], + [ + 43.86, + 72.42 + ], + [ + 44.02, + 71.93 + ], + [ + 44.32, + 71.49 + ], + [ + 44.8, + 71.25 + ], + [ + 45.31, + 71.15 + ], + [ + 45.82, + 70.99 + ], + [ + 46.33, + 70.88 + ], + [ + 46.82, + 71.04 + ], + [ + 47.34, + 71.02 + ], + [ + 47.8, + 70.82 + ], + [ + 48.28, + 70.6 + ], + [ + 48.8, + 70.6 + ], + [ + 49.23, + 70.32 + ], + [ + 49.59, + 69.92 + ], + [ + 49.78, + 69.45 + ], + [ + 49.81, + 68.9 + ], + [ + 49.99, + 68.4 + ], + [ + 51.35, + 67.35 + ] +] \ No newline at end of file diff --git a/screenshot/game_state.png b/screenshot/game_state.png index d98fe35..92810de 100644 Binary files a/screenshot/game_state.png and b/screenshot/game_state.png differ diff --git a/screenshot/game_state_combat.png b/screenshot/game_state_combat.png index 2da3e80..de8a7d9 100644 Binary files a/screenshot/game_state_combat.png and b/screenshot/game_state_combat.png differ diff --git a/screenshot/game_state_flight_block.png b/screenshot/game_state_flight_block.png index c2b7168..ddb2d15 100644 Binary files a/screenshot/game_state_flight_block.png and b/screenshot/game_state_flight_block.png differ diff --git a/screenshot/game_state_follow.png b/screenshot/game_state_follow.png index ceb5a74..7ab94d9 100644 Binary files a/screenshot/game_state_follow.png and b/screenshot/game_state_follow.png differ diff --git a/screenshot/game_state_hp.png b/screenshot/game_state_hp.png index 479024f..17dc1a0 100644 Binary files a/screenshot/game_state_hp.png and b/screenshot/game_state_hp.png differ diff --git a/screenshot/game_state_logistics_death.png b/screenshot/game_state_logistics_death.png index 3277361..4743f89 100644 Binary files a/screenshot/game_state_logistics_death.png and b/screenshot/game_state_logistics_death.png differ diff --git a/screenshot/game_state_mp.png b/screenshot/game_state_mp.png index 2da3e80..5691984 100644 Binary files a/screenshot/game_state_mp.png and b/screenshot/game_state_mp.png differ diff --git a/screenshot/game_state_target.png b/screenshot/game_state_target.png index 871eaa7..509c85b 100644 Binary files a/screenshot/game_state_target.png and b/screenshot/game_state_target.png differ diff --git a/screenshot/game_state_x.png b/screenshot/game_state_x.png index 8a70a1e..ccce0f4 100644 Binary files a/screenshot/game_state_x.png and b/screenshot/game_state_x.png differ diff --git a/screenshot/game_state_y.png b/screenshot/game_state_y.png index 304e98e..74cae33 100644 Binary files a/screenshot/game_state_y.png and b/screenshot/game_state_y.png differ diff --git a/wow_multikey_gui.py b/wow_multikey_gui.py index f362987..ec41e87 100644 --- a/wow_multikey_gui.py +++ b/wow_multikey_gui.py @@ -583,6 +583,9 @@ class WoWMultiKeyGUI(QMainWindow): self.init_ui() self.find_wow_window() + # 加载参数配置到界面 + self._load_params_config() + # 初始化全局热键监听 (F8 用于拾取录制) self.kb_listener = keyboard.Listener(on_press=self._on_hotkey_press) self.kb_listener.start() @@ -1139,7 +1142,6 @@ class WoWMultiKeyGUI(QMainWindow): self.gs_offset_left.setValue(cfg.get('offset_left', 20)) self.gs_offset_top.setValue(cfg.get('offset_top', 45)) self.gs_mount_key.setText(str(cfg.get('mount_key', 'x') or 'x')) - self.gs_enable_mount.setChecked(bool(cfg.get('enable_mount', True))) self.gs_mount_hold.setValue(float(cfg.get('mount_hold_sec', 1.6))) self.gs_hearthstone_key.setText(str(cfg.get('hearthstone_key', 'b') or 'b')) self.gs_bag_full_hearthstone.setChecked(bool(cfg.get('bag_full_hearthstone', False))) @@ -1175,7 +1177,6 @@ class WoWMultiKeyGUI(QMainWindow): cfg['offset_left'] = self.gs_offset_left.value() cfg['offset_top'] = self.gs_offset_top.value() cfg['mount_key'] = (self.gs_mount_key.text().strip() or 'x') - cfg['enable_mount'] = self.gs_enable_mount.isChecked() cfg['mount_hold_sec'] = float(self.gs_mount_hold.value()) cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value()) cfg['hearthstone_key'] = (self.gs_hearthstone_key.text().strip() or 'b') diff --git a/wow_multikey_qt.json b/wow_multikey_qt.json index 9669e73..b043fdd 100644 --- a/wow_multikey_qt.json +++ b/wow_multikey_qt.json @@ -11,6 +11,6 @@ "food_key": "f1", "eat_hp_threshold": 30, "eat_max_wait_sec": 30.0, - "enable_mouse_loot": false + "enable_mouse_loot": true } } \ No newline at end of file