Add route profile patrol selection

This commit is contained in:
王鹏
2026-05-12 17:09:19 +08:00
parent 0d493ee180
commit 2a4684e700
6 changed files with 1462 additions and 185 deletions

View File

@@ -14,6 +14,7 @@ from game_state import parse_game_state, load_layout_config
from coordinate_patrol import CoordinatePatrol
from death_manager import DeathManager
from logistics_manager import LogisticsManager
from route_profile import load_route_profile
# 定义按键常量
KEY_TAB = '3'
@@ -239,6 +240,7 @@ class AutoBotMove:
self,
waypoints=None,
waypoints_path=None,
route_profile_path=None,
prepare_route_path=None,
vendor_path=None,
attack_loop_path=None,
@@ -289,11 +291,35 @@ class AutoBotMove:
self.logistics_resume_cooldown_until = 0.0
# stop_check: 返回 True 表示需要立即停止(用于中断阻塞中的后勤/路线导航)
self._stop_check = stop_check if callable(stop_check) else (lambda: False)
if waypoints is None:
self.route_profile = None
self.route_profile_name = ""
self.patrol_routes = []
self.current_patrol_route_name = ""
if route_profile_path:
self.route_profile = load_route_profile(route_profile_path)
self.route_profile_name = self.route_profile.get("name") or ""
self.patrol_routes = list(self.route_profile.get("patrol_routes") or [])
if self.patrol_routes:
waypoints = self.patrol_routes[0]["points"]
prepare_route = self.route_profile.get("prepare_route")
if prepare_route:
self.prepare_route_waypoints = list(prepare_route["points"])
else:
self.prepare_route_waypoints = []
print(
f">>> [路线方案] 已加载: {self.route_profile_name}"
f"巡逻路线 {len(self.patrol_routes)}"
)
elif waypoints is None:
path = waypoints_path or get_config_path(WAYPOINTS_FILE)
waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS
self.prepare_route_waypoints = []
if prepare_route_path:
self.prepare_route_waypoints = []
else:
self.prepare_route_waypoints = []
if (not route_profile_path) and prepare_route_path:
try:
self.prepare_route_waypoints = [
(float(point[0]), float(point[1]))
@@ -320,11 +346,17 @@ class AutoBotMove:
resurrection_waypoints_path=resurrection_waypoints_path,
resurrection_route_a_path=resurrection_route_a_path,
resurrection_route_b_path=resurrection_route_b_path,
resurrection_routes=(
self.route_profile.get("resurrection_routes") if self.route_profile else None
),
release_spirit_key=layout.get('release_spirit_key', '9') if release_spirit_key is None else release_spirit_key,
resurrect_key=layout.get('resurrect_key', '0') if resurrect_key is None else resurrect_key,
)
vendor_file = vendor_path or get_config_path('vendor.json')
self.logistics_manager = LogisticsManager(vendor_file)
if self.route_profile:
self.logistics_manager.set_repair_route(self.route_profile.get("vendor_route"))
self.logistics_manager.set_mailbox_route(self.route_profile.get("mailbox_route"))
self.logistics_manager.bag_full_hearthstone = bool(layout.get("bag_full_hearthstone", False))
self.logistics_manager.hearthstone_key = str(layout.get("hearthstone_key", "b") or "b")
self.logistics_manager.hearthstone_cast_sec = float(layout.get("hearthstone_wait_sec", 12.0))
@@ -345,6 +377,31 @@ class AutoBotMove:
self.logistics_manager.mailbox_open_wait_sec = float(layout.get("mailbox_open_wait_sec", 2.0))
self.logistics_manager.mail_send_wait_sec = float(layout.get("mail_send_wait_sec", 60.0))
if self.patrol_routes and self.prepare_route_completed:
self._select_random_patrol_route("启动")
def _select_random_patrol_route(self, reason=""):
if not self.patrol_routes:
return False
route = random.choice(self.patrol_routes)
points = list(route.get("points") or [])
if not points:
return False
self.current_patrol_route_name = str(route.get("name") or "未命名巡逻路线")
reverse_route = random.choice((False, True))
direction_name = "逆向" if reverse_route else "正向"
if reverse_route:
points = list(reversed(points))
self.patrol_controller.waypoints = points
self.patrol_controller.current_index = 0
self.patrol_controller.reset_stuck()
prefix = f"{reason}" if reason else ""
print(
f">>> [巡逻路线] {prefix}随机选择: "
f"{self.current_patrol_route_name},方向: {direction_name}{len(points)} 点)"
)
return True
def _has_prepare_route(self) -> bool:
return bool(self.prepare_route_waypoints)
@@ -364,6 +421,7 @@ class AutoBotMove:
if self.prepare_route_waypoints:
print(">>> [后勤] 全自动续跑:重置准备路线,准备返回巡逻。")
else:
self._select_random_patrol_route("后勤返回")
print(">>> [后勤] 全自动续跑:未配置准备路线,直接恢复巡逻。")
def _snap_prepare_route_to_nearest(self, state):
@@ -418,6 +476,7 @@ class AutoBotMove:
self.is_moving = False
self.patrol_controller.stop_all()
self.patrol_controller.reset_stuck()
self._select_random_patrol_route("准备路线完成")
print(">>> [准备路线] 准备路线完成,开始正式巡逻...")
return
@@ -443,6 +502,7 @@ class AutoBotMove:
self.is_moving = False
self.patrol_controller.stop_all()
self.patrol_controller.reset_stuck()
self._select_random_patrol_route("准备路线完成")
print(">>> [准备路线] 准备路线完成,开始正式巡逻...")
def _is_effective_target(self, state) -> bool:

