diff --git a/repo/js/背包材料统计/assets/Bagpack.png b/repo/js/背包材料统计/assets/Bagpack.png new file mode 100644 index 000000000..361984014 Binary files /dev/null and b/repo/js/背包材料统计/assets/Bagpack.png differ diff --git a/repo/js/背包材料统计/assets/CultivationItems.png b/repo/js/背包材料统计/assets/CultivationItems.png new file mode 100644 index 000000000..e5f84e511 Binary files /dev/null and b/repo/js/背包材料统计/assets/CultivationItems.png differ diff --git a/repo/js/背包材料统计/assets/Materials.png b/repo/js/背包材料统计/assets/Materials.png new file mode 100644 index 000000000..18dbd0be0 Binary files /dev/null and b/repo/js/背包材料统计/assets/Materials.png differ diff --git a/repo/js/背包材料统计/assets/SliderBottom.png b/repo/js/背包材料统计/assets/SliderBottom.png new file mode 100644 index 000000000..77ff48320 Binary files /dev/null and b/repo/js/背包材料统计/assets/SliderBottom.png differ diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js new file mode 100644 index 000000000..b051e22a7 --- /dev/null +++ b/repo/js/背包材料统计/main.js @@ -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": "0", + "I": "1", "l": "1", "i": "1", "1": "1", + "Z": "2", "z": "2", "2": "2", + "E": "3", "e": "3", "3": "3", + "A": "4", "a": "4", "4": "4", + "S": "5", "s": "5", "5": "5", + "G": "6", "b": "6", "6": "6", + "T": "7", "t": "7", "7": "7", + "B": "8", "b": "8", "8": "8", + "g": "9", "q": "9", "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(); +})(); diff --git a/repo/js/背包材料统计/manifest.json b/repo/js/背包材料统计/manifest.json new file mode 100644 index 000000000..abb53e329 --- /dev/null +++ b/repo/js/背包材料统计/manifest.json @@ -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" +} \ No newline at end of file diff --git a/repo/js/背包材料统计/settings.json b/repo/js/背包材料统计/settings.json new file mode 100644 index 000000000..720e35ef0 --- /dev/null +++ b/repo/js/背包材料统计/settings.json @@ -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摩拉、树脂(待做)" + } +] \ No newline at end of file