import math import time import logging import pyautogui from player_position import PlayerPosition # 游戏朝向约定:正北=0°,正西=90°,正南=180°,正东=270°(逆时针递增) # 游戏坐标约定:人物面朝正北,按w一秒,坐标y会增加0.1 反之减少0.1 。如果人物面朝正东,按w一秒,坐标x会增加0.1 如果人物面朝正西,按w一秒,坐标x会减少0.1 。 # 按键w前进,按键a左转,按键d右转,按键s后退 (转向时最好按0.5s就停止,以免转向过度) # 当前人物的坐标和朝向根据player_position.py模块获取 class PlayerMovement: MOVE_SPEED = 0.1 # 每秒移动距离(坐标单位) TURN_RATE = 216.0 # 每秒转向角度(度);实测约 216°/s(0.5s ≈ 108°) MAX_TURN_DURATION = 0.5 # 原地转向单次最长按键时间(秒),避免转向过度 SMOOTH_TURN_DURATION = 0.15 # 边走边转时每次按键时长(秒) ANGLE_TOLERANCE = 5.0 # 朝向允许误差(度) COARSE_TURN_THRESHOLD = 90.0 # 超过此角度差则停下来原地粗对齐,再继续行走 HEADING_CHECK_INTERVAL = 5 # 每隔几次位置读取才做一次朝向校正 STUCK_CHECK_COUNT = 3 # 连续多少次坐标无明显位移则判定为卡死 STUCK_MOVE_THRESHOLD = 0.05 # 每次检测周期内最小期望位移(坐标单位) # 实测方向:按 a 朝向减小(右转),按 d 朝向增大(左转) # 游戏朝向为顺时针:北=0°, 东=90°, 南=180°, 西=270° def __init__(self): self.player_position = PlayerPosition() self.logger = logging.getLogger(__name__) def get_required_heading(self, current_x: float, current_y: float, target_x: float, target_y: float) -> float: """计算从当前位置朝向目标位置所需的游戏朝向角度。 实测游戏规律: 朝向 0° → 南(y 减小),朝向 180° → 北(y 增加) 朝向 90° → 东(x 增加),朝向 270° → 西(x 减小) 即:dx_actual ∝ sin(heading),dy_actual ∝ -cos(heading) 因此目标朝向 = atan2(dx, -dy) 验证:需北移(dy>0) → atan2(0,-1)=180° ✓;需东移(dx>0) → atan2(1,0)=90° ✓ """ dx = target_x - current_x dy = target_y - current_y if dx == 0 and dy == 0: return 0.0 return math.degrees(math.atan2(dx, -dy)) % 360 def turn_to_heading(self, target_heading: float, max_attempts: int = 10) -> bool: """转向到目标朝向。 按键 a 左转(朝向增大),按键 d 右转(朝向减小)。 每次按键不超过 MAX_TURN_DURATION 秒,多次微调直到误差在 ANGLE_TOLERANCE 内。 Args: target_heading: 目标朝向角度(0~360) max_attempts: 最大调整次数 Returns: bool: 是否成功对准目标朝向 """ for attempt in range(max_attempts): current_heading = self.player_position.get_heading_with_retry() if current_heading is None: self.logger.warning("无法获取当前朝向,转向中止") return False diff = (target_heading - current_heading) % 360 if diff <= self.ANGLE_TOLERANCE or diff >= (360 - self.ANGLE_TOLERANCE): self.logger.info(f"朝向已对齐:当前 {current_heading:.1f}°,目标 {target_heading:.1f}°") return True # 实测:d 使朝向增大,a 使朝向减小(与按键名称相反) # diff ≤ 180:需增大朝向 → 按 d;diff > 180:需减小朝向 → 按 a if diff <= 180: key, angle_to_turn = 'd', diff else: key, angle_to_turn = 'a', 360 - diff turn_duration = min(angle_to_turn / self.TURN_RATE, self.MAX_TURN_DURATION) self.logger.info( f"[转向 {attempt + 1}] 当前 {current_heading:.1f}° → 目标 {target_heading:.1f}°," f"按 '{key}' {turn_duration:.2f}s(需转 {angle_to_turn:.1f}°)" ) pyautogui.keyDown(key) time.sleep(turn_duration) pyautogui.keyUp(key) time.sleep(0.15) # 等待游戏刷新朝向 self.logger.warning(f"转向失败:超过最大尝试次数 {max_attempts}") return False def move_forward(self, duration: float): """按住 w 键向前移动指定时间。 Args: duration: 移动时间(秒) """ self.logger.info(f"向前移动 {duration:.2f}s") pyautogui.keyDown('w') time.sleep(duration) pyautogui.keyUp('w') def _escape_stuck(self): """卡死脱困组合拳:后退 + 随机转向 + 跳跃。 当连续多次检测到坐标无明显位移时调用,尝试脱离障碍物。 """ import random self.logger.warning("检测到角色卡死,执行脱困动作") # 1. 松开前进键,后退 0.8s pyautogui.keyUp('w') pyautogui.keyDown('s') time.sleep(0.8) pyautogui.keyUp('s') # 2. 随机左转或右转 0.3~0.5s turn_key = random.choice(['a', 'd']) turn_dur = random.uniform(0.3, 0.5) pyautogui.keyDown(turn_key) time.sleep(turn_dur) pyautogui.keyUp(turn_key) # 3. 跳跃(按空格)同时向前冲 0.5s pyautogui.keyDown('w') pyautogui.press('space') time.sleep(0.5) pyautogui.keyUp('w') self.logger.info("脱困动作完成,重新开始前进") def move_to(self, target_x: float, target_y: float, position_tolerance: float = 0.5, max_iterations: int = 100) -> bool: """平滑移动人物到目标坐标(边走边转向)。 策略: 1. 起步前若角度偏差 > COARSE_TURN_THRESHOLD,先原地粗对齐; 2. 按住 w 持续前进,每次读取朝向后同时按 a/d 微调方向(边走边转); 3. 若行走中出现大角度偏差(绕过障碍等),短暂停步原地修正后继续; 4. 到达判定半径内松开 w,结束移动。 Args: target_x, target_y: 目标坐标 position_tolerance: 到达判定半径(坐标单位),默认 0.5 max_iterations: 最大循环次数,防止死循环 Returns: bool: 是否成功到达目标位置 """ self.logger.info(f"开始平滑移动到目标位置 ({target_x}, {target_y})") # 起步前先判断是否已在目标范围内,避免重复调用时原地乱转 init_pos = self.player_position.get_position_with_retry() if init_pos is None: self.logger.warning("无法获取当前坐标,移动中止") return False if (abs(target_x - init_pos[0]) <= position_tolerance and abs(target_y - init_pos[1]) <= position_tolerance): self.logger.info(f"已在目标位置 ({target_x}, {target_y}) 范围内,无需移动") return True # 起步前粗对齐:角度偏差过大时先原地转向,避免一开始就走偏 init_heading = self.player_position.get_heading_with_retry() if init_heading is not None: init_required = self.get_required_heading(init_pos[0], init_pos[1], target_x, target_y) init_diff = (init_required - init_heading) % 360 init_abs_diff = init_diff if init_diff <= 180 else 360 - init_diff if init_abs_diff > self.COARSE_TURN_THRESHOLD: self.logger.info(f"起步角度差 {init_abs_diff:.1f}°,先原地粗对齐") if not self.turn_to_heading(init_required): self.logger.warning("起步粗对齐失败,移动中止") return False # 按住 w 开始持续前进 pyautogui.keyDown('w') # 卡死检测:记录最近 STUCK_CHECK_COUNT 次坐标,用于判断是否停滞 recent_positions = [] try: for iteration in range(max_iterations): # 每隔 HEADING_CHECK_INTERVAL 次才读一次朝向并校正 # 读取当前位置(OCR 耗时期间 w 持续按住,角色一直在走) current_pos = self.player_position.get_position_with_retry() if current_pos is None: self.logger.warning("无法获取当前坐标,移动中止") return False current_x, current_y = current_pos dx = target_x - current_x dy = target_y - current_y distance = math.sqrt(dx ** 2 + dy ** 2) self.logger.info( f"[平滑 {iteration + 1}] 当前 ({current_x}, {current_y})," f"目标 ({target_x}, {target_y}),剩余距离 {distance:.2f}" ) # 卡死检测:维护滑动窗口,比较最新与最旧坐标的位移 recent_positions.append((current_x, current_y)) if len(recent_positions) > self.STUCK_CHECK_COUNT: recent_positions.pop(0) if len(recent_positions) == self.STUCK_CHECK_COUNT: oldest_x, oldest_y = recent_positions[0] moved = math.sqrt((current_x - oldest_x) ** 2 + (current_y - oldest_y) ** 2) if moved < self.STUCK_MOVE_THRESHOLD: self._escape_stuck() recent_positions.clear() pyautogui.keyDown('w') # x 和 y 都在容差范围内即视为到达 if abs(dx) <= position_tolerance and abs(dy) <= position_tolerance: self.logger.info(f"已到达目标位置 ({target_x}, {target_y})") return True # 接近目标时松开 w 停下,再做一次静止判定,避免冲过头后死循环 if distance <= position_tolerance * 2: pyautogui.keyUp('w') time.sleep(0.2) final_pos = self.player_position.get_position_with_retry() if final_pos is not None: fdx = target_x - final_pos[0] fdy = target_y - final_pos[1] if abs(fdx) <= position_tolerance and abs(fdy) <= position_tolerance: self.logger.info(f"已到达目标位置 ({target_x}, {target_y})") return True # 静止后仍未到达,重新按住 w 继续前进 pyautogui.keyDown('w') # 每隔 HEADING_CHECK_INTERVAL 次读一次朝向,减少 OCR 频率 if iteration % self.HEADING_CHECK_INTERVAL != 0: continue current_heading = self.player_position.get_heading_with_retry() if current_heading is None: continue required_heading = self.get_required_heading(current_x, current_y, target_x, target_y) diff = (required_heading - current_heading) % 360 abs_diff = diff if diff <= 180 else 360 - diff if abs_diff > self.COARSE_TURN_THRESHOLD: # 偏差过大:停步原地粗修正,再重新按住 w pyautogui.keyUp('w') self.logger.info(f"偏差 {abs_diff:.1f}° 过大,停步原地修正") if not self.turn_to_heading(required_heading): self.logger.warning("修正转向失败,移动中止") return False pyautogui.keyDown('w') elif abs_diff > self.ANGLE_TOLERANCE: # 小幅偏差:边走边转,同时按住 w 和转向键 # diff ≤ 180 需增大朝向 → d;diff > 180 需减小朝向 → a turn_key = 'd' if diff <= 180 else 'a' turn_time = min(abs_diff / self.TURN_RATE, self.SMOOTH_TURN_DURATION) self.logger.info( f"边走边转:按 '{turn_key}' {turn_time:.2f}s(偏差 {abs_diff:.1f}°)" ) pyautogui.keyDown(turn_key) time.sleep(turn_time) pyautogui.keyUp(turn_key) else: # 方向已对齐,让角色多走一段再重新检测,减少 OCR 频率 time.sleep(1) finally: pyautogui.keyUp('w') self.logger.warning(f"移动失败:超过最大迭代次数 {max_iterations}") return False def move_to_position(self, target_x: float, target_y: float, tolerance: float = 0.5, max_iterations: int = 100) -> bool: """移动人物到目标坐标(combat_engine 对外接口)。 Args: target_x, target_y: 目标坐标 tolerance: 到达判定半径(坐标单位),默认 0.5 max_iterations: 最大循环次数 Returns: bool: 是否成功到达目标位置 """ return self.move_to(target_x, target_y, position_tolerance=tolerance, max_iterations=max_iterations)