add 脱战血量低就地吃面包

This commit is contained in:
王鹏
2026-03-25 10:51:25 +08:00
parent 9b4a9d5ab8
commit 9ab1e8737c
7 changed files with 174 additions and 2 deletions

View File

@@ -14,6 +14,9 @@ from logistics_manager import LogisticsManager
KEY_TAB = '3' KEY_TAB = '3'
KEY_LOOT = '4' # 假设你在游戏里设置了互动按键为 F KEY_LOOT = '4' # 假设你在游戏里设置了互动按键为 F
KEY_ATTACK = '2' # 假设你的主攻击技能是 1 KEY_ATTACK = '2' # 假设你的主攻击技能是 1
DEFAULT_FOOD_KEY = 'f1' # 面包快捷键
DEFAULT_EAT_HP_THRESHOLD = 30
DEFAULT_EAT_MAX_WAIT_SEC = 30.0
# 巡逻点配置文件 # 巡逻点配置文件
WAYPOINTS_FILE = 'waypoints.json' WAYPOINTS_FILE = 'waypoints.json'
@@ -79,7 +82,18 @@ def load_attack_loop(path):
class AutoBotMove: 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.last_tab_time = 0
self.is_running = True self.is_running = True
self.is_moving = False self.is_moving = False
@@ -87,7 +101,14 @@ class AutoBotMove:
# 记录上一帧是否处于战斗/有目标,用于检测“刚刚脱战”的瞬间 # 记录上一帧是否处于战斗/有目标,用于检测“刚刚脱战”的瞬间
self._was_in_combat_or_target = False 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.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.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: if waypoints is None:
path = waypoints_path or get_config_path(WAYPOINTS_FILE) path = waypoints_path or get_config_path(WAYPOINTS_FILE)
waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS
@@ -102,6 +123,12 @@ class AutoBotMove:
vendor_file = vendor_path or get_config_path('vendor.json') vendor_file = vendor_path or get_config_path('vendor.json')
self.logistics_manager = LogisticsManager(vendor_file) 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): def execute_disengage_loot(self):
"""从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。""" """从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。"""
try: try:
@@ -140,8 +167,39 @@ class AutoBotMove:
if random.random() < 0.3: if random.random() < 0.3:
pydirectinput.press(KEY_ATTACK) 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): 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) 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) 跑尸 # 1. 死亡状态:尸体(1) 记录坐标并释放灵魂;灵魂(2) 跑尸
if death == 1: if death == 1:
self.patrol_controller.stop_all() self.patrol_controller.stop_all()
@@ -160,12 +218,16 @@ class AutoBotMove:
self.patrol_controller.stop_all() self.patrol_controller.stop_all()
self.is_moving = False self.is_moving = False
self.patrol_controller.reset_stuck() 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 return
# 3. 战斗/有目标:停止移动,执行攻击逻辑;仅在「跑向怪」短窗口内做卡死检测 # 3. 战斗/有目标:停止移动,执行攻击逻辑;仅在「跑向怪」短窗口内做卡死检测
in_combat_or_target = bool(state['combat'] or state['target']) in_combat_or_target = bool(state['combat'] or state['target'])
if in_combat_or_target: if in_combat_or_target:
# 被动进战时立即打断进食等待,转入正常战斗流程
self._eating_started_at = None
if self.target_acquired_time is None: if self.target_acquired_time is None:
self.target_acquired_time = time.time() self.target_acquired_time = time.time()
if self.is_moving: if self.is_moving:
@@ -206,6 +268,18 @@ class AutoBotMove:
pass pass
self.target_acquired_time = None # 脱战/无目标时清零,下次获得目标再计时 self.target_acquired_time = None # 脱战/无目标时清零,下次获得目标再计时
self._was_in_combat_or_target = False 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 内) # 4. 没战斗没目标:巡逻(卡死检测在 patrol_controller.navigate 内)
self.is_moving = True self.is_moving = True
self.patrol_controller.navigate(state) self.patrol_controller.navigate(state)

View File

