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

@@ -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,