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 # 默认布局参数(与 LogicBeacon:锚点 + 9 个数据块 对齐,需盖住第 9、10 格) _DEFAULTS = { "pixel_size": 17, "block_start_x": 30, "scan_region_width": 190, "scan_region_height": 15, "offset_left": 20, "offset_top": 45, # 巡逻上马(coordinate_patrol,与 GUI「参数配置」一致) "mount_key": "x", "mount_hold_sec": 1.6, "mount_retry_after_sec": 2.0, } 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 _tri_state_from_r(channel_byte): """Lua SetColorTexture 的 R 通道 0 / 0.5 / 1.0 → 与最近档位对齐(抗截图量化误差)""" v = channel_byte / 255.0 return min((0.0, 0.5, 1.0), key=lambda t: abs(t - v)) def _tri_state_label(value, low_mid_high): """将 0 / 0.5 / 1.0 三档转为对应中文说明(抗浮点误差)""" v = float(value) if value is not None else 0.0 if v < 0.25: return low_mid_high[0] if v < 0.75: return low_mid_high[1] return low_mid_high[2] def format_game_state_line(state): """单行状态文字(中文),供 CLI 与 GUI 共用。""" 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']}%" combat_txt = '战斗中' if state['combat'] else '未战斗' target_txt = '有敌对目标' if state['target'] else '无有效目标' flight_txt = _tri_state_label( state.get('flight', 0), ('地面', '待起飞', '飞行中'), ) mounted_txt = '已上马' if state.get('mounted') else '未上马' flyable_txt = '区域可飞' if state.get('flyable') else '区域不可飞' follow_txt = _tri_state_label( state.get('follow', 0), ('未跟随', '需补跟随', '跟随中'), ) return ( f"{hp_part} | " f"{combat_txt} {target_txt} | " f"空格:{state.get('free_slots', 0)} 耐久:{state.get('durability', 0):.0%} {death_txt} | " f"x:{state['x']} y:{state['y']} 朝向:{state['facing']:.1f}° | " f"{flight_txt} {mounted_txt} {flyable_txt} {follow_txt}" ) 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 一格 → … x, y, 飞行块(pixels[9]), 跟随(pixels[10]) CHANNEL_NAMES = ( 'hp', 'mp', 'combat', 'target', 'logistics_death', 'x', 'y', 'flight_block', 'follow', ) 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) # 第 9 像素(Lua pixels[9]):R=飞行信号,G=上马,B=可飞区域 p9 = get_val(7) state['flight'] = _tri_state_from_r(p9[0]) state['mounted'] = p9[1] > 127 state['flyable'] = p9[2] > 127 # 第 10 像素(Lua pixels[10]):R=跟随信号 p10 = get_val(8) state['follow'] = _tri_state_from_r(p10[0]) return state if __name__ == "__main__": print("开始监控魔兽状态... (Ctrl+C 退出)") try: while True: state = parse_game_state() if state: print(f"\r[状态] {format_game_state_line(state)}", end="") time.sleep(0.5) except KeyboardInterrupt: print("\n已停止。")