js:锄地一条龙1.0版本 (#1297)

This commit is contained in:
mno
2025-07-07 19:48:43 +08:00
committed by GitHub
parent 0ac5e57c34
commit 85e6137402
355 changed files with 54384 additions and 0 deletions

View File

@@ -0,0 +1,990 @@
//拾取时上下滑动的时间
const timeMoveUp = 300;
const timeMoveDown = 1200;
(async function () {
//自定义配置处理
const operationMode = settings.operationMode || "运行锄地路线";
const k = settings.efficiencyIndex || 0.5;
const targetEliteNum = settings.targetEliteNum || 400;
const targetMonsterNum = settings.targetMonsterNum || 2000;
const partyName = settings.partyName || "";
// 获取 settings 中的标签,如果没有则使用默认值
let group1Tags = (settings.tagsForGroup1 || "蕈兽").split("").filter(Boolean);
const group2Tags = (settings.tagsForGroup2 || "").split("").filter(Boolean);
const group3Tags = (settings.tagsForGroup3 || "").split("").filter(Boolean);
const group4Tags = (settings.tagsForGroup4 || "").split("").filter(Boolean);
// 将 group2Tags、group3Tags 和 group4Tags 的内容添加到 group1Tags 中,并去除重复项
group1Tags = [...new Set([...group1Tags, ...group2Tags, ...group3Tags, ...group4Tags])];
const excludeTags = (settings.excludeTags || "").split("").map(tag => tag.trim()).filter(tag => tag.length > 0);
const accountName = settings.accountName || "默认账户";
// 拾取黑白名单处理
const ocrPickupContent = await file.readText("assets/拾取名单.json");
const ocrPickupJson = JSON.parse(ocrPickupContent);
const targetTexts = ocrPickupJson["白名单"];
const blacklistKeywords = ocrPickupJson["黑名单"];
if (!settings.accountName) {
for (let i = 0; i < 30; i++) {
log.error("你没有打开过自定义配置");
log.error("请先阅读README.md后再使用");
await sleep(2000);
}
}
//预处理路线并建立对象
log.info("开始预处理地图追踪文件");
pathings = await processPathings();
log.info("开始合并index数据");
//优先使用index中的数据
await updatePathings("assets/index1.json");
await updatePathings("assets/index2.json");
//加载路线cd信息
await initializeCdTime(pathings, accountName)
//按照用户配置标记可用路线
log.info("开始按照用户配置标记可用路线");
await markPathings(pathings, group1Tags, group2Tags, group3Tags, group4Tags, excludeTags);
//找出最优组合
log.info("开始寻找最优组合");
await findBestRouteGroups(pathings, k, targetEliteNum, targetMonsterNum);
//分配到不同路径组
log.info("开始分配到不同路径组");
groupCounts = await assignGroups(pathings, group1Tags, group2Tags, group3Tags, group4Tags);
/*
pathings.forEach((pathing, index) => {
log.info(`路径 ${index + 1}:`);
log.info(` fullPath: ${pathing.fullPath}`);
log.info(` fileName: ${pathing.fileName}`);
log.info(` group: ${pathing.group}`);
log.info(` cdTime: ${pathing.cdTime}`);
log.info(` tags: ${pathing.tags.join(', ')}`);
log.info(` available: ${pathing.available}`);
log.info(` selected: ${pathing.selected}`);
log.info(` 预计用时: ${pathing.t} 秒`);
log.info(` 普通怪物数量: ${pathing.m}`);
log.info(` 精英怪物数量: ${pathing.e}`);
log.info(` 普通怪物摩拉值: ${pathing.mora_m}`);
log.info(` 精英怪物摩拉值: ${pathing.mora_e}`);
log.info(` 是否使用index数据: ${pathing.usingIndex}`);
log.info(''); // 空行分隔每个路径的信息
});
*/
//根据操作模式选择不同的处理方式
if (operationMode === "输出地图追踪文件") {
log.info("开始复制并输出地图追踪文件\n请前往js文件夹查看");
await copyPathingsByGroup(pathings);
} else if (operationMode === "运行锄地路线") {
await switchPartyIfNeeded(partyName)
log.info("开始运行锄地路线");
await processPathingsByGroup(pathings, targetTexts, blacklistKeywords, accountName);
} else {
log.info("开始强制刷新CD");
await initializeCdTime(pathings, "");
await updateCdTimeRecord(pathings, accountName);
}
})();
//预处理路线,建立对象
async function processPathings() {
// 读取怪物信息
const monsterInfoContent = await file.readText("assets/monsterInfo.json");
const monsterInfoObject = JSON.parse(monsterInfoContent);
// 读取路径文件夹中的所有文件
let pathings = await readFolder("pathing", true);
// 定义解析 description 的函数
function parseDescription(desc) {
const routeInfo = {
time: 60, // 预计用时初始化为60秒
monsterInfo: {}
};
// 正则表达式匹配预计用时
const timeMatch = desc.match(/预计用时([\d\.]+)秒/);
if (timeMatch) {
routeInfo.time = parseFloat(timeMatch[1]);
}
// 正则表达式匹配怪物信息
const monsterMatch = desc.match(/包含以下怪物:(.*?)。/);
if (monsterMatch) {
const monsterList = monsterMatch[1].split('、');
monsterList.forEach(monsterStr => {
const [countStr, monsterName] = monsterStr.split('只');
const count = parseInt(countStr.trim(), 10);
routeInfo.monsterInfo[monsterName.trim()] = count;
});
}
return routeInfo;
}
let index = 0
// 遍历每个路径文件并处理
for (const pathing of pathings) {
pathing.usingIndex = false;
index++;
pathing.index = index;
const pathingContent = await file.readText(pathing.fullPath);
const parsedContent = JSON.parse(pathingContent);
const description = parsedContent.info?.description || "";
pathing.description = description;
pathing.tags = parsedContent.info?.tags || [];
// 解析 description 获取预计用时和怪物信息
const routeInfo = parseDescription(description);
// 初始化 pathing 对象的属性
pathing.t = routeInfo.time; // 预计用时初始化为60秒如果 description 中有值则覆盖
pathing.m = 0; // 普通怪物数量
pathing.e = 0; // 精英怪物数量
pathing.mora_m = 0; // 普通怪物摩拉值
pathing.mora_e = 0; // 精英怪物摩拉值
// 处理怪物信息
for (const [monsterName, count] of Object.entries(routeInfo.monsterInfo)) {
const monster = monsterInfoObject.find(m => m.name === monsterName);
if (monster) {
if (monster.type === "普通") {
pathing.m += count; // 增加普通怪物数量
pathing.mora_m += count * 40.5 * monster.moraRate; // 增加普通怪物摩拉值
} else if (monster.type === "精英") {
pathing.e += count; // 增加精英怪物数量
pathing.mora_e += count * 200 * monster.moraRate; // 增加精英怪物摩拉值
}
// 添加标签
if (monster.tags && monster.tags.length > 0) {
pathing.tags.push(...monster.tags);
}
}
}
// 去除重复标签
pathing.tags = [...new Set(pathing.tags)];
}
return pathings; // 返回处理后的 pathings 数组
}
async function markPathings(pathings, group1Tags, group2Tags, group3Tags, group4Tags, excludeTags) {
// 找出存在于 group1Tags 中且不在其他组标签中的标签
const uniqueTags = group1Tags.filter(tag => {
return !group2Tags.includes(tag) && !group3Tags.includes(tag) && !group4Tags.includes(tag);
});
pathings.forEach(pathing => {
// 初始化 pathing.tags 和 pathing.monsterInfo 以确保它们存在
pathing.tags = pathing.tags || [];
pathing.monsterInfo = pathing.monsterInfo || {};
// 检查路径的 tags 是否包含 uniqueTags
const containsUniqueTag = uniqueTags.some(uniqueTag => pathing.tags.includes(uniqueTag));
// 检查 fullPath、tags 或 monsterInfo 是否包含 excludeTags 中的任意一个子字符串
const containsExcludeTag = excludeTags.some(excludeTag => {
// 检查 fullPath 是否包含 excludeTag
const fullPathContainsExcludeTag = pathing.fullPath && pathing.fullPath.includes(excludeTag);
// 检查 tags 是否包含 excludeTag
const tagsContainExcludeTag = pathing.tags.some(tag => tag.includes(excludeTag));
// 检查 monsterInfo 的键是否包含 excludeTag
const monsterInfoContainsExcludeTag = Object.keys(pathing.monsterInfo).some(monsterName => monsterName.includes(excludeTag));
// 返回是否包含任意一个 excludeTag
return fullPathContainsExcludeTag || tagsContainExcludeTag || monsterInfoContainsExcludeTag;
});
// 如果包含 uniqueTags 或 excludeTags则标记为 false否则标记为 true
pathing.available = !(containsUniqueTag || containsExcludeTag);
});
}
async function findBestRouteGroups(pathings, k, targetEliteNum, targetMonsterNum) {
// 初始化变量
let currentTargetEliteNum = targetEliteNum; // 当前目标精英怪数量
let iterationCount = 0; // 循环次数
// 初始化统计变量
let totalSelectedElites = 0; // 总精英怪数量
let totalSelectedMonsters = 0; // 总普通怪数量
let totalGainCombined = 0; // 总收益
let totalTimeCombined = 0; // 总耗时
// 遍历 pathings计算并添加 G1、G2、E1 和 E2 属性
pathings.forEach(pathing => {
pathing.selected = false; // 初始化 selected 属性为 false
// 计算 G1 和 E1
const G1 = pathing.mora_e + pathing.mora_m; // 进入一组的收益
pathing.G1 = G1;
pathing.E1 = pathing.e === 0 ? 0 : (G1 / pathing.e) ** k * (G1 / pathing.t); // 进入一组的效率
// 计算 G2 和 E2
const G2 = pathing.mora_m; // 进入二组的收益
pathing.G2 = G2;
pathing.E2 = pathing.m === 0 ? 0 : (G2 / pathing.m) ** k * (G2 / pathing.t); // 进入二组的效率
});
// 封装第一轮选择逻辑
function selectRoutesByEliteTarget(targetEliteNum) {
// 重置选中状态和统计变量
pathings.forEach(pathing => pathing.selected = false); // 每轮循环前重置选中状态
totalSelectedElites = 0; // 重置总精英怪数量
totalSelectedMonsters = 0; // 重置总普通怪数量
totalGainCombined = 0; // 重置总收益
totalTimeCombined = 0; // 重置总耗时
// 按 E1 从高到低排序
pathings.sort((a, b) => b.E1 - a.E1);
// 第一轮选择:根据当前目标精英怪数量选择路径
for (const pathing of pathings) {
if (pathing.E1 > 0 && pathing.available && totalSelectedElites < targetEliteNum) {
pathing.selected = true;
totalSelectedElites += pathing.e;
totalSelectedMonsters += pathing.m;
totalGainCombined += pathing.G1;
totalTimeCombined += pathing.t;
}
}
}
// 封装第二轮选择逻辑
function selectRoutesByMonsterTarget(targetMonsterNum) {
// 按 E2 从高到低排序
pathings.sort((a, b) => b.E2 - a.E2);
// 第二轮选择:根据剩余的普通怪数量目标选择路径
for (const pathing of pathings) {
if (pathing.E2 > 0 && pathing.available && !pathing.selected && totalSelectedMonsters < targetMonsterNum) {
pathing.selected = true;
totalSelectedElites += pathing.e; // 第二轮选择中也可能包含精英怪
totalSelectedMonsters += pathing.m;
totalGainCombined += pathing.G2;
totalTimeCombined += pathing.t;
}
}
}
// 循环调整目标精英怪数量
while (iterationCount < 5) {
// 第一轮选择
selectRoutesByEliteTarget(currentTargetEliteNum);
// 第二轮选择:直接传入剩余的小怪数量目标
selectRoutesByMonsterTarget(targetMonsterNum);
// 检查精英怪总数是否满足条件
const diff = totalSelectedElites - targetEliteNum;
currentTargetEliteNum -= Math.round(0.6 * diff); // 调整目标精英怪数量,乘以系数并取整
if (totalSelectedElites === targetEliteNum) {
break; // 如果满足目标,直接终止循环
}
iterationCount++; // 增加循环次数
}
// 为最终选中且精英怪数量为0的路线添加小怪标签
pathings.forEach(pathing => {
if (pathing.selected && pathing.e === 0) {
pathing.tags.push("小怪");
}
});
// 按原始索引排序
pathings.sort((a, b) => a.index - b.index);
// 输出日志信息
log.info(`总精英怪数量: ${totalSelectedElites}`);
log.info(`总普通怪数量: ${totalSelectedMonsters}`);
log.info(`总收益: ${totalGainCombined} 摩拉`);
// 将总用时转换为时、分、秒表示
const hours = Math.floor(totalTimeCombined / 3600);
const minutes = Math.floor((totalTimeCombined % 3600) / 60);
const seconds = totalTimeCombined % 60;
log.info(`总用时: ${hours}${minutes}${seconds.toFixed(0)}`);
}
async function assignGroups(pathings, group1Tags, group2Tags, group3Tags, group4Tags) {
// 初始化记录各组路线数量的对象
const groupCounts = {
0: 0, // 默认组
1: 0, // 不包含 group1Tags 的组
2: 0, // 包含 group1Tags 且包含 group2Tags 的组
3: 0, // 包含 group1Tags 但不包含 group2Tags包含 group3Tags 的组
4: 0 // 包含 group1Tags 但不包含 group2Tags 和 group3Tags包含 group4Tags 的组
};
// 遍历 pathings 数组
pathings.forEach(pathing => {
// 只处理 selected 为 true 的项
if (pathing.selected) {
// 默认 group 为 0
pathing.group = 0;
// 如果 tags 不包含 group1Tags 中的任意一个,则改为 1
if (!group1Tags.some(tag => pathing.tags.includes(tag))) {
pathing.group = 1;
} else {
// 如果包含 group1Tags 中的任意一个,则检查 group2Tags
if (group2Tags.some(tag => pathing.tags.includes(tag))) {
pathing.group = 2;
} else {
// 如果包含 group1Tags 但不包含 group2Tags则检查 group3Tags
if (group3Tags.some(tag => pathing.tags.includes(tag))) {
pathing.group = 3;
} else {
// 如果包含 group1Tags 但不包含 group2Tags 和 group3Tags则检查 group4Tags
if (group4Tags.some(tag => pathing.tags.includes(tag))) {
pathing.group = 4;
}
}
}
}
// 更新对应的组计数
groupCounts[pathing.group]++;
}
});
// 返回组计数对象
return groupCounts;
}
async function runPathWithOcr(pathFilePath, targetTexts, blacklistKeywords) {
// 定义替换映射表
const replacementMap = {
"监": "盐",
"卵": "卯"
};
let thisMoveUpTime = 0;
let lastMoveUp = 0;
// 定义状态变量
let state = { completed: false, cancelRequested: false };
// 定义图像路径和目标文本列表
const imagePath = `assets/F_Dialogue.png`;
const textxRange = { min: 1210, max: 1412 };
const texttolerance = 30; // Y 坐标容错范围
// 定义一个函数用于执行路径文件
async function executePathFile(filePath) {
try {
await pathingScript.runFile(filePath);
await sleep(1);
} catch (error) {
log.error(`执行路径文件时发生错误:${error.message}`);
state.cancelRequested = true; // 修改状态变量
}
log.info(`路径文件 ${filePath} 执行完成`);
state.completed = true; // 修改状态变量
}
// 定义一个函数用于执行OCR识别和交互
async function performOcrAndInteract(imagePath, targetTexts, textxRange, texttolerance) {
async function performOcr(targetTexts, xRange, yRange, timeout = 200) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 在捕获的区域内进行OCR识别
let ra = captureGameRegion();
let resList = ra.findMulti(RecognitionObject.ocr(
xRange.min, yRange.min,
xRange.max - xRange.min, yRange.max - yRange.min
));
// 遍历识别结果,检查是否找到目标文本
let results = [];
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);
}
// 如果 targetTexts 为空,则直接将所有文本视为匹配
if (targetTexts.length === 0) {
results.push({ text: correctedText, x: res.x, y: res.y, width: res.width, height: res.height });
} else {
// 否则,检查是否包含目标文本
for (let targetText of targetTexts) {
if (correctedText.includes(targetText)) {
results.push({ text: correctedText, x: res.x, y: res.y, width: res.width, height: res.height });
break; // 匹配到一个目标文本后即可跳出循环
}
}
}
}
return results;
} catch (error) {
log.error(`识别文字时发生异常: ${error.message}`);
return [];
}
}
log.warn("OCR识别超时");
return [];
}
while (!state.completed && !state.cancelRequested) {
// 尝试找到 F 图标并返回其坐标
async function findFIcon(imagePath, xMin, yMin, width, height, timeout = 500) {
let startTime = Date.now();
while (Date.now() - startTime < timeout && !state.cancelRequested) {
try {
let template = file.ReadImageMatSync(imagePath);
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 (error) {
log.error(`识别图像时发生异常: ${error.message}`);
if (state.cancelRequested) {
break; // 如果请求了取消,则退出循环
}
return null;
}
await sleep(2); // 每次检测间隔 2 毫秒
}
if (state.cancelRequested) {
log.info("图像识别任务已取消");
}
return null;
}
//检查是否在主界面
async function isMainUI() {
// 修改后的图像路径
const imagePath = "assets/MainUI.png";
// 修改后的识别区域(左上角区域)
const xMin = 0;
const yMin = 0;
const width = 150; // 识别区域宽度
const height = 150; // 识别区域高度
// 尝试次数设置为 2 次
const maxAttempts = 2;
let attempts = 0;
while (attempts < maxAttempts && !state.cancelRequested) {
try {
let template = file.ReadImageMatSync(imagePath);
let recognitionObject = RecognitionObject.TemplateMatch(template, xMin, yMin, width, height);
let result = captureGameRegion().find(recognitionObject);
if (result.isExist()) {
return true; // 如果找到图标,返回 true
}
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
if (state.cancelRequested) {
break; // 如果请求了取消,则退出循环
}
return false; // 发生异常时返回 false
}
attempts++; // 增加尝试次数
await sleep(2); // 每次检测间隔 2 毫秒
}
if (state.cancelRequested) {
log.info("图像识别任务已取消");
}
return false; // 如果尝试次数达到上限或取消,返回 false
}
// 尝试找到 F 图标
let fRes = await findFIcon(imagePath, 1102, 335, 34, 400, 200);
if (!fRes) {
if (await isMainUI()) {
//log.info("在主界面,尝试下滑");
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
}
continue;
}
// 获取 F 图标的中心点 Y 坐标
let centerYF = fRes.y + fRes.height / 2;
// 在当前屏幕范围内进行 OCR 识别
let ocrResults = await performOcr(targetTexts, textxRange, { min: fRes.y - texttolerance, max: fRes.y + fRes.height + texttolerance * 2 }, 200);
// 检查所有目标文本是否在当前页面中
let foundTarget = false;
for (let ocrResult of ocrResults) {
// 检查是否包含黑名单关键词
let containsBlacklistKeyword = blacklistKeywords.some(blacklistKeyword => ocrResult.text.includes(blacklistKeyword));
if (containsBlacklistKeyword) {
log.debug(`包含黑名单,不拾取: ${ocrResult.text}`);
continue;
}
log.info(`交互或拾取: "${ocrResult.text}"`);
// 获取当前时间并格式化为 HH:mm:ss.sss 格式
function formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}
// 获取当前时间
const now = new Date();
// 格式化当前时间
const formattedTime = formatTime(now);
// 输出日志
log.debug(`[${formattedTime}][INF] BetterGenshinImpact.GameTask.AutoPick.AutoPickTrigger\n交互或拾取:"${ocrResult.text}"`);
// 计算目标文本的中心Y坐标
let centerYTargetText = ocrResult.y + ocrResult.height / 2;
if (Math.abs(centerYTargetText - centerYF) <= texttolerance) {
keyPress("F"); // 执行交互操作
await sleep(5); // 操作后暂停 5 毫秒
foundTarget = true;
break;
}
}
// 如果在当前页面中没有找到任何目标文本,则根据时间决定滚动方向
if (!foundTarget) {
const currentTime = new Date().getTime(); // 获取当前时间(毫秒)
// 如果距离上次上翻超过1秒则执行上翻
if (currentTime - lastMoveUp > timeMoveDown) {
await keyMouseScript.runFile(`assets/滚轮上翻.json`);
// 如果这是第一次上翻,记录这次上翻的时间
if (thisMoveUpTime === 0) {
thisMoveUpTime = currentTime; // 记录第一次上翻的时间
}
// 检查是否需要更新 lastMoveUp
if (currentTime - thisMoveUpTime >= timeMoveUp) {
lastMoveUp = currentTime; // 更新 lastMoveUp 为第一次上翻的时间
thisMoveUpTime = 0; // 重置 thisMoveUpTime以便下一次上翻时重新记录
}
} else {
// 否则执行下翻
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
}
}
if (state.cancelRequested) {
break;
}
}
}
// 启动路径文件执行任务
const pathTask = executePathFile(pathFilePath);
// 启动 OCR 检测和交互任务
const ocrTask = performOcrAndInteract(imagePath, targetTexts, textxRange, texttolerance);
// 等待两个任务都完成
try {
await Promise.allSettled([pathTask, ocrTask]);
} catch (error) {
log.error(`执行任务时发生错误:${error.message}`);
state.cancelRequested = true; // 设置取消标志
} finally {
state.cancelRequested = true; // 设置取消标志
}
}
// 定义 readFolder 函数
async function readFolder(folderPath, onlyJson) {
log.info(`开始读取文件夹:${folderPath}`);
// 新增一个堆栈,初始时包含 folderPath
const folderStack = [folderPath];
// 新增一个数组,用于存储文件信息对象
const files = [];
// 当堆栈不为空时,继续处理
while (folderStack.length > 0) {
// 从堆栈中弹出一个路径
const currentPath = folderStack.pop();
// 读取当前路径下的所有文件和子文件夹路径
const filesInSubFolder = file.ReadPathSync(currentPath);
// 临时数组,用于存储子文件夹路径
const subFolders = [];
for (const filePath of filesInSubFolder) {
if (file.IsFolder(filePath)) {
// 如果是文件夹,先存储到临时数组中
subFolders.push(filePath);
} else {
// 如果是文件,根据 onlyJson 判断是否存储
if (onlyJson) {
if (filePath.endsWith(".json")) {
const fileName = filePath.split('\\').pop(); // 提取文件名
const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组
files.push({
fullPath: filePath,
fileName: fileName,
folderPathArray: folderPathArray
});
//log.info(`找到 JSON 文件:${filePath}`);
}
} else {
const fileName = filePath.split('\\').pop(); // 提取文件名
const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组
files.push({
fullPath: filePath,
fileName: fileName,
folderPathArray: folderPathArray
});
//log.info(`找到文件:${filePath}`);
}
}
}
// 将临时数组中的子文件夹路径按原顺序压入堆栈
folderStack.push(...subFolders.reverse()); // 反转子文件夹路径
}
return files;
}
async function copyPathingsByGroup(pathings) {
// 遍历 pathings 数组
for (const pathing of pathings) {
// 只处理 selected 为 true 的项
if (pathing.selected) {
// 读取文件内容
const content = await file.readText(pathing.fullPath);
// 构造目标路径
const groupFolder = `pathingOut/group${pathing.group}`;
const targetPath = `${groupFolder}/${pathing.fileName}`;
// 写入文件内容
await file.writeText(targetPath, content, false);
}
}
}
async function processPathingsByGroup(pathings, targetTexts, blacklistKeywords, accountName) {
// 定义路径组名称到组号的映射
const groupMapping = {
"路径组一": 1,
"路径组二": 2,
"路径组三": 3,
"路径组四": 4
};
// 从全局 settings 中获取用户选择的路径组名称
const selectedGroupName = settings.groupIndex || "路径组一"; // 默认值为 "路径组一"
// 将路径组名称映射到组号
const targetGroup = groupMapping[selectedGroupName];
// 初始化变量,用于标记当前路径是该组的第几个
let groupPathCount = 0;
// 获取该组的总路径数
const totalPathsInGroup = pathings.filter(pathing => pathing.group === targetGroup).length;
if (settings.onlyArtifacts) {
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
}
// 初始化统计变量
let totalElites = 0; // 总精英怪数量
let totalMonsters = 0; // 总小怪数量
let totalGain = 0; // 总收益
let totalEstimatedTime = 0; // 预计总时间
// 遍历 pathings 数组,计算当前组的总计精英怪数量、小怪数量、总计收益和预计总时间
for (const pathing of pathings) {
if (pathing.group === targetGroup) {
totalElites += pathing.e || 0; // 精英怪数量
totalMonsters += pathing.m || 0; // 小怪数量
totalGain += pathing.G1 || 0; // 收益
totalEstimatedTime += pathing.t || 0; // 预计时间
}
}
// 输出当前组的总计信息
log.info(`当前组 ${selectedGroupName} 的总计信息:`);
log.info(`精英怪数量: ${totalElites}`);
log.info(`小怪数量: ${totalMonsters}`);
log.info(`预计收益: ${totalGain} 摩拉`);
// 将预计总时间转换为时、分、秒表示
const hours = Math.floor(totalEstimatedTime / 3600);
const minutes = Math.floor((totalEstimatedTime % 3600) / 60);
const seconds = totalEstimatedTime % 60;
log.info(`预计用时: ${hours}${minutes}${seconds.toFixed(0)}`);
// 遍历 pathings 数组
for (const pathing of pathings) {
// 检查路径是否属于指定的组
if (pathing.group === targetGroup) {
// 增加路径计数
groupPathCount++;
// 输出当前路径的序号信息
log.info(`当前路径 ${pathing.fileName} 是第 ${targetGroup} 组第 ${groupPathCount}/${totalPathsInGroup}`);
// 获取当前时间
const now = new Date();
// 检查 cdTime 是否晚于当前时间
const cdTime = new Date(pathing.cdTime);
if (cdTime > now) {
log.info(`该路线未刷新,跳过。`);
continue;
}
// 输出路径已刷新并开始处理的信息
log.info(`该路线已刷新,开始处理。`);
await fakeLog(`${pathing.fileName}`, false, true, 0);
if (!settings.onlyArtifacts) {
// 调用 runPathWithOcr 函数处理路径
await runPathWithOcr(pathing.fullPath, targetTexts, blacklistKeywords);
} else {
await pathingScript.runFile(pathing.fullPath);
}
try {
await sleep(1);
} catch (error) {
break;
}
await fakeLog(`${pathing.fileName}`, false, false, 0);
// 计算下一个 UTC 时间的晚上 8 点
const nextEightClock = new Date(now);
nextEightClock.setUTCHours(20, 0, 0, 0); // 设置为 UTC 时间的 20:00
if (nextEightClock <= now) {
// 如果设置的时间小于等于当前时间,说明需要取下一个晚上 8 点
nextEightClock.setUTCHours(20 + 24, 0, 0, 0); // 设置为下一个 UTC 时间的 20:00
}
// 更新路径的 cdTime
pathing.cdTime = nextEightClock.toLocaleString();
await updateCdTimeRecord(pathings, accountName)
// 输出路径的下次可用时间(本地时间格式)
log.info(`路径 ${pathing.fileName} 下次可用时间为 ${nextEightClock.toLocaleString()}`);
}
}
}
//加载cd信息
async function initializeCdTime(pathings, accountName) {
try {
// 构造文件路径
const filePath = `records/${accountName}.json`;
// 尝试读取文件内容
const fileContent = await file.readText(filePath);
// 解析 JSON 数据
const cdTimeData = JSON.parse(fileContent);
// 遍历 pathings 数组
pathings.forEach(pathing => {
// 找到对应的 cdTime 数据
const cdTimeEntry = cdTimeData.find(entry => entry.fileName === pathing.fileName);
// 如果找到对应的项,则更新 cdTime
if (cdTimeEntry) {
pathing.cdTime = new Date(cdTimeEntry.cdTime).toLocaleString();
} else {
// 如果没有找到对应的项,则使用默认值 new Date(0)
pathing.cdTime = new Date(0).toLocaleString();
}
});
} catch (error) {
pathings.forEach(pathing => {
pathing.cdTime = new Date(0).toLocaleString();
});
}
}
async function updateCdTimeRecord(pathings, accountName) {
try {
// 构造文件路径
const filePath = `records/${accountName}.json`;
// 构造要写入文件的 JSON 数据
const cdTimeData = pathings.map(pathing => ({
fileName: pathing.fileName,
//description: pathing.description,
cdTime: pathing.cdTime
}));
// 将更新后的内容写回文件
await file.writeText(filePath, JSON.stringify(cdTimeData, null, 2), false);
// 输出日志
log.info(`所有路径的 cdTime 已更新并保存到文件 ${filePath}`);
} catch (error) {
// 捕获并记录错误
log.error(`更新 cdTime 时出错: ${error.message}`);
}
}
// fakeLog 函数,使用方法:将本函数放在主函数前,调用时请务必使用await否则可能出现v8白框报错
//在js开头处伪造该js结束运行的日志信息如 await fakeLog("js脚本", true, true, 0);
//在js结尾处伪造该js开始运行的日志信息如 await fakeLog("js脚本", true, false, 2333);
//duration项目仅在伪造结束信息时有效且无实际作用可以任意填写当你需要在日志中输出特定值时才需要单位为毫秒
//在调用地图追踪前伪造该地图追踪开始运行的日志信息,如 await fakeLog(`地图追踪.json`, false, true, 0);
//在调用地图追踪后伪造该地图追踪结束运行的日志信息,如 await fakeLog(`地图追踪.json`, false, false, 0);
//如此便可以在js运行过程中伪造地图追踪的日志信息可以在日志分析等中查看
async function fakeLog(name, isJs, isStart, duration) {
await sleep(10);
const currentTime = Date.now();
// 参数检查
if (typeof name !== 'string') {
log.error("参数 'name' 必须是字符串类型!");
return;
}
if (typeof isJs !== 'boolean') {
log.error("参数 'isJs' 必须是布尔型!");
return;
}
if (typeof isStart !== 'boolean') {
log.error("参数 'isStart' 必须是布尔型!");
return;
}
if (typeof currentTime !== 'number' || !Number.isInteger(currentTime)) {
log.error("参数 'currentTime' 必须是整数!");
return;
}
if (typeof duration !== 'number' || !Number.isInteger(duration)) {
log.error("参数 'duration' 必须是整数!");
return;
}
// 将 currentTime 转换为 Date 对象并格式化为 HH:mm:ss.sss
const date = new Date(currentTime);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
const formattedTime = `${hours}:${minutes}:${seconds}.${milliseconds}`;
// 将 duration 转换为分钟和秒,并保留三位小数
const durationInSeconds = duration / 1000; // 转换为秒
const durationMinutes = Math.floor(durationInSeconds / 60);
const durationSeconds = (durationInSeconds % 60).toFixed(3); // 保留三位小数
// 使用四个独立的 if 语句处理四种情况
if (isJs && isStart) {
// 处理 isJs = true 且 isStart = true 的情况
const logMessage = `正在伪造js开始的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 开始执行JS脚本: "${name}"`;
log.debug(logMessage);
}
if (isJs && !isStart) {
// 处理 isJs = true 且 isStart = false 的情况
const logMessage = `正在伪造js结束的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}${durationSeconds}\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------`;
log.debug(logMessage);
}
if (!isJs && isStart) {
// 处理 isJs = false 且 isStart = true 的情况
const logMessage = `正在伪造地图追踪开始的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 开始执行地图追踪任务: "${name}"`;
log.debug(logMessage);
}
if (!isJs && !isStart) {
// 处理 isJs = false 且 isStart = false 的情况
const logMessage = `正在伪造地图追踪结束的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}${durationSeconds}\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------`;
log.debug(logMessage);
}
}
// 更新 pathings 的函数,接受索引文件路径作为参数
async function updatePathings(indexFilePath) {
try {
// 读取文件内容
const fileContent = await file.readText(indexFilePath);
// 将文件内容解析为 JSON 格式
const data = JSON.parse(fileContent);
// 遍历解析后的 JSON 数据
for (const item of data) {
// 检查 pathings 中是否存在某个对象的 fileName 属性与 item.fileName 相同
const existingPathing = pathings.find(pathing => pathing.fileName === item.fileName);
if (existingPathing) {
existingPathing.usingIndex = true;
// 直接覆盖其他字段,但先检查是否存在有效值
if (item.时间 !== undefined) existingPathing.t = item.时间;
if (item.精英摩拉 !== undefined) existingPathing.mora_e = item.精英摩拉;
if (item.小怪摩拉 !== undefined) existingPathing.mora_m = item.小怪摩拉;
if (item.小怪数量 !== undefined) existingPathing.m = item.小怪数量;
if (item.精英数量 !== undefined) existingPathing.e = item.精英数量;
// 使用 Set 来存储 tags避免重复项
const tagsSet = new Set(existingPathing.tags);
for (const key in item) {
if (key !== "fileName" && key !== "时间" && key !== "精英摩拉" && key !== "小怪摩拉" && key !== "小怪数量" && key !== "精英数量") {
if (item[key] === 1) {
tagsSet.add(key);
}
}
}
existingPathing.tags = Array.from(tagsSet);
}
}
} catch (error) {
log.error("Error:", error);
}
}
//切换队伍
async function switchPartyIfNeeded(partyName) {
if (!partyName) {
await genshin.returnMainUi();
return;
}
try {
log.info("正在尝试切换至" + partyName);
if (!await genshin.switchParty(partyName)) {
log.info("切换队伍失败,前往七天神像重试");
await genshin.tpToStatueOfTheSeven();
await genshin.switchParty(partyName);
}
} catch {
log.error("队伍切换失败,可能处于联机模式或其他不可切换状态");
notification.error(`队伍切换失败,可能处于联机模式或其他不可切换状态`);
await genshin.returnMainUi();
}
}