* 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>
841 lines
33 KiB
JavaScript
841 lines
33 KiB
JavaScript
// 定义所有食材及其对应的路径文件和 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}`);
|
||
}
|
||
})(); |