diff --git a/repo/js/AutoPlan/README.md b/repo/js/AutoPlan/README.md index d112f4d1b..31a8d2ba8 100644 --- a/repo/js/AutoPlan/README.md +++ b/repo/js/AutoPlan/README.md @@ -12,6 +12,8 @@ - 限制只在特定星期几执行(例如周日限定本、周本) - 设置执行优先级(数字越大越先跑) - 支持三种配置来源(输入 / UID专属 / bgi_tools远程) +- 自动检测幽境开启状态 + ![check](md/check.jpg) ## 配置项说明 @@ -293,18 +295,20 @@ 欢迎提交 issue 或 PR 改进脚本~ 祝刷本愉快! -## 版本密钥 +## 版本对应表 -| 版本 | 密钥 | -|-------|---------------------| -| 0.0.1 | oiJbmjU2R0NniiwiZxh | -| 0.0.2 | oiJbmjU2R0NniiwiZxh | +| 版本 | 密钥 | bettergi-scripts-tools对应版本 | +|:-----:|:-------------------:|:--------------------------:| +| 0.0.1 | oiJbmjU2R0NniiwiZxh | 0.0.4+ | +| 0.0.2 | oiJbmjU2R0NniiwiZxh | 0.0.5+ | +| 0.0.3 | oiJbmjU2R0NniiwiZxh | 0.0.6+ | ## 版本历史(简要) +### 0.0.3 2026.03.13 +- 健全体力识别(双重识别兜底) ### 0.0.2 2026.03.07 - 新增幽境支持 ### 0.0.1 2026.01.30 - - 基本功能完成 - 支持三种配置加载方式 - 支持 bgi_tools http 拉取/推送配置 diff --git a/repo/js/AutoPlan/assets/original_resin.png b/repo/js/AutoPlan/assets/original_resin.png new file mode 100644 index 000000000..7aea4d586 Binary files /dev/null and b/repo/js/AutoPlan/assets/original_resin.png differ diff --git a/repo/js/AutoPlan/config/config.js b/repo/js/AutoPlan/config/config.js index a36524d3d..df0cd2c37 100644 --- a/repo/js/AutoPlan/config/config.js +++ b/repo/js/AutoPlan/config/config.js @@ -3,6 +3,7 @@ import {ocrUid} from "../utils/uid"; const config = { //setting设置放在这个json run: { + exclude_run_exception: false,//忽略运行异常 loop_plan: false,//启用循环体力计划 retry_count: 3,//复活重试次数 config: '', @@ -186,6 +187,8 @@ async function initConfig() { ? retryCount : config.run.retry_count; + config.run.exclude_run_exception=settings.exclude_run_exception + config.run.loop_plan = settings.loop_plan !== undefined ? settings.loop_plan : config.run.loop_plan const bgi_tools_token = settings.bgi_tools_token || "Authorization= " // const list = Array.from(bgi_tools_token.split("=")).map(item => item.trim()); diff --git a/repo/js/AutoPlan/main.js b/repo/js/AutoPlan/main.js index d2069aeda..05a634b09 100644 --- a/repo/js/AutoPlan/main.js +++ b/repo/js/AutoPlan/main.js @@ -2,7 +2,7 @@ import {config, initConfig, initSettings, LoadType} from './config/config'; import {ocrUid} from './utils/uid'; import {getDayOfWeek, outDomainUI, outStygianOnslaughtUI, throwError,toMainUi} from './utils/tool'; import {pullJsonConfig, pushAllCountryConfig, pushAllJsonConfig} from './utils/bgi_tools'; -import {ocrPhysical} from "./utils/physical"; +import {countOriginalResin, ocrPhysical,countAllResin} from "./utils/physical"; import {findStygianOnslaught} from "./utils/activity"; /** @@ -54,9 +54,13 @@ async function autoDomain(autoFight) { // fragileResinUseCount: number; await sleep(1000) //流程->返回主页 打开地图 返回主页 - const physicalOcr = await ocrPhysical(true, true) - config.user.physical.current = physicalOcr.current - config.user.physical.min = physicalOcr.min + // const physicalOcr = await ocrPhysical(true, true) + // config.user.physical.current = physicalOcr.current + // config.user.physical.min = physicalOcr.min + + const currentPhysical = await countAllResin() + config.user.physical.current = currentPhysical.originalResinCount; + const physical = config.user.physical if (domainParam.specifyResinUse && physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) { throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`) @@ -104,7 +108,9 @@ async function autoDomain(autoFight) { if (errorMessage.includes("复活") && domainParam.DomainName) { continue; } - throw e; + if (!config.run.exclude_run_exception||config.run.loop_plan) {//排除异常 与循环计划互斥 + throw e; + } } } } finally { @@ -172,7 +178,9 @@ async function autoLeyLineOutcrop(autoLeyLineOutcrop) { if (errorMessage.includes("复活")) { continue; } - throw e; + if (!config.run.exclude_run_exception||config.run.loop_plan) {//排除异常 与循环计划互斥 + throw e; + } } } } @@ -246,9 +254,12 @@ async function autoStygianOnslaught(autoStygianOnslaught) { // fragileResinUseCount: number; await sleep(1000) //流程->返回主页 打开地图 返回主页 - const physicalOcr = await ocrPhysical(true, true) - config.user.physical.current = physicalOcr.current - config.user.physical.min = physicalOcr.min + // const physicalOcr = await ocrPhysical(true, true) + // config.user.physical.current = physicalOcr.current + // config.user.physical.min = physicalOcr.min + const currentPhysical = await countAllResin() + config.user.physical.current = currentPhysical.originalResinCount; + const physical = config.user.physical if (physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) { throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`) @@ -278,7 +289,9 @@ async function autoStygianOnslaught(autoStygianOnslaught) { if (errorMessage.includes("复活")) { continue; } - throw e; + if (!config.run.exclude_run_exception||config.run.loop_plan) {//排除异常 与循环计划互斥 + throw e; + } } } } finally { @@ -676,7 +689,7 @@ async function main() { if (isStygianOnslaught) { //圣遗物秘境名称 const holyRelicDomainNames = config.domainList.filter(item => !item.hasOrder).map(item => item.name); - const filter = list.find(item => item.runType === config.user.runTypes[0] && holyRelicDomainNames.includes(item.autoFight.domainName)); + const filter = list.find(item => item.runType === config.user.runTypes[0] && holyRelicDomainNames.includes(item.autoFight?.domainName)); if (filter) { // 幽境危战添加秘境顺序前 list.forEach(item => { @@ -702,8 +715,9 @@ async function main() { await autoRunList(list); if (config.run.loop_plan) { // 重新获取当前体力值 - const physicalOcr = await ocrPhysical(true, true); - config.user.physical.current = physicalOcr.current; + // const physicalOcr = await ocrPhysical(true, true); + const currentPhysical = await countAllResin() + config.user.physical.current = currentPhysical.originalResinCount; //循环 if (config.user.physical.current < config.user.physical.min) { //体力耗尽 diff --git a/repo/js/AutoPlan/manifest.json b/repo/js/AutoPlan/manifest.json index feb9d4c92..c8077be74 100644 --- a/repo/js/AutoPlan/manifest.json +++ b/repo/js/AutoPlan/manifest.json @@ -1,6 +1,6 @@ { "name": "自动体力计划", - "version": "0.0.2", + "version": "0.0.3", "description": "", "settings_ui": "settings.json", "main": "main.js", diff --git a/repo/js/AutoPlan/md/check.jpg b/repo/js/AutoPlan/md/check.jpg new file mode 100644 index 000000000..63ed991fa Binary files /dev/null and b/repo/js/AutoPlan/md/check.jpg differ diff --git a/repo/js/AutoPlan/settings.json b/repo/js/AutoPlan/settings.json index 754d7da14..866ac7550 100644 --- a/repo/js/AutoPlan/settings.json +++ b/repo/js/AutoPlan/settings.json @@ -17,6 +17,11 @@ "label": "自动秘境计划配置\n语法:说明太长 去看文档", "default": "" }, + { + "name": "exclude_run_exception", + "type": "checkbox", + "label": "忽略运行异常\n(当前计划出现异常后,会执行下一个计划)与循环体力计划冲突" + }, { "name": "loop_plan", "type": "checkbox", diff --git a/repo/js/AutoPlan/utils/bgi_tools.js b/repo/js/AutoPlan/utils/bgi_tools.js index 45dd88889..97c0ec26a 100644 --- a/repo/js/AutoPlan/utils/bgi_tools.js +++ b/repo/js/AutoPlan/utils/bgi_tools.js @@ -6,7 +6,7 @@ * @returns {Promise} */ async function pullJsonConfig(uid, http_api) { - http_api += "?uid=" + uid + http_api += "?uid=" + uid+"&enable=" + true const res = await http.request("GET", http_api // , JSON.stringify({"Content-Type": "application/json"}) ) diff --git a/repo/js/AutoPlan/utils/physical.js b/repo/js/AutoPlan/utils/physical.js index a399925d4..adc7d2cfb 100644 --- a/repo/js/AutoPlan/utils/physical.js +++ b/repo/js/AutoPlan/utils/physical.js @@ -1,4 +1,4 @@ -import {getJsonPath, toMainUi,throwError} from "./tool"; +import {getJsonPath, toMainUi, throwError, findImgAndClick} from "./tool"; //==================================================== const genshinJson = { width: 1920,//genshin.width, @@ -8,7 +8,40 @@ const genshinJson = { // const OpenModeCountMin = settings.openModeCountMin // let AlreadyRunsCount=0 // let NeedRunsCount=0 -const TemplateOrcJson={x: 1568, y: 16, width: 225, height: 60,} +const TemplateOrcJson = {x: 1568, y: 16, width: 225, height: 60,} + +// ==================== 常量定义 ==================== + +// 树脂图标识别对象 +const RESIN_ICONS = { + ORIGINAL: RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/original_resin.png")), + // CONDENSED: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/condensed_resin.png")), + // FRAGILE: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/fragile_resin.png")), + // TRANSIENT: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/transient_resin.png")), + // REPLENISH_BUTTON: RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/replenish_resin_button.png")) +}; +// 配置常量 +const CONFIG = { + RECOGNITION_TIMEOUT: 2000, // 图像识别超时时间(毫秒) + SLEEP_INTERVAL: 500, // 循环间隔时间(毫秒) + UI_DELAY: 1500, // UI操作延迟时间(毫秒) + MAP_ZOOM_LEVEL: 6, // 地图缩放级别 + + // 点击坐标 + COORDINATES: { + MAP_SWITCH: {x: 1840, y: 1020}, // 地图右下角切换按钮 + MONDSTADT: {x: 1420, y: 180}, // 蒙德选择按钮 + AVOID_SELECTION: {x: 1090, y: 450} // 避免选中效果的点击位置 + }, + + // OCR识别区域配置 + OCR_REGIONS: { + ORIGINAL_RESIN: {width: 200, height: 40}, + CONDENSED_RESIN: {width: 90, height: 40}, + OTHER_RESIN: {width: 0, height: 60} // width会根据图标宽度动态设置 + } +}; + //==================================================== @@ -17,14 +50,14 @@ const TemplateOrcJson={x: 1568, y: 16, width: 225, height: 60,} * @param {string} str - 包含数字的字符串 * @returns {number} - 由字符串中所有数字组合而成的整数 */ -async function saveOnlyNumber(str,defaultValue=0) { +async function saveOnlyNumber(str, defaultValue = 0) { // 使用正则表达式匹配字符串中的所有数字 // \d+ 匹配一个或多个数字 // .join('') 将匹配到的数字数组连接成一个字符串 // parseInt 将连接后的字符串转换为整数 try { return parseInt(str.match(/\d+/g).join('')); - }catch (e) { + } catch (e) { return defaultValue } } @@ -40,7 +73,7 @@ async function saveOnlyNumber(str,defaultValue=0) { * - min {number}: 最小可执行体力值 * - current {number}: 当前剩余体力值 */ -async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isResinExhaustionMode=true) { +async function ocrPhysical(opToMainUi = false, openMap = false, minPhysical = 20, isResinExhaustionMode = true) { // 检查是否启用体力识别功能,如果未启用则直接返回默认结果 if (!isResinExhaustionMode) { log.info(`===未启用===`) @@ -57,14 +90,14 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes await toMainUi(); // 切换到主界面 } - if (openMap){ + if (openMap) { await sleep(ms) //打开地图界面 await keyPress('M') } await sleep(ms) log.debug(`===[点击+]===`) - //点击+ 按钮 x=1264,y=39,width=18,height=19 + // //点击+ 按钮 x=1264,y=39,width=18,height=19 let add_buttonJSON = getJsonPath('add_button'); let add_objJson = { path: `${add_buttonJSON.path}${add_buttonJSON.name}${add_buttonJSON.type}`, @@ -73,23 +106,28 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes width: 52, height: 49, } - let templateMatchAddButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${add_objJson.path}`), add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height); - let regionA = captureGameRegion() - // let deriveCrop = regionA.DeriveCrop(add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height); - try { - let buttonA = regionA.find(templateMatchAddButtonRo); - - await sleep(ms) - if (!buttonA.isExist()) { - log.error(`${add_objJson.path}匹配异常`) - throwError(`${add_objJson.path}匹配异常`) - } - await buttonA.click() - }finally { - // deriveCrop.dispose() - regionA.dispose() + // + // let templateMatchAddButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${add_objJson.path}`), add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height); + // let regionA = captureGameRegion() + // // let deriveCrop = regionA.DeriveCrop(add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height); + // try { + // let buttonA = regionA.find(templateMatchAddButtonRo); + // + // await sleep(ms) + // if (!buttonA.isExist()) { + // log.error(`${add_objJson.path}匹配异常`) + // throwError(`${add_objJson.path}匹配异常`) + // } + // await buttonA.click() + // } finally { + // // deriveCrop.dispose() + // regionA.dispose() + // } + const addClick = await findImgAndClick(`${add_objJson.path}`, 1248, 21, 50, 50); + if (addClick === null) { + log.error(`${add_objJson.path}匹配异常`) + return undefined } - await sleep(ms) log.debug(`===[定位原粹树脂]===`) @@ -103,16 +141,17 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes height: TemplateOrcJson.height, } let templateMatchButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${tmJson.path}`), tmJson.x, tmJson.y, tmJson.width, tmJson.height); - let region =captureGameRegion() + let region = captureGameRegion() let button try { button = region.find(templateMatchButtonRo); await sleep(ms) - if ((!button)||!button.isExist()) { + if ((!button) || !button.isExist()) { log.error(`${tmJson.path} 匹配异常`) - throwError(`${tmJson.path} 匹配异常`) + // throwError(`${tmJson.path} 匹配异常`) + return undefined } - }finally { + } finally { region.dispose() } @@ -125,7 +164,7 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes // y: 32, y: button.y, // width: 61, - width: Math.abs(genshinJson.width - button.x - button.width), + width: Math.abs(genshinJson.width - button.x - button.width), height: 26 } @@ -133,11 +172,11 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes let region3 = captureGameRegion() try { - let recognitionObjectOcr = RecognitionObject.Ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height); + let recognitionObjectOcr = RecognitionObject.ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height); let res = region3.find(recognitionObjectOcr); log.debug(`[OCR原粹树脂]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`); - let text=res.text.split('/')[0] + let text = res.text.split('/')[0] let current = await saveOnlyNumber(text) let execute = (current - minPhysical) >= 0 log.debug(`最小可执行原粹树脂:{min},原粹树脂:{key}`, minPhysical, current,) @@ -149,7 +188,9 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes current: current, } } catch (e) { - throwError(`识别失败,err:${e.message}`) + // throwError(`识别失败,err:${e.message}`) + log.error(`识别失败,err:${e.message}`) + return undefined } finally { region3.dispose() //返回地图操作 @@ -160,6 +201,269 @@ async function ocrPhysical(opToMainUi = false,openMap=false,minPhysical=20,isRes } +// ==================== UI操作函数 ==================== + +/** + * 打开并设置地图界面 + */ +async function openMap() { + log.info("打开地图界面"); + await keyPress("M"); + await sleep(CONFIG.UI_DELAY); + + // 切换到国家选择界面 + // click(CONFIG.COORDINATES.MAP_SWITCH.x, CONFIG.COORDINATES.MAP_SWITCH.y); + // await sleep(CONFIG.UI_DELAY); + + // 选择蒙德 + // click(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y); + // await sleep(CONFIG.UI_DELAY); + // await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y) + + // 设置地图缩放级别,排除识别干扰 + await genshin.setBigMapZoomLevel(CONFIG.MAP_ZOOM_LEVEL); + log.info("地图界面设置完成"); +} + +/** + * 统计所有树脂数量的主函数 + * @returns {Object} 包含所有树脂数量的对象 + */ +async function countAllResin() { + let shouldRestoreMainUi = false + try { + // setGameMetrics(1920, 1080, 1); + // log.info("开始统计树脂数量"); + let resinCounts = { + original: 0, + transient: undefined, + fragile: undefined, + condensed: undefined + } + await toMainUi(); + await sleep(CONFIG.UI_DELAY); + shouldRestoreMainUi = true + // 打开地图界面统计原粹/浓缩树脂 + await openMap(); + await sleep(CONFIG.UI_DELAY); + let tryPass = true; + try { + // log.info("[开始]统计补充树脂界面中的树脂"); + resinCounts.original = await countOriginalResin(false, false); + moveMouseTo(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y) + await sleep(500); + // resinCounts.transient = await countTransientResin(); + // resinCounts.fragile = await countFragileResin(); + // log.info("[完成]统计补充树脂界面中的树脂"); + // 点击避免选中效果影响统计 + click(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y); + } catch (e) { + tryPass = false + } + await sleep(CONFIG.UI_DELAY); + log.info("开始统计地图界面中的树脂"); + if (!tryPass) { + // 如果第一次尝试失败,则切换到蒙德 + await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y) + resinCounts.original = await countOriginalResin(!tryPass); + } + // resinCounts.condensed = await countCondensedResin(); + // if (!tryPass) { + // 打开补充树脂界面统计须臾/脆弱树脂 + // await openReplenishResinUi(); + // await sleep(CONFIG.UI_DELAY); + + // 点击避免选中效果影响统计 + // click(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y); + // await sleep(500); + + // log.info("开始统计补充树脂界面中的树脂"); + // resinCounts.transient = await countTransientResin(); + // resinCounts.fragile = await countFragileResin(); + // } + // 显示结果 + displayResults(resinCounts); + + // 返回主界面 + await genshin.returnMainUi(); + await sleep(CONFIG.UI_DELAY); + + log.info("树脂统计完成"); + return { + originalResinCount: resinCounts.original, + condensedResinCount: resinCounts.condensed, + transientResinCount: resinCounts.transient, + fragileResinCount: resinCounts.fragile + }; + + } catch (error) { + log.error(`统计树脂数量时发生异常: ${error.message}`); + throw error; + } finally { + if (shouldRestoreMainUi) { + await toMainUi(); + await sleep(CONFIG.UI_DELAY); + } + } +} + +/** + * 切换到国家选择界面的异步函数 + * 通过点击指定坐标并等待界面加载来完成切换操作 + */ +async function switchtoCountrySelection(x, y) { + // 切换到国家选择界面 + click(CONFIG.COORDINATES.MAP_SWITCH.x, CONFIG.COORDINATES.MAP_SWITCH.y); + await sleep(CONFIG.UI_DELAY); + click(x, y); + await sleep(CONFIG.UI_DELAY); +} + +function displayResults(results) { + const resultText = `原粹:${results.original} 浓缩:${results.condensed} 须臾:${results.transient} 脆弱:${results.fragile}`; + + log.info(`============ 树脂统计结果 ============`); + log.info(`原粹树脂数量: ${results.original}`); + log.info(`浓缩树脂数量: ${results.condensed}`); + log.info(`须臾树脂数量: ${results.transient}`); + log.info(`脆弱树脂数量: ${results.fragile}`); + log.info(`====================================`); +} + +/** + * 统计原粹树脂数量 + * @returns {number} 原粹树脂数量 + */ +async function countOriginalResin(tryOriginalMode, opToMainUi, openMap) { + if (tryOriginalMode) { + log.info("尝试使用原始模式"); + return await countOriginalResinBackup() + } else { + log.info('尝试使用优化模式'); + let ocr_physical = await ocrPhysical(opToMainUi, openMap); + + log.debug(`ocrPhysical: {0}`, JSON.stringify(ocr_physical)) + await sleep(600) + // ocrPhysical = false//模拟异常 + if (ocr_physical/* && ocrPhysical.ok*/) { + return ocr_physical?.current; + } else { + //异常 退出至地图 尝试使用原始模式 + await keyPress("VK_ESCAPE") + log.error(`ocrPhysical error`); + throw new Error("ocrPhysical error"); + } + } +} + +async function countOriginalResinBackup() { + const originalResin = await recognizeImage(RESIN_ICONS.ORIGINAL); + if (!originalResin) { + log.warn(`未找到原粹树脂图标`); + return 0; + } + + const ocrRegion = { + x: originalResin.x, + y: originalResin.y, + width: CONFIG.OCR_REGIONS.ORIGINAL_RESIN.width, + height: CONFIG.OCR_REGIONS.ORIGINAL_RESIN.height + }; + + // 匹配 xxx/200 格式中的第一个数字(1-3位) + const count = await recognizeNumberByOCR(ocrRegion, /(\d{1,3})\/\d+/); + if (count !== null) { + log.info(`原粹树脂数量: ${count}`); + return count; + } + + log.warn(`未能识别原粹树脂数量`); + return 0; +} + + +// ==================== 工具函数 ==================== + +/** + * 通用图像识别函数 + * @param {Object} recognitionObject - 识别对象 + * @param {number} timeout - 超时时间(毫秒) + * @returns {Object|null} 识别结果或null + */ +async function recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TIMEOUT) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + let gameRegion = undefined + try { + gameRegion = captureGameRegion(); + // 直接链式调用,不保存gameRegion变量,避免内存管理问题 + const imageResult = gameRegion.find(recognitionObject); + if (imageResult.isExist()) { + return imageResult; + } + } catch (error) { + log.error(`识别图像时发生异常: ${error.message}`); + } finally { + if (gameRegion) { + gameRegion.dispose(); + } + } + await sleep(CONFIG.SLEEP_INTERVAL); + } + + log.warn(`经过多次尝试,仍然无法识别图像`); + return null; +} + +/** + * 通过OCR识别数字 + * @param {Object} ocrRegion - OCR识别区域 + * @param {RegExp} pattern - 匹配模式 + * @returns {number|null} 识别到的数字或null + */ +async function recognizeNumberByOCR(ocrRegion, pattern) { + let resList = null; + let captureRegion = null; + try { + const ocrRo = RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); + captureRegion = captureGameRegion(); + resList = captureRegion.findMulti(ocrRo); + + if (!resList || resList.length === 0) { + log.warn("OCR未识别到任何文本"); + return null; + } + + 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; + } + } + } + return null; + } catch (error) { + log.error(`OCR识别时发生异常: ${error.message}`); + return null; + } finally { + if (resList && typeof resList.dispose === 'function') { + resList.dispose(); + } + if (captureRegion && typeof captureRegion.dispose === 'function') { + captureRegion.dispose(); + } + } +} + export { ocrPhysical, + countOriginalResin, + countAllResin, } \ No newline at end of file diff --git a/repo/js/AutoPlan/utils/tool.js b/repo/js/AutoPlan/utils/tool.js index 02d2bb8ff..4422daf93 100644 --- a/repo/js/AutoPlan/utils/tool.js +++ b/repo/js/AutoPlan/utils/tool.js @@ -368,7 +368,64 @@ async function findTextAndClick( return null; } +/** + * 通用找图并点击(支持图片文件路径、Mat) + * @param {string|Mat} target 图片路径或已构造的 Mat + * @param {number} [x=0] 识别区域左上角 X + * @param {number} [y=0] 识别区域左上角 Y + * @param {number} [w=1920] 识别区域宽度 + * @param {number} [h=1080] 识别区域高度 + * @param {number} [timeout=1000] 识别时间上限(毫秒) + * @param {number} [interval=50] 每次识别之间的等待间隔(毫秒) + * @param {number} [preClickDelay=50] 点击前等待时间(毫秒) + * @param {number} [postClickDelay=50] 点击后等待时间(毫秒) + * + * @returns + * - RecognitionResult | null + */ +async function findImgAndClick( + target, + x = 0, + y = 0, + w = 1920, + h = 1080, + timeout = 1000, + interval = 50, + preClickDelay = 50, + postClickDelay = 50 +) { + const ro = + typeof target === 'string' + ? RecognitionObject.TemplateMatch( + file.readImageMatSync(target), + x, y, w, h + ) + : RecognitionObject.TemplateMatch( + target, + x, y, w, h + ); + const start = Date.now(); + + while (Date.now() - start <= timeout) { + const gameRegion = captureGameRegion(); + try { + const res = gameRegion.find(ro); + if (!res.isEmpty()) { + await sleep(preClickDelay); + res.click(); + await sleep(postClickDelay); + return res; + } + } finally { + gameRegion.dispose(); + } + + await sleep(interval); + } + + return null; +} /** * 抛出错误函数 * 该函数用于显示错误通知并抛出错误对象 @@ -395,5 +452,6 @@ export { isInOutStygianOnslaughtUI, outStygianOnslaughtUI, findTextAndClick, + findImgAndClick, throwError, } \ No newline at end of file