mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-15 03:23:22 +08:00
* js:狗粮批发2.1.0 移除路径内调时,改为自定义配置控制 * js:战斗好感 1.更新了版本号 2.修复摩拉识别使用的错误模板图片路径 3.增加1000Stars选项 * combat:战斗策略 1.新增仅用于盗宝团好感的那维莱特策略 2.为不适合锄地的策略添加-副本后缀 3.删除可完全被万能策略上位替代的策略:月草c,伊芙爱万玛,芙茜万夏 4.删除完全不合理的莱爱万夜和莱爱万夜.只用战技(并不适合锄地却写适合锄地) * 改为归档 * js:锄地一条龙 修改触发分配结果不合理的触发条件 * 移除小怪2000部分的json5文件 锄地规划纯害人来的
1497 lines
56 KiB
JavaScript
1497 lines
56 KiB
JavaScript
// 初始化自定义配置并赋予默认值
|
||
let artifactPartyName = settings.artifactPartyName || "狗粮";//狗粮队伍名称
|
||
let combatPartyName = settings.combatPartyName;//清怪队伍名称
|
||
let minIntervalTime = settings.fastMode
|
||
? 10
|
||
: Number(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 || "默认账户";//账户名
|
||
let TMthreshold = +settings.TMthreshold || 0.9;//拾取阈值
|
||
|
||
//文件路径
|
||
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 decomposeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/decompose.png"));
|
||
const quickChooseRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/quickChoose.png"));
|
||
const confirmRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/confirm.png"));
|
||
const doDecomposeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/doDecompose.png"));
|
||
const doDecompose2Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/doDecompose2.png"));
|
||
|
||
const outDatedRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/ConfirmButton.png"), 760, 700, 100, 100);
|
||
|
||
const normalPathA = settings.fastMode ? "" : "assets/ArtifactsPath/普通98点1号线";
|
||
const normalPathB = settings.fastMode ? "" : "assets/ArtifactsPath/普通98点2号线";
|
||
const extraPath = settings.fastMode ? "" : "assets/ArtifactsPath/额外";
|
||
|
||
//初始化变量
|
||
let artifactExperienceDiff = 0;
|
||
let moraDiff = 0;
|
||
let state = {};
|
||
let record = {};
|
||
let CDInfo = [];
|
||
let failcount = 0;
|
||
let autoSalvageCount = 0;
|
||
let furinaState = "unknown";
|
||
|
||
let targetItems;
|
||
let pickupDelay = 100;
|
||
let timeMove = 1000;
|
||
let findFInterval = (+settings.findFInterval || 100);
|
||
if (findFInterval < 16) {
|
||
findFInterval = 16;
|
||
}
|
||
if (findFInterval > 200) {
|
||
findFInterval = 200;
|
||
}
|
||
let lastRoll = new Date();
|
||
let checkDelay = Math.round(findFInterval / 2);
|
||
let timeMoveUp = Math.round(timeMove * 0.45);
|
||
let timeMoveDown = Math.round(timeMove * 0.55);
|
||
let rollingDelay = 25;
|
||
let gameRegion;
|
||
let lastsettimeTime = 0;
|
||
|
||
(async function () {
|
||
setGameMetrics(1920, 1080, 1);
|
||
dispatcher.AddTrigger(new RealtimeTimer("AutoSkip"));
|
||
targetItems = await loadTargetItems();
|
||
state.activatePickUp = false;
|
||
{
|
||
//校验自定义配置,从未打开过自定义配置时进行警告
|
||
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';//根据日期奇偶数确定普通路线
|
||
|
||
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(Date.now() - 4 * 60 * 60 * 1000)
|
||
.toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit' })
|
||
.replace(/\//g, '/');
|
||
|
||
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;
|
||
|
||
if (!onlyActivate || state.runningEndingAndExtraRoute != "收尾额外A") {
|
||
//执行收尾和额外路线
|
||
await runEndingAndExtraPath();
|
||
if (state.cancel) return;
|
||
}
|
||
|
||
//切回黑芙
|
||
if (settings.furina) {
|
||
await pathingScript.runFile('assets/furina/强制黑芙.json');
|
||
}
|
||
|
||
//运行后按自定义配置清理狗粮
|
||
artifactExperienceDiff += await processArtifacts(21);
|
||
moraDiff += await mora();
|
||
log.info(`狗粮路线获取摩拉: ${moraDiff}`);
|
||
log.info(`狗粮路线获取狗粮经验: ${artifactExperienceDiff}`);
|
||
// ========== 主流程末尾:替换原来的“修改records”区块 ==========
|
||
const todayKey = `日期:${record.lastRunDate},运行收尾路线${record.lastRunEndingRoute}`;
|
||
let merged = false;
|
||
|
||
// 先扫描数组,找同一天同收尾路线
|
||
for (let i = 0; i < record.records.length; i++) {
|
||
const line = record.records[i];
|
||
if (line && line.startsWith(todayKey)) {
|
||
// 解析原记录的经验、摩拉
|
||
const match = line.match(/狗粮经验(-?\d+),摩拉(-?\d+)/);
|
||
if (match) {
|
||
const oldExp = Number(match[1]);
|
||
const oldMora = Number(match[2]);
|
||
// 累加并取正
|
||
const newExp = Math.max(0, oldExp + artifactExperienceDiff);
|
||
const newMora = Math.max(0, oldMora + moraDiff);
|
||
record.records[i] = `${todayKey},狗粮经验${newExp},摩拉${newMora}`;
|
||
merged = true;
|
||
log.info(`检测到同日记录,已合并更新:经验 ${newExp},摩拉 ${newMora}`);
|
||
}
|
||
break; // 同一天只可能有一条,找到就停
|
||
}
|
||
}
|
||
|
||
// 如果没找到同一天,再走原来的“整体后移插新记录”逻辑
|
||
if (!merged) {
|
||
for (let i = record.records.length - 1; i > 0; i--) {
|
||
record.records[i] = record.records[i - 1];
|
||
}
|
||
record.records[0] = `${todayKey},狗粮经验${Math.max(0, artifactExperienceDiff)},摩拉${Math.max(0, moraDiff)}`;
|
||
}
|
||
|
||
// 通知与写盘保持不变
|
||
if (settings.notify) {
|
||
notification.Send(`${todayKey},狗粮经验${Math.max(0, artifactExperienceDiff)},摩拉${Math.max(0, 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(33550336).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);
|
||
}
|
||
|
||
/* ---------- 判断今日是否运行(北京时间 04:00 分界,手动拼接 UTC 20 点) ---------- */
|
||
if (record.lastRunDate) {
|
||
const [y, m, d] = record.lastRunDate.split('/').map(Number);
|
||
|
||
// 1. 用 UTC 构造记录日期 00:00:00
|
||
const recordUtc = Date.UTC(y, m - 1, d); // 毫秒
|
||
|
||
// 2. 减 24 小时得到“前一天”
|
||
const prevUtc = recordUtc - 24 * 60 * 60 * 1000;
|
||
|
||
// 3. 从毫秒时间戳里取出 UTC 年月日
|
||
const prev = new Date(prevUtc);
|
||
const yy = prev.getUTCFullYear();
|
||
const mm = prev.getUTCMonth() + 1; // 1-based
|
||
const dd = prev.getUTCDate();
|
||
|
||
// 4. 严格按模板字符串拼成合法日期
|
||
const lastRun4AM = new Date(
|
||
`${yy}-${String(mm).padStart(2, '0')}-${String(dd).padStart(2, '0')}T20:00:00.000Z`
|
||
).getTime();
|
||
|
||
//log.info(`lastRun4AM = ${new Date(lastRun4AM).toISOString()}`);
|
||
|
||
const now = Date.now(); // 当前毫秒时间戳
|
||
//log.info(`时间差为 ${now - lastRun4AM} ms`);
|
||
|
||
if (now - lastRun4AM < 24 * 60 * 60 * 1000) {
|
||
log.info("今日已经运行过狗粮");
|
||
state.runnedToday = true;
|
||
} else {
|
||
state.runnedToday = false;
|
||
}
|
||
|
||
if (record.lastActivateTime.getTime() - 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");
|
||
if (await findAndClick(outDatedRo, true, 1500)) {
|
||
log.info("检测到过期物品弹窗,处理");
|
||
await sleep(1000);
|
||
}
|
||
let type = "圣遗物";
|
||
await findAndClick([`assets/RecognitionObject/背包界面/${type}1.png`, `assets/RecognitionObject/背包界面/${type}2.png`])
|
||
await sleep(500);
|
||
if (!await findAndClick(decomposeRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
await sleep(1000);
|
||
|
||
// 识别已储存经验(1570-880-1650-930)
|
||
let digits = await numberTemplateMatch("assets/已储存经验数字", 1573, 885, 74, 36);
|
||
|
||
let initialValue = 0;
|
||
if (digits >= 0) {
|
||
initialValue = digits;
|
||
log.info(`已储存经验识别成功: ${initialValue}`);
|
||
} else {
|
||
log.warn(`在指定区域未识别到有效数字: ${initialValue}`);
|
||
}
|
||
|
||
let firstNumber = 0;
|
||
let firstNumber2 = 0;
|
||
|
||
if (settings.keep4Star) {
|
||
if (!await findAndClick(quickChooseRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
moveMouseTo(960, 540);
|
||
await sleep(1000);
|
||
|
||
// 点击“确认选择”按钮
|
||
if (!await findAndClick(confirmRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
await sleep(1000);
|
||
|
||
let match = await numberTemplateMatch("assets/分解数量数字", 100, 885, 200, 50);
|
||
match = (match - 1000) / 10000;
|
||
|
||
if (match >= 0) {
|
||
firstNumber = match;
|
||
log.info(`1-4星总数量: ${firstNumber}`);
|
||
} else {
|
||
log.info("识别失败");
|
||
}
|
||
keyPress("VK_ESCAPE");
|
||
|
||
await sleep(500);
|
||
if (!await findAndClick(decomposeRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
await sleep(500);
|
||
}
|
||
if (!await findAndClick(quickChooseRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
moveMouseTo(960, 540);
|
||
await sleep(1000);
|
||
|
||
if (settings.keep4Star) {
|
||
await click(370, 370);//取消选择四星
|
||
await sleep(1000);
|
||
}
|
||
// 点击“确认选择”按钮
|
||
if (!await findAndClick(confirmRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
await sleep(2000);
|
||
|
||
let match2 = await numberTemplateMatch("assets/分解数量数字", 100, 885, 200, 50);
|
||
match2 = (match2 - 1000) / 10000;
|
||
if (match2 >= 0) {
|
||
firstNumber2 = match2
|
||
log.info(`分解总数是: ${firstNumber2}`);
|
||
} else {
|
||
log.info("识别失败");
|
||
}
|
||
//识别当前总经验
|
||
if (settings.notify) {
|
||
notification.Send(`当前经验如图`);
|
||
}
|
||
// 当前总经验(1470-880-205-70)
|
||
|
||
let digits2 = await numberTemplateMatch("assets/分解可获得经验数字", 1469, 899, 180, 37, 0.95, 0.85, 5, 1);
|
||
|
||
let newValue = 0;
|
||
if (digits2 >= 0) {
|
||
newValue = digits2
|
||
log.info(`当前总经验识别成功: ${newValue}`);
|
||
} else {
|
||
log.warn(`在指定区域未识别到有效数字: ${newValue}`);
|
||
}
|
||
|
||
if (settings.decomposeMode === "分解(经验瓶)") {
|
||
log.info(`用户选择了分解,执行分解`);
|
||
// 根据用户配置,分解狗粮
|
||
await sleep(1000);
|
||
// 点击分解按钮
|
||
if (!await findAndClick(doDecomposeRo)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
await sleep(500);
|
||
|
||
// 4. "进行分解"按钮// 点击进行分解按钮
|
||
if (!await findAndClick(doDecompose2Ro)) {
|
||
await genshin.returnMainUi();
|
||
return 0;
|
||
}
|
||
await sleep(1000);
|
||
|
||
// 5. 关闭确认界面
|
||
await click(1340, 755);
|
||
await sleep(1000);
|
||
}
|
||
else {
|
||
log.info(`用户未选择分解,不执行分解`);
|
||
}
|
||
|
||
// 7. 计算分解获得经验=总经验-上次剩余
|
||
let 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;
|
||
}
|
||
let result = resultExperience;
|
||
await genshin.returnMainUi();
|
||
return result;
|
||
}
|
||
|
||
async function destroyArtifacts(times = 1) {
|
||
await genshin.returnMainUi();
|
||
await sleep(250);
|
||
keyPress("B");
|
||
if (await findAndClick(outDatedRo)) {
|
||
log.info("检测到过期物品弹窗,处理");
|
||
await sleep(1000);
|
||
}
|
||
let type = "圣遗物";
|
||
await findAndClick([`assets/RecognitionObject/背包界面/${type}1.png`, `assets/RecognitionObject/背包界面/${type}2.png`])
|
||
try {
|
||
for (let i = 0; i < times; i++) {
|
||
// 点击摧毁
|
||
if (!await findAndClick(DeleteButtonRo)) {
|
||
await genshin.returnMainUi();
|
||
return;
|
||
}
|
||
await sleep(600);
|
||
// 点击自动添加
|
||
if (!await findAndClick(AutoAddButtonRo)) {
|
||
await genshin.returnMainUi();
|
||
return;
|
||
}
|
||
await sleep(900);
|
||
click(150, 150);
|
||
await sleep(300);
|
||
click(150, 220);
|
||
await sleep(300);
|
||
click(150, 300);
|
||
if (!settings.keep4Star) {
|
||
await sleep(300);
|
||
click(150, 370);
|
||
}
|
||
// 点击快捷放入
|
||
if (!await findAndClick(ConfirmButtonRo)) {
|
||
await genshin.returnMainUi();
|
||
return;
|
||
}
|
||
await sleep(600);
|
||
// 点击摧毁
|
||
if (!await findAndClick(DestoryButtonRo)) {
|
||
await genshin.returnMainUi();
|
||
return;
|
||
}
|
||
await sleep(600);
|
||
// 弹出页面点击摧毁
|
||
if (!await findAndClick(MidDestoryButtonRo)) {
|
||
await genshin.returnMainUi();
|
||
return;
|
||
}
|
||
await sleep(600);
|
||
click(960, 1000);// 点击空白处
|
||
await sleep(1000);
|
||
}
|
||
} catch (ex) {
|
||
log.info("背包里的圣遗物已摧毁完毕,提前结束")
|
||
} finally {
|
||
await genshin.returnMainUi();
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
async function mora() {
|
||
|
||
let result = 0;
|
||
let tryTimes = 0;
|
||
while (result === 0 && tryTimes < 3) {
|
||
await genshin.returnMainUi();
|
||
await sleep(100);
|
||
log.info("开始尝试识别摩拉");
|
||
keyPress("B");
|
||
await sleep(1500);
|
||
let type = "养成道具";
|
||
await findAndClick([`assets/RecognitionObject/背包界面/${type}1.png`, `assets/RecognitionObject/背包界面/${type}2.png`])
|
||
|
||
let moraRes = 0;
|
||
await sleep(1000);
|
||
if (settings.notify) {
|
||
notification.Send(`当前摩拉如图`);
|
||
}
|
||
|
||
const moraRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/mora.png"));
|
||
const gameRegion = captureGameRegion();
|
||
let moraX = 336;
|
||
let moraY = 1004;
|
||
try {
|
||
const result = gameRegion.find(moraRo);
|
||
if (result.isExist()) {
|
||
moraX = result.x;
|
||
moraY = result.y;
|
||
}
|
||
} catch (err) {
|
||
} finally {
|
||
gameRegion.dispose();
|
||
}
|
||
|
||
moraRes = await numberTemplateMatch("assets/背包摩拉数字", moraX, moraY, 300, 40, 0.95, 0.85, 10);
|
||
|
||
if (moraRes >= 0) {
|
||
log.info(`成功识别到摩拉数值: ${moraRes}`);
|
||
result = moraRes;
|
||
} else {
|
||
log.warn("未能识别到摩拉数值。");
|
||
}
|
||
|
||
await sleep(500);
|
||
tryTimes++;
|
||
await genshin.returnMainUi();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
//切换队伍
|
||
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) {
|
||
if (state.cancel) return;
|
||
const CDInfoFilePath = `CDInfo/${accountName}.json`;
|
||
await file.writeText(CDInfoFilePath, JSON.stringify(CDInfo), false);
|
||
}
|
||
|
||
//运行普通路线
|
||
async function runNormalPath(doStop) {
|
||
furinaState = "unknown";
|
||
if (state.cancel) return;
|
||
const routeMap = { A: normalPathA, B: normalPathB };
|
||
const normalPath = routeMap[state.runningRoute];
|
||
const normalCombatPath = normalPath + "/清怪";
|
||
const normalExecutePath = normalPath + "/执行";
|
||
if (combatPartyName) {
|
||
log.info("填写了清怪队伍,执行清怪路线");
|
||
await runPaths(normalCombatPath, combatPartyName, doStop, "black");
|
||
}
|
||
state.activatePickUp = true;
|
||
await runPaths(normalExecutePath, artifactPartyName, doStop, "white");
|
||
state.activatePickUp = false;
|
||
}
|
||
|
||
async function runActivatePath() {
|
||
//furinaState = "unknown";
|
||
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/替补收尾路线";
|
||
}
|
||
if (onlyActivate) {
|
||
log.warn("勾选了联机狗粮,将只激活,不执行收尾和额外路线");
|
||
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 (!settings.fastMode) {
|
||
if (!forceAlternate && state.runningEndingAndExtraRoute === "收尾额外A") {
|
||
await runPaths(endingActivatePath, "", false);
|
||
}
|
||
await runPaths(extraActivatePath, "", false);
|
||
|
||
await runPaths(endingPreparePath, "", false);
|
||
await runPaths(extraPreparePath, "", false);
|
||
|
||
if (combatPartyName) {
|
||
log.info("填写了清怪队伍,执行清怪路线");
|
||
await runPaths(extraCombatPath, combatPartyName, false, "black");
|
||
await runPaths(endingCombatPath, combatPartyName, false, "black");
|
||
}
|
||
}
|
||
}
|
||
|
||
async function runEndingAndExtraPath() {
|
||
furinaState = "unknown";
|
||
if (state.cancel) return;
|
||
let endingPath = state.runningEndingAndExtraRoute === "收尾额外A"
|
||
? "assets/ArtifactsPath/优先收尾路线"
|
||
: "assets/ArtifactsPath/替补收尾路线";
|
||
if (forceAlternate) {
|
||
endingPath = state.runningRoute === "A"
|
||
? "assets/ArtifactsPath/优先收尾路线"
|
||
: "assets/ArtifactsPath/替补收尾路线";
|
||
}
|
||
if (onlyActivate) {
|
||
endingPath = state.runningEndingAndExtraRoute === "收尾额外A"
|
||
? "assets/ArtifactsPath/联机收尾/优先收尾路线"
|
||
: "assets/ArtifactsPath/联机收尾/替补收尾路线";
|
||
}
|
||
let extraPath = state.runningEndingAndExtraRoute === "收尾额外A"
|
||
? "assets/ArtifactsPath/额外/所有额外"
|
||
: "assets/ArtifactsPath/额外/仅12h额外";
|
||
endingPath = endingPath + "/执行";
|
||
extraPath = extraPath + "/执行";
|
||
if (settings.fastMode) {
|
||
log.info("启用了急速模式,直接运行高铁路线");
|
||
endingPath = state.runningEndingAndExtraRoute === "收尾额外A"
|
||
? "assets/ArtifactsPath/高铁/高铁1号线"
|
||
: "assets/ArtifactsPath/高铁/高铁2号线";
|
||
if (forceAlternate) {
|
||
endingPath = state.runningRoute === "A"
|
||
? "assets/ArtifactsPath/高铁/高铁1号线"
|
||
: "assets/ArtifactsPath/高铁/高铁2号线";
|
||
}
|
||
extraPath = "";
|
||
}
|
||
state.activatePickUp = true;
|
||
await runPaths(endingPath, artifactPartyName, false, "white");
|
||
await runPaths(extraPath, artifactPartyName, false, "white");
|
||
state.activatePickUp = false;
|
||
}
|
||
|
||
async function runPaths(folderFilePath, PartyName, doStop, furinaRequirement = "") {
|
||
if (state.cancel) return;
|
||
if (folderFilePath === "") {
|
||
return;
|
||
}
|
||
let Paths = await readFolder(folderFilePath, true);
|
||
let furinaChecked = false;
|
||
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 * 1000)) && 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 && PartyName) {
|
||
//如果与当前队伍不同,尝试切换队伍,并更新队伍
|
||
await switchPartyIfNeeded(PartyName);
|
||
state.currentParty = PartyName;
|
||
furinaState = "unknown";
|
||
}
|
||
if (settings.furina && !furinaChecked) {
|
||
furinaChecked = true;
|
||
if (furinaRequirement === "white") {
|
||
log.info("勾选了芙宁娜选项,正在强制切换芙宁娜状态为白芙");
|
||
log.warn("非必要请尽量不要勾选该选项");
|
||
await pathingScript.runFile('assets/furina/强制白芙.json');
|
||
furinaState = "white";
|
||
} else if (furinaRequirement === "black") {
|
||
log.info("勾选了芙宁娜选项,正在强制切换芙宁娜状态为黑芙");
|
||
log.warn("非必要请尽量不要勾选该选项");
|
||
await pathingScript.runFile('assets/furina/强制黑芙.json');
|
||
furinaState = "black";
|
||
}
|
||
}
|
||
if (settings.autoSalvage && autoSalvageCount >= 4) {
|
||
autoSalvageCount = 0;
|
||
if (settings.decomposeMode === "分解(经验瓶)") {
|
||
artifactExperienceDiff += await processArtifacts(1);
|
||
} else {
|
||
await processArtifacts(1);
|
||
}
|
||
} else {
|
||
autoSalvageCount++;
|
||
}
|
||
const pathInfo = await parsePathing(Path.fullPath);
|
||
try {
|
||
log.info(`当前进度:${Path.fileName}为${folderFilePath}第${i + 1}/${Paths.length}个`);
|
||
await runPath(Path.fullPath, null);
|
||
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;
|
||
}
|
||
if (pathInfo.ok) {
|
||
await genshin.returnMainUi();
|
||
await sleep(500);
|
||
|
||
const maxAttempts = 3;
|
||
let attempts = 0;
|
||
|
||
while (attempts < maxAttempts) {
|
||
try {
|
||
const cur = await genshin.getPositionFromMap(pathInfo.map_name);
|
||
const dist = Math.hypot(cur.x - pathInfo.x, cur.y - pathInfo.y);
|
||
|
||
if (dist < 50) break; // 成功跳出
|
||
|
||
attempts++;
|
||
log.warn(
|
||
`路线 ${Path.fileName} 第 ${attempts} 次检测失败 ` +
|
||
`(距离 ${dist.toFixed(2)}) —— ` +
|
||
`当前(${cur.x.toFixed(2)}, ${cur.y.toFixed(2)}) ` +
|
||
`目标(${pathInfo.x.toFixed(2)}, ${pathInfo.y.toFixed(2)})`
|
||
);
|
||
|
||
if (attempts === maxAttempts) {
|
||
failcount++;
|
||
skiprecord = true;
|
||
await sleep(5000);
|
||
break;
|
||
}
|
||
|
||
await sleep(1000);
|
||
} catch (err) {
|
||
log.error(`发生错误:${err.message}`);
|
||
skiprecord = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!skiprecord) {
|
||
CDInfo = [...new Set([...CDInfo, Path.fullPath])];
|
||
await writeCDInfo(accountName);
|
||
}
|
||
}
|
||
|
||
if (doStop && new Date() < state.aimActivateTime) {
|
||
const maxWaitMs = settings.maxWaitingTime * 60 * 1000;
|
||
const needWaitMs = state.aimActivateTime - new Date();
|
||
if (needWaitMs <= maxWaitMs && needWaitMs > 0) {
|
||
log.info(`等待 ${needWaitMs} 毫秒到达预定时间`);
|
||
await sleep(needWaitMs);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function parsePathing(pathFilePath) {
|
||
try {
|
||
const raw = await file.readText(pathFilePath);
|
||
const json = JSON.parse(raw);
|
||
|
||
// 只要 positions 不是数组就直接失败
|
||
if (!Array.isArray(json.positions)) {
|
||
log.error("文件positions字段异常");
|
||
return { ok: false };
|
||
}
|
||
|
||
// 从 info.map_name 读取,不存在时兜底为 "Teyvat"
|
||
const map_name =
|
||
typeof json.info?.map_name === 'string' && json.info.map_name.trim() !== ''
|
||
? json.info.map_name
|
||
: 'Teyvat';
|
||
|
||
// 从后往前找第一个 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,
|
||
};
|
||
}
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
|
||
async function runPath(fullPath, targetItemPath = null) {
|
||
let settimeInterval = 10 * 60 * 1000;
|
||
if (settings.setTimeMode && settings.setTimeMode != "不调节时间" && (((new Date() - lastsettimeTime) > settimeInterval))) {
|
||
if (settings.setTimeMode === "尽量调为白天") {
|
||
await pathingScript.runFile("assets/调为白天.json");
|
||
} else {
|
||
await pathingScript.runFile("assets/调为夜晚.json");
|
||
}
|
||
lastsettimeTime = new Date();
|
||
}
|
||
state = state || {}; // 若已存在则保持原引用,否则新建空对象
|
||
state.running = true;
|
||
|
||
/* ---------- 主任务 ---------- */
|
||
const pathingTask = (async () => {
|
||
log.info(`开始执行路线: ${fullPath}`);
|
||
await fakeLog(fullPath, false, true, 0);
|
||
await pathingScript.runFile(fullPath);
|
||
await fakeLog(fullPath, false, false, 0);
|
||
state.running = false;
|
||
})();
|
||
|
||
/* ---------- 伴随任务 ---------- */
|
||
|
||
const pickupTask = (async () => {
|
||
if (state.activatePickUp) {
|
||
await recognizeAndInteract();
|
||
}
|
||
})();
|
||
|
||
const errorProcessTask = (async () => {
|
||
const revivalRo1 = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/revival.png"));
|
||
let errorCheckCount = 9;
|
||
while (state.running) {
|
||
await sleep(100);
|
||
errorCheckCount++;
|
||
if (errorCheckCount > 50) {
|
||
errorCheckCount = 0;
|
||
//log.info("尝试识别并点击复苏按钮");
|
||
if (await findAndClick(revivalRo1, true, 2, 3)) {
|
||
log.info("识别到复苏按钮,点击复苏");
|
||
}
|
||
}
|
||
}
|
||
})();
|
||
|
||
/* ---------- 并发等待 ---------- */
|
||
await Promise.allSettled([pathingTask, pickupTask, errorProcessTask]);
|
||
}
|
||
|
||
//加载拾取物图片
|
||
async function loadTargetItems() {
|
||
const targetItemPath = 'assets/targetItems'; // 固定目录
|
||
const items = await readFolder(targetItemPath, false);
|
||
// 统一预加载模板
|
||
for (const it of items) {
|
||
it.template = file.ReadImageMatSync(it.fullPath);
|
||
it.itemName = it.fileName.replace(/\.png$/i, '');
|
||
}
|
||
return items;
|
||
}
|
||
|
||
// 定义一个函数用于拾取
|
||
async function recognizeAndInteract() {
|
||
//log.info("调试-开始执行图像识别与拾取任务");
|
||
let lastcenterYF = 0;
|
||
let lastItemName = "";
|
||
let fIcontemplate = file.ReadImageMatSync('assets/F_Dialogue.png');
|
||
let mainUITemplate = file.ReadImageMatSync("assets/MainUI.png");
|
||
let thisMoveUpTime = 0;
|
||
let lastMoveDown = 0;
|
||
|
||
gameRegion = captureGameRegion();
|
||
//主循环
|
||
while (state.running) {
|
||
gameRegion.dispose();
|
||
gameRegion = captureGameRegion();
|
||
let centerYF = await findFIcon();
|
||
if (!centerYF) {
|
||
if (await isMainUI()) {
|
||
if (new Date() - lastRoll >= 200) {
|
||
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
|
||
lastRoll = new Date();
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
//log.info(`调试-成功找到f图标,centerYF为${centerYF}`);
|
||
let foundTarget = false;
|
||
let itemName = await performTemplateMatch(centerYF);
|
||
if (itemName) {
|
||
//log.info(`调试-识别到物品${itemName}`);
|
||
if (Math.abs(lastcenterYF - centerYF) <= 20 && lastItemName === itemName) {
|
||
//log.info("调试-相同物品名和相近y坐标,本次不拾取");
|
||
await sleep(2 * pickupDelay);
|
||
lastcenterYF = -20;
|
||
lastItemName = null;
|
||
} else {
|
||
keyPress("F");
|
||
log.info(`交互或拾取:"${itemName}"`);
|
||
lastcenterYF = centerYF;
|
||
lastItemName = itemName;
|
||
await sleep(pickupDelay);
|
||
}
|
||
} else {
|
||
/*
|
||
log.info("识别失败,尝试截图");
|
||
await refreshTargetItems(centerYF);
|
||
lastItemName = "";
|
||
*/
|
||
}
|
||
|
||
if (!foundTarget) {
|
||
//log.info(`调试-执行滚轮动作`);
|
||
const currentTime = new Date().getTime();
|
||
if (currentTime - lastMoveDown > timeMoveUp) {
|
||
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
|
||
if (thisMoveUpTime === 0) thisMoveUpTime = currentTime;
|
||
if (currentTime - thisMoveUpTime >= timeMoveDown) {
|
||
lastMoveDown = currentTime;
|
||
thisMoveUpTime = 0;
|
||
}
|
||
} else {
|
||
await keyMouseScript.runFile(`assets/滚轮上翻.json`);
|
||
}
|
||
await sleep(rollingDelay);
|
||
}
|
||
}
|
||
|
||
async function performTemplateMatch(centerYF) {
|
||
try {
|
||
let result;
|
||
let itemName = null;
|
||
for (const targetItem of targetItems) {
|
||
//log.info(`正在尝试匹配${targetItem.itemName}`);
|
||
const cnLen = Math.min([...targetItem.itemName].filter(c => c >= '\u4e00' && c <= '\u9fff').length, 5);
|
||
const recognitionObject = RecognitionObject.TemplateMatch(
|
||
targetItem.template,
|
||
1219,
|
||
centerYF - 15,
|
||
12 + 28 * cnLen + 2,
|
||
30
|
||
);
|
||
|
||
recognitionObject.Threshold = TMthreshold;
|
||
recognitionObject.InitTemplate();
|
||
result = gameRegion.find(recognitionObject);
|
||
if (result.isExist()) {
|
||
itemName = targetItem.itemName;
|
||
break;
|
||
}
|
||
}
|
||
return itemName;
|
||
} catch (error) {
|
||
log.error(`模板匹配时发生异常: ${error.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function findFIcon() {
|
||
let recognitionObject = RecognitionObject.TemplateMatch(fIcontemplate, 1102, 335, 34, 400);
|
||
recognitionObject.Threshold = 0.95;
|
||
recognitionObject.InitTemplate();
|
||
try {
|
||
let result = gameRegion.find(recognitionObject);
|
||
if (result.isExist()) {
|
||
return Math.round(result.y + result.height / 2);
|
||
}
|
||
} catch (error) {
|
||
log.error(`识别图像时发生异常: ${error.message}`);
|
||
if (!state.running)
|
||
return null;
|
||
}
|
||
await sleep(checkDelay);
|
||
return null;
|
||
}
|
||
|
||
async function isMainUI() {
|
||
const recognitionObject = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150);
|
||
const maxAttempts = 1;
|
||
let attempts = 0;
|
||
|
||
while (attempts < maxAttempts && state.running) {
|
||
try {
|
||
const result = gameRegion.find(recognitionObject);
|
||
if (result.isExist()) return true;
|
||
} catch (error) {
|
||
log.error(`识别图像时发生异常: ${error.message}`);
|
||
if (!state.running) break;
|
||
return false;
|
||
}
|
||
attempts++;
|
||
await sleep(checkDelay);
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在指定区域内,用 0-9 的 PNG 模板做「多阈值 + 非极大抑制」数字识别,
|
||
* 最终把检测到的数字按左右顺序拼成一个整数返回。
|
||
*
|
||
* @param {string} numberPngFilePath - 存放 0.png ~ 9.png 的文件夹路径(不含文件名)
|
||
* @param {number} x - 待识别区域的左上角 x 坐标,默认 0
|
||
* @param {number} y - 待识别区域的左上角 y 坐标,默认 0
|
||
* @param {number} w - 待识别区域的宽度,默认 1920
|
||
* @param {number} h - 待识别区域的高度,默认 1080
|
||
* @param {number} maxThreshold - 模板匹配起始阈值,默认 0.95(最高可信度)
|
||
* @param {number} minThreshold - 模板匹配最低阈值,默认 0.8(最低可信度)
|
||
* @param {number} splitCount - 在 maxThreshold 与 minThreshold 之间做几次等间隔阈值递减,默认 3
|
||
* @param {number} maxOverlap - 非极大抑制时允许的最大重叠像素,默认 2;只要 x 或 y 方向重叠大于该值即视为重复框
|
||
*
|
||
* @returns {number} 识别出的整数;若没有任何有效数字框则返回 -1
|
||
*
|
||
* @example
|
||
* const mora = await numberTemplateMatch('摩拉数字', 860, 70, 200, 40);
|
||
* if (mora >= 0) console.log(`当前摩拉:${mora}`);
|
||
*/
|
||
async function numberTemplateMatch(
|
||
numberPngFilePath,
|
||
x = 0, y = 0, w = 1920, h = 1080,
|
||
maxThreshold = 0.95,
|
||
minThreshold = 0.8,
|
||
splitCount = 3,
|
||
maxOverlap = 2
|
||
) {
|
||
let ros = [];
|
||
for (let i = 0; i <= 9; i++) {
|
||
ros[i] = RecognitionObject.TemplateMatch(
|
||
file.ReadImageMatSync(`${numberPngFilePath}/${i}.png`), x, y, w, h);
|
||
}
|
||
|
||
function setThreshold(roArr, newThreshold) {
|
||
for (let i = 0; i < roArr.length; i++) {
|
||
roArr[i].Threshold = newThreshold;
|
||
roArr[i].InitTemplate();
|
||
}
|
||
}
|
||
|
||
const gameRegion = captureGameRegion();
|
||
const allCandidates = [];
|
||
|
||
/* 1. splitCount 次等间隔阈值递减 */
|
||
for (let k = 0; k < splitCount; k++) {
|
||
const curThr = maxThreshold - (maxThreshold - minThreshold) * k / Math.max(splitCount - 1, 1);
|
||
setThreshold(ros, curThr);
|
||
|
||
/* 2. 0-9 每个模板跑一遍,所有框都收 */
|
||
for (let digit = 0; digit <= 9; digit++) {
|
||
const res = gameRegion.findMulti(ros[digit]);
|
||
if (res.count === 0) continue;
|
||
|
||
for (let i = 0; i < res.count; i++) {
|
||
const box = res[i];
|
||
allCandidates.push({
|
||
digit: digit,
|
||
x: box.x,
|
||
y: box.y,
|
||
w: box.width,
|
||
h: box.height,
|
||
thr: curThr
|
||
});
|
||
}
|
||
}
|
||
|
||
}
|
||
gameRegion.dispose();
|
||
|
||
/* 3. 无结果提前返回 -1 */
|
||
if (allCandidates.length === 0) {
|
||
return -1;
|
||
}
|
||
|
||
/* 4. 非极大抑制(必须 x、y 两个方向重叠都 > maxOverlap 才视为重复) */
|
||
const adopted = [];
|
||
for (const c of allCandidates) {
|
||
let overlap = false;
|
||
for (const a of adopted) {
|
||
const xOverlap = Math.max(0, Math.min(c.x + c.w, a.x + a.w) - Math.max(c.x, a.x));
|
||
const yOverlap = Math.max(0, Math.min(c.y + c.h, a.y + a.h) - Math.max(c.y, a.y));
|
||
if (xOverlap > maxOverlap && yOverlap > maxOverlap) {
|
||
overlap = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!overlap) {
|
||
adopted.push(c);
|
||
//log.info(`在 [${c.x},${c.y},${c.w},${c.h}] 找到数字 ${c.digit},匹配阈值=${c.thr}`);
|
||
}
|
||
}
|
||
|
||
/* 5. 按 x 排序,拼整数;仍无有效框时返回 -1 */
|
||
if (adopted.length === 0) return -1;
|
||
adopted.sort((a, b) => a.x - b.x);
|
||
|
||
return adopted.reduce((num, item) => num * 10 + item.digit, 0);
|
||
}
|
||
|
||
/**
|
||
* 通用找图/找RO并可选点击(支持单图片文件路径、单RO、图片文件路径数组、RO数组)
|
||
* @param {string|string[]|RecognitionObject|RecognitionObject[]} target
|
||
* @param {boolean} [doClick=true] 是否点击
|
||
* @param {number} [timeout=3000] 识别时间上限(ms)
|
||
* @param {number} [interval=50] 识别间隔(ms)
|
||
* @param {number} [retType=0] 0-返回布尔;1-返回 Region 结果
|
||
* @param {number} [preClickDelay=50] 点击前等待
|
||
* @param {number} [postClickDelay=50] 点击后等待
|
||
* @returns {boolean|Region} 根据 retType 返回是否成功或最终 Region
|
||
*/
|
||
async function findAndClick(target,
|
||
doClick = true,
|
||
timeout = 3000,
|
||
interval = 50,
|
||
retType = 0,
|
||
preClickDelay = 50,
|
||
postClickDelay = 50) {
|
||
try {
|
||
// 1. 统一转成 RecognitionObject 数组
|
||
let ros = [];
|
||
if (Array.isArray(target)) {
|
||
ros = target.map(t =>
|
||
(typeof t === 'string')
|
||
? RecognitionObject.TemplateMatch(file.ReadImageMatSync(t))
|
||
: t
|
||
);
|
||
} else {
|
||
ros = [(typeof target === 'string')
|
||
? RecognitionObject.TemplateMatch(file.ReadImageMatSync(target))
|
||
: target];
|
||
}
|
||
|
||
const start = Date.now();
|
||
let found = null;
|
||
|
||
while (Date.now() - start <= timeout) {
|
||
const gameRegion = captureGameRegion();
|
||
try {
|
||
// 依次尝试每一个 ro
|
||
for (const ro of ros) {
|
||
const res = gameRegion.find(ro);
|
||
if (!res.isEmpty()) { // 找到
|
||
found = res;
|
||
if (doClick) {
|
||
await sleep(preClickDelay);
|
||
res.click();
|
||
await sleep(postClickDelay);
|
||
}
|
||
break; // 成功即跳出 for
|
||
}
|
||
}
|
||
if (found) break; // 成功即跳出 while
|
||
} finally {
|
||
gameRegion.dispose();
|
||
}
|
||
await sleep(interval); // 没找到时等待
|
||
}
|
||
|
||
// 3. 按需返回
|
||
return retType === 0 ? !!found : (found || null);
|
||
|
||
} catch (error) {
|
||
log.error(`执行通用识图时出现错误:${error.message}`);
|
||
return retType === 0 ? false : null;
|
||
}
|
||
} |