View File

@@ -2,6 +2,7 @@ import math
import time
from hardware_control import hw_ctrl
from route_profile import normalize_route_list
class DeathManager:
@@ -13,16 +14,19 @@ class DeathManager:
resurrection_waypoints_path=None,
resurrection_route_a_path=None,
resurrection_route_b_path=None,
resurrection_routes=None,
release_spirit_key='9',
resurrect_key='0',
):
self.patrol_system = patrol_system
self.release_spirit_key = str(release_spirit_key).strip() or '9'
self.resurrect_key = str(resurrect_key).strip() or '0'
self.resurrection_route_a = self._load_waypoints(
resurrection_route_a_path or resurrection_waypoints_path
self.resurrection_routes = self._load_resurrection_routes(
resurrection_routes,
resurrection_waypoints_path=resurrection_waypoints_path,
resurrection_route_a_path=resurrection_route_a_path,
resurrection_route_b_path=resurrection_route_b_path,
)
self.resurrection_route_b = self._load_waypoints(resurrection_route_b_path)
self.corpse_pos = None
self.graveyard_pos = None
@@ -53,6 +57,29 @@ class DeathManager:
pass
return None
def _load_resurrection_routes(
self,
resurrection_routes=None,
resurrection_waypoints_path=None,
resurrection_route_a_path=None,
resurrection_route_b_path=None,
):
routes = []
for route in normalize_route_list(resurrection_routes, "复活路线"):
routes.append((route["name"], route["points"]))
if routes:
return routes
legacy_routes = [
("A", resurrection_route_a_path or resurrection_waypoints_path),
("B", resurrection_route_b_path),
]
for name, path in legacy_routes:
points = self._load_waypoints(path)
if points:
routes.append((name, points))
return routes
def _clear_death_run_state(self):
self.graveyard_pos = None
self.selected_resurrection_route = None
@@ -61,12 +88,7 @@ class DeathManager:
self._resurrection_completed = False
def _build_resurrection_route_candidates(self):
candidates = []
if self.resurrection_route_a:
candidates.append(("A", self.resurrection_route_a))
if self.resurrection_route_b:
candidates.append(("B", self.resurrection_route_b))
return candidates
return list(self.resurrection_routes)
def _record_graveyard_pos(self, state):
if self.graveyard_pos is not None:

View File

@@ -3,6 +3,7 @@ import math
import os
import time
from hardware_control import hw_ctrl
from route_profile import normalize_points
# 修理商所在位置(游戏坐标),按实际位置修改
VENDOR_POS = (30.08, 71.51)
@@ -18,6 +19,8 @@ class LogisticsManager:
self.bag_full = False
self.is_returning = False
self.route_file = route_file or VENDOR_FILE
self.route_points = []
self.route_name = ""
self.bag_full_hearthstone = False # 包满时用炉石回城而非走路修理
self.hearthstone_key = "b" # 炉石按键
self.hearthstone_cast_sec = 12.0 # 炉石施法等待秒数
@@ -26,6 +29,8 @@ class LogisticsManager:
self.bag_slot_threshold = 2
self.durability_threshold = 0.2
self.mailbox_route_file = os.path.join("recorder", "mailbox.json")
self.mailbox_route_points = []
self.mailbox_route_name = ""
self.mailbox_interact_key = "8"
self.mail_recipient_key = ""
self.mail_send_key = "f8"
@@ -46,6 +51,35 @@ class LogisticsManager:
return cwd_path
return os.path.join(os.path.dirname(os.path.abspath(__file__)), path)
def set_repair_route(self, route):
route = route or {}
self.route_name = str(route.get("name") or "修理商路线").strip()
self.route_points = normalize_points(route.get("points") or [])
def set_mailbox_route(self, route):
route = route or {}
self.mailbox_route_name = str(route.get("name") or "邮箱路线").strip()
self.mailbox_route_points = normalize_points(route.get("points") or [])
def _load_configured_route(self, configured_points, route_file, source_name, label):
points = normalize_points(configured_points or [])
if points:
return points, source_name or label
resolved_file = self._resolve_path(route_file)
if not resolved_file or not os.path.exists(resolved_file):
return None, resolved_file
try:
with open(resolved_file, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as exc:
print(f">>> [{label}] 路线读取失败: {exc}")
return None, resolved_file
points = normalize_points(data)
return points, resolved_file
def _sleep_with_stop(self, seconds, stop_check=None):
end_at = time.time() + max(0.0, float(seconds))
while time.time() < end_at:
@@ -199,9 +233,14 @@ class LogisticsManager:
炉石 -> 跑到邮箱路线终点 -> 交互打开邮箱 -> 按 MailboxCourier 宏键 -> 等待后停止。
Python 不判断邮件是否发完,发送细节交给游戏内插件。
"""
route_file = self._resolve_path(self.mailbox_route_file)
if not route_file or not os.path.exists(route_file):
print(f">>> [后勤-邮箱] 邮箱路线不存在,已停止: {route_file}")
path, route_source = self._load_configured_route(
self.mailbox_route_points,
self.mailbox_route_file,
self.mailbox_route_name,
"后勤-邮箱",
)
if not path:
print(f">>> [后勤-邮箱] 邮箱路线不存在或为空,已停止: {route_source}")
self.is_returning = False
return False
@@ -216,20 +255,7 @@ class LogisticsManager:
self.is_returning = False
return False
try:
with open(route_file, "r", encoding="utf-8") as f:
path = json.load(f)
except Exception as exc:
print(f">>> [后勤-邮箱] 邮箱路线读取失败: {exc}")
self.is_returning = False
return False
if not path:
print(f">>> [后勤-邮箱] 邮箱路线为空,已停止: {route_file}")
self.is_returning = False
return False
print(f">>> [后勤-邮箱] 开始跑邮箱路线: {route_file}")
print(f">>> [后勤-邮箱] 开始跑邮箱路线: {route_source}")
old_enable_mount = getattr(patrol, "enable_mount", None)
if old_enable_mount is not None:
patrol.enable_mount = False
@@ -282,9 +308,14 @@ class LogisticsManager:
耐久低修理流程:
炉石 -> 从修理路线最近点接入 -> 到达 NPC -> 按目标键 -> 按交互/修理键。
"""
route_file = self._resolve_path(self.route_file)
if not route_file or not os.path.exists(route_file):
print(f">>> [后勤-修理] 修理路线不存在,已停止: {route_file}")
path, route_source = self._load_configured_route(
self.route_points,
self.route_file,
self.route_name,
"后勤-修理",
)
if not path:
print(f">>> [后勤-修理] 修理路线不存在或为空,已停止: {route_source}")
self.is_returning = False
return False
@@ -299,20 +330,7 @@ class LogisticsManager:
self.is_returning = False
return False
try:
with open(route_file, "r", encoding="utf-8") as f:
path = json.load(f)
except Exception as exc:
print(f">>> [后勤-修理] 修理路线读取失败: {exc}")
self.is_returning = False
return False
if not path:
print(f">>> [后勤-修理] 修理路线为空,已停止: {route_file}")
self.is_returning = False
return False
print(f">>> [后勤-修理] 开始跑修理路线: {route_file}")
print(f">>> [后勤-修理] 开始跑修理路线: {route_source}")
ok = patrol.navigate_path(
get_state,
path,

File diff suppressed because it is too large Load Diff

103
route_profile.py Normal file
View File

@@ -0,0 +1,103 @@
import json
import os
def normalize_points(raw_points):
"""Return route points as [(x, y), ...], dropping malformed entries."""
points = []
if not isinstance(raw_points, list):
return points
for point in raw_points:
if not isinstance(point, (list, tuple)) or len(point) < 2:
continue
try:
points.append((float(point[0]), float(point[1])))
except (TypeError, ValueError):
continue
return points
def _looks_like_points(raw_value):
if not isinstance(raw_value, list) or not raw_value:
return False
first = raw_value[0]
if not isinstance(first, (list, tuple)) or len(first) < 2:
return False
try:
float(first[0])
float(first[1])
except (TypeError, ValueError):
return False
return True
def normalize_route(raw_route, fallback_name=""):
"""Normalize {"name": ..., "points": ...} into a route dict."""
if raw_route is None:
return None
if isinstance(raw_route, dict):
name = str(raw_route.get("name") or fallback_name or "未命名路线").strip()
raw_points = (
raw_route.get("points")
or raw_route.get("waypoints")
or raw_route.get("path")
or []
)
else:
name = str(fallback_name or "未命名路线").strip()
raw_points = raw_route
points = normalize_points(raw_points)
if not points:
return None
return {"name": name or fallback_name or "未命名路线", "points": points}
def normalize_route_list(raw_routes, fallback_prefix):
routes = []
if raw_routes is None:
return routes
if _looks_like_points(raw_routes):
route = normalize_route(raw_routes, fallback_prefix)
return [route] if route else []
if isinstance(raw_routes, dict):
raw_routes = raw_routes.get("routes") or raw_routes.get("items") or []
if not isinstance(raw_routes, list):
return routes
for idx, raw_route in enumerate(raw_routes, start=1):
route = normalize_route(raw_route, f"{fallback_prefix}{idx}")
if route:
routes.append(route)
return routes
def normalize_route_profile(data, fallback_name=""):
if not isinstance(data, dict):
raise ValueError("路线方案 JSON 必须是对象格式")
name = str(data.get("name") or fallback_name or "未命名方案").strip()
patrol_routes = normalize_route_list(data.get("patrol_routes"), "巡逻路线")
if not patrol_routes:
raise ValueError("路线方案必须包含至少一条 patrol_routes")
return {
"name": name or fallback_name or "未命名方案",
"prepare_route": normalize_route(data.get("prepare_route"), "准备路线"),
"patrol_routes": patrol_routes,
"resurrection_routes": normalize_route_list(data.get("resurrection_routes"), "复活路线"),
"vendor_route": normalize_route(data.get("vendor_route"), "修理商路线"),
"mailbox_route": normalize_route(data.get("mailbox_route"), "邮箱路线"),
}
def load_route_profile(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
fallback_name = os.path.splitext(os.path.basename(path))[0]
return normalize_route_profile(data, fallback_name=fallback_name)

View File

@@ -23,6 +23,7 @@ import win32gui
import win32api
import win32con
from pynput import keyboard
from route_profile import load_route_profile
def _config_base():
@@ -67,6 +68,19 @@ def list_recorder_json():
return result
def list_route_profile_json():
"""列出 recorder 下的大路线方案 JSON显示名使用 JSON 内的 name。"""
result = []
for filename, path in list_recorder_json():
try:
profile = load_route_profile(path)
except Exception:
continue
display_name = str(profile.get("name") or os.path.splitext(filename)[0]).strip()
result.append((display_name or filename, path))
return result
def get_combat_loops_dir():
"""combat_loops 文件夹路径,不存在则创建"""
path = os.path.join(_config_base(), COMBAT_LOOPS_DIR)
@@ -93,10 +107,20 @@ class WaypointPreviewCanvas(QWidget):
ZOOM_MIN = 0.2
ZOOM_MAX = 10.0
ZOOM_STEP = 1.15
ROUTE_COLORS = [
QColor(0, 200, 255),
QColor(255, 214, 0),
QColor(70, 220, 120),
QColor(255, 105, 180),
QColor(255, 150, 60),
QColor(160, 130, 255),
QColor(90, 220, 210),
QColor(255, 100, 100),
]
def __init__(self, parent=None):
super().__init__(parent)
self.points = [] # [(x,y), ...],游戏坐标
self.routes = [] # [{"name": str, "points": [(x,y), ...]}, ...]
self.zoom_scale = 1.0 # 1.0 = 0~100 刚好铺满画布
self.pan_x = 0.0
self.pan_y = 0.0
@@ -106,7 +130,24 @@ class WaypointPreviewCanvas(QWidget):
def set_points(self, points):
"""设置要绘制的路径点列表。"""
self.points = [(float(p[0]), float(p[1])) for p in points] if points else []
self.set_routes([{"name": "巡逻路线", "points": points or []}])
def set_routes(self, routes):
"""设置多条巡逻路线,并用不同颜色绘制。"""
normalized = []
for idx, route in enumerate(routes or [], start=1):
raw_points = route.get("points") if isinstance(route, dict) else []
points = []
for point in raw_points or []:
try:
points.append((float(point[0]), float(point[1])))
except (TypeError, ValueError, IndexError):
continue
if not points:
continue
name = str(route.get("name") or f"巡逻路线{idx}").strip()
normalized.append({"name": name or f"巡逻路线{idx}", "points": points})
self.routes = normalized
self.update()
def reset_view(self):
@@ -171,35 +212,51 @@ class WaypointPreviewCanvas(QWidget):
x1, y1 = to_canvas((100, 100))
painter.drawRect(int(x0), int(y0), int(x1 - x0), int(y1 - y0))
if not self.points:
all_points = []
for route in self.routes:
all_points.extend(route["points"])
if not all_points:
painter.setPen(QPen(QColor(200, 200, 200), 1))
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, "无巡逻点数据")
return
# 绘制路径连线
painter.setPen(QPen(QColor(0, 200, 255), 2))
for i in range(len(self.points) - 1):
x1, y1 = to_canvas(self.points[i])
x2, y2 = to_canvas(self.points[i + 1])
painter.drawLine(int(x1), int(y1), int(x2), int(y2))
for idx, route in enumerate(self.routes):
color = self.ROUTE_COLORS[idx % len(self.ROUTE_COLORS)]
points = route["points"]
# 绘制所有巡逻点
painter.setPen(QPen(QColor(255, 255, 0), 1))
painter.setBrush(QColor(255, 255, 0))
for p in self.points:
px, py = to_canvas(p)
painter.drawEllipse(int(px) - 2, int(py) - 2, 4, 4)
painter.setPen(QPen(color, 2))
for i in range(len(points) - 1):
x1, y1 = to_canvas(points[i])
x2, y2 = to_canvas(points[i + 1])
painter.drawLine(int(x1), int(y1), int(x2), int(y2))
# 起点与终点标记(覆盖在普通点之上)
start_x, start_y = to_canvas(self.points[0])
end_x, end_y = to_canvas(self.points[-1])
painter.setPen(QPen(QColor(0, 255, 0), 2))
painter.setBrush(QColor(0, 255, 0))
painter.drawEllipse(int(start_x) - 4, int(start_y) - 4, 8, 8)
painter.setPen(QPen(color, 1))
painter.setBrush(color)
for p in points:
px, py = to_canvas(p)
painter.drawEllipse(int(px) - 2, int(py) - 2, 4, 4)
painter.setPen(QPen(QColor(255, 80, 80), 2))
painter.setBrush(QColor(255, 80, 80))
painter.drawEllipse(int(end_x) - 4, int(end_y) - 4, 8, 8)
start_x, start_y = to_canvas(points[0])
end_x, end_y = to_canvas(points[-1])
painter.setPen(QPen(QColor(245, 245, 245), 2))
painter.setBrush(color)
painter.drawEllipse(int(start_x) - 5, int(start_y) - 5, 10, 10)
painter.setPen(QPen(QColor(30, 30, 30), 2))
painter.setBrush(color)
painter.drawEllipse(int(end_x) - 5, int(end_y) - 5, 10, 10)
legend_x = rect.left() + 12
legend_y = rect.top() + 12
painter.setPen(QPen(QColor(230, 230, 230), 1))
for idx, route in enumerate(self.routes[:12]):
color = self.ROUTE_COLORS[idx % len(self.ROUTE_COLORS)]
y = legend_y + idx * 18
painter.setBrush(color)
painter.setPen(QPen(color, 1))
painter.drawRect(legend_x, y + 4, 10, 10)
painter.setPen(QPen(QColor(230, 230, 230), 1))
painter.drawText(legend_x + 16, y + 14, route["name"])
# ============ 多键控制 ============
@@ -302,6 +359,7 @@ class GameLoopWorker(QThread):
def __init__(
self,
mode,
route_profile_path=None,
waypoints_path=None,
prepare_route_path=None,
vendor_path=None,
@@ -342,6 +400,7 @@ class GameLoopWorker(QThread):
self.quest_follow_bot = None
self.flight_bot = None
self.recorder = None
self.route_profile_path = route_profile_path
self.waypoints_path = waypoints_path
self.prepare_route_path = prepare_route_path
self.vendor_path = vendor_path
@@ -417,6 +476,7 @@ class GameLoopWorker(QThread):
try:
from auto_bot_move import AutoBotMove, load_waypoints
self.bot_move = AutoBotMove(
route_profile_path=self.route_profile_path,
waypoints_path=self.waypoints_path,
prepare_route_path=self.prepare_route_path,
vendor_path=self.vendor_path,
@@ -441,6 +501,9 @@ class GameLoopWorker(QThread):
except ImportError as e:
self.log_signal.emit(f"❌ 巡逻打怪依赖加载失败: {e}")
return
except Exception as e:
self.log_signal.emit(f"❌ 路线方案加载失败: {e}")
return
if self.mode == 'combat':
try:
@@ -738,30 +801,14 @@ class WoWMultiKeyGUI(QMainWindow):
# 巡逻打怪配置(仅选择巡逻打怪模式时显示)
self.patrol_group = QGroupBox("巡逻打怪配置")
patrol_layout = QFormLayout(self.patrol_group)
self.waypoints_combo = QComboBox()
self.waypoints_combo.setMinimumWidth(200)
self.vendor_combo = QComboBox()
self.vendor_combo.setMinimumWidth(200)
self.mailbox_route_combo = QComboBox()
self.mailbox_route_combo.setMinimumWidth(200)
self.route_profile_combo = QComboBox()
self.route_profile_combo.setMinimumWidth(240)
self.patrol_attack_loop_combo = QComboBox()
self.patrol_attack_loop_combo.setMinimumWidth(200)
self.prepare_route_combo = QComboBox()
self.prepare_route_combo.setMinimumWidth(200)
self._refresh_recorder_combos()
refresh_btn = QPushButton("🔄 刷新列表")
refresh_btn.clicked.connect(self._refresh_recorder_combos)
patrol_layout.addRow("准备路线 JSON:", self.prepare_route_combo)
patrol_layout.addRow("巡逻点 JSON:", self.waypoints_combo)
patrol_layout.addRow("修理商 JSON:", self.vendor_combo)
patrol_layout.addRow("邮箱路线 JSON:", self.mailbox_route_combo)
self.resurrection_route_a_combo = QComboBox()
self.resurrection_route_a_combo.setMinimumWidth(200)
self.resurrection_route_b_combo = QComboBox()
self.resurrection_route_b_combo.setMinimumWidth(200)
self._refresh_recorder_combos()
patrol_layout.addRow("复活路线 A JSON:", self.resurrection_route_a_combo)
patrol_layout.addRow("复活路线 B JSON:", self.resurrection_route_b_combo)
patrol_layout.addRow("路线方案 JSON:", self.route_profile_combo)
patrol_layout.addRow("攻击循环:", self.patrol_attack_loop_combo)
patrol_layout.addRow("", refresh_btn)
self.patrol_group.setVisible(False)
@@ -930,7 +977,7 @@ class WoWMultiKeyGUI(QMainWindow):
self.preview_waypoints_combo.setMinimumWidth(220)
self._refresh_preview_waypoints()
self.preview_waypoints_combo.currentIndexChanged.connect(self._on_preview_waypoints_changed)
preview_file_layout.addRow("选择 JSON:", self.preview_waypoints_combo)
preview_file_layout.addRow("路线方案 JSON:", self.preview_waypoints_combo)
preview_layout.addWidget(preview_file_group)
self.preview_canvas = WaypointPreviewCanvas()
@@ -1570,35 +1617,34 @@ class WoWMultiKeyGUI(QMainWindow):
self._load_params_config()
def _refresh_preview_waypoints(self):
"""刷新巡逻点预览下拉列表"""
"""刷新路线方案预览下拉列表"""
self.preview_waypoints_combo.blockSignals(True)
self.preview_waypoints_combo.clear()
self.preview_waypoints_combo.addItem("-- 请选择 --", "")
for name, path in list_recorder_json():
for name, path in list_route_profile_json():
self.preview_waypoints_combo.addItem(name, path)
self.preview_waypoints_combo.blockSignals(False)
if self.preview_waypoints_combo.count() > 1:
self.preview_waypoints_combo.setCurrentIndex(1)
def _on_preview_waypoints_changed(self, index):
"""选择巡逻点 JSON 时加载并绘制在画布上"""
"""选择路线方案 JSON 时加载并绘制所有巡逻路线。"""
path = self.preview_waypoints_combo.currentData()
if not path or not os.path.exists(path):
self.preview_canvas.set_points([])
self.preview_canvas.set_routes([])
return
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
# 期望格式:[[x,y], ...] 或 [[x,y, ...], ...]
points = []
for item in data:
if isinstance(item, (list, tuple)) and len(item) >= 2:
points.append((float(item[0]), float(item[1])))
self.preview_canvas.set_points(points)
self.log(f"已加载巡逻点预览: {path} (共 {len(points)} 点)")
profile = load_route_profile(path)
routes = profile.get("patrol_routes") or []
total_points = sum(len(route.get("points") or []) for route in routes)
self.preview_canvas.set_routes(routes)
self.log(
f"已加载路线方案预览: {profile.get('name')} "
f"{len(routes)} 条巡逻路线,共 {total_points} 点)"
)
except Exception as e:
self.log(f"加载巡逻点预览失败: {e}")
self.preview_canvas.set_points([])
self.log(f"加载路线方案预览失败: {e}")
self.preview_canvas.set_routes([])
def _reset_preview_canvas(self):
"""重置巡逻点预览视图(缩放 + 平移)。"""
@@ -1606,51 +1652,19 @@ class WoWMultiKeyGUI(QMainWindow):
self.preview_canvas.reset_view()
def _refresh_recorder_combos(self):
"""刷新巡逻点、修理商、复活点路线下拉列表"""
items = list_recorder_json()
combos_with_default = [
(self.waypoints_combo, 'waypoints.json'),
(self.vendor_combo, 'vendor.json'),
]
"""刷新路线方案和其他 recorder 下拉列表"""
if hasattr(self, "route_profile_combo"):
self.route_profile_combo.blockSignals(True)
self.route_profile_combo.clear()
self.route_profile_combo.addItem("-- 请选择 --", "")
for name, path in list_route_profile_json():
self.route_profile_combo.addItem(name, path)
if self.route_profile_combo.count() > 1:
self.route_profile_combo.setCurrentIndex(1)
self.route_profile_combo.blockSignals(False)
if hasattr(self, "repair_vendor_combo"):
combos_with_default.append((self.repair_vendor_combo, 'vendor.json'))
if hasattr(self, "mailbox_route_combo"):
self.mailbox_route_combo.blockSignals(True)
self.mailbox_route_combo.clear()
self.mailbox_route_combo.addItem("-- 置空(不跑邮箱路线) --", "")
for name, path in items:
self.mailbox_route_combo.addItem(name, path)
idx = self.mailbox_route_combo.findData(os.path.join(get_recorder_dir(), 'mailbox.json'))
if idx >= 0:
self.mailbox_route_combo.setCurrentIndex(idx)
self.mailbox_route_combo.blockSignals(False)
for combo, default_name in combos_with_default:
combo.blockSignals(True)
combo.clear()
combo.addItem("-- 请选择 --", "")
for name, path in items:
combo.addItem(name, path)
idx = combo.findData(os.path.join(get_recorder_dir(), default_name))
if idx >= 0:
combo.setCurrentIndex(idx)
elif combo.count() > 1:
combo.setCurrentIndex(1)
combo.blockSignals(False)
# 复活路线下拉(无默认值,可置空)
for combo_name in (
"prepare_route_combo",
"resurrection_route_a_combo",
"resurrection_route_b_combo",
):
if hasattr(self, combo_name):
combo = getattr(self, combo_name)
combo.blockSignals(True)
combo.clear()
empty_text = "-- 置空(直接巡逻) --" if combo_name == "prepare_route_combo" else "-- 置空(直接跑尸体) --"
combo.addItem(empty_text, "")
for name, path in items:
combo.addItem(name, path)
combo.blockSignals(False)
self._refresh_repair_vendor_json_combo()
def _refresh_repair_vendor_json_combo(self):
"""刷新“回城修理配置”的修理商下拉框。"""
@@ -1785,6 +1799,7 @@ class WoWMultiKeyGUI(QMainWindow):
if mode is None:
QMessageBox.warning(self, "提示", "请先选择模式(状态监控 / 巡逻打怪 / 自动打怪 / 任务跟随 / 飞行模式 / 回城修理)")
return
route_profile_path = None
waypoints_path = None
prepare_route_path = None
vendor_path = None
@@ -1793,46 +1808,25 @@ class WoWMultiKeyGUI(QMainWindow):
resurrection_route_a_path = None
resurrection_route_b_path = None
if mode == 'patrol':
prep = self.prepare_route_combo.currentData() or ""
wp = self.waypoints_combo.currentData() or ""
vp = self.vendor_combo.currentData() or ""
mp = self.mailbox_route_combo.currentData() or ""
route_a = self.resurrection_route_a_combo.currentData() or ""
route_b = self.resurrection_route_b_combo.currentData() or ""
if not wp:
QMessageBox.warning(self, "提示", "巡逻打怪模式需选择巡逻点 JSON 文件")
profile_path = self.route_profile_combo.currentData() or ""
if not profile_path:
QMessageBox.warning(self, "提示", "巡逻打怪模式需选择路线方案 JSON 文件")
return
if not vp:
QMessageBox.warning(self, "提示", "巡逻打怪模式需选择修理商 JSON 文件")
if not os.path.exists(profile_path):
QMessageBox.warning(self, "提示", f"路线方案文件不存在: {profile_path}")
return
if not os.path.exists(wp):
QMessageBox.warning(self, "提示", f"巡逻点文件不存在: {wp}")
try:
profile = load_route_profile(profile_path)
except Exception as e:
QMessageBox.warning(self, "提示", f"路线方案 JSON 无效: {e}")
return
if not os.path.exists(vp):
QMessageBox.warning(self, "提示", f"修理商文件不存在: {vp}")
if not profile.get("vendor_route"):
QMessageBox.warning(self, "提示", "路线方案需包含 vendor_route 修理商路线")
return
if prep and not os.path.exists(prep):
QMessageBox.warning(self, "提示", f"准备路线文件不存在: {prep}")
if self.gs_enable_bag_full_mail.isChecked() and not profile.get("mailbox_route"):
QMessageBox.warning(self, "提示", "已启用包满邮寄,路线方案需包含 mailbox_route 邮箱路线")
return
if self.gs_enable_bag_full_mail.isChecked():
if not mp:
QMessageBox.warning(self, "提示", "已启用包满邮寄,请选择邮箱路线 JSON 文件")
return
if not os.path.exists(mp):
QMessageBox.warning(self, "提示", f"邮箱路线文件不存在: {mp}")
return
if route_a and not os.path.exists(route_a):
QMessageBox.warning(self, "提示", f"复活路线 A 文件不存在: {route_a}")
return
if route_b and not os.path.exists(route_b):
QMessageBox.warning(self, "提示", f"复活路线 B 文件不存在: {route_b}")
return
waypoints_path = wp
prepare_route_path = prep or None
vendor_path = vp
mailbox_route_path = mp or None
resurrection_route_a_path = route_a or None
resurrection_route_b_path = route_b or None
route_profile_path = profile_path
attack_loop_path = None
if mode == 'patrol':
attack_loop_path = self.patrol_attack_loop_combo.currentData() or None
@@ -1916,7 +1910,8 @@ class WoWMultiKeyGUI(QMainWindow):
use_ghost_box = self.gs_use_ghost_box.isChecked()
self.game_worker = GameLoopWorker(
mode, waypoints_path=waypoints_path, prepare_route_path=prepare_route_path, vendor_path=vendor_path,
mode, route_profile_path=route_profile_path,
waypoints_path=waypoints_path, prepare_route_path=prepare_route_path, vendor_path=vendor_path,
mailbox_route_path=mailbox_route_path,
attack_loop_path=attack_loop_path,
skinning_wait_sec=skinning_wait_sec,