/** * 原神地脉花自动化脚本 (Genshin Impact Ley Line Outcrop Automation Script) * * 功能:自动寻找并完成地脉花挑战,领取奖励 * * 术语对照表: * 中文 - 英文: * 地脉之花 - Ley Line Outcrop * 地脉 - Ley Line * 启示之花 - Blossom of Revelation (蓝花,产出经验书) * 藏金之花 - Blossom of Wealth (黄花,产出摩拉) */ // 全局变量 let leyLineX = 0; // 地脉花X坐标 let leyLineY = 0; // 地脉花Y坐标 let currentFlower = null; // 当前花的引用 let strategyName = ""; // 任务策略名称 let retryCount = 0; // 重试次数 let marksStatus = true; // 自定义标记状态 let currentRunTimes = 0; // 当前运行次数 /** * 主函数 - 脚本入口点 */ (async function () { dispatcher.addTimer(new RealtimeTimer("AutoPick")); try { await runLeyLineOutcropScript(); } catch (error) { log.error("出错了! {error}", error.message); if (!marksStatus) { await openCustomMarks(); } } })(); /** * 运行地脉花脚本的主要逻辑 * @returns {Promise} */ async function runLeyLineOutcropScript() { // 初始化 await initializeGame(); // 加载配置和设置并校验 const config = await loadConfig(); const settings = loadSettings(); retryCount = 0; // 显示设置信息 logSettings(settings); // 开局准备 await prepareForLeyLineRun(settings); // 执行地脉花挑战 await runLeyLineChallenges(config, settings); // 完成后恢复自定义标记 if (!marksStatus) { await openCustomMarks(); } } /** * 初始化游戏状态 * @returns {Promise} */ async function initializeGame() { await genshin.returnMainUi(); setGameMetrics(1920, 1080, 1); } /** * 记录设置信息到日志 * @param {Object} settings - 用户设置对象 */ function logSettings(settings) { log.info(`地脉花类型:${settings.leyLineOutcropType}`); log.info(`国家:${settings.country}`); if (settings.friendshipTeam) { log.info(`好感队:${settings.friendshipTeam}`); } log.info(`刷取次数:${settings.timesValue}`); if (settings.reRun) { log.info("已开启可重跑模式,将选择可重跑路线"); } } /** * 执行地脉花挑战前的准备工作 * @param {Object} settings - 用户设置对象 * @returns {Promise} */ async function prepareForLeyLineRun(settings) { // 开局传送到七天神像 await genshin.tpToStatueOfTheSeven(); // 切换战斗队伍 if (settings.team) { log.info(`切换至队伍 ${settings.team}`); await genshin.switchParty(settings.team); } } /** * 执行地脉花挑战的主要逻辑 * @param {Object} config - 配置对象 * @param {Object} settings - 用户设置对象 * @returns {Promise} */ async function runLeyLineChallenges(config, settings) { while (currentRunTimes < settings.timesValue) { // 寻找地脉花位置 await findLeyLineOutcrop(settings.country, settings.leyLineOutcropType); // 查找并执行对应的策略 const foundStrategy = await executeMatchingStrategy(config, settings); // 未找到策略的错误处理 if (!foundStrategy) { handleNoStrategyFound(); return; } } } /** * 执行匹配的地脉花策略 * @param {Object} config - 配置对象 * @param {Object} settings - 用户设置对象 * @returns {Promise} 是否找到并执行了策略 */ async function executeMatchingStrategy(config, settings) { let foundStrategy = false; // 从配置中查找匹配的位置和策略 if (config.leyLinePositions[settings.country]) { const positions = config.leyLinePositions[settings.country]; for (const position of positions) { if (isNearPosition(leyLineX, leyLineY, position.x, position.y, config.errorThreshold)) { foundStrategy = true; strategyName = position.strategy; order = position.order; log.info(`找到匹配的地脉花策略:${strategyName},次序:${order}`); // 使用 LeyLineOutcropData.json 数据处理路径 await executePathsUsingNodeData(position, settings); break; } } } return foundStrategy; } /** * 使用节点数据执行路径 * @param {Object} position - 位置对象 * @param {Object} settings - 用户设置对象 * @returns {Promise} */ async function executePathsUsingNodeData(position, settings) { try { const nodeData = await loadNodeData(); let currentNodePosition = position; const targetNode = findTargetNodeByPosition(nodeData, currentNodePosition.x, currentNodePosition.y); if (!targetNode) { log.error(`未找到与坐标(${currentNodePosition.x}, ${currentNodePosition.y})匹配的目标节点`); return; } log.info(`找到目标节点: ID ${targetNode.id}, 位置(${targetNode.position.x}, ${targetNode.position.y})`); const paths = findPathsToTarget(nodeData, targetNode); if (paths.length === 0) { log.error(`未找到通向目标节点(ID: ${targetNode.id})的路径`); return; } // 选择最短的路径执行 const optimalPath = selectOptimalPath(paths); log.info(`选择了含有 ${optimalPath.routes.length} 个路径点的最优路径`); // 执行路径 await executePath(optimalPath, settings); currentRunTimes++; // 如果达到刷取次数上限,退出循环 if (currentRunTimes >= settings.timesValue) { return; } // 循环检查并执行当前节点的单一next路径,直到遇到没有next或有多个next的情况 let currentNode = targetNode; while (currentNode.next && currentRunTimes < settings.timesValue) { if(currentNode.next.length === 1){ // 获取下一个节点的ID 和 路径,并在节点数据中找到下一个节点 const nextNodeId = currentNode.next[0].target; const nextRoute = currentNode.next[0].route; const nextNode = nodeData.node.find(node => node.id === nextNodeId); if (!nextNode) { return; } // 创建一个适合executePath函数的路径对象 const pathObject = { startNode: currentNode, targetNode: nextNode, routes: [nextRoute] }; log.info(`直接执行下一个节点路径: ${nextRoute}`); await executePath(pathObject, settings); currentRunTimes++; log.info(`完成节点 ID ${nextNodeId}, 已执行 ${currentRunTimes}/${settings.timesValue} 次`); // 更新当前节点为下一个节点,继续检查 currentNode = nextNode; currentNodePosition = { x: nextNode.position.x, y: nextNode.position.y }; } else if(currentNode.next.length > 1){ // 如果存在分支路线,先打开大地图判断下一个地脉花的位置,根据下一个地脉花的位置选择路线 log.info("检测到多个分支路线,开始查找下一个地脉花位置"); // 备份当前地脉花坐标 const currentLeyLineX = leyLineX; const currentLeyLineY = leyLineY; // 打开大地图 await genshin.returnMainUi(); keyPress("M"); await sleep(1000); // 查找下一个地脉花 const found = await locateLeyLineOutcrop(settings.leyLineOutcropType); await genshin.returnMainUi(); if (!found) { log.warn("无法在分支点找到下一个地脉花,退出本次循环"); return; } log.info(`找到下一个地脉花,位置: (${leyLineX}, ${leyLineY})`); // 计算每个分支节点到地脉花的距离,选择最近的路径 let closestRoute = null; let closestDistance = Infinity; let closestNodeId = null; for (const nextRoute of currentNode.next) { const nextNodeId = nextRoute.target; const nextNode = nodeData.node.find(node => node.id === nextNodeId); if (!nextNode) continue; const distance = calculate2DDistance( leyLineX, leyLineY, nextNode.position.x, nextNode.position.y ); log.info(`路线到地脉花距离: ID ${nextNodeId}, 距离: ${distance.toFixed(2)}`); if (distance < closestDistance) { closestDistance = distance; closestRoute = nextRoute.route; closestNodeId = nextNodeId; } } if (!closestRoute) { log.error("无法找到合适的路线,终止执行"); // 恢复原始坐标 leyLineX = currentLeyLineX; leyLineY = currentLeyLineY; return; } const nextNode = nodeData.node.find(node => node.id === closestNodeId); log.info(`选择最近的路线: ${closestRoute}, 目标节点ID: ${closestNodeId}。`); // 创建路径对象并执行 const pathObject = { startNode: currentNode, targetNode: nextNode, routes: [closestRoute] }; await executePath(pathObject, settings); currentRunTimes++; // 更新当前节点为下一个节点,继续检查 currentNode = nextNode; currentNodePosition = { x: nextNode.position.x, y: nextNode.position.y }; } else{ log.info("当前路线完成,退出循环"); break; } } } catch (error) { log.error(`执行路径时出错: ${error.message}`); throw error; } } /** * 加载节点数据 * @returns {Promise} 节点数据对象 */ async function loadNodeData() { try { const nodeDataText = await file.readText("LeyLineOutcropData.json"); return JSON.parse(nodeDataText); } catch (error) { log.error(`加载节点数据失败: ${error.message}`); throw new Error("无法加载 LeyLineOutcropData.json 文件"); } } /** * 根据位置找到对应的目标节点 * @param {Object} nodeData - 节点数据 * @param {number} x - 目标X坐标 * @param {number} y - 目标Y坐标 * @returns {Object|null} 找到的节点或null */ function findTargetNodeByPosition(nodeData, x, y) { const errorThreshold = 50; // 坐标匹配误差范围 for (const node of nodeData.node) { if (node.type === "blossom" && Math.abs(node.position.x - x) <= errorThreshold && Math.abs(node.position.y - y) <= errorThreshold) { return node; } } return null; } /** * 查找到达目标节点的所有可能路径 * @param {Object} nodeData - 节点数据 * @param {Object} targetNode - 目标节点 * @returns {Array} 可行路径数组 */ function findPathsToTarget(nodeData, targetNode) { // 构建节点映射表 const nodeMap = {}; nodeData.node.forEach(node => { nodeMap[node.id] = node; }); log.info(`目标节点ID: ${targetNode.id}, 类型: ${targetNode.type}, 区域: ${targetNode.region}`); // 采用广度优先搜索查找所有可能路径 return breadthFirstPathSearch(nodeData, targetNode, nodeMap); } /** * 使用广度优先搜索算法查找从传送点到目标的所有路径 * @param {Object} nodeData - 节点数据 * @param {Object} targetNode - 目标节点 * @param {Object} nodeMap - 节点映射 * @returns {Array} 找到的所有可行路径 */ function breadthFirstPathSearch(nodeData, targetNode, nodeMap) { // 存储找到的所有有效路径 const validPaths = []; // 获取所有传送点作为起点 const teleportNodes = nodeData.node.filter(node => node.type === "teleport"); log.info(`找到 ${teleportNodes.length} 个传送点作为可能的起点`); // 对每个传送点,尝试查找到目标的路径 for (const startNode of teleportNodes) { // 初始化队列,每个元素包含 [当前节点, 路径信息] const queue = [[startNode, { startNode: startNode, routes: [], visitedNodes: new Set([startNode.id]) }]]; // 广度优先搜索 while (queue.length > 0) { const [currentNode, pathInfo] = queue.shift(); // 如果已经到达目标节点 if (currentNode.id === targetNode.id) { validPaths.push({ startNode: pathInfo.startNode, targetNode: targetNode, routes: [...pathInfo.routes] }); continue; // 找到一条路径,继续搜索其他可能路径 } // 检查当前节点的下一个连接 if (currentNode.next && currentNode.next.length > 0) { for (const nextRoute of currentNode.next) { const nextNodeId = nextRoute.target; // 避免循环 if (pathInfo.visitedNodes.has(nextNodeId)) { continue; } const nextNode = nodeMap[nextNodeId]; if (!nextNode) { continue; } // 创建新的路径信息 const newPathInfo = { startNode: pathInfo.startNode, routes: [...pathInfo.routes, nextRoute.route], visitedNodes: new Set([...pathInfo.visitedNodes, nextNodeId]) }; // 加入队列 queue.push([nextNode, newPathInfo]); } } } } // 检查是否存在反向路径 const reversePaths = findReversePathsIfNeeded(nodeData, targetNode, nodeMap, validPaths); validPaths.push(...reversePaths); log.info(`共找到 ${validPaths.length} 条有效路径`); return validPaths; } /** * 如果需要,尝试查找反向路径(从目标节点的前置节点到传送点再到目标) * @param {Object} nodeData - 节点数据 * @param {Object} targetNode - 目标节点 * @param {Object} nodeMap - 节点映射 * @param {Array} existingPaths - 已找到的路径 * @returns {Array} 找到的反向路径 */ function findReversePathsIfNeeded(nodeData, targetNode, nodeMap, existingPaths) { // 如果已经找到路径,或者目标节点没有前置节点,则不需要查找反向路径 if (existingPaths.length > 0 || !targetNode.prev || targetNode.prev.length === 0) { return []; } const reversePaths = []; // 检查每个前置节点 for (const prevNodeId of targetNode.prev) { const prevNode = nodeMap[prevNodeId]; if (!prevNode) continue; // 找到从前置节点到传送点的路径 const pathsToPrevNode = []; // 获取所有能从这个前置节点到达的传送点 const teleportNodes = nodeData.node.filter(node => node.type === "teleport" && node.next.some(route => route.target === prevNode.id) ); for (const teleportNode of teleportNodes) { // 寻找传送点到前置节点的路径 const route = teleportNode.next.find(r => r.target === prevNode.id); if (route) { // 找到路径从前置节点到目标 const nextRoute = prevNode.next.find(r => r.target === targetNode.id); if (nextRoute) { reversePaths.push({ startNode: teleportNode, targetNode: targetNode, routes: [route.route, nextRoute.route] }); } } } } return reversePaths; } /** * 从多个可行路径中选择最优的一条 * @param {Array} paths - 路径数组 * @returns {Object} 最优路径 */ function selectOptimalPath(paths) { if (!paths || paths.length === 0) { throw new Error("没有可用路径"); } // 按路径段数从少到多排序 paths.sort((a, b) => a.routes.length - b.routes.length); // 记录路径选择日志 for (let i = 0; i < Math.min(paths.length, 3); i++) { log.info(`路径选项 ${i+1}: 起点ID ${paths[i].startNode.id}, ${paths[i].routes.length} 段路径`); for (let j = 0; j < paths[i].routes.length; j++) { log.info(` - 路径 ${j+1}: ${paths[i].routes[j]}`); } } return paths[0]; // 返回路径段最少的路径 } /** * 执行路径 * @param {Object} path - 路径对象 * @param {Object} settings - 用户设置对象 * @returns {Promise} */ async function executePath(path, settings) { log.info(`开始执行路径,起始点ID: ${path.startNode.id}, 目标点ID: ${path.targetNode.id}`); log.info(`路径包含 ${path.routes.length} 个路径段`); // 依次执行每个路径段 for (let i = 0; i < path.routes.length; i++) { const routePath = path.routes[i]; log.info(`执行路径 ${i+1}/${path.routes.length}: ${routePath}`); try { // 运行路径文件 await pathingScript.runFile(routePath); } catch (error) { log.error(`执行路径段 ${i+1} 时出错: ${error.message}`); throw error; } } const routePath = path.routes[path.routes.length - 1]; const targetPath = routePath.replace('assets/pathing/', 'assets/pathing/target/').replace('-rerun', ''); // 处理地脉花 log.info(`处理地脉花: ${targetPath}`); await processLeyLineOutcrop(settings.timeout, targetPath); await switchToFriendshipTeamIfNeeded(settings); await attemptReward(settings); } /** * 如果需要,切换到好感队 * @param {Object} settings - 用户设置对象 * @returns {Promise} */ async function switchToFriendshipTeamIfNeeded(settings) { if (!settings.friendshipTeam) { return; } log.info(`切换至队伍 ${settings.friendshipTeam}`); try { await genshin.switchParty(settings.friendshipTeam); } catch (error) { // 切换失败时的恢复策略 keyPress("ESCAPE"); await sleep(500); keyPress("ESCAPE"); await sleep(500); await genshin.returnMainUi(); log.info(`重新切换至队伍 ${settings.friendshipTeam}`); await genshin.switchParty(settings.friendshipTeam); } } /** * 处理未找到策略的情况 */ function handleNoStrategyFound() { log.error("未找到对应的地脉花策略,请再次运行脚本"); log.error("如果仍然不行,请截图{1}游戏界面,并反馈给作者!", "*完整的*"); log.error("完整的游戏界面!完整的游戏界面!完整的游戏界面!"); } /** * 配置相关函数 */ /** * 加载配置文件 * @returns {Object} 配置对象 */ async function loadConfig() { try { const configData = JSON.parse(await file.readText("config.json")); return configData; } catch (error) { log.error(`加载配置文件失败: ${error.message}`); throw new Error("配置文件加载失败,请检查config.json文件是否存在"); } } /** * 加载并验证用户设置 * @returns {Object} 处理过的设置对象 */ function loadSettings() { try { // 从全局settings加载用户设置 const settingsData = { start: settings.start, leyLineOutcropType: settings.leyLineOutcropType, country: settings.country, team: settings.team, reRun: settings.reRun, friendshipTeam: settings.friendshipTeam, timeout: settings.timeout * 1000 ? settings.timeout * 1000 : 120000, count: settings.count ? settings.count : "6" }; // 验证必要的设置 if (!settingsData.start) { throw new Error("请仔细阅读脚本介绍,并在调度器内进行配置,如果你是直接运行的脚本,请将脚本加入调度器内运行!"); } if (!settingsData.leyLineOutcropType) { throw new Error("请在游戏中确认地脉花的类型,然后在js设置中选择地脉花的类型。"); } if (!settingsData.country) { throw new Error("请在游戏中确认地脉花的第一个点的位置,然后在js设置中选择地脉花所在的国家。"); } if (settingsData.friendshipTeam && !settingsData.team) { throw new Error("未配置战斗队伍!当配置了好感队时必须配置战斗队伍!"); } // 处理刷取次数 if (!/^-?\d+\.?\d*$/.test(settingsData.count)) { log.warn(`刷取次数 ${settingsData.count} 不是数字,使用默认次数6次`); settingsData.timesValue = 6; } else { // 转换为数字 const num = parseFloat(settingsData.count); // 范围检查 if (num < 1) { settingsData.timesValue = 1; log.info(`⚠️ 次数 ${num} 小于1,已调整为1`); } else { // 处理小数 if (!Number.isInteger(num)) { settingsData.timesValue = Math.floor(num); log.info(`⚠️ 次数 ${num} 不是整数,已向下取整为 ${settingsData.timesValue}`); } else { settingsData.timesValue = num; } } } return settingsData; } catch (error) { log.error(`加载设置失败: ${error.message}`); throw error; } } /** * 地脉花寻找和定位相关函数 */ /** * 查找地脉花位置 * @param {string} country - 国家名称 * @param {string} type - 地脉花类型 * @returns {Promise} */ async function findLeyLineOutcrop(country, type) { const config = await loadConfig(); const maxRetries = 6; log.info("开始寻找地脉花"); if (!config.mapPositions[country] || config.mapPositions[country].length === 0) { throw new Error(`未找到国家 ${country} 的位置信息`); } const defaultPos = config.mapPositions[country][0]; await genshin.moveMapTo(defaultPos.x, defaultPos.y, country); await sleep(1000); // 移动完等地图稳定 currentFlower = null; for (let retryCount = 0; retryCount < maxRetries; retryCount++) { log.info(`第 ${retryCount + 1} 次尝试定位地脉花`); const found = await locateLeyLineOutcrop(type); if (found) { // log.info("成功找到地脉花!"); return; // 找到就直接结束 } // 第一次失败,执行特殊操作 if (retryCount === 0) { log.warn("未找到地脉花,关闭自定义标记并继续尝试"); await closeCustomMarks(); } // 如果 shouldMoveMap 建议移动地图,就移动一下 if (shouldMoveMap(country, retryCount)) { const position = await getMapPosition(country, retryCount, config); log.info(`移动到特定位置:(${position.x},${position.y}), ${position.name}`); await genshin.moveMapTo(position.x, position.y); await sleep(1000); // 移动后也等一下 } await sleep(1000); // 每次循环结束也等一下,防止太快 } // 如果到这里还没找到 throw new Error("寻找地脉花失败,已达最大重试次数"); } /** * 在地图上定位地脉花 * @param {string} type - 地脉花类型 * @returns {Promise} 是否找到地脉花 */ async function locateLeyLineOutcrop(type) { await sleep(500); // 确保画面稳定 await genshin.setBigMapZoomLevel(3.0); const iconPath = type === "蓝花(经验书)" ? "assets/icon/Blossom_of_Revelation.png" : "assets/icon/Blossom_of_Wealth.png"; const flowerList = captureGameRegion().findMulti(RecognitionObject.TemplateMatch(file.ReadImageMatSync(iconPath))); if (flowerList && flowerList.count > 0) { currentFlower = flowerList[0]; const flowerType = type === "蓝花(经验书)" ? "经验" : "摩拉"; log.info(`找到${flowerType}地脉花,位置:(${currentFlower.x},${currentFlower.y})`); const center = genshin.getPositionFromBigMap(); const mapZoomLevel = genshin.getBigMapZoomLevel(); const mapScaleFactor = 2.361; leyLineX = (960 - currentFlower.x - 25) * mapZoomLevel / mapScaleFactor + center.x; leyLineY = (540 - currentFlower.y - 25) * mapZoomLevel / mapScaleFactor + center.y; log.info(`地脉花的实际坐标:(${leyLineX},${leyLineY})`); return true; } else { log.warn("未找到地脉花"); return false; } } /** * 判断是否需要移动地图 * @param {string} country - 国家名称 * @param {number} retryCount - 重试次数 * @returns {boolean} 是否需要移动地图 */ function shouldMoveMap(country, retryCount) { if (retryCount == 0) return false; // 不同国家的重试策略 const countryRetryMap = { "蒙德": [0, 1, 2], "璃月": [0, 1, 2, 3], "稻妻": [0, 1, 2, 3, 4], "枫丹": [0, 1, 2], "纳塔": [0, 1, 2, 3, 4] }; return countryRetryMap[country] && countryRetryMap[country].includes(retryCount); } /** * 获取地图移动位置 * @param {string} country - 国家名称 * @param {number} retryCount - 重试次数 * @param {Object} config - 配置对象 * @returns {Object} 包含x,y坐标的位置对象 */ async function getMapPosition(country, retryCount, config) { // 从配置文件获取位置 if (config.mapPositions[country]) { const positions = config.mapPositions[country]; // 确保retryCount不超过位置数量 let index = Math.min(retryCount, positions.length - 1); log.warn(`retryCount:${retryCount}`); log.warn(`countryIndex:${index}`); return positions[index]; } // 默认返回 log.warn(`未找到国家 ${country} 的位置信息`); return { x: 0, y: 0, name: "默认位置" }; } /** * 判断坐标是否在指定位置附近(误差范围内) * @param {number} x - 当前X坐标 * @param {number} y - 当前Y坐标 * @param {number} targetX - 目标X坐标 * @param {number} targetY - 目标Y坐标 * @param {number} threshold - 误差阈值 * @returns {boolean} 是否在指定范围内 */ function isNearPosition(x, y, targetX, targetY, threshold) { // 使用配置中的阈值或默认值50 const errorThreshold = threshold || 50; return Math.abs(x - targetX) <= errorThreshold && Math.abs(y - targetY) <= errorThreshold; } /** * 计算两点之间的二维欧几里得距离 * @param {number} x1 - 第一个点的X坐标 * @param {number} y1 - 第一个点的Y坐标 * @param {number} x2 - 第二个点的X坐标 * @param {number} y2 - 第二个点的Y坐标 * @returns {number} 两点之间的距离 */ function calculate2DDistance(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); } /** * 奖励和战斗相关函数 */ /** * 判断是否为地脉花并处理 * @param {number} timeout - 超时时间 * @param {string} targetPath - 目标路径 * @param {number} [retries=0] - 当前函数内重试次数 * @returns {Promise} */ async function processLeyLineOutcrop(timeout, targetPath, retries = 0) { // 设置最大重试次数,防止死循环 const MAX_RETRIES = 3; // 如果超过最大重试次数,记录错误并返回,避免死循环 if (retries >= MAX_RETRIES) { log.error(`打开地脉花失败,已重试${MAX_RETRIES}次,终止处理`); log.error("我辣么大一个地脉花哪去了?"); throw new Error("打开地脉花失败"); } let ocr = captureGameRegion().find(RecognitionObject.ocrThis); if (ocr && ocr.text.includes("地脉溢口")) { log.info("识别到地脉溢口"); // await openOutcrop(targetPath); keyPress("F"); await autoFight(timeout); await autoNavigateToReward(); } else if (ocr && ocr.text.includes("打倒所有敌人")) { log.info("地脉花已经打开,直接战斗"); await autoFight(timeout); await autoNavigateToReward(); } else if (ocr && ocr.text.includes("地脉之花")) { log.info("识别到地脉之花"); } else { log.warn(`未识别到地脉花文本,当前重试次数: ${retries + 1}/${MAX_RETRIES}`); try { await pathingScript.runFile(targetPath); await processLeyLineOutcrop(timeout, targetPath, retries + 1); } catch (error) { throw new Error(`打开地脉花失败: ${error.message}`); } } } /** * 尝试领取地脉花奖励 * @param {Object} settings - 设置对象,包含friendshipTeam * @returns {Promise} */ async function attemptReward(settings) { const MAX_RETRY = 5; // 超时处理 if (retryCount >= MAX_RETRY) { retryCount = 0; throw new Error("超过最大重试次数,领取奖励失败"); } log.info("领取奖励,优先使用浓缩树脂"); keyPress("F"); await sleep(500); // 识别是否为地脉之花界面 let resList = captureGameRegion().findMulti(RecognitionObject.ocrThis); let isValid = false; let condensedResin = null; let originalResin = null; let isResinEmpty = false; let dobuleReward = 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("补充原粹树脂")) { isValid = true; isResinEmpty = true; } else if (res.text.includes("产出")) { isValid = true; dobuleReward = true; } } // 处理不同的树脂情况 if (originalResin && dobuleReward == true) { log.info("选择使用原粹树脂,获得双倍产出"); click(Math.round(originalResin.x + originalResin.width / 2), Math.round(originalResin.y + originalResin.height / 2)); if (settings.friendshipTeam) { log.info("切换回战斗队伍"); await genshin.switchParty(settings.team); } return; } else if (condensedResin) { log.info("选择使用浓缩树脂"); click(Math.round(condensedResin.x + condensedResin.width / 2), Math.round(condensedResin.y + condensedResin.height / 2)); if (settings.friendshipTeam) { log.info("切换回战斗队伍"); await genshin.switchParty(settings.team); } return; } else if (originalResin) { log.info("选择使用原粹树脂"); click(Math.round(originalResin.x + originalResin.width / 2), Math.round(originalResin.y + originalResin.height / 2)); if (settings.friendshipTeam) { log.info("切换回战斗队伍"); await genshin.switchParty(settings.team); } return; } else if (isResinEmpty) { log.error("识别到补充原粹树脂,看来树脂用完了呢"); await keyPress("VK_ESCAPE"); throw new Error("树脂已用完"); } } // 界面不正确,尝试重试 if (!isValid) { log.info("当前界面不是地脉之花界面,重试"); await genshin.returnMainUi(); await sleep(1000); retryCount++; await autoNavigateToReward(); await attemptReward(settings); } } /** * 开启地脉花 * @param {string} targetPath - 目标路径 * @returns {Promise} 区域是否出现地脉任务 */ async function openOutcrop(targetPath) { let ocrRegion1 = { x: 800, y: 200, width: 300, height: 100 }; // 中心区域 let ocrRegion2 = { x: 0, y: 200, width: 300, height: 300 }; // 追踪任务区域 let startTime = Date.now(); let recognized = false; keyPress("F"); // 前5秒识别中心区域弹出的横幅任务提示 while (Date.now() - startTime < 5000) { if (recognizeFightText(ocrRegion1)) { recognized = true; break; } keyPress("F"); await sleep(500); } // 如果5秒内没有识别成功,再追加识别追踪任务区域及尝试重新开启地脉花 if (!recognized) { let secondStartTime = Date.now(); while (Date.now() - secondStartTime < 60000) { if (recognizeFightText(ocrRegion1) || recognizeFightText(ocrRegion2)) { recognized = true; break; } await pathingScript.runFile(targetPath); keyPress("F"); await sleep(500); } } } /** * 识别地脉开启进入战斗文本 * @param {Object} ocrRegion - OCR识别区域 * @returns {Promise} 区域是否出现战斗文本 */ function recognizeFightText(ocrRegion) { try { let result = captureGameRegion().find(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height)); let text = result.text; keywords = ["打倒", "所有", "敌人"]; for (let keyword of keywords) { if (text.includes(keyword)) { return true; } } return false; } catch (error) { log.error("OCR过程中出错: {0}", error); } } /** * 自动战斗 * @param {number} timeout - 超时时间 * @returns {Promise} */ async function autoFight(timeout) { const cts = new CancellationTokenSource(); try { const ocrRegionX = 850; const ocrRegionY = 230; const ocrRegionWidth = 1040 - ocrRegionX; const ocrRegionHeight = 300 - ocrRegionY; let ocrRegion = { x: ocrRegionX, y: ocrRegionY, width: ocrRegionWidth, height: ocrRegionHeight }; log.info("开始战斗"); dispatcher.RunTask(new SoloTask("AutoFight"), cts); let fightResult = await recognizeTextInRegion(ocrRegion, timeout) ? "成功" : "失败"; log.info(`战斗结束,战斗结果:${fightResult}`); cts.cancel(); } catch (error) { log.error(`执行过程中出错: ${error}`); } } /** * 识别战斗结果 * @param {Object} ocrRegion - OCR识别区域 * @param {number} timeout - 超时时间 * @returns {Promise} 战斗是否成功 */ async function recognizeTextInRegion(ocrRegion, timeout) { let startTime = Date.now(); const successKeywords = ["挑战达成", "战斗胜利", "挑战成功"]; const failureKeywords = ["挑战失败"]; // 循环检测直到超时 while (Date.now() - startTime < timeout) { try { let result = captureGameRegion().find(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height)); let text = result.text; // 检查成功关键词 for (let keyword of successKeywords) { if (text.includes(keyword)) { log.info("检测到战斗成功关键词: {0}", keyword); return true; } } // 检查失败关键词 for (let keyword of failureKeywords) { if (text.includes(keyword)) { log.warn("检测到战斗失败关键词: {0}", keyword); return false; } } } catch (error) { log.error("OCR过程中出错: {0}", error); } await sleep(1000); // 检查间隔 } log.warn("在超时时间内未检测到战斗结果"); return false; } /** * 地图标记相关函数 */ /** * 关闭自定义标记,使用前确保在地图界面 * @returns {Promise} */ async function closeCustomMarks() { // await genshin.returnMainUi(); // keyPress("M"); await sleep(600); click(60, 1020); await sleep(600); let button = captureGameRegion().find(RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/open.png"),)); if (button) { marksStatus = false; log.info("关闭自定义标记"); click(Math.round(button.x + button.width / 2), Math.round(button.y + button.height / 2)); await sleep(600); keyPress("ESCAPE"); } else { log.error("未找到开关按钮"); } } /** * 打开自定义标记,使用前确保在地图界面 * @returns {Promise} */ async function openCustomMarks() { await sleep(600); click(60, 1020); await sleep(600); let button = captureGameRegion().findMulti(RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/close.png"),)); if (button) { for (let i = 0; i < button.count; i++) { let b = button[i]; if (b.y > 280 && b.y < 350) { log.info("打开自定义标记"); marksStatus = true; click(Math.round(b.x + b.width / 2), Math.round(b.y + b.height / 2)); } } } else { log.error("未找到开关按钮"); } } /** * 自动导航到地脉花奖励点 * @returns {Promise} */ async function autoNavigateToReward() { // 定义识别对象 const boxIconRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/box.png")); const rewardTextRo = RecognitionObject.Ocr(1210, 515, 200, 50); // 领奖区域检测 let advanceNum = 0; // 前进次数 // 调整初始视角为俯视角 log.info("调整为俯视视角..."); middleButtonClick(); await sleep(1000); moveMouseBy(0, 1030); await sleep(500); moveMouseBy(0, 920); await sleep(500); moveMouseBy(0, 710); await sleep(500); log.info("开始自动导航到地脉花..."); while (true) { // 1. 优先检查是否已到达领奖点 let captureRegion = captureGameRegion(); let rewardTextArea = captureRegion.DeriveCrop(1210, 515, 200, 50); let ocrResults = rewardTextArea.findMulti(RecognitionObject.ocrThis); if (advanceNum % 15 == 0 && advanceNum >= 10) { log.warn("前进又超时15次啦,先往旁边挪挪再继续试试") keyDown("s"); await sleep(500); keyUp("s"); middleButtonClick(); await sleep(1000); keyDown("w"); await sleep(1000); keyUp("w"); } else if (advanceNum > 45) { throw new Error('前进时间超时'); } // 检测到地脉之花文字则结束 else if (ocrResults.count > 0 && ocrResults[0].text.trim().length > 0) { for (let i = 0; i < ocrResults.count; i++) { if (ocrResults[i].text.includes("地脉之花")) { log.info("已到达领奖点,检测到文字: " + ocrResults[i].text); return; } } } // 2. 未到达领奖点,则调整视野 await adjustViewForReward(boxIconRo, advanceNum); // 3. 前进一小步 keyDown("w"); await sleep(900); keyUp("w"); await sleep(100); // 等待角色移动稳定 advanceNum++; } } /** * 调整视野直到图标位于正前方 * @param {Object} boxIconRo - 宝箱图标识别对象 * @param {number} advanceNum - 当前前进次数 * @returns {Promise} */ async function adjustViewForReward(boxIconRo, advanceNum) { for (let i = 0; i < 100; i++) { // 每10次执行一轮异常页面检查 if (i % 10 == 0) { // 识别误触发领取导致超时的情况 let resList = captureGameRegion().findMulti(RecognitionObject.ocrThis); if (resList && resList.count > 0) { for (let i = 0; i < resList.count; i++) { let res = resList[i]; if (res.text.includes("原粹树脂")) { log.info("误触发领取页面,尝试关闭页面") keyPress("ESCAPE"); await sleep(500); keyPress("ESCAPE"); await sleep(500); await genshin.returnMainUi(); } } } // 识别误触发其他页面导致超时的情况 const paimonMenuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/paimon_menu.png"), 0, 0, genshin.width / 3.0, genshin.width / 5.0); let res = captureGameRegion().Find(paimonMenuRo); if (res.isEmpty()) { log.info("误触发其他页面,尝试关闭页面") click(960, 800); keyPress("ESCAPE"); await sleep(500); keyPress("ESCAPE"); await sleep(500); await genshin.returnMainUi(); } } let captureRegion = captureGameRegion(); let iconRes = captureRegion.Find(boxIconRo); if (!iconRes) { // 未找到图标,小幅度转动视角 moveMouseBy(20, 0); await sleep(100); continue; } if (iconRes.x >= 920 && iconRes.x <= 980 && iconRes.y <= 540) { log.info(`视野已调正,前进第 ${advanceNum} 次`); return; } else { // 小幅度调整 let adjustAmount = iconRes.x < 920 ? -20 : 20; let adjustAmount2 = iconRes.y < 540 ? 1 : 10; moveMouseBy(adjustAmount * adjustAmount2, 0); await sleep(100); } } // throw new Error('视野调整超时'); }