Files
小鹰划船不用桨 4c2e1157a4 JS脚本:提瓦特百货商店(新建) (#1031)
* feature: 更新第一版提瓦特商店,在OCR买菜基础上进行大规模重构,补充了蒙德,璃月和稻妻的商人,增加了时间重试机制,增加了售罄检测

* Update repo/js/提瓦特百货商店/assets/Pathing/蒙德杂货商人-布兰琪.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update repo/js/提瓦特百货商店/assets/Pathing/璃月万民堂老板-卯师傅.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feature: 更新第一版提瓦特商店,在OCR买菜基础上进行大规模重构,补充了蒙德,璃月和稻妻的商人,增加了时间重试机制,增加了售罄检测

---------

Co-authored-by: 秋云 <physligl@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-23 20:38:02 +08:00

841 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 定义所有食材及其对应的路径文件和 NPC
// 定义路径常量
const paths = {
monde: {
grocery: `assets/Pathing/蒙德杂货商人-布兰琪.json`,
restaurant: `assets/Pathing/蒙德餐馆-莎拉.json`,
plantShop: `assets/Pathing/蒙德植物商人-芙萝拉.json`,
meatShop: `assets/Pathing/蒙德肉类商人-杜拉夫.json`
},
liyue: {
grocery: `assets/Pathing/璃月荣发商铺-东升.json`,
wanmin: `assets/Pathing/璃月万民堂老板-卯师傅.json`,
plantShop: `assets/Pathing/璃月植物类商人-阿桂.json`,
fruitShop: `assets/Pathing/璃月水果和鱼肉-博来.json`,
generalShop: `assets/Pathing/璃月商人-长顺.json`,
fishShop1: `assets/Pathing/璃月鱼贩1-老孙.json`,
fishShop2: `assets/Pathing/璃月鱼贩2-老高.json`,
restaurant: `assets/Pathing/璃月客栈老板娘-菲尔戈黛特.json`,
mill: `assets/Pathing/璃月轻策庄磨坊主-小白.json`,
yilong: `assets/Pathing/璃月遗珑埠百货商人-丰泰.json`,
snackStall: `assets/Pathing/璃月小吃摊摊主-连芳.json`
},
inazuma: {
grocery: `assets/Pathing/稻妻九十九物店主-葵.json`,
generalShop: `assets/Pathing/稻妻百货商人-葵.json`,
restaurant: `assets/Pathing/稻妻餐馆-志村勘兵卫.json`,
fishShop: `assets/Pathing/稻妻征集店名的店主-山城健太.json`,
haijishop: `assets/Pathing/稻妻海祇岛百货商人-清子.json`
},
fontaine: {
grocery: `assets/Pathing/枫丹达莫维百货店主-布希柯.json`,
cafe: `assets/Pathing/枫丹咖啡厅露泽店主-阿鲁埃.json`
},
sumeru: {
cafe: `assets/Pathing/须弥城咖啡馆代理店长-恩忒卡.json`,
fishShop: `assets/Pathing/须弥鱼贩-珀姆.json`,
portFish: `assets/Pathing/须弥奥摩斯港鱼贩-布特罗斯.json`,
villageMerchant: `assets/Pathing/须弥阿如村商人-阿扎莱.json`
}
};
// 定义 ingredient 对应路径
const ingredientPaths = {
"枫达": [paths.fontaine.grocery, paths.fontaine.cafe],
"盐": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop],
"洋葱": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop],
"牛奶": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop, paths.inazuma.haijishop],
"番茄": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop, paths.inazuma.haijishop],
"卷心菜": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop],
"土豆": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.liyue.generalShop, paths.inazuma.generalShop, paths.inazuma.haijishop],
"小麦": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop, paths.inazuma.haijishop],
"胡椒": [paths.monde.grocery, paths.liyue.grocery, paths.inazuma.grocery, paths.fontaine.grocery, paths.inazuma.generalShop],
"稻米": [paths.liyue.grocery, paths.inazuma.grocery, paths.liyue.yilong, paths.inazuma.generalShop],
"虾仁": [paths.liyue.grocery, paths.inazuma.grocery, paths.sumeru.fishShop, paths.sumeru.portFish, paths.liyue.fishShop1, paths.liyue.fishShop2, paths.inazuma.generalShop],
"豆腐": [paths.liyue.grocery, paths.inazuma.grocery, paths.liyue.mill, paths.liyue.yilong, paths.inazuma.generalShop, paths.inazuma.haijishop],
"杏仁": [paths.liyue.grocery, paths.fontaine.grocery, paths.liyue.mill],
"鱼肉": [paths.liyue.wanmin, paths.inazuma.restaurant, paths.sumeru.fishShop, paths.sumeru.portFish, paths.sumeru.villageMerchant, paths.liyue.fruitShop, paths.liyue.fishShop1, paths.liyue.fishShop2],
"螃蟹": [paths.liyue.wanmin, paths.inazuma.restaurant, paths.sumeru.fishShop, paths.sumeru.portFish, paths.liyue.fishShop1, paths.liyue.fishShop2],
"海草": [paths.sumeru.fishShop, paths.sumeru.portFish],
"秃秃豆": [paths.fontaine.grocery, paths.sumeru.villageMerchant],
"咖啡豆": [paths.sumeru.cafe, paths.fontaine.cafe],
"香辛料": [paths.sumeru.villageMerchant],
"发酵果实汁": [paths.fontaine.grocery],
"提瓦特煎蛋": [paths.monde.restaurant],
"野菇鸡肉串": [paths.monde.restaurant, paths.liyue.restaurant, paths.inazuma.restaurant],
"渔人吐司": [paths.monde.restaurant, paths.inazuma.restaurant],
"面粉": [paths.monde.restaurant, paths.inazuma.restaurant],
"奶油": [paths.monde.restaurant],
"熏禽肉": [paths.monde.restaurant],
"黄油": [paths.monde.restaurant, paths.liyue.restaurant],
"火腿": [paths.monde.restaurant],
"糖": [paths.monde.restaurant, paths.liyue.generalShop, paths.inazuma.restaurant],
"蟹黄": [paths.monde.restaurant, paths.liyue.yilong],
"果酱": [paths.monde.restaurant],
"奶酪": [paths.monde.restaurant, paths.liyue.generalShop],
"培根": [paths.monde.restaurant, paths.inazuma.restaurant],
"香肠": [paths.monde.restaurant, paths.liyue.restaurant, paths.inazuma.restaurant],
"甜甜花": [paths.monde.plantShop],
"风车菊": [paths.monde.plantShop],
"塞西莉亚花": [paths.monde.plantShop],
"小灯草": [paths.monde.plantShop],
"嘟嘟莲": [paths.monde.plantShop],
"禽肉": [paths.monde.meatShop],
"鸟蛋": [paths.monde.meatShop],
"兽肉": [paths.monde.meatShop],
"冰雾花花朵": [paths.liyue.plantShop],
"烈焰花花朵": [paths.liyue.plantShop],
"琉璃袋": [paths.liyue.plantShop, paths.liyue.yilong],
"莲蓬": [paths.liyue.plantShop],
"马尾": [paths.liyue.plantShop],
"清心": [paths.liyue.plantShop],
"苹果": [paths.liyue.fruitShop],
"日落果": [paths.liyue.fruitShop],
"星螺": [paths.liyue.fruitShop],
"电气水晶": [paths.liyue.generalShop],
"石珀": [paths.liyue.generalShop, paths.liyue.yilong],
"杏仁豆腐": [paths.liyue.restaurant],
"松茸酿肉卷": [paths.liyue.restaurant],
"香嫩椒椒鸡": [paths.liyue.restaurant],
"山珍热卤面": [paths.liyue.restaurant],
"松茸": [paths.liyue.restaurant],
"霓裳花": [paths.liyue.plantShop, paths.liyue.mill, paths.liyue.yilong],
"琉璃百合": [paths.liyue.mill],
"轻策农家菜": [paths.liyue.mill],
"沉玉仙茗": [paths.liyue.yilong, paths.liyue.snackStall],
"清水玉": [paths.liyue.yilong],
"夜泊石": [paths.liyue.yilong],
"绝云椒椒": [paths.liyue.yilong],
"玉纹茶叶蛋": [paths.liyue.snackStall],
"沉玉茶露": [paths.liyue.snackStall],
"茶熏乳鸽": [paths.liyue.snackStall],
"茶好月圆": [paths.liyue.snackStall],
"鸣草": [paths.inazuma.generalShop],
"堇瓜": [paths.inazuma.restaurant],
"白萝卜": [paths.inazuma.restaurant],
"珊瑚真珠": [paths.inazuma.fishShop],
"鳗肉": [paths.inazuma.fishShop]
};
// 定义所有可能的食材,注意料理名字长度可能超过识图范围
const ingredients = Object.keys(ingredientPaths);
// 定义食材名称和图片文件名的映射表
const ingredientImageMap = Object.fromEntries(
ingredients.map(name => [name, `${name}.png`])
);
// 定义替换映射表
const replacementMap = {
"监": "盐",
"卵": "卯"
};
// 定义所有NPC名注意名字长度可能超过识图范围
// 🎭 NPC 映射
// <20><> 提取路径对应的NPC姓名从路径中"-"后的名字提取)
const npcNames = {};
for (const region in paths) {
const regionPaths = paths[region];
for (const key in regionPaths) {
const path = regionPaths[key];
const match = path.match(/-([^\/]+)\.json$/); // 提取 -后姓名
if (match) {
const name = match[1];
npcNames[path] = [name]; // 用数组包装(支持后续扩展多个别名)
}
}
}
// 筛选出用户选择的食材及其对应的路径文件和 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 settime(time) {
try {
const centerX = 1441;
const centerY = 501;
const radius = 100;
let angle;
angle = (90 + time * 15) % 360;
angle = angle >= 0 ? angle : 360 + angle;
const angle1 = (angle + 90) % 360;
const angle2 = (angle + 180) % 360;
const angle3 = (angle + 270) % 360;
const radians = angle * (Math.PI / 180);
const radians1 = angle1 * (Math.PI / 180);
const radians2 = angle2 * (Math.PI / 180);
const radians3 = angle3 * (Math.PI / 180);
const x = centerX + radius * Math.cos(radians);
const y = centerY + radius * Math.sin(radians);
const x1 = centerX + radius * Math.cos(radians1);
const y1 = centerY + radius * Math.sin(radians1);
const x2 = centerX + radius * Math.cos(radians2);
const y2 = centerY + radius * Math.sin(radians2);
const x3 = centerX + radius * Math.cos(radians3);
const y3 = centerY + radius * Math.sin(radians3);
await sleep(2000);
await moveMouseSmoothly(centerX, centerY, x1, y1);
await sleep(2000);
await moveMouseSmoothly(centerX, centerY, x2, y2);
await sleep(2000);
await moveMouseSmoothly(centerX, centerY, x3, y3);
await sleep(2000);
await moveMouseSmoothly(centerX, centerY, x, y);
} catch (err) {
log.warn(`设置时间操作失败: ${err}`);
}
}
//拖动鼠标
async function moveMouseSmoothly(x1, y1, x2, y2) {
try {
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const steps = Math.max(Math.abs(deltaX), Math.abs(deltaY));
const stepX = deltaX / steps;
const stepY = deltaY / steps;
await moveMouseTo(x1, y1);
await leftButtonDown();
for (let i = 1; i <= steps; i++) {
const newX = x1 + stepX * i;
const newY = y1 + stepY * i;
const validX = Math.round(newX);
const validY = Math.round(newY);
await moveMouseTo(validX, validY);
await sleep(10);
}
await leftButtonUp();
} catch (err) {
log.warn(`鼠标移动失败: ${err}`);
}
}
// 设置游戏时间的主函数
async function setGameTime(targetTime) {
try {
log.info(`设置时间到 ${targetTime}`);
// 打开菜单
await keyPress("Escape");
await sleep(1000);
// 点击时间设置按钮
await click(50, 700);
await sleep(2000);
// 设置具体时间
await settime(targetTime);
await sleep(3000);
// 确认设置
await click(1500, 1000);
await sleep(20000);
// 关闭菜单
await keyPress("Escape");
await sleep(2000);
await keyPress("Escape");
await sleep(2000);
log.info("时间设置完成");
} catch (err) {
log.warn(`设置游戏时间失败: ${err}`);
}
}
// 定义一个函数用于模拟按键操作
async function simulateKeyOperations(key, duration) {
try {
keyDown(key);
await sleep(duration);
keyUp(key);
await sleep(500); // 释放按键后等待 500 毫秒
} catch (err) {
log.warn(`模拟按键操作失败: ${err}`);
}
}
// 定义一个函数用于购买食材
async function purchaseIngredient(ingredient) {
try {
log.info(`购买食材: ${ingredient}`);
// 在购买前进行识别
let ComfirmRoResult1 = await recognizeImage("assets/Comfirm.png", 1585, 1005, 30, 30, 2000);
let soldOutResult = await recognizeImage("assets/已售罄.png", 1320, 920, 31, 31, 2000);
if (soldOutResult) {
log.warn(`食材: ${ingredient}已售罄`);
return;
}
if (ComfirmRoResult1) {
// 模拟购买操作的后续点击
await click(1600, 1020);
await sleep(1000); // 购买
} else {
log.warn(`食材: ${ingredient}背包已满`);
return; // 退出操作
}
// 在点击选择100个之前进行识别
let ComfirmRoResult2 = await recognizeImage("assets/Comfirm.png", 995, 766, 30, 30, 2000);
if (ComfirmRoResult2) {
log.info("选择100个的");
await click(1181, 600);
await sleep(200); // 选择100个
} else {
log.warn("尝试重新点击购买");
await click(1600, 1020);
await sleep(1000); // 购买
return; // 退出操作
}
await click(1320, 780);
await sleep(1000); // 最终确认
await click(1320, 780);
await sleep(1000); // 点击空白
} catch (err) {
log.warn(`购买食材 ${ingredient} 失败: ${err}`);
}
}
// 定义一个通用的图像识别函数
function recognizeImage(templatePath, xMin, yMin, width, height, timeout = 2000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
let template = file.ReadImageMatSync(templatePath);
let recognitionObject = RecognitionObject.TemplateMatch(template, xMin, yMin, width, height);
let result = captureGameRegion().find(recognitionObject);
if (result.isExist()) {
return { success: true, x: result.x, y: result.y, width: result.width, height: result.height };
}
} catch (err) {
log.warn(`识别图像时发生异常: ${err}`);
return null;
}
}
log.warn("图像识别超时");
return null;
}
// 定义一个函数用于执行OCR识别
function performOcr(targetText, xRange, yRange, tolerance, timeout = 2000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 调整区域范围以包含容错区间
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(new RegExp(wrongChar, 'g'), correctChar);
}
if (correctedText.includes(targetText)) {
return { success: true, x: res.x, y: res.y, width: res.width, height: res.height };
}
}
} catch (err) {
log.info(`OCR识别超时: ${err}`);
return { success: false };
}
}
log.warn("OCR识别超时");
return { success: false };
}
// 定义一个函数用于识别食材
async function recognizeIngredient(ingredient) {
try {
let recognized = false;
const clickOffset = 30; // 点击坐标偏移
// 尝试 OCR 识别
let ocrResult = await performOcr(ingredient, { min: 210, max: 390 }, { min: 105, max: 920 }, 10);
if (ocrResult.success) {
log.info(`通过 OCR 识别找到食材: ${ingredient}`);
await click(ocrResult.x, ocrResult.y + clickOffset);
await sleep(1000);
recognized = true;
} else {
// OCR 识别失败,尝试图像识别
let imagePath = `assets/Picture/${ingredientImageMap[ingredient]}`;
if (!imagePath) {
log.warn(`未找到食材 '${ingredient}' 的图片文件`);
return recognized;
}
let imageResult = recognizeImage(imagePath, 120, 90, 95, 865, 1000);
if (imageResult) {
log.info(`通过图像识别找到食材: ${ingredient}`);
let x = Math.round(imageResult.x);
let y = Math.round(imageResult.y);
await click(x, y);
await sleep(1000);
recognized = true;
} else {
log.warn(`未能识别到食材: ${ingredient}`);
}
}
return recognized;
} catch (err) {
log.warn(`识别食材 ${ingredient} 失败: ${err}`);
return false;
}
}
// 定义一个函数用于识别并点击用户选择的食材
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; // 容错区间
// 检查 F 图标和右边水平对齐的文字
async function checkNpcAndFAlignment(npcName, fDialogueRo) {
try {
log.info("进入 checkNpcAndFAlignment");
// 检查F图标和NPC是否对齐的函数
async function checkAlignment() {
let ra = captureGameRegion();
let fRes = ra.find(fDialogueRo);
if (!fRes.isExist()) {
log.info("未找到F图标");
return false;
}
let centerYF = fRes.y + fRes.height / 2;
let ocrResult = await performOcr(npcName, npcxRange, { min: fRes.y, max: fRes.y + fRes.height }, tolerance);
if (!ocrResult.success) {
log.info("未找到NPC名称");
return false;
}
let centerYnpcName = ocrResult.y + ocrResult.height / 2;
let isAligned = Math.abs(centerYnpcName - centerYF) <= npctolerance;
log.info(`NPC和F图标是否对齐: ${isAligned}`);
return isAligned;
}
// 执行滚轮下滑和检查的函数
async function tryScrollAndCheck() {
for (let i = 0; i < 3; i++) {
log.info(`执行第${i + 1}次滚轮下滑`);
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
await sleep(1000);
if (await checkAlignment()) {
log.info(`在第${i + 1}次滚轮下滑后找到对齐的NPC`);
return true;
}
}
return false;
}
// 主要逻辑
// 1. 先检查当前状态
if (await checkAlignment()) {
log.info("初始状态已对齐");
return true;
}
// 2. 尝试3次滚轮下滑
log.info("开始尝试滚轮下滑");
if (await tryScrollAndCheck()) {
return true;
}
// 3. 调整到8点
log.info("滚轮下滑失败调整到8点");
await setGameTime(8);
await sleep(2000);
// 4. 8点时检查并尝试滚轮下滑
if (await checkAlignment()) {
log.info("8点时初始状态已对齐");
return true;
}
if (await tryScrollAndCheck()) {
return true;
}
// 5. 调整到18点
log.info("8点尝试失败调整到18点");
await setGameTime(18);
await sleep(2000);
// 6. 18点时检查并尝试滚轮下滑
if (await checkAlignment()) {
log.info("18点时初始状态已对齐");
return true;
}
if (await tryScrollAndCheck()) {
return true;
}
// 7. 如果都失败了,重新加载路径
log.info("所有尝试都失败,重新加载路径");
await pathingScript.runFile(filePath);
await sleep(500);
// 8. 最后再检查一次
return await checkAlignment();
} catch (err) {
log.warn(`检查NPC和F对齐失败: ${err}`);
return false;
}
}
// 执行点击操作
async function performClickOperations(filePath) {
// 根据NPC执行不同的点击操作
if (filePath === paths.monde.meatShop) {
log.info("执行杜拉夫的特殊点击操作");
for (let i = 0; i < 3; i++) {
await click(1300, 580);
await sleep(i < 2 ? 500 : 1000);
}
await click(1320, 780);
await sleep(1000);
// 检查是否出现购买按钮或已售罄图标
let buyButtonResult = await recognizeImage("assets/Comfirm.png", 1585, 1005, 31, 31, 2000);
let soldOutResult = await recognizeImage("assets/已售罄.png", 1320, 916, 50, 50, 2000);
if (buyButtonResult) {
log.info("成功触发购买界面");
} else if (soldOutResult) {
log.info("检测到已售罄状态");
} else {
log.warn("杜拉夫操作后未检测到购买按钮或已售罄图标");
}
return buyButtonResult || soldOutResult;
}
// 其他NPC的标准点击操作
const maxAttempts = 10;
let attempts = 0;
let success = false;
while (attempts < maxAttempts && !success) {
attempts++;
// 其他NPC的标准点击操作
await keyPress("F");
await sleep(50);
await click(1300, 500);
await sleep(50);
// 检查是否出现购买按钮或已售罄图标
let buyButtonResult = await recognizeImage("assets/Comfirm.png", 1585, 1005, 31, 31, 2000);
let soldOutResult = await recognizeImage("assets/已售罄.png", 1320, 916, 50, 50, 2000);
if (buyButtonResult || soldOutResult) {
if (buyButtonResult) {
log.info("成功触发购买界面");
} else {
log.info("检测到已售罄状态");
}
success = true;
break;
}
}
if (!success) {
log.error(`未能进入商店界面,重试次数已达 ${maxAttempts} 次,返回主界面并继续下一条路径`);
await genshin.returnMainUi();
return;
}
return success;
}
// 执行完整的购买流程
async function executePurchaseFlow() {
for (const npcName of npcNames) {
log.info(`尝试识别 NPC: ${npcName}`);
let isAligned = await checkNpcAndFAlignment(npcName, fDialogueRo);
let scrollAttempts = 0;
const maxScrollAttempts = 5;
while (!isAligned && scrollAttempts < maxScrollAttempts) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
await sleep(1000);
scrollAttempts++;
if (scrollAttempts >= maxScrollAttempts) {
log.error(`滚轮操作次数已达上限 ${maxScrollAttempts} 次,退出循环`);
break;
}
let ra = captureGameRegion();
let fRes = ra.find(fDialogueRo);
if (!fRes.isExist()) {
log.warn("未找到 F 图标");
continue;
}
let centerYF = fRes.y + fRes.height / 2;
let ocrResult = await performOcr(npcName, npcxRange, { min: fRes.y, max: fRes.y + fRes.height }, tolerance);
if (!ocrResult.success) {
log.warn(`OCR 识别未找到 NPC: ${npcName}`);
continue;
}
let centerYnpcName = ocrResult.y + ocrResult.height / 2;
if (Math.abs(centerYnpcName - centerYF) <= npctolerance) {
isAligned = true;
log.info(`NPC '${npcName}' 和 F 图标水平对齐NPC: ${centerYnpcName}, F 图标: ${centerYF}`);
} else {
log.info(`NPC '${npcName}' 和 F 图标未水平对齐NPC: ${centerYnpcName}, F 图标: ${centerYF}`);
}
}
if (isAligned) {
keyPress("F");
await sleep(2500);
await performClickOperations(filePath);
let ComfirmRoResult = null;
let C_maxAttempts = 2;
let C_attempts = 0;
while (!ComfirmRoResult && C_attempts < C_maxAttempts) {
ComfirmRoResult = await recognizeImage("assets/Comfirm.png", 1585, 1005, 31, 31, 2000);
if (ComfirmRoResult) {
log.info("识别到购买按钮,执行食材选择");
break;
} else {
log.warn("未识别到购买按钮,尝试重新识别");
}
await sleep(500);
await performClickOperations(filePath);
C_attempts++;
}
if (!ComfirmRoResult) {
log.warn("未在规定时间内完成对话");
return;
}
let purchasedIngredients = new Set();
let allIngredientsFound = false;
let scrollAttemptsForIngredients = 0;
const maxScrollAttemptsForIngredients = 3;
while (!allIngredientsFound && scrollAttemptsForIngredients < maxScrollAttemptsForIngredients) {
allIngredientsFound = true;
for (const ingredient of selectedIngredients) {
if (purchasedIngredients.has(ingredient)) {
log.info(`跳过已购买的食材: ${ingredient}`);
continue;
}
let recognized = await recognizeIngredient(ingredient);
if (recognized) {
log.info(`识别到 '${ingredient}',执行购买操作`);
await purchaseIngredient(ingredient);
purchasedIngredients.add(ingredient);
} else {
allIngredientsFound = false;
}
}
if (!allIngredientsFound) {
log.info(`在当前页面未找到所有食材,尝试翻页`);
await PageScroll(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 {
log.error(`未能找到正确的 NPC '${npcName}' 或未成功交互,跳过该 NPC`);
}
}
log.error("未能找到正确的 NPC 或未成功交互,跳过该路径");
}
// 对于老高的商店,执行两遍完整流程
if (filePath === paths.liyue.fishShop2) {
log.info("执行双重购买流程");
// 第一遍
await executePurchaseFlow();
await sleep(1000);
// 第二遍
await executePurchaseFlow();
} else {
// 其他商店正常执行一遍
await executePurchaseFlow();
}
}
// 自动执行划页操作
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 (err) {
log.warn(`执行滑动操作失败: ${err}`);
}
}
// 主函数
async function AutoPath() {
try {
log.info("开始执行自动寻路任务");
// 创建一个Map来存储每个NPC需要购买的所有食材
let npcIngredientMap = new Map();
// 遍历选中的食材按NPC分组
for (let [path, ingredients] of selectedPaths) {
if (!npcIngredientMap.has(path)) {
npcIngredientMap.set(path, new Set());
}
ingredients.forEach(ingredient => {
npcIngredientMap.get(path).add(ingredient);
});
}
// 定义区域顺序
const regionOrder = {
'monde': 2, // 蒙德
'liyue': 3, // 璃月
'sumeru': 4, // 须弥
'fontaine': 5 // 枫丹
};
// 将Map转换为数组并排序
let sortedPaths = Array.from(npcIngredientMap.entries()).sort((a, b) => {
// 按照正常区域顺序排序
const getRegion = (path) => {
for (const region in paths) {
if (Object.values(paths[region]).includes(path)) {
return region;
}
}
return '';
};
const regionA = getRegion(a[0]);
const regionB = getRegion(b[0]);
return (regionOrder[regionA] || 999) - (regionOrder[regionB] || 999);
});
// 按排序后的顺序访问NPC
for (let [path, ingredientSet] of sortedPaths) {
log.info(`访问NPC路径: ${path}`);
log.info(`需要购买的食材: ${Array.from(ingredientSet).join(", ")}`);
let npcName = npcNames[path];
// 将Set转换回数组
await clickSelectedIngredients(Array.from(ingredientSet), path, npcName);
}
} catch (err) {
log.warn(`自动寻路任务失败: ${err}`);
}
}
// 执行主函数
(async function () {
try {
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
// 执行自动寻路
await AutoPath();
} catch (err) {
log.warn(`主函数执行失败: ${err}`);
}
})();