js,背包材料统计 (#719)

模板匹配材料,OCR识别数量。\n数字太小可能无法识别,用?代替。\n目前支持 养成道具 和 素材 的两个大类。\n材料种类数量或导入js本地\n图包文件夹images放入assets下\n链接看manifest.json说明
This commit is contained in:
JJMdzh
2025-05-11 17:10:57 +08:00
committed by GitHub
parent ad017b0625
commit a6bd9eaed1
7 changed files with 572 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1,528 @@
(async function () {
// 初始化游戏窗口大小和返回主界面
setGameMetrics(1920, 1080, 1);
// 配置参数
const pageScrollCount = 22; // 最多滑页次数
const OCRdelay = Math.min(99, Math.max(0, Math.floor(Number(settings.OcrDelay) || 10))); // OCR基准时长
// 材料分类映射表
const materialTypeMap = {
"锻造素材": "5",
"怪物掉落素材": "3",
"一般素材": "5",
"周本素材": "3",
"烹饪食材": "5",
"角色突破素材": "3",
"木材": "5",
"宝石": "3",
"鱼饵鱼类": "5",
"角色天赋素材": "3",
"武器突破素材": "3",
};
// 获取设置中的材料分类,默认为"一般素材"
const materialsCategory = settings.materials || "一般素材";
// 材料前位定义
const materialPriority = {
"锻造素材": 1,
"怪物掉落素材": 1,
"一般素材": 2,
"周本素材": 2,
"烹饪食材": 3,
"角色突破素材": 3,
"木材": 4,
"宝石": 4,
"鱼饵鱼类": 5,
"角色天赋素材": 5,
"武器突破素材": 6,
};
// 获取当前材料分类的前位
const currentPriority = materialPriority[materialsCategory];
const previousPriority = Math.max(1, currentPriority - 1); // 获取上一个前位
// log.info(`正在寻找前位为 "${previousPriority}" 的材料`);
// 获取上一个前位的所有材料分类
const previousPriorityMaterials = Object.keys(materialPriority)
.filter(mat => materialPriority[mat] === previousPriority);
// 获取当前材料分类的 menuOffset 对应值
const validValues = new Set([materialTypeMap[materialsCategory]]);
// 过滤出符合条件的材料分类
const finalFilteredMaterials = previousPriorityMaterials
.filter(mat => validValues.has(materialTypeMap[mat]));
// 根据材料分类获取对应的 menuOffset
const menuOffset = materialTypeMap[materialsCategory];
if (!menuOffset) {
log.error(`未找到材料分类 "${materialsCategory}" 的对应菜单偏移值`);
return;
}
// 提前计算所有动态坐标
const menuClickX = Math.round(575 + (Number(menuOffset) - 1) * 96.25); // 背包菜单的 X 坐标
// 自定义 basename 函数
function basename(filePath) {
const lastSlashIndex = filePath.lastIndexOf('\\'); // 或者使用 '/',取决于你的路径分隔符
return filePath.substring(lastSlashIndex + 1);
}
// OCR识别文本
async function recognizeText(ocrRegion, timeout = 10000, retryInterval = 20, maxAttempts = 10, maxFailures = 3) {
let startTime = Date.now();
let retryCount = 0;
let failureCount = 0; // 用于记录连续失败的次数
const results = [];
const frequencyMap = {}; // 用于记录每个结果的出现次数
const replacementMap = {
"O": "0", "o": "0", "Q": "0", "": "0",
"I": "1", "l": "1", "i": "1", "": "1",
"Z": "2", "z": "2", "": "2",
"E": "3", "e": "3", "": "3",
"A": "4", "a": "4", "": "4",
"S": "5", "s": "5", "": "5",
"G": "6", "b": "6", "": "6",
"T": "7", "t": "7", "": "7",
"B": "8", "b": "8", "": "8",
"g": "9", "q": "9", "": "9",
};
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
let captureRegion = captureGameRegion();
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.85; // 适当降低阈值以提高速度
let resList = captureRegion.findMulti(ocrObject);
if (resList.count === 0) {
failureCount++;
if (failureCount >= maxFailures) {
ocrRegion.x += 3; // 每次缩小6像素
ocrRegion.width -= 6; // 每次缩小6像素
retryInterval += 10;
if (ocrRegion.width <= 12) {
return { success: false };
}
}
retryCount++;
await sleep(retryInterval);
continue;
}
for (let res of resList) {
let text = res.text;
text = text.split('').map(char => replacementMap[char] || char).join('');
results.push(text);
if (!frequencyMap[text]) {
frequencyMap[text] = 0;
}
frequencyMap[text]++;
if (frequencyMap[text] >= 2) {
return { success: true, text: text };
}
}
await sleep(retryInterval);
}
const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]);
return sortedResults.length > 0 ? { success: true, text: sortedResults[0] } : { success: false };
}
// 滚动页面
async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) {
moveMouseTo(999, 750);
await sleep(50);
leftButtonDown();
const steps = Math.ceil(totalDistance / stepDistance);
for (let j = 0; j < steps; j++) {
const remainingDistance = totalDistance - j * stepDistance;
const moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance;
moveMouseBy(0, -moveDistance);
await sleep(delayMs);
}
await sleep(700);
leftButtonUp();
await sleep(100);
}
// 扫描材料
async function scanMaterials(materialsCategory) {
// 获取前位材料名单
const priorityMaterialNames = [];
for (const category of finalFilteredMaterials) {
const materialIconDir = `assets/images/${category}`;
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
priorityMaterialNames.push(name);
}
}
// 获取当前材料分类的材料图片文件夹路径
const materialIconDir = `assets/images/${materialsCategory}`;
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
// 创建材料种类集合
const materialCategories = [];
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
for (const filePath of materialIconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
materialCategories.push({ name: name, filePath: filePath });
allMaterials.add(name); // 将材料名称添加到集合中
}
// 已识别的材料集合,避免重复扫描
const recognizedMaterials = new Set();
// 扫描背包中的材料
const tolerance = 1; // 容错区间
const startX = 117;
const startY = 121;
const OffsetWidth = 147;
const columnWidth = 123;
const columnHeight = 750;
const maxColumns = 8;
// 用于存储图片名和材料数量的数组
const materialInfo = [];
const unmatchedMaterialNames = new Set();// 使用 Set 来存储未匹配的材料名称,确保不重复
// 是否已经开始计时
let hasFoundFirstMaterial = false;
// 记录上一次发现材料的时间
let lastFoundTime = null;
// 初始化标志变量,确保在整个扫描过程中保持状态
let foundPriorityMaterial = false;
let shouldEndScan = false;
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
// log.info(`第 ${scroll+1} 页`);
// 随机选择一句俏皮话
const scanPhrases = [
"扫描中... 太好啦,有这么多素材!",
"扫描中... 不错的珍宝!",
"扫描中... 侦查骑士,发现目标!",
"扫描中... 嗯哼,意外之喜!",
"扫描中... 嗯?",
"扫描中... 很好,没有放过任何角落!",
"扫描中... 会有烟花材料嘛?",
"扫描中... 嗯,这是什么?",
"扫描中... 这些宝藏积灰了,先清洗一下",
"扫描中... 哇!都是好东西!",
"扫描中... 不虚此行!",
"扫描中... 瑰丽的珍宝,令人欣喜。",
"扫描中... 是对长高有帮助的东西吗?",
"扫描中... 嗯!品相卓越!",
"扫描中... 虽无法比拟黄金,但终有价值。",
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
"扫描中... 房租和伙食费,都有着落啦!",
"扫描中... 还不赖。",
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
"扫描中... 运气还不错。",
];
// 创建一个数组,用于存储未使用的俏皮话
let tempPhrases = [...scanPhrases];
// 打乱数组顺序,确保随机性
tempPhrases.sort(() => Math.random() - 0.5);
// 记录扫描开始时间
let phrasesStartTime = Date.now();
// 扫描
const scanX = startX + (maxColumns - 1) * OffsetWidth;
const scanY = startY;
if (!foundPriorityMaterial) {
for (const name of priorityMaterialNames) {
if (recognizedMaterials.has(name)) {
continue; // 如果已经识别过,跳过
}
const filePath = `assets/images/${finalFilteredMaterials}/${name}.png`;
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载材料图库失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1146, scanY, columnWidth, columnHeight);
recognitionObject.threshold = 0.8; // 设置识别阈值
const result = captureGameRegion().find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
foundPriorityMaterial = true; // 标记找到前位材料
log.info(`发现前位材料: ${name},开始全列扫描`);
break; // 发现前位材料后,退出当前循环
}
}
}
// 如果找到前位材料,则进行全列扫描
if (foundPriorityMaterial) {
for (let column = maxColumns - 1; column >= 0; column--) {
const scanX = startX + column * OffsetWidth;
const scanY = startY;
for (const { name, filePath } of materialCategories) {
if (recognizedMaterials.has(name)) {
continue; // 如果已经识别过,跳过
}
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, scanY, columnWidth, columnHeight);
recognitionObject.threshold = 0.9; // 设置识别阈值
const result = captureGameRegion().find(recognitionObject);
if (result.isExist()) {
recognizedMaterials.add(name); // 标记为已识别
await moveMouseTo(result.x, result.y); // 移动鼠标至图片
await sleep(10);
const ocrRegion = {
x: result.x - 1 * tolerance,
y: result.y + 97 - 1 * tolerance,
width: 66 + 2 * tolerance,
height: 22 + 2 * tolerance
};
const ocrResult = await recognizeText(ocrRegion, 1000, OCRdelay, 10, 3);
if (ocrResult.success) {
materialInfo.push({ name: name, count: ocrResult.text });
} else {
log.warn("{芝麻大的数看不清(>ε<)}");
materialInfo.push({ name: name, count: "?" });
}
// 如果是第一次发现材料,开始计时
if (!hasFoundFirstMaterial) {
hasFoundFirstMaterial = true;
lastFoundTime = Date.now();
} else {
// 更新上一次发现材料的时间
lastFoundTime = Date.now();
}
}
}
}
}
// 每2秒输出一句俏皮话
const phrasesTime = Date.now();
if (phrasesTime - phrasesStartTime >= 2000) {
const selectedPhrase = tempPhrases.shift();
log.info(selectedPhrase);
if (tempPhrases.length === 0) {
tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5);
}
phrasesStartTime = phrasesTime;
}
// 检查材料识别情况
if (recognizedMaterials.size === allMaterials.size) {
log.info("所有材料均已识别!");
shouldEndScan = true;
break; // 立即退出当前循环
}
// 如果已经发现过材料检查是否超过3秒未发现新的材料
if (hasFoundFirstMaterial) {
const currentTime = Date.now();
if (currentTime - lastFoundTime > 5000) {
log.info("未发现新的材料,结束扫描");
shouldEndScan = true;
break; // 立即退出当前循环
}
// 如果未超过3秒继续扫描无需额外操作
}
// 检查是否已经滑到最后一页
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
sliderBottomRo.threshold = 0.8;
const sliderBottomResult = captureGameRegion().find(sliderBottomRo);
if (sliderBottomResult.isExist()) {
log.info("已到达最后一页!");
shouldEndScan = true;
break; // 如果识别到滑动条底部,终止滑动
}
// 如果还没有到达最后一页,继续滑页
if (scroll < pageScrollCount) {
await scrollPage(680, 10, 5);
await sleep(10); // 滑动后等待10毫秒
}
}
// 检查是否需要结束扫描
if (shouldEndScan) {
// 输出识别到的材料数量
log.info(`共识别到 ${recognizedMaterials.size} 种材料`);
const now = new Date();// 获取当前时间
const formattedTime = now.toLocaleString(); // 使用本地时间格式化
const allMaterialsArray = Array.from(allMaterials);
// 过滤 allMaterials找出不在 recognizedMaterials 中的材料名称
for (const name of allMaterials) {
if (!recognizedMaterials.has(name)) {
unmatchedMaterialNames.add(name); // 使用 Set 的 add 方法添加名称
}
}
const unmatchedMaterialNamesArray = Array.from(unmatchedMaterialNames);
// 写入本地文件
const filePath = "recognized_materials.txt";
const logContent = `\n${formattedTime}\n ${materialsCategory} 种类: ${recognizedMaterials.size} 数量: \n${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")}\n 未匹配的材料 种类: ${unmatchedMaterialNamesArray .length} 数量: \n${unmatchedMaterialNamesArray.join(",")}\n 图库的材料 种类: ${allMaterialsArray .length} 数量: \n${allMaterialsArray.join(",")}\n`;
const result = file.WriteTextSync(filePath, logContent, true); // 追加模式
if (result) {
log.info("成功将识别到的材料写入本地文件");
} else {
log.error("写入本地文件失败");
}
}
}
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38);
const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38);
const CultivationItemsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/CultivationItems.png"), 749, 30, 38, 38);
// 定义一个函数用于识别图像
async function recognizeImage(recognitionObject, timeout = 5000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 尝试识别图像
let imageResult = captureGameRegion().find(recognitionObject);
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 };
}
// 主逻辑函数
async function MaterialPath() {
const maxStage = 4; // 最大阶段数
let stage = 0; // 当前阶段
while (stage <= maxStage) {
switch (stage) {
case 0: // 返回主界面
await genshin.returnMainUi();
await sleep(500);
stage = 1;
break;
case 1: // 打开背包界面
keyPress("B"); // 打开背包界面
await sleep(1000);
// 尝试识别背包图标
let backpackResult = await recognizeImage(BagpackRo, 2000);
if (backpackResult.success) {
stage = 2; // 进入下一阶段
} else {
log.warn("未识别到背包图标,重新尝试");
stage = 0; // 回退到阶段0
}
break;
case 2: // 点击动态坐标
click(menuClickX, 75); // 点击菜单
await sleep(500);
stage = 3; // 进入下一阶段
break;
case 3: // 识别材料分类
let CategoryObject;
switch (materialsCategory) {
case "锻造素材":
case "一般素材":
case "烹饪食材":
case "木材":
case "鱼饵鱼类":
CategoryObject = MaterialsRo;
break;
case "怪物掉落素材":
case "周本素材":
case "角色突破素材":
case "宝石":
case "角色天赋素材":
case "武器突破素材":
CategoryObject = CultivationItemsRo;
break;
default:
log.error("未知的材料分类");
stage = 0; // 回退到阶段0
return;
}
// 尝试识别材料分类图标
let CategoryResult = await recognizeImage(CategoryObject, 2000);
if (CategoryResult.success && CategoryResult.x !== 0 && CategoryResult.y !== 0) {
log.info(`识别到${materialsCategory} 所在分类。`);
stage = 4; // 进入下一阶段
} else {
log.warn("未识别到材料分类图标,重新尝试");
stage = 2; // 回退到阶段2
}
break;
case 4: // 扫描材料
log.info("芭芭拉,冲鸭!");
await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端
await sleep(200);
leftButtonDown(); // 长按左键重置材料滑条
await sleep(300);
leftButtonUp();
await sleep(200);
// 调用扫描材料的逻辑
if (!await scanMaterials(materialsCategory)) {
// log.warn(`${pageScrollCount} 页扫描完。`);
}
// 扫描完成后,流程结束
stage = maxStage + 1; // 确保退出循环
break;
}
}
// 返回主界面
await genshin.returnMainUi();
log.info("扫描流程结束,返回主界面。");
}
// 执行主逻辑
await MaterialPath();
})();

