167 lines
5.0 KiB
Python
167 lines
5.0 KiB
Python
|
|
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已停止。")
|