给OCR一点时间 (#2597)

* 给OCR一点时间

OCR识别前增加等待时间,增加两模板匹配

* Update README.md

* 增加重购逻辑

* 调用setTime函数

* 换回原来的识图函数
This commit is contained in:
skyflag2022
2026-01-01 16:13:16 +08:00
committed by GitHub
parent 829e52c55c
commit 90f926feb7
6 changed files with 425 additions and 360 deletions

View File

@@ -19,208 +19,75 @@ let userName = settings.userName || "默认账户";
return userName;
}
// 设置游戏时间
async function setTime(hour, minute) {
// 关于setTime
// 原作者: Tim
// 脚本名称: SetTimeMinute - 精确调整游戏时间到分钟
// 脚本版本: 1.0
// Hash: f5c2547dfc286fc643c733d630f775e8fbf12971
// 设置游戏分辨率和DPI缩放
setGameMetrics(1920, 1080, 1);
// 圆心坐标
const centerX = 1441;
const centerY = 501.6;
// 半径
const r1 = 30;
const r2 = 150;
const r3 = 300;
const stepDuration = 50;
function getPosition(r, index) {
let angle = index * Math.PI / 720;
return [Math.round(centerX + r * Math.cos(angle)), Math.round(centerY + r * Math.sin(angle))];
}
async function mouseClick(x, y) {
moveMouseTo(x, y);
await sleep(50);
leftButtonDown();
await sleep(50);
leftButtonUp();
await sleep(stepDuration);
}
async function mouseClickAndMove(x1, y1, x2, y2) {
moveMouseTo(x1, y1);
await sleep(50);
leftButtonDown();
await sleep(50);
moveMouseTo(x2, y2);
await sleep(50);
leftButtonUp();
await sleep(stepDuration);
}
async function setTime(hour, minute) {
const end = (hour + 6) * 60 + minute - 20;
const n = 3;
for (let i = - n + 1; i < 1; i++) {
let [x, y] = getPosition(r1, end + i * 1440 / n);
await mouseClick(x, y);
}
let [x1, y1] = getPosition(r2, end + 5);
let [x2, y2] = getPosition(r3, end + 20 + 0.5);
await mouseClickAndMove(x1, y1, x2, y2);
}
let h = Math.floor(hour + minute / 60);
const m = Math.floor(hour * 60 + minute) - h * 60;
h = ((h % 24) + 24) % 24;
log.info(`设置时间到 ${h}${m}`);
await keyPress("Escape");
await sleep(1000);
await click(50, 700);
await sleep(2000);
await setTime(h, m);
await sleep(1000);
await click(1500, 1000);//确认
await sleep(2000);
await genshin.returnMainUi();
}
/**
* 判断任务是否已刷新
* 判断任务是否已刷新固定为每周四4点刷新
* @param {string} filePath - 存储最后完成时间的文件路径
* @param {object} options - 配置选项
* @param {string} [options.refreshType] - 刷新类型: 'hourly'|'daily'|'weekly'|'monthly'|'custom'
* @param {number} [options.customHours] - 自定义小时数(用于'custom'类型)
* @param {number} [options.dailyHour=4] - 每日刷新的小时(0-23)
* @param {number} [options.weeklyDay=1] - 每周刷新的星期(0-6, 0是周日)
* @param {number} [options.weeklyHour=4] - 每周刷新的小时(0-23)
* @param {number} [options.monthlyDay=1] - 每月刷新的日期(1-31)
* @param {number} [options.monthlyHour=4] - 每月刷新的小时(0-23)
* @returns {Promise<boolean>} - 是否已刷新
*/
async function isTaskRefreshed(filePath, options = {}) {
const {
refreshType = 'hourly', // 默认每小时刷新
customHours = 24, // 自定义刷新小时数默认24
dailyHour = 4, // 每日刷新默认凌晨4点
weeklyDay = 1, // 每周刷新默认周一(0是周日)
weeklyHour = 4, // 每周刷新默认凌晨4点
monthlyDay = 1, // 每月刷新默认第1天
monthlyHour = 4 // 每月刷新默认凌晨4点
} = options;
async function isTaskRefreshed(filePath) {
const WEEKLY_DAY = 4; // 周四0是周日1是周一4是周四
const WEEKLY_HOUR = 4; // 凌晨4点
try {
// 读取文件内容
let content = await file.readText(filePath);
const lastTime = new Date(content);
const nowTime = new Date();
let shouldRefresh = false;
switch (refreshType) {
case 'hourly': // 每小时刷新
shouldRefresh = (nowTime - lastTime) >= 3600 * 1000;
break;
case 'daily': // 每天固定时间刷新
// 检查是否已经过了当天的刷新时间
const todayRefresh = new Date(nowTime);
todayRefresh.setHours(dailyHour, 0, 0, 0);
// 如果当前时间已经过了今天的刷新时间,检查上次完成时间是否在今天刷新之前
if (nowTime >= todayRefresh) {
shouldRefresh = lastTime < todayRefresh;
} else {
// 否则检查上次完成时间是否在昨天刷新之前
const yesterdayRefresh = new Date(todayRefresh);
yesterdayRefresh.setDate(yesterdayRefresh.getDate() - 1);
shouldRefresh = lastTime < yesterdayRefresh;
}
break;
case 'weekly': // 每周固定时间刷新
// 获取本周的刷新时间
const thisWeekRefresh = new Date(nowTime);
// 计算与本周指定星期几的差值
const dayDiff = (thisWeekRefresh.getDay() - weeklyDay + 7) % 7;
thisWeekRefresh.setDate(thisWeekRefresh.getDate() - dayDiff);
thisWeekRefresh.setHours(weeklyHour, 0, 0, 0);
// 如果当前时间已经过了本周的刷新时间
if (nowTime >= thisWeekRefresh) {
shouldRefresh = lastTime < thisWeekRefresh;
} else {
// 否则检查上次完成时间是否在上周刷新之前
const lastWeekRefresh = new Date(thisWeekRefresh);
lastWeekRefresh.setDate(lastWeekRefresh.getDate() - 7);
shouldRefresh = lastTime < lastWeekRefresh;
}
break;
case 'monthly': // 每月固定时间刷新
// 获取本月的刷新时间
const thisMonthRefresh = new Date(nowTime);
// 设置为本月指定日期的凌晨
thisMonthRefresh.setDate(monthlyDay);
thisMonthRefresh.setHours(monthlyHour, 0, 0, 0);
// 如果当前时间已经过了本月的刷新时间
if (nowTime >= thisMonthRefresh) {
shouldRefresh = lastTime < thisMonthRefresh;
} else {
// 否则检查上次完成时间是否在上月刷新之前
const lastMonthRefresh = new Date(thisMonthRefresh);
lastMonthRefresh.setMonth(lastMonthRefresh.getMonth() - 1);
shouldRefresh = lastTime < lastMonthRefresh;
}
break;
case 'custom': // 自定义小时数刷新
shouldRefresh = (nowTime - lastTime) >= customHours * 3600 * 1000;
break;
default:
throw new Error(`未知的刷新类型: ${refreshType}`);
}
// 如果文件内容无效或不存在,视为需要刷新
if (!content || isNaN(lastTime.getTime())) {
// 如果文件内容为空或无效,视为需要刷新
if (!content) {
await file.writeText(filePath, '');
shouldRefresh = true;
}
if (shouldRefresh) {
notification.send(`购买狗粮已经刷新,执行脚本`);
return true;
} else {
log.info(`购买狗粮未刷新`);
return false;
}
} catch (error) {
// 如果文件不存在创建新文件并返回true(视为需要刷新)
const createResult = await file.writeText(filePath, '');
if (createResult) {
log.info("创建新时间记录文件成功,执行脚本");
return true;
}
else throw new Error(`创建新文件失败`);
const lastTime = new Date(content);
const nowTime = new Date();
// 检查上次记录时间是否有效
if (isNaN(lastTime.getTime())) {
log.info("时间记录文件内容无效,执行脚本");
return true;
}
// 获取本周的刷新时间
const thisWeekRefresh = new Date(nowTime);
// 计算与本周周四的差值
const dayDiff = (thisWeekRefresh.getDay() - WEEKLY_DAY + 7) % 7;
thisWeekRefresh.setDate(thisWeekRefresh.getDate() - dayDiff);
thisWeekRefresh.setHours(WEEKLY_HOUR, 0, 0, 0);
// 如果当前时间已经过了本周的刷新时间
if (nowTime >= thisWeekRefresh) {
// 检查上次完成时间是否在本周刷新之前
if (lastTime < thisWeekRefresh) {
notification.send("购买狗粮已经刷新,执行脚本");
return true;
}
} else {
// 否则检查上次完成时间是否在上周刷新之前
const lastWeekRefresh = new Date(thisWeekRefresh);
lastWeekRefresh.setDate(lastWeekRefresh.getDate() - 7);
if (lastTime < lastWeekRefresh) {
notification.send("购买狗粮已经刷新,执行脚本");
return true;
}
}
log.info("购买狗粮未刷新");
return false;
} catch (error) {
// 如果文件不存在或读取失败创建新文件并返回true
log.info(`文件读取失败: ${error.message},创建新文件`);
await file.writeText(filePath, '');
return true;
}
}
// 购买圣遗物
async function purChase(locationName) {
// 寻路
log.info(`加载路径文件: ${locationName}`);
let filePath = `assets/Pathing/${locationName}.json`;
await pathingScript.runFile(filePath);
await sleep(1000);
// 购买圣遗物 - 只执行购买部分(不包含寻路)
async function purchaseOnly(locationName, isRetry = false) {
log.info(`开始购买流程: ${locationName}${isRetry ? ' (重试)' : ''}`);
// 定义模板
let fDialogueRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/F_Dialogue.png"), 1050, 400, 100, 400);
@@ -234,7 +101,7 @@ let userName = settings.userName || "默认账户";
let fRes = ra.find(fDialogueRo);
ra.dispose();
if (!fRes.isExist()) {
let f_attempts = null; // 初始化尝试次数
let f_attempts = 0; // 初始化尝试次数
while (f_attempts < 6) { // 最多尝试 5 次
f_attempts++;
log.info(`当前尝试次数:${f_attempts}`);
@@ -246,9 +113,8 @@ let userName = settings.userName || "默认账户";
await sleep(500);
} else if (f_attempts <= 5) {
// 第 4-5 次尝试
log.info("重新加载路径文件");
await pathingScript.runFile(filePath);
await sleep(500);
log.warn("无法找到NPC退出购买流程");
return { success: false, reason: "npc_not_found" };
} else {
// 第 6 次尝试,尝试次数已达上限
log.warn("尝试次数已达上限");
@@ -269,116 +135,218 @@ let userName = settings.userName || "默认账户";
// 如果尝试次数用完仍未找到 F 图标,返回 false
if (!fRes.isExist()) {
log.warn("经过多次尝试后仍未找到 F 图标");
return false;
return { success: false, reason: "npc_not_found" };
}
}
return true
return { success: true, reason: "" };
}
let isAligned = await checkFAlignment(fDialogueRo);
if(isAligned){
// 进入对话选项
for (let i = 0; i < 5; i++) {
// 最多 F 5次
let captureRegion = captureGameRegion(); // 获取一张截图
let res = captureRegion.Find(shopDialogueRo);
captureRegion.dispose();
if (res.isEmpty()) {
keyPress("F");
await sleep(1000);
} else {
res.click();
log.info("已到达对话选项界面,点击商店图标({x},{y},{h},{w})", res.x, res.y, res.width, res.Height);
break;
let alignmentResult = await checkFAlignment(fDialogueRo);
if(!alignmentResult.success){
// 返回一个对象,包含购买数量和失败原因
return { purchasedCount: 0, failureReason: alignmentResult.reason, aligned: false };
}
// 进入对话选项
for (let i = 0; i < 5; i++) {
// 最多 F 5次
let captureRegion = captureGameRegion(); // 获取一张截图
let res = captureRegion.Find(shopDialogueRo);
captureRegion.dispose();
if (res.isEmpty()) {
keyPress("F");
await sleep(1000);
} else {
res.click();
log.info("已到达对话选项界面,点击商店图标({x},{y},{h},{w})", res.x, res.y, res.width, res.Height);
break;
}
await sleep(500);
}
// 进入商店界面
for (let i = 0; i < 5; i++) {
// 最多 F 5次
let captureRegion = captureGameRegion(); // 获取一张截图
let res = captureRegion.Find(shopDialogueRo2);
captureRegion.dispose();
if (res.isEmpty()) {
keyPress("F");
await sleep(1000);
} else {
log.info("已到达商店界面");
break;
}
await sleep(500);
}
if (locationName=='稻妻购买狗粮'){
click(200, 400); await sleep(500); // 选择狗粮
}
// 购买狗粮
let purchasedCount = 0; // 记录购买次数
for (let i = 0; i < 6; i++) {
// 最多购买6次
let captureRegion = captureGameRegion(); // 获取一张截图
let res = captureRegion.Find(conFirmRo);
captureRegion.dispose();
if (res.isEmpty()) {
log.info('圣遗物已售罄');
break;
}else{
// 识别到购买标识,模拟购买操作的后续点击
await click(1600, 1020);
await sleep(1000); // 购买
await click(1320, 780);
await sleep(1000); // 最终确认
await click(1320, 780);
await sleep(1000); // 点击空白
purchasedCount++; // 购买成功计数加1
}
await sleep(500);
}
// 返回购买结果对象
return { purchasedCount, failureReason: purchasedCount === 0 ? "sold_out" : "", aligned: true };
}
// 完整的购买流程(包含寻路)
async function purChase(locationName) {
// 寻路
log.info(`加载路径文件: ${locationName}`);
let filePath = `assets/Pathing/${locationName}.json`;
await pathingScript.runFile(filePath);
await sleep(1000);
// 执行购买
return await purchaseOnly(locationName);
}
// 检查函数,如果未买完则重新对话购买
async function checkAndPurchase(locationName) {
let maxRetries = 2; // 最大重试次数加上第一次共3次
let retryCount = 0;
let totalPurchased = 0;
// 第一次执行完整的购买流程(包含寻路)
log.info(`开始执行 ${locationName} 的完整购买流程`);
let purchaseResult = await purChase(locationName);
let purchasedCount = purchaseResult.purchasedCount;
let failureReason = purchaseResult.failureReason;
let aligned = purchaseResult.aligned;
totalPurchased += purchasedCount;
// 如果购买数量为0检查失败原因
if (purchasedCount === 0) {
if (failureReason === "npc_not_found" || !aligned) {
// NPC对齐失败重新执行一次完整购买流程
log.info(`${locationName} NPC对齐失败重新执行完整购买流程`);
await genshin.returnMainUi();
await sleep(2000);
purchaseResult = await purChase(locationName);
purchasedCount = purchaseResult.purchasedCount;
failureReason = purchaseResult.failureReason;
aligned = purchaseResult.aligned;
totalPurchased = purchasedCount; // 重置总购买数
if (purchasedCount === 0 && (failureReason === "npc_not_found" || !aligned)) {
log.warn(`${locationName} 第二次完整购买仍然NPC对齐失败跳过此地点`);
return 0;
}
await sleep(500);
}
// 进入商店界面
for (let i = 0; i < 5; i++) {
// 最多 F 5次
let captureRegion = captureGameRegion(); // 获取一张截图
let res = captureRegion.Find(shopDialogueRo2);
captureRegion.dispose();
if (res.isEmpty()) {
keyPress("F");
await sleep(1000);
} else {
log.info("已到达商店界面");
break;
}
await sleep(500);
}
if (locationName=='稻妻购买狗粮'){
click(200, 400); await sleep(500); // 选择狗粮
}
// 购买狗粮
for (let i = 0; i < 5; i++) {
// 最多购买5次
let captureRegion = captureGameRegion(); // 获取一张截图
let res = captureRegion.Find(conFirmRo);
captureRegion.dispose();
if (res.isEmpty()) {
log.info('圣遗物已售罄');
break;
}else{
// 识别到购买标识,模拟购买操作的后续点击
await click(1600, 1020);
await sleep(1000); // 购买
await click(1320, 780);
await sleep(1000); // 最终确认
await click(1320, 780);
await sleep(1000); // 点击空白
}
await sleep(500);
} else if (failureReason === "sold_out") {
// 商店已售罄,说明之前已经买过了
log.info(`${locationName} 商店已售罄,之前已完整购买过`);
return 0;
}
}
// 如果第一次没买完且数量大于0尝试重新对话购买
while (totalPurchased < 5 && retryCount < maxRetries) {
retryCount++;
log.info(`${retryCount} 次重试购买 ${locationName},已购买 ${totalPurchased}`);
// 返回主界面
await genshin.returnMainUi();
await sleep(2000);
// 重新执行购买(不包含寻路)
log.info(`重新执行 ${locationName} 的购买流程(不包含寻路)`);
purchaseResult = await purchaseOnly(locationName, true);
purchasedCount = purchaseResult.purchasedCount;
failureReason = purchaseResult.failureReason;
// 如果重新购买时数量为0检查失败原因
if (purchasedCount === 0) {
if (failureReason === "npc_not_found") {
log.info(`${locationName} 重试时NPC对齐失败停止重试`);
break;
} else if (failureReason === "sold_out") {
log.info(`${locationName} 重试时已无圣遗物可购买,停止重试`);
break;
}
}
totalPurchased += purchasedCount;
if (totalPurchased >= 5) {
log.info(`成功购买 ${locationName} 的所有圣遗物`);
break;
}
}
if (totalPurchased < 5 && totalPurchased > 0) {
log.warn(`购买 ${locationName} 未完成,只购买了 ${totalPurchased} 个圣遗物`);
} else if (totalPurchased === 0) {
log.info(`${locationName} 无圣遗物可购买`);
}
return totalPurchased;
}
async function main() {
await genshin.returnMainUi();
if(settings.select1){
await purChase('蒙德购买狗粮');
}
await genshin.returnMainUi();
// 使用数组存储要执行的地点
const purchaseTasks = [
{ enabled: settings.select1, name: '蒙德购买狗粮' },
{ enabled: settings.select2, name: '璃月购买狗粮1' },
{ enabled: settings.select3, name: '璃月购买狗粮2', time: { hour: 19, minute: 0 } },
{ enabled: settings.select4, name: '稻妻购买狗粮' },
{ enabled: settings.select5, name: '须弥购买狗粮' },
{ enabled: settings.select6, name: '枫丹购买狗粮' },
{ enabled: settings.select7, name: '纳塔购买狗粮' },
{ enabled: settings.select8, name: '挪德卡莱购买狗粮' }
];
if(settings.select2){
await purChase('璃月购买狗粮1');
}
let totalPurchased = 0;
if(settings.select3){
await setTime(19,00)
await purChase('璃月购买狗粮2');
}
for (const task of purchaseTasks) {
if (task.enabled) {
// 如果有时间设置,先设置时间
if (task.time) {
await genshin.setTime(task.time.hour, task.time.minute)
}
if(settings.select4){
await purChase('稻妻购买狗粮');
}
if(settings.select5){
await purChase('须弥购买狗粮');
}
// 执行检查并购买
let count = await checkAndPurchase(task.name);
totalPurchased += count;
if(settings.select6){
await purChase('枫丹购买狗粮');
}
log.info(`${task.name} 完成,购买了 ${count} 个圣遗物`);
if(settings.select7){
await purChase('纳塔购买狗粮');
}
// 返回主界面准备下一个任务
await genshin.returnMainUi();
await sleep(1000);
}
}
if(settings.select8){
await purChase('挪德卡莱购买狗粮');
}
notification.send(`所有任务完成,总共购买了 ${totalPurchased} 个圣遗物`);
await file.writeText(recordPath, new Date().toISOString());
}
await file.writeText(recordPath, new Date().toISOString());
}
userName = await getUserName();
const recordPath = `assets/${userName}.txt`;
//每周四4点刷新
if( await isTaskRefreshed(recordPath, {
refreshType: 'weekly',
weeklyDay: 4, // 周四
weeklyHour: 4 // 凌晨4点
})|| settings.select9){
if( await isTaskRefreshed(recordPath)|| settings.select9){
await main();
}
})();

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "自动购买狗粮(8点位)",
"version": "2.3.1",
"version": "2.4",
"bgi_version": "0.52",
"description": "自动购买狗粮",
"authors": [

View File

@@ -1,28 +1,128 @@
公版的bgi在执行js脚本的时候默认是关闭自动吃药的除非脚本作者在代码中开启。
# 药品消耗统计脚本使用指南
所以目前本脚本推荐搭配茶包版bgi使用。
## 📋 一、脚本概述
在搜索框搜索茶包,可以找到作者,点击作者名字,进入作者主页就可下载。
本脚本是一款专为《原神》设计的自动化工具,主要功能如下:
找不到下载地址的可以阅读锄地js的readme群号在readme的最后加群来测试茶包。
- **自动识别**通过OCR技术读取背包中指定药品的数量
- **智能统计**:记录每日药品消耗/新增变化统计周期为当日4点至次日4点
- **数据管理**自动保存30天历史记录过期数据自动清理
- **智能提醒**:通过系统通知推送药品变化信息
原理是将每天4点后第一次运行的数据记为初始值后续再次运行得出数量变化。
## 🛠️ 二、环境与工具要求
记录保存30天的数据每次运行保存一次数据。
### 必备工具茶包版BGI
如果当天运行过后,想初始化新料理,可以直接修改料理名称,修改后如果没有本地数据,运行会初始化。
**为什么不推荐公版BGI**
如果新料理今日已初始化,需要手动初始化,可以在选项设置中勾选,勾选后会删除当天的同名记录。
公版BGI执行JS脚本时默认关闭自动吃药功能且无法通过脚本代码开启。本脚本的设计逻辑完全依赖自动吃药功能的正常运行
手动运行初始化一次后记得取消勾选,不然运行就一直初始化。
- 药品消耗主要发生在自动吃药场景中
- 脚本未内置开启自动吃药的适配代码
脚本运行前需要在自定义设置里填写准确的食物名称。
### 系统配置要求
使用前请确认已装备便携营养袋并且已打开bgi的自动吃药功能。
1. **游戏分辨率**1920×1080固定不可改
2. **游戏内设置**
- 装备「便携营养袋」
- 确保游戏界面无遮挡、无缩放
3. **模板文件**:确认`assets/RecognitionObject/`文件夹包含所有必要的模板图片
脚本会记录每次运行读取到的数量,所以如果你打算做菜,建议第一次运行前或最后一次运行后做菜。
## 📥 三、获取茶包版BGI
账户名只允许使用数字中英文同时长度在20个字符以内。
1. **直接获取**:在脚本仓库搜索关键词「茶包」,进入作者主页下载
2. **备用方案**查阅「锄地一条龙js」的README文档通过文档末尾的群号加入测测莫酱交流群获取
## ⚙️ 四、使用前配置
### 1. 基础检查
- [ ] 游戏分辨率为1920×1080
- [ ] 已装备「便携营养袋」
- [ ] 茶包版BGI中已开启「自动吃药」功能
### 2. 参数设置(关键步骤)
| 参数项 | 填写要求 | 示例 |
|--------|----------|------------|
| **userName** | 1-20字符仅支持中英文、数字 | "旅行者001" |
| **recoveryFoodName** | 与游戏内名称完全一致 | "美味的甜甜花酿鸡" |
| **resurrectionFoodName** | 与游戏内名称完全一致 | "美味的提瓦特煎蛋" |
| **initSelect** | 默认关闭,仅初始化时临时开启 | 取消勾选 |
**重要**:药品名称必须完全匹配。
## 🔄 五、操作流程
### 场景一:首次使用 / 新账户
1. 完成上述配置后运行脚本
2. 脚本自动执行:
- 校验账户名合法性
- 领取月卡(如可领取)
- 打开背包识别药品数量
- 创建记录文件(路径:`assets/账户名.txt`
3. 收到「今日初始化完成」通知
### 场景二:日常使用
- **每日首次运行**4点后记录为当日初始值
- **后续运行**:对比最新数量,计算消耗/新增
- **自动记录**:每次运行都会保存一次记录数据
- **智能清理**仅保留30天内数据
### 场景三:更换药品或重置数据
1. 在设置中**勾选`initSelect`选项**
2. 运行脚本一次
3. **务必取消勾选`initSelect`**
**效果**:删除当日同名药品记录,以当前数量重新初始化。
## 📊 六、核心机制说明
### 统计周期规则
- 以**凌晨4点**为分界点
- 举例当前时间3点统计昨天4点至今的数据
- 当前时间5点统计今天4点起的数据
### 记录格式示例
```
时间:2023/10/15 14:30:45-美味的甜甜花酿鸡-18
时间:2023/10/15 14:30:45-美味的提瓦特煎蛋-6
```
### 异常处理机制
| 异常情况 | 自动处理方式 |
|----------|-------------|
| 药品未识别 | 数量设为0发送通知提醒 |
| 记录文件格式错误 | 自动重置文件 |
## ⚠️ 七、重要注意事项
### 每日首次使用时机建议
- **在锄地一条龙前**
- **在药品制作后**
- **避免**在两次脚本运行之间制作大量药品
### 常见误区
1. **`initSelect`常开**:导致每次运行都重新初始化
2. **名称不准确**:最常导致识别失败的原因
3. **窗口遮挡**影响OCR识别准确率
4. **多账户混淆**:每个账户有独立记录文件
## 🔧 八、故障排查指南
### 快速自查表
| 问题现象 | 优先检查项 |
|----------|------------|
| 药品数量识别为0 | 1. 药品名称准确性<br>2. 背包中是否有该药品<br>3. 分辨率是否为1920×1080 |
| 统计数据异常 | 1. 是否在脚本运行期间操作药品<br>2. 是否每日4点后首次运行<br>3. `initSelect`是否误开启 |
| 自动吃药无效 | 1. 是否使用茶包版BGI<br>2. BGI中自动吃药功能是否开启 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -10,6 +10,7 @@ const ocrRegion = {
const filterButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/filterButton.png"));
const resetButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/resetButton.png"));
const researchRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/research.png"));
const searchInterfaceRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/interface.png"));
const confirmButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/confirmButton.png"), 350, 1000, 50, 50);
(async function () {
// 检验账户名
@@ -415,104 +416,100 @@ const confirmButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("a
}
async function recognizeNumberByOCR(ocrRegion, pattern) {
let captureRegion = null;
try {
const ocrRo = RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
captureRegion = captureGameRegion();
const resList = captureRegion.findMulti(ocrRo);
let captureRegion = null;
try {
const ocrRo = RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
captureRegion = captureGameRegion();
const resList = captureRegion.findMulti(ocrRo);
if (!resList || resList.length === 0) {
log.warn("OCR未识别到任何文本");
return null;
}
for (const res of resList) {
if (!res || !res.text) {
continue;
if (!resList || resList.length === 0) {
log.warn("OCR未识别到任何文本");
return null;
}
const numberMatch = res.text.match(pattern);
if (numberMatch) {
const number = parseInt(numberMatch[1] || numberMatch[0]);
if (!isNaN(number)) {
return number;
for (const res of resList) {
if (!res || !res.text) {
continue;
}
const numberMatch = res.text.match(pattern);
if (numberMatch) {
const number = parseInt(numberMatch[1] || numberMatch[0]);
if (!isNaN(number)) {
return number;
}
}
}
}
}
catch (error) {
log.error(`OCR识别时发生异常: ${error.message}`);
}
finally {
if (captureRegion) {
captureRegion.dispose();
catch (error) {
log.error(`OCR识别时发生异常: ${error.message}`);
}
finally {
if (captureRegion) {
captureRegion.dispose();
}
}
return null;
}
return null;
}
async function findAndClick(target, maxAttempts = 20) {
for (let attempts = 0; attempts < maxAttempts; attempts++) {
const gameRegion = captureGameRegion();
async function findAndClick(target, maxAttempts = 50, clicks=true) {
for (let i = 0; i < maxAttempts; i++) {
const rg = captureGameRegion();
try {
const result = gameRegion.find(target);
if (result.isExist) {
result.click();
return true; // 成功立刻返回
const res = rg.find(target);
if (res.isExist()) {
if(clicks)
{await sleep(50); res.click()}
return true;
}
log.warn(`识别失败,第 ${attempts + 1} 次重试`);
} catch (err) {
} finally {
gameRegion.dispose();
}
if (attempts < maxAttempts - 1) { // 最后一次不再 sleep
await sleep(250);
}
} finally { rg.dispose(); }
if (i < maxAttempts - 1) await sleep(50);
}
return false;
}
async function getFoodNum(){
keyPress("B");//打开背包
await handleExpiredItems(); //处理过期物品弹窗
await sleep(1000);
click(863, 51);//选择食物
await sleep(500);
findAndClick(filterButtonRo);//筛选
await sleep(500);
findAndClick(resetButtonRo);//重置
await sleep(500);
findAndClick(researchRo);//点击搜索,输入名字
await findAndClick(filterButtonRo);//筛选
await findAndClick(searchInterfaceRo,50,false);//搜索界面
await findAndClick(resetButtonRo);//重置按钮
await findAndClick(researchRo);//搜索输入框
inputText(recoveryFoodName);
await sleep(500);
findAndClick(confirmButtonRo)
await sleep(500);
await findAndClick(confirmButtonRo);//确认按钮
await sleep(1000);
var recoveryNumber=await recognizeNumberByOCR(ocrRegion,/\d+/) //识别回血药数量
// 处理回血药识别结果
if (recoveryNumber === null) {
recoveryNumber = 0;
notification.send(`未识别到回血药数量设置数量为0药品名${recoveryFoodName}`)
await sleep(5000);
click(863, 51);//选择食物
await sleep(1000);
}
findAndClick(filterButtonRo);//筛选
await sleep(500);
findAndClick(resetButtonRo);//重置
await sleep(500);
findAndClick(researchRo);//点击搜索,输入名字
await findAndClick(filterButtonRo);//筛选
await findAndClick(searchInterfaceRo,50,false);//搜索界面
await findAndClick(resetButtonRo);//重置按钮
await findAndClick(researchRo);//搜索输入框
inputText(resurrectionFoodName);
await sleep(500);
findAndClick(confirmButtonRo);//确认筛选
await sleep(500);
await findAndClick(confirmButtonRo);//确认按钮
await sleep(1000); // 增加等待时间
var resurrectionNumber=await recognizeNumberByOCR(ocrRegion,/\d+/) //识别复活药数量
// 处理复活药识别结果
if (resurrectionNumber === null) {
resurrectionNumber = 0;
notification.send(`未识别到复活药数量设置数量为0药品名${resurrectionFoodName}`)
await sleep(5000);
click(863, 51);//选择食物
await sleep(1000);
}
findAndClick(filterButtonRo);//筛选
await sleep(500);
findAndClick(resetButtonRo);//重置
await sleep(500);
findAndClick(confirmButtonRo);//确认筛选
await findAndClick(filterButtonRo);//筛选
await findAndClick(searchInterfaceRo,50,false);//搜索界面
await findAndClick(resetButtonRo);//重置
await findAndClick(confirmButtonRo);//确认按钮
await genshin.returnMainUi();
return { recoveryNumber, resurrectionNumber };
}
@@ -522,9 +519,9 @@ const confirmButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("a
setGameMetrics(1920, 1080, 1);
// 点击领月卡
await genshin.blessingOfTheWelkinMoon();
await sleep(1000);
await sleep(500);
await genshin.returnMainUi();
await sleep(1000);
await sleep(500);
// 获取食物数量
return await getFoodNum();
}

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "吃药统计",
"version": "1.3",
"version": "1.4",
"bgi_version": "0.51",
"description": "用于统计指定两个食物的消耗,推荐锄地前后使用",
"authors": [