更新:添加鼠标图标识别、复活逻辑优化、参数配置加载修复、目标血量100%检测

This commit is contained in:
王鹏
2026-04-19 15:53:45 +08:00
parent 33dc741fd9
commit 6ec468a78b
46 changed files with 900 additions and 131 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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)

View File

@@ -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 秒或回满)

View File

@@ -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,

View File

@@ -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 }

View File

@@ -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,

View File

@@ -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

View File

@@ -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已停止。")
print("\n已停止。")

View File

@@ -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
}
"hearthstone_key": "6",
"bag_full_hearthstone": true
}

View File

@@ -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()

BIN
images/cursor/Attack.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/cursor/LootAll.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/cursor/Mine.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
images/cursor/Pickup.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/cursor/Point.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/cursor/REPAIRNPC.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/cursor/Skin.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/cursor/Taxi.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
images/cursor/Trainer.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -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]]
[[-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]]

View File

@@ -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
]
]

View File

@@ -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
]
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 B

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

After

Width:  |  Height:  |  Size: 85 B

View File

@@ -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')

View File

@@ -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
}
}