@@ -56,11 +56,16 @@ class CoordinatePatrol:
已上马返回 True。 已上马返回 True。
未上马则松开移动键、按住上马键 MOUNT_HOLD_SEC本帧不走路返回 False。 未上马则松开移动键、按住上马键 MOUNT_HOLD_SEC本帧不走路返回 False。
state 无 mounted 字段时视为无法判断,不拦巡逻(兼容未开 LogicBeacon state 无 mounted 字段时视为无法判断,不拦巡逻(兼容未开 LogicBeacon
尸体/灵魂不按上马键,但返回 True 以便寻路继续(跑尸时 navigate_to_point 依赖此项)。
""" """
if "mounted" not in state: if "mounted" not in state:
return True return True
if state.get("mounted"): if state.get("mounted"):
return True return True
# game_state.death_state: 0 存活 / 1 尸体 / 2 灵魂 — 幽灵无法/不需上马,不得拦走路
ds = state.get("death_state")
if ds in (1, 2):
return True
self.stop_all() self.stop_all()
now = time.time() now = time.time()
if now < self._next_mount_allowed: if now < self._next_mount_allowed:

View File

@@ -8,9 +8,19 @@ class DeathManager:
self.corpse_pos = None self.corpse_pos = None
self.patrol_system = patrol_system self.patrol_system = patrol_system
self.is_running_to_corpse = False 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): def on_death(self, state):
"""1. 死亡瞬间调用:从 player_position 获取坐标并记录""" """1. 死亡瞬间调用:从 player_position 获取坐标并记录"""
if self._spirit_release_sent:
return
self._spirit_release_sent = True
self.corpse_pos = (state['x'], state['y']) self.corpse_pos = (state['x'], state['y'])
self.is_running_to_corpse = True self.is_running_to_corpse = True
print(f">>> [系统] 记录死亡坐标: {self.corpse_pos},准备释放灵魂...") print(f">>> [系统] 记录死亡坐标: {self.corpse_pos},准备释放灵魂...")

View File

@@ -31,3 +31,22 @@
- **GUI 更新**:在 `wow_multikey_gui.py` 的「飞行模式配置」页,「降落按键」默认值与占位提示从 `x` 改为 `p` - **GUI 更新**:在 `wow_multikey_gui.py` 的「飞行模式配置」页,「降落按键」默认值与占位提示从 `x` 改为 `p`
- **行为更新**`flight_mode.py``FlightModeBot` 的默认 `land_key``x` 改为 `p`,保证未手动填写/未在配置中存在时仍使用 `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` 保留默认值,旧配置文件可直接运行。

View File

@@ -305,6 +305,9 @@ class GameLoopWorker(QThread):
record_min_distance=None, record_min_distance=None,
attack_loop_path=None, attack_loop_path=None,
skinning_wait_sec=None, skinning_wait_sec=None,
food_key=None,
eat_hp_threshold=None,
eat_max_wait_sec=None,
quest_follow_follow_key=None, quest_follow_follow_key=None,
quest_follow_interact_key=None, quest_follow_interact_key=None,
quest_follow_interval_sec=None, quest_follow_interval_sec=None,
@@ -328,6 +331,15 @@ class GameLoopWorker(QThread):
self.record_min_distance = record_min_distance self.record_min_distance = record_min_distance
self.attack_loop_path = attack_loop_path or None self.attack_loop_path = attack_loop_path or None
self.skinning_wait_sec = skinning_wait_sec 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_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" self.quest_follow_interact_key = (quest_follow_interact_key or "4").strip().lower() or "4"
try: try:
@@ -355,6 +367,10 @@ class GameLoopWorker(QThread):
vendor_path=self.vendor_path, vendor_path=self.vendor_path,
attack_loop_path=self.attack_loop_path, attack_loop_path=self.attack_loop_path,
skinning_wait_sec=self.skinning_wait_sec, 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: except ImportError as e:
self.log_signal.emit(f"❌ 巡逻打怪依赖加载失败: {e}") self.log_signal.emit(f"❌ 巡逻打怪依赖加载失败: {e}")
@@ -825,6 +841,23 @@ class WoWMultiKeyGUI(QMainWindow):
self.skinning_wait_spin.setSuffix("") self.skinning_wait_spin.setSuffix("")
params_right.addRow("剥皮等待时间:", self.skinning_wait_spin) 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 = QLineEdit()
self.gs_mount_key.setPlaceholderText("如 x") self.gs_mount_key.setPlaceholderText("如 x")
self.gs_mount_key.setMaxLength(16) self.gs_mount_key.setMaxLength(16)
@@ -886,8 +919,14 @@ class WoWMultiKeyGUI(QMainWindow):
bot_cfg = (self.config or {}).get('bot') or {} bot_cfg = (self.config or {}).get('bot') or {}
v = bot_cfg.get('skinning_wait_sec', 1.5) v = bot_cfg.get('skinning_wait_sec', 1.5)
self.skinning_wait_spin.setValue(float(v)) 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: except Exception:
self.skinning_wait_spin.setValue(1.5) 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): def _save_params_config(self):
"""保存「参数配置」界面到 game_state_config.json多分辨率并写入 wow_multikey_qt.jsonbot 参数)""" """保存「参数配置」界面到 game_state_config.json多分辨率并写入 wow_multikey_qt.jsonbot 参数)"""
@@ -908,6 +947,9 @@ class WoWMultiKeyGUI(QMainWindow):
self.config = self.config or {} self.config = self.config or {}
self.config.setdefault('bot', {}) self.config.setdefault('bot', {})
self.config['bot']['skinning_wait_sec'] = float(self.skinning_wait_spin.value()) 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._save_main_config()
self.log(f"✅ 参数配置已保存至 {path},并更新 bot 参数") 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)) skinning_wait_sec = float(((self.config or {}).get('bot') or {}).get('skinning_wait_sec', 1.5))
except Exception: except Exception:
skinning_wait_sec = 1.5 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( self.game_worker = GameLoopWorker(
mode, waypoints_path=waypoints_path, vendor_path=vendor_path, mode, waypoints_path=waypoints_path, vendor_path=vendor_path,
attack_loop_path=attack_loop_path, attack_loop_path=attack_loop_path,
skinning_wait_sec=skinning_wait_sec, 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_follow_key=self.quest_follow_follow_edit.text(),
quest_follow_interact_key=self.quest_follow_interact_edit.text(), quest_follow_interact_key=self.quest_follow_interact_edit.text(),
quest_follow_interval_sec=self.quest_follow_interval_spin.value(), quest_follow_interval_sec=self.quest_follow_interval_spin.value(),
@@ -1355,6 +1412,13 @@ class WoWMultiKeyGUI(QMainWindow):
"""统一停止游戏循环,更新所有相关 UI""" """统一停止游戏循环,更新所有相关 UI"""
if self.game_worker: if self.game_worker:
self.game_worker.running = False 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线程会在下一轮循环自然退出 # 不在这里 wait(),避免阻塞 GUI线程会在下一轮循环自然退出
self.game_start_btn.setEnabled(True) self.game_start_btn.setEnabled(True)
self.game_stop_btn.setEnabled(False) self.game_stop_btn.setEnabled(False)