diff --git a/__pycache__/auto_bot_move.cpython-311.pyc b/__pycache__/auto_bot_move.cpython-311.pyc index 656a55b..ea113bd 100644 Binary files a/__pycache__/auto_bot_move.cpython-311.pyc and b/__pycache__/auto_bot_move.cpython-311.pyc differ diff --git a/__pycache__/wow_multikey_gui.cpython-311.pyc b/__pycache__/wow_multikey_gui.cpython-311.pyc index 44049fb..0e46835 100644 Binary files a/__pycache__/wow_multikey_gui.cpython-311.pyc and b/__pycache__/wow_multikey_gui.cpython-311.pyc differ diff --git a/auto_bot_move.py b/auto_bot_move.py index 6ca2d19..2ef3bea 100644 --- a/auto_bot_move.py +++ b/auto_bot_move.py @@ -14,6 +14,9 @@ from logistics_manager import LogisticsManager KEY_TAB = '3' KEY_LOOT = '4' # 假设你在游戏里设置了互动按键为 F KEY_ATTACK = '2' # 假设你的主攻击技能是 1 +DEFAULT_FOOD_KEY = 'f1' # 面包快捷键 +DEFAULT_EAT_HP_THRESHOLD = 30 +DEFAULT_EAT_MAX_WAIT_SEC = 30.0 # 巡逻点配置文件 WAYPOINTS_FILE = 'waypoints.json' @@ -79,7 +82,18 @@ def load_attack_loop(path): class AutoBotMove: - def __init__(self, waypoints=None, waypoints_path=None, vendor_path=None, attack_loop_path=None, skinning_wait_sec=None): + def __init__( + self, + waypoints=None, + waypoints_path=None, + vendor_path=None, + attack_loop_path=None, + skinning_wait_sec=None, + food_key=None, + eat_hp_threshold=None, + eat_max_wait_sec=None, + stop_check=None, + ): self.last_tab_time = 0 self.is_running = True self.is_moving = False @@ -87,7 +101,14 @@ class AutoBotMove: # 记录上一帧是否处于战斗/有目标,用于检测“刚刚脱战”的瞬间 self._was_in_combat_or_target = False self.skinning_wait_sec = float(skinning_wait_sec) if skinning_wait_sec is not None else 1.5 + self.food_key = (food_key or DEFAULT_FOOD_KEY).strip().lower() or DEFAULT_FOOD_KEY + self.eat_hp_threshold = int(eat_hp_threshold) if eat_hp_threshold is not None else DEFAULT_EAT_HP_THRESHOLD + self.eat_max_wait_sec = float(eat_max_wait_sec) if eat_max_wait_sec is not None else DEFAULT_EAT_MAX_WAIT_SEC self.attack_loop_config = load_attack_loop(attack_loop_path) + self._prev_death_state = 0 + self._eating_started_at = None + # stop_check: 返回 True 表示需要立即停止(用于中断阻塞中的后勤/路线导航) + self._stop_check = stop_check if callable(stop_check) else (lambda: False) if waypoints is None: path = waypoints_path or get_config_path(WAYPOINTS_FILE) waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS @@ -102,6 +123,12 @@ class AutoBotMove: vendor_file = vendor_path or get_config_path('vendor.json') self.logistics_manager = LogisticsManager(vendor_file) + def _should_stop(self) -> bool: + try: + return bool(self._stop_check()) + except Exception: + return False + def execute_disengage_loot(self): """从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。""" try: @@ -140,8 +167,39 @@ class AutoBotMove: if random.random() < 0.3: pydirectinput.press(KEY_ATTACK) + def _start_eating(self): + """开始就地吃面包恢复。""" + self._eating_started_at = time.time() + pydirectinput.press(self.food_key) + + def _should_keep_eating(self, state) -> bool: + """ + 返回 True 表示继续等待回血(暂停巡逻与找怪); + 返回 False 表示结束进食等待。 + """ + if self._eating_started_at is None: + return False + hp = state.get('hp') + if hp is not None and hp >= 100: + self._eating_started_at = None + return False + if (time.time() - self._eating_started_at) >= self.eat_max_wait_sec: + self._eating_started_at = None + return False + return True + def execute_logic(self, state): + if self._should_stop(): + # 在停止按钮点击后,确保马上松开移动键,并避免继续执行后勤交互。 + self.patrol_controller.stop_all() + self.logistics_manager.is_returning = False + self.is_moving = False + return + death = state.get('death_state', 0) + if self._prev_death_state in (1, 2) and death == 0: + self.death_manager.reset_when_alive() + self._prev_death_state = death # 1. 死亡状态:尸体(1) 记录坐标并释放灵魂;灵魂(2) 跑尸 if death == 1: self.patrol_controller.stop_all() @@ -160,12 +218,16 @@ class AutoBotMove: self.patrol_controller.stop_all() self.is_moving = False self.patrol_controller.reset_stuck() - self.logistics_manager.run_route1_round(parse_game_state, self.patrol_controller) + # 中断策略:一旦 GUI 停止,后续 get_state 返回 None,使 navigate_path 立即退出。 + get_state = (lambda: None if self._should_stop() else parse_game_state()) + self.logistics_manager.run_route1_round(get_state, self.patrol_controller) return # 3. 战斗/有目标:停止移动,执行攻击逻辑;仅在「跑向怪」短窗口内做卡死检测 in_combat_or_target = bool(state['combat'] or state['target']) if in_combat_or_target: + # 被动进战时立即打断进食等待,转入正常战斗流程 + self._eating_started_at = None if self.target_acquired_time is None: self.target_acquired_time = time.time() if self.is_moving: @@ -206,6 +268,18 @@ class AutoBotMove: pass self.target_acquired_time = None # 脱战/无目标时清零,下次获得目标再计时 self._was_in_combat_or_target = False + + # 4. 脱战低血量:就地吃面包(最多等待 30 秒或回满) + hp = state.get('hp') + if self._eating_started_at is None and hp is not None and hp < self.eat_hp_threshold: + if self.is_moving: + self.patrol_controller.stop_all() + self.is_moving = False + self._start_eating() + if self._should_keep_eating(state): + # 进食期间不巡逻、不主动找目标(不按 Tab) + return + # 4. 没战斗没目标:巡逻(卡死检测在 patrol_controller.navigate 内) self.is_moving = True self.patrol_controller.navigate(state) diff --git a/coordinate_patrol.py b/coordinate_patrol.py index 99e8aba..2bd9b6a 100644 --- a/coordinate_patrol.py +++ b/coordinate_patrol.py @@ -56,11 +56,16 @@ class CoordinatePatrol: 已上马返回 True。 未上马则松开移动键、按住上马键 MOUNT_HOLD_SEC,本帧不走路;返回 False。 state 无 mounted 字段时视为无法判断,不拦巡逻(兼容未开 LogicBeacon)。 + 尸体/灵魂不按上马键,但返回 True 以便寻路继续(跑尸时 navigate_to_point 依赖此项)。 """ if "mounted" not in state: return True if state.get("mounted"): return True + # game_state.death_state: 0 存活 / 1 尸体 / 2 灵魂 — 幽灵无法/不需上马,不得拦走路 + ds = state.get("death_state") + if ds in (1, 2): + return True self.stop_all() now = time.time() if now < self._next_mount_allowed: diff --git a/death_manager.py b/death_manager.py index 1cfedb6..f8abd1b 100644 --- a/death_manager.py +++ b/death_manager.py @@ -8,9 +8,19 @@ class DeathManager: self.corpse_pos = None self.patrol_system = patrol_system self.is_running_to_corpse = False + self._spirit_release_sent = False + + def reset_when_alive(self): + """存活时清标志,避免下次死亡无法再次释放灵魂/记录尸体坐标。""" + self._spirit_release_sent = False + self.corpse_pos = None + self.is_running_to_corpse = False def on_death(self, state): """1. 死亡瞬间调用:从 player_position 获取坐标并记录""" + if self._spirit_release_sent: + return + self._spirit_release_sent = True self.corpse_pos = (state['x'], state['y']) self.is_running_to_corpse = True print(f">>> [系统] 记录死亡坐标: {self.corpse_pos},准备释放灵魂...") diff --git a/docs/history.md b/docs/history.md index fa5e8d3..f65abde 100644 --- a/docs/history.md +++ b/docs/history.md @@ -31,3 +31,22 @@ - **GUI 更新**:在 `wow_multikey_gui.py` 的「飞行模式配置」页,「降落按键」默认值与占位提示从 `x` 改为 `p`。 - **行为更新**:`flight_mode.py` 中 `FlightModeBot` 的默认 `land_key` 从 `x` 改为 `p`,保证未手动填写/未在配置中存在时仍使用 `p`。 +## 2026-03-25 + +### 脱战低血就地吃面包 + +- **新增逻辑**:`auto_bot_move.py` 在脱战状态下,当 `hp < 30%` 时按一次面包键开始恢复。 +- **等待规则**:进入恢复后,最多等待 30 秒;若血量先到 `100%` 则提前结束。 +- **行为约束**:恢复期间不巡逻、不主动按 `Tab` 寻找目标。 +- **战斗中断**:若恢复过程中被动进入战斗(`combat/target` 变为真),立即中断恢复并进入正常战斗逻辑。 + +### 吃面包参数接入 GUI 配置 + +- **参数配置页新增**:`wow_multikey_gui.py` 的「参数配置」页新增: + - `吃面包按键`(`food_key`,默认 `f1`) + - `吃面包血量阈值`(`eat_hp_threshold`,默认 `30`) + - `吃面包最长等待`(`eat_max_wait_sec`,默认 `30.0` 秒) +- **配置持久化**:上述参数保存到 `wow_multikey_qt.json` 的 `bot` 节点。 +- **运行时透传**:`start_game_loop` → `GameLoopWorker` → `AutoBotMove` 全链路透传并生效。 +- **兼容性**:`AutoBotMove` 保留默认值,旧配置文件可直接运行。 + diff --git a/wow_multikey_gui.py b/wow_multikey_gui.py index 63b176a..dfea01d 100644 --- a/wow_multikey_gui.py +++ b/wow_multikey_gui.py @@ -305,6 +305,9 @@ class GameLoopWorker(QThread): record_min_distance=None, attack_loop_path=None, skinning_wait_sec=None, + food_key=None, + eat_hp_threshold=None, + eat_max_wait_sec=None, quest_follow_follow_key=None, quest_follow_interact_key=None, quest_follow_interval_sec=None, @@ -328,6 +331,15 @@ class GameLoopWorker(QThread): self.record_min_distance = record_min_distance self.attack_loop_path = attack_loop_path or None self.skinning_wait_sec = skinning_wait_sec + self.food_key = (food_key or "f1").strip().lower() or "f1" + try: + self.eat_hp_threshold = int(eat_hp_threshold) + except (TypeError, ValueError): + self.eat_hp_threshold = 30 + try: + self.eat_max_wait_sec = float(eat_max_wait_sec) + except (TypeError, ValueError): + self.eat_max_wait_sec = 30.0 self.quest_follow_follow_key = (quest_follow_follow_key or "f").strip().lower() or "f" self.quest_follow_interact_key = (quest_follow_interact_key or "4").strip().lower() or "4" try: @@ -355,6 +367,10 @@ class GameLoopWorker(QThread): vendor_path=self.vendor_path, attack_loop_path=self.attack_loop_path, skinning_wait_sec=self.skinning_wait_sec, + food_key=self.food_key, + eat_hp_threshold=self.eat_hp_threshold, + eat_max_wait_sec=self.eat_max_wait_sec, + stop_check=lambda: not self.running, ) except ImportError as e: self.log_signal.emit(f"❌ 巡逻打怪依赖加载失败: {e}") @@ -825,6 +841,23 @@ class WoWMultiKeyGUI(QMainWindow): self.skinning_wait_spin.setSuffix(" 秒") params_right.addRow("剥皮等待时间:", self.skinning_wait_spin) + self.food_key_edit = QLineEdit() + self.food_key_edit.setPlaceholderText("如 f1") + self.food_key_edit.setMaxLength(16) + self.food_key_edit.setText("f1") + self.eat_hp_threshold_spin = QSpinBox() + self.eat_hp_threshold_spin.setRange(1, 100) + self.eat_hp_threshold_spin.setValue(30) + self.eat_hp_threshold_spin.setSuffix(" %") + self.eat_max_wait_spin = QDoubleSpinBox() + self.eat_max_wait_spin.setRange(1.0, 120.0) + self.eat_max_wait_spin.setSingleStep(1.0) + self.eat_max_wait_spin.setValue(30.0) + self.eat_max_wait_spin.setSuffix(" 秒") + params_right.addRow("吃面包按键:", self.food_key_edit) + params_right.addRow("吃面包血量阈值:", self.eat_hp_threshold_spin) + params_right.addRow("吃面包最长等待:", self.eat_max_wait_spin) + self.gs_mount_key = QLineEdit() self.gs_mount_key.setPlaceholderText("如 x") self.gs_mount_key.setMaxLength(16) @@ -886,8 +919,14 @@ class WoWMultiKeyGUI(QMainWindow): bot_cfg = (self.config or {}).get('bot') or {} v = bot_cfg.get('skinning_wait_sec', 1.5) self.skinning_wait_spin.setValue(float(v)) + self.food_key_edit.setText(str(bot_cfg.get('food_key', 'f1')).strip() or 'f1') + self.eat_hp_threshold_spin.setValue(int(bot_cfg.get('eat_hp_threshold', 30))) + self.eat_max_wait_spin.setValue(float(bot_cfg.get('eat_max_wait_sec', 30.0))) except Exception: self.skinning_wait_spin.setValue(1.5) + self.food_key_edit.setText('f1') + self.eat_hp_threshold_spin.setValue(30) + self.eat_max_wait_spin.setValue(30.0) def _save_params_config(self): """保存「参数配置」界面到 game_state_config.json(多分辨率)并写入 wow_multikey_qt.json(bot 参数)""" @@ -908,6 +947,9 @@ class WoWMultiKeyGUI(QMainWindow): self.config = self.config or {} self.config.setdefault('bot', {}) self.config['bot']['skinning_wait_sec'] = float(self.skinning_wait_spin.value()) + self.config['bot']['food_key'] = self.food_key_edit.text().strip() or 'f1' + self.config['bot']['eat_hp_threshold'] = int(self.eat_hp_threshold_spin.value()) + self.config['bot']['eat_max_wait_sec'] = float(self.eat_max_wait_spin.value()) self._save_main_config() self.log(f"✅ 参数配置已保存至 {path},并更新 bot 参数") @@ -1283,11 +1325,26 @@ class WoWMultiKeyGUI(QMainWindow): skinning_wait_sec = float(((self.config or {}).get('bot') or {}).get('skinning_wait_sec', 1.5)) except Exception: skinning_wait_sec = 1.5 + try: + food_key = str(((self.config or {}).get('bot') or {}).get('food_key', 'f1')).strip() or 'f1' + except Exception: + food_key = 'f1' + try: + eat_hp_threshold = int(((self.config or {}).get('bot') or {}).get('eat_hp_threshold', 30)) + except Exception: + eat_hp_threshold = 30 + try: + eat_max_wait_sec = float(((self.config or {}).get('bot') or {}).get('eat_max_wait_sec', 30.0)) + except Exception: + eat_max_wait_sec = 30.0 self.game_worker = GameLoopWorker( mode, waypoints_path=waypoints_path, vendor_path=vendor_path, attack_loop_path=attack_loop_path, skinning_wait_sec=skinning_wait_sec, + food_key=food_key, + eat_hp_threshold=eat_hp_threshold, + eat_max_wait_sec=eat_max_wait_sec, quest_follow_follow_key=self.quest_follow_follow_edit.text(), quest_follow_interact_key=self.quest_follow_interact_edit.text(), quest_follow_interval_sec=self.quest_follow_interval_spin.value(), @@ -1355,6 +1412,13 @@ class WoWMultiKeyGUI(QMainWindow): """统一停止游戏循环,更新所有相关 UI""" if self.game_worker: self.game_worker.running = False + # 额外释放一次移动按键,避免刚好卡在阻塞逻辑里导致按键滞留。 + try: + if getattr(self.game_worker, "bot_move", None): + self.game_worker.bot_move.patrol_controller.stop_all() + self.game_worker.bot_move.logistics_manager.is_returning = False + except Exception: + pass # 不在这里 wait(),避免阻塞 GUI;线程会在下一轮循环自然退出 self.game_start_btn.setEnabled(True) self.game_stop_btn.setEnabled(False)