add 巡逻模式优先上马

This commit is contained in:
王鹏
2026-03-23 09:50:08 +08:00
parent be436b66a7
commit f517c29579
20 changed files with 233 additions and 6 deletions

View File

@@ -5,7 +5,7 @@ import sys
import time import time
import pydirectinput import pydirectinput
from game_state import parse_game_state from game_state import parse_game_state, load_layout_config
from coordinate_patrol import CoordinatePatrol from coordinate_patrol import CoordinatePatrol
from death_manager import DeathManager from death_manager import DeathManager
from logistics_manager import LogisticsManager from logistics_manager import LogisticsManager
@@ -91,7 +91,13 @@ class AutoBotMove:
if waypoints is None: if waypoints is None:
path = waypoints_path or get_config_path(WAYPOINTS_FILE) path = waypoints_path or get_config_path(WAYPOINTS_FILE)
waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS
self.patrol_controller = CoordinatePatrol(waypoints) layout = load_layout_config()
self.patrol_controller = CoordinatePatrol(
waypoints,
mount_key=str(layout.get("mount_key", "x") or "x"),
mount_hold_sec=float(layout.get("mount_hold_sec", 1.6)),
mount_retry_after_sec=float(layout.get("mount_retry_after_sec", 2.0)),
)
self.death_manager = DeathManager(self.patrol_controller) self.death_manager = DeathManager(self.patrol_controller)
vendor_file = vendor_path or get_config_path('vendor.json') vendor_file = vendor_path or get_config_path('vendor.json')
self.logistics_manager = LogisticsManager(vendor_file) self.logistics_manager = LogisticsManager(vendor_file)

View File

