953 lines
38 KiB
JavaScript
953 lines
38 KiB
JavaScript
|
||
|
||
(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);//传送到神像回血
|
||
return;
|
||
}
|
||
}
|
||
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}数量已经满足要求!!!`);
|
||
return;
|
||
}
|
||
}
|
||
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(`没有选择挑战首领,跳过执行`);
|
||
|
||
|
||
})();
|