mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-06-01 00:25:50 +08:00
390 lines
17 KiB
JavaScript
390 lines
17 KiB
JavaScript
const ocrRegionConfig = {
|
||
activity: {x: 267, y: 197, width: 226, height: 616},//活动识别区域坐标和尺寸
|
||
remainingTime: {x: 497, y: 202, width: 1417, height: 670},//剩余时间识别区域坐标和尺寸
|
||
}
|
||
const xyConfig = {
|
||
top: {x: 344, y: 273},
|
||
bottom: {x: 342, y: 791},
|
||
}
|
||
|
||
export class scroll {
|
||
/**
|
||
* 模拟页面滚动功能
|
||
* @param {number} totalDistance - 总滚动距离
|
||
* @param {boolean} [isUp=false] - 是否向上滚动,默认为向下滚动
|
||
* @param {number} [waitCount=6] - 每隔多少步等待一次,默认为6步
|
||
* @param {number} [stepDistance=30] - 每次滚动的步长距离,默认为30像素
|
||
* @param {number} [delayMs=1000] - 每次等待的毫秒数,默认为1000毫秒
|
||
* @returns {Promise<void>} - 返回一个Promise,表示滚动操作完成
|
||
*/
|
||
static async page(totalDistance, isUp = false, waitCount = 6, stepDistance = 30, delayMs = 1000) {
|
||
let ms = 600 // 初始延迟时间,单位毫秒
|
||
await sleep(ms); // 等待初始延迟时间
|
||
leftButtonDown(); // 按下左键
|
||
await sleep(ms);
|
||
// 计算总步数
|
||
let steps = Math.floor(totalDistance / stepDistance);
|
||
// 开始循环滚动
|
||
for (let j = 0; j < steps; j++) {
|
||
// 计算剩余距离
|
||
let remainingDistance = totalDistance - j * stepDistance;
|
||
// 确定本次移动距离
|
||
let moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance;
|
||
// 如果是向上滚动,则移动距离取反
|
||
if (isUp) {
|
||
//向上活动
|
||
moveDistance = -moveDistance
|
||
}
|
||
// 执行鼠标移动
|
||
moveMouseBy(0, -moveDistance);
|
||
// 取消注释后会在每一步后等待
|
||
// await sleep(delayMs);
|
||
// 每隔waitCount步等待一次
|
||
if (j % waitCount === 0) {
|
||
await sleep(delayMs);
|
||
}
|
||
}
|
||
// 滚动完成后释放左键
|
||
await sleep(ms);
|
||
leftButtonUp();
|
||
await sleep(ms);
|
||
}
|
||
|
||
/**
|
||
* 根据活动滑动页面
|
||
* @param {boolean} isUp - 滑动方向,true表示向上滑动,false表示向下滑动
|
||
* @param {number} total - 滑动总量
|
||
* @param {number} waitCount - 等待次数
|
||
* @param {number} stepDistance - 每次滑动的步长距离
|
||
* @param {number} scrollPageCount - 滑动页面的次数
|
||
* @returns {Promise<void>} - 返回一个Promise,表示异步操作的完成
|
||
*/
|
||
static async pagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30, scrollPageCount = 4) {
|
||
// 根据滚动方向设置坐标位置
|
||
// 如果是向上滚动,使用顶部坐标;否则使用底部坐标
|
||
let x = isUp ? xyConfig.top.x : xyConfig.bottom.x; // 根据滚动方向获取x坐标
|
||
let y = isUp ? xyConfig.top.y : xyConfig.bottom.y; // 根据滚动方向获取y坐标
|
||
// 记录滑动方向
|
||
log.info(`活动页面-${isUp ? '向上' : '向下'}滑动`);
|
||
// 注释:坐标信息已注释掉,避免日志过多
|
||
// log.info(`坐标:${x},${y}`);
|
||
// 根据配置的滑动次数执行循环
|
||
for (let i = 0; i < scrollPageCount; i++) {
|
||
// 移动到坐标位置
|
||
await moveMouseTo(x, y)
|
||
//80 18次滑动偏移量 46次测试未发现偏移
|
||
await scroll.page(total, isUp, waitCount, stepDistance)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 滚动到活动页面顶部的异步方法
|
||
* @param {Object} ocrRegion - OCR识别区域配置,默认为ocrRegionConfig.activity
|
||
* @returns {Promise<void>}
|
||
* @throws {Error} 当超过最大尝试次数仍未回到顶部时抛出错误
|
||
*/
|
||
static async pagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) {
|
||
let ms = 800; // 等待时间,单位毫秒
|
||
let topActivityName = null; // 上一次检测到的顶部活动名称
|
||
let sameTopCount = 0; // 连续出现相同顶部名称的次数
|
||
const requiredSameCount = 1; // 需要连续几次相同才确认到顶(推荐 2~3)
|
||
let attemptIndex = 0; // 总尝试次数计数器
|
||
const maxAttempts = config.toTopCount; // 可配置,默认为15次
|
||
|
||
log.info("开始滚动到活动页面顶部...");
|
||
|
||
while (attemptIndex < maxAttempts) {
|
||
attemptIndex++;
|
||
log.info(`第 {attemptIndex} 次尝试回顶`, attemptIndex);
|
||
|
||
// 移动鼠标到安全位置,避免干扰截图
|
||
await moveMouseTo(0, 20);
|
||
|
||
// 截图 + OCR 识别活动列表区域
|
||
let captureRegion = null;
|
||
try {
|
||
captureRegion = captureGameRegion();
|
||
const ocrObject = RecognitionObject.Ocr(
|
||
ocrRegion.x,
|
||
ocrRegion.y,
|
||
ocrRegion.width,
|
||
ocrRegion.height
|
||
);
|
||
// 可选:提升识别率
|
||
// ocrObject.threshold = 0.8;
|
||
|
||
let resList = captureRegion.findMulti(ocrObject);
|
||
// captureRegion.dispose();
|
||
|
||
// 如果完全没识别到任何活动,可能是页面异常或已在顶(极少情况)
|
||
if (resList.length === 0) {
|
||
log.warn("顶部OCR未识别到任何活动条目,可能是页面为空或识别失败");
|
||
// 再尝试一次向上滚大距离
|
||
// await scrollPagesByActivity(true); // true = 向上
|
||
await scroll.pagesByActivity(true, 80 * 4, 6, 60, 1);
|
||
await sleep(ms);
|
||
continue;
|
||
}
|
||
|
||
// 取当前识别到的最顶部活动名称(resList[0] 通常是列表最上面的)
|
||
const currentTopName = resList[0].text.trim();
|
||
|
||
log.info(`当前检测到的顶部活动: {currentTopName}`, currentTopName);
|
||
|
||
// 判断是否与上一次相同
|
||
if (currentTopName === topActivityName) {
|
||
sameTopCount++;
|
||
log.debug(`顶部活动连续相同 ${sameTopCount} 次`);
|
||
|
||
if (sameTopCount >= requiredSameCount) {
|
||
log.info(`已连续 {sameTopCount} 次检测到相同顶部活动,确认回到页面最顶部!`, sameTopCount);
|
||
return; // 成功回到顶部
|
||
}
|
||
} else {
|
||
// 顶部名称变了,说明还在向上滚动,重置计数
|
||
topActivityName = currentTopName;
|
||
sameTopCount = 1; // 这次算第一次
|
||
}
|
||
|
||
// 未达到稳定状态,继续向上滚动一页(可根据实际情况调整滚动距离)
|
||
// 这里使用更大滚动距离确保能快速回顶
|
||
// await scrollPagesByActivity(true); // true = 向上
|
||
await scroll.pagesByActivity(true, 80 * 4, 6, 60, 1);
|
||
|
||
await sleep(ms); // 给页面滚动和渲染留时间
|
||
} finally {
|
||
// 确保资源被正确释放
|
||
if (captureRegion) {
|
||
captureRegion.dispose();
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 超过最大尝试次数仍未稳定
|
||
throw new Error(`回到活动页面顶部失败:尝试 ${attemptIndex} 次后仍未检测到稳定顶部活动`);
|
||
}
|
||
|
||
|
||
/**
|
||
* 查找指定活动的静态异步方法
|
||
* @param {string} name - 要查找的活动名称
|
||
* @param {string} key - 活动中要匹配的关键字
|
||
* @param {string} value - 关键字对应的值
|
||
* @param {string} activityKey - 默认为"F5",用于打开活动页面的按键
|
||
* @returns {Object} 返回一个包含查找结果的对象,包含name、key、value三个布尔值属性
|
||
*/
|
||
static async findActivity(name, key, value, activityKey = "F5") {
|
||
const ms = 1000; // 定义时间单位,1秒
|
||
// 1. 打开活动页面(默认 F5)
|
||
await keyPress(activityKey); // 模拟按键打开活动页面
|
||
await sleep(ms * 2); // 等待2秒确保页面加载完成
|
||
// 2. 先强制滚动到最顶部(非常重要!)
|
||
try {
|
||
await scroll.pagesByActivityToTop(); // 滚动到页面顶部
|
||
await sleep(ms); // 等待1秒确保滚动完成
|
||
} catch (e) {
|
||
log.warn("回到顶部失败,但继续尝试执行"); // 捕获并处理异常
|
||
}
|
||
// 初始化查找结果对象
|
||
let findActivity = {
|
||
name: false,
|
||
key: false,
|
||
value: false
|
||
};
|
||
|
||
// 如果未指定活动名称,直接返回失败结果
|
||
if (!name) {
|
||
log.warn("未指定活动名称,无法查找");
|
||
return findActivity;
|
||
}
|
||
|
||
|
||
|
||
// 初始化底部检测相关变量
|
||
let lastPageBottomName = null; // 记录上一页底部活动名称
|
||
let sameBottomCount = 0; // 连续相同底部活动计数
|
||
const sameBottomCountMax = 1; // 最大连续相同次数
|
||
let scannedPages = 0; // 已扫描页数计数
|
||
const maxPages = 25; // 最大扫描页数限制
|
||
let previousPageActivities = new Set(); // 存储上一页所有活动名称
|
||
|
||
// 4. 主循环:逐页向下扫描
|
||
while (scannedPages < maxPages) {
|
||
scannedPages++;
|
||
log.info(`正在扫描第 ${scannedPages} 页`);
|
||
|
||
await moveMouseTo(0, 20); // 移动鼠标到页面顶部
|
||
|
||
let captureRegion = null; // 初始化屏幕捕获区域
|
||
try {
|
||
captureRegion = captureGameRegion(); // 捕获游戏屏幕区域
|
||
|
||
// 创建OCR识别对象
|
||
const ocrObject = RecognitionObject.Ocr(
|
||
ocrRegionConfig.activity.x,
|
||
ocrRegionConfig.activity.y,
|
||
ocrRegionConfig.activity.width,
|
||
ocrRegionConfig.activity.height
|
||
);
|
||
let resList = captureRegion.findMulti(ocrObject); // 使用OCR识别活动
|
||
|
||
// 如果当前页没有识别到任何活动,视为已到底部
|
||
if (resList.length === 0) {
|
||
log.info("当前页未识别到任何活动,视为已到页面底部");
|
||
break;
|
||
}
|
||
|
||
// ============ 重复页检测 ============
|
||
const currentPageNames = new Set(); // 存储当前页所有活动名称
|
||
for (let res of resList) {
|
||
currentPageNames.add(res.text.trim()); // 添加活动名称到集合
|
||
}
|
||
|
||
// 检测当前页与上一页的重合度
|
||
if (previousPageActivities.size > 0) {
|
||
let overlapCount = 0;
|
||
for (let actName of currentPageNames) {
|
||
if (previousPageActivities.has(actName)) overlapCount++;
|
||
}
|
||
const overlapRatio = overlapCount / previousPageActivities.size;
|
||
|
||
// 如果重合度超过70%,认为已到底部
|
||
if (overlapRatio >= 0.7) {
|
||
log.info(`检测到当前页与上一页高度重复(重合率 ${Math.round(overlapRatio * 100)}%),已到达底部,停止扫描`);
|
||
break;
|
||
}
|
||
}
|
||
previousPageActivities = currentPageNames; // 更新上一页活动集合
|
||
// ===================================
|
||
|
||
let currentPageBottomName = null; // 当前页底部活动名称
|
||
let foundTarget = false; // 是否找到目标活动标志
|
||
|
||
// 遍历当前页所有识别到的活动条目
|
||
for (let res of resList) {
|
||
const activityName = res.text.trim(); // 获取活动名称
|
||
currentPageBottomName = activityName; // 更新底部活动名
|
||
|
||
// 【关键修改】检查是否是目标活动名称(精确匹配)
|
||
if (activityName.includes(name)) {
|
||
findActivity.name = true; // 找到活动名称
|
||
log.info(`找到目标活动:${activityName}`);
|
||
await click(res.x, res.y); // 点击活动
|
||
await sleep(ms); // 等待1秒
|
||
foundTarget = true; // 设置找到目标标志
|
||
|
||
// 如果没有指定 key,找到活动名称就直接返回成功
|
||
if (!key) {
|
||
log.info(`已找到指定活动 [${activityName}],无需匹配关键字`);
|
||
findActivity.key = true;
|
||
break;
|
||
}
|
||
|
||
// 如果指定了 key,进行关键字匹配
|
||
const text = await Ocr.key(activityName, key); // OCR识别关键字
|
||
if (text && text.includes(key)) {
|
||
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}]`);
|
||
findActivity.key = true; // 找到关键字
|
||
if (value) {
|
||
findActivity.value = text.includes(value) // 检查值
|
||
}
|
||
if (findActivity.value) {
|
||
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}] 和值 [${value}]`);
|
||
}
|
||
} else {
|
||
log.info(`活动 [${activityName}] 不包含关键字 [${key}],继续查找`);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果找到目标活动,直接退出主循环
|
||
if (foundTarget) {
|
||
break;
|
||
}
|
||
|
||
// 5. 判断是否已到达页面底部(单一判断逻辑)
|
||
if (currentPageBottomName && currentPageBottomName === lastPageBottomName) {
|
||
sameBottomCount++;
|
||
if (sameBottomCount >= sameBottomCountMax) {
|
||
log.info(`连续 ${sameBottomCountMax} 次检测到相同底部活动,已确认到达页面最底部,扫描结束`);
|
||
break;
|
||
}
|
||
} else {
|
||
sameBottomCount = 0;
|
||
}
|
||
lastPageBottomName = currentPageBottomName;
|
||
|
||
// 6. 向下滑动一页,继续下一轮
|
||
await scroll.pagesByActivity(false);
|
||
await sleep(ms);
|
||
} finally {
|
||
if (captureRegion) {
|
||
captureRegion.dispose();
|
||
}
|
||
}
|
||
}
|
||
return findActivity;
|
||
}
|
||
}
|
||
|
||
export class Ocr{
|
||
/**
|
||
* 在指定区域进行OCR识别并匹配关键词
|
||
* @param {string} activityName - 活动名称,用于日志记录
|
||
* @param {string} key - 要匹配的关键词
|
||
* @param {Object} ocrRegion - OCR识别区域配置,默认为剩余时间区域
|
||
* @param {number} ocrRegion.x - 识别区域左上角X坐标
|
||
* @param {number} ocrRegion.y - 识别区域左上角Y坐标
|
||
* @param {number} ocrRegion.width - 识别区域宽度
|
||
* @param {number} ocrRegion.height - 识别区域高度
|
||
* @returns {string|null} 匹配到的文本列表(用'<-->'连接),未匹配到则返回null
|
||
*/
|
||
static key(activityName, key, ocrRegion = ocrRegionConfig.remainingTime) {
|
||
if (!key) {
|
||
return null
|
||
}
|
||
let captureRegion = captureGameRegion();
|
||
try {
|
||
let list = new Array()
|
||
const ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
|
||
let resList = captureRegion.findMulti(ocrObject);
|
||
|
||
for (let res of resList) {
|
||
log.debug(`[info][{key}]{activityName}--{time}`, key, activityName, res.text);
|
||
if (res.text.includes(key)) {
|
||
log.debug(`[{key}][命中]{activityName}--{time}`, key, activityName, res.text);
|
||
list.push(res.text.trim())
|
||
}
|
||
}
|
||
|
||
if (list.length > 0) {
|
||
return list.join('<-->')
|
||
}
|
||
return null;
|
||
|
||
} finally {
|
||
captureRegion.dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function findStygianOnslaught() {
|
||
const findActivity = {
|
||
name: "幽境危战",
|
||
key: "紊乱爆发期",
|
||
value: "已结束",
|
||
}
|
||
const findResult = await scroll.findActivity(findActivity.name, findActivity.key, findActivity.value);
|
||
if ((!findResult.name) || (findResult.name && findResult.key && findResult.value)) {
|
||
// 幽境危战 紊乱爆发期 已结束
|
||
return false
|
||
}
|
||
//正常模式
|
||
return true
|
||
}
|
||
|
||
export {
|
||
findStygianOnslaught
|
||
} |