diff --git a/repo/js/AutoLeyLineOutcrop/README.md b/repo/js/AutoLeyLineOutcrop/README.md index 921986e82..e829f3409 100644 --- a/repo/js/AutoLeyLineOutcrop/README.md +++ b/repo/js/AutoLeyLineOutcrop/README.md @@ -79,6 +79,7 @@ ## 更新日志 ### 4.4 - 新增树脂耗尽模式 +- 新增树脂耗尽模式下刷取次数取小值 - 新增更新提醒 - 新增一条龙模式 - BGI最低版本要求改为0.52.0 diff --git a/repo/js/AutoLeyLineOutcrop/assets/icon/200.png b/repo/js/AutoLeyLineOutcrop/assets/icon/200.png new file mode 100644 index 000000000..5fdf7d70b Binary files /dev/null and b/repo/js/AutoLeyLineOutcrop/assets/icon/200.png differ diff --git a/repo/js/AutoLeyLineOutcrop/assets/icon/add_button.jpg b/repo/js/AutoLeyLineOutcrop/assets/icon/add_button.jpg new file mode 100644 index 000000000..cf344bf81 Binary files /dev/null and b/repo/js/AutoLeyLineOutcrop/assets/icon/add_button.jpg differ diff --git a/repo/js/AutoLeyLineOutcrop/assets/icon/yue.png b/repo/js/AutoLeyLineOutcrop/assets/icon/yue.png new file mode 100644 index 000000000..dfa75916a Binary files /dev/null and b/repo/js/AutoLeyLineOutcrop/assets/icon/yue.png differ diff --git a/repo/js/AutoLeyLineOutcrop/main.js b/repo/js/AutoLeyLineOutcrop/main.js index 14978e6e7..b82bedb75 100644 --- a/repo/js/AutoLeyLineOutcrop/main.js +++ b/repo/js/AutoLeyLineOutcrop/main.js @@ -17,9 +17,9 @@ let recheckCount = 0; // 树脂重新检查次数(防止无限递归) const MAX_RECHECK_COUNT = 3; // 最大重新检查次数 let consecutiveFailureCount = 0; // 连续战斗失败次数 const MAX_CONSECUTIVE_FAILURES = 5; // 最大连续失败次数,超过后终止脚本 -const ocrRegion1 = { x: 800, y: 200, width: 300, height: 100 }; // 中心区域 -const ocrRegion2 = { x: 0, y: 200, width: 300, height: 300 }; // 追踪任务区域 -const ocrRegion3 = { x: 1200, y: 520, width: 300, height: 300 }; // 拾取区域 +const ocrRegion1 = {x: 800, y: 200, width: 300, height: 100}; // 中心区域 +const ocrRegion2 = {x: 0, y: 200, width: 300, height: 300}; // 追踪任务区域 +const ocrRegion3 = {x: 1200, y: 520, width: 300, height: 300}; // 拾取区域 // 预定义识别对象 const openRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/open.png")); @@ -37,22 +37,20 @@ const ocrRoThis = RecognitionObject.ocrThis; (async function () { try { await runLeyLineOutcropScript(); - } - catch (error) { + } catch (error) { // 全局错误捕获,记录并发送错误日志 log.error("出错了: {error}", error.message); if (isNotification) { notification.error(`出错了: ${error.message}`); } - } - finally { + } finally { // 确保退出奖励界面(如果在奖励界面) try { await ensureExitRewardPage(); } catch (exitError) { log.warn(`退出奖励界面时出错: ${exitError.message}`); } - + if (!marksStatus) { // 任何时候都确保自定义标记处于打开状态 await openCustomMarks(); @@ -68,10 +66,10 @@ const ocrRoThis = RecognitionObject.ocrThis; async function runLeyLineOutcropScript() { // 初始化加载配置和设置并校验 initialize(); - + // 处理树脂耗尽模式(如果开启) let runTimesValue = await handleResinExhaustionMode(); - if(runTimesValue <= 0) { + if (runTimesValue <= 0) { throw new Error("树脂耗尽,脚本将结束运行"); } @@ -79,7 +77,7 @@ async function runLeyLineOutcropScript() { // 执行地脉花挑战 await runLeyLineChallenges(); - + // 如果是树脂耗尽模式,执行完毕后再次检查是否还有树脂 if (settings.isResinExhaustionMode) { await recheckResinAndContinue(); @@ -102,13 +100,14 @@ async function initialize() { "loadSettings.js", "processLeyLineOutcrop.js", "recognizeTextInRegion.js", + "physical.js", "calCountByResin.js" - ]; + ]; for (const fileName of utils) { eval(file.readTextSync(`utils/${fileName}`)); } } catch (error) { - throw new Error(`JS文件缺失: ${error.message}`); + throw new Error(`JS文件缺失: ${error.message}`); } // 2. 加载配置文件 try { @@ -169,17 +168,17 @@ async function handleResinExhaustionMode() { if (!settings.isResinExhaustionMode) { return settings.timesValue; } - + log.info("树脂耗尽模式已开启,开始统计可刷取次数"); - + try { // 调用树脂统计函数 const resinResult = await calCountByResin(); - + if (!resinResult || typeof resinResult.count !== 'number') { throw new Error("树脂统计返回结果无效"); } - + // 检查统计到的次数是否有效 if (resinResult.count <= 0) { log.warn("统计到的可刷取次数为0,脚本将不会执行任何刷取操作"); @@ -187,27 +186,32 @@ async function handleResinExhaustionMode() { notification.send("树脂耗尽模式:统计到的可刷取次数为0,脚本将结束运行"); } } - - // 使用统计到的次数替换设置中的刷取次数 - settings.timesValue = resinResult.count; - + if (physical.OpenModeCountMin) { + settings.timesValue = Math.min(resinResult.count, settings.timesValue); + log.info(`当前开启模式刷取数量: {key}`, settings.timesValue); + } else { + // 使用统计到的次数替换设置中的刷取次数 + settings.timesValue = resinResult.count; + } + + physical.NeedRunsCount = settings.timesValue; log.info(`树脂统计成功:`); log.info(` 原粹树脂可刷取: ${resinResult.originalResinTimes} 次`); log.info(` 浓缩树脂可刷取: ${resinResult.condensedResinTimes} 次`); log.info(` 须臾树脂可刷取: ${resinResult.transientResinTimes} 次${settings.useTransientResin ? '' : '(未开启使用)'}`); log.info(` 脆弱树脂可刷取: ${resinResult.fragileResinTimes} 次${settings.useFragileResin ? '' : '(未开启使用)'}`); - log.info(` 总计可刷取次数: ${resinResult.count} 次`); - + log.info(` 总计可刷取次数: {count} 次,最小替换:{key}`, (physical.OpenModeCountMin ? settings.timesValue : resinResult.count), (physical.OpenModeCountMin ? "开启" : "未开启")); + // 发送通知 if (isNotification) { - const notificationText = + const notificationText = `全自动地脉花脚本已启用树脂耗尽模式\n\n` + `树脂统计结果(当前可刷取次数):\n` + `原粹树脂: ${resinResult.originalResinTimes} 次\n` + `浓缩树脂: ${resinResult.condensedResinTimes} 次\n` + `须臾树脂: ${resinResult.transientResinTimes} 次${settings.useTransientResin ? '' : '(未开启)'}\n` + `脆弱树脂: ${resinResult.fragileResinTimes} 次${settings.useFragileResin ? '' : '(未开启)'}\n\n` + - `总计可刷取: ${resinResult.count} 次\n`; + `总计可刷取: ${physical.OpenModeCountMin ? settings.timesValue : resinResult.count} 次\n最小替换:${(physical.OpenModeCountMin ? "开启" : "未开启")}\n`; notification.send(notificationText); } @@ -216,7 +220,7 @@ async function handleResinExhaustionMode() { // 统计失败,使用设置中的刷取次数 log.error(`树脂统计失败: ${error.message}`); log.warn(`将使用设置中的刷取次数: ${settings.timesValue}`); - + if (isNotification) { notification.send(`树脂耗尽模式:统计失败,将使用设置中的刷取次数 ${settings.timesValue} 次\n错误信息: ${error.message}`); } @@ -231,7 +235,14 @@ async function handleResinExhaustionMode() { async function recheckResinAndContinue() { // 递归深度检查,防止无限循环 recheckCount++; - + if (physical.OpenModeCountMin) { + physical.AlreadyRunsCount++; + if (physical.NeedRunsCount<=physical.AlreadyRunsCount){ + log.info(`[已开启取小值]树脂耗尽模式:任务已完成,已经运行{count}次`,physical.AlreadyRunsCount); + return; + } + } + if (recheckCount > MAX_RECHECK_COUNT) { log.warn(`已达到最大重新检查次数限制 (${MAX_RECHECK_COUNT} 次),停止继续检查`); if (isNotification) { @@ -239,27 +250,27 @@ async function recheckResinAndContinue() { } return; } - + log.info("=".repeat(50)); log.info(`树脂耗尽模式:任务已完成,开始检查树脂状态...`); log.info("=".repeat(50)); - + try { // 重新统计树脂 const resinResult = await calCountByResin(); - + if (!resinResult || typeof resinResult.count !== 'number') { log.warn("树脂统计返回结果无效,结束运行"); return; } - + log.info(`树脂检查结果:`); log.info(` 原粹树脂可刷取: ${resinResult.originalResinTimes} 次`); log.info(` 浓缩树脂可刷取: ${resinResult.condensedResinTimes} 次`); log.info(` 须臾树脂可刷取: ${resinResult.transientResinTimes} 次${settings.useTransientResin ? '' : '(未开启使用)'}`); log.info(` 脆弱树脂可刷取: ${resinResult.fragileResinTimes} 次${settings.useFragileResin ? '' : '(未开启使用)'}`); log.info(` 总计可刷取次数: ${resinResult.count} 次`); - + // 安全检查:如果检测到的次数异常多,可能是识别错误 if (resinResult.count > 50) { log.warn(`检测到异常的可刷取次数 (${resinResult.count}),为安全起见停止运行`); @@ -268,23 +279,23 @@ async function recheckResinAndContinue() { } return; } - + // 如果还有树脂可用,继续执行 if (resinResult.count > 0) { log.info(`检测到还有 ${resinResult.count} 次可刷取,继续执行地脉花挑战...`); log.info(`(这是第 ${recheckCount} 次额外检查并继续执行)`); - + if (isNotification) { notification.send(`树脂耗尽模式:检测到还有 ${resinResult.count} 次可刷取,继续执行(第 ${recheckCount} 次额外执行)`); } - + // 重置运行次数并更新目标次数 currentRunTimes = 0; settings.timesValue = resinResult.count; - + // 递归调用继续执行地脉花挑战和重新检查 await runLeyLineChallenges(); - + // 执行完后再次检查(递归) await recheckResinAndContinue(); } else { @@ -333,7 +344,7 @@ async function prepareForLeyLineRun() { setGameMetrics(1920, 1080, 1); // 看起来没什么用 // 1. 开局传送到七天神像 if (!oneDragonMode) { - await genshin.tpToStatueOfTheSeven(); + await genshin.tpToStatueOfTheSeven(); } // 2. 切换战斗队伍 @@ -425,7 +436,7 @@ async function loadNodeData() { try { const nodeDataText = await file.readText("LeyLineOutcropData.json"); const rawData = JSON.parse(nodeDataText); - + // 适配数据结构:将原始数据转换为代码期望的格式 return adaptNodeData(rawData); } catch (error) { @@ -444,7 +455,7 @@ function adaptNodeData(rawData) { node: [], indexes: rawData.indexes }; - + // 添加传送点,设置type为"teleport" if (rawData.teleports) { for (const teleport of rawData.teleports) { @@ -456,7 +467,7 @@ function adaptNodeData(rawData) { }); } } - + // 添加地脉花节点,设置type为"blossom" if (rawData.blossoms) { for (const blossom of rawData.blossoms) { @@ -468,13 +479,13 @@ function adaptNodeData(rawData) { }); } } - + // 根据edges构建next和prev关系 if (rawData.edges) { for (const edge of rawData.edges) { const sourceNode = adaptedData.node.find(node => node.id === edge.source); const targetNode = adaptedData.node.find(node => node.id === edge.target); - + if (sourceNode && targetNode) { sourceNode.next.push({ target: edge.target, @@ -484,9 +495,9 @@ function adaptNodeData(rawData) { } } } - + log.debug(`适配数据完成:传送点 ${rawData.teleports ? rawData.teleports.length : 0} 个,地脉花 ${rawData.blossoms ? rawData.blossoms.length : 0} 个,边缘 ${rawData.edges ? rawData.edges.length : 0} 个`); - + return adaptedData; } @@ -532,7 +543,7 @@ function findPathsToTarget(nodeData, targetNode) { /** * 如果需要,尝试查找反向路径(从目标节点的前置节点到传送点再到目标) - * @param {Object} nodeData - 节点数据 + * @param {Object} nodeData - 节点数据 * @param {Object} targetNode - 目标节点 * @param {Object} nodeMap - 节点映射 * @param {Array} existingPaths - 已找到的路径 @@ -628,13 +639,13 @@ async function executePath(path) { const routePath = path.routes[path.routes.length - 1]; const targetPath = routePath.replace('assets/pathing/', 'assets/pathing/target/').replace('-rerun', ''); await processLeyLineOutcrop(settings.timeout, targetPath); - + // 尝试领取奖励,如果失败则抛出异常停止执行 const rewardSuccess = await attemptReward(); if (!rewardSuccess) { throw new Error("无法领取奖励,树脂不足或其他原因"); } - + // 成功完成地脉花挑战,重置连续失败计数器 consecutiveFailureCount = 0; } @@ -677,14 +688,14 @@ async function handleNoStrategyFound() { log.error("未找到对应的地脉花策略,请再次运行脚本"); log.error("如果仍然不行,请截图{1}游戏界面,并反馈给作者!", "*完整的*"); log.error("完整的游戏界面!完整的游戏界面!完整的游戏界面!"); - + // 确保退出奖励界面 TODO: 可能会影响debug,先不执行ensureExitRewardPage // try { // await ensureExitRewardPage(); // } catch (exitError) { // log.warn(`退出奖励界面时出错: ${exitError.message}`); // } - + if (isNotification) { notification.error("未找到对应的地脉花策略"); await genshin.returnMainUi(); @@ -789,7 +800,7 @@ async function autoFight(timeout) { logFightResult = fightResult ? "成功" : "失败"; log.info(`战斗结束,战斗结果:${logFightResult}`); cts.cancel(); - + try { await fightTask; } catch (error) { @@ -800,7 +811,7 @@ async function autoFight(timeout) { log.warn(`战斗任务结束时出现异常: ${error.message}`); } } - + return fightResult; } @@ -993,7 +1004,7 @@ async function adjustViewForReward(boxIconRo, token) { captureRegion.dispose(); if (!iconRes.isExist()) { log.warn("未找到图标,等待一下"); - await sleep(1000); + await sleep(1000); continue; // 没有找到图标等一秒再继续 // throw new Error('未找到图标,没有地脉花'); } diff --git a/repo/js/AutoLeyLineOutcrop/manifest.json b/repo/js/AutoLeyLineOutcrop/manifest.json index 5a26293bb..276de2756 100644 --- a/repo/js/AutoLeyLineOutcrop/manifest.json +++ b/repo/js/AutoLeyLineOutcrop/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "全自动地脉花", - "version": "4.4.6", + "version": "4.4.7", "tags": ["地脉花"], "bgi_version": "0.52.0", "description": "基于OCR图像识别的全自动刷取地脉花。\n💡更多信息请查看README! \n\n----------注意事项----------\n●仅支持BetterGI 0.52.0 及以上版本!\n●部分地脉花因特殊原因不支持全自动,具体的点位请在手册中查看。\n●树脂使用的优先级:2倍原粹树脂 > 浓缩树脂 > 原粹树脂。\n●运行时会传送到七天神像设置中设置的七天神像,需要关闭七天神像设置中的“是否就近七天神像恢复血量”,并指定七天神像。\n●战斗策略注意调度器设置中地图追踪行走配置里的“允许在JsSpript中使用”和“覆盖JS中的自动战斗配置”,只有在都打开的情况下脚本才会使用下面的战斗配置,否则会使用独立任务中的战斗策略。战斗超时时间不能大于脚本自定义配置中的时间。\n\n如果遇到问题,请先参照README中的方法进行解决。", @@ -33,6 +33,10 @@ { "name": "羊汪汪", "links": "https://github.com/ColinXHL" + }, + { + "name": "云端客", + "links": "https://github.com/Kirito520Asuna" } ], "settings_ui": "settings.json", diff --git a/repo/js/AutoLeyLineOutcrop/settings.json b/repo/js/AutoLeyLineOutcrop/settings.json index 8f2b4c24f..aa1ac06c8 100644 --- a/repo/js/AutoLeyLineOutcrop/settings.json +++ b/repo/js/AutoLeyLineOutcrop/settings.json @@ -28,6 +28,16 @@ ], "default": "稻妻" }, + { + "name": "isResinExhaustionMode", + "type": "checkbox", + "label": "树脂耗尽模式\n开启后会自动统计所有可用树脂并计算可刷取次数,替换上方设置的刷取次数\n注意:请同时设置树脂刷取次数,将在统计失败时使用" + }, + { + "name": "openModeCountMin", + "type": "checkbox", + "label": "刷取次数取小值\n开启后树脂耗尽模式中计算可刷取次数,和上方设置的刷取次数取小值" + }, { "name": "count", "type": "input-text", @@ -77,11 +87,6 @@ "type": "checkbox", "label": "一条龙模式\n开启后,会跳过开始的前往七天神像以及强制更新" }, - { - "name": "isResinExhaustionMode", - "type": "checkbox", - "label": "树脂耗尽模式\n开启后会自动统计所有可用树脂并计算可刷取次数,替换上方设置的刷取次数\n注意:请同时设置树脂刷取次数,将在统计失败时使用" - }, { "name": "isGoToSynthesizer", "type": "select", diff --git a/repo/js/AutoLeyLineOutcrop/utils/calCountByResin.js b/repo/js/AutoLeyLineOutcrop/utils/calCountByResin.js index 180bd850a..e67a397d4 100644 --- a/repo/js/AutoLeyLineOutcrop/utils/calCountByResin.js +++ b/repo/js/AutoLeyLineOutcrop/utils/calCountByResin.js @@ -17,20 +17,20 @@ const RESIN_ICONS = { // 普通数字识别对象(1-4) const NUMBER_ICONS = [ - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1.png")), value: 1 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2.png")), value: 2 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3.png")), value: 3 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4.png")), value: 4 } + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1.png")), value: 1}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2.png")), value: 2}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3.png")), value: 3}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4.png")), value: 4} ]; // 白色数字识别对象(0-5,用于浓缩树脂) const WHITE_NUMBER_ICONS = [ - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num0_white.png")), value: 0 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1_white.png")), value: 1 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2_white.png")), value: 2 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3_white.png")), value: 3 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4_white.png")), value: 4 }, - { ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num5_white.png")), value: 5 } + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num0_white.png")), value: 0}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1_white.png")), value: 1}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2_white.png")), value: 2}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3_white.png")), value: 3}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4_white.png")), value: 4}, + {ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num5_white.png")), value: 5} ]; // 配置常量 @@ -39,19 +39,19 @@ const CONFIG = { 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 } // 避免选中效果的点击位置 + 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会根据图标宽度动态设置 + ORIGINAL_RESIN: {width: 200, height: 40}, + CONDENSED_RESIN: {width: 90, height: 40}, + OTHER_RESIN: {width: 0, height: 60} // width会根据图标宽度动态设置 } }; @@ -73,7 +73,7 @@ let resinCounts = { */ async function recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TIMEOUT) { const startTime = Date.now(); - + while (Date.now() - startTime < timeout) { try { // 直接链式调用,不保存gameRegion变量,避免内存管理问题 @@ -86,7 +86,7 @@ async function recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TI } await sleep(CONFIG.SLEEP_INTERVAL); } - + log.warn(`经过多次尝试,仍然无法识别图像`); return null; } @@ -143,10 +143,10 @@ async function recognizeWhiteNumberByImage(ocrRegion) { * @returns {boolean} 是否在区域内 */ function isPointInRegion(point, region) { - return point.x >= region.x && - point.x <= region.x + region.width && - point.y >= region.y && - point.y <= region.y + region.height; + return point.x >= region.x && + point.x <= region.x + region.width && + point.y >= region.y && + point.y <= region.y + region.height; } /** @@ -162,17 +162,17 @@ async function recognizeNumberByOCR(ocrRegion, pattern) { 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]); @@ -201,7 +201,26 @@ async function recognizeNumberByOCR(ocrRegion, pattern) { * 统计原粹树脂数量 * @returns {number} 原粹树脂数量 */ -async function countOriginalResin() { +/** + * 统计原粹树脂数量 + * @returns {number} 原粹树脂数量 + */ +async function countOriginalResin(tryOriginalMode,opToMainUi) { + if (tryOriginalMode) { + log.info("尝试使用原始模式"); + return await countOriginalResinBackup() + } else { + let ocrPhysical = await physical.ocrPhysical(opToMainUi); + await sleep(600) + if (ocrPhysical && ocrPhysical.ok) { + return ocrPhysical.remainder; + } else { + throw new Error("ocrPhysical error"); + } + } +} + +async function countOriginalResinBackup() { const originalResin = await recognizeImage(RESIN_ICONS.ORIGINAL); if (!originalResin) { log.warn(`未找到原粹树脂图标`); @@ -269,7 +288,7 @@ async function countCondensedResin() { // OCR识别整个界面的文本 let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height); textList = captureRegion.findMulti(ocrRo); - + for (const res of textList) { if (res.text.includes("当前拥有")) { const match = res.text.match(/当前拥有\s*([0-5ss])/); @@ -371,20 +390,33 @@ async function openMap() { log.info("打开地图界面"); 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.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); - + // 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("地图界面设置完成"); } +/** + * 切换到国家选择界面的异步函数 + * 通过点击指定坐标并等待界面加载来完成切换操作 + */ +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); +} + /** * 打开补充树脂界面 */ @@ -405,7 +437,7 @@ async function openReplenishResinUi() { */ function displayResults(results) { const resultText = `原粹:${results.original} 浓缩:${results.condensed} 须臾:${results.transient} 脆弱:${results.fragile}`; - + log.info(`============ 树脂统计结果 ============`); log.info(`原粹树脂数量: ${results.original}`); log.info(`浓缩树脂数量: ${results.condensed}`); @@ -420,42 +452,55 @@ function displayResults(results) { * 统计所有树脂数量的主函数 * @returns {Object} 包含所有树脂数量的对象 */ - this.countAllResin = async function() { +this.countAllResin = async function () { try { setGameMetrics(1920, 1080, 1); log.info("开始统计树脂数量"); - + // 返回主界面 - await genshin.returnMainUi(); + let toMainUi=true + // await genshin.returnMainUi(); + // await sleep(CONFIG.UI_DELAY); + + let tryPass = true; + try { + resinCounts.original = await countOriginalResin(false,toMainUi); + } catch (e) { + tryPass = false + } await sleep(CONFIG.UI_DELAY); - + // 打开地图界面统计原粹/浓缩树脂 await openMap(); await sleep(CONFIG.UI_DELAY); - log.info("开始统计地图界面中的树脂"); - resinCounts.original = await countOriginalResin(); + + if (!tryPass){ + // 如果第一次尝试失败,则切换到蒙德 + await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y) + resinCounts.original = await countOriginalResin(!tryPass); + } resinCounts.condensed = await countCondensedResin(); - + // 打开补充树脂界面统计须臾/脆弱树脂 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, @@ -463,7 +508,7 @@ function displayResults(results) { transientResinCount: resinCounts.transient, fragileResinCount: resinCounts.fragile }; - + } catch (error) { log.error(`统计树脂数量时发生异常: ${error.message}`); throw error; @@ -480,7 +525,7 @@ function displayResults(results) { * fragileResinTimes: number * } */ -this.calCountByResin = async function() { +this.calCountByResin = async function () { try { let countResult = await this.countAllResin(); let count = 0; @@ -508,7 +553,7 @@ this.calCountByResin = async function() { // 2. 浓缩树脂:每个计算为1次 let condensedResinTimes = countResult.condensedResinCount; log.info(`浓缩树脂可刷取次数: ${condensedResinTimes}`); - + // 3. 须臾树脂:检查设置是否开启 let transientResinTimes = 0; if (settings.useTransientResin) { @@ -517,7 +562,7 @@ this.calCountByResin = async function() { } else { log.info(`须臾树脂未开启使用,跳过计算`); } - + // 4. 脆弱树脂:检查设置是否开启 let fragileResinTimes = 0; if (settings.useFragileResin) { @@ -526,7 +571,7 @@ this.calCountByResin = async function() { } else { log.info(`脆弱树脂未开启使用,跳过计算`); } - + // 计算总次数 count = originalResinTimes + condensedResinTimes + transientResinTimes + fragileResinTimes; log.info(`总计可刷取次数: ${count}`); diff --git a/repo/js/AutoLeyLineOutcrop/utils/physical.js b/repo/js/AutoLeyLineOutcrop/utils/physical.js new file mode 100644 index 000000000..c59db88f6 --- /dev/null +++ b/repo/js/AutoLeyLineOutcrop/utils/physical.js @@ -0,0 +1,252 @@ +const commonPath = 'assets/icon/' +const commonMap = new Map([ + ['main_ui', { + path: `${commonPath}`, + name: 'paimon_menu', + type: '.png', + }], + ['yue', { + path: `${commonPath}`, + name: 'yue', + type: '.png', + }], + ['200', { + path: `${commonPath}`, + name: '200', + type: '.png', + }], + ['add_button', { + path: `${commonPath}`, + name: 'add_button', + type: '.jpg', + }], +]) +//==================================================== +const genshinJson = { + width: 1920,//genshin.width, + height: 1080,//genshin.height, +} +const MinPhysical = settings.minPhysical?parseInt(settings.minPhysical+''):parseInt(20+'') +const OpenModeCountMin = settings.openModeCountMin +let AlreadyRunsCount=0 +let NeedRunsCount=0 +const TemplateOrcJson={x: 1568, y: 16, width: 225, height: 60,} +//==================================================== +/** + * 根据键值获取JSON路径 + * @param {string} key - 要查找的键值 + * @returns {any} 返回与键值对应的JSON路径值 + */ +function getJsonPath(key) { + return commonMap.get(key); // 通过commonMap的get方法获取指定键对应的值 +} + +/** + * 从字符串中提取数字并组合成一个整数 + * @param {string} str - 包含数字的字符串 + * @returns {number} - 由字符串中所有数字组合而成的整数 + */ +async function saveOnlyNumber(str) { + // 使用正则表达式匹配字符串中的所有数字 + // \d+ 匹配一个或多个数字 + // .join('') 将匹配到的数字数组连接成一个字符串 + // parseInt 将连接后的字符串转换为整数 + return parseInt(str.match(/\d+/g).join('')); +} + +/** + * 识别原粹树脂(体力)的函数 + * @param {boolean} [opToMainUi=false] - 是否操作到主界面 + * @returns {Promise} 返回一个包含识别结果的Promise对象 + * - ok {boolean}: 是否可执行(体力是否足够) + * - min {number}: 最小可执行体力值 + * - remainder {number}: 当前剩余体力值 + */ +async function ocrPhysical(opToMainUi = false) { + // 检查是否启用体力识别功能,如果未启用则直接返回默认结果 + if (!settings.isResinExhaustionMode) { + log.info(`===未启用===`) + return { + ok: true, + min: 0, + remainder: 0, + } + } + log.info(`===开始识别原粹树脂===`) + let ms = 1000 // 定义操作延迟时间(毫秒) + if (opToMainUi) { + await toMainUi(); // 切换到主界面 + } + //设置最小可执行体力值 + let minPhysical = MinPhysical + //打开地图界面 + await keyPress('M') + await sleep(ms) + log.debug(`===[点击+]===`) + //点击+ 按钮 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}`, + x: 1264, + y: 39, + width: genshinJson.width - 1264, + height: 60, + } + 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 buttonA = captureGameRegion().find(templateMatchAddButtonRo); + let buttonA = region.find(templateMatchAddButtonRo); + regionA.Dispose() + + await sleep(ms) + if (!buttonA.isExist()) { + log.error(`未找到${add_objJson.path}请检查路径是否正确`) + throwError(`未找到${add_objJson.path}请检查路径是否正确`) + } + await buttonA.click() + // let add_obj = { + // x: 1264, + // y: 39, + // } + // await click(add_obj.x, add_obj.y) + await sleep(ms) + + log.debug(`===[定位原粹树脂]===`) + //定位月亮 + let jsonPath = getJsonPath('yue'); + let tmJson = { + path: `${jsonPath.path}${jsonPath.name}${jsonPath.type}`, + x: TemplateOrcJson.x, + y: TemplateOrcJson.y, + width: TemplateOrcJson.width, + height: TemplateOrcJson.height, + } + let templateMatchButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${tmJson.path}`), tmJson.x, tmJson.y, tmJson.width, tmJson.height); + let region =captureGameRegion() + // let button = captureGameRegion().find(templateMatchButtonRo); + let button = region.find(templateMatchButtonRo); + region.Dispose() + await sleep(ms) + if (!button.isExist()) { + log.error(`${tmJson.path} 匹配异常`) + throwError(`${tmJson.path} 匹配异常`) + } + + log.debug(`===[定位/200]===`) + //定位200 + let jsonPath2 = getJsonPath('200'); + let tmJson2 = { + path: `${jsonPath2.path}${jsonPath2.name}${jsonPath2.type}`, + x: TemplateOrcJson.x, + y: TemplateOrcJson.y, + width: TemplateOrcJson.width, + height: TemplateOrcJson.height, + } + let templateMatchButtonRo2 = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${tmJson2.path}`), tmJson2.x, tmJson2.y, tmJson2.width, tmJson2.height); + let region2 = captureGameRegion() + let button2 = region2.find(templateMatchButtonRo2); + region2.Dispose() + + await sleep(ms) + if (!button2.isExist()) { + log.error(`${tmJson2.path} 匹配异常`) + throwError(`${tmJson2.path} 匹配异常`) + } + + log.debug(`===[识别原粹树脂]===`) + //识别体力 x=1625,y=31,width=79,height=30 / x=1689,y=35,width=15,height=26 + let ocr_obj = { + // x: 1623, + x: button.x + button.width-20, + // y: 32, + y: button.y, + // width: 61, + width: Math.abs(button2.x - button.x - button.width+20), + height: button2.height + } + + log.debug(`ocr_obj: x={x},y={y},width={width},height={height}`, ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height) + + try { + let recognitionObjectOcr = RecognitionObject.Ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height); + let region3 = captureGameRegion() + let res = region3.find(recognitionObjectOcr); + region3.Dispose() + + log.info(`[OCR原粹树脂]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`); + let remainder = await saveOnlyNumber(res.text) + let execute = (remainder - minPhysical) >= 0 + log.info(`最小可执行原粹树脂:{min},原粹树脂:{key}`, minPhysical, remainder,) + + await keyPress('VK_ESCAPE') + return { + ok: execute, + min: minPhysical, + remainder: remainder, + } + } catch (e) { + throwError(`识别失败,err:${e.message}`) + } finally { + if (opToMainUi) { + await toMainUi(); // 切换到主界面 + } + } + +} + +/** + * 抛出错误函数 + * 该函数用于显示错误通知并抛出错误对象 + * @param {string} msg - 错误信息,将用于通知和错误对象 + */ +function throwError(msg) { + // 使用notification组件显示错误通知 + // notification.error(`${msg}`); + if (setting.isNotification) { + notification.error(`${msg}`); + } + // 抛出一个包含错误信息的Error对象 + throw new Error(`${msg}`); +} + +// 判断是否在主界面的函数 +const isInMainUI = () => { + // let name = '主界面' + let main_ui = getJsonPath('main_ui'); + // 定义识别对象 + let paimonMenuRo = RecognitionObject.TemplateMatch( + file.ReadImageMatSync(`${main_ui.path}${main_ui.name}${main_ui.type}`), + 0, + 0, + genshinJson.width / 3.0, + genshinJson.width / 5.0 + ); + let captureRegion = captureGameRegion(); + let res = captureRegion.find(paimonMenuRo); + captureRegion.Dispose() + return !res.isEmpty(); +}; + +async function toMainUi() { + let ms = 300 + let index = 1 + await sleep(ms); + while (!isInMainUI()) { + await sleep(ms); + await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面 + await sleep(ms); + if (index > 3) { + throwError(`多次尝试返回主界面失败`); + } + index += 1 + } + +} + +this.physical = { + ocrPhysical, + MinPhysical, + OpenModeCountMin, + AlreadyRunsCount, + NeedRunsCount, +} \ No newline at end of file