@@ -15,7 +15,6 @@ ANGLE_DEADZONE_DEG = 5.0 # 死区,此范围内不按 A/D、只按 W
# 随机行为跳跃概率每帧触发概率0.005 ≈ 平均 200 帧一次) # 随机行为跳跃概率每帧触发概率0.005 ≈ 平均 200 帧一次)
RANDOM_JUMP_PROB = 0.05 RANDOM_JUMP_PROB = 0.05
class CoordinatePatrol: class CoordinatePatrol:
"""按航点坐标巡逻,用 pyautogui 按键转向与前进。""" """按航点坐标巡逻,用 pyautogui 按键转向与前进。"""
@@ -26,6 +25,9 @@ class CoordinatePatrol:
angle_threshold_deg=ANGLE_THRESHOLD_DEG, angle_threshold_deg=ANGLE_THRESHOLD_DEG,
angle_deadzone_deg=ANGLE_DEADZONE_DEG, angle_deadzone_deg=ANGLE_DEADZONE_DEG,
random_jump_prob=RANDOM_JUMP_PROB, random_jump_prob=RANDOM_JUMP_PROB,
mount_key="x",
mount_hold_sec=1.6,
mount_retry_after_sec=2.0,
): ):
""" """
Args: Args:
@@ -33,6 +35,7 @@ class CoordinatePatrol:
arrival_threshold: 到达判定距离(游戏坐标单位),默认 0.5 arrival_threshold: 到达判定距离(游戏坐标单位),默认 0.5
angle_threshold_deg: 朝向容差(度),超过此值才按 A/D 转向,默认 ANGLE_THRESHOLD_DEG angle_threshold_deg: 朝向容差(度),超过此值才按 A/D 转向,默认 ANGLE_THRESHOLD_DEG
angle_deadzone_deg: 转向死区(度),此范围内不按 A/D、只按 W默认 ANGLE_DEADZONE_DEG angle_deadzone_deg: 转向死区(度),此范围内不按 A/D、只按 W默认 ANGLE_DEADZONE_DEG
mount_key / mount_hold_sec / mount_retry_after_sec: 未上马时先上马(与 game_state_config / GUI 一致)
""" """
self.waypoints = waypoints self.waypoints = waypoints
self.current_index = 0 self.current_index = 0
@@ -43,6 +46,33 @@ class CoordinatePatrol:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.last_turn_end_time = 0 # 最近一次结束 A/D 转向的时间,供卡死检测排除原地转向 self.last_turn_end_time = 0 # 最近一次结束 A/D 转向的时间,供卡死检测排除原地转向
self.stuck_handler = StuckHandler() self.stuck_handler = StuckHandler()
self._next_mount_allowed = 0.0
self.mount_key = str(mount_key).strip() or "x"
self.mount_hold_sec = float(mount_hold_sec)
self.mount_retry_after_sec = float(mount_retry_after_sec)
def _ensure_mounted(self, state):
"""
已上马返回 True。
未上马则松开移动键、按住上马键 MOUNT_HOLD_SEC本帧不走路返回 False。
state 无 mounted 字段时视为无法判断,不拦巡逻(兼容未开 LogicBeacon
"""
if "mounted" not in state:
return True
if state.get("mounted"):
return True
self.stop_all()
now = time.time()
if now < self._next_mount_allowed:
return False
self.logger.info(
f">>> 未上马,先按 {self.mount_key} {self.mount_hold_sec:.1f}s 上马"
)
pyautogui.keyDown(self.mount_key)
time.sleep(self.mount_hold_sec)
pyautogui.keyUp(self.mount_key)
self._next_mount_allowed = time.time() + self.mount_retry_after_sec
return False
@staticmethod @staticmethod
def get_distance(p1, p2): def get_distance(p1, p2):
@@ -168,6 +198,9 @@ class CoordinatePatrol:
self.stop_all() self.stop_all()
return return
if not self._ensure_mounted(state):
return
# 2. 卡死检测:位移检测逻辑保持在巡逻中 # 2. 卡死检测:位移检测逻辑保持在巡逻中
if self.stuck_handler.check_stuck(state, self.last_turn_end_time): if self.stuck_handler.check_stuck(state, self.last_turn_end_time):
self.logger.error("!!! 检测到卡死,启动脱困程序 !!!") self.logger.error("!!! 检测到卡死,启动脱困程序 !!!")
@@ -250,6 +283,9 @@ class CoordinatePatrol:
self.stop_all() self.stop_all()
return False return False
if not self._ensure_mounted(state):
return False
# 1. 卡死检测 # 1. 卡死检测
if self.stuck_handler.check_stuck(state, self.last_turn_end_time): if self.stuck_handler.check_stuck(state, self.last_turn_end_time):
self.stop_all() self.stop_all()
@@ -311,6 +347,7 @@ class CoordinatePatrol:
""" """
按 path 依次走完所有点后返回。每次调用都必须传入 path。 按 path 依次走完所有点后返回。每次调用都必须传入 path。
get_state: 可调用对象,每次调用返回当前状态 dict需包含 'x','y','facing' get_state: 可调用对象,每次调用返回当前状态 dict需包含 'x','y','facing'
若含 'mounted'LogicBeacon路径开始前会先上马每段 navigate_to_point 也会校验。
path: [[x,y], ...] 或 [(x,y), ...](如 json.load 得到),本次要走的全部航点。 path: [[x,y], ...] 或 [(x,y), ...](如 json.load 得到),本次要走的全部航点。
forward: True=正序0→nFalse=倒序n→0 forward: True=正序0→nFalse=倒序n→0
阻塞直到走完 path 中所有点或出错,走完返回 True。内含卡死检测。 阻塞直到走完 path 中所有点或出错,走完返回 True。内含卡死检测。
@@ -321,6 +358,17 @@ class CoordinatePatrol:
points = [(float(p[0]), float(p[1])) for p in path] points = [(float(p[0]), float(p[1])) for p in path]
if not forward: if not forward:
points = points[::-1] points = points[::-1]
# 路径开始前:若 state 含 mounted 且未上马,先完成上马再逐点移动
while True:
state = get_state()
if state is None:
self.stop_all()
return False
if self._ensure_mounted(state):
break
time.sleep(0.05)
for i, target in enumerate(points): for i, target in enumerate(points):
self.logger.info(f">>> 路径点 {i + 1}/{len(points)}: {target}") self.logger.info(f">>> 路径点 {i + 1}/{len(points)}: {target}")
while True: while True:

View File

@@ -21,6 +21,10 @@ _DEFAULTS = {
"scan_region_height": 15, "scan_region_height": 15,
"offset_left": 20, "offset_left": 20,
"offset_top": 45, "offset_top": 45,
# 巡逻上马coordinate_patrol与 GUI「参数配置」一致
"mount_key": "x",
"mount_hold_sec": 1.6,
"mount_retry_after_sec": 2.0,
} }
SCREENSHOT_DIR = 'screenshot' SCREENSHOT_DIR = 'screenshot'

View File

@@ -4,5 +4,8 @@
"scan_region_width": 190, "scan_region_width": 190,
"scan_region_height": 15, "scan_region_height": 15,
"offset_left": 20, "offset_left": 20,
"offset_top": 45 "offset_top": 45,
"mount_key": "x",
"mount_hold_sec": 1.6,
"mount_retry_after_sec": 2.0
} }

View File

