first commit

This commit is contained in:
王鹏
2026-03-18 09:04:37 +08:00
commit b7719b377d
121 changed files with 116104 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

158
auto_bot.py Normal file
View File

@@ -0,0 +1,158 @@
import json
import os
import random
import time
import pydirectinput
from game_state import parse_game_state
from stuck_handler import StuckHandler
# 定义按键常量
KEY_TAB = '3'
KEY_LOOT = '4' # 假设你在游戏里设置了互动按键为 F
KEY_ATTACK = '2' # 假设你的主攻击技能是 1
def load_attack_loop(path):
"""从 JSON 加载攻击循环配置。支持 hp_below_percent/hp_key、mp_below_percent/mp_key0=不启用)"""
if not path or not os.path.exists(path):
return None
try:
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
steps = cfg.get('steps') or []
trigger = float(cfg.get('trigger_chance', 0.3))
hp_below = int(cfg.get('hp_below_percent', 0)) or 0
mp_below = int(cfg.get('mp_below_percent', 0)) or 0
return {
'trigger_chance': trigger,
'steps': steps,
'hp_below_percent': hp_below,
'hp_key': (cfg.get('hp_key') or '').strip() or None,
'mp_below_percent': mp_below,
'mp_key': (cfg.get('mp_key') or '').strip() or None,
}
except Exception:
return None
class AutoBot:
def __init__(self, waypoints=None, attack_loop_path=None, skinning_wait_sec=None):
self.last_tab_time = 0
self.is_running = True
self.attack_loop_config = load_attack_loop(attack_loop_path)
self.tab_no_target_count = 0 # 连续按 Tab 仍无目标的次数,满 5 次则转向
self.stuck_handler = StuckHandler()
self.target_acquired_time = None # 本次获得目标的时间(目前仅用于调试/保留接口)
self.last_turn_end_time = 0 # 最近一次结束 A/D 转向的时间,供卡死检测排除
self.skinning_wait_sec = float(skinning_wait_sec) if skinning_wait_sec is not None else 1.5
def execute_disengage_loot(self):
"""从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。"""
try:
# 拾取
pydirectinput.press(KEY_LOOT)
time.sleep(0.5)
# 剥皮
pydirectinput.press(KEY_LOOT)
time.sleep(self.skinning_wait_sec)
except Exception:
pass
def execute_combat_logic(self, state):
if self.attack_loop_config:
cfg = self.attack_loop_config
if random.random() >= cfg['trigger_chance']:
return
hp = state.get('hp')
if hp is not None and cfg.get('hp_below_percent') and cfg.get('hp_key') and hp < cfg['hp_below_percent']:
pydirectinput.press(cfg['hp_key'])
time.sleep(0.2)
mp = state.get('mp')
if mp is not None and cfg.get('mp_below_percent') and cfg.get('mp_key') and mp < cfg['mp_below_percent']:
pydirectinput.press(cfg['mp_key'])
time.sleep(0.2)
# 每次攻击前选中目标
pydirectinput.press(KEY_LOOT)
for step in cfg['steps']:
key = step.get('key') or KEY_ATTACK
delay = float(step.get('delay', 0.5))
pydirectinput.press(key)
time.sleep(delay)
return
# 默认:模拟手动按键的节奏
if random.random() < 0.3:
pydirectinput.press(KEY_ATTACK)
time.sleep(0.5)
def execute_logic(self, state):
current_time = time.time()
# --- 场景 1没有目标按 Tab 选怪;连续 5 次无目标则随机左/右转再重试 ---
if not state['target']:
self.execute_disengage_loot()
self.target_acquired_time = None # 脱战/无目标时清零
self.stuck_handler.reset() # 避免脱战期间保留上次站桩的计时,导致重新选到目标立刻误判卡死
self.tab_no_target_count = min(self.tab_no_target_count, 5)
if self.tab_no_target_count >= 5:
# 随机左转或右转一段时间,再重新获取目标
turn_key = random.choice(["a", "d"]) # a=左转, d=右转
pydirectinput.keyDown(turn_key)
time.sleep(random.uniform(0.3, 0.6))
pydirectinput.keyUp(turn_key)
self.tab_no_target_count = 0
self.last_tab_time = current_time
self.last_turn_end_time = current_time # 供卡死检测排除刚转向的一小段时间
elif current_time - self.last_tab_time > random.uniform(0.5, 1.2):
pydirectinput.press(KEY_TAB)
self.last_tab_time = current_time
self.tab_no_target_count += 1
else:
self.tab_no_target_count = 0 # 有目标时清零
# 跑向怪阶段的卡死检测:只在目标血量为 100% 时认为是在“刚刚开怪、接近怪物”,避免残血目标也触发跑向怪卡死逻辑
target_hp = state.get('target_hp')
if target_hp >= 100:
try:
if self.stuck_handler.check_stuck(state, self.last_turn_end_time):
print(">>> [自动打怪] 可能卡住障碍物,执行脱困")
self.stuck_handler.resolve_stuck()
self.stuck_handler.reset()
return
except Exception:
pass
# --- 场景 2有目标且不在战斗中按交互键 ---
if state['target'] and not state['combat']:
if random.random() < 0.3: # 只有 30% 的循环频率触发按键,模拟真人频率
pydirectinput.press(KEY_LOOT)
# --- 场景 3有目标且在战斗中自动按技能 ---
elif state['target'] and state['combat']:
self.execute_combat_logic(state)
# --- 场景 3脱战且有可拾取物需要你在 Lua 增加拾取位,见下文) ---
# if not state['in_combat'] and state['has_loot']:
# pydirectinput.press(KEY_LOOT)
# 在 main 循环中使用:从 game_state 获取 state
if __name__ == "__main__":
bot = AutoBot()
try:
while True:
state = parse_game_state()
if state:
target_hp = state.get('target_hp')
if target_hp is not None:
hp_part = f"血量:{state['hp']}% 目标血:{target_hp}% | 法力:{state['mp']}% | "
else:
hp_part = f"血量:{state['hp']}% | 法力:{state['mp']}% | "
print(
f"\r[状态] {hp_part}"
f"战斗:{'YES' if state['combat'] else 'NO '} | "
f"目标:{'YES' if state['target'] else 'NO '}",
end=""
)
bot.execute_logic(state)
time.sleep(0.1)
except KeyboardInterrupt:
print("\n已停止。")

235
auto_bot_move.py Normal file
View File

@@ -0,0 +1,235 @@
import json
import os
import random
import sys
import time
import pydirectinput
from game_state import parse_game_state
from coordinate_patrol import CoordinatePatrol
from death_manager import DeathManager
from logistics_manager import LogisticsManager
# 定义按键常量
KEY_TAB = '3'
KEY_LOOT = '4' # 假设你在游戏里设置了互动按键为 F
KEY_ATTACK = '2' # 假设你的主攻击技能是 1
# 巡逻点配置文件
WAYPOINTS_FILE = 'waypoints.json'
# 默认巡逻航点waypoints.json 不存在或无效时使用)
DEFAULT_WAYPOINTS = [(23.8, 71.0), (27.0, 79.0), (31.0, 72.0)]
def _config_base():
"""打包成 exe 时,优先从 exe 同目录读配置,否则用内嵌资源目录。"""
if getattr(sys, 'frozen', False):
exe_dir = os.path.dirname(sys.executable)
if os.path.exists(exe_dir):
return exe_dir
return getattr(sys, '_MEIPASS', exe_dir)
return os.path.dirname(os.path.abspath(__file__))
def get_config_path(filename):
"""解析配置文件路径:先 exe 同目录,再打包内嵌目录。"""
base = _config_base()
p = os.path.join(base, filename)
if os.path.exists(p):
return p
if getattr(sys, 'frozen', False):
meipass = getattr(sys, '_MEIPASS', '')
if meipass:
p2 = os.path.join(meipass, filename)
if os.path.exists(p2):
return p2
return os.path.join(base, filename)
def load_waypoints(path=None):
path = path or get_config_path(WAYPOINTS_FILE)
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
return []
def load_attack_loop(path):
"""从 JSON 加载攻击循环配置。支持 hp_below_percent/hp_key、mp_below_percent/mp_key0=不启用)"""
if not path or not os.path.exists(path):
return None
try:
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
steps = cfg.get('steps') or []
trigger = float(cfg.get('trigger_chance', 0.3))
hp_below = int(cfg.get('hp_below_percent', 0)) or 0
mp_below = int(cfg.get('mp_below_percent', 0)) or 0
return {
'trigger_chance': trigger,
'steps': steps,
'hp_below_percent': hp_below,
'hp_key': (cfg.get('hp_key') or '').strip() or None,
'mp_below_percent': mp_below,
'mp_key': (cfg.get('mp_key') or '').strip() or None,
}
except Exception:
return None
class AutoBotMove:
def __init__(self, waypoints=None, waypoints_path=None, vendor_path=None, attack_loop_path=None, skinning_wait_sec=None):
self.last_tab_time = 0
self.is_running = True
self.is_moving = False
self.target_acquired_time = None # 本次获得目标的时间,用于仅在做「跑向怪」时做卡死检测
# 记录上一帧是否处于战斗/有目标,用于检测“刚刚脱战”的瞬间
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.attack_loop_config = load_attack_loop(attack_loop_path)
if waypoints is None:
path = waypoints_path or get_config_path(WAYPOINTS_FILE)
waypoints = load_waypoints(path) or DEFAULT_WAYPOINTS
self.patrol_controller = CoordinatePatrol(waypoints)
self.death_manager = DeathManager(self.patrol_controller)
vendor_file = vendor_path or get_config_path('vendor.json')
self.logistics_manager = LogisticsManager(vendor_file)
def execute_disengage_loot(self):
"""从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。"""
try:
# 拾取
pydirectinput.press(KEY_LOOT)
time.sleep(0.5)
# 剥皮
pydirectinput.press(KEY_LOOT)
time.sleep(self.skinning_wait_sec)
except Exception:
pass
def execute_combat_logic(self, state):
if self.attack_loop_config:
cfg = self.attack_loop_config
if random.random() >= cfg['trigger_chance']:
return
hp = state.get('hp')
if hp is not None and cfg.get('hp_below_percent') and cfg.get('hp_key') and hp < cfg['hp_below_percent']:
pydirectinput.press(cfg['hp_key'])
time.sleep(0.2)
mp = state.get('mp')
if mp is not None and cfg.get('mp_below_percent') and cfg.get('mp_key') and mp < cfg['mp_below_percent']:
pydirectinput.press(cfg['mp_key'])
time.sleep(0.2)
# 每次攻击前选中目标
pydirectinput.press(KEY_LOOT)
for step in cfg['steps']:
key = step.get('key') or KEY_ATTACK
delay = float(step.get('delay', 0.5))
pydirectinput.press(key)
time.sleep(delay)
return
# 默认:模拟手动按键的节奏
if random.random() < 0.3:
pydirectinput.press(KEY_ATTACK)
def execute_logic(self, state):
death = state.get('death_state', 0)
# 1. 死亡状态:尸体(1) 记录坐标并释放灵魂;灵魂(2) 跑尸
if death == 1:
self.patrol_controller.stop_all()
self.is_moving = False
self.patrol_controller.reset_stuck()
self.death_manager.on_death(state)
return
if death == 2:
self.death_manager.run_to_corpse(state)
return
# 2. 后勤检查(脱战时):空格或耐久不足则回城
self.logistics_manager.check_logistics(state)
if self.logistics_manager.is_returning:
if self.is_moving:
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)
return
# 3. 战斗/有目标:停止移动,执行攻击逻辑;仅在「跑向怪」短窗口内做卡死检测
in_combat_or_target = bool(state['combat'] or state['target'])
if in_combat_or_target:
if self.target_acquired_time is None:
self.target_acquired_time = time.time()
if self.is_moving:
self.patrol_controller.stop_all()
self.is_moving = False
self.patrol_controller.reset_stuck()
# 跑向怪阶段的卡死检测:只在目标血量为 100% 时认为是在“刚刚开怪、接近怪物”,避免残血目标也触发跑向怪卡死逻辑
target_hp = state.get('target_hp')
if target_hp is not None and target_hp >= 100:
try:
if self.patrol_controller.stuck_handler.check_stuck(
state, self.patrol_controller.last_turn_end_time
):
print(">>> [战斗] 跑向怪时可能卡住障碍物,执行脱困")
self.patrol_controller.stop_all()
self.patrol_controller.stuck_handler.resolve_stuck()
self.patrol_controller.reset_stuck()
return
except Exception:
pass
self.execute_combat_logic(state)
self._was_in_combat_or_target = True
return
else:
# 从“有战斗/目标”切换到“完全脱战”的瞬间:尝试智能接回前方最近巡逻点
if self._was_in_combat_or_target:
# 执行脱战拾取
self.execute_disengage_loot()
x, y = state.get('x'), state.get('y')
if x is not None and y is not None:
try:
current_pos = (float(x), float(y))
# max_ahead: 前方查看 10 个点max_dist: 最大允许接入距离 10
self.patrol_controller.snap_to_forward_waypoint(
current_pos, max_ahead=10, max_dist=10.0, skip_current=True
)
except Exception:
pass
self.target_acquired_time = None # 脱战/无目标时清零,下次获得目标再计时
self._was_in_combat_or_target = False
# 4. 没战斗没目标:巡逻(卡死检测在 patrol_controller.navigate 内)
self.is_moving = True
self.patrol_controller.navigate(state)
# 5. 顺便每隔几秒按一下 Tab主动找怪
if not state['target'] and (time.time() - self.last_tab_time > 2.0):
pydirectinput.press(KEY_TAB)
self.last_tab_time = time.time()
# 在 main 循环中使用:从 game_state 获取 state
if __name__ == "__main__":
bot = AutoBotMove()
try:
while True:
state = parse_game_state()
if state:
death_txt = ('存活', '尸体', '灵魂')[state.get('death_state', 0)]
target_hp = state.get('target_hp')
if target_hp is not None:
hp_part = f"血:{state['hp']}% 目标血:{target_hp}% 法:{state['mp']}%"
else:
hp_part = f"血:{state['hp']}% 法:{state['mp']}%"
print(
f"\r[状态] {hp_part} | "
f"战斗:{'Y' if state['combat'] else 'N'} 目标:{'Y' if state['target'] else 'N'} | "
f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | "
f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}°",
end=""
)
bot.execute_logic(state)
time.sleep(0.1)
except KeyboardInterrupt:
print("\n已停止。")

