// 掉落检测状态 // detectedExpOrMora:上一轮是否识别到经验/摩拉图标(用于判断“连续两轮都没有掉落则停”) // noExpOrMoraCount:连续“没有识别到掉落”的计数器 let detectedExpOrMora = true; let noExpOrMoraCount = 0; let running = true; let fighting = false; let consecutiveMaxRetryCount = 0; const warnedEnemyTypes = new Set(); // 默认突发任务 OCR 关键词(敌人配置未提供时使用) const DEFAULT_OCR_KEYWORDS = ["突发", "任务", "打倒", "消灭", "敌人", "所有"]; // 各敌人类型的参数配置 // - ocrKeywords:用于 detectTaskTrigger / waitForBattleResult 的 OCR 关键字 // - preparePath:准备路径(通常是传送点->触发区域附近) // - postBattlePath:战斗后补充路径(如拾取/对话) // - failReturnPath:失败后的回退路径(通常回到准备点/触发点) // - failReturnSleepMs:失败回退后额外等待(给加载/稳定留时间) // - initialDelayMs:首轮额外等待(给地图/加载/触发留时间) const ENEMY_CONFIG = { "愚人众": { ocrKeywords: ["买卖", "不成", "正义存", "愚人众", "禁止", "危险", "运输", "打倒", "盗宝团", "丘丘人", "今晚", "伙食", "所有人"], preparePath: "愚人众-准备", failReturnPath: "愚人众-准备", }, "盗宝团": { ocrKeywords: ["岛上", "无贼", "消灭", "鬼鬼祟祟", "盗宝团"], }, "鳄鱼": { ocrKeywords: ["张牙", "舞爪", "恶党", "鳄鱼", "打倒", "所有", "鳄鱼"], preparePath: "鳄鱼-准备", failReturnPath: "鳄鱼-准备", failReturnSleepMs: 5000, initialDelayMs: 5000, postBattlePath: "鳄鱼-拾取", }, "蕈兽": { ocrKeywords: ["实验家", "变成", "实验品", "击败", "所有", "魔物"], preparePath: "蕈兽-准备", postBattlePath: "蕈兽-对话", }, "雷萤术士": { ocrKeywords: ["雷萤", "术士", "圆滚滚", "不可食用", "威撼", "攀岩", "消灭", "准备", "打倒", "所有", "魔物", "盗宝团", "击败", "成员", "盗亦无道"], preparePath: "雷萤术士-准备", }, }; // 经验/摩拉模板匹配资源 // 这里保留 Mat 引用,便于脚本结束时主动释放,降低长时间运行的资源占用风险 let expMat = file.ReadImageMatSync("assets/exp.png"); const expRo = RecognitionObject.TemplateMatch(expMat, 74, 341, 207 - 74, 803 - 341); expRo.Threshold = 0.85; expRo.Use3Channels = true; expRo.InitTemplate(); let moraMat = file.ReadImageMatSync("assets/mora.png"); const moraRo = RecognitionObject.TemplateMatch(moraMat, 74, 341, 207 - 74, 803 - 341); moraRo.Threshold = 0.85; moraRo.Use3Channels = true; moraRo.InitTemplate(); const GAME_REGION_CACHE_SIZE = 3; // 游戏区域截图缓存大小上限 const gameRegionManager = { cache: [], // 缓存队列,保存近GAME_REGION_CACHE_SIZE张截图 lastCapture: new Date(), isDisposing: false, isCapturing: false }; let runTimes = parseNumericSetting(settings.runTimes, 10); let enemyType = settings.enemyType || "盗宝团"; const ocrTimeout = parseNumericSetting(settings.ocrTimeout, 10); const fightTimeout = parseNumericSetting(settings.fightTimeout, 120); const NO_TASK_MISS_CONFIRMATION_MS = 5000; const BATTLE_POINT_CONFIRMATION_MS = 5000; (async function () { const startTime = Date.now(); await switchPartyIfNeeded(settings.partyName); log.info(`当前选择的敌人类型: ${enemyType}`); log.info(`${enemyType}好感开始...`); if (settings.disablePickup) { log.info("已 禁用 自动拾取任务"); } else { dispatcher.addTimer(new RealtimeTimer("AutoPick")); log.info("已 启用 自动拾取任务"); } //准备 try { // 清理丘丘人(仅盗宝团需要) const qiuQiuRenTimeout = parseNumericSetting(settings.qiuQiuRen, 0); if (qiuQiuRenTimeout > 0 && enemyType === "盗宝团") { log.info("清理原住民..."); await AutoPath("盗宝团-准备"); log.info("开始清理战斗,超时时间: {0}秒...", qiuQiuRenTimeout); const clearCts = new CancellationTokenSource(); try { if (settings.disableAsyncFight) { fighting = true; await dispatcher.runTask(new SoloTask("AutoFight")); fighting = false; } else { const clearTask = dispatcher.runTask(new SoloTask("AutoFight"), clearCts); fighting = true; const maxLoops = Math.ceil(4 * qiuQiuRenTimeout); const timeoutTask = (async () => { for (let i = 0; i < maxLoops; i++) { try { await sleep(1) } catch (e) { break; } if (!fighting) break; await sleep(250); } if (fighting) { try { clearCts.cancel(); } catch { } } })(); await clearTask; fighting = false; await Promise.allSettled([clearTask, timeoutTask]); } } catch (e) { log.warn(`清理战斗异常: ${e.message}`); } finally { fighting = false; try { clearCts.cancel(); } catch { } } } const { preparePath } = getEnemyConfig(enemyType); if (preparePath) { log.info(`导航到${enemyType}触发点...`); await AutoPath(preparePath); } } catch (error) { } //主循环 try { await AutoFriendshipDev(); log.info(`${enemyType}好感运行总时长:${LogTimeTaken(startTime)}`); } catch (error) { log.error(`主循环中出现错误 ${error.message}`) } })(); /** * 获取地图坐标,失败时返回 null * @returns {{x:number,y:number}|null} */ function safeGetPositionFromMap() { try { return genshin.getPositionFromMap(); } catch (error) { return null; } } /** * 获取敌人配置;未知 enemyType 仅提示一次并回退为空配置。 * @param {string} enemyType * @returns {object} */ function getEnemyConfig(enemyType) { // 根据敌人类型返回配置对象(不存在则返回空对象,调用方负责兜底) const cfg = ENEMY_CONFIG[enemyType]; if (!cfg && !warnedEnemyTypes.has(enemyType)) { warnedEnemyTypes.add(enemyType); log.warn(`未知 enemyType: ${enemyType},将使用默认配置`); } return cfg || {}; } /** * 战后附加流程:按敌人配置跑 postBattlePath;蕈兽包含对话交互。 * @returns {Promise} */ async function runPostBattle() { // 战斗后处理:按敌人配置执行附加路径;部分敌人需要对话交互 const { postBattlePath } = getEnemyConfig(enemyType); if (postBattlePath) { await AutoPath(postBattlePath); } if (enemyType === "蕈兽") { await sleep(50); keyPress("F"); await sleep(50); keyPress("F"); await sleep(500); await genshin.chooseTalkOption("下次"); await sleep(500); } } /** * 失败恢复:回七天神像并走回退路径。 * @returns {Promise} */ async function recoverAfterFailure() { //失败统一返回七天神像 await genshin.tpToStatueOfTheSeven(); const { failReturnPath, failReturnSleepMs, preparePath } = getEnemyConfig(enemyType); if (failReturnPath) { await AutoPath(failReturnPath); } else if (preparePath) { await AutoPath(preparePath); } else { await AutoPath(`${enemyType}-触发点`); } if (failReturnSleepMs) { await sleep(failReturnSleepMs); } } /** * 执行路径文件(AutoPath/*.json);失败返回 false,取消错误透传。 * @param {string} locationName * @returns {Promise} */ async function AutoPath(locationName) { // 统一包装路径执行:避免 runFile 抛错导致整个脚本中断 try { const filePath = `assets/AutoPath/${locationName}.json`; await pathingScript.runFile(filePath); return true; } catch (error) { log.error(`执行 ${locationName} 路径时发生错误: ${error.message}`); return false; } } /** * 计算从 startTime 到现在的耗时字符串。 * @param {number} startTimeParam * @returns {string} */ function LogTimeTaken(startTimeParam) { const currentTime = Date.now(); const totalTimeInSeconds = (currentTime - startTimeParam) / 1000; const minutes = Math.floor(totalTimeInSeconds / 60); const seconds = totalTimeInSeconds % 60; return `${minutes} 分 ${seconds.toFixed(0).padStart(2, '0')} 秒`; } /** * 估算完成时间(按平均单轮耗时线性推算)。 * @param {number} startTime * @param {number} current * @param {number} total * @returns {string} */ function CalculateEstimatedCompletion(startTime, current, total) { if (current === 0) return "计算中..."; const elapsedTime = Date.now() - startTime; const timePerTask = elapsedTime / current; const remainingTasks = total - current; const remainingTime = timePerTask * remainingTasks; const completionDate = new Date(Date.now() + remainingTime); return `${completionDate.toLocaleTimeString()} (约 ${Math.round(remainingTime / 60000)} 分钟)`; } /** * 导航到触发点 * @returns {Promise} */ async function navigateToTriggerPoint() { const triggerPoint = await getAutoPathEndCoords(`${enemyType}-触发点`, "触发点"); if (!triggerPoint) { log.warn(`未配置 ${enemyType} 的 triggerPoint,跳过触发点距离校验`); return; } let retryCount = 0; const maxRetries = 3; while (retryCount < maxRetries) { try { await sleep(1) } catch (e) { break; } if (!running) { break; } const pos = safeGetPositionFromMap(); if (pos) { const distance = getDistanceBetweenPositions(pos, triggerPoint); if (distance <= 8) { log.info(`已到达触发点附近,距离: ${distance.toFixed(2)}米`); return; } else { log.info(`未到达触发点,当前距离: ${distance.toFixed(2)}米,正在导航...`); } } await AutoPath(`${enemyType}-触发点`); retryCount++; } } /** * 读取 AutoPath 文件末端坐标。 * @param {string} locationName * @param {string} label * @returns {Promise<{x:number,y:number}|null>} */ async function getAutoPathEndCoords(locationName, label) { const path = `assets/AutoPath/${locationName}.json`; try { const content = await file.readText(path); const data = JSON.parse(content); if (data.positions && Array.isArray(data.positions) && data.positions.length > 0) { const lastPosition = data.positions[data.positions.length - 1]; const x = Number(lastPosition.x); const y = Number(lastPosition.y); if (Number.isFinite(x) && Number.isFinite(y)) { return { x, y }; } } } catch (error) { log.warn(`读取${label}配置失败: ${error.message}`); } return null; } /** * 计算两个坐标点之间的距离。 * @param {{x:number,y:number}} posA * @param {{x:number,y:number}} posB * @returns {number} */ function getDistanceBetweenPositions(posA, posB) { const dx = Number(posA.x) - Number(posB.x); const dy = Number(posA.y) - Number(posB.y); return Math.sqrt(dx * dx + dy * dy); } /** * 读取战斗点坐标。 * @returns {Promise<{x:number,y:number}|null>} */ async function getBattlePointCoords() { return getAutoPathEndCoords(`${enemyType}-战斗点`, "战斗点"); } /** * 判断当前位置是否仍在战斗点附近。 * @param {number} [distanceThreshold=25] * @returns {Promise} */ async function isNearBattlePoint(distanceThreshold = 25) { const battlePointCoords = await getBattlePointCoords(); if (!battlePointCoords) { return false; } const pos = safeGetPositionFromMap(); if (!pos || typeof pos !== "object") { return false; } const dist = getDistanceBetweenPositions(pos, battlePointCoords); return Number.isFinite(dist) && dist <= distanceThreshold; } /** * 连续确认当前位置仍在战斗点附近。 * @param {number} [durationMs=5000] * @param {number} [distanceThreshold=25] * @returns {Promise} */ async function confirmNearBattlePoint(durationMs = BATTLE_POINT_CONFIRMATION_MS, distanceThreshold = 25) { const pollIntervalMs = 500; const startTime = Date.now(); while (Date.now() - startTime < durationMs) { if (!await isNearBattlePoint(distanceThreshold)) { return false; } try { await sleep(pollIntervalMs); } catch (e) { return false; } } return true; } /** * 导航到战斗点 * @returns {Promise} */ async function navigateToBattlePoint() { const battlePoint = await getBattlePointCoords(); if (!battlePoint) { log.warn(`未配置 ${enemyType} 的 battlePoint,跳过战斗点距离校验`); return; } let retryCount = 0; const maxRetries = 3; while (retryCount < maxRetries) { try { await sleep(1) } catch (e) { break; } if (!running) { break; } const pos = safeGetPositionFromMap(); if (pos) { const distance = Math.sqrt(Math.pow(pos.x - battlePoint.x, 2) + Math.pow(pos.y - battlePoint.y, 2)); if (distance <= 8) { log.info(`已到达战斗点附近,距离: ${distance.toFixed(2)}米`); return; } else { log.info(`未到达战斗点,当前距离: ${distance.toFixed(2)}米,正在导航...`); } } await AutoPath(`${enemyType}-战斗点`); retryCount++; } } // OCR检测突发任务 /** * OCR 检测是否触发突发任务。 * @returns {Promise} */ async function detectTaskTrigger() { // 修复点:OCR 的截图对象与 findMulti 返回对象可能持有底层资源 // 必须在 finally 中释放,避免长时间循环导致资源累积 const ocrKeywords = getOcrKeywords(enemyType); let ocrStatus = false; let ocrStartTime = Date.now(); while (Date.now() - ocrStartTime < ocrTimeout * 1000 && !ocrStatus) { try { await sleep(1) } catch (e) { break; } if (!running) { break; } let resList = null; try { resList = (await getGameRegion()).findMulti(RecognitionObject.ocr(0, 200, 300, 300)); for (let o = 0; o < resList.count; o++) { let res = resList[o]; for (let keyword of ocrKeywords) { if (res && res.text && String(res.text).includes(keyword)) { ocrStatus = true; log.info("检测到突发任务触发"); break; } } if (ocrStatus) break; } } catch (error) { log.error(`OCR检测突发任务过程中出错: ${error && error.message ? error.message : error}`); } if (!ocrStatus) { await sleep(1000); } } return ocrStatus; } /** * 执行 AutoFight 战斗任务。 * @returns {Promise<{status:string,errorMessage?:string}>} * * @description * 使用全局变量:settings.disableAsyncFight, fightTimeout, enemyType * * 支持两种战斗模式: * - 同步模式 (disableAsyncFight=true): 直接执行 AutoFight,依赖配置组的"战斗结束检测"自行退出 * - 异步模式 (默认): 启动战斗任务后等待 OCR 检测结果,检测到结果后取消战斗 * * 错误处理:捕获所有异常并返回错误信息,不向上抛出 * 清理工作:无论成功失败,最终都会释放鼠标左键 */ async function executeBattleTasks() { log.info("开始战斗!"); const cts = new CancellationTokenSource(); try { if (settings.disableAsyncFight) { // 同步模式:直接执行战斗,依赖配置组的"战斗结束检测"自行退出 await dispatcher.runTask(new SoloTask("AutoFight")); return { status: "success" }; } else { // 异步模式:并发启动战斗 + OCR 检测结果 const fightTask = dispatcher.runTask(new SoloTask("AutoFight"), cts); fighting = true; const statusPromise = waitForBattleResult(cts); await fightTask; fighting = false; const results = await Promise.allSettled([fightTask, statusPromise]); const status = results[1].value; return { status }; } } catch (error) { const msg = error && error.message ? String(error.message) : ""; // 特别处理:如果是"取消自动任务"错误,视为成功 if (msg.includes("取消自动任务") || msg.includes("A task was canceled")) { return { status: "success" }; } log.error(`战斗执行过程中出错: ${msg}`); return { status: "error", errorMessage: msg }; } finally { try { cts.cancel(); } catch { } keyUp("VK_LBUTTON"); } } /** * OCR 轮询战斗结果:success/failure/out_of_area/cancelled;超时抛 BATTLE_TIMEOUT。 * @param {*} cts - CancellationTokenSource,用于取消任务 * @returns {Promise<"success"|"failure"|"out_of_area"|"cancelled">} * * @description * 使用全局变量:fightTimeout, enemyType */ async function waitForBattleResult(cts = null) { const timeout = fightTimeout * 1000; // 战斗结果 OCR 判定: // - 返回 "success":识别到成功关键字/特殊条件 // - 返回 "failure":识别到失败关键字 // - 返回 "out_of_area":连续多次识别不到事件关键字,认为离开触发区域 // - 超时:抛出 Error("战斗超时,未检测到结果") // // 失败后的恢复统一在 executeSingleFriendshipRound 中串行处理,避免路径并发冲突 const fightStartTime = Date.now(); const successKeywords = ["事件", "完成"]; const failureKeywords = ["失败"]; const eventKeywords = getOcrKeywords(enemyType); const pollIntervalMs = 500; let notFindStartTime = 0; while (Date.now() - fightStartTime < timeout) { try { await sleep(1) } catch (e) { break; } if (!running) { break; } if (!fighting) { break; } let result = null; let result2 = null; try { // 沿用最初版写死的 OCR 框(1080p 下的“事件完成”识别区域) result = (await getGameRegion()).find(RecognitionObject.ocr(850, 150, 200, 80)); result2 = (await getGameRegion()).find(RecognitionObject.ocr(0, 200, 300, 300)); let text = result && result.text ? String(result.text) : ""; text = text ? text.replace(/\s+/g, "") : ""; let text2 = result2 && result2.text ? String(result2.text) : ""; text2 = text2 ? text2.replace(/\s+/g, "") : ""; if (enemyType === "蕈兽" && text2.includes("维沙瓦")) { log.info("战斗结果:成功"); try { cts.cancel(); } catch { } // 取消任务 return "success"; } // 检查成功关键词:只要开战后识别到“事件/完成”等关键词即可认为本轮结束 if (Date.now() - fightStartTime >= 2000) { for (let keyword of successKeywords) { if (text.includes(keyword)) { log.info("检测到战斗成功关键词: {0}", keyword); log.info("战斗结果:成功"); try { cts.cancel(); } catch { } // 取消任务 return "success"; } } } // 检查失败关键词 for (let keyword of failureKeywords) { if (text.includes(keyword)) { log.warn("检测到战斗失败关键词: {0}", keyword); try { cts.cancel(); } catch { } // 取消任务 return "failure"; } } if (enemyType !== "蕈兽") { // 检查事件关键词 let find = 0; for (let keyword of eventKeywords) { if (text2.includes(keyword)) { find++; } } if (find === 0) { if (notFindStartTime === 0) { notFindStartTime = Date.now(); } const notFindDuration = Date.now() - notFindStartTime; log.info("未检测到任务触发关键词:已持续 {0} 毫秒", notFindDuration); } else { notFindStartTime = 0; } if (notFindStartTime > 0 && Date.now() - notFindStartTime >= NO_TASK_MISS_CONFIRMATION_MS) { if (await isNearBattlePoint()) { log.info("触发关键词消失但仍在战斗点附近,视为本轮结束"); try { cts.cancel(); } catch { } // 取消任务 return "success"; } log.warn("不在任务触发区域,战斗失败"); try { cts.cancel(); } catch { } // 取消任务 return "out_of_area"; } } } catch (error) { log.error("OCR过程中出错", error); } // 统一的检查间隔 await sleep(pollIntervalMs); } log.warn("在超时时间内未检测到战斗结果"); try { cts.cancel(); } catch { } // 取消任务 } // 执行单次好感任务循环 /** * 执行单轮好感流程(触发检测→导航→战斗→判定→战后/恢复)。 * @param {number} roundIndex * @returns {Promise} 返回 false 表示整体应提前结束 */ async function executeSingleFriendshipRound(roundIndex) { // 单轮流程: // 1) 导航到触发点附近 // 2) 通过 OCR 判断是否已触发突发任务 // 3) 导航到战斗点(避免与失败回退路径并发冲突) // 4) 启动战斗 + OCR 判定胜负/是否离开区域 // 5) 成功则执行战斗后流程;失败则回退到准备/触发点并进入下一轮 // 导航到触发点 await navigateToTriggerPoint(); //根据经验/摩拉决定是否需要终止任务 if (!detectedExpOrMora && settings.loopTillNoExpOrMora) { noExpOrMoraCount++; log.warn("上次运行未检测到经验或摩拉"); if (noExpOrMoraCount >= 2) { log.warn("连续两次循环没有经验或摩拉掉落,提前终止"); return false; } } else { noExpOrMoraCount = 0; detectedExpOrMora = false; } //检测任务是否触发 const { initialDelayMs } = getEnemyConfig(enemyType); if (roundIndex === 0 && initialDelayMs) { await sleep(initialDelayMs); } let ocrStatus; if (roundIndex === 0) { ocrStatus = await detectTaskTrigger(); } if (!ocrStatus) { if (settings.use1000Stars) { await genshin.wonderlandCycle(); } else { await genshin.relogin(); } ocrStatus = await detectTaskTrigger(); } if (!ocrStatus) { if (await confirmNearBattlePoint()) { log.info("未识别到突发任务,但当前位置已连续 5 秒仍在战斗点附近,视为本轮成功结束"); await runPostBattle(); return true; } // 本轮未检测到突发任务:按设计直接结束整个脚本循环 notification.send(`未识别到突发任务,${enemyType}好感结束`); log.info(`未识别到突发任务,${enemyType}好感结束`); return false; // 返回 false 表示需要终止循环 } //导航至战斗点 await navigateToBattlePoint(); const maxRetryCount = 2; let retryCount = 0; while (true) { try { await sleep(1) } catch (e) { break; } if (!running) { break; } const battleResult = await executeBattleTasks(); if (battleResult.status === "success") { consecutiveMaxRetryCount = 0; // 重置连续最大重试计数器 await runPostBattle(); return true; } // 战斗失败,执行恢复 log.warn(`战斗失败,状态: ${battleResult.status},错误信息: ${battleResult.errorMessage || '无'}`); if (retryCount >= maxRetryCount) { consecutiveMaxRetryCount++; log.warn(`已尝试恢复 ${maxRetryCount} 次,第 ${consecutiveMaxRetryCount} 次触发最大重试`); if (consecutiveMaxRetryCount >= 2) { log.error(`连续两次达到最大重试次数,终止任务`); notification.send(`${enemyType}好感任务失败,连续两次达到最大重试次数`); return false; } log.info("尝试容错处理:传送至七天神像并切换队伍"); await genshin.teleportToStatue(); await switchPartyIfNeeded(settings.partyName); log.info("容错处理完成,进入下一轮"); return true; } await recoverAfterFailure(); retryCount++; log.info(`第 ${retryCount} 次恢复后,重新导航至战斗点...`); await navigateToBattlePoint(); log.info(`第 ${retryCount} 次恢复后,重新执行战斗...`); } } // 记录进度信息 /** * 输出当前进度与预计完成时间。 * @param {number} startTime * @param {number} currentRound * @param {number} totalRounds */ function logProgress(startTime, currentRound, totalRounds) { const estimatedCompletion = CalculateEstimatedCompletion(startTime, currentRound + 1, totalRounds); const currentTime = LogTimeTaken(startTime); log.info(`当前进度:${currentRound + 1}/${totalRounds} (${((currentRound + 1) / totalRounds * 100).toFixed(1)}%)`); log.info(`当前运行总时长:${currentTime}`); log.info(`预计完成时间:${estimatedCompletion}`); } // 执行 N 次好感任务并输出日志 - 重构后的主函数 /** * 主循环:执行指定次数;在未触发/连续无掉落等条件下提前退出。 * @returns {Promise} */ async function AutoFriendshipDev() { const startFirstTime = Date.now(); let detectExpOrMoraTask; let cancelled = false; let successCount = 0; let failureCount = 0; try { if (settings.loopTillNoExpOrMora) { detectExpOrMoraTask = detectExpOrMora(); } for (let i = 0; i < runTimes; i++) { try { await sleep(1); } catch (e) { break; } try { const success = await executeSingleFriendshipRound(i); if (!success) break; successCount++; logProgress(startFirstTime, i, runTimes); } catch (error) { continue; } } } finally { if (!cancelled) { log.info(`本次运行统计:成功 ${successCount} 次,失败 ${failureCount} 次`); } running = false; if (detectExpOrMoraTask) { try { await detectExpOrMoraTask; } catch { } } } } /** * 后台循环:模板匹配检测经验/摩拉掉落图标(用于提前终止循环)。 * @returns {Promise} */ async function detectExpOrMora() { // 后台循环:通过模板匹配检测经验/摩拉图标 // 注意:该循环依赖 running 停止;必须保证任何退出路径都会把 running=false while (running) { try { await sleep(1); } catch (e) { break; } if (!detectedExpOrMora) { let res1 = null; let res2 = null; try { res1 = (await getGameRegion()).find(expRo); if (res1.isExist()) { log.info("识别到经验"); detectedExpOrMora = true; continue; } res2 = (await getGameRegion()).find(moraRo); if (res2.isExist()) { log.info("识别到摩拉"); detectedExpOrMora = true; continue; } } catch (e) { log.error(`检测经验和摩拉掉落过程中出现错误 ${e.message}`); } } else { await sleep(200); } await sleep(200); } } /** * 解析数值类型配置项 * @param {*} value 配置原始值(可能是字符串、数字、undefined、null) * @param {number} defaultVal 默认值 * @returns {number} 若 value 为空字符串/undefined/null 或转换后为 NaN,返回 defaultVal;否则返回转换后的数值(包含 0) */ function parseNumericSetting(value, defaultVal) { if (value === undefined || value === null || value === '') return defaultVal; const n = Number(value); return isNaN(n) || n === 0 ? defaultVal : n; } /** * 获取 OCR 关键词(优先敌人配置,否则回退默认)。 * @param {string} enemyType * @returns {string[]} */ function getOcrKeywords(enemyType) { // OCR 关键词获取:优先使用敌人类型配置,否则回退到默认关键词 const { ocrKeywords } = getEnemyConfig(enemyType); return ocrKeywords || DEFAULT_OCR_KEYWORDS; } /** * 可选切换队伍;失败则尝试回七天神像后重试。 * @param {string} partyName * @returns {Promise} */ async function switchPartyIfNeeded(partyName) { // 可选队伍切换:为空则直接回到主界面,避免停留在菜单/对话等状态 if (!partyName) { await genshin.returnMainUi(); return; } try { log.info("正在尝试切换至" + partyName); if (!await genshin.switchParty(partyName)) { log.info("切换队伍失败,前往七天神像重试"); await genshin.tpToStatueOfTheSeven(); await genshin.switchParty(partyName); } } catch { log.error("队伍切换失败,可能处于联机模式或其他不可切换状态"); notification.error(`队伍切换失败,可能处于联机模式或其他不可切换状态`); await genshin.returnMainUi(); } } /** * 验证超时时间设置 * @param {number|string} value - 用户设置的超时时间(秒) * @param {number} defaultValue - 默认超时时间(秒) * @param {string} timeoutType - 超时类型名称 * @returns {number} - 验证后的超时时间(秒) */ function validateTimeoutSetting(value, defaultValue, timeoutType) { // 转换为数字 const timeout = Number(value); // 检查是否为有效数字且大于0 if (!isFinite(timeout) || timeout <= 0) { log.warn(`${timeoutType} 超时设置无效,必须是大于0的数字,将使用默认值 ${defaultValue} 秒`); return defaultValue; } log.info(`${timeoutType}超时设置为 ${timeout} 秒`); return timeout; } /** * 获取游戏区域截图,根据时间间隔决定是否重新捕获 * * @param {number} [minInterval=17] - 最小截图间隔(毫秒),默认17ms(约60fps) * @param {boolean} [asyncDispose=false] - 是否异步释放旧截图,默认false * @returns {Promise} 游戏区域截图对象 * * @description * 使用 gameRegionManager 对象管理以下属性: * - cache: 缓存队列,保存近5张截图 * - lastCapture: 上一次捕获游戏区域的时间戳 * - isDisposing: 标记是否正在释放旧截图,用于安全锁 * - isCapturing: 标记是否正在执行截图操作,用于全局锁 */ async function getGameRegion(minInterval = 17, asyncDispose = false) { async function disposeOldGameRegion() { gameRegionManager.isDisposing = true; try { // 当缓存队列超过GAME_REGION_CACHE_SIZE个时,销毁最旧的截图 while (gameRegionManager.cache.length > GAME_REGION_CACHE_SIZE) { const oldestRegion = gameRegionManager.cache.shift(); if (oldestRegion) { oldestRegion.dispose(); } } } catch (error) { log.error(`释放旧游戏区域截图失败: ${error.message}`); } finally { gameRegionManager.isDisposing = false; } } // 等待其他任务完成截图 while (gameRegionManager.isCapturing) { await sleep(1); } gameRegionManager.isCapturing = true; try { if (new Date() - gameRegionManager.lastCapture >= minInterval || gameRegionManager.cache.length === 0) { while (gameRegionManager.isDisposing) { await sleep(1); } gameRegionManager.lastCapture = new Date(); const newRegion = captureGameRegion(); gameRegionManager.cache.push(newRegion); // 根据参数决定是否等待释放完成 if (asyncDispose) { disposeOldGameRegion(); } else { await disposeOldGameRegion(); } } } catch (error) { log.error(`获取游戏区域截图失败: ${error.message}`); } finally { gameRegionManager.isCapturing = false; // 返回最新的截图 return gameRegionManager.cache[gameRegionManager.cache.length - 1]; } }