From ea5d51bc558ce49c689a28074e741d6c923bcc01 Mon Sep 17 00:00:00 2001 From: Colin Xu <127581131+ColinXHL@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:15:01 +0800 Subject: [PATCH] =?UTF-8?q?[=E8=87=AA=E5=8A=A8=E5=9C=B0=E8=84=89=E8=8A=B1]?= =?UTF-8?q?=E9=80=82=E9=85=8D=E6=96=B0=E7=9A=84=E5=A5=96=E5=8A=B1=E9=A2=86?= =?UTF-8?q?=E5=8F=96=E7=95=8C=E9=9D=A2=E5=8F=8A=E4=BC=98=E5=8C=96=E9=80=80?= =?UTF-8?q?=E5=87=BA=E9=80=BB=E8=BE=91=20(#2145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 适配新的奖励领取界面及优化退出逻辑 - 在各功能函数中添加奖励界面退出校验机制 - 增强奖励尝试及路径执行的错误处理与日志记录 - 适配新的奖励领取界面,基于当前状态优化树脂消耗决策 * Update version to 4.3 and add new contributor information in manifest.json * ”将检查是否关闭奖励界面“的逻辑从"未找到对应的地脉花策略"中移除 --- repo/js/AutoLeyLineOutcrop/README.md | 4 + .../RecognitionObject/replenish_button.png | Bin 0 -> 3082 bytes .../RecognitionObject/switch_button.png | Bin 0 -> 696 bytes .../RecognitionObject/use_button.png | Bin 0 -> 2338 bytes repo/js/AutoLeyLineOutcrop/main.js | 28 +- repo/js/AutoLeyLineOutcrop/manifest.json | 6 +- .../AutoLeyLineOutcrop/utils/attemptReward.js | 595 +++++++++++++++--- .../utils/executePathsUsingNodeData.js | 8 + .../utils/findLeyLineOutcrop.js | 2 + .../utils/findLeyLineOutcropByBook.js | 7 +- .../utils/processLeyLineOutcrop.js | 3 + 11 files changed, 568 insertions(+), 85 deletions(-) create mode 100644 repo/js/AutoLeyLineOutcrop/RecognitionObject/replenish_button.png create mode 100644 repo/js/AutoLeyLineOutcrop/RecognitionObject/switch_button.png create mode 100644 repo/js/AutoLeyLineOutcrop/RecognitionObject/use_button.png diff --git a/repo/js/AutoLeyLineOutcrop/README.md b/repo/js/AutoLeyLineOutcrop/README.md index fb55bc2ce..3284d6cc7 100644 --- a/repo/js/AutoLeyLineOutcrop/README.md +++ b/repo/js/AutoLeyLineOutcrop/README.md @@ -48,6 +48,10 @@ 打开大地图,点击左下角设置,开启自定义标记。 脚本非正常结束运行时会出现该问题。 ## 更新日志 +### 4.3 +- 适配新的奖励领取界面 +- 修复挪德卡莱识别错误及通过冒险之证寻找地脉花总会选择挪德卡莱的bug +- 修复卡在奖励领取界面无法执行其他配置的bug ### 4.2 - 新增挪德卡莱区域 ### 4.1 diff --git a/repo/js/AutoLeyLineOutcrop/RecognitionObject/replenish_button.png b/repo/js/AutoLeyLineOutcrop/RecognitionObject/replenish_button.png new file mode 100644 index 0000000000000000000000000000000000000000..c1cfff9201e422a7cf1adf2ee4dfb8a92d68f1ca GIT binary patch literal 3082 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 浓缩树脂 > 原粹树脂。\n●运行时会传送到七天神像设置中设置的七天神像,需要关闭七天神像设置中的“是否就近七天神像恢复血量”,并指定七天神像。\n●战斗策略注意调度器设置中地图追踪行走配置里的“允许在JsSpript中使用”和“覆盖JS中的自动战斗配置”,只有在都打开的情况下脚本才会使用下面的战斗配置,否则会使用独立任务中的战斗策略。战斗超时时间不能大于脚本自定义配置中的时间。\n\n如果遇到问题,请先参照手册中的方法进行解决。", @@ -29,6 +29,10 @@ { "name": "寒烟", "links": "https://github.com/214-hanyan" + }, + { + "name": "ColinXu", + "links": "https://github.com/ColinXHL" } ], "settings_ui": "settings.json", diff --git a/repo/js/AutoLeyLineOutcrop/utils/attemptReward.js b/repo/js/AutoLeyLineOutcrop/utils/attemptReward.js index 38a243b36..0c5a68c3a 100644 --- a/repo/js/AutoLeyLineOutcrop/utils/attemptReward.js +++ b/repo/js/AutoLeyLineOutcrop/utils/attemptReward.js @@ -38,9 +38,467 @@ this.clickWithVerification = async function(x, y, targetText, maxRetries = 20) { } /** - * 尝试领取地脉花奖励 + * 验证是否在奖励界面 + * 使用OCR识别"地脉之花"或"激活地脉之花"文字,不受分辨率影响 + * @returns {Promise} + */ +this.verifyRewardPage = async function() { + try { + let captureRegion = captureGameRegion(); + + // 使用OCR识别上半区域 + let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height / 2); + let textList = captureRegion.findMulti(ocrRo); + captureRegion.dispose(); + + let isValid = false; + if (textList && textList.count > 0) { + for (let i = 0; i < textList.count; i++) { + let text = textList[i].text; + // 识别关键文字 + if (text.includes("激活地脉之花") || + text.includes("选择激活方式")) { + isValid = true; + log.info(`奖励界面验证: 成功(识别到文字: "${text}")`); + break; + } + } + } + + // 已注释:减少日志输出 + // if (!isValid) { + // log.info(`奖励界面验证: 失败(未识别到关键文字)`); + // } + + return isValid; + } catch (error) { + log.error(`验证奖励界面失败: ${error.message}`); + return false; + } +} + +/** + * 检查原粹树脂是否耗尽(通过OCR识别"补充"文字) + * 如果原粹树脂耗尽,第一个按钮会变成"补充"按钮 + * @param {ImageRegion} captureRegion - 截图区域 + * @returns {Promise} + */ +async function checkOriginalResinEmpty(captureRegion) { + try { + // 使用OCR识别"补充"文字 + let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height); + let textList = captureRegion.findMulti(ocrRo); + + if (textList && textList.count > 0) { + for (let i = 0; i < textList.count; i++) { + let text = textList[i].text; + if (text.includes("补充")) { + log.warn("检测到补充文字,原粹树脂已耗尽"); + return true; + } + } + } + + return false; + } catch (error) { + log.error(`检查原粹树脂状态失败: ${error.message}`); + return false; + } +} + +/** + * 查找并排序所有使用按钮(通过OCR识别"使用"文字) + * 注意:如果原粹树脂耗尽,第一个位置是"补充"按钮,不会被识别为"使用"按钮 + * @param {ImageRegion} captureRegion - 截图区域 + * @returns {Promise} + */ +async function findAndSortUseButtons(captureRegion) { + try { + // 使用OCR识别所有"使用"文字 + let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height); + let textList = captureRegion.findMulti(ocrRo); + + if (!textList || textList.count === 0) { + log.warn("未找到任何文本"); + return []; + } + + // 查找只包含"使用"两个字的文本(真正的按钮) + let buttons = []; + for (let i = 0; i < textList.count; i++) { + let textRegion = textList[i]; + let text = textRegion.text.trim(); // 去除首尾空格 + + // 只匹配恰好是"使用"的文本,排除描述性文字 + if (text === "使用") { + // 按钮就在文本位置(OCR识别到的就是按钮本身) + let buttonX = Math.round(textRegion.x + textRegion.width / 2); + let buttonY = Math.round(textRegion.y + textRegion.height / 2); + let textY = textRegion.y; // 提前保存Y坐标值 + let textContent = textRegion.text; // 提前保存文本内容 + + // 创建虚拟按钮Region对象(不保存textRegion引用,避免dispose后访问失败) + let virtualButton = { + index: buttons.length, + region: { + x: buttonX, + y: buttonY, + click: function() { + click(buttonX, buttonY); + } + }, + x: buttonX, + y: textY, // 用文本的Y坐标进行排序 + text: textContent // 保存文本内容而非引用 + }; + + buttons.push(virtualButton); + } + } + + if (buttons.length === 0) { + log.warn("未找到包含'使用'的文本"); + return []; + } + + // 按Y坐标排序 + buttons.sort((a, b) => a.y - b.y); + + log.info(`找到 ${buttons.length} 个使用按钮`); + + return buttons; + } catch (error) { + log.error(`查找使用按钮失败: ${error.message}`); + return []; + } +} + +/** + * 分析树脂选项并决定使用哪个 + * @param {ImageRegion} captureRegion - 截图区域 + * @param {Array} sortedButtons - 排序后的使用按钮数组 + * @param {boolean} isOriginalResinEmpty - 原粹树脂是否耗尽 + * @returns {Promise} + */ +async function analyzeResinOptions(captureRegion, sortedButtons, isOriginalResinEmpty) { + try { + // OCR识别整个界面的文本 + let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height); + let textList = captureRegion.findMulti(ocrRo); + + if (!textList || textList.count === 0) { + log.warn("OCR未识别到任何文本"); + return null; + } + + // 收集所有识别到的文本 + let allTexts = []; + for (let i = 0; i < textList.count; i++) { + allTexts.push({ + text: textList[i].text, + y: textList[i].y + }); + } + + + // 检测是否有双倍/多倍产出 + let hasDoubleReward = allTexts.some(t => + t.text.includes("双倍") || + t.text.includes("2倍产出") || + t.text.includes("2倍") + ); + + // 只在有双倍产出时输出 + if (hasDoubleReward) { + log.info("检测到双倍产出"); + } + + // 识别树脂类型(注意:如果原粹树脂耗尽,应该忽略这些识别) + let hasOriginalResin20 = !isOriginalResinEmpty && allTexts.some(t => + (t.text.includes("20") && t.text.includes("原粹树脂")) || + (t.text.includes("20个") && t.text.includes("原粹树脂")) + ); + + let hasOriginalResin40 = !isOriginalResinEmpty && allTexts.some(t => + (t.text.includes("40") && t.text.includes("原粹树脂")) || + (t.text.includes("40个") && t.text.includes("原粹树脂")) + ); + + let hasCondensedResin = allTexts.some(t => + t.text.includes("浓缩树脂") || t.text.includes("浓缩") + ); + + let hasTransientResin = allTexts.some(t => + t.text.includes("须臾树脂") || t.text.includes("须臾") + ); + + let hasFragileResin = allTexts.some(t => + t.text.includes("脆弱树脂") || t.text.includes("脆弱") + ); + + let hasPrimogems = allTexts.some(t => + t.text.includes("原石") && t.text.includes("3次") + ); + + // 输出识别到的树脂类型(调试用) + log.info(`识别到的树脂类型 - 原粹20:${hasOriginalResin20}, 原粹40:${hasOriginalResin40}, 浓缩:${hasCondensedResin}, 须臾:${hasTransientResin}, 脆弱:${hasFragileResin}, 原石:${hasPrimogems}, 双倍:${hasDoubleReward}`); + + // 决策逻辑(根据原粹树脂是否耗尽,决策不同) + let choice = null; + + if (isOriginalResinEmpty) { + // ===== 原粹树脂耗尽的情况 ===== + // 此时第一个"使用"按钮对应的是浓缩/须臾/脆弱树脂 + log.warn("原粹树脂已耗尽,检测是否有其他可用树脂"); + + if (hasCondensedResin && sortedButtons.length >= 1) { + choice = { + type: "使用1个浓缩树脂(原粹耗尽)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } else if (hasTransientResin && sortedButtons.length >= 1) { + choice = { + type: "使用1个须臾树脂(原粹耗尽)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } else if (hasFragileResin && sortedButtons.length >= 1 && settings.useFragileResin) { + choice = { + type: "使用1个脆弱树脂(原粹耗尽)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } else { + // 输出详细的调试信息 + if (hasFragileResin && !settings.useFragileResin) { + log.warn(`原粹树脂耗尽,检测到脆弱树脂但配置禁止使用(settings.useFragileResin=${settings.useFragileResin})`); + } else { + log.warn(`原粹树脂耗尽且无其他可用树脂(浓缩:${hasCondensedResin}, 须臾:${hasTransientResin}, 脆弱:${hasFragileResin}, 原石:${hasPrimogems})`); + } + return null; + } + } else { + // ===== 原粹树脂充足的情况 ===== + // 第一个"使用"按钮对应原粹树脂 + // 第二个"使用"按钮对应浓缩/须臾/脆弱树脂 + + // 优先级1: 如果有双倍产出,优先使用原粹树脂 + if (hasDoubleReward && (hasOriginalResin20 || hasOriginalResin40)) { + // 如果当前是20个原粹树脂,先尝试切换到40个 + if (hasOriginalResin20 && !hasOriginalResin40) { + let switchSuccess = await trySwitch20To40Resin(captureRegion); + if (switchSuccess) { + choice = { + type: "使用40个原粹树脂(从20切换,双倍产出)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } else { + choice = { + type: "使用20个原粹树脂(双倍产出)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } + } else { + choice = { + type: hasOriginalResin40 ? "使用40个原粹树脂(双倍产出)" : "使用20个原粹树脂(双倍产出)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } + } + // 优先级2: 优先使用浓缩树脂 + else if (hasCondensedResin && sortedButtons.length >= 2) { + choice = { + type: "使用1个浓缩树脂", + button: sortedButtons[1], + buttonIndex: 1 + }; + } + // 优先级3: 使用须臾树脂 + else if (hasTransientResin && sortedButtons.length >= 2) { + choice = { + type: "使用1个须臾树脂", + button: sortedButtons[1], + buttonIndex: 1 + }; + } + // 优先级4: 使用原粹树脂 + else if (hasOriginalResin20 || hasOriginalResin40) { + // 如果当前是20个原粹树脂,先尝试切换到40个 + if (hasOriginalResin20 && !hasOriginalResin40) { + let switchSuccess = await trySwitch20To40Resin(captureRegion); + if (switchSuccess) { + choice = { + type: "使用40个原粹树脂(从20切换)", + button: sortedButtons[0], + buttonIndex: 0 + }; + } else { + choice = { + type: "使用20个原粹树脂", + button: sortedButtons[0], + buttonIndex: 0 + }; + } + } else { + choice = { + type: hasOriginalResin40 ? "使用40个原粹树脂" : "使用20个原粹树脂", + button: sortedButtons[0], + buttonIndex: 0 + }; + } + } + // 优先级5: 如果配置允许,使用脆弱树脂 + else if (hasFragileResin && settings.useFragileResin && sortedButtons.length >= 2) { + choice = { + type: "使用1个脆弱树脂", + button: sortedButtons[1], + buttonIndex: 1 + }; + } + // 默认: 点击第一个按钮(原粹树脂) + else if (sortedButtons.length >= 1) { + // 尝试切换到40个原粹树脂(如果当前是20个) + if (hasOriginalResin20 && !hasOriginalResin40) { + let switchSuccess = await trySwitch20To40Resin(captureRegion); + choice = { + type: switchSuccess ? "默认使用40个原粹树脂(从20切换)" : "默认使用20个原粹树脂", + button: sortedButtons[0], + buttonIndex: 0 + }; + } else { + choice = { + type: "默认使用原粹树脂", + button: sortedButtons[0], + buttonIndex: 0 + }; + } + } + } + + return choice; + + } catch (error) { + log.error(`分析树脂选项失败: ${error.message}`); + return null; + } +} + +/** + * 尝试将20个原粹树脂切换到40个原粹树脂 + * @param {ImageRegion} captureRegion - 截图区域 + * @returns {Promise} 是否成功切换 + */ +async function trySwitch20To40Resin(captureRegion) { + try { + log.info("检测到20个原粹树脂,尝试切换到40个"); + + // 检测切换按钮 + const switchButtonIcon = file.ReadImageMatSync("RecognitionObject/switch_button.png"); + const switchButtonRo = RecognitionObject.TemplateMatch(switchButtonIcon); + switchButtonRo.threshold = 0.7; // 设置合适的阈值 + + // 查找切换按钮 + let switchButtonPos = captureRegion.find(switchButtonRo); + + if (!switchButtonPos || switchButtonPos.isEmpty()) { + log.info("未找到切换按钮(树脂不足40或按钮不可用),保持使用20个原粹树脂"); + return false; + } + + // 找到可用的切换按钮,点击切换 + log.info(`找到切换按钮,点击切换到40个原粹树脂`); + switchButtonPos.click(); + await sleep(800); + + // 验证是否切换成功 + let newCaptureRegion = captureGameRegion(); + let ocrRo = RecognitionObject.Ocr(0, 0, newCaptureRegion.width, newCaptureRegion.height); + let textList = newCaptureRegion.findMulti(ocrRo); + newCaptureRegion.dispose(); + + if (textList && textList.count > 0) { + for (let i = 0; i < textList.count; i++) { + let text = textList[i].text; + if ((text.includes("40") && text.includes("原粹")) || + (text.includes("40个") && text.includes("树脂"))) { + log.info("成功切换到40个原粹树脂"); + return true; + } + } + } + + log.warn("点击切换按钮后,未能确认切换到40个原粹树脂"); + return false; + + } catch (error) { + log.error(`切换树脂数量失败: ${error.message}`); + return false; + } +} + +/** + * 切换回战斗队伍 * @returns {Promise} */ +async function switchBackToCombatTeam() { + try { + log.info("切换回战斗队伍"); + await sleep(500); + const switchSuccess = await switchTeam(settings.team); + if (!switchSuccess) { + log.warn("切换队伍可能失败"); + } + } catch (error) { + log.error(`切换队伍失败: ${error.message}`); + } +} + +/** + * 确保退出奖励界面 + * 循环检测并退出,直到确认不在奖励界面 + * @returns {Promise} + */ +this.ensureExitRewardPage = async function() { + const MAX_ATTEMPTS = 5; // 最多尝试5次 + let attempts = 0; + + try { + log.info("检查是否需要退出奖励界面"); + + while (attempts < MAX_ATTEMPTS) { + attempts++; + + // 检测是否在奖励界面 + let isInRewardPage = await verifyRewardPage(); + + if (!isInRewardPage) { + log.info("已确认不在奖励界面"); + return; + } + + // 还在奖励界面,按ESC退出 + log.info(`检测到仍在奖励界面,按ESC退出 (第${attempts}次)`); + keyPress("VK_ESCAPE"); + await sleep(800); // 等待界面关闭动画 + } + + // 超过最大尝试次数 + log.warn(`已尝试${MAX_ATTEMPTS}次退出奖励界面,可能仍在界面中`); + + } catch (error) { + log.error(`退出奖励界面时出错: ${error.message}`); + } +} + +/** + * 尝试领取地脉花奖励(图像识别+OCR混合版本) + * @param {number} retryCount - 重试次数 + * @returns {Promise} + */ this.attemptReward = async function (retryCount = 0) { const MAX_RETRY = 3; if (retryCount >= MAX_RETRY) { @@ -49,90 +507,63 @@ this.attemptReward = async function (retryCount = 0) { log.info("开始领取地脉奖励"); keyPress("F"); - await sleep(500); + await sleep(800); - // 识别是否为地脉之花界面 - let captureRegion = captureGameRegion(); - let resList = captureRegion.findMulti(ocrRoThis); // 使用预定义的ocrRoThis对象 - captureRegion.dispose(); - let isValid = false; - let condensedResin = null; - let originalResin = null; - let fragileResin = null; - let isResinEmpty = false; - let dobuleReward = false; - let isOriginalResinEmpty = false; - - if (resList && resList.count > 0) { - // 分析识别到的文本 - for (let i = 0; i < resList.count; i++) { - let res = resList[i]; - if (res.text.includes("浓缩树脂")) { - isValid = true; - condensedResin = res; - } else if (res.text.includes("原粹树脂")) { - isValid = true; - originalResin = res; - } else if (res.text.includes("脆弱树脂") && settings.fragileResin) { - isValid = true; - fragileResin = res; - } else if (res.text.includes("双倍掉落")) { - isValid = true; - dobuleReward = true; - } else if (res.text.includes("补充")){ - isValid = true; - isOriginalResinEmpty = true; - } else { - isValid = true; - isResinEmpty = true; - } - } // 处理不同的树脂情况 - if (originalResin && dobuleReward && !isOriginalResinEmpty) { - log.info("选择使用原粹树脂,获得双倍产出"); - await clickWithVerification( - Math.round(originalResin.x + originalResin.width / 2) + 400, - Math.round(originalResin.y + originalResin.height / 2), - "使用" - ); - } else if (condensedResin) { - log.info("选择使用浓缩树脂"); - await clickWithVerification( - Math.round(condensedResin.x + condensedResin.width / 2) + 400, - Math.round(condensedResin.y + condensedResin.height / 2), - "使用" - ); - } else if (originalResin && !isOriginalResinEmpty) { - log.info("选择使用原粹树脂"); - await clickWithVerification( - Math.round(originalResin.x + originalResin.width / 2) + 400, - Math.round(originalResin.y + originalResin.height / 2), - "使用" - ); - } else if (fragileResin) { - log.info("选择使用脆弱树脂"); - await clickWithVerification( - Math.round(fragileResin.x + fragileResin.width / 2) + 400, - Math.round(fragileResin.y + fragileResin.height / 2), - "使用" - ); - } else if (isResinEmpty && isOriginalResinEmpty) { - log.error("树脂用完了呢"); - keyPress("VK_ESCAPE"); - throw new Error("原粹树脂不足20,无法领取奖励"); - } - if (settings.friendshipTeam) { - log.info("切换回战斗队伍"); - await sleep(500); - const switchSuccess = await switchTeam(settings.team); - } - } - - // 界面不正确,尝试重试 - if (!isValid) { - log.info("当前界面不是地脉之花界面,重试"); + // 步骤1: 验证是否在奖励界面 + if (!await verifyRewardPage()) { + log.warn("当前不在奖励界面,尝试重试"); await genshin.returnMainUi(); await sleep(1000); await autoNavigateToReward(); - await attemptReward(++retryCount); + return await this.attemptReward(++retryCount); } + + let captureRegion = captureGameRegion(); + + // 步骤2: 检查原粹树脂是否耗尽(通过"补充"按钮) + let isOriginalResinEmpty = await checkOriginalResinEmpty(captureRegion); + + // 步骤3: 识别所有使用按钮并排序 + let sortedButtons = await findAndSortUseButtons(captureRegion); + + if (sortedButtons.length === 0) { + log.error("未找到任何使用按钮"); + captureRegion.dispose(); + keyPress("VK_ESCAPE"); + await sleep(500); + await ensureExitRewardPage(); + return false; + } + + // 步骤4: 根据原粹树脂状态调整决策逻辑 + let resinChoice = await analyzeResinOptions(captureRegion, sortedButtons, isOriginalResinEmpty); + + if (!resinChoice) { + // 已在 analyzeResinOptions 中输出详细错误信息,这里不再重复 + captureRegion.dispose(); + keyPress("VK_ESCAPE"); + await sleep(500); + await ensureExitRewardPage(); + return false; + } + + // 步骤5: 点击对应的使用按钮 + log.info(`选择: ${resinChoice.type},点击按钮 (X=${resinChoice.button.x}, Y=${resinChoice.button.y})`); + resinChoice.button.region.click(); + await sleep(1000); + + // 步骤6: 如果需要切换回战斗队伍 + if (settings.friendshipTeam) { + await switchBackToCombatTeam(); + } + + captureRegion.dispose(); + + // 等待领奖动画/道具到账 + await sleep(1200); + + // 确保完全退出奖励界面 + await ensureExitRewardPage(); + + return true; } \ No newline at end of file diff --git a/repo/js/AutoLeyLineOutcrop/utils/executePathsUsingNodeData.js b/repo/js/AutoLeyLineOutcrop/utils/executePathsUsingNodeData.js index 96700c8a0..f7d05dd4e 100644 --- a/repo/js/AutoLeyLineOutcrop/utils/executePathsUsingNodeData.js +++ b/repo/js/AutoLeyLineOutcrop/utils/executePathsUsingNodeData.js @@ -11,6 +11,7 @@ this.executePathsUsingNodeData = async function (position) { if (!targetNode) { log.error(`未找到与坐标(${currentNodePosition.x}, ${currentNodePosition.y})匹配的目标节点`); + await ensureExitRewardPage(); return; } log.debug(`找到目标节点: ID ${targetNode.id}, 位置(${targetNode.position.x}, ${targetNode.position.y})`); @@ -18,6 +19,7 @@ this.executePathsUsingNodeData = async function (position) { if (paths.length === 0) { log.error(`未找到通向目标节点(ID: ${targetNode.id})的路径`); + await ensureExitRewardPage(); return; } @@ -45,6 +47,7 @@ this.executePathsUsingNodeData = async function (position) { const nextNode = nodeData.node.find(node => node.id === nextNodeId); if (!nextNode) { + await ensureExitRewardPage(); return; } const pathObject = { @@ -81,6 +84,7 @@ this.executePathsUsingNodeData = async function (position) { if (!found) { log.warn("无法在分支点找到下一个地脉花,退出本次循环"); + await ensureExitRewardPage(); return; } log.info(`找到下一个地脉花,位置: (${leyLineX}, ${leyLineY})`); @@ -114,6 +118,7 @@ this.executePathsUsingNodeData = async function (position) { // 恢复原始坐标 leyLineX = currentLeyLineX; leyLineY = currentLeyLineY; + await ensureExitRewardPage(); return; } const nextNode = nodeData.node.find(node => node.id === selectedNodeId); @@ -122,6 +127,7 @@ this.executePathsUsingNodeData = async function (position) { // 恢复原始坐标 leyLineX = currentLeyLineX; leyLineY = currentLeyLineY; + await ensureExitRewardPage(); return; } @@ -149,10 +155,12 @@ this.executePathsUsingNodeData = async function (position) { catch (error) { if(error.message.includes("战斗失败")) { log.error("战斗失败,重新寻找地脉花后重试"); + await ensureExitRewardPage(); return; } // 其他错误需要向上传播 log.error(`执行路径时出错: ${error.message}`); + await ensureExitRewardPage(); throw error; } } \ No newline at end of file diff --git a/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcrop.js b/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcrop.js index f91149cd7..85cd37e9d 100644 --- a/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcrop.js +++ b/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcrop.js @@ -10,6 +10,7 @@ async function (country, type) { await sleep(1000); log.info("开始寻找地脉花"); if (!config.mapPositions[country] || config.mapPositions[country].length === 0) { + await ensureExitRewardPage(); throw new Error(`未找到国家 ${country} 的位置信息`); } @@ -29,6 +30,7 @@ async function (country, type) { } // 如果到这里还没找到 + await ensureExitRewardPage(); throw new Error("寻找地脉花失败,已达最大重试次数"); } diff --git a/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcropByBook.js b/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcropByBook.js index 43ee8185b..88c949ceb 100644 --- a/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcropByBook.js +++ b/repo/js/AutoLeyLineOutcrop/utils/findLeyLineOutcropByBook.js @@ -5,6 +5,11 @@ * @returns {Promise} */ this.findLeyLineOutcropByBook = async function (country, type) { + // 挪德卡莱会被识别成挪德卡菜,需要特殊处理 + if (country === "挪德卡莱") { + country = "挪德卡"; + } + await genshin.returnMainUi(); await sleep(1000); log.info("使用冒险之证寻找地脉花"); @@ -30,7 +35,7 @@ this.findLeyLineOutcropByBook = async function (country, type) { captureRegion1.dispose(); for (let i = 0; i < resList.count; i++) { let res = resList[i]; - if (res.text.includes(country) || res.text.includes("挪德")) { + if (res.text.includes(country)) { res.click(); } } diff --git a/repo/js/AutoLeyLineOutcrop/utils/processLeyLineOutcrop.js b/repo/js/AutoLeyLineOutcrop/utils/processLeyLineOutcrop.js index f351445f0..81abc3e24 100644 --- a/repo/js/AutoLeyLineOutcrop/utils/processLeyLineOutcrop.js +++ b/repo/js/AutoLeyLineOutcrop/utils/processLeyLineOutcrop.js @@ -16,6 +16,7 @@ async function (timeout, targetPath, retries = 0) { if (retries >= MAX_RETRIES) { const errorMsg = `开启地脉花失败,已重试${MAX_RETRIES}次,终止处理`; log.error("我辣么大一个地脉花哪去了?"); + await ensureExitRewardPage(); throw new Error(errorMsg); } @@ -51,6 +52,7 @@ async function (timeout, targetPath, retries = 0) { // 执行战斗 const fightResult = await autoFight(timeout); if (!fightResult) { + await ensureExitRewardPage(); throw new Error("战斗失败"); } @@ -62,6 +64,7 @@ async function (timeout, targetPath, retries = 0) { // 保留原始错误信息 const errorMsg = `地脉花处理失败 (重试${retries}/${MAX_RETRIES}): ${error.message}`; log.error(errorMsg); + await ensureExitRewardPage(); throw error; } finally { // 确保资源释放