Add route profile patrol selection
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user