View File

@@ -0,0 +1,14 @@
{
"manifest_version": 1,
"name": "背包材料统计 ",
"version": "1.3",
"bgi_version": "0.44.8",
"description": "默认四行为一页模板匹配材料OCR识别数量。\n数字太小可能无法识别用?代替。\n目前支持 养成道具 和 素材 的两个大类。\n材料种类数量或导入js本地\n图包文件夹images放入assets下\n链接https://share.weiyun.com/DVBGMPzU 密码sg7avi",
"authors": [
{
"name": "吉吉喵"
}
],
"settings_ui": "settings.json",
"main": "main.js"
}

View File

@@ -0,0 +1,30 @@
[
{
"name": "materials",
"type": "select",
"label": "选择材料范围(默认一般素材)",
"options": [
"一般素材",
"怪物掉落素材",
"烹饪食材",
"周本素材",
"木材",
"角色突破素材",
"鱼饵鱼类",
"锻造素材",
"宝石",
"角色天赋素材",
"武器突破素材",
]
},
{
"name": "OcrDelay",
"type": "input-text",
"label": "OCR基准时间默认:10 毫秒)"
},
{
"name": "ascension",
"type": "checkbox",
"label": "更大更慢更准推荐10~50\n\n=============\n数字太小可能无法识别用?代替。\n支持 养成道具、素材 两个大类。\n材料种类、数量会导入该js目录。\n=============\n摩拉、树脂待做"
}
]