From 90f926feb7a090a7acae856b152eb655b3a6265e Mon Sep 17 00:00:00 2001 From: skyflag2022 <107539971+skyflag2022@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:13:16 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=99OCR=E4=B8=80=E7=82=B9=E6=97=B6?= =?UTF-8?q?=E9=97=B4=20(#2597)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 给OCR一点时间 OCR识别前增加等待时间,增加两模板匹配 * Update README.md * 增加重购逻辑 * 调用setTime函数 * 换回原来的识图函数 --- repo/js/PurchaseArtifacts/main.js | 526 ++++++++---------- repo/js/PurchaseArtifacts/manifest.json | 2 +- repo/js/营养袋吃药统计/README.md | 126 ++++- .../assets/RecognitionObject/interface.png | Bin 0 -> 1566 bytes repo/js/营养袋吃药统计/main.js | 129 +++-- repo/js/营养袋吃药统计/manifest.json | 2 +- 6 files changed, 425 insertions(+), 360 deletions(-) create mode 100644 repo/js/营养袋吃药统计/assets/RecognitionObject/interface.png diff --git a/repo/js/PurchaseArtifacts/main.js b/repo/js/PurchaseArtifacts/main.js index ad568b8be..a55a91844 100644 --- a/repo/js/PurchaseArtifacts/main.js +++ b/repo/js/PurchaseArtifacts/main.js @@ -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} - 是否已刷新 */ - 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(); } })(); diff --git a/repo/js/PurchaseArtifacts/manifest.json b/repo/js/PurchaseArtifacts/manifest.json index c338600b8..f03db4474 100644 --- a/repo/js/PurchaseArtifacts/manifest.json +++ b/repo/js/PurchaseArtifacts/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "自动购买狗粮(8点位)", - "version": "2.3.1", + "version": "2.4", "bgi_version": "0.52", "description": "自动购买狗粮", "authors": [ diff --git a/repo/js/营养袋吃药统计/README.md b/repo/js/营养袋吃药统计/README.md index 9a508a139..632ab2d34 100644 --- a/repo/js/营养袋吃药统计/README.md +++ b/repo/js/营养袋吃药统计/README.md @@ -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. 药品名称准确性
2. 背包中是否有该药品
3. 分辨率是否为1920×1080 | +| 统计数据异常 | 1. 是否在脚本运行期间操作药品
2. 是否每日4点后首次运行
3. `initSelect`是否误开启 | +| 自动吃药无效 | 1. 是否使用茶包版BGI
2. BGI中自动吃药功能是否开启 | diff --git a/repo/js/营养袋吃药统计/assets/RecognitionObject/interface.png b/repo/js/营养袋吃药统计/assets/RecognitionObject/interface.png new file mode 100644 index 0000000000000000000000000000000000000000..b70d1784295cff26fe369f742ffa9c329ff58985 GIT binary patch literal 1566 zcmV+(2I2XMP)P000>X1^@s6#OZ}&00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1+z&+K~zXfjaF?> zlV=pZ`aCVArG?T0EmR5!h@!w$bWTJ^=G;qWbo;QMuzgtOPgq=*Eiv22?dy^y+dgcG zON^P*WEwThw<*4W3=rg{mKF--t@NGlTz6Z*M6;c8_o4T5Kj)nLx~_BX$FObB>sgE0 zh?6}{IMm^Vwb+C^kAwK*#yBRYmyph^VYAt-MJThGaqK`n_O-gfXRqIXhD)+O5MDuM zEt_AtVMcx#3_x*_Q8_5Fm|&F$jhal#pGntH<+7rwwj8^*y3n}Af$%~M4!c>6B9ktO z8R4t8qtagEmKZu%=FMJUQ7rlMfo|}?SUXJ-N0@~NT=6u z_VO_17USsN?L|+g50#D$=(L{BTAVo2gce^pOoA`1)?>#T`+R7rb1KXW#>`g}DI}8V zJi$4Vv)L?GVkyPdax{sIh-EYVnpp_9n02)+*|joVNayy6zrupwLc;OGjX3k^VZ7hd z3|Cn(G!i3bGU_-fiJm#tjSt^!QOM9o!L%RF58#_0@8VYfG~&q|#*fG5@cp@d+~l&@H&*{E2UP5 zF>!YY6s=N_`&vC{7NOWn%!(JAK-DBg+PTA{{8Txt!l#ZOji+EqLr5xtJ*ryR-J2*eSTb24Kk`4Dyia? zMM?n}uAcoWTW&92hlF9fOer*!?W2H z=%Kj%^rXnHPWq59k3A(iG!a2$Iia9obBqe!5z&7bfSvq`9zf6`zM6{(J)-HlWu96d!_9U49}|Ko09_OW8CFCT@504i)uf# z2P0uT9GO+U?1d8EY$=i_ig|UXN1dErfu*aX2JOuiN)S$w@38}Q*emkofyWSoqN+t&L$44@{WQRPG8DK#A-nr|ec=RN!Bu#Ms zR}exnHM5N0t~`-O8o;VR%%gXAOBI^sSmt{HcYIYK{Q2S_9txZ>skH+7DhSVavyfH5 z^F_u}j@WZ8gK{OiEeYoTEJz9=x|&i2SqMNgeEHcY*`1hPe~JrP~= z(BjJ2v~BTdq{z{9+vc0wt1&7EkqH Q9{>OV07*qoM6N<$g3t%`)c^nh literal 0 HcmV?d00001 diff --git a/repo/js/营养袋吃药统计/main.js b/repo/js/营养袋吃药统计/main.js index 26f12d3f0..1cd0a787a 100644 --- a/repo/js/营养袋吃药统计/main.js +++ b/repo/js/营养袋吃药统计/main.js @@ -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(); } diff --git a/repo/js/营养袋吃药统计/manifest.json b/repo/js/营养袋吃药统计/manifest.json index 8520e34f5..ca1dacb6c 100644 --- a/repo/js/营养袋吃药统计/manifest.json +++ b/repo/js/营养袋吃药统计/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "吃药统计", - "version": "1.3", + "version": "1.4", "bgi_version": "0.51", "description": "用于统计指定两个食物的消耗,推荐锄地前后使用", "authors": [