56
build.spec Normal file
View File

@@ -0,0 +1,56 @@
# -*- mode: python ; coding: utf-8 -*-
# 魔兽世界自动巡逻/打怪 - PyInstaller 打包配置
# 打包命令: pyinstaller build.spec
block_cipher = None
# 需要随 exe 一起发布的数据文件(运行时会在 exe 同目录或临时目录解出)
added_files = [
('recorder\\*.json', 'recorder'),
('game_state_config.json', '.'),
]
a = Analysis(
['auto_bot_move.py'], # 主入口
pathex=[],
binaries=[],
datas=added_files,
hiddenimports=[
'pydirectinput',
'pygetwindow',
'PIL',
'PIL._tkinter_finder',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='AutoBotMove',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True, # 保留控制台窗口,便于看状态和 Ctrl+C 停止
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

5515
build/build/Analysis-00.toc Normal file

File diff suppressed because it is too large Load Diff

BIN
build/build/AutoBotMove.pkg Normal file

Binary file not shown.

3146
build/build/EXE-00.toc Normal file

File diff suppressed because it is too large Load Diff

3124
build/build/PKG-00.toc Normal file

File diff suppressed because it is too large Load Diff

BIN
build/build/PYZ-00.pyz Normal file

Binary file not shown.

2394
build/build/PYZ-00.toc Normal file

File diff suppressed because it is too large Load Diff

8
build/build/Tree-00.toc Normal file
View File

@@ -0,0 +1,8 @@
('recorder',
'recorder',
[],
'DATA',
[('recorder\\vendor.json', 'recorder\\vendor.json', 'DATA'),
('recorder\\waypoints.json', 'recorder\\waypoints.json', 'DATA'),
('recorder\\银色前线基地-修理商.json', 'recorder\\银色前线基地-修理商.json', 'DATA'),
('recorder\\银色前线基地-巡逻点.json', 'recorder\\银色前线基地-巡逻点.json', 'DATA')])

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

208
build/build/warn-build.txt Normal file
View File

@@ -0,0 +1,208 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named pyimod02_importers - imported by C:\Users\南音\AppData\Local\Programs\Python\Python311\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed), C:\Users\南音\AppData\Local\Programs\Python\Python311\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgres.py (delayed)
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), distutils.util (delayed, conditional, optional), distutils.archive_util (optional), netrc (delayed, conditional), getpass (delayed), http.server (delayed, optional), webbrowser (delayed), setuptools._distutils.archive_util (optional), setuptools._distutils.util (delayed, conditional, optional)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), distutils.archive_util (optional), setuptools._distutils.archive_util (optional)
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
missing module named fcntl - imported by subprocess (optional)
missing module named 'org.python' - imported by copy (optional), xml.sax (delayed, conditional)
missing module named org - imported by pickle (optional)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named _manylinux - imported by packaging._manylinux (delayed, optional), setuptools._vendor.packaging._manylinux (delayed, optional), pkg_resources._vendor.packaging._manylinux (delayed, optional)
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named termios - imported by getpass (optional), tty (top-level)
missing module named 'java.lang' - imported by platform (delayed, optional), xml.sax._exceptions (conditional)
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
missing module named pyparsing - imported by pkg_resources._vendor.pyparsing.diagram (top-level), setuptools._vendor.pyparsing.diagram (top-level)
missing module named railroad - imported by pkg_resources._vendor.pyparsing.diagram (top-level), setuptools._vendor.pyparsing.diagram (top-level)
missing module named readline - imported by site (delayed, optional), rlcompleter (optional), cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, optional)
missing module named 'pkg_resources.extern.pyparsing' - imported by pkg_resources._vendor.packaging.markers (top-level), pkg_resources._vendor.packaging.requirements (top-level)
missing module named 'pkg_resources.extern.importlib_resources' - imported by pkg_resources._vendor.jaraco.text (optional)
missing module named 'pkg_resources.extern.more_itertools' - imported by pkg_resources._vendor.jaraco.functools (top-level)
missing module named 'com.sun' - imported by pkg_resources._vendor.appdirs (delayed, conditional, optional)
missing module named com - imported by pkg_resources._vendor.appdirs (delayed)
missing module named 'win32com.gen_py' - imported by win32com (conditional, optional)
missing module named _winreg - imported by platform (delayed, optional), pkg_resources._vendor.appdirs (delayed, conditional)
missing module named pkg_resources.extern.packaging - imported by pkg_resources.extern (top-level), pkg_resources (top-level)
missing module named pkg_resources.extern.appdirs - imported by pkg_resources.extern (top-level), pkg_resources (top-level)
missing module named 'pkg_resources.extern.jaraco' - imported by pkg_resources (top-level), pkg_resources._vendor.jaraco.text (top-level)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named usercustomize - imported by site (delayed, optional)
missing module named sitecustomize - imported by site (delayed, optional)
missing module named 'setuptools.extern.pyparsing' - imported by setuptools._vendor.packaging.requirements (top-level), setuptools._vendor.packaging.markers (top-level)
missing module named collections.Sequence - imported by collections (conditional), pyautogui (conditional), setuptools._vendor.ordered_set (optional)
missing module named collections.MutableSet - imported by collections (optional), setuptools._vendor.ordered_set (optional)
missing module named 'setuptools.extern.jaraco' - imported by setuptools._reqs (top-level), setuptools._entry_points (top-level), setuptools.command.egg_info (top-level), setuptools._vendor.jaraco.text (top-level)
missing module named setuptools.extern.importlib_resources - imported by setuptools.extern (conditional), setuptools._importlib (conditional), setuptools._vendor.jaraco.text (optional)
missing module named setuptools.extern.tomli - imported by setuptools.extern (delayed), setuptools.config.pyprojecttoml (delayed)
missing module named setuptools.extern.importlib_metadata - imported by setuptools.extern (conditional), setuptools._importlib (conditional)
missing module named setuptools.extern.ordered_set - imported by setuptools.extern (top-level), setuptools.dist (top-level)
missing module named setuptools.extern.packaging - imported by setuptools.extern (top-level), setuptools.dist (top-level), setuptools.command.egg_info (top-level), setuptools.depends (top-level)
missing module named 'setuptools.extern.more_itertools' - imported by setuptools.dist (top-level), setuptools.config.expand (delayed), setuptools._itertools (top-level), setuptools._entry_points (top-level), setuptools.msvc (top-level), setuptools._vendor.jaraco.functools (top-level)
missing module named 'setuptools.extern.packaging.version' - imported by setuptools.config.setupcfg (top-level), setuptools.msvc (top-level)
missing module named 'setuptools.extern.packaging.utils' - imported by setuptools.wheel (top-level)
missing module named 'setuptools.extern.packaging.tags' - imported by setuptools.wheel (top-level)
missing module named trove_classifiers - imported by setuptools.config._validate_pyproject.formats (optional)
missing module named 'setuptools.extern.packaging.specifiers' - imported by setuptools.config.setupcfg (top-level), setuptools.config._apply_pyprojecttoml (delayed)
missing module named 'setuptools.extern.packaging.requirements' - imported by setuptools.config.setupcfg (top-level)
missing module named importlib_metadata - imported by setuptools._importlib (delayed, optional)
missing module named PIL._imagingagg - imported by PIL (delayed, conditional, optional), PIL.ImageDraw (delayed, conditional, optional)
missing module named collections.Callable - imported by collections (optional), cffi.api (optional)
missing module named _dummy_thread - imported by cffi.lock (conditional, optional), numpy.core.arrayprint (optional)
missing module named dummy_thread - imported by cffi.lock (conditional, optional)
missing module named thread - imported by cffi.lock (conditional, optional), cffi.cparser (conditional, optional)
missing module named cStringIO - imported by cffi.ffiplatform (optional)
missing module named cPickle - imported by pycparser.ply.yacc (delayed, optional)
missing module named cffi._pycparser - imported by cffi (optional), cffi.cparser (optional)
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
missing module named psutil - imported by numpy.testing._private.utils (delayed, optional)
missing module named numpy.core.result_type - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.float_ - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.number - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.object_ - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.max - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.all - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.errstate - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.bool_ - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.inf - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.isnan - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.array2string - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.imag - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.real - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.iscomplexobj - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.signbit - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.isscalar - imported by numpy.core (delayed), numpy.testing._private.utils (delayed), numpy.lib.polynomial (top-level)
missing module named numpy.core.array - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.isnat - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.ndarray - imported by numpy.core (top-level), numpy.testing._private.utils (top-level), numpy.lib.utils (top-level)
missing module named numpy.core.array_repr - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.arange - imported by numpy.core (top-level), numpy.testing._private.utils (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.empty - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.float32 - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.intp - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.linspace - imported by numpy.core (top-level), numpy.lib.index_tricks (top-level)
missing module named numpy.core.iinfo - imported by numpy.core (top-level), numpy.lib.twodim_base (top-level)
missing module named numpy.core.transpose - imported by numpy.core (top-level), numpy.lib.function_base (top-level)
missing module named numpy._typing._ufunc - imported by numpy._typing (conditional)
missing module named numpy.uint - imported by numpy (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.core.asarray - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.utils (top-level), numpy.fft._pocketfft (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.integer - imported by numpy.core (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.sqrt - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.conjugate - imported by numpy.core (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.swapaxes - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.zeros - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.reciprocal - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sort - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.argsort - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sign - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.count_nonzero - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.divide - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.matmul - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.asanyarray - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.atleast_2d - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.prod - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.amax - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.amin - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.moveaxis - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.geterrobj - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.finfo - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.isfinite - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sum - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.multiply - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.add - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.dot - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.Inf - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.newaxis - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.complexfloating - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.inexact - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.cdouble - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.csingle - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.double - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.single - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.intc - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.empty_like - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named threadpoolctl - imported by numpy.lib.utils (delayed, optional)
missing module named numpy.core.ufunc - imported by numpy.core (top-level), numpy.lib.utils (top-level)
missing module named numpy.core.ones - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.hstack - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.atleast_1d - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.atleast_3d - imported by numpy.core (top-level), numpy.lib.shape_base (top-level)
missing module named numpy.core.vstack - imported by numpy.core (top-level), numpy.lib.shape_base (top-level)
missing module named pickle5 - imported by numpy.compat.py3k (optional)
missing module named yaml - imported by numpy.__config__ (delayed)
missing module named numpy.eye - imported by numpy (delayed), numpy.core.numeric (delayed)
missing module named numpy.recarray - imported by numpy (top-level), numpy.lib.recfunctions (top-level), numpy.ma.mrecords (top-level)
missing module named numpy.expand_dims - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.array - imported by numpy (top-level), numpy.ma.core (top-level), numpy.ma.extras (top-level), numpy.ma.mrecords (top-level)
missing module named numpy.iscomplexobj - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.amin - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.amax - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.isinf - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.isnan - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.isfinite - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.float64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.float32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.uint64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level)
missing module named numpy.uint32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level)
missing module named numpy.uint16 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.uint8 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int16 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int8 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.bytes_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.str_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.void - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.object_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.datetime64 - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.timedelta64 - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.number - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.complexfloating - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.floating - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.integer - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ctypeslib (top-level)
missing module named numpy.unsignedinteger - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.bool_ - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ma.core (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.generic - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.dtype - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.array_api._typing (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level), numpy.ctypeslib (top-level)
missing module named numpy.ndarray - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ma.core (top-level), numpy.ma.extras (top-level), numpy.lib.recfunctions (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level), numpy.ctypeslib (top-level)
missing module named numpy.ufunc - imported by numpy (top-level), numpy._typing (top-level), numpy.testing.overrides (top-level)
missing module named numpy.histogramdd - imported by numpy (delayed), numpy.lib.twodim_base (delayed)
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
missing module named defusedxml - imported by PIL.Image (optional)
missing module named Quartz - imported by pygetwindow._pygetwindow_macos (top-level), pyautogui._pyautogui_osx (optional)
missing module named 'Xlib.XK' - imported by pyautogui._pyautogui_x11 (top-level)
missing module named 'Xlib.ext' - imported by pyautogui._pyautogui_x11 (top-level)
missing module named Xlib - imported by mouseinfo (conditional), pyautogui._pyautogui_x11 (top-level)
missing module named 'Xlib.display' - imported by pyautogui._pyautogui_x11 (top-level)
missing module named AppKit - imported by pyperclip (delayed, conditional, optional), pyautogui._pyautogui_osx (top-level)
missing module named Tkinter - imported by mouseinfo (conditional, optional)
missing module named 'rubicon.objc' - imported by mouseinfo (conditional)
missing module named rubicon - imported by mouseinfo (conditional)
missing module named Foundation - imported by pyperclip (delayed, conditional, optional)
missing module named PyQt5 - imported by pyperclip (delayed, conditional, optional)
missing module named qtpy - imported by pyperclip (delayed, conditional, optional)

31743
build/build/xref-build.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,223 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named 'org.python' - imported by copy (optional), xml.sax (delayed, conditional)
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), distutils.util (delayed, conditional, optional), distutils.archive_util (optional), netrc (delayed, conditional), getpass (delayed), http.server (delayed, optional), webbrowser (delayed), setuptools._distutils.archive_util (optional), setuptools._distutils.util (delayed, conditional, optional)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), distutils.archive_util (optional), setuptools._distutils.archive_util (optional)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named pyimod02_importers - imported by C:\Users\南音\AppData\Local\Programs\Python\Python311\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed), C:\Users\南音\AppData\Local\Programs\Python\Python311\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgres.py (delayed)
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
missing module named fcntl - imported by subprocess (optional)
missing module named org - imported by pickle (optional)
missing module named _manylinux - imported by packaging._manylinux (delayed, optional), setuptools._vendor.packaging._manylinux (delayed, optional), pkg_resources._vendor.packaging._manylinux (delayed, optional)
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named termios - imported by getpass (optional), tty (top-level)
missing module named 'java.lang' - imported by platform (delayed, optional), xml.sax._exceptions (conditional)
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
missing module named pyparsing - imported by pkg_resources._vendor.pyparsing.diagram (top-level), setuptools._vendor.pyparsing.diagram (top-level)
missing module named railroad - imported by pkg_resources._vendor.pyparsing.diagram (top-level), setuptools._vendor.pyparsing.diagram (top-level)
missing module named readline - imported by site (delayed, optional), rlcompleter (optional), cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, optional)
missing module named 'pkg_resources.extern.pyparsing' - imported by pkg_resources._vendor.packaging.markers (top-level), pkg_resources._vendor.packaging.requirements (top-level)
missing module named 'pkg_resources.extern.importlib_resources' - imported by pkg_resources._vendor.jaraco.text (optional)
missing module named 'pkg_resources.extern.more_itertools' - imported by pkg_resources._vendor.jaraco.functools (top-level)
missing module named 'com.sun' - imported by pkg_resources._vendor.appdirs (delayed, conditional, optional)
missing module named com - imported by pkg_resources._vendor.appdirs (delayed)
missing module named 'win32com.gen_py' - imported by win32com (conditional, optional)
missing module named _winreg - imported by platform (delayed, optional), pkg_resources._vendor.appdirs (delayed, conditional)
missing module named pkg_resources.extern.packaging - imported by pkg_resources.extern (top-level), pkg_resources (top-level)
missing module named pkg_resources.extern.appdirs - imported by pkg_resources.extern (top-level), pkg_resources (top-level)
missing module named 'pkg_resources.extern.jaraco' - imported by pkg_resources (top-level), pkg_resources._vendor.jaraco.text (top-level)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named usercustomize - imported by site (delayed, optional)
missing module named sitecustomize - imported by site (delayed, optional)
missing module named 'setuptools.extern.pyparsing' - imported by setuptools._vendor.packaging.requirements (top-level), setuptools._vendor.packaging.markers (top-level)
missing module named collections.Sequence - imported by collections (conditional), pyautogui (conditional), setuptools._vendor.ordered_set (optional)
missing module named collections.MutableSet - imported by collections (optional), setuptools._vendor.ordered_set (optional)
missing module named 'setuptools.extern.jaraco' - imported by setuptools._reqs (top-level), setuptools._entry_points (top-level), setuptools.command.egg_info (top-level), setuptools._vendor.jaraco.text (top-level)
missing module named setuptools.extern.importlib_resources - imported by setuptools.extern (conditional), setuptools._importlib (conditional), setuptools._vendor.jaraco.text (optional)
missing module named setuptools.extern.tomli - imported by setuptools.extern (delayed), setuptools.config.pyprojecttoml (delayed)
missing module named setuptools.extern.importlib_metadata - imported by setuptools.extern (conditional), setuptools._importlib (conditional)
missing module named setuptools.extern.ordered_set - imported by setuptools.extern (top-level), setuptools.dist (top-level)
missing module named setuptools.extern.packaging - imported by setuptools.extern (top-level), setuptools.dist (top-level), setuptools.command.egg_info (top-level), setuptools.depends (top-level)
missing module named 'setuptools.extern.more_itertools' - imported by setuptools.dist (top-level), setuptools.config.expand (delayed), setuptools._itertools (top-level), setuptools._entry_points (top-level), setuptools.msvc (top-level), setuptools._vendor.jaraco.functools (top-level)
missing module named 'setuptools.extern.packaging.version' - imported by setuptools.config.setupcfg (top-level), setuptools.msvc (top-level)
missing module named 'setuptools.extern.packaging.utils' - imported by setuptools.wheel (top-level)
missing module named 'setuptools.extern.packaging.tags' - imported by setuptools.wheel (top-level)
missing module named trove_classifiers - imported by setuptools.config._validate_pyproject.formats (optional)
missing module named 'setuptools.extern.packaging.specifiers' - imported by setuptools.config.setupcfg (top-level), setuptools.config._apply_pyprojecttoml (delayed)
missing module named 'setuptools.extern.packaging.requirements' - imported by setuptools.config.setupcfg (top-level)
missing module named importlib_metadata - imported by setuptools._importlib (delayed, optional)
missing module named PIL._imagingagg - imported by PIL (delayed, conditional, optional), PIL.ImageDraw (delayed, conditional, optional)
missing module named collections.Callable - imported by collections (optional), cffi.api (optional), socks (optional)
missing module named _dummy_thread - imported by cffi.lock (conditional, optional), numpy.core.arrayprint (optional)
missing module named dummy_thread - imported by cffi.lock (conditional, optional)
missing module named thread - imported by cffi.lock (conditional, optional), cffi.cparser (conditional, optional)
missing module named cStringIO - imported by cffi.ffiplatform (optional)
missing module named cPickle - imported by pycparser.ply.yacc (delayed, optional)
missing module named cffi._pycparser - imported by cffi (optional), cffi.cparser (optional)
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
missing module named psutil - imported by numpy.testing._private.utils (delayed, optional)
missing module named numpy.core.result_type - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.float_ - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.number - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.object_ - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.max - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.all - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.errstate - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.bool_ - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.inf - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.isnan - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (delayed)
missing module named numpy.core.array2string - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.imag - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.real - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.lib.iscomplexobj - imported by numpy.lib (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.signbit - imported by numpy.core (delayed), numpy.testing._private.utils (delayed)
missing module named numpy.core.isscalar - imported by numpy.core (delayed), numpy.testing._private.utils (delayed), numpy.lib.polynomial (top-level)
missing module named numpy.core.array - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.isnat - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.ndarray - imported by numpy.core (top-level), numpy.testing._private.utils (top-level), numpy.lib.utils (top-level)
missing module named numpy.core.array_repr - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.arange - imported by numpy.core (top-level), numpy.testing._private.utils (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.empty - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.float32 - imported by numpy.core (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.intp - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.core.linspace - imported by numpy.core (top-level), numpy.lib.index_tricks (top-level)
missing module named numpy.core.iinfo - imported by numpy.core (top-level), numpy.lib.twodim_base (top-level)
missing module named numpy.core.transpose - imported by numpy.core (top-level), numpy.lib.function_base (top-level)
missing module named numpy._typing._ufunc - imported by numpy._typing (conditional)
missing module named numpy.uint - imported by numpy (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.core.asarray - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.utils (top-level), numpy.fft._pocketfft (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.integer - imported by numpy.core (top-level), numpy.fft.helper (top-level)
missing module named numpy.core.sqrt - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.conjugate - imported by numpy.core (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.swapaxes - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.zeros - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.fft._pocketfft (top-level)
missing module named numpy.core.reciprocal - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sort - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.argsort - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sign - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.count_nonzero - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.divide - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.matmul - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.asanyarray - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.atleast_2d - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.prod - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.amax - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.amin - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.moveaxis - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.geterrobj - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.finfo - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.isfinite - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.sum - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.multiply - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.add - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.dot - imported by numpy.core (top-level), numpy.linalg.linalg (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.Inf - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.newaxis - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.complexfloating - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.inexact - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.cdouble - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.csingle - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.double - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.single - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.intc - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named numpy.core.empty_like - imported by numpy.core (top-level), numpy.linalg.linalg (top-level)
missing module named threadpoolctl - imported by numpy.lib.utils (delayed, optional)
missing module named numpy.core.ufunc - imported by numpy.core (top-level), numpy.lib.utils (top-level)
missing module named numpy.core.ones - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.hstack - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.atleast_1d - imported by numpy.core (top-level), numpy.lib.polynomial (top-level)
missing module named numpy.core.atleast_3d - imported by numpy.core (top-level), numpy.lib.shape_base (top-level)
missing module named numpy.core.vstack - imported by numpy.core (top-level), numpy.lib.shape_base (top-level)
missing module named pickle5 - imported by numpy.compat.py3k (optional)
missing module named yaml - imported by numpy.__config__ (delayed)
missing module named numpy.eye - imported by numpy (delayed), numpy.core.numeric (delayed)
missing module named numpy.recarray - imported by numpy (top-level), numpy.lib.recfunctions (top-level), numpy.ma.mrecords (top-level)
missing module named numpy.expand_dims - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.array - imported by numpy (top-level), numpy.ma.core (top-level), numpy.ma.extras (top-level), numpy.ma.mrecords (top-level)
missing module named numpy.iscomplexobj - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.amin - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.amax - imported by numpy (top-level), numpy.ma.core (top-level)
missing module named numpy.isinf - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.isnan - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.isfinite - imported by numpy (top-level), numpy.testing._private.utils (top-level)
missing module named numpy.float64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.float32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.uint64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level)
missing module named numpy.uint32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level)
missing module named numpy.uint16 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.uint8 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int64 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int32 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int16 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.int8 - imported by numpy (top-level), numpy.array_api._typing (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.bytes_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.str_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.void - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.object_ - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.datetime64 - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.timedelta64 - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.number - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.complexfloating - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.floating - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.integer - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ctypeslib (top-level)
missing module named numpy.unsignedinteger - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.bool_ - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ma.core (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level)
missing module named numpy.generic - imported by numpy (top-level), numpy._typing._array_like (top-level)
missing module named numpy.dtype - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.array_api._typing (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level), numpy.ctypeslib (top-level)
missing module named numpy.ndarray - imported by numpy (top-level), numpy._typing._array_like (top-level), numpy.ma.core (top-level), numpy.ma.extras (top-level), numpy.lib.recfunctions (top-level), numpy.ma.mrecords (top-level), numpy.random.mtrand (top-level), numpy.random.bit_generator (top-level), numpy.random._philox (top-level), numpy.random._sfc64 (top-level), numpy.random._generator (top-level), numpy.random._mt19937 (top-level), numpy.ctypeslib (top-level)
missing module named numpy.ufunc - imported by numpy (top-level), numpy._typing (top-level), numpy.testing.overrides (top-level)
missing module named numpy.histogramdd - imported by numpy (delayed), numpy.lib.twodim_base (delayed)
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
missing module named defusedxml - imported by PIL.Image (optional)
missing module named 'Xlib.XK' - imported by pyautogui._pyautogui_x11 (top-level)
missing module named 'Xlib.ext' - imported by pyautogui._pyautogui_x11 (top-level)
missing module named Xlib - imported by mouseinfo (conditional), pyautogui._pyautogui_x11 (top-level)
missing module named 'Xlib.display' - imported by pyautogui._pyautogui_x11 (top-level)
missing module named AppKit - imported by pyperclip (delayed, conditional, optional), pyautogui._pyautogui_osx (top-level)
missing module named Quartz - imported by pygetwindow._pygetwindow_macos (top-level), pyautogui._pyautogui_osx (optional)
missing module named Tkinter - imported by mouseinfo (conditional, optional)
missing module named 'rubicon.objc' - imported by mouseinfo (conditional)
missing module named rubicon - imported by mouseinfo (conditional)
missing module named Foundation - imported by pyperclip (delayed, conditional, optional)
missing module named PyQt5 - imported by pyperclip (delayed, conditional, optional)
missing module named qtpy - imported by pyperclip (delayed, conditional, optional)
missing module named simplejson - imported by requests.compat (conditional, optional)
missing module named dummy_threading - imported by requests.cookies (optional)
missing module named 'h2.events' - imported by urllib3.http2.connection (top-level)
missing module named 'h2.connection' - imported by urllib3.http2.connection (top-level)
missing module named h2 - imported by urllib3.http2.connection (top-level)
missing module named zstandard - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named brotli - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named brotlicffi - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named win_inet_pton - imported by socks (conditional, optional)
missing module named bcrypt - imported by cryptography.hazmat.primitives.serialization.ssh (optional)
missing module named cryptography.x509.UnsupportedExtension - imported by cryptography.x509 (optional), urllib3.contrib.pyopenssl (optional)
missing module named chardet - imported by requests (optional)
missing module named 'pyodide.ffi' - imported by urllib3.contrib.emscripten.fetch (delayed, optional)
missing module named pyodide - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named js - imported by urllib3.contrib.emscripten.fetch (top-level)

File diff suppressed because it is too large Load Diff

40
build_exe.ps1 Normal file
View File

@@ -0,0 +1,40 @@
# Build WoW_MultiTool.exe (GUI)
# Usage: .\build_exe.ps1
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
Write-Host "Checking dependencies..." -ForegroundColor Cyan
& pip show pyinstaller 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host "Installing dependencies..." -ForegroundColor Yellow
pip install -r requirements.txt
}
Write-Host "Building WoW_MultiTool.exe ..." -ForegroundColor Cyan
pyinstaller --noconfirm build_wow_multikey.spec
if ($LASTEXITCODE -ne 0) { exit 1 }
$outExe = "dist\WoW_MultiTool.exe"
Write-Host "Copying recorder folder to dist..." -ForegroundColor Cyan
if (Test-Path "dist\recorder") { Remove-Item "dist\recorder" -Recurse -Force -ErrorAction SilentlyContinue }
if (Test-Path "recorder") { Copy-Item "recorder" "dist\recorder" -Recurse -Force }
Write-Host "Copying game_state_config.json to dist..." -ForegroundColor Cyan
if (Test-Path "game_state_config.json") { Copy-Item "game_state_config.json" "dist\game_state_config.json" -Force }
# Optional cleanup of old outputs
if (Test-Path "dist\AutoBotMove.exe") { Remove-Item "dist\AutoBotMove.exe" -Force -ErrorAction SilentlyContinue }
if (Test-Path "dist\vendor.json") { Remove-Item "dist\vendor.json" -Force -ErrorAction SilentlyContinue }
if (Test-Path "dist\waypoints.json") { Remove-Item "dist\waypoints.json" -Force -ErrorAction SilentlyContinue }
if (Test-Path $outExe) {
Write-Host "`nBuild done!" -ForegroundColor Green
Write-Host "Output: $outExe" -ForegroundColor Green
if (Test-Path "dist\recorder") { Write-Host "Copied: dist\recorder" -ForegroundColor Green }
exit 0
} else {
Write-Host "Build failed: $outExe not found." -ForegroundColor Red
exit 1
}

57
build_wow_multikey.spec Normal file
View File

@@ -0,0 +1,57 @@
# -*- mode: python ; coding: utf-8 -*-
# WoW 多键控制器 GUI - PyInstaller 打包配置
# 打包命令: pyinstaller build_wow_multikey.spec
block_cipher = None
# 巡逻打怪 / 录制模式需要
added_files = [
('recorder\\*.json', 'recorder'),
('game_state_config.json', '.'),
]
a = Analysis(
['wow_multikey_gui.py'],
pathex=[],
binaries=[],
datas=added_files,
hiddenimports=[
'win32gui', 'win32api', 'win32con',
'game_state', 'auto_bot_move', 'auto_bot', 'recorder',
'coordinate_patrol', 'death_manager', 'logistics_manager',
'stuck_handler', 'player_movement', 'player_position',
'pydirectinput', 'pygetwindow', 'pyautogui', 'PIL',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='WoW_MultiTool',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # GUI 程序,无控制台窗口
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

11171
combat.log Normal file

File diff suppressed because it is too large Load Diff

70
combat_detector.py Normal file
View File

@@ -0,0 +1,70 @@
"""
战斗状态检测模块,通过模板匹配判断角色是否处于战斗中
"""
import os
import logging
import cv2
import numpy as np
import pyautogui
from config import GameConfig
class CombatDetector:
"""通过截图与模板匹配判断当前是否处于战斗状态"""
TEMPLATES = [
'images/1.png',
'images/2.png',
'images/3.png',
'images/4.png',
]
SCREENSHOT_PATH = 'screenshot/combat_screenshot.png'
def __init__(self, config: GameConfig):
self.config = config
self.screen_width, self.screen_height = pyautogui.size()
self.logger = logging.getLogger(__name__)
os.makedirs(os.path.dirname(self.SCREENSHOT_PATH), exist_ok=True)
def is_in_combat(self) -> bool:
"""判断是否处于战斗中。
截取屏幕左上半区域,逐一与模板图片做归一化相关系数匹配,
任一模板匹配值超过阈值即视为处于战斗状态。
Returns:
bool: 处于战斗中返回 True否则返回 False
"""
try:
region_width = self.screen_width // 2
region_height = self.screen_height // 2
screenshot = pyautogui.screenshot(region=(0, 0, region_width, region_height))
screenshot.save(self.SCREENSHOT_PATH)
screenshot_cv = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
for template_file in self.TEMPLATES:
if not os.path.exists(template_file):
self.logger.warning(f"模板文件不存在,跳过: {template_file}")
continue
template = cv2.imread(template_file)
result = cv2.matchTemplate(
screenshot_cv,
template,
cv2.TM_CCOEFF_NORMED
)
_, max_val, _, _ = cv2.minMaxLoc(result)
if max_val > self.config.recognition_threshold:
self.logger.info(
f"战斗状态: 是(模板: {template_file},匹配值: {max_val:.2f}"
)
return True
self.logger.info("战斗状态: 否(所有模板均未匹配)")
return False
except Exception as e:
self.logger.error(f"判断战斗状态失败: {str(e)}")
return False

160
combat_engine.py Normal file
View File

@@ -0,0 +1,160 @@
"""
战斗引擎模块,负责核心战斗逻辑实现
"""
import time
import logging
import pyautogui
from config import GameConfig
from text_finder import TextFinder
from combat_detector import CombatDetector
from player_movement import PlayerMovement
from game_state import GameStateReader
class CombatEngine:
# 巡逻路径:依次移动到这些坐标,循环往复
PATROL_WAYPOINTS = [
(23.8, 71.0),
(27.0, 79.0),
(31.0, 72.0)
]
# 每走完 PATROL_STEP 坐标单位后检测一次目标;值越小检测越频繁但移动越碎
PATROL_STEP = 3.0
def __init__(self, config: GameConfig):
self.config = config
self.screen_width, self.screen_height = pyautogui.size()
self._setup_safety_measures()
self.text_finder = TextFinder()
self.combat_detector = CombatDetector(config)
self.player_movement = PlayerMovement()
self.game_state_reader = GameStateReader()
self.logger = logging.getLogger(__name__)
def _setup_safety_measures(self) -> None:
"""设置安全防护措施"""
pyautogui.FAILSAFE = True
pyautogui.PAUSE = self.config.action_delay
def _find_target_on_screen(self) -> bool:
"""在屏幕上寻找怪物目标"""
try:
# 先按3键
pyautogui.press('3')
# 等待一段时间,确保画面更新
time.sleep(0.5)
pyautogui.press('4')
time.sleep(3)
if self.combat_detector.is_in_combat():
return True
return False
except Exception as e:
raise RuntimeError(f"目标识别失败: {str(e)}")
def _execute_attack(self) -> None:
"""执行攻击动作"""
try:
# 等待1秒
time.sleep(0.5)
if(self.combat_detector.is_in_combat()):
while(self.combat_detector.is_in_combat()):
pyautogui.press('2')
# time.sleep(1)
pyautogui.press('4')
else:
# time.sleep(1)
# 怪物死亡按4键拾取物品
pyautogui.press('4')
time.sleep(0.5)
pyautogui.press('4')
except Exception as e:
raise RuntimeError(f"攻击执行失败: {str(e)}")
def run_combat_loop(self) -> None:
"""战斗主循环。
按顺序巡逻 PATROL_WAYPOINTS 中的坐标:
- 每移动一小段后检测是否存在目标;
- 找到目标立即停止移动,执行攻击,打完后继续巡逻;
- 未找到目标则继续向当前巡逻点前进,到达后切换下一个。
"""
waypoint_index = 0
while True:
try:
# 获取并打印游戏状态(血量、法力、战斗、目标)
state = self.game_state_reader.get_game_state()
self.logger.info(
f"游戏状态 | 血量:{state['hp_percent']}% 法力:{state['mp_percent']}% "
f"战斗中:{state['in_combat']} 有目标:{state['has_target']}"
)
# target_x, target_y = self.PATROL_WAYPOINTS[waypoint_index]
# self.logger.info(f"前往巡逻点 ({target_x}, {target_y})")
# # 分段移动:每次最多走 MOVE_STEP_DURATION 秒,间隔检测目标
# target_found = self._move_until_target_or_arrived(target_x, target_y)
# if target_found:
# self.logger.info("发现目标,停止移动,开始攻击")
# self._execute_attack()
# self.logger.info("攻击结束,继续巡逻")
# else:
# self.logger.info(f"已到达巡逻点 ({target_x}, {target_y}),切换下一个")
# waypoint_index = (waypoint_index + 1) % len(self.PATROL_WAYPOINTS)
except KeyboardInterrupt:
raise
except Exception as e:
if self.config.auto_recovery:
time.sleep(self.config.error_recovery_delay)
continue
raise
def _move_until_target_or_arrived(self, target_x: float, target_y: float) -> bool:
"""向目标坐标移动,期间每走一小段就检测一次是否存在目标。
到达判定使用轴对齐方式(与 player_movement.move_to 保持一致),
避免欧氏距离与轴对齐判定不一致导致的死循环。
Returns:
bool: 找到目标返回 True到达目标坐标未找到目标返回 False
"""
import math
from player_position import PlayerPosition
tol = self.config.move_tolerance
pos_reader = PlayerPosition()
while True:
pos = pos_reader.get_position_with_retry()
if pos is None:
self.logger.warning("无法读取坐标,中止移动")
return False
dx = target_x - pos[0]
dy = target_y - pos[1]
# 轴对齐到达判定(与 player_movement.move_to 内部逻辑一致)
if abs(dx) <= tol and abs(dy) <= tol:
self.logger.info(f"已到达巡逻点 ({target_x}, {target_y})")
return False
distance = math.sqrt(dx ** 2 + dy ** 2)
# 检测目标(每移动一小段检测一次)
if self._find_target_on_screen():
return True
# 向目标方向走最多 PATROL_STEP 坐标单位,走完后再检测一次目标
step = min(distance, self.PATROL_STEP)
ratio = step / distance
mid_x = pos[0] + dx * ratio
mid_y = pos[1] + dy * ratio
self.logger.info(f"移动小段 → ({mid_x:.2f}, {mid_y:.2f}),剩余距离 {distance:.2f}")
self.player_movement.move_to_position(mid_x, mid_y, tolerance=tol)

14
combat_loops/DK.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "DK",
"trigger_chance": 0.5,
"steps": [
{
"key": "2",
"delay": 0.5
}
],
"hp_below_percent": 0,
"hp_key": "",
"mp_below_percent": 0,
"mp_key": ""
}

40
config.py Normal file
View File

@@ -0,0 +1,40 @@
"""
游戏配置模块
"""
from dataclasses import dataclass
import cv2
import numpy as np
from typing import Tuple
@dataclass
class GameConfig:
# 图像识别参数
monster_template_path: str = 'monster_template.png'
recognition_threshold: float = 0.55
scan_interval: float = 1.0
# 操作参数
attack_button: str = 'left'
action_delay: float = 0.1
# 移动参数
move_tolerance: float = 0.5 # 到达巡逻点的判定半径(坐标单位)
# 系统参数
auto_recovery: bool = True
error_recovery_delay: float = 3.0
# 计算属性
@property
def monster_template(self) -> np.ndarray:
"""加载怪物模板图像"""
img = cv2.imread(self.monster_template_path, cv2.IMREAD_COLOR)
if img is None:
raise FileNotFoundError(f"模板图像未找到: {self.monster_template_path}")
return img
@classmethod
def load_config(cls) -> 'GameConfig':
"""加载配置文件"""
# 实际项目中可以从配置文件读取
return cls()

349
coordinate_patrol.py Normal file
View File

@@ -0,0 +1,349 @@
"""
坐标巡逻模块:按航点列表循环巡逻,朝向计算与 player_movement 约定一致atan2(dx,-dy));内含卡死检测与脱困。
"""
import math
import random
import time
import logging
import pyautogui
from stuck_handler import StuckHandler
# 转向与死区常量(度)
ANGLE_THRESHOLD_DEG = 15.0 # 朝向容差,超过此值才按 A/D 转向
ANGLE_DEADZONE_DEG = 5.0 # 死区,此范围内不按 A/D、只按 W减少左右微调
# 随机行为跳跃概率每帧触发概率0.005 ≈ 平均 200 帧一次)
RANDOM_JUMP_PROB = 0.05
class CoordinatePatrol:
"""按航点坐标巡逻,用 pyautogui 按键转向与前进。"""
def __init__(
self,
waypoints,
arrival_threshold=0.5,
angle_threshold_deg=ANGLE_THRESHOLD_DEG,
angle_deadzone_deg=ANGLE_DEADZONE_DEG,
random_jump_prob=RANDOM_JUMP_PROB,
):
"""
Args:
waypoints: 航点列表,游戏坐标格式 [(x1, y1), (x2, y2), ...],如 [(26.0, 78.0), (30.0, 82.0)]
arrival_threshold: 到达判定距离(游戏坐标单位),默认 0.5
angle_threshold_deg: 朝向容差(度),超过此值才按 A/D 转向,默认 ANGLE_THRESHOLD_DEG
angle_deadzone_deg: 转向死区(度),此范围内不按 A/D、只按 W默认 ANGLE_DEADZONE_DEG
"""
self.waypoints = waypoints
self.current_index = 0
self.arrival_threshold = arrival_threshold
self.angle_threshold_deg = angle_threshold_deg
self.angle_deadzone_deg = angle_deadzone_deg
self.random_jump_prob = float(random_jump_prob)
self.logger = logging.getLogger(__name__)
self.last_turn_end_time = 0 # 最近一次结束 A/D 转向的时间,供卡死检测排除原地转向
self.stuck_handler = StuckHandler()
@staticmethod
def get_distance(p1, p2):
return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
def snap_to_forward_waypoint(self, current_pos, max_ahead=10, max_dist=10.0, skip_current=True):
"""
根据当前位置,将巡逻索引 current_index 智能“接回”前方最近的航点。
典型场景:战斗结束后,角色可能偏离路径,希望从前进方向上的最近航点继续巡逻,
而不是跑回战斗前的旧航点。
Args:
current_pos: 当前坐标 (x, y),游戏坐标。
max_ahead: 向前最多查看多少个航点(以 current_index 为起点),默认 10。
max_dist: 认为“可以接回”的最大距离阈值,超过则不调整索引,单位同坐标,默认 10.0。
skip_current: 是否跳过 current_index 本身,直接从下一个点开始找,避免立刻又去原来的点。
Returns:
bool: True 表示已调整 current_indexFalse 表示未调整(距离太远或数据不完整)。
"""
if current_pos is None:
return False
if not self.waypoints:
return False
try:
cx, cy = float(current_pos[0]), float(current_pos[1])
except Exception:
return False
n = len(self.waypoints)
if n == 0:
return False
start_idx = (self.current_index + (1 if skip_current else 0)) % n
best_idx = None
best_dist = None
# 在 current_index 之后的若干个点里寻找最近的一个
for offset in range(max_ahead + 1):
idx = (start_idx + offset) % n
wp = self.waypoints[idx]
d = self.get_distance((cx, cy), (float(wp[0]), float(wp[1])))
if (best_dist is None) or (d < best_dist):
best_dist = d
best_idx = idx
if best_idx is None or best_dist is None:
return False
if best_dist > float(max_dist):
# 离前方所有点都太远,则不强行跳索引,维持原行为
return False
# 将当前巡逻索引接到前方最近的点或其后一个点
self.current_index = best_idx
self.reset_stuck()
self.logger.info(
f">>> 战斗结束智能接回巡逻current_pos={current_pos}, "
f"接入点索引={best_idx}, 距离={best_dist:.2f}"
)
return True
def get_target_heading_deg(self, current_pos, target_pos):
"""计算朝向目标所需的游戏朝向。dx_actual ∝ sin(heading)dy_actual ∝ -cos(heading) → 目标朝向 = atan2(dx, -dy)。"""
dx = target_pos[0] - current_pos[0]
dy = target_pos[1] - current_pos[1]
if dx == 0 and dy == 0:
return 0.0
return math.degrees(math.atan2(dx, -dy)) % 360
def _turn_duration_from_angle(self, angle_diff_deg):
"""
专门为“边走边转”设计的非线性脉冲函数。
逻辑:
- 大于 45°给予一个较长脉冲快速切入方向。
- 15° 到 45°线性修正。
- 小于 15°极小脉冲防止在直线上摇摆。
"""
abs_angle = abs(angle_diff_deg)
# 基础转向系数 (针对边走边转优化,值越小越不容易画龙)
# 推荐范围 0.002 - 0.0035
scale_factor = 0.0025
if abs_angle > 45:
# 大角度:给予一个明显的转向力,但不超过 0.2s 避免丢失 W 的推力感
base_duration = min(abs_angle * scale_factor * 1.2, 0.20)
elif abs_angle > 15:
# 中角度:标准线性修正
base_duration = abs_angle * scale_factor
else:
# 小角度微调:使用极短脉冲 (物理按键触发的底线)
base_duration = 0.04
# 随机化:模拟人工轻点按键的不规则性
# 这里的随机范围要小,否则会导致修正量不稳定
jitter = random.uniform(0.005, 0.015)
final_duration = base_duration + jitter
# 最终安全检查:严禁超过 0.25s,否则会造成明显的顿挫
return min(final_duration, 0.25)
def reset_stuck(self):
"""重置卡死检测状态(切航点、停止巡逻、死亡/后勤/战斗时由外部调用)。"""
self.stuck_handler.reset()
def navigate(self, state):
"""
优化版导航:支持边走边转,非阻塞平滑移动。
"""
if state is None:
self.stop_all()
return
# 1. 基础状态解析与校验
x, y = state.get("x"), state.get("y")
heading = state.get("facing")
if x is None or y is None or heading is None:
self.logger.warning("state 数据不完整,跳过导航帧")
self.stop_all()
return
# 2. 卡死检测:位移检测逻辑保持在巡逻中
if self.stuck_handler.check_stuck(state, self.last_turn_end_time):
self.logger.error("!!! 检测到卡死,启动脱困程序 !!!")
self.stop_all()
self.stuck_handler.resolve_stuck()
return
curr_pos = (float(x), float(y))
heading = float(heading)
target_pos = self.waypoints[self.current_index]
dist = self.get_distance(curr_pos, target_pos)
# 3. 到达判定(到点后平滑切换下一个航点,不再完全刹车)
if dist < self.arrival_threshold:
self.logger.info(f">>> 到达航点 {self.current_index},切换至下一目标(平滑过渡)")
self.current_index = (self.current_index + 1) % len(self.waypoints)
# 重置卡死检测,避免在航点附近因为轻微抖动被误判卡死
self.reset_stuck()
# 不再调用 stop_all(),让 W 保持按下,下一帧直接朝下一个航点继续前进
return
# 4. 计算航向逻辑
target_heading = self.get_target_heading_deg(curr_pos, target_pos)
angle_diff = (target_heading - heading + 180) % 360 - 180
abs_diff = abs(angle_diff)
# --- [平滑移动核心:边走边转控制层] ---
# A. 始终保持前进动力
pyautogui.keyDown("w")
# B. 转向决策逻辑
if abs_diff <= self.angle_deadzone_deg:
if random.random() < self.random_jump_prob: # 频率建议设低,约每 200 帧跳一次
self.logger.info(">>> 随机跳跃:模拟真人并辅助脱困")
pyautogui.press("space")
# 在死区内:绝对直线行驶,松开所有转向键
pyautogui.keyUp("a")
pyautogui.keyUp("d")
elif abs_diff > self.angle_threshold_deg:
# 超出阈值:执行平滑脉冲转向
turn_key = "d" if angle_diff > 0 else "a"
other_key = "a" if angle_diff > 0 else "d"
# 确保不会同时按下左右键
pyautogui.keyUp(other_key)
# 获取针对平滑移动优化的短脉冲时长
duration = self._turn_duration_from_angle(abs_diff)
# 关键:执行短促转向脉冲
pyautogui.keyDown(turn_key)
# 这里的 sleep 极短 (建议 < 0.1s),不会造成移动卡顿
time.sleep(duration)
pyautogui.keyUp(turn_key)
self.last_turn_end_time = time.time()
# self.logger.debug(f"修正航向: {turn_key} | 角度差: {angle_diff:.1f}°")
else:
# 在死区与阈值之间:为了平滑,不进行任何按键动作,仅靠 W 前进
pyautogui.keyUp("a")
pyautogui.keyUp("d")
return
def navigate_to_point(self, state, target_pos, arrival_threshold=None):
"""
优化版:平滑导航,支持边走边转。
"""
if state is None:
self.stop_all()
return False
current_pos = (state.get('x'), state.get('y'))
current_facing = state.get('facing')
if None in current_pos or current_facing is None:
self.stop_all()
return False
# 1. 卡死检测
if self.stuck_handler.check_stuck(state, self.last_turn_end_time):
self.stop_all()
self.stuck_handler.resolve_stuck()
return False
current_pos = (float(current_pos[0]), float(current_pos[1]))
current_facing = float(current_facing)
threshold = arrival_threshold if arrival_threshold is not None else self.arrival_threshold
# 2. 距离判断
dist = self.get_distance(current_pos, target_pos)
if dist < threshold:
self.stop_all()
return True
# 3. 计算角度差
target_heading = self.get_target_heading_deg(current_pos, target_pos)
angle_diff = (target_heading - current_facing + 180) % 360 - 180
abs_diff = abs(angle_diff)
# --- 平滑移动核心逻辑 ---
# 只要没到终点,始终保持前进 W 键按下
pyautogui.keyDown("w")
# 4. 根据偏角决定转向动作
if abs_diff <= self.angle_deadzone_deg:
if random.random() < self.random_jump_prob: # 频率建议设低,约每 200 帧跳一次
self.logger.info(">>> 随机跳跃:模拟真人并辅助脱困")
pyautogui.press("space")
# 在死区内,确保转向键松开,直线前进
pyautogui.keyUp("a")
pyautogui.keyUp("d")
else:
# 在死区外,需要修正方向
turn_key = "d" if angle_diff > 0 else "a"
other_key = "a" if angle_diff > 0 else "d"
# 松开反方向键
pyautogui.keyUp(other_key)
# 计算本次修正的时长
# 边走边转时duration 应比原地转向更短,建议使用 0.05s - 0.1s 的短脉冲
duration = self._turn_duration_from_angle(abs_diff)
# 限制单次修正时间,防止转过头导致“画龙”
safe_duration = min(duration, 0.15)
pyautogui.keyDown(turn_key)
time.sleep(safe_duration) # 短暂物理延迟确保按键生效
pyautogui.keyUp(turn_key)
self.last_turn_end_time = time.time()
return False
def navigate_path(self, get_state, path, forward=True, arrival_threshold=None):
"""
按 path 依次走完所有点后返回。每次调用都必须传入 path。
get_state: 可调用对象,每次调用返回当前状态 dict需包含 'x','y','facing'
path: [[x,y], ...] 或 [(x,y), ...](如 json.load 得到),本次要走的全部航点。
forward: True=正序0→nFalse=倒序n→0
阻塞直到走完 path 中所有点或出错,走完返回 True。内含卡死检测。
"""
if not path:
self.stop_all()
return True
points = [(float(p[0]), float(p[1])) for p in path]
if not forward:
points = points[::-1]
for i, target in enumerate(points):
self.logger.info(f">>> 路径点 {i + 1}/{len(points)}: {target}")
while True:
state = get_state()
if state is None:
self.stop_all()
return False
arrived = self.navigate_to_point(state, target, arrival_threshold)
if arrived:
break
time.sleep(0.05)
self.stop_all()
self.logger.info(">>> 路径走完")
return True
def press_key(self, key):
"""只按下指定键,抬起其他移动键。"""
for k in ("w", "a", "d"):
if k == key:
pyautogui.keyDown(k)
else:
pyautogui.keyUp(k)
def stop_all(self):
for key in ["w", "a", "s", "d"]:
pyautogui.keyUp(key)

38
death_manager.py Normal file
View File

@@ -0,0 +1,38 @@
import time
import math
import pyautogui
class DeathManager:
def __init__(self, patrol_system):
self.corpse_pos = None
self.patrol_system = patrol_system
self.is_running_to_corpse = False
def on_death(self, state):
"""1. 死亡瞬间调用:从 player_position 获取坐标并记录"""
self.corpse_pos = (state['x'], state['y'])
self.is_running_to_corpse = True
print(f">>> [系统] 记录死亡坐标: {self.corpse_pos},准备释放灵魂...")
pyautogui.press('9') # 绑定宏: /run RepopMe()
time.sleep(5) # 等待加载界面
def run_to_corpse(self, state):
"""2. 跑尸寻路逻辑:坐标与朝向从 player_position 获取"""
if not self.corpse_pos:
return
is_arrived = self.patrol_system.navigate_to_point(state, self.corpse_pos)
# 如果距离尸体很近0.005 约等于 10-20 码)
if is_arrived:
print(">>> 已到达尸体附近,尝试复活...")
pyautogui.press('0') # 绑定宏: /run RetrieveCorpse()
time.sleep(5)
self.is_running_to_corpse = False
self.corpse_pos = None
return
def handle_resurrection_popup(self):
"""处理复活确认框"""
# 配合 Lua 插件自动点击,或者 Python 模拟按键
pydirectinput.press('enter')

BIN
dist/WoW_MultiTool.exe vendored Normal file

Binary file not shown.

22
dist/combat_loops/dk.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "dk",
"trigger_chance": 0.3,
"steps": [
{
"key": "2",
"delay": 0.5
},
{
"key": "4",
"delay": 0.5
},
{
"key": "4",
"delay": 0.5
}
],
"hp_below_percent": 0,
"hp_key": "",
"mp_below_percent": 0,
"mp_key": ""
}

8
dist/game_state_config.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"pixel_size": 17,
"block_start_x": 30,
"scan_region_width": 155,
"scan_region_height": 15,
"offset_left": 20,
"offset_top": 45
}

30
dist/recorder/vendor.json vendored Normal file
View File

@@ -0,0 +1,30 @@
[
[
85.35,
74.90
],
[
85.36,
74.93
],
[
85.66,
75.35
],
[
86.1,
75.59
],
[
86.61,
75.59
],
[
87.08,
75.82
],
[
87.24,
75.88
]
]

70
dist/recorder/waypoints.json vendored Normal file
View File

@@ -0,0 +1,70 @@
[
[
85.35,
74.90
],
[
85.32,
74.86
],
[
85.02,
74.38
],
[
84.8,
73.92
],
[
84.6,
73.46
],
[
84.39,
73.0
],
[
84.17,
72.55
],
[
83.88,
72.05
],
[
83.42,
71.84
],
[
83.3,
72.34
],
[
83.34,
72.9
],
[
83.37,
73.46
],
[
83.62,
73.94
],
[
83.97,
74.32
],
[
84.41,
74.67
],
[
84.94,
74.86
],
[
85.35,
74.90
]
]

70
dist/recorder/湿地刷布点.json vendored Normal file
View File

@@ -0,0 +1,70 @@
[
[
61.08,
73.18
],
[
61.26,
72.7
],
[
61.44,
72.21
],
[
61.62,
71.71
],
[
61.8,
71.23
],
[
61.98,
70.73
],
[
62.12,
70.23
],
[
62.3,
69.76
],
[
62.48,
69.26
],
[
62.3,
69.76
],
[
62.14,
70.25
],
[
61.99,
70.73
],
[
61.83,
71.21
],
[
61.67,
71.71
],
[
61.51,
72.2
],
[
61.33,
72.68
],
[
61.17,
73.18
]
]

View File

@@ -0,0 +1,86 @@
[
[
10.76,
60.55
],
[
11.12,
60.93
],
[
11.48,
61.31
],
[
12.01,
61.3
],
[
12.53,
61.35
],
[
12.91,
61.72
],
[
13.28,
62.08
],
[
13.8,
62.14
],
[
14.32,
62.07
],
[
14.53,
61.58
],
[
14.42,
61.04
],
[
14.31,
60.51
],
[
14.18,
60.01
],
[
14.03,
59.51
],
[
13.57,
59.28
],
[
13.05,
59.42
],
[
12.54,
59.55
],
[
12.11,
59.88
],
[
12.01,
60.41
],
[
11.53,
60.58
],
[
10.76,
60.55
]
]

View File

@@ -0,0 +1,46 @@
[
[
32.54,
41.22
],
[
32.67,
40.7
],
[
32.85,
40.19
],
[
32.98,
39.67
],
[
32.84,
39.14
],
[
32.35,
39.26
],
[
32.17,
39.76
],
[
31.98,
40.25
],
[
31.77,
40.75
],
[
31.57,
41.23
],
[
32.54,
41.22
]
]

View File

@@ -0,0 +1,246 @@
[
[
44.37,
34.77
],
[
44.37,
35.32
],
[
44.39,
35.82
],
[
44.4,
36.32
],
[
44.41,
36.82
],
[
44.42,
37.32
],
[
44.44,
37.82
],
[
44.46,
38.38
],
[
44.47,
38.94
],
[
44.49,
39.44
],
[
44.5,
39.94
],
[
44.52,
40.5
],
[
44.53,
41.0
],
[
44.55,
41.55
],
[
44.56,
42.1
],
[
44.67,
42.62
],
[
44.71,
43.12
],
[
44.71,
43.67
],
[
44.68,
44.17
],
[
44.66,
44.67
],
[
44.63,
45.17
],
[
44.6,
45.71
],
[
44.58,
46.26
],
[
44.55,
46.76
],
[
44.52,
47.26
],
[
44.54,
47.82
],
[
44.43,
48.35
],
[
44.26,
48.85
],
[
44.02,
49.29
],
[
43.73,
49.75
],
[
43.35,
50.13
],
[
42.92,
49.87
],
[
42.81,
49.35
],
[
42.77,
48.81
],
[
42.71,
48.26
],
[
42.66,
47.71
],
[
42.6,
47.15
],
[
42.56,
46.6
],
[
42.56,
46.09
],
[
42.57,
45.55
],
[
42.58,
45.05
],
[
42.58,
44.54
],
[
42.59,
44.04
],
[
42.6,
43.52
],
[
42.6,
42.97
],
[
42.62,
42.47
],
[
42.64,
41.97
],
[
42.66,
41.42
],
[
42.68,
40.87
],
[
42.69,
40.32
],
[
42.7,
39.82
],
[
42.7,
39.32
],
[
42.7,
38.82
],
[
42.71,
38.27
],
[
42.71,
37.77
],
[
42.72,
37.25
],
[
42.73,
36.75
],
[
42.76,
36.2
],
[
42.89,
35.7
],
[
43.25,
35.3
],
[
44.37,
34.77
]
]

View File

@@ -0,0 +1,26 @@
[
[
85.23,
74.7
],
[
85.51,
75.15
],
[
85.8,
75.58
],
[
86.31,
75.64
],
[
86.81,
75.63
],
[
87.26,
75.89
]
]

View File

@@ -0,0 +1,74 @@
[
[
85.31,
74.79
],
[
85.0,
74.37
],
[
84.71,
73.94
],
[
84.4,
73.49
],
[
84.11,
73.07
],
[
83.81,
72.65
],
[
83.52,
73.08
],
[
83.44,
73.59
],
[
83.54,
74.12
],
[
83.58,
74.66
],
[
83.68,
75.18
],
[
83.77,
75.71
],
[
83.87,
76.23
],
[
83.97,
76.76
],
[
84.36,
76.44
],
[
84.62,
75.99
],
[
84.89,
75.56
],
[
85.31,
74.79
]
]

View File

@@ -0,0 +1,34 @@
[
[
47.69,
70.25
],
[
47.55,
70.74
],
[
47.4,
71.22
],
[
47.25,
71.7
],
[
46.79,
71.5
],
[
46.93,
71.02
],
[
47.39,
70.8
],
[
47.69,
70.25
]
]

BIN
dist/screenshot/game_state.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

BIN
dist/screenshot/game_state_combat.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

BIN
dist/screenshot/game_state_hp.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

BIN
dist/screenshot/game_state_mp.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
dist/screenshot/game_state_target.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

BIN
dist/screenshot/game_state_x.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

BIN
dist/screenshot/game_state_y.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

32
dist/wow_multikey_qt.json vendored Normal file
View File

@@ -0,0 +1,32 @@
{
"空格(跳跃)": {
"enabled": false,
"interval": 10,
"jitter": 2
},
"1": {
"enabled": false,
"interval": 10,
"jitter": 2
},
"2": {
"enabled": true,
"interval": 1,
"jitter": 0
},
"3": {
"enabled": false,
"interval": 10,
"jitter": 2
},
"4": {
"enabled": true,
"interval": 2,
"jitter": 0
},
"5": {
"enabled": false,
"interval": 10,
"jitter": 2
}
}

19
docs/history.md Normal file
View File

@@ -0,0 +1,19 @@
# 更新记录History
## 2026-03-17
### 剥皮等待时间可配置(接入 GUI
- **新增 GUI 参数**:在 `wow_multikey_gui.py` 的「参数配置」页新增 **剥皮等待时间**(秒)可调项,保存后用于控制脱战后剥皮等待时长。
- **Bot 接入参数**
- `auto_bot_move.py``AutoBotMove` 新增 `skinning_wait_sec` 参数,`execute_disengage_loot()` 使用该值替代固定等待。
- `auto_bot.py``AutoBot` 新增 `skinning_wait_sec` 参数,`execute_disengage_loot()` 使用该值替代固定等待。
- **配置文件升级与兼容**
- `wow_multikey_qt.json` 结构升级为 `{ "keys": {...}, "bot": {...} }`,其中 `bot.skinning_wait_sec` 存储剥皮等待秒数。
- 兼容旧版仅包含按键配置的结构:加载时会自动包裹为 `keys`,不会丢失旧配置。
### 使用方式
1. 打开 GUI → **参数配置** → 设置 **剥皮等待时间** → 点击 **保存配置**
2. 启动 **巡逻打怪****自动打怪**,剥皮等待时间按保存值生效。

123
docs/mount_travel_plan.md Normal file
View File

@@ -0,0 +1,123 @@
# 脱战无目标上马跑路:实现方案(不改代码版)
本文档描述如何在 **`combat=False``target=False`** 时自动 **上马跑路/巡逻**,并在 **进入战斗或获得目标** 时立刻切回战斗逻辑。先给方案与落地路径,后续再按本文改代码实现。
## 目标与约束
- **目标**:没目标、没进战斗时,上马并持续移动(使用现有 `CoordinatePatrol.navigate()` 作为跑路/巡逻引擎)。
- **切回**:一旦 `combat=True``target=True`,立即停止移动并进入战斗逻辑(必要时下马/取消坐骑)。
- **稳定性**:避免反复按坐骑键导致“上马→下马→上马”抖动;避免与拾取/剥皮互相打断。
## 方案总览(按稳定性排序)
### 方案 A推荐最稳增加 `mounted` 状态 → 状态机闭环
**核心思路**:像当前读 `combat/target` 一样,再增加一个 `mounted`(是否在坐骑上)状态输入,然后用“小状态机”确保行为可控。
- **进入赶路条件**`not combat``not target`
-`mounted=False`:按一次上马键/坐骑宏
- 等待 `mounted=True`(带超时,例如 3 秒)
- `mounted=True` 后开始 `navigate()`(持续按 W + 转向)
- **进入战斗条件**`combat or target`
- `stop_all()` 立刻停下
- 如需要可按一次“取消坐骑/下马”(可选)
- 执行战斗逻辑
**优点**
- 不依赖固定 sleep不卡延迟几乎不会误触“下马”。
- 不会因为公共CD、网络卡顿、瞬时移动导致上马失败后一直走路。
**前置条件**
- 需要能从游戏侧输出 `mounted` 状态WeakAura/插件/像素条协议增加一格均可)。
---
### 方案 B无需新增状态最快落地盲按坐骑 + 冷却防抖
**核心思路**:不检测是否已经上马,只在满足条件时以**较低频率**尝试上马,并通过冷却与节流避免反复触发。
- **触发条件**(建议都满足):
- `not combat``not target`
- 不在“脱战拾取/剥皮冻结窗口”内(避免打断交互)
- 距离阈值满足(见方案 C
- 距离上次尝试上马已超过 `mount_try_cooldown`(建议 812 秒)
- **动作**
-`stop_all()`(移动会导致上马失败)
- 按一次坐骑键/宏
- `sleep(0.2~0.4)`(给客户端一点时间响应)
- 再进入 `navigate()`
- **切回战斗**
-`combat or target`:立刻 `stop_all()` 并进入战斗逻辑
**优点**
- 不需要改游戏状态协议,改动面最小。
**风险**
- 如果你绑定的“坐骑键”在已上马时会取消坐骑,那么盲按可能导致“按一下下马”。因此必须:
- 冷却要长812s
- 触发条件要保守(最好配合方案 C 的距离阈值)
- 坐骑键尽量绑定为“更不容易误下马”的宏(见方案 D
---
### 方案 C更像真人、效率更高基于距离阈值决定是否上马
**核心思路**:只有在“要跑很远”的时候才上马,短距离巡逻不浪费上马时间,也减少误触发频率。
- 计算当前位置到下一航点距离 `dist`
-`dist > mount_dist_threshold` 才允许上马尝试
**推荐阈值**
- `mount_dist_threshold = 8 ~ 15`(以你当前坐标系单位为准,按实际体感调整)
**与方案 B 的组合建议**
- 实际落地强烈建议用 **方案 C + 方案 B**:距离足够远才进行低频盲按上马。
---
### 方案 D强烈建议配套使用“智能坐骑宏”降低误下马
为了降低脚本“盲按一次”带来的不确定性,建议把坐骑键绑定为更可控的宏/按键策略:
- **尽量避免**:同一个键在“已上马”时必定执行下马的行为(会增加误下马概率)
- **建议**:脚本侧通过冷却和距离阈值尽量“只按一次”,并把坐骑宏设计为“尽量只上马”
> 注:不同版本/服环境宏条件支持不完全一致;这里给的是方向,具体宏内容以你客户端可用语法为准。
## 推荐落地路径(按投入产出)
### 路径 1最快能用方案 C + 方案 B
- **一套就能跑起来**,不改 `game_state` 协议。
- 关键参数:
- `mount_try_cooldown`: 812 秒
- `mount_dist_threshold`: 815
- `mount_press_settle_sleep`: 0.20.4 秒
- 关键注意:
- 上马前必须 `stop_all()`,否则容易上马失败。
- 与拾取/剥皮冻结窗口协调:冻结窗口未结束前,不要上马、不导航。
### 路径 2工业级稳定方案 A
- 增加 `mounted` 状态输入,形成闭环状态机。
- 上马只在 `mounted=False` 时触发,几乎消除“误下马/抖动”。
## 与拾取/剥皮的冲突处理(重要)
读条/交互(拾取、剥皮)会被以下动作打断:
- 任何移动键(尤其是 `W`)或转向键脉冲
- 换目标(如 Tab
- 再次进入战斗/获得目标
因此强烈建议:
- 在拾取/剥皮窗口内:**完全禁止导航与 Tab**(冻结移动/找怪)。
- 冻结窗口结束后再允许“上马与跑路”逻辑。
## 验收标准(你实现后怎么判断对不对)
- **脱战无目标**:角色停止原地抖动 → 成功上马 → 开始连续巡逻移动。
- **途中遭遇**:一出现 `combat=True``target=True` → 立即停下并进入战斗逻辑(不再继续按 W
- **不会抖动**:不会出现“每隔 1 秒按一下坐骑导致上马/下马来回切”的现象。

167
game_state.py Normal file
View File

@@ -0,0 +1,167 @@
import json
import os
import sys
import time
import ctypes
import pyautogui
import pygetwindow as gw
from PIL import Image
# 解决 Windows 高 DPI 缩放问题
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
# 默认布局参数
_DEFAULTS = {
"pixel_size": 17,
"block_start_x": 30,
"scan_region_width": 155,
"scan_region_height": 15,
"offset_left": 20,
"offset_top": 45,
}
SCREENSHOT_DIR = 'screenshot'
CONFIG_FILE = 'game_state_config.json'
def _config_base():
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
def _get_config_path():
base = _config_base()
p = os.path.join(base, CONFIG_FILE)
if os.path.exists(p):
return p
if getattr(sys, 'frozen', False) and getattr(sys, '_MEIPASS', ''):
p2 = os.path.join(sys._MEIPASS, CONFIG_FILE)
if os.path.exists(p2):
return p2
return p
def load_layout_config():
"""加载 game_state_config.json返回布局参数字典"""
path = _get_config_path()
cfg = dict(_DEFAULTS)
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8') as f:
loaded = json.load(f)
for k in _DEFAULTS:
if k in loaded:
cfg[k] = loaded[k]
except Exception:
pass
cfg['center_offset'] = cfg['pixel_size'] // 2
return cfg
def save_layout_config(cfg):
"""保存布局配置到 game_state_config.json"""
path = _get_config_path()
out = {k: cfg[k] for k in _DEFAULTS if k in cfg}
with open(path, 'w', encoding='utf-8') as f:
json.dump(out, f, indent=2, ensure_ascii=False)
return path
def get_wow_info():
windows = gw.getWindowsWithTitle('魔兽世界')
if not windows:
return None
wow = windows[0]
cfg = load_layout_config()
return {
"left": wow.left + cfg['offset_left'],
"top": wow.top + cfg['offset_top'],
"layout": cfg,
}
def parse_game_state():
pos = get_wow_info()
if not pos:
return None
cfg = pos['layout']
w = cfg['scan_region_width']
h = cfg['scan_region_height']
pixel_size = cfg['pixel_size']
block_start_x = cfg['block_start_x']
center_offset = cfg['center_offset']
# 截图并保存到 screenshot 文件夹
screenshot = pyautogui.screenshot(region=(pos['left'], pos['top'], w, h))
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
screenshot.save(os.path.join(SCREENSHOT_DIR, 'game_state.png'))
# 按截图像素分段:每 pixel_size 一格 → hp, mp, combat, target, logistics_death, x, y
CHANNEL_NAMES = ('hp', 'mp', 'combat', 'target', 'logistics_death', 'x', 'y')
for idx, name in enumerate(CHANNEL_NAMES):
left = block_start_x + idx * pixel_size
box = (left, 0, left + pixel_size, pixel_size)
crop = screenshot.crop(box)
crop.save(os.path.join(SCREENSHOT_DIR, f'game_state_{name}.png'))
# 每格取中心像素,避免裁到边缘混合色
def get_val(idx):
sample_x = block_start_x + (idx * pixel_size) + center_offset
sample_y = center_offset
return screenshot.getpixel((sample_x, sample_y))
state = {}
# idx 0=血(红), 1=法(蓝), 2=战斗(绿), 3=目标(红)
hp_px = get_val(0)
state['hp'] = round(hp_px[0] / 255 * 100)
state['target_hp'] = round(hp_px[1] / 255 * 100)
mp_px = get_val(1)
state['mp'] = round(mp_px[2] / 255 * 100)
cb_px = get_val(2)
state['combat'] = cb_px[1] > 150 # 绿色通道
tg_px = get_val(3)
state['target'] = tg_px[0] > 150 # 红色通道
# 第 5 像素:后勤与死亡 (R=空格数, G=耐久比例, B=死亡状态)
p5 = get_val(4)
state['free_slots'] = p5[0]
state['durability'] = round(p5[1] / 255.0, 3)
state['death_state'] = 0 if p5[2] < 50 else (1 if p5[2] < 200 else 2) # 0存活 1尸体 2灵魂
# 第 6 像素x 坐标
p6 = get_val(5)
state['x'] = round((p6[0] + p6[1]/100), 2)
raw_deg = (p6[2] / 255) * 360.0
state['facing'] = (360.0 - raw_deg) % 360.0
# 第 7 像素y 坐标
p7 = get_val(6)
state['y'] = round((p7[0] + p7[1]/100), 2)
return state
if __name__ == "__main__":
print("开始监控魔兽状态... (Ctrl+C 退出)")
try:
while True:
state = parse_game_state()
if state:
death_txt = ('存活', '尸体', '灵魂')[state.get('death_state', 0)]
print(
f"\r[状态] 血:{state['hp']}% 法:{state['mp']}% 目标血:{state.get('target_hp', 0)}% | "
f"战斗:{'Y' if state['combat'] else 'N'} 目标:{'Y' if state['target'] else 'N'} | "
f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | "
f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}°",
end=""
)
time.sleep(0.5)
except KeyboardInterrupt:
print("\n已停止。")

8
game_state_config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"pixel_size": 17,
"block_start_x": 30,
"scan_region_width": 155,
"scan_region_height": 15,
"offset_left": 20,
"offset_top": 45
}

BIN
images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
images/bl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
images/ls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
images/ls1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
images/yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

85
logistics_manager.py Normal file
View File

@@ -0,0 +1,85 @@
import json
import math
import time
import pydirectinput
# 修理商所在位置(游戏坐标),按实际位置修改
VENDOR_POS = (30.08, 71.51)
# 到达判定距离(与 coordinate_patrol 一致)
VENDOR_ARRIVAL_THRESHOLD = 0.1
# 修理商所在位置配置文件
VENDOR_FILE = 'vendor.json'
class LogisticsManager:
def __init__(self, route_file=None):
self.need_repair = False
self.bag_full = False
self.is_returning = False
self.route_file = route_file or VENDOR_FILE
def check_logistics(self, state):
"""
state['free_slots']: 剩余空格数量
state['durability']: 0.0 ~ 1.0 的耐久度
"""
# 触发阈值:空格少于 2 个,或耐久度低于 20%
if state['free_slots'] < 2 or state['durability'] < 0.2:
if not self.is_returning:
print(f">>> [后勤警告] 背包/耐久不足!触发回城程序。")
self.is_returning = True
else:
self.is_returning = False
def return_home(self):
"""执行回城动作"""
# 1. 停止当前巡逻
# 2. 寻找安全点或直接使用炉石
print(">>> 正在释放炉石...")
pydirectinput.press('7') # 假设炉石在 7 号键
time.sleep(15) # 等待炉石施法
def handle_town_visit(self, state, patrol):
"""回城流程:用 state 取当前坐标与朝向,调用 patrol.navigate_to_point 前往修理商;到达后按 F3 交互"""
is_arrived = patrol.navigate_to_point(
state, VENDOR_POS,
arrival_threshold=VENDOR_ARRIVAL_THRESHOLD,
)
if is_arrived:
print(">>> 到达修理商,执行交互宏")
self._do_vendor_interact()
self.is_returning = False
def _do_vendor_interact(self):
"""执行与修理商/背包的交互按键8、4"""
pydirectinput.press("8")
time.sleep(0.5)
pydirectinput.press("4")
time.sleep(2)
def run_route1_round(self, get_state, patrol, route_file=None):
"""
读取 route1.json 路径先正向走完执行交互8、4再反向走完然后结束。
get_state: 可调用对象,返回当前状态 dict含 x, y, facing
patrol: CoordinatePatrol 实例,用于 navigate_path。
route_file: 路径 JSON 文件路径,默认使用 __init__ 中的 route_file。
"""
route_file = route_file or self.route_file
with open(route_file, "r", encoding="utf-8") as f:
path = json.load(f)
if not path:
print(">>> [后勤] route1 为空,跳过")
return
print(">>> [后勤] 开始 route1 正向")
ok = patrol.navigate_path(get_state, path, forward=True, arrival_threshold=VENDOR_ARRIVAL_THRESHOLD)
if not ok:
print(">>> [后勤] 正向未完成,中止")
return
print(">>> [后勤] 正向到达,执行交互")
self._do_vendor_interact()
print(">>> [后勤] 开始 route1 反向")
ok = patrol.navigate_path(get_state, path, forward=False, arrival_threshold=VENDOR_ARRIVAL_THRESHOLD)
if not ok:
print(">>> [后勤] 反向未完成")
return
print(">>> [后勤] route1 往返结束")

51
main.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
魔兽世界自动打怪主入口模块
使用图像识别技术定位怪物并模拟攻击操作
"""
import sys
import logging
from typing import Optional, Tuple
from combat_engine import CombatEngine
from config import GameConfig
def setup_logging() -> None:
"""配置日志记录"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('combat.log'),
logging.StreamHandler()
]
)
def main() -> int:
"""主入口函数"""
try:
setup_logging()
logger = logging.getLogger(__name__)
# 加载配置
config = GameConfig.load_config()
logger.info("游戏配置加载成功")
# 初始化战斗引擎
engine = CombatEngine(config)
logger.info("战斗引擎初始化完成")
# 启动自动战斗
logger.info("启动自动打怪系统...")
engine.run_combat_loop()
return 0
except KeyboardInterrupt:
logger.info("用户中断程序执行")
return 0
except Exception as e:
logger.critical(f"程序异常终止: {str(e)}", exc_info=True)
return 1
if __name__ == "__main__":
sys.exit(main())

284
player_movement.py Normal file
View File

@@ -0,0 +1,284 @@
import math
import time
import logging
import pyautogui
from player_position import PlayerPosition
# 游戏朝向约定:正北=0°正西=90°正南=180°正东=270°逆时针递增
# 游戏坐标约定人物面朝正北按w一秒坐标y会增加0.1 反之减少0.1 。如果人物面朝正东按w一秒坐标x会增加0.1 如果人物面朝正西按w一秒坐标x会减少0.1 。
# 按键w前进按键a左转按键d右转按键s后退 (转向时最好按0.5s就停止,以免转向过度)
# 当前人物的坐标和朝向根据player_position.py模块获取
class PlayerMovement:
MOVE_SPEED = 0.1 # 每秒移动距离(坐标单位)
TURN_RATE = 216.0 # 每秒转向角度(度);实测约 216°/s0.5s ≈ 108°
MAX_TURN_DURATION = 0.5 # 原地转向单次最长按键时间(秒),避免转向过度
SMOOTH_TURN_DURATION = 0.15 # 边走边转时每次按键时长(秒)
ANGLE_TOLERANCE = 5.0 # 朝向允许误差(度)
COARSE_TURN_THRESHOLD = 90.0 # 超过此角度差则停下来原地粗对齐,再继续行走
HEADING_CHECK_INTERVAL = 5 # 每隔几次位置读取才做一次朝向校正
STUCK_CHECK_COUNT = 3 # 连续多少次坐标无明显位移则判定为卡死
STUCK_MOVE_THRESHOLD = 0.05 # 每次检测周期内最小期望位移(坐标单位)
# 实测方向:按 a 朝向减小(右转),按 d 朝向增大(左转)
# 游戏朝向为顺时针:北=0°, 东=90°, 南=180°, 西=270°
def __init__(self):
self.player_position = PlayerPosition()
self.logger = logging.getLogger(__name__)
def get_required_heading(self, current_x: float, current_y: float,
target_x: float, target_y: float) -> float:
"""计算从当前位置朝向目标位置所需的游戏朝向角度。
实测游戏规律:
朝向 0° → 南y 减小),朝向 180° → 北y 增加)
朝向 90° → 东x 增加),朝向 270° → 西x 减小)
dx_actual ∝ sin(heading)dy_actual ∝ -cos(heading)
因此目标朝向 = atan2(dx, -dy)
验证:需北移(dy>0) → atan2(0,-1)=180° ✓;需东移(dx>0) → atan2(1,0)=90° ✓
"""
dx = target_x - current_x
dy = target_y - current_y
if dx == 0 and dy == 0:
return 0.0
return math.degrees(math.atan2(dx, -dy)) % 360
def turn_to_heading(self, target_heading: float, max_attempts: int = 10) -> bool:
"""转向到目标朝向。
按键 a 左转(朝向增大),按键 d 右转(朝向减小)。
每次按键不超过 MAX_TURN_DURATION 秒,多次微调直到误差在 ANGLE_TOLERANCE 内。
Args:
target_heading: 目标朝向角度0~360
max_attempts: 最大调整次数
Returns:
bool: 是否成功对准目标朝向
"""
for attempt in range(max_attempts):
current_heading = self.player_position.get_heading_with_retry()
if current_heading is None:
self.logger.warning("无法获取当前朝向,转向中止")
return False
diff = (target_heading - current_heading) % 360
if diff <= self.ANGLE_TOLERANCE or diff >= (360 - self.ANGLE_TOLERANCE):
self.logger.info(f"朝向已对齐:当前 {current_heading:.1f}°,目标 {target_heading:.1f}°")
return True
# 实测d 使朝向增大a 使朝向减小(与按键名称相反)
# diff ≤ 180需增大朝向 → 按 ddiff > 180需减小朝向 → 按 a
if diff <= 180:
key, angle_to_turn = 'd', diff
else:
key, angle_to_turn = 'a', 360 - diff
turn_duration = min(angle_to_turn / self.TURN_RATE, self.MAX_TURN_DURATION)
self.logger.info(
f"[转向 {attempt + 1}] 当前 {current_heading:.1f}° → 目标 {target_heading:.1f}°,"
f"'{key}' {turn_duration:.2f}s需转 {angle_to_turn:.1f}°)"
)
pyautogui.keyDown(key)
time.sleep(turn_duration)
pyautogui.keyUp(key)
time.sleep(0.15) # 等待游戏刷新朝向
self.logger.warning(f"转向失败:超过最大尝试次数 {max_attempts}")
return False
def move_forward(self, duration: float):
"""按住 w 键向前移动指定时间。
Args:
duration: 移动时间(秒)
"""
self.logger.info(f"向前移动 {duration:.2f}s")
pyautogui.keyDown('w')
time.sleep(duration)
pyautogui.keyUp('w')
def _escape_stuck(self):
"""卡死脱困组合拳:后退 + 随机转向 + 跳跃。
当连续多次检测到坐标无明显位移时调用,尝试脱离障碍物。
"""
import random
self.logger.warning("检测到角色卡死,执行脱困动作")
# 1. 松开前进键,后退 0.8s
pyautogui.keyUp('w')
pyautogui.keyDown('s')
time.sleep(0.8)
pyautogui.keyUp('s')
# 2. 随机左转或右转 0.3~0.5s
turn_key = random.choice(['a', 'd'])
turn_dur = random.uniform(0.3, 0.5)
pyautogui.keyDown(turn_key)
time.sleep(turn_dur)
pyautogui.keyUp(turn_key)
# 3. 跳跃(按空格)同时向前冲 0.5s
pyautogui.keyDown('w')
pyautogui.press('space')
time.sleep(0.5)
pyautogui.keyUp('w')
self.logger.info("脱困动作完成,重新开始前进")
def move_to(self, target_x: float, target_y: float,
position_tolerance: float = 0.5, max_iterations: int = 100) -> bool:
"""平滑移动人物到目标坐标(边走边转向)。
策略:
1. 起步前若角度偏差 > COARSE_TURN_THRESHOLD先原地粗对齐
2. 按住 w 持续前进,每次读取朝向后同时按 a/d 微调方向(边走边转);
3. 若行走中出现大角度偏差(绕过障碍等),短暂停步原地修正后继续;
4. 到达判定半径内松开 w结束移动。
Args:
target_x, target_y: 目标坐标
position_tolerance: 到达判定半径(坐标单位),默认 0.5
max_iterations: 最大循环次数,防止死循环
Returns:
bool: 是否成功到达目标位置
"""
self.logger.info(f"开始平滑移动到目标位置 ({target_x}, {target_y})")
# 起步前先判断是否已在目标范围内,避免重复调用时原地乱转
init_pos = self.player_position.get_position_with_retry()
if init_pos is None:
self.logger.warning("无法获取当前坐标,移动中止")
return False
if (abs(target_x - init_pos[0]) <= position_tolerance and
abs(target_y - init_pos[1]) <= position_tolerance):
self.logger.info(f"已在目标位置 ({target_x}, {target_y}) 范围内,无需移动")
return True
# 起步前粗对齐:角度偏差过大时先原地转向,避免一开始就走偏
init_heading = self.player_position.get_heading_with_retry()
if init_heading is not None:
init_required = self.get_required_heading(init_pos[0], init_pos[1], target_x, target_y)
init_diff = (init_required - init_heading) % 360
init_abs_diff = init_diff if init_diff <= 180 else 360 - init_diff
if init_abs_diff > self.COARSE_TURN_THRESHOLD:
self.logger.info(f"起步角度差 {init_abs_diff:.1f}°,先原地粗对齐")
if not self.turn_to_heading(init_required):
self.logger.warning("起步粗对齐失败,移动中止")
return False
# 按住 w 开始持续前进
pyautogui.keyDown('w')
# 卡死检测:记录最近 STUCK_CHECK_COUNT 次坐标,用于判断是否停滞
recent_positions = []
try:
for iteration in range(max_iterations):
# 每隔 HEADING_CHECK_INTERVAL 次才读一次朝向并校正
# 读取当前位置OCR 耗时期间 w 持续按住,角色一直在走)
current_pos = self.player_position.get_position_with_retry()
if current_pos is None:
self.logger.warning("无法获取当前坐标,移动中止")
return False
current_x, current_y = current_pos
dx = target_x - current_x
dy = target_y - current_y
distance = math.sqrt(dx ** 2 + dy ** 2)
self.logger.info(
f"[平滑 {iteration + 1}] 当前 ({current_x}, {current_y})"
f"目标 ({target_x}, {target_y}),剩余距离 {distance:.2f}"
)
# 卡死检测:维护滑动窗口,比较最新与最旧坐标的位移
recent_positions.append((current_x, current_y))
if len(recent_positions) > self.STUCK_CHECK_COUNT:
recent_positions.pop(0)
if len(recent_positions) == self.STUCK_CHECK_COUNT:
oldest_x, oldest_y = recent_positions[0]
moved = math.sqrt((current_x - oldest_x) ** 2 + (current_y - oldest_y) ** 2)
if moved < self.STUCK_MOVE_THRESHOLD:
self._escape_stuck()
recent_positions.clear()
pyautogui.keyDown('w')
# x 和 y 都在容差范围内即视为到达
if abs(dx) <= position_tolerance and abs(dy) <= position_tolerance:
self.logger.info(f"已到达目标位置 ({target_x}, {target_y})")
return True
# 接近目标时松开 w 停下,再做一次静止判定,避免冲过头后死循环
if distance <= position_tolerance * 2:
pyautogui.keyUp('w')
time.sleep(0.2)
final_pos = self.player_position.get_position_with_retry()
if final_pos is not None:
fdx = target_x - final_pos[0]
fdy = target_y - final_pos[1]
if abs(fdx) <= position_tolerance and abs(fdy) <= position_tolerance:
self.logger.info(f"已到达目标位置 ({target_x}, {target_y})")
return True
# 静止后仍未到达,重新按住 w 继续前进
pyautogui.keyDown('w')
# 每隔 HEADING_CHECK_INTERVAL 次读一次朝向,减少 OCR 频率
if iteration % self.HEADING_CHECK_INTERVAL != 0:
continue
current_heading = self.player_position.get_heading_with_retry()
if current_heading is None:
continue
required_heading = self.get_required_heading(current_x, current_y, target_x, target_y)
diff = (required_heading - current_heading) % 360
abs_diff = diff if diff <= 180 else 360 - diff
if abs_diff > self.COARSE_TURN_THRESHOLD:
# 偏差过大:停步原地粗修正,再重新按住 w
pyautogui.keyUp('w')
self.logger.info(f"偏差 {abs_diff:.1f}° 过大,停步原地修正")
if not self.turn_to_heading(required_heading):
self.logger.warning("修正转向失败,移动中止")
return False
pyautogui.keyDown('w')
elif abs_diff > self.ANGLE_TOLERANCE:
# 小幅偏差:边走边转,同时按住 w 和转向键
# diff ≤ 180 需增大朝向 → ddiff > 180 需减小朝向 → a
turn_key = 'd' if diff <= 180 else 'a'
turn_time = min(abs_diff / self.TURN_RATE, self.SMOOTH_TURN_DURATION)
self.logger.info(
f"边走边转:按 '{turn_key}' {turn_time:.2f}s偏差 {abs_diff:.1f}°)"
)
pyautogui.keyDown(turn_key)
time.sleep(turn_time)
pyautogui.keyUp(turn_key)
else:
# 方向已对齐,让角色多走一段再重新检测,减少 OCR 频率
time.sleep(1)
finally:
pyautogui.keyUp('w')
self.logger.warning(f"移动失败:超过最大迭代次数 {max_iterations}")
return False
def move_to_position(self, target_x: float, target_y: float,
tolerance: float = 0.5, max_iterations: int = 100) -> bool:
"""移动人物到目标坐标combat_engine 对外接口)。
Args:
target_x, target_y: 目标坐标
tolerance: 到达判定半径(坐标单位),默认 0.5
max_iterations: 最大循环次数
Returns:
bool: 是否成功到达目标位置
"""
return self.move_to(target_x, target_y,
position_tolerance=tolerance,
max_iterations=max_iterations)

148
player_position.py Normal file
View File

@@ -0,0 +1,148 @@
"""
玩家位置模块,用于获取人物当前所在坐标
"""
import os
import time
import re
from typing import Optional, Tuple
import cv2
import numpy as np
import pyautogui
from text_finder import TextFinder
class PlayerPosition:
"""玩家位置类,用于获取人物当前所在坐标"""
def __init__(self):
self.screen_width, self.screen_height = pyautogui.size()
self.text_finder = TextFinder()
def get_position(self) -> Optional[Tuple[float, float]]:
"""获取人物当前所在坐标
Returns:
Optional[Tuple[float, float]]: 坐标 (x, y),如果识别失败返回 None
"""
try:
# 截图左上角区域
region_width = self.screen_width // 3
region_height = self.screen_height // 4
screenshot = pyautogui.screenshot(region=(0, 0, region_width, region_height))
os.makedirs('screenshot', exist_ok=True)
screenshot.save('screenshot/position_screenshot.png')
# 使用 text_finder 识别所有文字
text_info_list = self.text_finder.recognize_text_from_image(screenshot)
if not text_info_list:
print("未识别到任何文字")
return None
# 打印识别到的所有文字,用于调试
print(f"识别到的文字: {[info['text'] for info in text_info_list]}")
# 匹配坐标格式 xx / xx
# 匹配格式坐标x:25.77y:68.82 或 坐标x25.77y:68.82冒号可能被OCR漏识别
coord_pattern = re.compile(r'坐标[:]?\s*[xX][:]?\s*(\d+\.?\d*)\s*[yY][:]?\s*(\d+\.?\d*)')
for text_info in text_info_list:
text = text_info.get('text', '')
match = coord_pattern.search(text)
if match:
x_coord = float(match.group(1))
y_coord = float(match.group(2))
print(f"找到坐标: ({x_coord}, {y_coord})")
return (x_coord, y_coord)
print("未找到坐标格式")
return None
except Exception as e:
print(f"获取位置失败: {str(e)}")
return None
def get_position_with_retry(self, max_retries: int = 3, retry_delay: float = 0.5) -> Optional[Tuple[float, float]]:
"""带重试机制的获取人物当前所在坐标
Args:
max_retries (int): 最大重试次数,默认为 3
retry_delay (float): 每次重试的间隔时间(秒),默认为 0.5
Returns:
Optional[Tuple[float, float]]: 坐标 (x, y),如果识别失败返回 None
"""
for i in range(max_retries):
position = self.get_position()
if position:
return position
if i < max_retries - 1:
print(f"{i + 1} 次尝试失败,{retry_delay} 秒后重试...")
time.sleep(retry_delay)
print(f"经过 {max_retries} 次尝试,仍未获取到坐标")
return None
def get_heading(self) -> Optional[float]:
"""获取人物当前朝向
Returns:
Optional[float]: 朝向角度,如果识别失败返回 None
"""
try:
region_width = self.screen_width // 3
region_height = self.screen_height // 4
screenshot = pyautogui.screenshot(region=(0, 0, region_width, region_height))
os.makedirs('screenshot', exist_ok=True)
screenshot.save('screenshot/heading_screenshot.png')
text_info_list = self.text_finder.recognize_text_from_image(screenshot)
if not text_info_list:
print("未识别到任何文字")
return None
print(f"识别到的文字: {[info['text'] for info in text_info_list]}")
# 朝向格式可能有冒号也可能没有朝向123 或 朝向123
heading_pattern = re.compile(r'朝向[:]?\s*(\d+\.?\d*)\s*')
for text_info in text_info_list:
text = text_info.get('text', '')
text = text.replace(' ', '')
match = heading_pattern.search(text)
if match:
heading = float(match.group(1))
print(f"找到朝向: {heading}°")
return heading
print("未找到朝向格式")
return None
except Exception as e:
print(f"获取朝向失败: {str(e)}")
return None
def get_heading_with_retry(self, max_retries: int = 3, retry_delay: float = 0.5) -> Optional[float]:
"""带重试机制的获取人物当前朝向
Args:
max_retries (int): 最大重试次数,默认为 3
retry_delay (float): 每次重试的间隔时间(秒),默认为 0.5
Returns:
Optional[float]: 朝向角度,如果识别失败返回 None
"""
for i in range(max_retries):
heading = self.get_heading()
if heading is not None:
return heading
if i < max_retries - 1:
print(f"{i + 1} 次尝试失败,{retry_delay} 秒后重试...")
time.sleep(retry_delay)
print(f"经过 {max_retries} 次尝试,仍未获取到朝向")
return None

70
recorder.py Normal file
View File

@@ -0,0 +1,70 @@
import json
import math
import os
import time
from game_state import parse_game_state
# 默认配置文件名
WAYPOINTS_FILE = 'waypoints.json'
# 记录两个点之间的最小间距阈值(游戏坐标单位)
DEFAULT_MIN_DISTANCE = 0.5
class WaypointRecorder:
def __init__(self, min_distance=DEFAULT_MIN_DISTANCE, waypoints_file=None, on_point_added=None):
self.waypoints = []
self.min_distance = min_distance
self.last_pos = (0, 0)
self.waypoints_file = waypoints_file or WAYPOINTS_FILE
self.on_point_added = on_point_added # 可选回调: (pos, count) => None
def get_distance(self, p1, p2):
return math.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
def record(self, state):
"""state 来自 game_state.parse_game_state(),坐标字段为 x, y。"""
if state is None:
return
x, y = state.get('x'), state.get('y')
if x is None or y is None:
return
curr_pos = (float(x), float(y))
# 初始点记录
if self.last_pos == (0, 0):
self.add_point(curr_pos)
return
# 只有当移动距离超过阈值时才记录点
dist = self.get_distance(curr_pos, self.last_pos)
if dist >= self.min_distance:
self.add_point(curr_pos)
def add_point(self, pos):
self.waypoints.append(pos)
self.last_pos = pos
if self.on_point_added:
self.on_point_added(pos, len(self.waypoints))
else:
print(f"已记录点: {pos} | 当前总点数: {len(self.waypoints)}")
def save(self, path=None):
out_path = path or self.waypoints_file
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(self.waypoints, f, indent=4, ensure_ascii=False)
return out_path
# 使用示例(整合进你的主循环)
if __name__ == "__main__":
recorder = WaypointRecorder()
print("开始录制模式:请在游戏内跑动。按 Ctrl+C 停止并保存。")
try:
while True:
state = parse_game_state()
if state:
recorder.record(state)
time.sleep(0.5)
except KeyboardInterrupt:
path = recorder.save()
print(f"路径已保存至 {path}")

30
recorder/vendor.json Normal file
View File

@@ -0,0 +1,30 @@
[
[
85.35,
74.90
],
[
85.36,
74.93
],
[
85.66,
75.35
],
[
86.1,
75.59
],
[
86.61,
75.59
],
[
87.08,
75.82
],
[
87.24,
75.88
]
]

70
recorder/waypoints.json Normal file
View File

@@ -0,0 +1,70 @@
[
[
85.35,
74.90
],
[
85.32,
74.86
],
[
85.02,
74.38
],
[
84.8,
73.92
],
[
84.6,
73.46
],
[
84.39,
73.0
],
[
84.17,
72.55
],
[
83.88,
72.05
],
[
83.42,
71.84
],
[
83.3,
72.34
],
[
83.34,
72.9
],
[
83.37,
73.46
],
[
83.62,
73.94
],
[
83.97,
74.32
],
[
84.41,
74.67
],
[
84.94,
74.86
],
[
85.35,
74.90
]
]

View File

@@ -0,0 +1,70 @@
[
[
61.08,
73.18
],
[
61.26,
72.7
],
[
61.44,
72.21
],
[
61.62,
71.71
],
[
61.8,
71.23
],
[
61.98,
70.73
],
[
62.12,
70.23
],
[
62.3,
69.76
],
[
62.48,
69.26
],
[
62.3,
69.76
],
[
62.14,
70.25
],
[
61.99,
70.73
],
[
61.83,
71.21
],
[
61.67,
71.71
],
[
61.51,
72.2
],
[
61.33,
72.68
],
[
61.17,
73.18
]
]

View File

@@ -0,0 +1,86 @@
[
[
10.76,
60.55
],
[
11.12,
60.93
],
[
11.48,
61.31
],
[
12.01,
61.3
],
[
12.53,
61.35
],
[
12.91,
61.72
],
[
13.28,
62.08
],
[
13.8,
62.14
],
[
14.32,
62.07
],
[
14.53,
61.58
],
[
14.42,
61.04
],
[
14.31,
60.51
],
[
14.18,
60.01
],
[
14.03,
59.51
],
[
13.57,
59.28
],
[
13.05,
59.42
],
[
12.54,
59.55
],
[
12.11,
59.88
],
[
12.01,
60.41
],
[
11.53,
60.58
],
[
10.76,
60.55
]
]

View File

@@ -0,0 +1,46 @@
[
[
32.54,
41.22
],
[
32.67,
40.7
],
[
32.85,
40.19
],
[
32.98,
39.67
],
[
32.84,
39.14
],
[
32.35,
39.26
],
[
32.17,
39.76
],
[
31.98,
40.25
],
[
31.77,
40.75
],
[
31.57,
41.23
],
[
32.54,
41.22
]
]

View File

@@ -0,0 +1,246 @@
[
[
44.37,
34.77
],
[
44.37,
35.32
],
[
44.39,
35.82
],
[
44.4,
36.32
],
[
44.41,
36.82
],
[
44.42,
37.32
],
[
44.44,
37.82
],
[
44.46,
38.38
],
[
44.47,
38.94
],
[
44.49,
39.44
],
[
44.5,
39.94
],
[
44.52,
40.5
],
[
44.53,
41.0
],
[
44.55,
41.55
],
[
44.56,
42.1
],
[
44.67,
42.62
],
[
44.71,
43.12
],
[
44.71,
43.67
],
[
44.68,
44.17
],
[
44.66,
44.67
],
[
44.63,
45.17
],
[
44.6,
45.71
],
[
44.58,
46.26
],
[
44.55,
46.76
],
[
44.52,
47.26
],
[
44.54,
47.82
],
[
44.43,
48.35
],
[
44.26,
48.85
],
[
44.02,
49.29
],
[
43.73,
49.75
],
[
43.35,
50.13
],
[
42.92,
49.87
],
[
42.81,
49.35
],
[
42.77,
48.81
],
[
42.71,
48.26
],
[
42.66,
47.71
],
[
42.6,
47.15
],
[
42.56,
46.6
],
[
42.56,
46.09
],
[
42.57,
45.55
],
[
42.58,
45.05
],
[
42.58,
44.54
],
[
42.59,
44.04
],
[
42.6,
43.52
],
[
42.6,
42.97
],
[
42.62,
42.47
],
[
42.64,
41.97
],
[
42.66,
41.42
],
[
42.68,
40.87
],
[
42.69,
40.32
],
[
42.7,
39.82
],
[
42.7,
39.32
],
[
42.7,
38.82
],
[
42.71,
38.27
],
[
42.71,
37.77
],
[
42.72,
37.25
],
[
42.73,
36.75
],
[
42.76,
36.2
],
[
42.89,
35.7
],
[
43.25,
35.3
],
[
44.37,
34.77
]
]

View File

@@ -0,0 +1,26 @@
[
[
85.23,
74.7
],
[
85.51,
75.15
],
[
85.8,
75.58
],
[
86.31,
75.64
],
[
86.81,
75.63
],
[
87.26,
75.89
]
]

Some files were not shown because too many files have changed in this diff Show More