From 9ab1e8737c24290f7f279cfe989971b70666d31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Wed, 25 Mar 2026 10:51:25 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E8=84=B1=E6=88=98=E8=A1=80=E9=87=8F?= =?UTF-8?q?=E4=BD=8E=E5=B0=B1=E5=9C=B0=E5=90=83=E9=9D=A2=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/auto_bot_move.cpython-311.pyc | Bin 13956 -> 15016 bytes __pycache__/wow_multikey_gui.cpython-311.pyc | Bin 102328 -> 102905 bytes auto_bot_move.py | 78 ++++++++++++++++++- coordinate_patrol.py | 5 ++ death_manager.py | 10 +++ docs/history.md | 19 +++++ wow_multikey_gui.py | 64 +++++++++++++++ 7 files changed, 174 insertions(+), 2 deletions(-) diff --git a/__pycache__/auto_bot_move.cpython-311.pyc b/__pycache__/auto_bot_move.cpython-311.pyc index 656a55bbd61ab5654a6672749ded1089ddd23d42..ea113bd0da98ad672a3fede900169a47b9180bdb 100644 GIT binary patch delta 3051 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 Z8u#f4WbNtrr^bJ@Ryt%z%k3??e*p4o0!07- diff --git a/__pycache__/wow_multikey_gui.cpython-311.pyc b/__pycache__/wow_multikey_gui.cpython-311.pyc index 44049fb3198a26ca5a8ba2c7e7fa7efb631743bb..0e468354907ff0e7ecc61eac45fbeee4611cf564 100644 GIT binary patch delta 5880 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 SM!&85vO4ObTI}5ovHuGX3$KL$ delta 5279 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 RTRPq`C7(0N4|h`Pe*owR7Ek~H 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)