Files
skyflag2022 dda0d0755a Update (#2997)
* 增加延迟

* 修改
2026-03-16 01:59:18 +08:00

600 lines
24 KiB
JavaScript
Raw Permalink 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 notify = settings.notify
let account = settings.userName || "默认账户";
(async function () {
// 设置分辨率和缩放
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
keyPress("B");//打开背包
await sleep(1000);
// 关闭弹窗
await close_expired_stuff_popup_window();
let enterAttempts = 0;
while (enterAttempts < 10) {
await click(642,36);
const clicked = await clickPNG("分解",false);
if (clicked) break; // 找到并点击成功就退出循环
await sleep(750);
enterAttempts++;
await genshin.returnMainUi();
await sleep(100);
keyPress("B");
await sleep(1000);
}
await clickPNG("时间顺序",true,1);
await sleep(200);
await clickPNG("筛选");
await sleep(200);
click(30, 30);
await sleep(100);
await clickPNG("重置");
await sleep(200);
await clickPNG("祝圣之霜定义");
await sleep(200);
await clickPNG("未装备");
await sleep(200);
await clickPNG("未锁定");
await sleep(200);
await clickPNG("确认");
await sleep(200);
click(30, 30);
await sleep(100);
const smallBottle = await getBottleCount('背包小瓶', 'assets/RecognitionObject/三星.png');
const bigBottle = await getBottleCount('背包大瓶', 'assets/RecognitionObject/四星.png');
await clickPNG("筛选");
await sleep(200);
click(30, 30);
await sleep(100);
await clickPNG("重置");
await sleep(200);
await clickPNG("确认");
click(30, 30);
await sleep(100);
//点击分解
await clickPNG("分解");
await sleep(1000);
// 识别已储存经验1570-880-1650-930
const digits = await numberTemplateMatch("assets/已储存经验数字", 1573, 885, 74, 36);
let initialValue = 0;
if (digits >= 0) {
initialValue = digits;
log.info(`已储存经验识别成功: ${initialValue}`);
} else {
log.warn(`已储存经验值识别失败使用默认值0`);
}
await clickPNG("快速选择");
await sleep(500);
// 识别不同星级狗粮数量
const starPositions = [
{ star: 1, y: 130 },
{ star: 2, y: 200 },
{ star: 3, y: 270 },
{ star: 4, y: 340 }
];
const starCounts = {};
for (const { star, y } of starPositions) {
const count = await numberTemplateMatch("assets/选中狗粮数字", 570, y, 60, 50);
if (count < 0) {
log.warn(`${star}星狗粮位置未识别到有效数字`);
starCounts[`star${star}`] = 0; // 设置默认值为0
}else{
starCounts[`star${star}`] = count;
log.info(`${star}星狗粮识别到${count}`);
}
}
// 计算狗粮经验值
const expStar1 = starCounts.star1 * 420;
const expStar2 = starCounts.star2 * 840;
const expStar3 = starCounts.star3 * 1260;
const expStar4 = starCounts.star4 * 2520;
const expStars = expStar1 + expStar2 + expStar3 + expStar4;
// 库存经验值
const expSmall = parseInt(smallBottle || 0) * 2500;
const expBig = parseInt(bigBottle || 0) * 10000;
const expStock = expSmall + expBig + initialValue;
// 合计
const totalExp = expStars + expStock;
const totalCount = starCounts.star1 + starCounts.star2 + starCounts.star3 + starCounts.star4;
// 预计算所有需要格式化的值
const formattedExpStar1 = await formatExp(expStar1);
const formattedExpStar2 = await formatExp(expStar2);
const formattedExpStar3 = await formatExp(expStar3);
const formattedExpStar4 = await formatExp(expStar4);
const formattedExpStars = await formatExp(expStars);
const formattedExpSmall = await formatExp(expSmall);
const formattedExpBig = await formatExp(expBig);
const formattedExpStock = await formatExp(expStock);
const formattedTotalExp = await formatExp(totalExp);
// 记录保存功能
const userName = await getUserName();
const recordPath = `assets/${userName}.txt`;
// 构建通知消息
let message = `📦 圣遗物经验统计\n`;
message += `👤 账户名:${userName}\n`;;
message += `\n`;
message += `📊 [狗粮数量统计]\n`;
message += `📦 总数量:${totalCount}\n`;
message += `⭐ 1星${starCounts.star1}${expStar1 > 0 ? `${formattedExpStar1}` : ''}\n`;
message += `⭐ 2星${starCounts.star2}${expStar2 > 0 ? `${formattedExpStar2}` : ''}\n`;
message += `⭐ 3星${starCounts.star3}${expStar3 > 0 ? `${formattedExpStar3}` : ''}\n`;
message += `⭐ 4星${starCounts.star4}${expStar4 > 0 ? `${formattedExpStar4}` : ''}\n`;
message += `💰 狗粮经验合计:${formattedExpStars}\n`;
message += `\n`;
message += `🧪 [经验瓶数量]\n`;
message += `🧪 小经验瓶:${smallBottle || 0}${expSmall > 0 ? `${formattedExpSmall}` : ''}\n`;
message += `🧪 大经验瓶:${bigBottle || 0}${expBig > 0 ? `${formattedExpBig}` : ''}\n`;
message += `💰 库存经验合计:${formattedExpStock}(含储存${initialValue}点)\n`;
message += `\n`;
message += `✨ 总经验:${formattedTotalExp}\n`;
// 获取本地保存的数据
const localData = await getLocalData(recordPath);
// 更新记录
await updateRecord(recordPath, initialValue, parseInt(smallBottle || 0), parseInt(bigBottle || 0), starCounts, totalExp);
// 构建基础日志信息
const formattedExpStarsLog = await formatExp(expStars);
const formattedExpSmallLog = await formatExp(expSmall);
const formattedExpBigLog = await formatExp(expBig);
const formattedExpStockLog = await formatExp(expStock);
const formattedTotalExpLog = await formatExp(totalExp);
const baseLog = `狗粮:${totalCount}个(1★${starCounts.star1} 2★${starCounts.star2} 3★${starCounts.star3} 4★${starCounts.star4}) | 狗粮经验:${formattedExpStarsLog} | 小瓶:${formattedExpSmallLog} 大瓶:${formattedExpBigLog} 储存:${initialValue} | 库存经验:${formattedExpStockLog} | 总计:${formattedTotalExpLog}`;
// 计算变化量并添加到通知消息
let logMessage = baseLog;
if (localData.initialized.initialValue && localData.initialized.smallBottle && localData.initialized.bigBottle &&
localData.initialized.star1 && localData.initialized.star2 && localData.initialized.star3 && localData.initialized.star4 &&
localData.initialized.totalExp) {
// 计算本地记录中的狗粮经验合计
const localExpStar1 = localData.starCounts.star1 * 420;
const localExpStar2 = localData.starCounts.star2 * 840;
const localExpStar3 = localData.starCounts.star3 * 1260;
const localExpStar4 = localData.starCounts.star4 * 2520;
const localExpStars = localExpStar1 + localExpStar2 + localExpStar3 + localExpStar4;
// 计算总经验和狗粮经验合计的变化
const diffTotalExp = totalExp - localData.totalExp;
const diffExpStars = expStars - localExpStars;
// 添加变化量到通知消息
message += `\n`;
if (diffTotalExp !== 0 || diffExpStars !== 0) {
if (diffTotalExp !== 0) {
const totalChangeDesc = diffTotalExp > 0 ? '增加' : '减少';
const formattedDiffTotalExp = await formatExp(Math.abs(diffTotalExp));
message += `📈 总经验${totalChangeDesc}${formattedDiffTotalExp}\n`;
}
if (diffExpStars !== 0) {
const expStarsChangeDesc = diffExpStars > 0 ? '增加' : '减少';
const formattedDiffExpStars = await formatExp(Math.abs(diffExpStars));
message += `🐶 未分解的狗粮经验${expStarsChangeDesc}${formattedDiffExpStars}`;
}
} else {
// 如果没有变化,输出不变
message += `📊 经验数据无变化`;
}
// 构建完整日志(包含变化信息)
if (diffTotalExp !== 0 || diffExpStars !== 0) {
const totalChangeDesc = diffTotalExp > 0 ? '增加' : '减少';
const expStarsChangeDesc = diffExpStars > 0 ? '增加' : '减少';
const formattedDiffTotalExpLog = await formatExp(Math.abs(diffTotalExp));
const formattedDiffExpStarsLog = await formatExp(Math.abs(diffExpStars));
logMessage = `总经验${totalChangeDesc}${formattedDiffTotalExpLog} | 狗粮经验${expStarsChangeDesc}${formattedDiffExpStarsLog} | ${baseLog}`;
} else {
// 如果没有变化,日志也显示不变
logMessage = `经验数据无变化 | ${baseLog}`;
}
}
if (settings.notify) {
notification.send(message);
}
log.info(logMessage);
await genshin.returnMainUi();
// 格式化经验值显示
async function formatExp(num) {
if (num >= 10000) {
// 直接除法并转换为字符串,保留所有有效小数
return `${(num / 10000).toString()}`;
} else {
return `${num}`;
}
}
/**
* 在指定区域内,用 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 = [];
try{
/* 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
});
}
}
}
}catch{
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);
}
async function clickPNG(png, doClick = true, maxAttempts = 40, Threshold = 0.9) {
const pngRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`assets/RecognitionObject/${png}.png`));
pngRo.Threshold = Threshold;
pngRo.InitTemplate();
return await findAndClick(pngRo, maxAttempts, doClick);
}
async function findAndClick(target, maxAttempts = 20, doClick) {
//log.info("调试-开始检查");
for (let i = 0; i < maxAttempts; i++) {
//log.info("调试-检查一次");
const rg = captureGameRegion();
try {
const res = rg.find(target);
if (res.isExist()) { if (doClick) await sleep(16), res.click(), await sleep(50); return true; }
} finally { rg.dispose(); }
if (i < maxAttempts - 1) await sleep(50);
}
return false;
}
async function close_expired_stuff_popup_window() {
const game_region = captureGameRegion();
const text_x = 850;
const text_y = 273;
const text_w = 225;
const text_h = 51;
const ocr_res = game_region.find(RecognitionObject.ocr(text_x, text_y, text_w, text_h));
if (ocr_res) {
if (ocr_res.text.includes("物品过期")) {
log.info("检测到物品过期");
click(1000, 750);
await sleep(1000);
}
}
game_region.dispose();
}
/**
* 识别背包中指定物品的数量
* @param {string} itemName - 物品名称(仅用于日志)
* @param {string} templatePath - 模板图片路径
* @returns {Promise<string>} 识别到的数字字符串(可能为空)
*/
async function getBottleCount(itemName, templatePath) {
const ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(templatePath));
ro.InitTemplate();
for (let i = 0; i < 5; i++) {
const rg = captureGameRegion();
try {
const res = rg.find(ro);
if (res.isExist()) {
const regionToCheck = { x: res.x, y: res.y + 20, width: 70, height: 20 };
// 使用numberTemplateMatch函数识别数字
const count = await numberTemplateMatch(
'assets/背包数字', // 数字模板文件夹路径
regionToCheck.x, regionToCheck.y, regionToCheck.width, regionToCheck.height
);
const digits = count === -1 ? '' : count.toString();
log.info(`识别到${itemName}数量为${digits}`);
//log.info(`识别到${itemName}识别区域为${regionToCheck.x}, ${regionToCheck.y}, ${regionToCheck.width}, ${regionToCheck.height}`)
return digits; // 成功识别即返回
}
} finally {
rg.dispose();
}
if (i < 5 - 1) await sleep(50);
}
return ''; // 未找到时返回空字符串
}
// 检验账户名
async function getUserName() {
account = account.trim();
// 账户名规则数字、中英文长度1-20字符
if (!account || !/^[\u4e00-\u9fa5A-Za-z0-9]{1,20}$/.test(account)) {
log.error(`账户名${account}违规暂时使用默认账户名请查看readme后修改`)
account = "默认账户";
}
return account;
}
/**
* 获取本地记录中最新的一组数据
* @param {string} filePath - 记录文件路径
* @returns {Promise<object>} 包含经验数据的对象
*/
async function getLocalData(filePath) {
// 初始化返回结果
const result = {
initialValue: null,
smallBottle: null,
bigBottle: null,
starCounts: {
star1: null,
star2: null,
star3: null,
star4: null
},
totalExp: null,
initialized: {
initialValue: false,
smallBottle: false,
bigBottle: false,
star1: false,
star2: false,
star3: false,
star4: false,
totalExp: false
}
};
try {
// 尝试读取文件,不存在则直接返回空结果
const content = await file.readText(filePath);
const lines = content.split('\n').filter(line => line.trim());
if (lines.length === 0) return result;
// 数据匹配正则
const initialValueRegex = /已储存经验-(\d+)/;
const smallBottleRegex = /小经验瓶-(\d+)/;
const bigBottleRegex = /大经验瓶-(\d+)/;
const star1Regex = /1星狗粮-(\d+)/;
const star2Regex = /2星狗粮-(\d+)/;
const star3Regex = /3星狗粮-(\d+)/;
const star4Regex = /4星狗粮-(\d+)/;
const totalExpRegex = /总经验-(\d+)/;
// 遍历前几条记录,寻找最新的一组完整数据
for (const line of lines) {
// 匹配已储存经验
if (!result.initialized.initialValue) {
const match = line.match(initialValueRegex);
if (match) {
result.initialValue = parseInt(match[1]);
result.initialized.initialValue = true;
}
}
// 匹配小经验瓶
if (!result.initialized.smallBottle) {
const match = line.match(smallBottleRegex);
if (match) {
result.smallBottle = parseInt(match[1]);
result.initialized.smallBottle = true;
}
}
// 匹配大经验瓶
if (!result.initialized.bigBottle) {
const match = line.match(bigBottleRegex);
if (match) {
result.bigBottle = parseInt(match[1]);
result.initialized.bigBottle = true;
}
}
// 匹配1星狗粮
if (!result.initialized.star1) {
const match = line.match(star1Regex);
if (match) {
result.starCounts.star1 = parseInt(match[1]);
result.initialized.star1 = true;
}
}
// 匹配2星狗粮
if (!result.initialized.star2) {
const match = line.match(star2Regex);
if (match) {
result.starCounts.star2 = parseInt(match[1]);
result.initialized.star2 = true;
}
}
// 匹配3星狗粮
if (!result.initialized.star3) {
const match = line.match(star3Regex);
if (match) {
result.starCounts.star3 = parseInt(match[1]);
result.initialized.star3 = true;
}
}
// 匹配4星狗粮
if (!result.initialized.star4) {
const match = line.match(star4Regex);
if (match) {
result.starCounts.star4 = parseInt(match[1]);
result.initialized.star4 = true;
}
}
// 匹配总经验
if (!result.initialized.totalExp) {
const match = line.match(totalExpRegex);
if (match) {
result.totalExp = parseInt(match[1]);
result.initialized.totalExp = true;
}
}
// 所有数据都找到,提前终止遍历
if (result.initialized.initialValue &&
result.initialized.smallBottle &&
result.initialized.bigBottle &&
result.initialized.star1 &&
result.initialized.star2 &&
result.initialized.star3 &&
result.initialized.star4 &&
result.initialized.totalExp) {
break;
}
}
return result;
} catch (error) {
// 文件不存在或读取错误时返回空结果
return result;
}
}
/**
* 更新记录文件
* @param {string} filePath - 记录文件路径
* @param {number} initialValue - 已储存经验
* @param {number} smallBottle - 小经验瓶数量
* @param {number} bigBottle - 大经验瓶数量
* @param {object} starCounts - 狗粮数量
* @param {number} totalExp - 总经验值
*/
async function updateRecord(filePath, initialValue, smallBottle, bigBottle, starCounts, totalExp) {
// 生成当前时间字符串
const now = new Date();
const timeStr = `${now.getFullYear()}/${
String(now.getMonth() + 1).padStart(2, '0')
}/${
String(now.getDate()).padStart(2, '0')
} ${
String(now.getHours()).padStart(2, '0')
}:${
String(now.getMinutes()).padStart(2, '0')
}:${
String(now.getSeconds()).padStart(2, '0')
}`;
// 生成记录
const records = [
`时间:${timeStr}-已储存经验-${initialValue}`,
`时间:${timeStr}-小经验瓶-${smallBottle}`,
`时间:${timeStr}-大经验瓶-${bigBottle}`,
`时间:${timeStr}-1星狗粮-${starCounts.star1}`,
`时间:${timeStr}-2星狗粮-${starCounts.star2}`,
`时间:${timeStr}-3星狗粮-${starCounts.star3}`,
`时间:${timeStr}-4星狗粮-${starCounts.star4}`,
`时间:${timeStr}-总经验-${totalExp}`
];
try {
let content = await file.readText(filePath);
let lines = content.split('\n').filter(line => line.trim());
if (lines.length === 0) {
// 文件为空,直接写入新记录
await file.writeText(filePath, records.join('\n'));
return true;
}
// 添加新记录到最前面
lines.unshift(...records);
// 只保留1年内的记录
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
const recentLines = lines.filter(line => {
const timeMatch = line.match(/时间:(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/);
if (!timeMatch) return false;
const lineTime = new Date(timeMatch[1]);
return lineTime >= oneYearAgo;
});
// 写入文件
await file.writeText(filePath, recentLines.join('\n'));
return true;
} catch (error) {
// 文件不存在时创建新文件
await file.writeText(filePath, records.join('\n'));
return true;
}
}
})();