JS脚本:角色养成一条龙 (#1428)

This commit is contained in:
5117600049
2025-07-28 11:16:24 +08:00
committed by GitHub
parent a0a89f276e
commit 3043bac043
126 changed files with 1964 additions and 0 deletions

View File

@@ -0,0 +1,949 @@
(async function () {
//获取BOSS材料数量
async function getBossMaterialCount(bossName) {
await genshin.returnMainUi();
await sleep(500);
keyPress("F1");
await repeatOperationUntilTextFound({x: 250,y: 520,width: 100,height: 60,targetText: "讨伐",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await repeatOperationUntilTextFound({x: 380,y: 180,width: 100,height: 50,targetText: "全部",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await repeatOperationUntilTextFound({x: 400,y: 360,width: 100,height: 50,targetText: "精英",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await sleep(500);
await repeatOperationUntilTextFound({x: 380,y: 180,width: 100,height: 50,targetText: "精英",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await sleep(500);
await repeatOperationUntilTextFound({x: 400,y: 420,width: 100,height: 50,targetText: "首领",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await sleep(500);
click(956,844);await sleep(500);//点到最后
click(956,844);await sleep(500);//点到最后
await waitAndClickImage('boss/wolf');//点击狼王图标,避免其他图标识别失败
click(958,286);await sleep(500);//返回最上边
click(958,286);await sleep(500);//返回最上边
log.info(`正在查询数量`);
try {
const targetImageRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/boss/${bossName}.png`), 0, 0, 1920,1080);
targetImageRo.Threshold = 0.95;
const stopImageRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/boss/无相之风.png"), 0, 0, 1920,1080);
stopImageRo.Threshold = 0.95;
await findAndClickWithScroll(targetImageRo, stopImageRo, {maxAttempts: 30,scrollNum: 9});
if(bossName == '科培琉司的劫罚') click(1320,680);
else click(1236,680);
await sleep(800);
const result = await findImageAndOCR("assets/itemQuantityDetection.png", 200, 50, 0, 0);
if (result !== false) {
const quantity = positiveIntegerJudgment(result);
log.info(`识别到${bossName}材料数量: ${quantity}`);
return quantity;
} else {
log.warn(`${bossName}材料识别失败,请检查相关设置`);
}
} catch (error) {
notification.send(`${bossName}材料刷取失败,错误信息: ${error}`);
return 0;
}
}
/**
* 寻找特定图片并点击,未找到则滚动画面
* @param {RecognitionObject} targetRo - 要寻找并点击的目标图片识别对象
* @param {RecognitionObject} stopRo - 终止条件的图片识别对象(遇到此图片则停止)
* @param {Object} options - 配置选项
* @param {number} [options.maxAttempts=10] - 最大尝试次数
* @param {number} [options.scrollDelay=1000] - 滚动后的等待时间(毫秒)
* @param {number} [options.clickDelay=500] - 点击后的等待时间(毫秒)
* @returns {Promise<void>}
* @throws {Error} 当达到最大尝试次数或遇到终止图片时抛出错误
*/
async function findAndClickWithScroll(targetRo, stopRo, options = {}) {
const {
maxAttempts = 10,
scrollNum = 9,
clickDelay = 500
} = options;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// 1. 捕获当前游戏区域
const captureRegion = captureGameRegion();
// 3. 寻找目标图片
const targetResult = captureRegion.find(targetRo);
if (!targetResult.isEmpty()) {
// 找到目标,点击并返回
log.info(`找到目标图片,位置: (${targetResult.x}, ${targetResult.y})`);
targetResult.click();
await sleep(clickDelay);
return;
}
// 4. 未找到目标,滚动画面
log.info(`${attempt + 1} 次尝试未找到目标图片,将滚动画面...`);
for (let i = 0; i < scrollNum; i++) {
await keyMouseScript.runFile("assets/滚轮下滑.json");
}
// 2. 检查是否遇到终止图片
const stopResult = captureRegion.find(stopRo);
if (!stopResult.isEmpty()) {
throw new Error(`遇到终止图片,停止寻找目标图片。终止位置: (${stopResult.x}, ${stopResult.y})`);
}
}
// 达到最大尝试次数仍未找到
throw new Error(`${maxAttempts} 次尝试后仍未找到目标图片`);
}
//执行战斗并检测结束
async function restoredEnergyAutoFightAndEndDetection() {
await genshin.tp(178.55,384.4);
await repeatOperationUntilTextFound();//
keyPress("F");
await repeatOperationUntilTextFound({x: 1650,y: 1000,width: 160,height: 45,targetText: "单人挑战",stepDuration: 0,waitTime: 100,ifClick: true});//等待点击单人挑战
await sleep(200);
click(1180, 760);//队伍等级偏低、体力不够可能会出弹窗
await repeatOperationUntilTextFound({x: 1650,y: 1000,width: 160,height: 45,targetText: "开始挑战",stepDuration: 0,waitTime: 100,ifClick: true});//等待点击开始挑战
await sleep(2000);
await tpEndDetection();
keyDown("w");
await sleep(200);
keyDown("SHIFT");
await sleep(300);
keyUp("SHIFT");
await sleep(500);
keyDown("SHIFT");
await sleep(300);
keyUp("SHIFT");
await sleep(1000);
keyDown("SHIFT");
await sleep(300);
keyUp("SHIFT");
await sleep(500);
keyUp("w");
let challengeTime = 0;
//2分钟兜底
while (challengeTime < 5000) {
for (let i = 1;i < 5; i++) {
keyPress(i.toString());
await sleep(300);
leftButtonClick();
await sleep(400);
keyDown("e");
await sleep(400);
keyUp("e");
await sleep(500);
leftButtonClick();
await sleep(100);
let res = captureGameRegion().find(RecognitionObject.ocr(840, 935, 230, 40));
if (res.text.includes("自动退出")) {
log.info("检测到挑战成功");
return;
}
}
challengeTime = challengeTime + 200;
await sleep(100);
}
log.info("挑战超时,可能充能失败");
}
async function restoredEnergy() {
await genshin.returnMainUi();
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像,避免有倒下的角色
await restoredEnergyAutoFightAndEndDetection();//一直战斗直到检测到结束
await restoredEnergyAutoFightAndEndDetection();//一直战斗直到检测到结束
log.info("能量充满,任务结束");
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
}
//征讨之花领奖(图标识别)
const autoNavigateToReward = async () => {
// 定义识别对象
const boxIconRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/box.png"));
const rewardTextRo = RecognitionObject.Ocr(1210, 515, 200, 50);//领奖区域检测
let advanceNum = 0;//前进次数
//调整为俯视视野
middleButtonClick();
await sleep(800);
moveMouseBy(0, 1030);
await sleep(400);
moveMouseBy(0, 920);
await sleep(400);
moveMouseBy(0, 710);
log.info("开始领奖");
while (true) {
// 1. 优先检查是否已到达领奖点
let captureRegion = captureGameRegion();
let rewardTextArea = captureRegion.DeriveCrop(1210, 515, 200, 50);
let rewardResult = rewardTextArea.find(RecognitionObject.ocrThis);
// 检测到特点文字则结束!!!
if (rewardResult.text == "接触征讨之花") {
log.info(`总计前进第${advanceNum}`);
log.info("已到达领奖点,检测到文字: " + rewardResult.text);
return;
}
else if(advanceNum > 150){
log.info(`总计前进第${advanceNum}`);
throw new Error('前进时间超时');
}
// 2. 未到达领奖点,则调整视野
for(let i = 0; i < 100; i++){
captureRegion = captureGameRegion();
let iconRes = captureRegion.Find(boxIconRo);
let climbTextArea = captureRegion.DeriveCrop(1685, 1030, 65, 25);
let climbResult = climbTextArea.find(RecognitionObject.ocrThis);
// 检查是否处于攀爬状态
if (climbResult.text == "Space"){
log.info("检侧进入攀爬状态,尝试脱离");
keyPress("x");
await sleep(1000);
keyDown("a");
await sleep(800);
keyUp("a");
keyDown("w");
await sleep(800);
keyUp("w");
}
if (iconRes.x >= 920 && iconRes.x <= 980 && iconRes.y <= 540) {
advanceNum++;
break;
} else {
// 小幅度调整
if(iconRes.y >= 520) moveMouseBy(0, 920);
let adjustAmount = iconRes.x < 920 ? -20 : 20;
let distanceToCenter = Math.abs(iconRes.x - 920); // 计算与920的距离
let scaleFactor = Math.max(1, Math.floor(distanceToCenter / 50)); // 根据距离缩放最小为1
let adjustAmount2 = iconRes.y < 540 ? scaleFactor : 10;
moveMouseBy(adjustAmount * adjustAmount2, 0);
await sleep(100);
}
if(i > 20) throw new Error('视野调整超时');
}
// 3. 前进一小步
keyDown("w");
await sleep(200);
keyUp("w");
}
}
//检查是否为正整数
function positiveIntegerJudgment(testNumber) {
// 如果输入是字符串,尝试转换为数字
if (typeof testNumber === 'string') {
// 移除可能存在的非数字字符(如空格、百分号等)
const cleaned = testNumber.replace(/[^\d]/g, '');
testNumber = parseInt(cleaned, 10);
}
// 检查是否为有效的数字
if (typeof testNumber !== 'number' || isNaN(testNumber)) {
throw new Error(`无效的值: ${testNumber} (必须为数字)`);
}
// 检查是否为整数
if (!Number.isInteger(testNumber)) {
throw new Error(`必须为整数: ${testNumber}`);
}
return testNumber;
}
//返回当前体力值await queryStaminaValue();
async function queryStaminaValue() {
try {
await genshin.returnMainUi();
await sleep(1000);
keyPress("F1");
await sleep(2000);
click(300, 540);
await sleep(1000);
click(1570, 203);
await sleep(1000);
let captureRegion = captureGameRegion();
let stamina = captureRegion.find(RecognitionObject.ocr(1580, 20, 210, 55));
// 改进的分割方法
const staminaText = stamina.text.replace(/\s/g, ''); // 移除所有空格
// 使用正则表达式匹配数字部分(包括/或可能被误识别为其他字符的情况)
const matches = staminaText.match(/(\d+)[^\d]+(\d+)/);
if (!matches || matches.length < 3) {
throw new Error("无法解析体力值格式");
}
const currentValue = matches[1]; // 第一个数字是当前体力值
let validatedStamina = positiveIntegerJudgment(currentValue);
log.info(`剩余体力为:${validatedStamina}`);
await genshin.returnMainUi();
return validatedStamina;
} catch (error) {
log.error(`体力识别失败,默认为零`);
await genshin.returnMainUi();
return 0;
}
}
//检测传送结束 await tpEndDetection();
async function tpEndDetection() {
const region1 = RecognitionObject.ocr(1690, 230, 75, 350);// 队伍名称区域
const region2 = RecognitionObject.ocr(872, 681, 180, 30);// 点击任意处关闭
let tpTime = 0;
await sleep(1500);//点击传送后等待一段时间避免误判
//最多30秒传送时间
while (tpTime < 300) {
let capture = captureGameRegion();
let res1 = capture.find(region1);
let res2 = capture.find(region2);
if (!res1.isEmpty()|| !res2.isEmpty()){
log.info("传送完成");
await sleep(1000);//传送结束后有僵直
click(960, 810);//点击任意处
await sleep(500);
return;
}
tpTime++;
await sleep(100);
}
throw new Error('传送时间超时');
}
/**
* 自动导航直到检测到指定文字
* @param {Object} options 配置选项
* @param {number} [options.x=1210] 检测区域左上角x坐标
* @param {number} [options.y=515] 检测区域左上角y坐标
* @param {number} [options.width=200] 检测区域宽度
* @param {number} [options.height=50] 检测区域高度
* @param {string|RegExp} [options.targetText="奖励"] 要检测的目标文字
* @param {number} [options.maxSteps=100] 最大检查次数
* @param {number} [options.stepDuration=200] 每步前进持续时间(ms)
* @param {number} [options.waitTime=10] 单次等待时间(ms)
* @param {string} [options.moveKey="w"] 前进按键
* @param {boolean} [options.ifClick=false] 是否点击
* @returns {Promise<void>}
* await repeatOperationUntilTextFound(); 默认F区域检测到任何文字即停止前进
* await repeatOperationUntilTextFound({targetText: "日落果"}); F区域检测到指定文字即停止前进
*await repeatOperationUntilTextFound({x: 10,y: 10,width: 100,height: 100,targetText: "奖励",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
*/
const repeatOperationUntilTextFound = async ({
//默认区域为单个F图标右边的文字最多6个
x = 1210,
y = 515,
width = 200,
height = 50,
targetText = null,
maxSteps = 100,
stepDuration = 200,
waitTime = 10,
moveKey = "w",
ifClick = false,
} = {}) => {
/**
* 转义正则表达式中的特殊字符
* @param {string} string 要转义的字符串
* @returns {string} 转义后的字符串
*/
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};
// 预编译正则表达式(如果是字符串则转换并转义)
const textPattern = typeof targetText === 'string'
? new RegExp(escapeRegExp(targetText))
: targetText;
let stepsTaken = 0;
while (stepsTaken <= maxSteps) {
// 1. 捕获游戏区域并裁剪出检测区域
const captureRegion = captureGameRegion();
const textArea = captureRegion.DeriveCrop(x, y, width, height);
// 2. 执行OCR识别
const ocrResult = textArea.find(RecognitionObject.ocrThis);
const hasAnyText = ocrResult.text.trim().length > 0;
const matchesTarget = targetText === null
? hasAnyText
: textPattern.test(ocrResult.text);
if (matchesTarget) {
log.info(`检测到${targetText === null ? '文字' : '目标文字'}: ${ocrResult.text}`);
await sleep(500);
if (ifClick) click(Math.round(x + width / 2), Math.round(y + height / 2));
return true;
}
// 4. 检查步数限制
if (stepsTaken >= maxSteps) {
throw new Error(`检查次数超过最大限制: ${maxSteps},未查询到文字"${targetText}"`);
}
// 5. 前进一小步
if (stepDuration != 0) {
keyDown(moveKey);
await sleep(stepDuration);
keyUp(moveKey);
}
await sleep(waitTime);
stepsTaken++;
}
}
/**
* 等待图片出现并点击
* @param {string} imageName 图片名称(不带.png后缀且在assets文件中
* @param {number} [timeout=10000] 超时时间毫秒默认10秒
* @param {number} [checkInterval=500] 检查间隔毫秒默认500毫秒
* @returns {Promise<void>}
* @throws 如果超时未找到图片则抛出错误
*/
// 使用示例:
// await waitAndClickImage("paimon_menu");
//
// (2) 自定义偏移量
// await waitAndClickImage("confirm_button", 700, 0);
const waitAndClickImage = async (
imageName,
extraWidth = 0,
extraHeight = 0,
ifClick = true,
timeout = 10000,
checkInterval = 500,
threshold = 0.8 // 新增阈值参数默认值0.8
) => {
const startTime = Date.now();
const imagePath = `assets/${imageName}.png`;
// 读取模板图片
const templateMat = file.ReadImageMatSync(imagePath);
// 创建识别对象使用默认阈值0.8
const recognitionObj = RecognitionObject.TemplateMatch(templateMat, 0, 0, 1920, 1080);
recognitionObj.threshold = threshold;
while (Date.now() - startTime < timeout) {
// 捕获游戏区域
const captureRegion = captureGameRegion();
// 查找图片
const result = captureRegion.Find(recognitionObj);
if (!result.isEmpty()) {
log.info(`找到图片 ${imageName},位置(${result.x}, ${result.y}),正在点击...`);
if (ifClick) click(result.x+extraWidth,result.y+extraHeight);
await sleep(300); // 点击后稍作等待
return true;
}
await sleep(checkInterval);
}
throw new Error(`等待图片 ${imageName} 超时`);
}
/**
* 在游戏画面中查找指定图片并在其附近进行OCR识别
* @param {string} imagePath - 模板图片路径
* @param {number} ocrWidth - OCR区域宽度
* @param {number} ocrHeight - OCR区域高度
* @param {number} offsetX - OCR区域相对于模板匹配结果的X偏移
* @param {number} offsetY - OCR区域相对于模板匹配结果的Y偏移
* @returns {Promise<string|boolean>} - 返回OCR识别结果失败返回false
*/
async function findImageAndOCR(imagePath, ocrWidth, ocrHeight, offsetX, offsetY) {
try {
// 1. 读取模板图片并创建识别对象
const templateMat = file.ReadImageMatSync(imagePath);
const templateRo = RecognitionObject.TemplateMatch(templateMat);
// 2. 捕获游戏区域并查找模板图片
const captureRegion = captureGameRegion();
const foundRegion = captureRegion.Find(templateRo);
if (foundRegion.isEmpty()) {
log.info(`未找到模板图片: ${imagePath}`);
return false;
}
log.info("找到模板图片,位置({x},{y})", foundRegion.x, foundRegion.y);
// 3. 计算OCR区域位置基于模板匹配结果的位置+偏移量)
const ocrX = foundRegion.x + offsetX;
const ocrY = foundRegion.y + offsetY;
// 4. 创建OCR识别对象并识别
const ocrRo = RecognitionObject.Ocr(ocrX, ocrY, ocrWidth, ocrHeight);
const ocrResult = captureRegion.Find(ocrRo);
if (ocrResult.isEmpty() || !ocrResult.text || ocrResult.text.trim() === "") {
log.info("OCR未识别到内容");
return false;
}
log.info("OCR识别结果: {text}", ocrResult.text);
return ocrResult.text.trim();
} catch (error) {
log.error("识别过程中出错: {error}", error);
return false;
}
}
//前往刷天赋书或者武器(必须保证在材料介绍页面)await gotoAutoDomain(imageName = "weaponDomain");
async function gotoAutoDomain(imageName = "bookDomain") {
await sleep(1000);
await waitAndClickImage(imageName);
try {
await repeatOperationUntilTextFound({x: 1640,y: 960,width: 200,height: 100,targetText: "传送",stepDuration: 0, maxSteps:25, waitTime:100,ifClick: true});//用来等待点击文字,10s等待
} catch (error) {
log.info("秘境未开启");
await genshin.returnMainUi();
throw new Error(`秘境未在开启时间,跳过执行`);
}
log.info("开始前往天赋本秘境");
await sleep(1000);
await tpEndDetection();
await sleep(3000);//枫丹天赋材料本门口有水晶碟,可能影响
await repeatOperationUntilTextFound();
keyPress("F");
await repeatOperationUntilTextFound({x: 1650,y: 1000,width: 160,height: 45,targetText: "单人挑战",stepDuration: 0,waitTime: 100});//等待点击单人挑战
await dispatcher.runTask(new SoloTask("AutoDomain"));
}
// 技能书与国家、行列位置的映射
const bookToPosition = {
// 蒙德
"自由": {country: "蒙德天赋", row: 0},
"抗争": {country: "蒙德天赋", row: 1},
"诗文": {country: "蒙德天赋", row: 2},
// 璃月
"繁荣": {country: "璃月天赋", row: 0},
"勤劳": {country: "璃月天赋", row: 1},
"黄金": {country: "璃月天赋", row: 2},
// 稻妻
"浮世": {country: "稻妻天赋", row: 0},
"风雅": {country: "稻妻天赋", row: 1},
"天光": {country: "稻妻天赋", row: 2},
// 须弥
"净言": {country: "须弥天赋", row: 0},
"巧思": {country: "须弥天赋", row: 1},
"笃行": {country: "须弥天赋", row: 2},
// 枫丹
"公平": {country: "枫丹天赋", row: 0},
"正义": {country: "枫丹天赋", row: 1},
"秩序": {country: "枫丹天赋", row: 2},
// 纳塔
"角逐": {country: "纳塔天赋", row: 0},
"焚燔": {country: "纳塔天赋", row: 1},
"纷争": {country: "纳塔天赋", row: 2}
};
// 品质对应的列位置
const qualityPositions = [
{x: 1101, y: 0}, // 绿色 (0,0)
{x: 1180, y: 0}, // 蓝色 (0,1)
{x: 1260, y: 0} // 紫色 (0,2)
];
// 武器材料与国家、行列位置的映射
const weaponMaterialToPosition = {
// 蒙德
"高塔孤王": {country: "蒙德武器", row: 0},
"凛风奔狼": {country: "蒙德武器", row: 1},
"狮牙斗士": {country: "蒙德武器", row: 2},
// 璃月
"孤云寒林": {country: "璃月武器", row: 0},
"雾海云间": {country: "璃月武器", row: 1},
"漆黑陨铁": {country: "璃月武器", row: 2},
// 稻妻
"远海夷地": {country: "稻妻武器", row: 0},
"鸣神御灵": {country: "稻妻武器", row: 1},
"今昔剧话": {country: "稻妻武器", row: 2},
// 须弥
"谧林涓露": {country: "须弥武器", row: 0},
"绿洲花园": {country: "须弥武器", row: 1},
"烈日威权": {country: "须弥武器", row: 2},
// 枫丹
"幽谷弦音": {country: "枫丹武器", row: 0},
"纯圣露滴": {country: "枫丹武器", row: 1},
"无垢之海": {country: "枫丹武器", row: 2},
// 纳塔
"贡祭炽心": {country: "纳塔武器", row: 0},
"谵妄圣主": {country: "纳塔武器", row: 1},
"神合秘烟": {country: "纳塔武器", row: 2}
};
// 武器材料品质对应的列位置和品质名称4种品质
const weaponQualityPositions = [
{x: 1096, y: 0, quality: "绿色"}, // (0,0)
{x: 1178, y: 0, quality: "蓝色"}, // (0,1)
{x: 1259, y: 0, quality: "紫色"}, // (0,2)
{x: 1341, y: 0, quality: "金色"} // (0,3)
];
/**
* 获取指定技能书的材料数量
* @param {string} bookName 技能书名称
* @returns {Array<number>} 返回一个包含三个数字的数组,分别代表绿色、蓝色、紫色品质的材料数量
*/
async function getMaterialCount(bookName) {
// 检查输入的技能书名称是否有效
if (!bookToPosition.hasOwnProperty(bookName)) {
log.error("无效的技能书名称: " + bookName);
return [0, 0, 0];
}
const {country, row} = bookToPosition[bookName];
const results = [0, 0, 0];
try {
await genshin.returnMainUi();
await sleep(500);
keyPress("F1");
await repeatOperationUntilTextFound({x: 250,y: 420,width: 100,height: 60,targetText: "秘境",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await repeatOperationUntilTextFound({x: 415,y: 390,width: 300,height: 80,targetText: "天赋",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
// 1. 进入对应国家的副本
log.info(`正在点击${country}副本...`);
try {
await waitAndClickImage(country, 700, 35, true, 1000);
} catch (error) {
await sleep(500);
moveMouseTo(1600, 300);
leftButtonDown();
await sleep(500);
moveMouseTo(1600, 700);
await sleep(500);
moveMouseTo(1600, 500);
await sleep(100);
leftButtonUp();
await sleep(1000);
await waitAndClickImage(country, 700, 35, true, 3000);
}
// 等待加载
await sleep(1000);
// 2. 遍历三种品质的材料
for (let col = 0; col < 3; col++) {
// 计算点击位置 (使用你提供的点位信息)
const clickX = qualityPositions[col].x;
const clickY = 504 + row * 105; // 每行间隔约105像素
// 点击材料
log.info(`点击位置: (${clickX}, ${clickY})`);
click(clickX, clickY);
// 等待材料详情界面加载
await sleep(1500);
// 3. OCR识别数量
const result = await findImageAndOCR("assets/itemQuantityDetection.png", 200, 50, 0, 0);
if (result !== false) {
const quantity = positiveIntegerJudgment(result);
results[col] = quantity;
log.info(`识别到${["绿色", "蓝色", "紫色"][col]}品质材料数量: ${quantity}`);
} else {
log.warn("识别失败,将重试...");
// 简单重试机制
click(clickX, clickY);
await sleep(1500);
const retryResult = await findImageAndOCR("assets/itemQuantityDetection.png", 200, 50, 0, 0);
results[col] = retryResult !== false ? positiveIntegerJudgment(retryResult) : 0;
}
// 4. 点击空白处返回
if( col != 2 ) click(800, 10);
await sleep(1000);
}
return results;
} catch (error) {
log.error("获取材料数量时出错: " + error);
// 出错时尝试返回
click(800, 10);
await sleep(1000);
return results;
}
}
/**
* 获取指定武器材料的数量
* @param {string} materialName 武器材料名称
* @returns {Array<number>} 返回一个包含四个数字的数组,分别代表绿色、蓝色、紫色、金色品质的材料数量
*/
async function getWeaponMaterialCount(materialName) {
// 检查输入的武器材料名称是否有效
if (!weaponMaterialToPosition.hasOwnProperty(materialName)) {
log.error("无效的武器材料名称: " + materialName);
return [0, 0, 0, 0];
}
const {country, row} = weaponMaterialToPosition[materialName];
const results = [0, 0, 0, 0];
try {
await genshin.returnMainUi();
await sleep(500);
keyPress("F1");
await repeatOperationUntilTextFound({x: 250,y: 420,width: 100,height: 60,targetText: "秘境",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
await repeatOperationUntilTextFound({x: 415,y: 300,width: 300,height: 80,targetText: "武器",stepDuration: 0,waitTime: 100,ifClick: true});//用来等待点击文字,10s等待
// 1. 进入对应国家的副本
log.info(`正在点击${country}副本...`);
try {
await waitAndClickImage(country, 700, 35, true, 1000);
} catch (error) {
await sleep(500);
moveMouseTo(1600, 300);
leftButtonDown();
await sleep(500);
moveMouseTo(1600, 700);
await sleep(500);
moveMouseTo(1600, 500);
await sleep(100);
leftButtonUp();
await sleep(1000);
await waitAndClickImage(country, 700, 35, true, 3000);
}
// 等待加载
await sleep(1000);
// 2. 遍历四种品质的材料
for (let col = 0; col < 4; col++) {
const {x, y, quality} = weaponQualityPositions[col];
const clickX = x;
const clickY = 502 + row * 107; // 每行间隔约107像素
// 点击材料
log.info(`点击${quality}品质材料位置: (${clickX}, ${clickY})`);
click(clickX, clickY);
// 等待材料详情界面加载
await sleep(1500);
// 3. OCR识别数量
const result = await findImageAndOCR("assets/itemQuantityDetection.png", 200, 50, 0, 0);
if (result !== false) {
const quantity = positiveIntegerJudgment(result);
results[col] = quantity;
log.info(`识别到${quality}品质材料数量: ${quantity}`);
} else {
log.warn(`${quality}品质识别失败,将重试...`);
// 简单重试机制
click(clickX, clickY);
await sleep(1500);
const retryResult = await findImageAndOCR("assets/itemQuantityDetection.png", 200, 50, 0, 0);
results[col] = retryResult !== false ? positiveIntegerJudgment(retryResult) : 0;
}
// 4. 点击空白处返回
if( col != 3 ) click(800, 10);
await sleep(500);
}
return {
green: results[0], // 绿色
blue: results[1], // 蓝色
purple: results[2], // 紫色
gold: results[3] // 金色
};
} catch (error) {
log.error("获取武器材料数量时出错: " + error);
// 出错时尝试返回
click(800, 10);
await sleep(1000);
return {
green: 0,
blue: 0,
purple: 0,
gold: 0
};
}
}
//去刷天赋书
async function getTalentBook(materialName) {
while(1){
log.info(`准备刷取天赋书,开始检查体力`);
let afterStamina = await queryStaminaValue();
if ( afterStamina >= 20 ){
try {
log.info(`体力充足,开始检测物品数量`);
const bookCounts = await getMaterialCount(materialName);
res = 0.12*(bookRequireCounts[0]-bookCounts[0])+0.36*(bookRequireCounts[1]-bookCounts[1])+(bookRequireCounts[2]-bookCounts[2]);
if(res>0){
log.info(`${materialName}天赋书大约还差${res.toFixed(2)}本紫色品质没有刷取`);
await gotoAutoDomain();
}
else {
notification.send(`${materialName}天赋书数量已经满足要求!!!`);
return;
}
}
catch (error) {
notification.send(`${materialName}天赋书刷取失败,错误信息: ${error}`);
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
}
}
else{
notification.send(`体力值为${afterStamina},可能无法刷取${materialName}天赋书`);
return;
}
}
}
//去刷武器材料
async function getWeaponMaterial(materialName) {
while(1){
log.info(`准备刷取武器材料,开始检查体力`);
let afterStamina = await queryStaminaValue();
if ( afterStamina >= 20 ){
try {
log.info(`体力充足,开始检测物品数量`);
const weaponCounts = await getWeaponMaterialCount(materialName);
res = 0.12*(weaponRequireCounts[0]-weaponCounts.green)+0.36*(weaponRequireCounts[1]-weaponCounts.blue)+(weaponRequireCounts[2]-weaponCounts.purple)+3*(weaponRequireCounts[3]-weaponCounts.gold);
if(res>0){
log.info(`武器材料${materialName}大约还差${res.toFixed(2)}个紫色品质没有刷取`);
await gotoAutoDomain("weaponDomain");
}
else notification.send(`武器材料${materialName}数量已经满足要求!!!`);
}
catch (error) {
notification.send(`武器材料${materialName}刷取失败,错误信息: ${error}`);
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
return;
}
}
else{
notification.send(`体力值为${afterStamina},可能无法刷取武器材料${materialName}`);
return;
}
}
}
//去刷boss材料
async function getBossMaterial(bossName) {
while(1){
log.info(`准备刷取 boss 材料,开始检查体力`);
let afterStamina = await queryStaminaValue();
if ( afterStamina >= 40 ){
try {
log.info(`体力充足,开始检测物品数量`);
const bossCounts = await getBossMaterialCount(bossName);
let res = settings.bossRequireCounts-bossCounts;
if(res>0){
log.info(`${bossName}还差${res}个材料没有刷取`);
if(!settings.teamName) throw new Error('未输入队伍名称');
await genshin.returnMainUi();
await genshin.switchParty(settings.teamName);
if(settings.energyMax) await restoredEnergy();
else await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
log.info(`前往讨伐${bossName}`);
await pathingScript.runFile(`assets/goToBoss/${bossName}前往.json`);
await keyMouseScript.runFile(`assets/goToBoss/${bossName}前往键鼠.json`);
await sleep(1000);
log.info(`开始战斗`);
try {
await dispatcher.runTask(new SoloTask("AutoFight"));
} catch (error) {
//失败后最多只挑战一次,因为两次都打不过,基本上没戏,干脆直接报错结束
log.info(`挑战失败,再来一次`);
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
await pathingScript.runFile(`assets/goToBoss/${bossName}前往.json`);
await keyMouseScript.runFile(`assets/goToBoss/${bossName}前往键鼠.json`);
await dispatcher.runTask(new SoloTask("AutoFight"));
}
await sleep(1000);
log.info(`战斗结束,开始领奖`);
await autoNavigateToReward();//前往地脉之花
await sleep(600);
keyPress("F");
await sleep(800);
click(968, 759);//消耗树脂领取
await sleep(3000);
click(975, 1000);//点击空白区域
await sleep(1000);//等待 boss 刷新
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
log.info(`首领讨伐结束`);
}
else {
notification.send(`${bossName}材料数量已经满足要求!!!`);
return;
}
}
catch (error) {
notification.send(`${bossName}刷取失败,错误信息: ${error}`);
await genshin.tp(2297.6201171875,-824.5869140625);//传送到神像回血
return;
}
}
else{
notification.send(`体力值为${afterStamina},可能无法刷取武器材料${bossName}`);
return;
}
}
}
function parseAndValidateCounts(input, expectedCount) {
// 检查输入是否为字符串
if (typeof input !== 'string') {
throw new Error(`Input must be a string, got ${typeof input}`);
}
// 分割字符串
const parts = input.split('-');
// 初始化结果数组
const result = [];
// 处理每个部分
for (let i = 0; i < expectedCount; i++) {
if (i < parts.length) {
// 尝试转换为数字
const num = parseInt(parts[i], 10);
// 验证是否为有效正整数包括0
if (isNaN(num) || num < 0 || !Number.isInteger(num)) {
throw new Error(`Invalid number at position ${i}: '${parts[i]}'. Must be a non-negative integer.`);
}
result.push(num);
} else {
// 不足的部分补0
result.push(0);
}
}
return result;
}
let weaponRequireCounts;
let bookRequireCounts;
if(!settings.unfairContractTerms) throw new Error('未签署霸王条款,无法使用');
if(settings.talentBookName != "无" && settings.talentBookName){
try{
bookRequireCounts = parseAndValidateCounts(settings.talentBookRequireCounts, 3);
log.info(`天赋书方案解析成功: ${bookRequireCounts.join(', ')}`);
await getTalentBook(settings.talentBookName)}
catch (error) { notification.send(`${settings.talentBookName}刷取失败,错误信息: ${error}`);}
}
else log.info(`没有选择刷取天赋书,跳过执行`);
if(settings.weaponName != "无" && settings.weaponName){
try{
weaponRequireCounts = parseAndValidateCounts(settings.weaponMaterialRequireCounts, 4);
log.info(`武器材料方案解析成功: ${weaponRequireCounts.join(', ')}`);
await getWeaponMaterial(settings.weaponName)}
catch (error) { notification.send(`${settings.weaponName}刷取失败,错误信息: ${error}`);}
}
else log.info(`没有选择刷取武器材料,跳过执行`);
if(settings.bossName != "无" && settings.bossName){
try{await getBossMaterial(settings.bossName)}
catch (error) { notification.send(`${settings.bossName}刷取失败,错误信息: ${error}`);}
}
else log.info(`没有选择挑战首领,跳过执行`);
})();