@@ -0,0 +1,142 @@
[
[
12.1,
54.99
],
[
11.62,
54.79
],
[
11.16,
54.54
],
[
10.82,
54.13
],
[
10.54,
53.7
],
[
10.3,
53.24
],
[
10.06,
52.75
],
[
9.82,
52.31
],
[
9.55,
51.86
],
[
9.42,
51.35
],
[
9.39,
50.83
],
[
9.24,
50.35
],
[
9.15,
49.85
],
[
9.5,
49.44
],
[
9.94,
49.14
],
[
10.38,
48.85
],
[
10.81,
48.56
],
[
11.23,
48.26
],
[
11.65,
47.94
],
[
12.11,
47.65
],
[
12.59,
47.49
],
[
12.54,
48.01
],
[
12.44,
48.53
],
[
12.32,
49.03
],
[
12.2,
49.54
],
[
12.1,
50.07
],
[
12.1,
50.6
],
[
12.33,
51.1
],
[
12.55,
51.59
],
[
12.68,
52.11
],
[
12.78,
52.62
],
[
12.86,
53.12
],
[
12.83,
53.65
],
[
12.76,
54.17
],
[
12.1,
54.99
]
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 83 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 91 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: 74 B

After

Width:  |  Height:  |  Size: 84 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 83 B

View File

@@ -69,11 +69,11 @@ class StuckHandler:
pyautogui.keyDown(turn_key) pyautogui.keyDown(turn_key)
# 倒车时间稍长一点,离开障碍物 # 倒车时间稍长一点,离开障碍物
time.sleep(0.3) time.sleep(0.5)
# 3. 尝试跳跃脱离地形卡位 # 3. 尝试跳跃脱离地形卡位
pyautogui.press("space") pyautogui.press("space")
time.sleep(0.3) time.sleep(0.5)
pyautogui.keyUp(turn_key) pyautogui.keyUp(turn_key)
pyautogui.keyUp("s") pyautogui.keyUp("s")

View File

@@ -746,6 +746,24 @@ class WoWMultiKeyGUI(QMainWindow):
self.skinning_wait_spin.setSuffix("") self.skinning_wait_spin.setSuffix("")
params_right.addRow("剥皮等待时间:", self.skinning_wait_spin) params_right.addRow("剥皮等待时间:", self.skinning_wait_spin)
self.gs_mount_key = QLineEdit()
self.gs_mount_key.setPlaceholderText("如 x")
self.gs_mount_key.setMaxLength(16)
self.gs_mount_key.setText("x")
self.gs_mount_hold = QDoubleSpinBox()
self.gs_mount_hold.setRange(0.1, 30.0)
self.gs_mount_hold.setSingleStep(0.1)
self.gs_mount_hold.setValue(1.6)
self.gs_mount_hold.setSuffix("")
self.gs_mount_retry = QDoubleSpinBox()
self.gs_mount_retry.setRange(0.0, 60.0)
self.gs_mount_retry.setSingleStep(0.1)
self.gs_mount_retry.setValue(2.0)
self.gs_mount_retry.setSuffix("")
params_right.addRow("上马按键 (mount_key):", self.gs_mount_key)
params_right.addRow("上马按住时长 (mount_hold_sec):", self.gs_mount_hold)
params_right.addRow("上马重试间隔 (mount_retry_after_sec):", self.gs_mount_retry)
params_row.addLayout(params_left) params_row.addLayout(params_left)
params_row.addLayout(params_right) params_row.addLayout(params_right)
params_layout.addWidget(params_content) params_layout.addWidget(params_content)
@@ -780,6 +798,9 @@ class WoWMultiKeyGUI(QMainWindow):
self.gs_scan_height.setValue(cfg.get('scan_region_height', 15)) self.gs_scan_height.setValue(cfg.get('scan_region_height', 15))
self.gs_offset_left.setValue(cfg.get('offset_left', 20)) self.gs_offset_left.setValue(cfg.get('offset_left', 20))
self.gs_offset_top.setValue(cfg.get('offset_top', 45)) 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_mount_hold.setValue(float(cfg.get('mount_hold_sec', 1.6)))
self.gs_mount_retry.setValue(float(cfg.get('mount_retry_after_sec', 2.0)))
except Exception as e: except Exception as e:
self.log(f"加载参数配置失败: {e}") self.log(f"加载参数配置失败: {e}")
try: try:
@@ -800,6 +821,9 @@ class WoWMultiKeyGUI(QMainWindow):
cfg['scan_region_height'] = self.gs_scan_height.value() cfg['scan_region_height'] = self.gs_scan_height.value()
cfg['offset_left'] = self.gs_offset_left.value() cfg['offset_left'] = self.gs_offset_left.value()
cfg['offset_top'] = self.gs_offset_top.value() cfg['offset_top'] = self.gs_offset_top.value()
cfg['mount_key'] = (self.gs_mount_key.text().strip() or 'x')
cfg['mount_hold_sec'] = float(self.gs_mount_hold.value())
cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value())
path = save_layout_config(cfg) path = save_layout_config(cfg)
# bot 参数写入主配置文件 # bot 参数写入主配置文件
self.config = self.config or {} self.config = self.config or {}