Files
bettergi-scripts-list/repo/js/AAA-Artifacts-Bulk-Supply/main.js
2025-08-12 10:07:57 +08:00

1024 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// 初始化自定义配置并赋予默认值
let artifactPartyName = settings.artifactPartyName || "狗粮";//狗粮队伍名称
let combatPartyName = settings.combatPartyName;//清怪队伍名称
let minIntervalTime = settings.minIntervalTime || 1;//最短间隔时间(分钟)
let maxWaitingTime = settings.maxWaitingTime || 0;//最大额外等待时间(分钟)
let forceAlternate = settings.forceAlternate;//强制交替
let onlyActivate = settings.onlyActivate;//只运行激活额外和收尾
let decomposeMode = settings.decomposeMode || "保留";//狗粮分解模式
let keep4Star = settings.keep4Star;//保留四星
let autoSalvage = settings.autoSalvage;//启用自动分解
let notify = settings.notify;//启用通知
let accountName = settings.accountName || "默认账户";//账户名
//文件路径
const ArtifactsButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/ArtifactsButton.png"));
const DeleteButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/DeleteButton.png"));
const AutoAddButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/AutoAddButton.png"));
const ConfirmButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/ConfirmButton.png"));
const DestoryButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/DestoryButton.png"));
const MidDestoryButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/DestoryButton.png"), 900, 600, 500, 300);
const CharacterMenuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/CharacterMenu.png"), 60, 991, 38, 38);
const normalPathA = "assets/ArtifactsPath/普通A";
const normalPathB = "assets/ArtifactsPath/普通B";
const normalPathC = "assets/ArtifactsPath/普通C";
const extraPath = "assets/ArtifactsPath/额外";
//初始化变量
let artifactExperienceDiff = 0;
let moraDiff = 0;
let state = {};
let record = {};
let CDInfo = [];
let failcount = 0;
(async function () {
setGameMetrics(1920, 1080, 1);
{
//校验自定义配置,从未打开过自定义配置时进行警告
if (!settings.accountName) {
for (let i = 0; i < 15; i++) {
log.warn("你从来没有打开过自定义配置请仔细阅读readme后使用");
await sleep(1000);
}
}
}
//预处理
await readRecord(accountName);//读取记录文件
const epochTime = new Date('1970-01-01T20:00:00.000Z');
const now = new Date();
state.runningRoute = Math.floor((now - epochTime) / (24 * 60 * 60 * 1000)) % 2 === 0 ? 'A' : 'B';//根据日期奇偶数确定普通路线
if (new Date() - record.lastActivateTime < 12 * 60 * 60) {
//距离上次激活不足12小时时启用备用C路线
//state.runningRoute = `C`;
}
state.currentParty = "";
state.cancel = false;
log.info(`今日运行普通${state.runningRoute}路线`);
if (state.runnedToday) {
await readCDInfo(accountName);
} else {
await readCDInfo("重置cd信息");
}
await writeCDInfo(accountName);
//更新日期信息
record.lastRunDate = `${new Date().getFullYear()}/${String(new Date().getMonth() + 1).padStart(2, '0')}/${String(new Date().getDate()).padStart(2, '0')}`;
await writeRecord(accountName);
//运行前按自定义配置清理狗粮
if (settings.decomposeMode === "分解(经验瓶)") {
await processArtifacts(21);
} else {
artifactExperienceDiff -= await processArtifacts(21);
}
moraDiff -= await mora();
//执行普通路线,直到预定激活开始时间
log.info("开始第一次执行普通路线");
await runNormalPath(true);
if (state.cancel) return;
//执行激活路线
log.info("开始执行激活路线");
await runActivatePath();
if (state.cancel) return;
//执行剩余普通路线
log.info("开始第二次执行普通路线");
await runNormalPath(false);
if (state.cancel) return;
//执行收尾和额外路线
await runEndingAndExtraPath();
if (state.cancel) return;
//运行后按自定义配置清理狗粮
artifactExperienceDiff += await processArtifacts(21);
moraDiff += await mora();
log.info(`狗粮路线获取摩拉: ${moraDiff}`);
log.info(`狗粮路线获取狗粮经验: ${artifactExperienceDiff}`);
//修改records
for (let i = record.records.length - 1; i > 0; i--) {
record.records[i] = record.records[i - 1];
}
record.records[0] = `日期:${record.lastRunDate},运行收尾路线${record.lastRunEndingRoute},狗粮经验${artifactExperienceDiff},摩拉${moraDiff}`;
if (settings.notify) {
notification.Send(`日期:${record.lastRunDate},运行收尾路线${record.lastRunEndingRoute},狗粮经验${artifactExperienceDiff},摩拉${moraDiff}`);
}
await writeRecord(accountName);//修改记录文件
})();
async function readRecord(accountName) {
/* ---------- 文件名合法性校验 ---------- */
const illegalCharacters = /[\\/:*?"<>|]/;
const reservedNames = [
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
];
let finalAccountName = accountName;
if (accountName === "" ||
accountName.startsWith(" ") ||
accountName.endsWith(" ") ||
illegalCharacters.test(accountName) ||
reservedNames.includes(accountName.toUpperCase()) ||
accountName.length > 255
) {
log.error(`账户名 "${accountName}" 不合法,将使用默认值`);
finalAccountName = "默认账户";
await sleep(5000);
} else {
log.info(`账户名 "${accountName}" 合法`);
}
/* ---------- 读取记录文件 ---------- */
const recordFolderPath = "records/";
const recordFilePath = `records/${finalAccountName}.txt`;
const filesInSubFolder = file.ReadPathSync(recordFolderPath);
let fileExists = false;
for (const filePath of filesInSubFolder) {
if (filePath === `records\\${accountName}.txt`) {
fileExists = true;
break;
}
}
/* ---------- 初始化记录对象 ---------- */
record = {
lastRunDate: "1970/01/01",
lastActivateTime: new Date("1970-01-01T20:00:00.000Z"),
lastRunEndingRoute: "收尾额外A",
records: new Array(14).fill(""),
version: ""
};
let recordIndex = 0;
if (fileExists) {
log.info(`记录文件 ${recordFilePath} 存在`);
} else {
log.warn(`无记录文件,将使用默认数据`);
return;
}
const content = await file.readText(recordFilePath);
const lines = content.split("\n");
/* ---------- 逐行解析 ---------- */
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
/* 运行完成日期 */
if (line.startsWith("上次运行日期:")) {
record.lastRunDate = line.slice("上次运行日期:".length).trim();
}
/* 结束时间 / 激活收尾额外A时间视为同一含义 */
let timeStr = null;
if (line.startsWith("上次结束时间:")) {
timeStr = line.slice("上次结束时间:".length).trim();
} else if (line.startsWith("上次激活收尾路线时间:")) {
timeStr = line.slice("上次激活收尾路线时间:".length).trim();
}
if (timeStr) {
const d = new Date(timeStr);
if (!isNaN(d.getTime())) {
record.lastActivateTime = d; // 保持 Date 对象
}
}
/* 收尾路线 */
if (line.startsWith("上次运行收尾路线:")) {
record.lastRunEndingRoute = line.slice("上次运行收尾路线:".length).trim();
}
if (record.lastRunEndingRoute !== "收尾额外B") {
record.lastRunEndingRoute = "收尾额外A";
}
if (line.startsWith("日期") && recordIndex < record.records.length) {
record.records[recordIndex++] = line;
}
}
log.info(`上次运行日期: ${record.lastRunDate}`);
log.info(`上次激活路线开始时间: ${record.lastActivateTime.toLocaleString()}`);
/* ---------- 读取 manifest 版本 ---------- */
try {
const manifest = JSON.parse(await file.readText("manifest.json"));
record.version = manifest.version;
log.info(`当前版本为${record.version}`);
} catch (err) {
log.error("读取或解析 manifest.json 失败:", err);
}
/* ---------- 判断今日是否运行 ---------- */
if (record.lastRunDate) {
const [y, m, d] = record.lastRunDate.split("/").map(Number);
const lastRun4AM = new Date(y, m - 1, d, 4, 0, 0);
const now = new Date();
if (now - lastRun4AM < 24 * 60 * 60 * 1000) {
log.info("今日已经运行过狗粮");
state.runnedToday = true;
} else {
state.runnedToday = false;
}
if (record.lastActivateTime - lastRun4AM > 0 && state.runnedToday) {
log.info("今日已经运行过激活路线");
state.activatedToday = true;
} else {
state.activatedToday = false;
}
}
/* ---------- 计算下次可激活时间 ---------- */
if (record.lastRunEndingRoute === "收尾额外B") {
state.aimActivateTime = record.lastActivateTime;
log.info("上次运行的是收尾额外B可直接开始激活路线");
} else if (!state.activatedToday) {
state.aimActivateTime = new Date(
record.lastActivateTime.getTime() +
24 * 60 * 60 * 1000 +
minIntervalTime * 60 * 1000
);
log.info(`上次运行的是收尾额外A预计在 ${state.aimActivateTime.toLocaleString()} 开始激活路线`);
} else {
state.aimActivateTime = record.lastActivateTime;
log.info(` 今日已经开始过激活路线,直接开始激活路线`);
}
}
async function writeRecord(accountName) {
if (state.cancel) return;
const recordFilePath = `records/${accountName}.txt`;
const lines = [
`上次运行日期: ${record.lastRunDate}`,
`上次激活收尾路线时间: ${record.lastActivateTime.toISOString()}`,
`上次运行收尾路线: ${record.lastRunEndingRoute}`,
...record.records.filter(Boolean)
];
const content = lines.join('\n');
try {
await file.writeText(recordFilePath, content, false);
log.info(`记录已写入 ${recordFilePath}`);
} catch (e) {
log.error(`写入 ${recordFilePath} 失败:`, e);
}
}
async function processArtifacts(times = 1) {
await genshin.returnMainUi();
await sleep(500);
let result = 0;
try {
if (settings.decomposeMode === "销毁(摩拉)") {
result = await destroyArtifacts(times);
} else {
result = await decomposeArtifacts();
}
} catch (error) {
log.error(`处理狗粮分解时发生异常: ${error.message}`);
}
await genshin.returnMainUi();
return result;
async function decomposeArtifacts() {
keyPress("B");
await sleep(1000);
await click(670, 45);
await sleep(500);
await recognizeTextAndClick("分解", { x: 635, y: 991, width: 81, height: 57 });
await sleep(1000);
//识别已储存经验1570-880-1650-930
let regionToCheck1 = { x: 1570, y: 880, width: 80, height: 50 };
let initialNum = await recognizeTextInRegion(regionToCheck1);
let initialValue = 0;
if (initialNum && !isNaN(parseInt(initialNum, 10))) {
initialValue = parseInt(initialNum, 10);
log.info(`已储存经验识别成功: ${initialValue}`);
} else {
log.warn(`在指定区域未识别到有效数字: ${initialValue}`);
}
let regionToCheck3 = { x: 100, y: 885, width: 170, height: 50 };
let decomposedNum = 0;
let firstNumber = 0;
let firstNumber2 = 0;
if (settings.keep4Star) {
await recognizeTextAndClick("快速选择", { x: 248, y: 996, width: 121, height: 49 });
moveMouseTo(960, 540);
await sleep(1000);
await click(370, 1020); // 点击“确认选择”按钮
await sleep(1500);
decomposedNum = await recognizeTextInRegion(regionToCheck3);
// 使用正则表达式提取第一个数字
const match = decomposedNum.match(/已选(\d+)/);
// 检查是否匹配成功
if (match) {
// 将匹配到的第一个数字转换为数字类型并存储在变量中
firstNumber = Number(match[1]);
log.info(`1-4星总数量: ${firstNumber}`);
} else {
log.info("识别失败");
}
keyPress("VK_ESCAPE");
await recognizeTextAndClick("分解", { x: 635, y: 991, width: 81, height: 57 });
await sleep(1000);
}
await recognizeTextAndClick("快速选择", { x: 248, y: 996, width: 121, height: 49 });
moveMouseTo(960, 540);
await sleep(1000);
if (settings.keep4Star) {
await click(370, 370);//取消选择四星
await sleep(1000);
}
await click(370, 1020); // 点击“确认选择”按钮
await sleep(1500);
let decomposedNum2 = await recognizeTextInRegion(regionToCheck3);
// 使用正则表达式提取第一个数字
const match2 = decomposedNum2.match(/已选(\d+)/);
// 检查是否匹配成功
if (match2) {
// 将匹配到的第一个数字转换为数字类型并存储在变量中
firstNumber2 = Number(match2[1]);
log.info(`分解总数是: ${firstNumber2}`);
} else {
log.info("识别失败");
}
//识别当前总经验
let regionToCheck2 = { x: 1500, y: 900, width: 150, height: 100 };
let newNum = await recognizeTextInRegion(regionToCheck2);
let newValue = 0;
if (newNum && !isNaN(parseInt(newNum, 10))) {
newValue = parseInt(newNum, 10);
log.info(`当前总经验识别成功: ${newValue}`);
} else {
log.warn(`在指定区域未识别到有效数字: ${newValue}`);
}
if (settings.decomposeMode === "分解(经验瓶)") {
log.info(`用户选择了分解,执行分解`);
// 根据用户配置,分解狗粮
await sleep(1000);
await click(1620, 1020); // 点击分解按钮
await sleep(1000);
// 4. 识别"进行分解"按钮
await click(1340, 755); // 点击进行分解按钮
await sleep(1000);
// 5. 关闭确认界面
await click(1340, 755);
await sleep(1000);
}
else {
log.info(`用户未选择分解,不执行分解`);
}
// 7. 计算分解获得经验=总经验-上次剩余
const resinExperience = Math.max(newValue - initialValue, 0);
log.info(`分解可获得经验: ${resinExperience}`);
let fourStarNum = firstNumber - firstNumber2;
if (settings.keep4Star) {
log.info(`保留的四星数量: ${fourStarNum}`);
}
let resultExperience = resinExperience;
if (resultExperience === 0) {
resultExperience = initialValue;
}
const result = resultExperience;
await genshin.returnMainUi();
return result;
}
async function destroyArtifacts(times = 1) {
async function findAndClick(target) {
gameRegion = captureGameRegion();
gameRegion.find(target).click();
gameRegion.dispose();
}
await genshin.returnMainUi();
keyPress("B");
await sleep(1500);
const gameRegion = captureGameRegion();
let ArtifactsButton = gameRegion.find(ArtifactsButtonRo);
gameRegion.dispose();
if (ArtifactsButton.isExist()) {
log.info("识别到圣遗物按钮");
ArtifactsButton.click();
await sleep(1500);
}
try {
for (let i = 0; i < times; i++) {
await findAndClick(DeleteButtonRo);// 点击摧毁
await sleep(600);
await findAndClick(AutoAddButtonRo);// 点击自动添加
await sleep(600);
await sleep(300);
click(150, 150);
await sleep(300);
click(150, 220);
await sleep(300);
click(150, 300);
if (!settings.keep4Star) {
await sleep(300);
click(150, 370);
}
await findAndClick(ConfirmButtonRo);// 点击快捷放入
await sleep(600);
await findAndClick(DestoryButtonRo);// 点击摧毁
await sleep(600);
await findAndClick(MidDestoryButtonRo);// 弹出页面点击摧毁
await sleep(600);
click(960, 1000);// 点击空白处
await sleep(1000);
}
} catch (ex) {
log.info("背包里的圣遗物已摧毁完毕,提前结束")
} finally {
await genshin.returnMainUi();
}
}
}
async function mora() {
// 定义一个函数用于识别图像
async function recognizeImage(recognitionObject, timeout = 5000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 尝试识别图像
const gameRegion = captureGameRegion();
let imageResult = gameRegion.find(recognitionObject);
gameRegion.dispose;
if (imageResult) {
// log.info(`成功识别图像,坐标: x=${imageResult.x}, y=${imageResult.y}`);
// log.info(`图像尺寸: width=${imageResult.width}, height=${imageResult.height}`);
return { success: true, x: imageResult.x, y: imageResult.y };
}
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
}
await sleep(500); // 短暂延迟,避免过快循环
}
log.warn(`经过多次尝试,仍然无法识别图像`);
return { success: false };
}
let result = 0;
let tryTimes = 0;
while (result === 0 && tryTimes < 3) {
await genshin.returnMainUi();
await sleep(100);
log.info("开始尝试识别摩拉");
// 按下 C 键
keyPress("C");
await sleep(1500);
let recognized = false;
// 识别“角色菜单”图标或“天赋”文字
let startTime = Date.now();
while (Date.now() - startTime < 5000) {
// 尝试识别“角色菜单”图标
let characterMenuResult = await recognizeImage(CharacterMenuRo, 5000);
if (characterMenuResult.success) {
await click(177, 433);
await sleep(500);
recognized = true;
break;
}
// 尝试识别“天赋”文字
let targetText = "天赋";
let ocrRegion = { x: 133, y: 395, width: 115, height: 70 }; // 设置对应的识别区域
let talentResult = await recognizeTextAndClick(targetText, ocrRegion);
if (talentResult.success) {
log.info(`点击天赋文字,坐标: x=${talentResult.x}, y=${talentResult.y}`);
recognized = true;
break;
}
await sleep(1000); // 短暂延迟,避免过快循环
}
let recognizedText = "";
// 如果识别到了“角色菜单”或“天赋”,则识别“摩拉数值”
if (recognized) {
let ocrRegionMora = { x: 1620, y: 25, width: 152, height: 46 }; // 设置对应的识别区域
recognizedText = await recognizeTextInRegion(ocrRegionMora);
if (recognizedText) {
log.info(`成功识别到摩拉数值: ${recognizedText}`);
result = recognizedText;
} else {
log.warn("未能识别到摩拉数值。");
}
} else {
log.warn("未能识别到角色菜单或天赋");
}
await sleep(500);
tryTimes++;
await genshin.returnMainUi();
}
return Number(result);
}
// 定义一个独立的函数用于在指定区域进行 OCR 识别并输出识别内容
async function recognizeTextInRegion(ocrRegion, timeout = 5000) {
let startTime = Date.now();
let retryCount = 0; // 重试计数
while (Date.now() - startTime < timeout) {
try {
// 在指定区域进行 OCR 识别
const gameRegion = captureGameRegion();
let ocrResult = gameRegion.find(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height));
gameRegion.dispose();
if (ocrResult) {
let correctedText = ocrResult.text;
return correctedText; // 返回识别到的内容
} else {
log.warn(`OCR 识别区域未找到内容`);
return null; // 如果 OCR 未识别到内容,返回 null
}
} catch (error) {
retryCount++; // 增加重试计数
log.warn(`OCR 识别失败,正在进行第 ${retryCount} 次重试...`);
}
await sleep(500); // 短暂延迟,避免过快循环
}
log.warn(`经过多次尝试,仍然无法在指定区域识别到文字`);
return null; // 如果未识别到文字,返回 null
}
// 定义一个函数用于识别文字并点击
async function recognizeTextAndClick(targetText, ocrRegion, timeout = 3000) {
let startTime = Date.now();
let retryCount = 0; // 重试计数
while (Date.now() - startTime < timeout) {
try {
// 尝试 OCR 识别
const gameRegion = captureGameRegion();
let resList = gameRegion.findMulti(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height)); // 指定识别区域
gameRegion.dispose();
// 遍历识别结果,检查是否找到目标文本
for (let res of resList) {
let correctedText = res.text;
if (correctedText.includes(targetText)) {
// 如果找到目标文本,计算并点击文字的中心坐标
let centerX = Math.round(res.x + res.width / 2);
let centerY = Math.round(res.y + res.height / 2);
await click(centerX, centerY);
await sleep(500); // 确保点击后有足够的时间等待
return { success: true, x: centerX, y: centerY };
}
}
} catch (error) {
retryCount++; // 增加重试计数
log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`);
}
await sleep(1000); // 短暂延迟,避免过快循环
}
log.warn(`经过多次尝试,仍然无法识别文字: ${targetText},尝试点击默认中心位置`);
let centerX = Math.round(ocrRegion.x + ocrRegion.width / 2);
let centerY = Math.round(ocrRegion.y + ocrRegion.height / 2);
await click(centerX, centerY);
await sleep(1000);
return { success: false };
}
//切换队伍
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();
}
}
// 定义 readFolder 函数
async function readFolder(folderPath, onlyJson) {
// 新增一个堆栈,初始时包含 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;
}
//读取cd信息
async function readCDInfo(accountName) {
const CDInfoFolderPath = 'CDInfo/';
const CDInfoFilePath = `CDInfo/${accountName}.json`;
const filesInSubFolder = file.ReadPathSync(CDInfoFolderPath);
let fileExists = false;
for (const filePath of filesInSubFolder) {
if (filePath === `CDInfo\\${accountName}.json`) {
fileExists = true;
break;
}
}
if (fileExists) {
try {
const raw = await file.readText(CDInfoFilePath);
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.every(item => typeof item === 'string')) {
CDInfo = parsed;
} else {
log.warn('文件内容不是字符串数组,使用空数组');
CDInfo = [];
}
} catch (e) {
log.error(`读取或解析 ${CDInfoFilePath} 失败:`, e);
CDInfo = [];
}
} else {
CDInfo = [];
}
}
//更新cd信息
async function writeCDInfo(accountName) {
log.info("修改CDInfo文件");
if (state.cancel) return;
const CDInfoFilePath = `CDInfo/${accountName}.json`;
await file.writeText(CDInfoFilePath, JSON.stringify(CDInfo), false);
}
//运行普通路线
async function runNormalPath(doStop) {
//关闭拾取
dispatcher.ClearAllTriggers();
if (state.cancel) return;
const routeMap = { A: normalPathA, B: normalPathB, C: normalPathC };
const normalPath = routeMap[state.runningRoute];
const normalCombatPath = normalPath + "/清怪";
const normalExecutePath = normalPath + "/执行";
if (combatPartyName) {
log.info("填写了清怪队伍,执行清怪路线");
await runPaths(normalCombatPath, combatPartyName, doStop);
}
// 启用自动拾取的实时任务
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
await runPaths(normalExecutePath, artifactPartyName, doStop);
}
async function runActivatePath() {
//关闭拾取
dispatcher.ClearAllTriggers();
if (state.cancel) return;
if (!state.activatedToday) {
log.info("今日未执行过激活路线");
//判断收尾路线并更新record
if (new Date() >= state.aimActivateTime || record.lastRunEndingRoute === "收尾额外B") {
state.runningEndingAndExtraRoute = "收尾额外A";
} else {
state.runningEndingAndExtraRoute = "收尾额外B";
}
record.lastRunEndingRoute = state.runningEndingAndExtraRoute;
record.lastActivateTime = new Date();
await writeRecord(accountName);
} else {
log.info("今日执行过激活路线");
state.runningEndingAndExtraRoute = record.lastRunEndingRoute;
}
let endingPath = state.runningEndingAndExtraRoute === "收尾额外A"
? "assets/ArtifactsPath/优先收尾路线"
: "assets/ArtifactsPath/替补收尾路线";
if (forceAlternate) {
endingPath = state.runningRoute === "A"
? "assets/ArtifactsPath/优先收尾路线"
: "assets/ArtifactsPath/替补收尾路线";
}
const endingActivatePath = endingPath + "/激活";
const endingCombatPath = endingPath + "/清怪";
const endingPreparePath = endingPath + "/准备";
let extraPath = state.runningEndingAndExtraRoute === "收尾额外A"
? "assets/ArtifactsPath/额外/所有额外"
: "assets/ArtifactsPath/额外/仅12h额外";
const extraActivatePath = extraPath + "/激活";
const extraCombatPath = extraPath + "/清怪";
const extraPreparePath = extraPath + "/准备";
if (!forceAlternate && state.runningEndingAndExtraRoute === "收尾额外A") {
await runPaths(endingActivatePath, artifactPartyName, false);
}
await runPaths(extraActivatePath, combatPartyName, false);
if (combatPartyName) {
log.info("填写了清怪队伍,执行清怪路线");
await runPaths(extraCombatPath, artifactPartyName, false);
await runPaths(endingCombatPath, combatPartyName, false);
}
await runPaths(endingPreparePath, artifactPartyName, false);
await runPaths(extraPreparePath, combatPartyName, false);
}
async function runEndingAndExtraPath() {
// 启用自动拾取的实时任务
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
if (state.cancel) return;
let endingPath = state.runningEndingAndExtraRoute === "收尾额外A"
? "assets/ArtifactsPath/优先收尾路线"
: "assets/ArtifactsPath/替补收尾路线";
if (forceAlternate) {
endingPath = state.runningRoute === "A"
? "assets/ArtifactsPath/优先收尾路线"
: "assets/ArtifactsPath/替补收尾路线";
}
let extraPath = state.runningEndingAndExtraRoute === "收尾额外A"
? "assets/ArtifactsPath/额外/所有额外"
: "assets/ArtifactsPath/额外/仅12h额外";
endingPath = endingPath + "/执行";
await runPaths(endingPath, artifactPartyName, false);
extraPath = extraPath + "/执行";
await runPaths(extraPath, artifactPartyName, false);
}
async function runPaths(folderFilePath, PartyName, doStop) {
if (state.cancel) return;
let Paths = await readFolder(folderFilePath, true);
for (let i = 0; i < Paths.length; i++) {
let skiprecord = false;
if (state.cancel) return;
if (new Date() >= state.aimActivateTime && doStop) {
log.info("已经到达预定时间");
break;
} else if ((new Date() >= (state.aimActivateTime - minIntervalTime * 60)) && doStop) {
log.info(`即将到达预定时间,等待${state.aimActivateTime - new Date()}毫秒`);
await sleep(state.aimActivateTime - new Date())
break;
}
const Path = Paths[i];
let success = true;
// 如果 CDInfo 数组中已存在该文件名,则跳过
if (CDInfo.includes(Path.fullPath)) {
log.info(`路线${Path.fullPath}今日已运行,跳过`);
continue;
}
if (PartyName != state.currentParty) {
//如果与当前队伍不同,尝试切换队伍,并更新队伍
await switchPartyIfNeeded(PartyName);
state.currentParty = PartyName;
if (settings.furina) {
if (state.currentParty === artifactPartyName) {
await pathingScript.runFile('assets/furina/强制白芙.json');
} else {
await pathingScript.runFile('assets/furina/强制黑芙.json');
}
}
}
await fakeLog(Path.fileName, false, true, 0);
try {
log.info(`当前进度:${Path.fileName}${folderFilePath}${i + 1}/${Paths.length}`);
await pathingScript.runFile(Path.fullPath);
await sleep(1);
} catch (error) {
skiprecord = true;
log.error(`执行路径文件时发生错误:${error.message}`);
if (error.message === "A task was canceled.") {
log.warn("任务取消");
state.cancel = true;
}
success = false;
break;
}
await fakeLog(Path.fileName, false, false, 0);
const pathInfo = await parsePathing(Path.fullPath);
if (pathInfo.ok) {
//回到主界面
await genshin.returnMainUi();
await sleep(100);
try {
// 获取当前人物在指定地图上的坐标
const currentPosition = await genshin.getPositionFromMap(pathInfo.map_name);
// 计算与最后一个非 orientation 点的距离
const distToLast = Math.hypot(
currentPosition.x - pathInfo.x,
currentPosition.y - pathInfo.y
);
// 距离超过 100 认为路线没有正常完成(卡死或未开图等)
if (distToLast >= 100) {
failcount++;
skiprecord = true;
log.warn(`路线${Path.fileName},没有正常完成,请检查是否开图`);
await sleep(1000);
}
} catch (error) {
log.error(`发生错误:${error.message}`);
skiprecord = true;
}
}
if (!skiprecord) {
CDInfo = [...new Set([...CDInfo, Path.fullPath])];
await writeCDInfo(accountName);
}
}
}
async function parsePathing(pathFilePath) {
try {
const raw = await file.readText(pathFilePath);
const json = JSON.parse(raw);
if (!Array.isArray(json.positions) || typeof json.map_name !== 'string') {
return { ok: false };
}
// 从后往前找第一个 type !== "orientation" 的点
for (let i = json.positions.length - 1; i >= 0; i--) {
const p = json.positions[i];
if (p.type !== 'orientation' &&
typeof p.x === 'number' &&
typeof p.y === 'number') {
return {
ok: true,
x: p.x,
y: p.y,
map_name: json.map_name
};
}
}
return { ok: false };
} catch (err) {
log.error(`解析路径文件失败: ${err.message}`);
return { ok: false };
}
}
// 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);
}
}