// 定义所有食材的图像识别对象 let FengdaRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/FengdaRo.png")); let SaltRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/SaltRo.png")); let PepperRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/PepperRo.png")); let OnionRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/OnionRo.png")); let MilkRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/MilkRo.png")); let TomatoRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/TomatoRo.png")); let SpicesRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/SpicesRo.png")); let CabbageRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/CabbageRo.png")); let PotatoRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/PotatoRo.png")); let WheatRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/WheatRo.png")); let RiceRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/RiceRo.png")); let TofuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/TofuRo.png")); let AlmondRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/AlmondRo.png")); let FishRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/FishRo.png")); let CrabRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/CrabRo.png")); let ShrimpRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/ShrimpRo.png")); let CoffeeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/CoffeeRo.png")); let ToutuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/ToutuRo.png")); let FermentRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Picture/FermentRo.png")); // 定义所有可能的食材,注意料理名字长度可能超过识图范围 const ingredients = [ "枫达", "盐", "胡椒", "洋葱", "牛奶", "番茄", "香辛料", "卷心菜", "土豆", "小麦", "稻米", "豆腐", "杏仁", "鱼肉", "螃蟹", "虾仁", "咖啡豆", "秃秃豆", "发酵果实汁" ]; // 定义所有食材及其对应的路径文件和 NPC const mondstadtGroceryFilePath = `assets/Pathing/蒙德百货销售员布兰琪.json`; const liyueGroceryFilePath = `assets/Pathing/璃月荣发商铺店主东升.json`; const liyueWanminFilePath = `assets/Pathing/璃月万民堂老板卯师傅.json`; const groceryFilePath = `assets/Pathing/稻妻九十九物店主葵.json`; const charcoalFilePath = `assets/Pathing/稻妻志村屋店主志村勘兵卫.json`; const fengdanGroceryFilePath = `assets/Pathing/枫丹达莫维百货店主布希柯.json`; const cafeLuzheFilePath = `assets/Pathing/枫丹咖啡厅露泽店主阿鲁埃.json`; const sumiCitycafeFilePath = `assets/Pathing/须弥城咖啡馆代理店长恩忒卡.json`; const sumiCityFishPath = `assets/Pathing/须弥城鱼贩珀姆.json`; const omosPortFishPath = `assets/Pathing/须弥奥摩斯港鱼贩布特罗斯.json`; const azaleVillMerPath = `assets/Pathing/须弥阿如村商人阿扎莱.json`; const ingredientPaths = { "枫达": [fengdanGroceryFilePath, cafeLuzheFilePath], "盐": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "洋葱": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "牛奶": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "番茄": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "卷心菜": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "土豆": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "小麦": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "胡椒": [mondstadtGroceryFilePath, liyueGroceryFilePath, groceryFilePath, fengdanGroceryFilePath], "稻米": [liyueGroceryFilePath, groceryFilePath],// "虾仁": [liyueGroceryFilePath, groceryFilePath, sumiCityFishPath, omosPortFishPath], "豆腐": [liyueGroceryFilePath, groceryFilePath], "杏仁": [liyueGroceryFilePath, fengdanGroceryFilePath], "鱼肉": [liyueWanminFilePath, charcoalFilePath, sumiCityFishPath, omosPortFishPath, azaleVillMerPath], "螃蟹": [liyueWanminFilePath, charcoalFilePath, sumiCityFishPath, omosPortFishPath], "秃秃豆": [fengdanGroceryFilePath, azaleVillMerPath], "咖啡豆": [sumiCitycafeFilePath, cafeLuzheFilePath], "香辛料": [azaleVillMerPath], "发酵果实汁": [fengdanGroceryFilePath] }; // 定义食材名称和图片文件名的映射表 const ingredientImageMap = { "枫达": "FengdaRo.png", "盐": "SaltRo.png", "洋葱": "OnionRo.png", "牛奶": "MilkRo.png", "番茄": "TomatoRo.png", "卷心菜": "CabbageRo.png", "土豆": "PotatoRo.png", "小麦": "WheatRo.png", "胡椒": "PepperRo.png", "稻米": "RiceRo.png", "虾仁": "ShrimpRo.png", "豆腐": "TofuRo.png", "杏仁": "AlmondRo.png", "鱼肉": "FishRo.png", "螃蟹": "CrabRo.png", "秃秃豆": "ToutuRo.png", "咖啡豆": "CoffeeRo.png", "香辛料": "SpicesRo.png", "发酵果实汁": "FermentRo.png", // 可以继续添加更多食材的映射 }; // 定义替换映射表 const replacementMap = { "监": "盐", "卵": "卯" }; // 定义所有NPC名,注意名字长度可能超过识图范围 const npcNames = { [mondstadtGroceryFilePath]: ["布兰琪"], [liyueGroceryFilePath]: ["东升"], [liyueWanminFilePath]: ["卯师傅"],// ["卯师傅", "卵师傅"] [groceryFilePath]: ["葵"], [charcoalFilePath]: ["志村勘"], [fengdanGroceryFilePath]: ["布希柯"], [cafeLuzheFilePath]: ["阿鲁埃"], [sumiCityFishPath]: ["珀姆"], [sumiCitycafeFilePath]: ["恩忒卡"], [omosPortFishPath]: ["布特罗斯"], [azaleVillMerPath]: ["阿扎莱"] }; // 筛选出用户选择的食材及其对应的路径文件和 NPC let selectedIngredients = []; // 在函数外部声明一次 let selectedPaths = new Map(); for (let ingredient of ingredients) { if (settings[ingredient]) { selectedIngredients.push(ingredient); ingredientPaths[ingredient].forEach(path => { if (!selectedPaths.has(path)) { selectedPaths.set(path, []); } selectedPaths.get(path).push(ingredient); }); } } if (selectedIngredients.length === 0) { log.error("未选择任何食材,退出任务"); throw new Error("未选择任何食材,任务终止"); // 抛出异常以终止任务 } // 汇总即将购买的食材信息 let purchaseSummary = selectedIngredients.join(", "); log.info(`即将购买: ${purchaseSummary}`); // 定义一个函数用于模拟按键操作 async function simulateKeyOperations(key, duration) { keyDown(key); await sleep(duration); keyUp(key); await sleep(500); // 释放按键后等待 500 毫秒 } // 定义一个函数用于购买食材 async function purchaseIngredient(ingredient) { log.info(`购买食材: ${ingredient}`); // 模拟购买操作的后续点击 await click(1600, 1020); await sleep(1000); // 购买 await click(1181, 600); await sleep(200); // 选择100个 await click(1320, 780); await sleep(1000); // 最终确认 await click(1320, 780); await sleep(1000); // 点击空白 } // 定义一个通用的图像识别函数 function recognizeImage(templatePath, xMin, yMin, width, height) { let template = file.ReadImageMatSync(templatePath); let recognitionObject = RecognitionObject.TemplateMatch(template, xMin, yMin, width, height); let result = captureGameRegion().find(recognitionObject); return result.isExist() ? result : null; } // 定义一个函数用于执行OCR识别 async function performOcr(targetText, xRange, yRange, tolerance) { // 调整区域范围以包含容错区间 let adjustedXMin = xRange.min - tolerance; let adjustedXMax = xRange.max + tolerance; let adjustedYMin = yRange.min - tolerance; let adjustedYMax = yRange.max + tolerance; // 在捕获的区域内进行OCR识别 let ra = captureGameRegion(); let resList = ra.findMulti(RecognitionObject.ocr( adjustedXMin, adjustedYMin, adjustedXMax - adjustedXMin, adjustedYMax - adjustedYMin )); // 遍历识别结果,检查是否找到目标文本 for (let i = 0; i < resList.count; i++) { let res = resList[i]; // 后处理:根据替换映射表检查和替换错误识别的字符 let correctedText = res.text; for (let [wrongChar, correctChar] of Object.entries(replacementMap)) { correctedText = correctedText.replace(wrongChar, correctChar); } if (correctedText.includes(targetText)) { // 如果找到目标文本,直接返回坐标 return { success: true, x: res.x, y: res.y, width: res.width, height: res.height }; } } return { success: false }; // 未找到符合条件的文本 } // 定义一个函数用于识别食材 async function recognizeIngredient(ingredient) { let recognized = false; const clickOffset = 30; // 点击坐标偏移 // 尝试 OCR 识别 let ocrResult = await performOcr(ingredient, { min: 210, max: 390 }, { min: 105, max: 950 }, 10); if (ocrResult.success) { log.info(`通过 OCR 识别找到食材: ${ingredient}`); log.info(`坐标: x=${ocrResult.x}, y=${ocrResult.y}`); await click(ocrResult.x, ocrResult.y + clickOffset); await sleep(1000); recognized = true; } else { // OCR 识别失败,尝试图像识别 let imagePath = `assets/Picture/${ingredientImageMap[ingredient]}`; if (!imagePath) { log.error(`未找到食材 '${ingredient}' 的图片文件`); return recognized; } let imageResult = recognizeImage(imagePath, 120, 90, 95, 865); if (imageResult) { log.info(`通过图像识别找到食材: ${ingredient}`); imageResult.click(); await sleep(1000); recognized = true; } else { log.error(`未能识别到食材: ${ingredient}`); } } return recognized; } // 定义一个函数用于识别并点击用户选择的食材 async function clickSelectedIngredients(selectedIngredients, filePath, npcNames) { log.info(`加载路径文件: ${filePath}`); await pathingScript.runFile(filePath); await sleep(1000); // 识别并交互 NPC const npcxRange = { min: 1190, max: 1320 }; // npc X轴区间 const FxRange = { min: 1050, max: 1150 }; // F X轴坐标 const FyRange = { min: 400, max: 800 }; // F Y轴坐标 let fDialogueRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/F_Dialogue.png"), FxRange.min, FyRange.min, FxRange.max - FxRange.min, FyRange.max - FyRange.min); const tolerance = 12; // 容错区间 const npctolerance = 5; // 容错区间 // 执行点击操作 async function performClickOperations(filePath) { if (filePath === liyueGroceryFilePath || filePath === groceryFilePath || filePath === sumiCityFishPath) { log.info("执行璃月稻妻杂货商等的点击操作"); await click(1300, 650); await sleep(500); // 双击增加低帧点击成功率 await click(1300, 650); await sleep(500); await click(1300, 650); await sleep(1000); await click(1600, 1020); await sleep(1000); } else { log.info("执行其他路径文件的点击操作"); await click(1300, 580); await sleep(500); await click(1300, 580); await sleep(500); await click(1300, 580); await sleep(1000); await click(1600, 1020); await sleep(1000); } } // 检查 F 图标和右边水平对齐的文字 async function checkNpcAndFAlignment(npcName, fDialogueRo) { let ra = captureGameRegion(); let fRes = ra.find(fDialogueRo); if (!fRes.isExist()) { let f_attempts = 0; // 初始化尝试次数 while (f_attempts < 3) { // 最多尝试 3 次 f_attempts++; log.info(`当前尝试次数:${f_attempts}`); if (f_attempts === 1) { // 第一次未找到 F 图标 await simulateKeyOperations("S", 200); // 后退 200 毫秒 await sleep(200); await simulateKeyOperations("W", 800); // 前进 800 毫秒 } else if (f_attempts === 2) { // 第二次未找到 F 图标 log.info("重新加载路径文件"); await pathingScript.runFile(filePath); await sleep(500); } else { // 第三次未找到 F 图标 log.error("尝试次数已达上限"); return false; } log.error(`尝试 ${f_attempts + 1}:寻找 F 图标`); } } // 获取 F 图标的中心点 Y 坐标 let centerYF = fRes.y + fRes.height / 2; // 在 F 图标右侧水平方向上识别 NPC 名称 let ocrResult = await performOcr(npcName, npcxRange, { min: fRes.y, max: fRes.y + fRes.height }, tolerance); if (!ocrResult.success) { log.error(`OCR 识别未找到 NPC: ${npcName},尝试滚动`); return false; } // 获取 NPC 名称的中心点 Y 坐标 let centerYnpcName = ocrResult.y + ocrResult.height / 2; // 检查 NPC 名称和 F 图标的中心点 Y 坐标是否在容错范围内 if (Math.abs(centerYnpcName - centerYF) <= npctolerance) { return true; } else { log.info(`NPC '${npcName}' 和 F 图标未水平对齐, NPC: ${centerYnpcName}, F 图标: ${centerYF}`); return false; } } // 新增变量用于记录滚轮操作次数 let scrollAttempts = 0; const maxScrollAttempts = 5; // 最大滚轮操作次数限制 for (const npcName of npcNames) { log.info(`尝试识别 NPC: ${npcName}`); let isAligned = await checkNpcAndFAlignment(npcName, fDialogueRo); while (!isAligned && scrollAttempts < maxScrollAttempts) { // 如果未水平对齐,执行滚轮操作 await keyMouseScript.runFile(`assets/滚轮下翻.json`); await sleep(1000); // 检查是否超过最大滚轮操作次数 scrollAttempts++; if (scrollAttempts >= maxScrollAttempts) { log.error(`滚轮操作次数已达上限 ${maxScrollAttempts} 次,退出循环`); break; // 超过最大滚轮操作次数,终止循环 } // 重新检查 F 图标和 NPC 名称是否对齐 isAligned = await checkNpcAndFAlignment(npcName, fDialogueRo); } if (isAligned) { // 如果水平对齐,执行交互操作 keyPress("F"); await sleep(2500); // 执行点击操作 await performClickOperations(filePath); // 只有在成功对齐并交互后,才执行后续的食材购买操作 // 记录已购买的食材 let purchasedIngredients = new Set(); let allIngredientsFound = false; // 标记是否所有食材都已找到 let scrollAttemptsForIngredients = 0; const maxScrollAttemptsForIngredients = 3; // 最大翻页次数 while (!allIngredientsFound && scrollAttemptsForIngredients < maxScrollAttemptsForIngredients) { allIngredientsFound = true; // 假设本轮所有食材都已找到,若后续发现未找到则修改为 false for (const ingredient of selectedIngredients) { if (purchasedIngredients.has(ingredient)) { log.info(`跳过已购买的食材: ${ingredient}`); continue; // 跳过已购买的食材 } // await sleep(1000); // 尝试识别食材 let recognized = await recognizeIngredient(ingredient); if (recognized) { log.info(`识别到 '${ingredient}',执行购买操作`); await purchaseIngredient(ingredient); purchasedIngredients.add(ingredient); } else { log.error(`未能识别到食材: ${ingredient}`); allIngredientsFound = false; // 本轮有食材未找到 } } if (!allIngredientsFound) { log.info(`在当前页面未找到所有食材,尝试翻页`); await PageScroll(1); // 每轮翻页滑动1次 await sleep(1000); scrollAttemptsForIngredients++; } } if (!allIngredientsFound) { log.error(`在所有页面中未找到所有食材,跳过该路径`); } // 最后点击退出按钮 log.info("点击退出按钮..."); await click(1845, 45); // 退出 await sleep(2000); // 如果成功购买了所有食材,记录成功信息 if (allIngredientsFound) { log.info("成功购买了所有食材!"); } else { log.error("未能购买所有食材,部分食材可能未找到或未成功购买。"); } return; // 结束函数,后续逻辑不再执行 } else { // 如果未水平对齐且超过最大滚轮操作次数,记录错误信息并跳过该 NPC log.error(`未能找到正确的 NPC '${npcName}' 或未成功交互,跳过该 NPC`); } } // 如果没有找到任何 NPC 或未成功交互,则记录错误信息并退出 log.error("未能找到正确的 NPC 或未成功交互,跳过该路径"); } // 自动执行划页操作 async function PageScroll(scrollCount) { try { const clickX = 1200; // 假设点击的起始坐标 const clickY = 900; const totalDistance = 500; // 假设每次滑动的总距离 const stepDistance = 15; // 每步移动的距离 for (let i = 0; i < scrollCount; ++i) { log.info(`开始第 ${i + 1} 次滑动`); // 如果点击坐标为 (0, 0),则跳过点击 if (clickX !== 0 || clickY !== 0) { moveMouseTo(clickX, clickY); // 移动到指定坐标 await sleep(100); } // 按住鼠标左键 leftButtonDown(); // 将鼠标移动到目标位置,模拟更自然的拖动操作 const steps = totalDistance / stepDistance; // 分成若干步移动 for (let j = 0; j < steps; j++) { moveMouseBy(0, -stepDistance); // 每次移动 stepDistance 像素 await sleep(10); // 每次移动后延迟10毫秒 } // 释放鼠标左键 await sleep(700); leftButtonUp(); await sleep(100); } } catch (error) { log.error(`执行滑动操作时发生错误:${error.message}`); } } // 主函数 async function AutoPath() { log.info("开始执行自动寻路任务"); // 加载路径文件和 NPC 名称 for (let [path, ingredients] of selectedPaths) { let npcName = npcNames[path]; await clickSelectedIngredients(ingredients, path, npcName); } } // 执行主函数 (async function () { setGameMetrics(1920, 1080, 1); await genshin.returnMainUi(); await AutoPath(); })();