一.大环境背景:
类似于阴阳师刷副本的时候总是有压力,需要弄个个自动点击的脚本,但是官方有检查脚本的内容,这让普通的点击屏幕的python脚本显得非常困难,必须做的像个人一样的点击才行,现在我们来探讨下如何模拟人类行为实现自动点击,设想一个坐在屏幕前的人不可能一直专注于屏幕看着阴阳师的战斗结束,会有一些其他操作也就是非阴阳师上的浮点坐标点击,比如说玩其他游戏或者静止不动,在阴阳师对战结束的时候会有不理会或者延时点击等动作,这不是一个x轴为时间的简单线性的函数,他会有各种波动,具有一定的随机性,我们先简单的设想函数x轴为时间,y轴为点击次数,斜率为专注度建立坐标,当然这是我想象的,建立出来可能具有随机性但是能从随机中找寻函数波动的规律,那是否可以建立空间坐标系,那么z轴是什么呢?
显然简单的二次函数是不能满足目的的,因为需要考虑人类点击屏幕时的:
点击的空间位置(是否在游戏窗口内)
人的注意力状态(专注 / 分心 / 离开)
点击的力度/速度(按下到抬起的时间间隔)
倘若强行构建空间坐标系则z轴候选为:
| Z轴候选 | 含义 | 优势 |
| 点击响应延迟(ms) | 战斗结束到实际点击的时间差 | 最直接反映人的反应特性 |
| 注意力权重(0~1) | 当前对游戏的专注程度 | 可驱动其他参数变化 |
| 点击误差半径(px) | 点击偏离按钮中心的距离 | 模拟手指不精准的特性 |
这样看来这三者缺一不可,如果非要选一个的话我会选择 Z 轴 = 注意力权重 α(t),因为它可以同时影响:延迟时间,点击坐标的偏移量,是否产生"游离点击"
其实在这里我想到了更好的方法就是引入状态机而非单纯坐标系,
[专注状态] → 快速响应点击,坐标精准落在按钮上
[分心状态] → 延迟响应,可能先有几次屏幕其他位置的点击
[离开状态] → 长时间无操作(静止)
[回归状态] → 从离开/分心切换回专注,有一个"反应时间"
状态之间的转移可以用马尔可夫链描述(随机版本的 FSM P(Xt+1=Sj∣Xt=Si,Xt−1…)=P(Xt+1=Sj∣Xt=Si)),每个状态有转移概率。
二.重新认识这个问题的本质
1.向量的构建
我们要建模的不是一个"点",而是一个行为向量:
B(t) = [α(t), δ(t), ε(t), S(t)]
| 符号 | 含义 | 类型 |
| α(t) | 注意力权重 (0~1) | 连续变量,隐变量 |
| δ(t) | 点击响应延迟 (ms) | 连续变量,由α驱动 |
| ε(t) | 点击误差半径 (px) | 连续变量,由α驱动 |
| S(t) | 当前状态 | 离散变量(状态机) |
α(t) 是隐变量,它不可直接观测,但它驱动了所有可观测的行为参数。这正是为什么三维坐标系不够用——真实的驱动力藏在更高维度里。
2.状态机:离散行为的骨架
S ∈ {专注, 分心, 游离, 离开, 回归}
这里我们让ai为我们画个人类专注度的图(人类的专注权重不可能一直在游戏上)
┌─────────────────────────────────┐
│ │
┌─────▼──────┐ 长时间 ┌──────────┴───┐
开始 ───► │ 专注 │ ──────────► │ 离开 │
│ α: 0.7~1.0 │ │ α: 0~0.1 │
└─────┬──────┘ └──────────┬───┘
│ 注意力下降 │ 返回
▼ │
┌─────────────┐ ┌─────────▼────┐
│ 分心 │ │ 回归 │
│ α: 0.3~0.7 │ │ α: 0.1~0.5 │
└─────┬───────┘ └─────────┬────┘
│ 完全走神 │ 稳定
▼ ▼
┌─────────────┐ ┌──────────────┐
│ 游离 │ ──────────► │ 专注 │
│ α: 0.1~0.3 │ 重新关注 └──────────────┘
└─────────────┘
状态转移矩阵(马尔可夫链)
从现在开始大多数的模块代码都由Claude完成
import numpy as np
# 每秒的状态转移概率矩阵
# 行: 当前状态, 列: 下一状态
# [专注, 分心, 游离, 离开, 回归]
TRANSITION_MATRIX = np.array([
#专注 分心 游离 离开 回归
[0.95, 0.04, 0.005, 0.004, 0.001], # 专注
[0.10, 0.80, 0.08, 0.015, 0.005], # 分心
[0.05, 0.15, 0.70, 0.08, 0.02 ], # 游离
[0.00, 0.00, 0.00, 0.85, 0.15 ], # 离开
[0.30, 0.40, 0.20, 0.05, 0.05 ], # 回归
])
STATE_NAMES = ['专注', '分心', '游离', '离开', '回归']
class StateMachine:
def __init__(self):
self.current_state = 0 # 初始为专注
self.state_duration = 0 # 在当前状态持续的时间(秒)
def transition(self):
"""根据转移矩阵决定下一个状态"""
probs = TRANSITION_MATRIX[self.current_state]
next_state = np.random.choice(len(STATE_NAMES), p=probs)
if next_state != self.current_state:
print(f"状态切换: {STATE_NAMES[self.current_state]} → {STATE_NAMES[next_state]}")
self.state_duration = 0
else:
self.state_duration += 1
self.current_state = next_state
return self.current_state
3.注意力函数 α(t):连续行为的灵魂
α(t) 不是简单的随机数,它由三层叠加构成:
α(t) = α_base(t) × α_state(S) + α_noise(t)
第一层:基础节律(人的生理周期)
我查阅了一些人类平均的疲劳周期:
def alpha_base(t):
"""
人的注意力有自然的周期性波动
- 约4~5分钟的短周期(微注意力波动)
- 约20分钟的中周期(注意力疲劳周期)
"""
short_cycle = 0.1 * np.sin(2 * np.pi * t / 240) # 4分钟周期
medium_cycle = 0.15 * np.sin(2 * np.pi * t / 1200) # 20分钟周期
base = 0.65 # 人类平均基础专注度
return base + short_cycle + medium_cycle
第二层:状态修正系数
# 不同状态下注意力的范围映射
STATE_ALPHA_RANGE = {
0: (0.70, 1.00), # 专注
1: (0.30, 0.70), # 分心
2: (0.10, 0.30), # 游离
3: (0.00, 0.10), # 离开
4: (0.10, 0.50), # 回归(注意力正在恢复)
}
def alpha_state_modifier(state, base_alpha):
"""将基础注意力映射到当前状态允许的范围内"""
low, high = STATE_ALPHA_RANGE[state]
# 将base_alpha(0~1)线性映射到[low, high]
return low + (high - low) * base_alpha
第三层:随机噪声(人的不可预测性)
def alpha_noise(t):
"""
使用1/f噪声(粉红噪声)而不是白噪声
因为人类行为的随机性更接近粉红噪声的特性
"""
# 简化版:用多个不同频率的正弦波叠加模拟
noise = 0
for freq in [0.1, 0.3, 0.7, 1.5, 3.0]:
phase = np.random.uniform(0, 2*np.pi)
noise += (1/freq) * 0.02 * np.sin(2 * np.pi * freq * t + phase)
return noise
完整的注意力计算
class AttentionModel:
def __init__(self):
self.sm = StateMachine()
self.t = 0
def get_alpha(self):
"""获取当前时刻的注意力值"""
state = self.sm.transition()
base = alpha_base(self.t)
base = np.clip(base, 0, 1)
alpha = alpha_state_modifier(state, base)
alpha += alpha_noise(self.t)
alpha = np.clip(alpha, 0.01, 1.0)
self.t += 1
return alpha, state
四、三个行为参数如何由 α(t) 驱动?
参数1:响应延迟 δ(t)
def compute_delay(alpha, state):
"""
延迟由注意力和状态共同决定
人类反应时间分布更接近对数正态分布,而非正态分布
因为反应时间有下界(生理极限约150ms)但无上界
"""
# 基础反应时间(生理极限)
BASE_RT = 150 # ms
if state == 3: # 离开状态:可能几十秒到几分钟不点
return np.random.uniform(30000, 300000) # 30秒~5分钟
# 注意力越低,均值和方差都增大
mu = BASE_RT + (1 - alpha) * 2000 # 均值:150ms ~ 2150ms
sigma = (1 - alpha) * 800 + 50 # 标准差:50ms ~ 850ms
# 对数正态分布采样(更符合真实反应时间分布)
log_mu = np.log(mu)
log_sigma = sigma / mu # 近似转换
delay = np.random.lognormal(log_mu, log_sigma)
return max(BASE_RT, delay)
参数2:点击误差 ε(t)
def compute_click_error(alpha, target_x, target_y):
"""
点击误差不是均匀圆形分布,而是椭圆形高斯分布
因为手指在某个方向上更容易偏移(通常是垂直方向)
"""
# 注意力越低,误差越大
sigma_x = (1 - alpha) * 12 + 1 # 水平误差:1px ~ 13px
sigma_y = (1 - alpha) * 18 + 1 # 垂直误差更大:1px ~ 19px
# 高斯偏移
error_x = np.random.normal(0, sigma_x)
error_y = np.random.normal(0, sigma_y)
actual_x = target_x + error_x
actual_y = target_y + error_y
return actual_x, actual_y
参数3:游离点击(分心时的随机点击)
def maybe_stray_click(alpha, state, screen_w=1080, screen_h=1920):
"""
游离点击:不在游戏按钮上的随机点击
模拟用户在玩其他东西或乱点屏幕
"""
# 游离点击概率
stray_prob = {
0: 0.00, # 专注:不会乱点
1: 0.05, # 分心:偶尔乱点
2: 0.25, # 游离:经常乱点
3: 0.00, # 离开:不点
4: 0.10, # 回归:偶尔乱点
}
if np.random.random() < stray_prob[state]:
# 游离点击倾向于集中在屏幕边缘或特定区域(比如通知栏)
region = np.random.choice(['top', 'bottom', 'random'], p=[0.3, 0.4, 0.3])
if region == 'top':
x = np.random.uniform(0, screen_w)
y = np.random.uniform(0, 100)
elif region == 'bottom':
x = np.random.uniform(0, screen_w)
y = np.random.uniform(screen_h - 200, screen_h)
else:
x = np.random.uniform(0, screen_w)
y = np.random.uniform(0, screen_h)
return (x, y)
return None
五、整合:完整的行为生成器
import time
class HumanBehaviorSimulator:
"""
完整的人类行为模拟器
将状态机 + 注意力函数 + 三个行为参数整合
"""
def __init__(self, screen_w=1080, screen_h=1920):
self.attention_model = AttentionModel()
self.screen_w = screen_w
self.screen_h = screen_h
def simulate_click(self, target_x, target_y):
"""
模拟一次人类点击行为
返回: (实际点击坐标, 延迟时间, 是否有游离点击)
"""
# 1. 获取当前注意力和状态
alpha, state = self.attention_model.get_alpha()
print(f"[状态:{STATE_NAMES[state]}] 注意力:{alpha:.2f}")
# 2. 计算响应延迟
delay = compute_delay(alpha, state)
print(f" 响应延迟: {delay:.0f}ms")
# 3. 检查是否有游离点击(在正式点击之前)
stray = maybe_stray_click(alpha, state, self.screen_w, self.screen_h)
if stray:
print(f" 游离点击: ({stray[0]:.0f}, {stray[1]:.0f})")
# 4. 计算实际点击坐标
actual_x, actual_y = compute_click_error(alpha, target_x, target_y)
print(f" 目标坐标: ({target_x}, {target_y})")
print(f" 实际坐标: ({actual_x:.0f}, {actual_y:.0f})")
return {
'delay_ms': delay,
'stray_click': stray,
'actual_pos': (actual_x, actual_y),
'alpha': alpha,
'state': STATE_NAMES[state]
}
def run(self, target_x, target_y, click_count=10):
"""连续模拟多次点击"""
results = []
for i in range(click_count):
print(f"\n=== 第 {i+1} 次点击 ===")
result = self.simulate_click(target_x, target_y)
results.append(result)
# 实际执行延迟(可接入 adb 或 pyautogui)
# time.sleep(result['delay_ms'] / 1000)
return results
# 使用示例
simulator = HumanBehaviorSimulator()
simulator.run(target_x=540, target_y=960, click_count=5)
写到这里以及初具雏形了,但是还是不能完全应用到阴阳师上,原因由以下几点
1.确实开始战斗的位置在target_x/y,但是人类每次手指点击的坐标不可能这么精确,会有微小的偏移,(我需要引入点击区域的大小)每次浮点坐标要在这个范围内运动
2.阴阳师的战斗模式是 点击开始然后在一个误差不超过2s的时间内(这个时间我也需要拟定)结束,在结束的时候点击屏幕任意区域(这个区域大小我也需要拟定)返回副本入口,再点击开始战斗,这个过程中也存在着注意力分散
3.副本入口->副本中的点击也需要关联到分心状态注意力越低 → 等待间隔越长(越心不在焉,越久才检查一次),从这种频繁的check机制来还原人类的分心状态
4.最基本的东西我开始构建的时候忘了,浮点坐标与分心耗时的取值是整数,这并不满足随机性
import numpy as np
import time
import subprocess
import threading
import json
import os
import random
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
from dataclasses import dataclass
from typing import Optional
# ── 可选导入 pyautogui(鼠标模式)──────────────────────────
try:
import pyautogui
PYAUTOGUI_OK = True
except ImportError:
PYAUTOGUI_OK = False
CONFIG_FILE = "bot_config.json"
# ============================================================
# 第一层:常量定义
# ============================================================
STATE_NAMES = {0: '专注', 1: '分心', 2: '游离', 3: '离开', 4: '回归'}
TRANSITION_MATRIX = np.array([
[0.92, 0.06, 0.015, 0.004, 0.001],
[0.08, 0.78, 0.10, 0.025, 0.015],
[0.04, 0.18, 0.68, 0.08, 0.02 ],
[0.00, 0.00, 0.05, 0.82, 0.13 ],
[0.25, 0.45, 0.20, 0.05, 0.05 ],
])
STATE_ALPHA_RANGE = {
0: (0.72, 1.00),
1: (0.28, 0.72),
2: (0.08, 0.32),
3: (0.00, 0.08),
4: (0.10, 0.52),
}
# ============================================================
# 第二层:状态机
# ============================================================
class StateMachine:
def __init__(self):
self.current_state = 0
self.state_duration = 0
def transition(self) -> int:
probs = TRANSITION_MATRIX[self.current_state]
next_state = int(np.random.choice(len(STATE_NAMES), p=probs))
if next_state != self.current_state:
self.state_duration = 0
else:
self.state_duration += 1
self.current_state = next_state
return self.current_state
# ============================================================
# 第三层:注意力模型
# ============================================================
class AttentionModel:
def __init__(self):
self.sm = StateMachine()
self.t = 0.0
self.phase_offset = np.random.uniform(0, 2 * np.pi)
self.noise_phases = [np.random.uniform(0, 2 * np.pi) for _ in range(6)]
def _base_trend(self) -> float:
t = self.t
trend = (
0.65
+ 0.08 * np.sin(2 * np.pi * t / 300 + self.phase_offset)
+ 0.06 * np.sin(2 * np.pi * t / 1200 + self.phase_offset)
+ 0.04 * np.sin(2 * np.pi * t / 3600 + self.phase_offset)
)
return float(np.clip(trend, 0.0, 1.0))
def _noise(self) -> float:
t = self.t
freqs = [0.05, 0.12, 0.31, 0.73, 1.47, 3.10]
amps = [0.030, 0.025, 0.018, 0.012, 0.008, 0.005]
noise = sum(
a * np.sin(2 * np.pi * f * t + p)
for f, a, p in zip(freqs, amps, self.noise_phases)
)
return float(noise)
def _state_map(self, state: int, base: float) -> float:
low, high = STATE_ALPHA_RANGE[state]
return low + (high - low) * base
def get_alpha(self) -> tuple:
state = self.sm.transition()
base = self._base_trend()
noise = self._noise()
raw = float(np.clip(base + noise, 0.01, 0.99))
alpha = self._state_map(state, raw)
alpha = float(np.clip(alpha, 0.01, 1.0))
self.t += 1.0
return alpha, state
# ============================================================
# 第四层:行为参数函数
# ============================================================
def compute_delay_ms(alpha: float, state: int) -> float:
if state == 3:
minutes = np.random.exponential(scale=2.0)
minutes = float(np.clip(minutes, 0.5, 1.5))
return minutes * 60 * 1000
params = {
0: (220, 0.25),
1: (700, 0.55),
2: (2000, 0.60),
4: (900, 0.45),
}
mu_ms, sigma_ratio = params.get(state, (500, 0.5))
delay = float(np.random.lognormal(np.log(mu_ms), sigma_ratio))
if state == 1 and np.random.random() < 0.08:
delay += np.random.uniform(15000, 40000)
return float(np.clip(delay, 150.0, 30000.0))
def compute_click_pos(alpha, center_x, center_y, zone_w, zone_h):
sigma_x = (1.0 - alpha) * 14.0 + 1.5
sigma_y = (1.0 - alpha) * 20.0 + 1.5
zone_rand_x = np.random.uniform(-zone_w / 2, zone_w / 2) * 0.6
zone_rand_y = np.random.uniform(-zone_h / 2, zone_h / 2) * 0.6
jitter_x = float(np.random.normal(0, sigma_x))
jitter_y = float(np.random.normal(0, sigma_y))
x = center_x + zone_rand_x + jitter_x + np.random.uniform(-0.49, 0.49)
y = center_y + zone_rand_y + jitter_y + np.random.uniform(-0.49, 0.49)
return round(x, 2), round(y, 2)
def maybe_stray_click(alpha, state, screen_w=1080, screen_h=1920):
stray_prob = {0: 0.00, 1: 0.04, 2: 0.20, 3: 0.00, 4: 0.08}
if np.random.random() < stray_prob.get(state, 0):
x = float(np.random.uniform(50, screen_w - 50))
y = float(np.random.uniform(200, screen_h - 200))
return round(x, 2), round(y, 2)
return None
# ============================================================
# 第五层:数据类
# ============================================================
@dataclass
class ClickZone:
center_x: float
center_y: float
width: float
height: float
@dataclass
class BattleConfig:
start_zone: ClickZone
end_zone: ClickZone
battle_duration_base: float = 60.0
battle_duration_error: float = 3.0
return_wait_base: float = 3.0
return_wait_max_extra: float = 600.0
# ============================================================
# 第六层:ADB 工具函数
# ============================================================
def get_screen_size_adb():
try:
result = subprocess.run("adb shell wm size", shell=True,
capture_output=True, text=True, timeout=5)
for line in result.stdout.strip().splitlines():
if "Override size:" in line:
w, h = line.split("Override size:")[-1].strip().split("x")
return int(w), int(h)
if "Physical size:" in line:
w, h = line.split("Physical size:")[-1].strip().split("x")
return int(w), int(h)
except Exception:
pass
return None
def get_cpu_abi():
try:
r = subprocess.run("adb shell getprop ro.product.cpu.abi",
shell=True, capture_output=True, text=True, timeout=5)
return r.stdout.strip()
except Exception:
return None
def get_touch_info_adb():
try:
result = subprocess.run("adb shell getevent -p", shell=True,
capture_output=True, text=True, timeout=8)
current_dev = None
max_x = max_y = None
for line in result.stdout.splitlines():
s = line.strip()
if s.startswith("add device"):
current_dev = s.split(":")[-1].strip()
max_x = max_y = None
if current_dev and ("ABS_MT_POSITION_X" in s or s.startswith("0035")) and "max" in s:
try:
max_x = int(s.split("max")[1].split(",")[0].strip())
except Exception:
pass
if current_dev and ("ABS_MT_POSITION_Y" in s or s.startswith("0036")) and "max" in s:
try:
max_y = int(s.split("max")[1].split(",")[0].strip())
except Exception:
pass
if max_x and max_y:
return {"device": current_dev, "max_x": max_x, "max_y": max_y}
except Exception:
pass
return None
def convert_touch_coord(raw_x, raw_y, screen_w, screen_h,
touch_max_x, touch_max_y):
px = int(round(raw_x / touch_max_x * screen_w))
py = int(round(raw_y / touch_max_y * screen_h))
return px, py
def check_adb_device(log_func=None) -> bool:
result = subprocess.run("adb devices", shell=True,
capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
devices = [l for l in lines[1:] if l.strip() and 'device' in l]
if not devices:
if log_func: log_func("❌ 未检测到设备!请先连接手机或启动模拟器")
return False
if log_func: log_func(f"✅ 检测到设备: {devices[0]}")
return True
# ============================================================
# 第六层(续):MinitouchManager(支持ADB + 鼠标双模式)
# ============================================================
class MinitouchManager:
"""
双模式点击管理器:
- ADB模式:adb shell input tap(需要连接手机)
- 鼠标模式:pyautogui.click(控制电脑鼠标,用于模拟器)
"""
MODE_ADB = "adb"
MODE_MOUSE = "mouse"
def __init__(self, log_func=None):
self.log = log_func or print
self._ready = False
self.mode = self.MODE_ADB # 默认ADB模式
def set_screen(self, w: int, h: int):
pass
def setup(self) -> bool:
"""自动检测:有ADB设备用ADB,否则切换鼠标模式"""
# 优先尝试ADB
r = subprocess.run(
"adb devices", shell=True,
capture_output=True, text=True, timeout=5
)
lines = r.stdout.strip().split('\n')
devices = [l for l in lines[1:] if l.strip() and 'device' in l]
if devices:
self.mode = self.MODE_ADB
self._ready = True
self.log(" ✅ ADB 模式就绪(使用 adb shell input tap)")
self.log(f" 📱 设备: {devices[0]}")
return True
# ADB不可用,尝试鼠标模式
if PYAUTOGUI_OK:
self.mode = self.MODE_MOUSE
self._ready = True
self.log(" ⚠️ 未检测到ADB设备,已切换为【鼠标模式】")
self.log(" 🖱️ 将使用 pyautogui 控制电脑鼠标")
self.log(" ⚠️ 注意:鼠标模式坐标为电脑屏幕坐标,请重新配置!")
return True
else:
self.log(" ❌ 未检测到设备,且 pyautogui 未安装")
self.log(" 💡 请运行:pip install pyautogui")
return False
def force_mouse_mode(self) -> bool:
"""强制切换为鼠标模式"""
if not PYAUTOGUI_OK:
self.log(" ❌ pyautogui 未安装,请运行:pip install pyautogui")
return False
self.mode = self.MODE_MOUSE
self._ready = True
self.log(" 🖱️ 已强制切换为鼠标模式")
return True
def force_adb_mode(self) -> bool:
"""强制切换为ADB模式"""
r = subprocess.run(
"adb devices", shell=True,
capture_output=True, text=True, timeout=5
)
lines = r.stdout.strip().split('\n')
devices = [l for l in lines[1:] if l.strip() and 'device' in l]
if devices:
self.mode = self.MODE_ADB
self._ready = True
self.log(f" ✅ 已切换为ADB模式,设备: {devices[0]}")
return True
self.log(" ❌ 未检测到ADB设备")
return False
@property
def ready(self) -> bool:
return self._ready
@property
def mode_name(self) -> str:
return "ADB手机" if self.mode == self.MODE_ADB else "鼠标(电脑)"
def tap(self, x: float, y: float, log_func=None) -> bool:
_log = log_func or self.log
ix, iy = int(round(x)), int(round(y))
if self.mode == self.MODE_ADB:
r = subprocess.run(
f"adb shell input tap {ix} {iy}",
shell=True, capture_output=True, timeout=5
)
_log(f" [ADB tap] 点击: ({ix}, {iy})")
return r.returncode == 0
elif self.mode == self.MODE_MOUSE:
if not PYAUTOGUI_OK:
_log(" ❌ pyautogui 不可用")
return False
pyautogui.click(ix, iy)
_log(f" [鼠标点击] ({ix}, {iy})")
return True
return False
def stop(self):
self._ready = False
# ============================================================
# 第七层:Bot 核心
# ============================================================
class YinYangShiBot:
def __init__(self, config: BattleConfig,
screen_w: int = 1080, screen_h: int = 1920,
minitouch: MinitouchManager = None,
log_func=None, stop_event=None):
self.config = config
self.am = AttentionModel()
self.screen_w = screen_w
self.screen_h = screen_h
self.minitouch = minitouch
self.round = 0
self.log = log_func or print
self.stop_event = stop_event or threading.Event()
def _tap(self, x: float, y: float):
if self.minitouch and self.minitouch.ready:
self.minitouch.tap(x, y, self.log)
else:
self.log(f" ❌ 点击器不可用,跳过点击 ({x:.0f},{y:.0f})")
def _stopped(self) -> bool:
return self.stop_event.is_set()
def _sleep_with_log(self, delay_ms: float, label: str = "等待"):
total_s = delay_ms / 1000
elapsed = 0.0
interval = 5.0
self.log(f" [{label}] 总等待: {total_s:.1f}s")
while elapsed < total_s:
if self._stopped():
self.log(" [中断] 检测到停止信号")
return
sleep_time = min(interval, total_s - elapsed)
time.sleep(sleep_time)
elapsed += sleep_time
self.log(f" [{label}] 已等待: {elapsed:.1f}s / {total_s:.1f}s")
def run(self, rounds: int = 10):
self.log(f"开始刷副本,计划刷 {rounds} 轮\n")
for _ in range(rounds):
if self._stopped(): break
self._one_battle()
self.log(f"\n{'='*40}")
self.log(f" 全部完成!共刷了 {self.round} 轮")
self.log(f"{'='*40}\n")
def _one_battle(self):
self.round += 1
self.log(f"\n{'='*40}")
self.log(f" 第 {self.round} 轮战斗开始")
self.log(f"{'='*40}")
self._click_start()
if self._stopped(): return
self._wait_battle()
if self._stopped(): return
self._click_end_screen()
if self._stopped(): return
self._wait_return()
def _click_start(self):
alpha, state = self.am.get_alpha()
self.log(f"\n[点击开始] 状态:{STATE_NAMES[state]} 注意力:{alpha:.4f}")
delay_ms = compute_delay_ms(alpha, state)
z = self.config.start_zone
x, y = compute_click_pos(alpha, z.center_x, z.center_y,
z.width, z.height)
self.log(f" 点击坐标: ({x:.2f}, {y:.2f})")
self.log(f" 点击延迟: {delay_ms:.1f}ms ({delay_ms/1000:.2f}s)")
self._sleep_with_log(delay_ms, "点击开始延迟")
self._tap(x, y)
def _wait_battle(self):
error = np.random.uniform(-self.config.battle_duration_error,
self.config.battle_duration_error)
battle_time = self.config.battle_duration_base + error
self.log(f"\n[等待战斗] 预计时长: {battle_time:.2f}s")
start_time = time.time()
while True:
if self._stopped(): return
elapsed = time.time() - start_time
if elapsed >= battle_time: break
time.sleep(min(1.0, max(battle_time - elapsed, 0.01)))
alpha, state = self.am.get_alpha()
elapsed = time.time() - start_time
self.log(f" [{elapsed:.2f}s/{battle_time:.2f}s] "
f"状态:{STATE_NAMES[state]} 注意力:{alpha:.4f}")
self.log(f" 战斗结束!实际等待: {time.time() - start_time:.2f}s")
def _click_end_screen(self):
alpha, state = self.am.get_alpha()
self.log(f"\n[点击结算] 状态:{STATE_NAMES[state]} 注意力:{alpha:.4f}")
delay_ms = compute_delay_ms(alpha, state)
z = self.config.end_zone
x, y = compute_click_pos(alpha, z.center_x, z.center_y,
z.width, z.height)
self.log(f" 响应延迟: {delay_ms:.1f}ms ({delay_ms/1000:.2f}s)")
self.log(f" 点击坐标: ({x:.2f}, {y:.2f})")
stray = maybe_stray_click(alpha, state, self.screen_w, self.screen_h)
if stray:
stray_delay = np.random.uniform(200, 800)
self.log(f" [误触] 坐标:({stray[0]:.2f},{stray[1]:.2f})"
f" 延迟:{stray_delay:.1f}ms")
time.sleep(stray_delay / 1000)
self._tap(*stray)
self._sleep_with_log(delay_ms, "点击返回延迟")
self._tap(x, y)
def _wait_return(self):
wait_time = random.uniform(2.0, 3.5)
self.log(f"\n[等待返回] 预计等待: {wait_time:.2f}s")
time.sleep(wait_time)
alpha, state = self.am.get_alpha()
delay_ms = compute_delay_ms(alpha, state)
return_x, return_y = 540.0, 1600.0
self.log(f" [点击返回确认] 状态:{STATE_NAMES[state]} 注意力:{alpha:.4f}")
self.log(f" 响应延迟: {delay_ms:.1f}ms ({delay_ms/1000:.2f}s)")
self.log(f" 点击坐标: ({return_x:.2f}, {return_y:.2f})")
self._sleep_with_log(delay_ms, "返回确认延迟")
self._tap(return_x, return_y)
time.sleep(random.uniform(1.5, 2.5))
# ============================================================
# 第八层:配置持久化
# ============================================================
DEFAULT_CONFIG = {
"start_center_x": 540.0,
"start_center_y": 1650.0,
"start_width": 200.0,
"start_height": 80.0,
"end_center_x": 540.0,
"end_center_y": 960.0,
"end_width": 800.0,
"end_height": 1200.0,
"battle_duration_base": 10.0,
"battle_duration_error": 3.0,
"return_wait_base": 3.0,
"return_wait_max_extra": 600.0,
"rounds": 10,
"screen_w": 1080,
"screen_h": 1920,
}
def load_config() -> dict:
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
for k, v in DEFAULT_CONFIG.items():
data.setdefault(k, v)
return data
except Exception:
pass
return DEFAULT_CONFIG.copy()
def save_config(data: dict):
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# ============================================================
# 第九层:UI
# ============================================================
class BotUI:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("阴阳师刷副本 Bot")
self.root.resizable(False, False)
self.bot_thread = None
self.stop_event = threading.Event()
self.cfg = load_config()
self.coord_listening = False
self._touch_max_x = None
self._touch_max_y = None
self._screen_w_cache = 1080
self._screen_h_cache = 1920
self.minitouch = MinitouchManager(log_func=self._log)
self._build_ui()
self._fill_fields()
def _build_ui(self):
pad = {"padx": 6, "pady": 3}
# ── 顶部:设备 + 模式切换 ──────────────────────────
top = ttk.LabelFrame(self.root, text="设备 & 模式")
top.grid(row=0, column=0, columnspan=2, sticky="ew", padx=8, pady=4)
ttk.Button(top, text="检测ADB设备",
command=self._check_device).pack(side="left", **pad)
self.device_label = ttk.Label(top, text="未检测", foreground="gray")
self.device_label.pack(side="left", **pad)
ttk.Button(top, text="🔧 自动初始化",
command=self._setup_minitouch).pack(side="left", **pad)
ttk.Button(top, text="📱 强制ADB模式",
command=self._force_adb).pack(side="left", **pad)
ttk.Button(top, text="🖱️ 强制鼠标模式",
command=self._force_mouse).pack(side="left", **pad)
self.mt_label = ttk.Label(top, text="点击器: 未初始化",
foreground="gray")
self.mt_label.pack(side="left", **pad)
# ── 左侧:参数配置 ─────────────────────────────────
left = ttk.Frame(self.root)
left.grid(row=1, column=0, sticky="n", padx=8, pady=4)
f1 = ttk.LabelFrame(left, text="开始按钮区域")
f1.grid(row=0, column=0, sticky="ew", pady=4)
self.v_sx = self._row(f1, 0, "中心X", tip="开始按钮中心X坐标")
self.v_sy = self._row(f1, 1, "中心Y", tip="开始按钮中心Y坐标")
self.v_sw = self._row(f1, 2, "宽度", tip="点击区域宽度")
self.v_sh = self._row(f1, 3, "高度", tip="点击区域高度")
ttk.Button(f1, text="📍 获取坐标",
command=lambda: self._start_coord_pick(
self.v_sx, self.v_sy, "开始按钮")
).grid(row=4, column=0, columnspan=3, pady=3)
f2 = ttk.LabelFrame(left, text="结算界面区域")
f2.grid(row=1, column=0, sticky="ew", pady=4)
self.v_ex = self._row(f2, 0, "中心X", tip="自动计算(左上+右下中点)")
self.v_ey = self._row(f2, 1, "中心Y", tip="自动计算(左上+右下中点)")
self.v_ew = self._row(f2, 2, "宽度", tip="自动计算(右下X - 左上X)")
self.v_eh = self._row(f2, 3, "高度", tip="自动计算(右下Y - 左上Y)")
ttk.Button(f2, text="📍 框选区域(左上→右下)",
command=self._pick_end_zone
).grid(row=4, column=0, columnspan=3, pady=3)
f3 = ttk.LabelFrame(left, text="战斗参数")
f3.grid(row=2, column=0, sticky="ew", pady=4)
self.v_base = self._row(f3, 0, "战斗时长(s)", tip="战斗基础时长,单位秒")
self.v_err = self._row(f3, 1, "时长误差(s)", tip="时长随机误差范围")
self.v_rwb = self._row(f3, 2, "返回等待(s)", tip="返回后固定加载等待")
self.v_rmax = self._row(f3, 3, "最长分心(s)", tip="最长分心等待上限")
self.v_rounds = self._row(f3, 4, "刷副本轮数", tip="总共刷几轮")
f4 = ttk.LabelFrame(left, text="屏幕分辨率")
f4.grid(row=3, column=0, sticky="ew", pady=4)
self.v_scw = self._row(f4, 0, "宽度(px)", tip="手机/模拟器屏幕宽度")
self.v_sch = self._row(f4, 1, "高度(px)", tip="手机/模拟器屏幕高度")
ttk.Button(f4, text="📐 自动获取分辨率",
command=self._auto_get_resolution
).grid(row=2, column=0, columnspan=3, pady=3)
bf = ttk.Frame(left)
bf.grid(row=4, column=0, pady=6)
self.btn_start = ttk.Button(bf, text="▶ 开始运行",
command=self._start_bot)
self.btn_start.pack(side="left", padx=4)
self.btn_stop = ttk.Button(bf, text="⏹ 停止",
command=self._stop_bot, state="disabled")
self.btn_stop.pack(side="left", padx=4)
ttk.Button(bf, text="💾 保存配置",
command=self._save_cfg).pack(side="left", padx=4)
# ── 右侧:日志 ────────────────────────────────────
right = ttk.LabelFrame(self.root, text="运行日志")
right.grid(row=1, column=1, sticky="nsew", padx=8, pady=4)
self.log_box = scrolledtext.ScrolledText(
right, width=52, height=38,
state="disabled", font=("Consolas", 9))
self.log_box.pack(padx=4, pady=4)
ttk.Button(right, text="清空日志",
command=self._clear_log).pack(pady=2)
# ── 底部状态栏 ────────────────────────────────────
self.status_var = tk.StringVar(value="就绪")
ttk.Label(self.root, textvariable=self.status_var,
foreground="blue").grid(row=2, column=0,
columnspan=2, pady=3)
def _row(self, parent, row, label, tip="") -> tk.StringVar:
ttk.Label(parent, text=label, width=10).grid(
row=row, column=0, padx=4, pady=2, sticky="e")
var = tk.StringVar()
ttk.Entry(parent, textvariable=var, width=12).grid(
row=row, column=1, padx=4, pady=2)
if tip:
ttk.Label(parent, text=tip, foreground="gray",
font=("", 8)).grid(row=row, column=2,
padx=2, sticky="w")
return var
def _fill_fields(self):
c = self.cfg
self.v_sx.set(c["start_center_x"]); self.v_sy.set(c["start_center_y"])
self.v_sw.set(c["start_width"]); self.v_sh.set(c["start_height"])
self.v_ex.set(c["end_center_x"]); self.v_ey.set(c["end_center_y"])
self.v_ew.set(c["end_width"]); self.v_eh.set(c["end_height"])
self.v_base.set(c["battle_duration_base"])
self.v_err.set(c["battle_duration_error"])
self.v_rwb.set(c["return_wait_base"])
self.v_rmax.set(c["return_wait_max_extra"])
self.v_rounds.set(c["rounds"])
self.v_scw.set(c["screen_w"]); self.v_sch.set(c["screen_h"])
def _read_fields(self) -> Optional[dict]:
try:
return {
"start_center_x": float(self.v_sx.get()),
"start_center_y": float(self.v_sy.get()),
"start_width": float(self.v_sw.get()),
"start_height": float(self.v_sh.get()),
"end_center_x": float(self.v_ex.get()),
"end_center_y": float(self.v_ey.get()),
"end_width": float(self.v_ew.get()),
"end_height": float(self.v_eh.get()),
"battle_duration_base": float(self.v_base.get()),
"battle_duration_error": float(self.v_err.get()),
"return_wait_base": float(self.v_rwb.get()),
"return_wait_max_extra": float(self.v_rmax.get()),
"rounds": int(self.v_rounds.get()),
"screen_w": int(self.v_scw.get()),
"screen_h": int(self.v_sch.get()),
}
except ValueError as e:
messagebox.showerror("输入错误", f"参数格式有误:{e}")
return None
def _save_cfg(self):
data = self._read_fields()
if data:
save_config(data); self.cfg = data
self.status_var.set("✅ 配置已保存")
def _log(self, msg: str):
def _w():
self.log_box.config(state="normal")
self.log_box.insert("end", msg + "\n")
self.log_box.see("end")
self.log_box.config(state="disabled")
self.root.after(0, _w)
def _clear_log(self):
self.log_box.config(state="normal")
self.log_box.delete("1.0", "end")
self.log_box.config(state="disabled")
def _update_mt_label(self):
if self.minitouch.ready:
mode = self.minitouch.mode_name
self.mt_label.config(
text=f"点击器: {mode} ✅", foreground="green")
self.status_var.set(f"✅ 点击器就绪({mode})")
else:
self.mt_label.config(
text="点击器: 失败 ❌", foreground="red")
self.status_var.set("❌ 点击器初始化失败,查看日志")
def _check_device(self):
def _do():
ok = check_adb_device(self._log)
self.root.after(0, lambda: self.device_label.config(
text="已连接 ✅" if ok else "未连接 ❌",
foreground="green" if ok else "red"))
threading.Thread(target=_do, daemon=True).start()
def _setup_minitouch(self):
"""自动初始化:有ADB用ADB,否则用鼠标"""
def _do():
self.root.after(0, lambda: self.mt_label.config(
text="点击器: 初始化中...", foreground="orange"))
self._log("\n🔧 开始自动初始化点击器...")
ok = self.minitouch.setup()
self.root.after(0, self._update_mt_label)
threading.Thread(target=_do, daemon=True).start()
def _force_adb(self):
"""强制切换ADB模式"""
def _do():
self._log("\n📱 强制切换ADB模式...")
ok = self.minitouch.force_adb_mode()
self.root.after(0, self._update_mt_label)
threading.Thread(target=_do, daemon=True).start()
def _force_mouse(self):
"""强制切换鼠标模式"""
def _do():
self._log("\n🖱️ 强制切换鼠标模式...")
ok = self.minitouch.force_mouse_mode()
self.root.after(0, self._update_mt_label)
if ok:
self.root.after(0, lambda: messagebox.showinfo(
"鼠标模式",
"已切换为鼠标模式!\n\n"
"⚠️ 注意:\n"
"坐标现在是电脑屏幕坐标\n"
"请重新配置开始/结算区域坐标\n"
"建议使用「📍 获取坐标」在屏幕上点击获取"))
threading.Thread(target=_do, daemon=True).start()
def _auto_get_resolution(self):
def _do():
result = subprocess.run("adb devices", shell=True,
capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
devices = [l for l in lines[1:] if l.strip() and 'device' in l]
if devices:
size = get_screen_size_adb()
if size:
w, h = size
source = "手机(ADB)"
info = get_touch_info_adb()
if info:
self._touch_max_x = info["max_x"]
self._touch_max_y = info["max_y"]
self._log(f" 📐 触摸硬件范围: "
f"{info['max_x']} x {info['max_y']}")
self._screen_w_cache = w
self._screen_h_cache = h
self.minitouch.set_screen(w, h)
else:
w, h = (self.root.winfo_screenwidth(),
self.root.winfo_screenheight())
source = "电脑屏幕(回退)"
else:
w, h = (self.root.winfo_screenwidth(),
self.root.winfo_screenheight())
source = "电脑屏幕"
def _fill():
self.v_scw.set(w); self.v_sch.set(h)
self._log(f" 📐 分辨率 [{source}]: {w} x {h}")
self.status_var.set(f"📐 {w}x{h} [{source}]")
self.root.after(0, _fill)
threading.Thread(target=_do, daemon=True).start()
def _raw_to_pixel(self, raw_x: float, raw_y: float):
if self._touch_max_x and self._touch_max_y:
return convert_touch_coord(
raw_x, raw_y,
self._screen_w_cache, self._screen_h_cache,
self._touch_max_x, self._touch_max_y)
return int(round(raw_x)), int(round(raw_y))
def _start_bot(self):
data = self._read_fields()
if not data: return
if not self.minitouch.ready:
if not messagebox.askyesno(
"提示",
"点击器未就绪,点击将无法生效。\n"
"建议先点击「🔧 自动初始化」。\n\n"
"是否仍要继续?"
):
return
save_config(data)
config = BattleConfig(
start_zone=ClickZone(
data["start_center_x"], data["start_center_y"],
data["start_width"], data["start_height"]),
end_zone=ClickZone(
data["end_center_x"], data["end_center_y"],
data["end_width"], data["end_height"]),
battle_duration_base = data["battle_duration_base"],
battle_duration_error = data["battle_duration_error"],
return_wait_base = data["return_wait_base"],
return_wait_max_extra = data["return_wait_max_extra"],
)
self.stop_event.clear()
bot = YinYangShiBot(
config,
screen_w = data["screen_w"],
screen_h = data["screen_h"],
minitouch = self.minitouch,
log_func = self._log,
stop_event = self.stop_event,
)
def _run():
self.root.after(0, lambda: self.btn_start.config(state="disabled"))
self.root.after(0, lambda: self.btn_stop.config(state="normal"))
bot.run(rounds=data["rounds"])
self.root.after(0, lambda: self.btn_start.config(state="normal"))
self.root.after(0, lambda: self.btn_stop.config(state="disabled"))
self.root.after(0, lambda: self.status_var.set("✅ 运行完成"))
self.bot_thread = threading.Thread(target=_run, daemon=True)
self.bot_thread.start()
self.status_var.set(
f"🚀 运行中...({self.minitouch.mode_name})")
def _stop_bot(self):
self.stop_event.set()
self.status_var.set("⏹ 已发送停止信号...")
self._log("⏹ 用户请求停止,等待当前步骤完成...")
def _has_adb(self) -> bool:
result = subprocess.run("adb devices", shell=True,
capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
return bool([l for l in lines[1:] if l.strip() and 'device' in l])
def _start_coord_pick(self, var_x, var_y, label):
if self.coord_listening:
self._log("⚠️ 已有坐标监听在运行"); return
if self._has_adb():
self._start_coord_pick_adb(var_x, var_y, label)
else:
self._start_coord_pick_screen_single(var_x, var_y, label)
def _pick_end_zone(self):
if self.coord_listening:
self._log("⚠️ 已有坐标监听在运行"); return
if self._has_adb():
self._pick_end_zone_adb()
else:
self._pick_end_zone_screen()
def _start_coord_pick_adb(self, var_x, var_y, label):
self.coord_listening = True
self._log(f"\n📍 [ADB] 请点击手机屏幕上的【{label}】...")
def _listen():
try:
proc = subprocess.Popen("adb shell getevent -l", shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
x_val = y_val = None
timeout = time.time() + 15
for line in proc.stdout:
if time.time() > timeout:
self._log("⚠️ 超时"); break
line = line.strip()
if "ABS_MT_POSITION_X" in line:
try: x_val = int(line.split()[-1], 16)
except Exception: pass
elif "ABS_MT_POSITION_Y" in line:
try: y_val = int(line.split()[-1], 16)
except Exception: pass
if x_val is not None and y_val is not None:
px, py = self._raw_to_pixel(float(x_val), float(y_val))
def _fill(fx=float(px), fy=float(py)):
var_x.set(fx); var_y.set(fy)
self._log(f" ✅ 坐标: ({fx}, {fy})")
self.status_var.set(f"✅ 【{label}】: ({fx},{fy})")
self.coord_listening = False
self.root.after(0, _fill)
proc.kill(); return
proc.kill()
except Exception as e:
self._log(f" ❌ 监听失败: {e}")
finally:
self.coord_listening = False
threading.Thread(target=_listen, daemon=True).start()
def _pick_end_zone_adb(self):
self.coord_listening = True
self._log("\n📍 [ADB框选] 请点击结算界面【左上角】...")
def _listen_once(on_got):
proc = subprocess.Popen("adb shell getevent -l", shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True)
x_val = y_val = None
timeout = time.time() + 20
for line in proc.stdout:
if time.time() > timeout: proc.kill(); return
line = line.strip()
if "ABS_MT_POSITION_X" in line:
try: x_val = int(line.split()[-1], 16)
except Exception: pass
elif "ABS_MT_POSITION_Y" in line:
try: y_val = int(line.split()[-1], 16)
except Exception: pass
if x_val is not None and y_val is not None:
proc.kill()
px, py = self._raw_to_pixel(float(x_val), float(y_val))
on_got(float(px), float(py)); return
proc.kill()
def _step1():
def _got_tl(x1, y1):
self._log(f" ✅ 左上角: ({x1:.0f}, {y1:.0f})")
self.root.after(0, lambda: self._log("📍 请点击【右下角】..."))
time.sleep(0.8)
_listen_once(lambda x2, y2: _got_br(x1, y1, x2, y2))
_listen_once(_got_tl)
def _got_br(x1, y1, x2, y2):
cx = round((x1+x2)/2, 2); cy = round((y1+y2)/2, 2)
w = round(abs(x2-x1), 2); h = round(abs(y2-y1), 2)
def _fill():
self.v_ex.set(cx); self.v_ey.set(cy)
self.v_ew.set(w); self.v_eh.set(h)
self._log(f" ✅ 结算区域: 中心({cx},{cy}) {w}×{h}")
self.coord_listening = False
self.root.after(0, _fill)
threading.Thread(target=_step1, daemon=True).start()
def _pick_end_zone_screen(self):
self.coord_listening = True
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
overlay = tk.Toplevel(self.root)
overlay.attributes("-fullscreen", True)
overlay.attributes("-alpha", 0.35)
overlay.attributes("-topmost", True)
overlay.config(bg="black")
canvas = tk.Canvas(overlay, width=sw, height=sh,
bg="black", highlightthickness=0,
cursor="crosshair")
canvas.pack(fill="both", expand=True)
hint_id = canvas.create_text(sw//2, int(sh*0.06),
text="第1步:点击【左上角】 ESC取消",
font=("", 18, "bold"), fill="white")
coord_id = canvas.create_text(sw//2, int(sh*0.12),
text="", font=("Consolas", 14),
fill="#00ff99")
step = [1]; pt1 = [None, None]
def _on_move(e):
canvas.delete("cross")
canvas.create_line(e.x-25, e.y, e.x+25, e.y,
fill="red", width=2, tags="cross")
canvas.create_line(e.x, e.y-25, e.x, e.y+25,
fill="red", width=2, tags="cross")
canvas.itemconfig(coord_id, text=f"X={e.x} Y={e.y}")
def _on_click(e):
if step[0] == 1:
pt1[0], pt1[1] = e.x, e.y; step[0] = 2
canvas.itemconfig(hint_id,
text="第2步:点击【右下角】 ESC取消")
canvas.create_oval(e.x-6, e.y-6, e.x+6, e.y+6,
fill="yellow", outline="orange",
width=2, tags="mark")
else:
cx = round((pt1[0]+e.x)/2, 2)
cy = round((pt1[1]+e.y)/2, 2)
w = round(abs(e.x-pt1[0]), 2)
h = round(abs(e.y-pt1[1]), 2)
self.v_ex.set(cx); self.v_ey.set(cy)
self.v_ew.set(w); self.v_eh.set(h)
self._log(f" ✅ 结算区域: 中心({cx},{cy}) {w}×{h}")
self.coord_listening = False
overlay.destroy()
def _on_cancel(e=None):
self.coord_listening = False; overlay.destroy()
canvas.bind("<Motion>", _on_move)
canvas.bind("<Button-1>", _on_click)
canvas.bind("<Escape>", _on_cancel)
overlay.bind("<Escape>", _on_cancel)
overlay.protocol("WM_DELETE_WINDOW", _on_cancel)
canvas.focus_set()
def _start_coord_pick_screen_single(self, var_x, var_y, label):
self.coord_listening = True
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
overlay = tk.Toplevel(self.root)
overlay.attributes("-fullscreen", True)
overlay.attributes("-alpha", 0.35)
overlay.attributes("-topmost", True)
overlay.config(bg="black")
cc = tk.Canvas(overlay, width=sw, height=sh,
bg="black", highlightthickness=0,
cursor="crosshair")
cc.pack(fill="both", expand=True)
cc.create_text(sw//2, int(sh*0.08),
text=f"请点击【{label}】 ESC取消",
font=("", 18, "bold"), fill="white")
cid = cc.create_text(sw//2, int(sh*0.14),
text="", font=("Consolas", 14),
fill="#00ff99")
def _on_move(e):
cc.delete("cross")
cc.create_line(e.x-25, e.y, e.x+25, e.y,
fill="red", width=2, tags="cross")
cc.create_line(e.x, e.y-25, e.x, e.y+25,
fill="red", width=2, tags="cross")
cc.itemconfig(cid, text=f"X={e.x} Y={e.y}")
def _on_click(e):
var_x.set(e.x); var_y.set(e.y)
self._log(f" ✅ 坐标: ({e.x}, {e.y})")
self.coord_listening = False
overlay.destroy()
def _on_cancel(e=None):
self.coord_listening = False; overlay.destroy()
cc.bind("<Motion>", _on_move)
cc.bind("<Button-1>", _on_click)
cc.bind("<Escape>", _on_cancel)
overlay.bind("<Escape>", _on_cancel)
overlay.protocol("WM_DELETE_WINDOW", _on_cancel)
cc.focus_set()
# ============================================================
# 入口
# ============================================================
if __name__ == "__main__":
root = tk.Tk()
app = BotUI(root)
root.mainloop()
765

被折叠的 条评论
为什么被折叠?



