Files
wow/player_movement.py
2026-03-18 09:04:37 +08:00

285 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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°/s0.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需增大朝向 → 按 ddiff > 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 需增大朝向 → ddiff > 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)