Files
wow/game_state.py
2026-03-23 09:50:08 +08:00

222 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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已停止。")