更新:添加鼠标图标识别、复活逻辑优化、参数配置加载修复、目标血量100%检测
This commit is contained in:
290
auto_bot_move.py
290
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 秒或回满)
|
||||
|
||||
Reference in New Issue
Block a user