背包统计JS增加独立名单拾取,增加弹窗模块,增加对怪物名的支持 (#2101)
* 背包js增加独立名单拾取,增加弹窗模块,增加对怪物名的支持 v2.5 增加独立名单拾取,增加弹窗模块,这个2个是背包材料统计JS运行时实时运行。增加对怪物名的支持 * Delete repo/js/背包材料统计/materialsCD directory * Add files via upload * Add files via upload 更新说明 * Update manifest.json
@@ -1,58 +1,37 @@
|
||||
// ==UserScript==
|
||||
// @name 背包统计采集系统
|
||||
// @version 2.42
|
||||
// @description 识别路径文件,根据材料数量,自动执行路线,或者主动选择材料类别,统计材料数量
|
||||
// @author 吉吉喵
|
||||
// @match 原神版本:5.6;BGI 版本:0.44.8
|
||||
// ==/UserScript==
|
||||
|
||||
/**
|
||||
* === 重要免责声明 ===
|
||||
* 1. 使用风险
|
||||
* - 本脚本为开源学习项目,禁止用于商业用途或违反游戏条款的行为。
|
||||
* - 滥用可能导致游戏账号封禁,开发者不承担任何直接或间接责任。
|
||||
*
|
||||
* 2. 责任限制
|
||||
* - 本脚本按“现状”提供,不承诺兼容性、安全性或功能完整性。
|
||||
* - 因使用本脚本导致的账号、数据、设备损失,开发者概不负责。
|
||||
*
|
||||
* 3. 禁止条款
|
||||
* - 严禁逆向工程、恶意篡改或用于外挂等非法用途。
|
||||
* - 如游戏运营商提出要求,开发者保留随时停止维护的权利。
|
||||
*
|
||||
* 使用即表示您已阅读并同意上述条款。
|
||||
*
|
||||
* Last Updated: 2025-09-23
|
||||
*/
|
||||
# 背包材料统计
|
||||
# 背包材料统计 v2.5
|
||||
|
||||
## 简介:
|
||||
背包材料统计,可统计背包养成道具、部分食物、素材的数量,并保存本地;还可根据设定数量,自动根据CD执行采集路径。
|
||||
可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:
|
||||
+ 1.自动判断材料CD,不需要管材料CD有没有好;
|
||||
+ 2.可以随意添加路径,能自动排除低效、无效路径;
|
||||
+ 3.有独立名单识别,不会交互路边的npc或是神像;
|
||||
+ 4.有实时的弹窗模块,提供了常见的几种;
|
||||
|
||||
## 文件结构
|
||||
|
||||
./📁BetterGI/📁User/📁JsScript/
|
||||
📁背包材料统计/
|
||||
📁pathing/
|
||||
📁 薄荷/
|
||||
📄 薄荷1.json
|
||||
📁 薄荷效率/
|
||||
📄 薄荷-吉吉喵.json
|
||||
📁 苹果/
|
||||
📄 旅行者的果园.json
|
||||
📁 名刀镡/
|
||||
📄 旅行者的名刀镡.json
|
||||
📁history_record/ 背包统计 自动生成,每类的历史记录(每类中旧纪录上限为365个)
|
||||
📁overwrite_record/ 背包统计 自动生成,每类的最新一次记录
|
||||
📁pathing_record/ 自动生成,路径运行时间记录
|
||||
📁materialsCD/ 路径CD管理 自带的CD文件
|
||||
📄 latest_record.txt 背包统计 自动生成,最新一次统计
|
||||
📁assets 图包
|
||||
## 用前须知:要有一点的动手能力!该JS不提供路径文件,需要文件夹操作
|
||||
|
||||
## 使用方法
|
||||
1. 将脚本添加至调度器。
|
||||
2. 右键点击脚本以修改JS自定义配置:刷怪、采集推荐:2.仅📁pathing材料;纯材料数量识别选3.仅【材料分类】勾选。
|
||||
3. 执行路径功能前,需要📁pathing有【材料正式名】的文件夹,具体可参考BetterGI\Repos\bettergi-scripts-list-git\repo\pathing目录,选取【材料正式名】的文件夹,内有子文件夹或者路径。复制到BetterGI\User\JsScript\背包材料统计\pathing目录,具体参考## 文件结构
|
||||
|
||||
## 基础教程(会订阅路径、添加JS的可跳过)
|
||||
+ 1.仓库 订阅 所需路径文件;
|
||||
+ 2.打开路径所在文件夹BetterGI\Repos\bettergi-scripts-list-git\repo\pathing;
|
||||
把地方特产、敌人与魔物、矿物、食材与炼金四个文件夹复制到背包材料统计JS目录BetterGI\User\JsScript\背包材料统计\pathing中;最好手动去除重复多余路径;
|
||||
+ 3.推荐根据队伍来把路径分组放置。比如
|
||||
背包统计刷怪组,适合挂机输出的队:火神 奶奶 钟离 万叶,能够胜任需要刷怪的路径;
|
||||
背包统计附魔材料组,附魔队:钟离 芭芭拉 久岐忍 砂糖或班尼特,适合附魔采集;
|
||||
背包统计采集组,生存队:迪希雅 芭芭拉 瑶瑶 草神。适合一般情况下的采集;
|
||||
+ 4.找到背包材料统计js,右键 背包材料统计,选择JS修改脚本自定义设置;
|
||||
|
||||
## JS的自定义设置说明
|
||||
+ 1.目标数量,是扫描背包中材料数量,只有低于目标的材料,其路径才会被纳入执行序列;
|
||||
+ 2.优先级材料,是最高优先级,直接无视上述目标数量,纳入执行序列最顶层;
|
||||
+ 3.时间成本,在一个路径存在3到5次记录时,会计算时间成本,单个材料获取时间超过默认30秒的,则跳过;
|
||||
+ 4.发送通知,每类材料跑完会通知一次,全部材料跑完会汇总通知一次。需在BGI通知里开启,接收端企业微信的使用自行寻找;
|
||||
+ 5.取消扫描数量,是取消每个路径执行完的扫描;当大部分需要的路径都有足够多记录,就可以不需要新增运行记录,可以不扫描了,以节约时间;但全部材料执行始末会各扫一次,以汇总材料信息。
|
||||
+ 6.仅pathing材料,是平衡背包材料统计和路径CD管理的选项;选择仅pathing材料,则直接无视下方勾选的材料分类,只扫描pathing文件夹已有的路径材料,没有的就不扫,以缩短扫描时间;
|
||||
+ 7.弹窗名,不填默认assets\imageClick文件夹下所有弹窗循环执行;弹窗模块会在背包统计运行时全程保护路径,防止弹窗影响路径执行;
|
||||
+ 8.采用的CD分类,不在materialsCD文件夹的不执行,这个文件夹可以按格式新增材料CD分类txt,只要背包材料统计的图库里有,路径所在的文件夹名正确,就能按CD执行;
|
||||
+ 9.targetText文件夹里的是独立名单识别的名单,可以按随意新增txt,内容按格式,英文冒号前的名字也随便写;冒号后的文字会被当成目标来识别,不在targetText文件夹下不识别。
|
||||
|
||||
## 注意
|
||||
因食物部分图片未补足,为适配快速滑页,苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪,这八个食物必须有,且在第一行。不然这几个食物会无法识别。
|
||||
@@ -81,7 +60,8 @@
|
||||
+ v2.27 修复计算材料数错误、目标数量临界值、"3"识别成"三"等bug
|
||||
+ v2.28 材料更变时初始数量更新;正常记录排除0位移和0数量的路径记录(可能是卡路径,需手动根据0记录去甄别),新增材料名0后缀本地记录;新增背包弹窗识别
|
||||
+ v2.29 新增排除提示;调整平均时间成本计算;过滤掉差异较大的记录;
|
||||
+ v2.30 更改路径专注模式默认值,加log提示;去除注释掉的调试log;背包材料统计更改名为背包统计采集系统
|
||||
+ v2.30 更改路径专注模式默认值,加log提示;去除注释掉的调试log;
|
||||
+ v2.40 优化背包识别时占用的内存;增加通知;
|
||||
+ v2.41 修复勾选分类的本地记录bug,新增仅背包统计选项;如果本地记录已经遭到破坏。比如每条路径都产生大量材料名-0.txt,就只能手动清理或者删除本地记录pathing_record,重新跑;
|
||||
+ v2.42 增加无路径间的扫描、无数量记录的noRecord模式,适合路径记录已经炼成的玩家。新增怪物材料CD文件,以支持轻度刷怪需求。
|
||||
+ v2.42 增加无路径间的扫描、无数量记录的noRecord模式,适合路径记录已经炼成的玩家。新增怪物材料CD文件,以支持轻度刷怪需求。
|
||||
+ v2.5 增加独立名单拾取,增加弹窗模块,这个2个是背包材料统计JS运行时实时运行。增加对怪物名的支持。
|
||||
BIN
repo/js/背包材料统计/assets/E_Dialogue.png
Normal file
|
After Width: | Height: | Size: 547 B |
BIN
repo/js/背包材料统计/assets/F_Dialogue.png
Normal file
|
After Width: | Height: | Size: 515 B |
53
repo/js/背包材料统计/assets/Monster-Materials.txt
Normal file
@@ -0,0 +1,53 @@
|
||||
丘丘人:破损的面具,污秽的面具,不祥的面具
|
||||
丘丘萨满:导能绘卷,封魔绘卷,禁咒绘卷
|
||||
丘丘人射手:锐利的箭簇,牢固的箭簇,历战的箭簇
|
||||
丘丘暴徒/丘丘王:沉重号角,黑铜号角,黑晶号角
|
||||
丘丘游侠:来自何处的待放之花,何人所珍藏之花,漫游者的盛放之花
|
||||
|
||||
愚人众先遣队:新兵的徽记,士官的徽记,尉官的徽记
|
||||
萤术士:雾虚灯芯,雾虚草囊,雾虚花粉
|
||||
债务处理人:特工祭刀,猎兵祭刀,督察长祭刀
|
||||
冬国仕女:黯淡棱晶,水晶棱晶,偏光棱晶
|
||||
愚人众风役人:老旧的役人怀表,役人的制式怀表,役人的时时刻刻
|
||||
愚人众特辖队:磨损的执凭,精致的执凭,霜镌的执凭
|
||||
|
||||
盗宝团:寻宝鸦印,藏银鸦印,攫金鸦印
|
||||
野伏众/海乱鬼:破旧的刀镡,影打刀镡,名刀镡
|
||||
镀金旅团:褪色红绸,镶边红绸,织金红绸
|
||||
黑蛇众/黯色空壳:晦暗刻像,夤夜刻像,幽邃刻像
|
||||
部族龙形武士:卫从的木哨,战士的铁哨,龙冠武士的金哨
|
||||
|
||||
遗迹机械:混沌装置,混沌回路,混沌炉心
|
||||
元能构装体:混浊棱晶,破缺棱晶,辉光棱晶
|
||||
遗迹机兵:混沌机关,混沌枢纽,混沌真眼
|
||||
遗迹龙兽:混沌锚栓,混沌模块,混沌容器
|
||||
发条机关:啮合齿轮,机关正齿轮,奇械机芯齿轮
|
||||
秘源机兵:秘源轴,秘源机鞘,秘源真芯
|
||||
巡陆艇:毁损机轴,精制机轴,加固机轴
|
||||
|
||||
史莱姆:史莱姆凝液,史莱姆原浆,史莱姆清
|
||||
骗骗花:骗骗花蜜,微光花蜜,原素花蜜
|
||||
飘浮灵:浮游干核,浮游幽核,浮游晶化核
|
||||
蕈兽:蕈兽孢子,荧光孢粉,孢囊晶尘
|
||||
蕈兽:失活菌核,休眠菌核,茁壮菌核
|
||||
浊水幻灵:浊水的一滴,浊水的一掬,初生的浊水幻灵
|
||||
原海异种:异海凝珠,异海之块,异色结晶石
|
||||
隙境原体:隙间之核,外世突触,异界生命核
|
||||
魔像禁卫:残毁的剑柄,裂断的剑柄,未熄的剑柄
|
||||
大灵显化身:意志破碎的残片,意志明晰的寄偶,意志巡游的符像
|
||||
熔岩游像:聚燃的石块,聚燃的命种,聚燃的游像眼
|
||||
|
||||
龙蜥:脆弱的骨片,结实的骨片,石化的骨片
|
||||
圣骸兽:残毁的横脊,密固的横脊,锲纹的横脊
|
||||
玄文兽:羽状鳍翅,月色鳍翅,渊光鳍翅
|
||||
纳塔龙众:稚嫩的尖齿,老练的坚齿,横行霸者的利齿
|
||||
炉壳山鼬:冷裂壳块,蕴热的背壳,明燃的棱状壳
|
||||
蕴光异兽:失光块骨,稀光遗骼,繁光躯外骸
|
||||
|
||||
深渊法师/深渊使徒:地脉的旧枝,地脉的枯叶,地脉的新芽
|
||||
兽境群狼:隐兽利爪,隐兽指爪,隐兽鬼爪
|
||||
深邃拟覆叶:折光的胚芽,迷光的蜷叶之心,惑光的阔叶
|
||||
荒野狂猎:幽雾片甲,幽雾兜盔,幽雾化形
|
||||
霜夜灵嗣:霜夜的煌荣,霜夜的柔辉,霜夜的残照
|
||||
|
||||
地脉花:流浪者的经验,冒险家的经验,大英雄的经验
|
||||
BIN
repo/js/背包材料统计/assets/XP.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 17 KiB |
BIN
repo/js/背包材料统计/assets/imageClick/信件/icon/Back4.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
repo/js/背包材料统计/assets/imageClick/调查/Picture/image6.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
repo/js/背包材料统计/assets/imageClick/调查/icon/123.png
Normal file
|
After Width: | Height: | Size: 698 B |
BIN
repo/js/背包材料统计/assets/imageClick/过期物品/Picture/image1.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
repo/js/背包材料统计/assets/imageClick/过期物品/icon/Confirm.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
3
repo/js/背包材料统计/assets/滚轮下翻.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{"macroEvents":[{"type":6,"mouseX":0,"mouseY":-120,"time":0},
|
||||
{"type":6,"mouseX":0,"mouseY":0,"time":5}],
|
||||
"info":{"name":"","description":"","x":0,"y":0,"width":1920,"height":1080,"recordDpi":1}}
|
||||
234
repo/js/背包材料统计/lib/autoPick.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
|
||||
// 自动拾取逻辑
|
||||
|
||||
*/
|
||||
|
||||
// 解析文件内容,提取分类信息
|
||||
function parseCategoryContent(content) {
|
||||
if (!content) {
|
||||
log.warn(`文件内容为空`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const lines = content.split('\n').map(line => line.trim());
|
||||
const categories = {};
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line.includes(':')) return;
|
||||
|
||||
const [category, items] = line.split(':');
|
||||
if (!category || !items) return;
|
||||
|
||||
categories[category.trim()] = items.split(',').map(item => item.trim()).filter(item => item !== '');
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
// 从 targetText 文件夹中读取分类信息
|
||||
function readtargetTextCategories(targetTextDir) {
|
||||
// log.info(`开始读取材料分类信息:${targetTextDir}`);
|
||||
const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1);
|
||||
const materialCategories = {};
|
||||
|
||||
for (const filePath of targetTextFilePaths) {
|
||||
if (state.cancelRequested) break;
|
||||
const content = file.readTextSync(filePath);
|
||||
if (!content) {
|
||||
log.error(`加载文件失败:${filePath}`);
|
||||
continue; // 跳过当前文件
|
||||
}
|
||||
|
||||
const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名
|
||||
materialCategories[sourceCategory] = parseCategoryContent(content);
|
||||
}
|
||||
// log.info(`完成材料分类信息读取,分类信息:${JSON.stringify(materialCategories, null, 2)}`);
|
||||
return materialCategories;
|
||||
}
|
||||
// 定义替换映射表
|
||||
const replacementMap = {
|
||||
"监": "盐",
|
||||
"炽": "烬",
|
||||
"盞": "盏",
|
||||
"攜": "携",
|
||||
"於": "于",
|
||||
"卵": "卯"
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行OCR识别并匹配目标文本
|
||||
* @param {string[]} targetTexts - 待匹配的目标文本列表
|
||||
* @param {Object} xRange - X轴范围 { min: number, max: number }
|
||||
* @param {Object} yRange - Y轴范围 { min: number, max: number }
|
||||
* @param {number} timeout - 超时时间(毫秒),默认200ms
|
||||
* @param {Object} ra - 图像捕获对象(外部传入,需确保已初始化)
|
||||
* @returns {Promise<Object[]>} 识别结果数组,包含匹配目标的文本及坐标信息
|
||||
*/
|
||||
async function performOcr(targetTexts, xRange, yRange, timeout = 10, ra = null) {
|
||||
// 正则特殊字符转义工具函数(避免替换时的正则语法错误)
|
||||
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
const startTime = Date.now(); // 记录开始时间,用于超时判断
|
||||
|
||||
// 2. 计算识别区域宽高(xRange.max - xRange.min 为宽度,y同理)
|
||||
const regionWidth = xRange.max - xRange.min;
|
||||
const regionHeight = yRange.max - yRange.min;
|
||||
if (regionWidth <= 0 || regionHeight <= 0) {
|
||||
throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`);
|
||||
}
|
||||
|
||||
// 在超时时间内循环重试识别(处理临时识别失败)
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
// 1. 检查图像捕获对象是否有效
|
||||
if (!ra) {
|
||||
throw new Error("图像捕获对象(ra)未初始化");
|
||||
}
|
||||
|
||||
// 3. 执行OCR识别(在指定区域内查找多结果)
|
||||
const resList = ra.findMulti(
|
||||
RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight)
|
||||
);
|
||||
|
||||
// 4. 处理识别结果(文本修正 + 目标匹配)
|
||||
const results = [];
|
||||
for (let i = 0; i < resList.count; i++) {
|
||||
const res = resList[i];
|
||||
let correctedText = res.text; // 原始识别文本
|
||||
log.info(`原始识别文本: ${res.text}`);
|
||||
|
||||
// 4.1 修正识别错误(替换错误字符)
|
||||
for (const [wrongChar, correctChar] of Object.entries(replacementMap)) {
|
||||
const escapedWrong = escapeRegExp(wrongChar); // 转义特殊字符
|
||||
correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar);
|
||||
}
|
||||
|
||||
// 4.2 检查是否包含任意目标文本(避免重复添加同个结果)
|
||||
const isTargetMatched = targetTexts.some(target => correctedText.includes(target));
|
||||
if (isTargetMatched) {
|
||||
results.push({
|
||||
text: correctedText,
|
||||
x: res.x,
|
||||
y: res.y,
|
||||
width: res.width,
|
||||
height: res.height
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 识别成功,返回结果(无论是否匹配到目标,均返回当前识别到的有效结果)
|
||||
return results;
|
||||
|
||||
} catch (error) {
|
||||
// 识别异常时记录日志,继续重试(直到超时)
|
||||
log.error(`OCR识别异常(将重试): ${error.message}`);
|
||||
// 短暂等待后重试,避免高频失败占用资源
|
||||
await sleep(1);
|
||||
}
|
||||
await sleep(5); // 每次间隔 5 毫秒
|
||||
}
|
||||
|
||||
// 超时未完成识别,返回空数组
|
||||
log.warn(`OCR识别超时(超过${timeout}ms)`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// const OCRdelay = Math.min(100, Math.max(0, Math.floor(Number(settings.OcrDelay) || 2))); // F识别基准时长
|
||||
|
||||
// 尝试找到 F 图标并返回其坐标
|
||||
async function findFIcon(recognitionObject, timeout = 10, ra = null) {
|
||||
let startTime = Date.now();
|
||||
while (Date.now() - startTime < timeout && !state.cancelRequested) {
|
||||
try {
|
||||
let result = ra.find(recognitionObject);
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
return { success: true, x: result.x, y: result.y, width: result.width, height: result.height };
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`识别图像时发生异常: ${error.message}`);
|
||||
if (state.cancelRequested) {
|
||||
break; // 如果请求了取消,则退出循环
|
||||
}
|
||||
return null;
|
||||
}
|
||||
await sleep(5); // 每次检测间隔 5 毫秒
|
||||
}
|
||||
if (state.cancelRequested) {
|
||||
log.info("图像识别任务已取消");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 对齐并交互目标
|
||||
async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame=null) {
|
||||
let lastLogTime = Date.now();
|
||||
// 记录每个材料的识别次数(文本+坐标 → 计数)
|
||||
const recognitionCount = new Map();
|
||||
|
||||
while (!state.completed && !state.cancelRequested) {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastLogTime >= 10000) { // 每5秒记录一次日志
|
||||
log.info("检测中...");
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
await sleep(50); // 关键50时可避免F多目标滚动中拾取错,背包js这边有弹窗模块,就没必要增加延迟降低效率了
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
// 尝试找到 F 图标
|
||||
let fRes = await findFIcon(fDialogueRo, 10, cachedFrame);
|
||||
if (!fRes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取 F 图标的中心点 Y 坐标
|
||||
let centerYF = fRes.y + fRes.height / 2;
|
||||
|
||||
// 在当前屏幕范围内进行 OCR 识别
|
||||
let ocrResults = await performOcr(targetTexts, textxRange, { min: fRes.y - 3, max: fRes.y + 37 }, 10, cachedFrame);
|
||||
|
||||
// 检查所有目标文本是否在当前页面中
|
||||
let foundTarget = false;
|
||||
for (let targetText of targetTexts) {
|
||||
let targetResult = ocrResults.find(res => res.text.includes(targetText));
|
||||
if (targetResult) {
|
||||
log.info(`找到目标文本: ${targetText}`);
|
||||
|
||||
// 生成唯一标识并更新识别计数(文本+Y坐标)
|
||||
const materialId = `${targetText}-${targetResult.y}`;
|
||||
recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1);
|
||||
|
||||
let centerYTargetText = targetResult.y + targetResult.height / 2;
|
||||
if (Math.abs(centerYTargetText - centerYF) <= texttolerance) {
|
||||
// log.info(`目标文本 '${targetText}' 和 F 图标水平对齐`);
|
||||
if (recognitionCount.get(materialId) >= 1) {
|
||||
keyPress("F"); // 执行交互操作
|
||||
// log.info(`F键执行成功,识别计数: ${recognitionCount.get(materialId)}`);
|
||||
|
||||
// F键后清除计数,确保单次交互
|
||||
recognitionCount.delete(materialId);
|
||||
}
|
||||
|
||||
foundTarget = true;
|
||||
break; // 成功交互后退出当前循环,但继续检测
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果在当前页面中没有找到任何目标文本,则滚动到下一页
|
||||
if (!foundTarget) {
|
||||
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
|
||||
// verticalScroll(-20);
|
||||
}
|
||||
if (state.cancelRequested) {
|
||||
break;
|
||||
}
|
||||
cachedFrame?.dispose();
|
||||
}
|
||||
|
||||
if (state.cancelRequested) {
|
||||
log.info("检测任务已取消");
|
||||
} else if (!state.completed) {
|
||||
log.error("未能找到正确的目标文本或未成功交互,跳过该目标文本");
|
||||
}
|
||||
}
|
||||
733
repo/js/背包材料统计/lib/backStats.js
Normal file
@@ -0,0 +1,733 @@
|
||||
eval(file.readTextSync("lib/region.js"));
|
||||
|
||||
const holdX = Math.min(1920, Math.max(0, Math.floor(Number(settings.HoldX) || 1050)));
|
||||
const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 750)));
|
||||
const totalPageDistance = Math.max(10, Math.floor(Number(settings.PageScrollDistance) || 711));
|
||||
const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量
|
||||
const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 await sleep(imageDelay);
|
||||
|
||||
// 配置参数
|
||||
const pageScrollCount = 22; // 最多滑页次数
|
||||
|
||||
// 材料分类映射表
|
||||
const materialTypeMap = {
|
||||
"锻造素材": "5",
|
||||
"怪物掉落素材": "3",
|
||||
"一般素材": "5",
|
||||
"周本素材": "3",
|
||||
"烹饪食材": "5",
|
||||
"角色突破素材": "3",
|
||||
"木材": "5",
|
||||
"宝石": "3",
|
||||
"鱼饵鱼类": "5",
|
||||
"角色天赋素材": "3",
|
||||
"武器突破素材": "3",
|
||||
"采集食物": "4",
|
||||
"料理": "4",
|
||||
};
|
||||
|
||||
// 材料前位定义
|
||||
const materialPriority = {
|
||||
"养成道具": 1,
|
||||
"祝圣精华": 2,
|
||||
"锻造素材": 1,
|
||||
"怪物掉落素材": 1,
|
||||
"采集食物": 1,
|
||||
"一般素材": 2,
|
||||
"周本素材": 2,
|
||||
"料理": 2,
|
||||
"烹饪食材": 3,
|
||||
"角色突破素材": 3,
|
||||
"木材": 4,
|
||||
"宝石": 4,
|
||||
"鱼饵鱼类": 5,
|
||||
"角色天赋素材": 5,
|
||||
"武器突破素材": 6,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 提前计算所有动态坐标
|
||||
// 物品区左顶处物品左上角坐标(117,121)
|
||||
// 物品图片大小(123,152)
|
||||
// 物品间隔(24,24)
|
||||
// 第一点击区位置:123/2+117=178.5; 152/2+121=197
|
||||
// const menuClickX = Math.round(575 + (Number(menuOffset) - 1) * 96.25); // 背包菜单的 X 坐标
|
||||
|
||||
// log.info(`材料分类: ${materialsCategory}, 菜单偏移值: ${menuOffset}, 计算出的点击 X 坐标: ${menuClickX}`);
|
||||
|
||||
// OCR识别文本
|
||||
async function recognizeText(ocrRegion, timeout = 1000, retryInterval = 20, maxAttempts = 10, maxFailures = 3, ra = null) {
|
||||
let startTime = Date.now();
|
||||
let retryCount = 0;
|
||||
let failureCount = 0; // 用于记录连续失败的次数
|
||||
// const results = [];
|
||||
const frequencyMap = {}; // 用于记录每个结果的出现次数
|
||||
|
||||
const numberReplaceMap = {
|
||||
"O": "0", "o": "0", "Q": "0", "0": "0",
|
||||
"I": "1", "l": "1", "i": "1", "1": "1", "一": "1",
|
||||
"Z": "2", "z": "2", "2": "2", "二": "2",
|
||||
"E": "3", "e": "3", "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", "θ": "8", "8": "8",
|
||||
"g": "9", "q": "9", "9": "9",
|
||||
};
|
||||
|
||||
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
|
||||
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
|
||||
ocrObject.threshold = 0.85; // 适当降低阈值以提高速度
|
||||
let resList = ra.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 false;
|
||||
}
|
||||
}
|
||||
retryCount++;
|
||||
await sleep(retryInterval);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let res of resList) {
|
||||
let text = res.text;
|
||||
text = text.split('').map(char => numberReplaceMap[char] || char).join('');
|
||||
// results.push(text);
|
||||
|
||||
if (!frequencyMap[text]) {
|
||||
frequencyMap[text] = 0;
|
||||
}
|
||||
frequencyMap[text]++;
|
||||
|
||||
if (frequencyMap[text] >= 2) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(retryInterval);
|
||||
}
|
||||
|
||||
const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]);
|
||||
return sortedResults.length > 0 ? sortedResults[0] : false;
|
||||
}
|
||||
|
||||
// 优化后的滑动页面函数(基于通用函数)
|
||||
async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) {
|
||||
await mouseDrag({
|
||||
holdMouseX: holdX, // 固定起点X
|
||||
holdMouseY: holdY, // 固定起点Y
|
||||
totalDistance: totalDistance, // 原逻辑中是向上滑动(原代码中是-moveDistance)
|
||||
stepDistance,
|
||||
stepInterval: delayMs,
|
||||
waitBefore: 50,
|
||||
waitAfter: 700, // 原逻辑中松开后等待700ms
|
||||
repeat: 1
|
||||
});
|
||||
}
|
||||
|
||||
// 通用鼠标拖动函数(提取重复逻辑)
|
||||
/**
|
||||
* 通用鼠标拖动工具函数
|
||||
* @param {Object} options 拖动参数
|
||||
* @param {number} [options.holdX] 拖动起点X坐标(默认当前鼠标位置)
|
||||
* @param {number} [options.holdY] 拖动起点Y坐标(默认当前鼠标位置)
|
||||
* @param {number} options.totalDistance 总拖动距离(垂直方向,正数向下,负数向上)
|
||||
* @param {number} [options.stepDistance=10] 每步拖动距离
|
||||
* @param {number} [options.stepInterval=20] 步间隔时间(ms)
|
||||
* @param {number} [options.waitBefore=50] 按下左键前的等待时间(ms)
|
||||
* @param {number} [options.waitAfter=100] 拖动结束后的等待时间(ms)
|
||||
* @param {number} [options.repeat=1] 重复拖动次数
|
||||
* @param {Object} [options.state] 状态对象(用于外部控制中断)
|
||||
*/
|
||||
async function mouseDrag({
|
||||
holdMouseX,
|
||||
holdMouseY,
|
||||
totalDistance,
|
||||
stepDistance = 10,
|
||||
stepInterval = 20,
|
||||
waitBefore = 50,
|
||||
waitAfter = 100,
|
||||
repeat = 1,
|
||||
state = { isRunning: true }
|
||||
}) {
|
||||
try {
|
||||
// 移动到起点(如果指定了起点)
|
||||
if (holdMouseX !== undefined && holdMouseY !== undefined) {
|
||||
moveMouseTo(holdMouseX, holdMouseY);
|
||||
await sleep(waitBefore);
|
||||
}
|
||||
|
||||
leftButtonDown();
|
||||
await sleep(waitBefore); // 按下后短暂等待
|
||||
|
||||
for (let r = 0; r < repeat; r++) {
|
||||
if (!state.isRunning) break;
|
||||
|
||||
const steps = Math.ceil(Math.abs(totalDistance) / stepDistance);
|
||||
const direction = totalDistance > 0 ? 1 : -1; // 方向:正向下,负向上
|
||||
|
||||
for (let s = 0; s < steps; s++) {
|
||||
if (!state.isRunning) break;
|
||||
// 计算当前步实际移动距离(最后一步可能不足stepDistance)
|
||||
const remaining = Math.abs(totalDistance) - s * stepDistance;
|
||||
const move = Math.min(stepDistance, remaining) * direction;
|
||||
moveMouseBy(0, move); // 垂直拖动
|
||||
await sleep(stepInterval);
|
||||
}
|
||||
|
||||
// 更新进度(如果有状态对象)
|
||||
if (state) {
|
||||
state.progress = Math.min(100, Math.round((r + 1) / repeat * 100));
|
||||
}
|
||||
await sleep(waitAfter);
|
||||
}
|
||||
|
||||
leftButtonUp();
|
||||
await sleep(waitBefore); // 松开后等待稳定
|
||||
} catch (e) {
|
||||
log.error(`拖动出错: ${e.message}`);
|
||||
leftButtonUp(); // 确保鼠标抬起,避免卡死
|
||||
}
|
||||
}
|
||||
|
||||
function filterMaterialsByPriority(materialsCategory) {
|
||||
// 获取当前材料分类的优先级
|
||||
const currentPriority = materialPriority[materialsCategory];
|
||||
if (currentPriority === undefined) {
|
||||
throw new Error(`Invalid materialsCategory: ${materialsCategory}`);
|
||||
}
|
||||
|
||||
// 获取当前材料分类的 materialTypeMap 对应值
|
||||
const currentType = materialTypeMap[materialsCategory];
|
||||
if (currentType === undefined) {
|
||||
throw new Error(`Invalid materialTypeMap for: ${materialsCategory}`);
|
||||
}
|
||||
|
||||
// 获取所有优先级更高的材料分类(前位材料)
|
||||
const frontPriorityMaterials = Object.keys(materialPriority)
|
||||
.filter(mat => materialPriority[mat] < currentPriority && materialTypeMap[mat] === currentType);
|
||||
|
||||
// 获取所有优先级更低的材料分类(后位材料)
|
||||
const backPriorityMaterials = Object.keys(materialPriority)
|
||||
.filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType);
|
||||
// 合并当前和后位材料分类
|
||||
const finalFilteredMaterials = [...backPriorityMaterials,materialsCategory ];// 当前材料
|
||||
return finalFilteredMaterials
|
||||
}
|
||||
|
||||
// 扫描材料
|
||||
async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
// 获取当前+后位材料名单
|
||||
const priorityMaterialNames = [];
|
||||
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
|
||||
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({ category, name });
|
||||
}
|
||||
}
|
||||
|
||||
// 根据材料分类获取对应的材料图片文件夹路径
|
||||
const materialIconDir = `assets/images/${materialsCategory}`;
|
||||
|
||||
// 使用 ReadPathSync 读取所有材料图片路径
|
||||
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
|
||||
|
||||
// 创建材料种类集合
|
||||
const materialCategories = [];
|
||||
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
|
||||
const materialImages = {}; // 用于缓存加载的图片
|
||||
|
||||
// 检查 materialCategoryMap 中当前分类的数组是否为空
|
||||
const categoryMaterials = materialCategoryMap[materialsCategory] || [];
|
||||
const shouldScanAllMaterials = categoryMaterials.length === 0; // 如果为空,则扫描所有材料
|
||||
|
||||
for (const filePath of materialIconFilePaths) {
|
||||
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
|
||||
|
||||
// 如果 materialCategoryMap 中当前分类的数组不为空
|
||||
// 且当前材料名称不在指定的材料列表中,则跳过加载
|
||||
if (pathingMode.onlyPathing && !shouldScanAllMaterials && !categoryMaterials.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载图标失败:${filePath}`);
|
||||
continue; // 跳过当前文件
|
||||
}
|
||||
|
||||
materialCategories.push({ name, filePath });
|
||||
allMaterials.add(name); // 将材料名称添加到集合中
|
||||
materialImages[name] = mat; // 缓存图片
|
||||
}
|
||||
|
||||
// 已识别的材料集合,避免重复扫描
|
||||
const recognizedMaterials = new Set();
|
||||
const unmatchedMaterialNames = new Set(); // 未匹配的材料名称
|
||||
const materialInfo = []; // 存储材料名称和数量
|
||||
|
||||
// 扫描参数
|
||||
const tolerance = 1;
|
||||
const startX = 117;
|
||||
const startY = 121;
|
||||
const OffsetWidth = 146.428; // 146.428
|
||||
const columnWidth = 123;
|
||||
const columnHeight = 680;
|
||||
const maxColumns = 8;
|
||||
// 跟踪已截图的区域(避免重复)
|
||||
const capturedRegions = new Set();
|
||||
|
||||
// 扫描状态
|
||||
let hasFoundFirstMaterial = false;
|
||||
let lastFoundTime = null;
|
||||
let shouldEndScan = false;
|
||||
let foundPriorityMaterial = false;
|
||||
|
||||
// 俏皮话逻辑
|
||||
const scanPhrases = [
|
||||
"扫描中... 太好啦,有这么多素材!",
|
||||
"扫描中... 不错的珍宝!",
|
||||
"扫描中... 侦查骑士,发现目标!",
|
||||
"扫描中... 嗯哼,意外之喜!",
|
||||
"扫描中... 嗯?",
|
||||
"扫描中... 很好,没有放过任何角落!",
|
||||
"扫描中... 会有烟花材料嘛?",
|
||||
"扫描中... 嗯,这是什么?",
|
||||
"扫描中... 这些宝藏积灰了,先清洗一下",
|
||||
"扫描中... 哇!都是好东西!",
|
||||
"扫描中... 不虚此行!",
|
||||
"扫描中... 瑰丽的珍宝,令人欣喜。",
|
||||
"扫描中... 是对长高有帮助的东西吗?",
|
||||
"扫描中... 嗯!品相卓越!",
|
||||
"扫描中... 虽无法比拟黄金,但终有价值。",
|
||||
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
|
||||
"扫描中... 房租和伙食费,都有着落啦!",
|
||||
"扫描中... 还不赖。",
|
||||
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
|
||||
"扫描中... 运气还不错。",
|
||||
];
|
||||
|
||||
let tempPhrases = [...scanPhrases];
|
||||
tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性
|
||||
let phrasesStartTime = Date.now();
|
||||
// 扫描背包中的材料
|
||||
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
|
||||
|
||||
const ra = captureGameRegion();
|
||||
if (!foundPriorityMaterial) {
|
||||
for (const { category, name } of priorityMaterialNames) {
|
||||
if (recognizedMaterials.has(name)) {
|
||||
continue; // 如果已经识别过,跳过
|
||||
}
|
||||
|
||||
const filePath = `assets/images/${category}/${name}.png`;
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载材料图库失败:${filePath}`);
|
||||
continue; // 跳过当前文件
|
||||
}
|
||||
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1142, startY, columnWidth, columnHeight);
|
||||
recognitionObject.threshold = 0.8; // 设置识别阈值
|
||||
recognitionObject.Use3Channels = true;
|
||||
|
||||
const result = ra.find(recognitionObject);
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
foundPriorityMaterial = true; // 标记找到前位材料
|
||||
// drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除
|
||||
log.info(`发现当前或后位材料: ${name},开始全列扫描`);
|
||||
break; // 发现前位材料后,退出当前循环
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPriorityMaterial) {
|
||||
for (let column = 0; column < maxColumns; column++) {
|
||||
const scanX0 = startX + column * OffsetWidth;
|
||||
const scanX = Math.round(scanX0);
|
||||
|
||||
|
||||
for (let i = 0; i < materialCategories.length; i++) {
|
||||
const { name } = materialCategories[i];
|
||||
if (recognizedMaterials.has(name)) {
|
||||
continue; // 如果已经识别过,跳过
|
||||
}
|
||||
|
||||
const mat = materialImages[name];
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
|
||||
recognitionObject.threshold = 0.85;
|
||||
recognitionObject.Use3Channels = true;
|
||||
|
||||
const result = ra.find(recognitionObject);
|
||||
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
recognizedMaterials.add(name);
|
||||
moveMouseTo(result.x, result.y);
|
||||
|
||||
// drawAndClearRedBox(result, ra, 100);// 调用异步函数绘制红框并延时清除
|
||||
const ocrRegion = {
|
||||
x: result.x - tolerance,
|
||||
y: result.y + 97 - tolerance,
|
||||
width: 66 + 2 * tolerance,
|
||||
height: 22 + 2 * tolerance
|
||||
};
|
||||
const ocrResult = await recognizeText(ocrRegion, 1000, 10, 10, 3, ra);
|
||||
materialInfo.push({ name, count: ocrResult || "?" });
|
||||
|
||||
if (!hasFoundFirstMaterial) {
|
||||
hasFoundFirstMaterial = true;
|
||||
lastFoundTime = Date.now();
|
||||
} else {
|
||||
lastFoundTime = Date.now();
|
||||
}
|
||||
}
|
||||
await sleep(imageDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 每5秒输出一句俏皮话
|
||||
const phrasesTime = Date.now();
|
||||
if (phrasesTime - phrasesStartTime >= 5000) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (hasFoundFirstMaterial && Date.now() - lastFoundTime > 5000) {
|
||||
log.info("未发现新的材料,结束扫描");
|
||||
shouldEndScan = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查是否到达最后一页
|
||||
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
|
||||
sliderBottomRo.threshold = 0.8;
|
||||
const sliderBottomResult = ra.find(sliderBottomRo);
|
||||
if (sliderBottomResult.isExist()) {
|
||||
log.info("已到达最后一页!");
|
||||
shouldEndScan = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 滑动到下一页
|
||||
if (scroll < pageScrollCount) {
|
||||
await scrollPage(-totalPageDistance, 10, 5);
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理未匹配的材料
|
||||
for (const name of allMaterials) {
|
||||
if (!recognizedMaterials.has(name)) {
|
||||
unmatchedMaterialNames.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 日志记录
|
||||
const now = new Date();
|
||||
const formattedTime = now.toLocaleString();
|
||||
const scanMode = shouldScanAllMaterials ? "全材料扫描" : "指定材料扫描";
|
||||
const logContent = `
|
||||
${formattedTime}
|
||||
${scanMode} - ${materialsCategory} 种类: ${recognizedMaterials.size} 数量:
|
||||
${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")}
|
||||
未匹配的材料 种类: ${unmatchedMaterialNames.size} 数量:
|
||||
${Array.from(unmatchedMaterialNames).join(",")}
|
||||
`;
|
||||
|
||||
const categoryFilePath = `history_record/${materialsCategory}.txt`; // 勾选【材料分类】的历史记录
|
||||
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`; // 所有的历史记录分类储存
|
||||
const latestFilePath = "latest_record.txt"; // 所有的历史记录集集合
|
||||
if (pathingMode.onlyCategory) {
|
||||
writeLog(categoryFilePath, logContent);
|
||||
}
|
||||
writeLog(overwriteFilePath, logContent);
|
||||
writeLog(latestFilePath, logContent); // 覆盖模式?
|
||||
|
||||
// 返回结果
|
||||
return materialInfo;
|
||||
}
|
||||
|
||||
function writeLog(filePath, logContent) {
|
||||
try {
|
||||
// 1. 读取现有内容(原样读取,不做任何分割处理)
|
||||
let existingContent = "";
|
||||
try {
|
||||
existingContent = file.readTextSync(filePath);
|
||||
} catch (e) {
|
||||
// 文件不存在则保持空
|
||||
}
|
||||
|
||||
// 2. 拼接新记录(新记录加在最前面,用两个换行分隔,保留原始格式)
|
||||
const finalContent = logContent + "\n\n" + existingContent;
|
||||
|
||||
// 3. 按行分割,保留最近365条完整记录(按原始换行分割,不过滤)
|
||||
const lines = finalContent.split("\n");
|
||||
const keepLines = lines.length > 365 * 5 ? lines.slice(0, 365 * 5) : lines; // 假设每条记录最多5行
|
||||
const result = file.writeTextSync(filePath, keepLines.join("\n"), false);
|
||||
|
||||
if (result) {
|
||||
log.info(`写入成功: ${filePath}`);
|
||||
} else {
|
||||
log.error(`写入失败: ${filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// 只在文件完全不存在时创建,避免覆盖
|
||||
file.writeTextSync(filePath, logContent, false);
|
||||
log.info(`创建新文件: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
|
||||
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);
|
||||
const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38);
|
||||
|
||||
const specialMaterials = [
|
||||
"水晶块", "魔晶块", "星银矿石", "紫晶块", "萃凝晶", "铁块", "白铁块",
|
||||
"精锻用魔矿", "精锻用良矿", "精锻用杂矿"
|
||||
];
|
||||
function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) {
|
||||
// 将 materialCategoryMap 中的所有材料名提取出来
|
||||
const allMaterials = Object.values(materialCategoryMap).flat();
|
||||
|
||||
// 筛选 pathingMaterialCounts 中的材料,只保留 materialCategoryMap 中定义的材料,并且数量低于 targetCount 或 count 为 "?" 或 name 在 specialMaterials 中
|
||||
return pathingMaterialCounts
|
||||
.filter(item =>
|
||||
allMaterials.includes(item.name) &&
|
||||
(item.count < targetCount || item.count === "?")
|
||||
)
|
||||
.map(item => {
|
||||
// 如果 name 在 specialMaterials 数组中
|
||||
if (specialMaterials.includes(item.name)) {
|
||||
// 如果 count 是 "?",直接保留
|
||||
if (item.count === "?") {
|
||||
return item;
|
||||
}
|
||||
// 否则,将 count 除以 10 并向下取整
|
||||
item.count = Math.floor(item.count / 10);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
function dynamicMaterialGrouping(materialCategoryMap) {
|
||||
// 初始化动态分组对象
|
||||
const dynamicMaterialGroups = {};
|
||||
|
||||
// 遍历 materialCategoryMap 的 entries
|
||||
for (const category in materialCategoryMap) {
|
||||
const type = materialTypeMap[category]; // 获取材料分类对应的组编号(3、4、5)
|
||||
if (!dynamicMaterialGroups[type]) {
|
||||
dynamicMaterialGroups[type] = []; // 初始化组
|
||||
}
|
||||
dynamicMaterialGroups[type].push(category); // 将分类加入对应组
|
||||
}
|
||||
|
||||
// 对每组内的材料分类按照 materialPriority 排序
|
||||
for (const type in dynamicMaterialGroups) {
|
||||
dynamicMaterialGroups[type].sort((a, b) => materialPriority[a] - materialPriority[b]);
|
||||
}
|
||||
|
||||
// 将分组结果转换为数组并按类型排序(3, 4, 5)
|
||||
const sortedGroups = Object.entries(dynamicMaterialGroups)
|
||||
.map(([type, categories]) => ({ type: parseInt(type), categories }))
|
||||
.sort((a, b) => a.type - b.type);
|
||||
|
||||
// 返回分组结果
|
||||
return sortedGroups;
|
||||
}
|
||||
|
||||
// 主逻辑函数
|
||||
async function MaterialPath(materialCategoryMap, cachedFrame = null) {
|
||||
|
||||
// 1. 先记录原始名称与别名的映射关系(用于最后反向转换)
|
||||
const nameMap = new Map();
|
||||
Object.values(materialCategoryMap).flat().forEach(originalName => {
|
||||
const aliasName = MATERIAL_ALIAS[originalName] || originalName;
|
||||
nameMap.set(aliasName, originalName); // 存储:别名→原始名
|
||||
});
|
||||
|
||||
// 2. 转换materialCategoryMap为别名(用于内部处理)
|
||||
const processedMap = {};
|
||||
Object.entries(materialCategoryMap).forEach(([category, names]) => {
|
||||
processedMap[category] = names.map(name => MATERIAL_ALIAS[name] || name);
|
||||
});
|
||||
materialCategoryMap = processedMap;
|
||||
|
||||
const maxStage = 4; // 最大阶段数
|
||||
let stage = 0; // 当前阶段
|
||||
let currentGroupIndex = 0; // 当前处理的分组索引
|
||||
let currentCategoryIndex = 0; // 当前处理的分类索引
|
||||
let materialsCategory = ""; // 当前处理的材料分类名称
|
||||
const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息
|
||||
|
||||
const sortedGroups = dynamicMaterialGrouping(materialCategoryMap);
|
||||
// log.info("材料 动态[分组]结果:");
|
||||
sortedGroups.forEach(group => {
|
||||
log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`);
|
||||
});
|
||||
|
||||
while (stage <= maxStage) {
|
||||
switch (stage) {
|
||||
case 0: // 返回主界面
|
||||
log.info("返回主界面");
|
||||
await genshin.returnMainUi();
|
||||
await sleep(500);
|
||||
stage = 1; // 进入下一阶段
|
||||
break;
|
||||
|
||||
case 1: // 打开背包界面
|
||||
// log.info("打开背包界面");
|
||||
keyPress("B"); // 打开背包界面
|
||||
await sleep(1000);
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
const backpackResult = await recognizeImage(BagpackRo, cachedFrame, 2000);
|
||||
if (backpackResult.isDetected) {
|
||||
// log.info("成功识别背包图标");
|
||||
stage = 2; // 进入下一阶段
|
||||
} else {
|
||||
log.warn("未识别到背包图标,重新尝试");
|
||||
stage = 0; // 回退
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // 按分组处理材料分类
|
||||
if (currentGroupIndex < sortedGroups.length) {
|
||||
const group = sortedGroups[currentGroupIndex];
|
||||
|
||||
if (currentCategoryIndex < group.categories.length) {
|
||||
materialsCategory = group.categories[currentCategoryIndex];
|
||||
const offset = materialTypeMap[materialsCategory];
|
||||
const menuClickX = Math.round(575 + (offset - 1) * 96.25);
|
||||
// log.info(`点击坐标 (${menuClickX},75)`);
|
||||
click(menuClickX, 75);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
stage = 3; // 进入下一阶段
|
||||
} else {
|
||||
currentGroupIndex++;
|
||||
currentCategoryIndex = 0; // 重置分类索引
|
||||
stage = 2; // 继续处理下一组
|
||||
}
|
||||
} else {
|
||||
stage = 5; // 跳出循环
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: // 识别材料分类
|
||||
let CategoryObject;
|
||||
switch (materialsCategory) {
|
||||
case "锻造素材":
|
||||
case "一般素材":
|
||||
case "烹饪食材":
|
||||
case "木材":
|
||||
case "鱼饵鱼类":
|
||||
CategoryObject = MaterialsRo;
|
||||
break;
|
||||
case "采集食物":
|
||||
case "料理":
|
||||
CategoryObject = FoodRo;
|
||||
break;
|
||||
case "怪物掉落素材":
|
||||
case "周本素材":
|
||||
case "角色突破素材":
|
||||
case "宝石":
|
||||
case "角色天赋素材":
|
||||
case "武器突破素材":
|
||||
CategoryObject = CultivationItemsRo;
|
||||
break;
|
||||
default:
|
||||
log.error("未知的材料分类");
|
||||
stage = 0; // 回退到阶段0
|
||||
return;
|
||||
}
|
||||
|
||||
const CategoryResult = await recognizeImage(CategoryObject, cachedFrame);
|
||||
if (CategoryResult.isDetected) {
|
||||
log.info(`识别到${materialsCategory} 所在分类。`);
|
||||
stage = 4; // 进入下一阶段
|
||||
} else {
|
||||
log.warn("未识别到材料分类图标,重新尝试");
|
||||
log.warn(`识别结果:${JSON.stringify(CategoryResult)}`);
|
||||
stage = 2; // 回退到阶段2
|
||||
}
|
||||
break;
|
||||
|
||||
case 4: // 扫描材料
|
||||
log.info("芭芭拉,冲鸭!");
|
||||
await moveMouseTo(1288, 124); // 移动鼠标至滑条顶端
|
||||
await sleep(200);
|
||||
leftButtonDown(); // 长按左键重置材料滑条
|
||||
await sleep(300);
|
||||
leftButtonUp();
|
||||
await sleep(200);
|
||||
|
||||
// 扫描材料并获取低于目标数量的材料
|
||||
const lowCountMaterials = await scanMaterials(materialsCategory, materialCategoryMap);
|
||||
allLowCountMaterials.push(lowCountMaterials);
|
||||
|
||||
currentCategoryIndex++;
|
||||
stage = 2; // 返回阶段2处理下一个分类
|
||||
break;
|
||||
|
||||
case 5: // 所有分组处理完毕
|
||||
log.info("所有分组处理完毕,返回主界面");
|
||||
await genshin.returnMainUi();
|
||||
stage = maxStage + 1; // 确保退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await genshin.returnMainUi(); // 返回主界面
|
||||
log.info("扫描流程结束");
|
||||
|
||||
|
||||
// 3. 处理完成后,将输出结果转换回原始名称
|
||||
const finalResult = allLowCountMaterials.map(categoryMaterials => {
|
||||
return categoryMaterials.map(material => {
|
||||
// 假设material包含name属性,将别名转回原始名
|
||||
return {
|
||||
...material,
|
||||
name: nameMap.get(material.name) || material.name // 反向映射
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
cachedFrame?.dispose();
|
||||
return finalResult; // 返回转换后的结果(如"晶蝶")
|
||||
}
|
||||
|
||||
|
||||
62
repo/js/背包材料统计/lib/displacement.js
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
/*
|
||||
|
||||
// 位移计算逻辑
|
||||
|
||||
*/
|
||||
|
||||
// 辅助函数:计算两点之间的距离
|
||||
function calculateDistance(initialPosition, finalPosition) {
|
||||
const deltaX = finalPosition.X - initialPosition.X;
|
||||
const deltaY = finalPosition.Y - initialPosition.Y;
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
}
|
||||
/*
|
||||
// 位移监测函数
|
||||
async function monitorDisplacement(monitoring, resolve) {
|
||||
// 获取对象的实际初始位置
|
||||
let lastPosition = genshin.getPositionFromMap();
|
||||
let cumulativeDistance = 0; // 初始化累计位移量
|
||||
let lastUpdateTime = Date.now(); // 记录上一次位置更新的时间
|
||||
|
||||
while (monitoring) {
|
||||
const currentPosition = genshin.getPositionFromMap(); // 获取当前位置
|
||||
const currentTime = Date.now(); // 获取当前时间
|
||||
|
||||
// 计算位移量
|
||||
const deltaX = currentPosition.X - lastPosition.X;
|
||||
const deltaY = currentPosition.Y - lastPosition.Y;
|
||||
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
// 如果位移量小于0.5,则视为0
|
||||
if (distance < 0.5) {
|
||||
distance = 0;
|
||||
}
|
||||
|
||||
// 如果有位移,更新累计位移量和最后更新时间
|
||||
if (distance > 0) {
|
||||
cumulativeDistance += distance; // 累计位移量
|
||||
lastUpdateTime = currentTime; // 更新最后更新时间
|
||||
}
|
||||
|
||||
// 检测是否超过5秒没有位移
|
||||
if (currentTime - lastUpdateTime >= 5000) {
|
||||
// 触发跳跃
|
||||
keyPress(VK_SPACE);
|
||||
lastUpdateTime = currentTime; // 重置最后更新时间
|
||||
}
|
||||
|
||||
// 输出位移信息和累计位移量
|
||||
log.info(`时间:${(currentTime - lastUpdateTime) / 1000}秒,位移信息: X=${currentPosition.X}, Y=${currentPosition.Y}, 当前位移量=${distance.toFixed(2)}, 累计位移量=${cumulativeDistance.toFixed(2)}`);
|
||||
|
||||
// 更新最后位置
|
||||
lastPosition = currentPosition;
|
||||
|
||||
// 等待1秒再次检查
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// 当监测结束时,返回累计位移量
|
||||
resolve(cumulativeDistance);
|
||||
}
|
||||
*/
|
||||
159
repo/js/背包材料统计/lib/exp.js
Normal file
@@ -0,0 +1,159 @@
|
||||
|
||||
// ===================== 狗粮模式专属函数 =====================
|
||||
// 1. 狗粮分解配置与OCR区域
|
||||
const AUTO_SALVAGE_CONFIG = {
|
||||
autoSalvage3: settings.autoSalvage3 || "是",
|
||||
autoSalvage4: settings.autoSalvage4 || "是"
|
||||
};
|
||||
const OCR_REGIONS = {
|
||||
expStorage: { x: 1472, y: 883, width: 170, height: 34 },
|
||||
expCount: { x: 1472, y: 895, width: 170, height: 34 }
|
||||
};
|
||||
|
||||
// 2. 数字替换映射表(处理OCR识别误差)
|
||||
const numberReplaceMap = {
|
||||
"O": "0", "o": "0", "Q": "0", "0": "0",
|
||||
"I": "1", "l": "1", "i": "1", "1": "1", "一": "1",
|
||||
"Z": "2", "z": "2", "2": "2", "二": "2",
|
||||
"E": "3", "e": "3", "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", "θ": "8", "8": "8",
|
||||
"g": "9", "q": "9", "9": "9",
|
||||
};
|
||||
|
||||
// 3. OCR文本处理
|
||||
function processExpText(text) {
|
||||
let correctedText = text || "";
|
||||
let removedSymbols = [];
|
||||
|
||||
// 替换错误字符
|
||||
for (const [wrong, correct] of Object.entries(numberReplaceMap)) {
|
||||
correctedText = correctedText.replace(new RegExp(wrong, 'g'), correct);
|
||||
}
|
||||
|
||||
// 提取纯数字
|
||||
let finalText = '';
|
||||
for (const char of correctedText) {
|
||||
if (/[0-9]/.test(char)) {
|
||||
finalText += char;
|
||||
} else if (char.trim() !== '') {
|
||||
removedSymbols.push(char);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
processedText: finalText,
|
||||
removedSymbols: [...new Set(removedSymbols)]
|
||||
};
|
||||
}
|
||||
|
||||
// 4. OCR识别EXP
|
||||
async function recognizeExpRegion(regionName, ra = null, timeout = 2000) {
|
||||
const ocrRegion = OCR_REGIONS[regionName];
|
||||
if (!ocrRegion) {
|
||||
log.error(`[狗粮OCR] 无效区域:${regionName}`);
|
||||
return { success: false, expCount: 0 };
|
||||
}
|
||||
|
||||
log.info(`[狗粮OCR] 识别${regionName}(x=${ocrRegion.x}, y=${ocrRegion.y})`);
|
||||
const startTime = Date.now();
|
||||
let retryCount = 0;
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const ocrResult = ra.find(RecognitionObject.ocr(
|
||||
ocrRegion.x,
|
||||
ocrRegion.y,
|
||||
ocrRegion.width,
|
||||
ocrRegion.height
|
||||
));
|
||||
log.info(`[狗粮OCR] 原始文本:${ocrResult.text}`);
|
||||
|
||||
if (ocrResult?.text) {
|
||||
const { processedText, removedSymbols } = processExpText(ocrResult.text);
|
||||
if (removedSymbols.length > 0) {
|
||||
log.info(`[狗粮OCR] 去除无效字符:${removedSymbols.join(', ')}`);
|
||||
}
|
||||
const expCount = processedText ? parseInt(processedText, 10) : 0;
|
||||
log.info(`[狗粮OCR] ${regionName}结果:${expCount}`);
|
||||
return { success: true, expCount };
|
||||
}
|
||||
} catch (error) {
|
||||
retryCount++;
|
||||
log.warn(`[狗粮OCR] ${regionName}第${retryCount}次识别失败:${error.message}`);
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
log.error(`[狗粮OCR] ${regionName}超时未识别,默认0`);
|
||||
return { success: false, expCount: 0 };
|
||||
}
|
||||
|
||||
// 5. 狗粮分解流程
|
||||
async function executeSalvageWithOCR() {
|
||||
log.info("[狗粮分解] 开始执行分解流程");
|
||||
let storageExp = 0;
|
||||
let countExp = 0;
|
||||
|
||||
let cachedFrame = null;
|
||||
|
||||
try {
|
||||
keyPress("B"); await sleep(1000);
|
||||
const coords = [
|
||||
[670, 40], // 打开背包
|
||||
[660, 1010], // 打开分解
|
||||
[300, 1020], // 打开分解选项页面
|
||||
// [200, 150, 500], // 勾选1星狗粮
|
||||
// [200, 220, 500], // 勾选2星狗粮
|
||||
[200, 300, 500, AUTO_SALVAGE_CONFIG.autoSalvage3 !== '否'], // 3星(按配置)
|
||||
[200, 380, 500, AUTO_SALVAGE_CONFIG.autoSalvage4 !== '否'], // 4星(按配置)
|
||||
[340, 1000], // 确认选择
|
||||
[1720, 1015], // 点击是否分解
|
||||
[1320, 756], // 确认分解
|
||||
[1840, 45, 1500], // 退出
|
||||
[1840, 45], // 退出
|
||||
[1840, 45], // 退出
|
||||
];
|
||||
|
||||
for (const coord of coords) {
|
||||
const [x, y, delay = 1000, condition = true] = coord;
|
||||
if (condition) {
|
||||
click(x, y);
|
||||
await sleep(delay);
|
||||
log.debug(`[狗粮分解] 点击(${x},${y}),延迟${delay}ms`);
|
||||
|
||||
// 分解前识别储存EXP
|
||||
if (x === 660 && y === 1010) {
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
const { expCount } = await recognizeExpRegion("expStorage", cachedFrame, 1000);
|
||||
storageExp = expCount;
|
||||
}
|
||||
|
||||
// 分解后识别新增EXP
|
||||
if (x === 340 && y === 1000) {
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
const { expCount } = await recognizeExpRegion("expCount", cachedFrame, 1000);
|
||||
countExp = expCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalExp = countExp - storageExp; // 分解新增EXP = 分解后 - 分解前
|
||||
log.info(`[狗粮分解] 完成,新增EXP:${totalExp}(分解前:${storageExp},分解后:${countExp})`);
|
||||
return { success: true, totalExp: Math.max(totalExp, 0) }; // 避免负数
|
||||
} catch (error) {
|
||||
log.error(`[狗粮分解] 失败:${error.message}`);
|
||||
return { success: false, totalExp: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 判断是否为狗粮资源(关键词匹配)
|
||||
function isFoodResource(resourceName) {
|
||||
const foodKeywords = ["12h狗粮", "24h狗粮"];
|
||||
return resourceName && foodKeywords.some(keyword => resourceName.includes(keyword));
|
||||
}
|
||||
153
repo/js/背包材料统计/lib/file.js
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
// ==============================================
|
||||
// 5. 角色识别与策略执行相关函数(保留原始功能)
|
||||
// ==============================================
|
||||
// 工具函数
|
||||
function basename(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') return '';
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const lastSlashIndex = normalizedPath.lastIndexOf('/');
|
||||
return lastSlashIndex !== -1 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath;
|
||||
}
|
||||
|
||||
/*
|
||||
// 如果路径存在且返回的是数组,则认为是目录Folder
|
||||
function pathExists(path) {
|
||||
try { return file.readPathSync(path)?.length >= 0; }
|
||||
catch { return false; }
|
||||
}
|
||||
*/
|
||||
|
||||
function pathExists(path) {
|
||||
try {
|
||||
return file.isFolder(path);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断文件是否存在(非目录且能读取)
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
// 先排除目录(是目录则不是文件)
|
||||
if (file.isFolder(filePath)) {
|
||||
return false;
|
||||
}
|
||||
// 尝试读取文件(能读则存在)
|
||||
file.readTextSync(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 带深度限制的非递归文件夹读取
|
||||
function readAllFilePaths(dir, depth = 0, maxDepth = 3, includeExtensions = ['.png', '.json', '.txt'], includeDirs = false) {
|
||||
if (!pathExists(dir)) {
|
||||
log.error(`目录 ${dir} 不存在`);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const filePaths = [];
|
||||
const stack = [[dir, depth]]; // 存储(路径, 当前深度)的栈
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [currentDir, currentDepth] = stack.pop();
|
||||
const entries = file.readPathSync(currentDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const isDirectory = pathExists(entry);
|
||||
if (isDirectory) {
|
||||
if (includeDirs) filePaths.push(entry);
|
||||
if (currentDepth < maxDepth) stack.push([entry, currentDepth + 1]);
|
||||
} else {
|
||||
const ext = entry.substring(entry.lastIndexOf('.')).toLowerCase();
|
||||
if (includeExtensions.includes(ext)) filePaths.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
// log.info(`完成目录 ${dir} 的递归读取,共找到 ${filePaths.length} 个文件`);
|
||||
return filePaths;
|
||||
} catch (error) {
|
||||
log.error(`读取目录 ${dir} 错误: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/*
|
||||
// 新记录在最下面
|
||||
async function writeFile(filePath, content, isAppend = false, maxRecords = 365) {
|
||||
try {
|
||||
if (isAppend) {
|
||||
// 读取现有内容(如果文件不存在则为空)
|
||||
const existingContent = file.readTextSync(filePath) || "";
|
||||
// 分割成记录数组(过滤空字符串)
|
||||
const records = existingContent.split("\n\n").filter(Boolean);
|
||||
|
||||
// 关键修复:将新内容添加到末尾,然后只保留最后maxRecords条
|
||||
const allRecords = [...records, content]; // 新内容放在最后
|
||||
const latestRecords = allRecords.slice(-maxRecords); // 整体截取最新的maxRecords条
|
||||
|
||||
// 拼接成最终内容
|
||||
const finalContent = latestRecords.join("\n\n");
|
||||
const result = file.WriteTextSync(filePath, finalContent, false);
|
||||
|
||||
// 日志输出(可根据需要启用)
|
||||
// log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`);
|
||||
return result;
|
||||
} else {
|
||||
// 非追加模式:直接覆盖写入
|
||||
const result = file.WriteTextSync(filePath, content, false);
|
||||
// log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`);
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// 发生错误时尝试创建文件并写入
|
||||
const result = file.WriteTextSync(filePath, content, false);
|
||||
log.info(result ? `[新建] 成功创建: ${filePath}` : `[新建] 创建失败: ${filePath}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
*/
|
||||
// 新记录在最上面20250531
|
||||
async function writeFile(filePath, content, isAppend = false, maxRecords = 365) {
|
||||
try {
|
||||
if (isAppend) {
|
||||
// 读取现有内容,处理文件不存在的情况
|
||||
let existingContent = "";
|
||||
try {
|
||||
existingContent = file.readTextSync(filePath);
|
||||
} catch (err) {
|
||||
// 文件不存在时视为空内容
|
||||
existingContent = "";
|
||||
}
|
||||
|
||||
// 分割现有记录并过滤空记录
|
||||
const records = existingContent.split("\n\n").filter(Boolean);
|
||||
|
||||
// 新内容放在最前面,形成完整记录列表
|
||||
const allRecords = [content, ...records];
|
||||
|
||||
// 只保留最新的maxRecords条(超过则删除最老的)
|
||||
const keptRecords = allRecords.slice(0, maxRecords);
|
||||
|
||||
// 拼接记录并写入文件
|
||||
const finalContent = keptRecords.join("\n\n");
|
||||
const result = file.WriteTextSync(filePath, finalContent, false);
|
||||
|
||||
// log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`);
|
||||
return result;
|
||||
} else {
|
||||
// 覆盖模式直接写入
|
||||
const result = file.WriteTextSync(filePath, content, false);
|
||||
// log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`);
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
// 出错时尝试创建/写入文件
|
||||
const result = file.WriteTextSync(filePath, content, false);
|
||||
log.info(result ? `[新建/恢复] 成功处理: ${filePath}` : `[新建/恢复] 处理失败: ${filePath}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
195
repo/js/背包材料统计/lib/imageClick.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// 新增:独立的预加载函数,负责所有资源预处理
|
||||
async function preloadImageResources(specificNames) {
|
||||
log.info("开始预加载所有图片资源");
|
||||
|
||||
// 统一参数格式(与原逻辑一致)
|
||||
let preSpecificNames = specificNames;
|
||||
if (typeof specificNames === 'string') {
|
||||
preSpecificNames = [specificNames];
|
||||
}
|
||||
const isAll = !preSpecificNames || preSpecificNames.length === 0;
|
||||
if (isAll) {
|
||||
log.info("未指定具体弹窗名称,将执行所有弹窗目录处理");
|
||||
} else {
|
||||
log.info(`指定处理弹窗名称:${preSpecificNames.join(', ')}`);
|
||||
}
|
||||
|
||||
// 定义根目录(与原代码一致)
|
||||
const rootDir = "assets/imageClick";
|
||||
|
||||
// 获取所有子目录(与原代码一致)
|
||||
const subDirs = readAllFilePaths(rootDir, 0, 0, [], true);
|
||||
|
||||
// 筛选目标目录(与原代码一致)
|
||||
const targetDirs = isAll
|
||||
? subDirs
|
||||
: subDirs.filter(subDir => {
|
||||
const dirName = basename(subDir);
|
||||
return preSpecificNames.includes(dirName);
|
||||
});
|
||||
|
||||
if (targetDirs.length === 0) {
|
||||
log.info(`未找到与指定名称匹配的目录,名称列表:${preSpecificNames?.join(', ') || '所有'}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 预加载所有目录的资源(原imageClick内的资源加载逻辑)
|
||||
const preloadedResources = [];
|
||||
for (const subDir of targetDirs) {
|
||||
const dirName = basename(subDir);
|
||||
// log.info(`开始预处理弹窗类型:${dirName}`);
|
||||
|
||||
// 查找icon和Picture文件夹(与原代码一致)
|
||||
const entries = readAllFilePaths(subDir, 0, 1, [], true);
|
||||
const iconDir = entries.find(entry => entry.endsWith('\icon'));
|
||||
const pictureDir = entries.find(entry => entry.endsWith('\Picture'));
|
||||
|
||||
if (!iconDir) {
|
||||
log.warn(`未找到 icon 文件夹,跳过分类文件夹:${subDir}`);
|
||||
continue;
|
||||
}
|
||||
if (!pictureDir) {
|
||||
log.warn(`未找到 Picture 文件夹,跳过分类文件夹:${subDir}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 读取图片文件(与原代码一致)
|
||||
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
|
||||
// 预创建icon识别对象(与原代码一致)
|
||||
const iconRecognitionObjects = [];
|
||||
for (const filePath of iconFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载图标失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080);
|
||||
iconRecognitionObjects.push({ name: basename(filePath), ro: recognitionObject, iconDir });
|
||||
}
|
||||
|
||||
// 预创建图库区域(与原代码一致)
|
||||
const pictureRegions = [];
|
||||
for (const filePath of pictureFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载图库图片失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
pictureRegions.push({ name: basename(filePath), region: new ImageRegion(mat, 0, 0) });
|
||||
}
|
||||
|
||||
// 预计算匹配区域(与原代码一致)
|
||||
const foundRegions = [];
|
||||
for (const picture of pictureRegions) {
|
||||
for (const icon of iconRecognitionObjects) {
|
||||
const foundRegion = picture.region.find(icon.ro);
|
||||
if (foundRegion.isExist()) {
|
||||
foundRegions.push({
|
||||
pictureName: picture.name,
|
||||
iconName: icon.name,
|
||||
region: foundRegion,
|
||||
iconDir: icon.iconDir
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存预处理结果
|
||||
preloadedResources.push({
|
||||
dirName,
|
||||
foundRegions
|
||||
});
|
||||
}
|
||||
|
||||
log.info(`预加载完成,共处理 ${preloadedResources.length} 个目录`);
|
||||
return preloadedResources;
|
||||
}
|
||||
|
||||
// 新增:imageClick后台任务函数
|
||||
async function imageClickBackgroundTask() {
|
||||
log.info("imageClick后台任务已启动");
|
||||
const imageClickDelay = Math.min(99, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 5)))*1000;
|
||||
// 可以根据需要配置要处理的弹窗名称
|
||||
const specificNamesStr = settings.PopupNames || "";
|
||||
const specificNames = specificNamesStr
|
||||
.split(/[,,、 \s]+/)
|
||||
.map(name => name.trim())
|
||||
.filter(name => name !== "");
|
||||
|
||||
// 调用独立预加载函数(循环前仅执行一次)
|
||||
const preloadedResources = await preloadImageResources(specificNames);
|
||||
if (preloadedResources.length === 0) {
|
||||
log.info("无可用预加载资源,任务结束");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// 循环执行,仅使用预加载资源
|
||||
while (!state.completed && !state.cancelRequested) {
|
||||
try {
|
||||
// 调用imageClick时传入预加载资源
|
||||
await imageClick(preloadedResources, null, specificNames, true);
|
||||
} catch (error) {
|
||||
log.info(`弹窗识别失败(继续重试):${error.message}`);
|
||||
}
|
||||
// 短暂等待后再次执行
|
||||
await sleep(imageClickDelay);
|
||||
}
|
||||
|
||||
log.info("imageClick后台任务已结束");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 优化:使用预加载资源,保留所有原执行逻辑
|
||||
async function imageClick(preloadedResources, ra = null, specificNames = null, useNewScreenshot = false) {
|
||||
// 保留原参数格式处理(兼容历史调用)
|
||||
if (typeof specificNames === 'string') {
|
||||
specificNames = [specificNames];
|
||||
}
|
||||
const isAll = !specificNames || specificNames.length === 0;
|
||||
|
||||
// 遍历预处理好的资源(原targetDirs循环逻辑)
|
||||
for (const resource of preloadedResources) {
|
||||
const { dirName, foundRegions } = resource;
|
||||
for (const foundRegion of foundRegions) {
|
||||
// 保留原识别对象创建逻辑(使用预处理的路径)
|
||||
const tolerance = 1;
|
||||
const iconMat = file.readImageMatSync(`${foundRegion.iconDir}/${foundRegion.iconName}`);
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(
|
||||
iconMat,
|
||||
foundRegion.region.x - tolerance,
|
||||
foundRegion.region.y - tolerance,
|
||||
foundRegion.region.width + 2 * tolerance,
|
||||
foundRegion.region.height + 2 * tolerance
|
||||
);
|
||||
recognitionObject.threshold = 0.85;
|
||||
|
||||
// 保留原识别逻辑
|
||||
const result = await recognizeImage(
|
||||
recognitionObject,
|
||||
ra,
|
||||
1000, // timeout
|
||||
500, // interval
|
||||
useNewScreenshot,
|
||||
dirName // iconType
|
||||
);
|
||||
|
||||
// 保留原点击逻辑
|
||||
if (result.isDetected && result.x !== 0 && result.y !== 0) {
|
||||
const x = Math.round(result.x + result.width / 2);
|
||||
const y = Math.round(result.y + result.height / 2);
|
||||
log.info(`即将点击【${dirName}】类型下的图标:${foundRegion.iconName},位置: (${x}, ${y})`);
|
||||
await click(x, y);
|
||||
log.info(`点击【${dirName}】类型下的${foundRegion.iconName}成功`);
|
||||
await sleep(10);
|
||||
return { success: true };
|
||||
} else {
|
||||
// log.info(`未发现弹窗【${dirName}】的图标:${foundRegion.iconName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有目标处理完毕仍未成功(保留原返回逻辑)
|
||||
return { success: false };
|
||||
}
|
||||
151
repo/js/背包材料统计/lib/region.js
Normal file
@@ -0,0 +1,151 @@
|
||||
|
||||
// ###########################################################################
|
||||
// 【核心工具函数】
|
||||
// ###########################################################################
|
||||
|
||||
var globalLatestRa = null;
|
||||
async function recognizeImage(
|
||||
recognitionObject,
|
||||
ra,
|
||||
timeout = 1000,
|
||||
interval = 500,
|
||||
useNewScreenshot = false,
|
||||
iconType = null
|
||||
) {
|
||||
let startTime = Date.now();
|
||||
globalLatestRa = ra;
|
||||
const originalRa = ra;
|
||||
let tempRa = null; // 用于管理临时创建的资源
|
||||
|
||||
try {
|
||||
while (Date.now() - startTime < timeout) {
|
||||
let currentRa;
|
||||
if (useNewScreenshot) {
|
||||
// 释放之前的临时资源
|
||||
if (tempRa) {
|
||||
tempRa.dispose();
|
||||
}
|
||||
tempRa = captureGameRegion();
|
||||
currentRa = tempRa;
|
||||
globalLatestRa = currentRa;
|
||||
} else {
|
||||
// 不使用新截图时直接使用原始ra,不重复释放
|
||||
currentRa = originalRa;
|
||||
}
|
||||
|
||||
if (currentRa) {
|
||||
try {
|
||||
const result = currentRa.find(recognitionObject);
|
||||
if (result.isExist() && result.x !== 0 && result.y !== 0) {
|
||||
return {
|
||||
isDetected: true,
|
||||
iconType: iconType,
|
||||
x: result.x,
|
||||
y: result.y,
|
||||
width: result.width,
|
||||
height: result.height,
|
||||
ra: globalLatestRa,
|
||||
usedNewScreenshot: useNewScreenshot
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`【${iconType || '未知'}识别异常】: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(interval);
|
||||
}
|
||||
} finally {
|
||||
// 释放临时资源但保留全局引用的资源
|
||||
if (tempRa && tempRa !== globalLatestRa) {
|
||||
tempRa.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDetected: false,
|
||||
iconType: iconType,
|
||||
x: null,
|
||||
y: null,
|
||||
width: null,
|
||||
height: null,
|
||||
ra: globalLatestRa,
|
||||
usedNewScreenshot: useNewScreenshot
|
||||
};
|
||||
}
|
||||
|
||||
// 定义一个异步函数来绘制红框并延时清除
|
||||
async function drawAndClearRedBox(searchRegion, ra, delay = 500) {
|
||||
let drawRegion = null;
|
||||
try {
|
||||
// 创建绘制区域
|
||||
drawRegion = ra.DeriveCrop(
|
||||
searchRegion.x, searchRegion.y,
|
||||
searchRegion.width, searchRegion.height
|
||||
);
|
||||
drawRegion.DrawSelf("icon"); // 绘制红框
|
||||
|
||||
// 等待指定时间
|
||||
await sleep(delay);
|
||||
|
||||
// 清除红框 - 使用更可靠的方式
|
||||
if (drawRegion && typeof drawRegion.DrawSelf === 'function') {
|
||||
// 可能需要使用透明绘制来清除,或者绘制一个0大小的区域
|
||||
ra.DeriveCrop(0, 0, 0, 0).DrawSelf("icon");
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("红框绘制异常:" + e.message);
|
||||
} finally {
|
||||
// 正确释放资源,如果dispose方法存在的话
|
||||
if (drawRegion && typeof drawRegion.dispose === 'function') {
|
||||
drawRegion.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截图保存函数
|
||||
function imageSaver(mat, saveFile) {
|
||||
// 获取当前时间并格式化为 "YYYY-MM-DD_HH-MM-SS"
|
||||
const now = new Date();
|
||||
const timestamp = `${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 scriptDir = getScriptDirPath();
|
||||
if (!scriptDir) {
|
||||
log.error("无法获取脚本目录");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建完整的目标目录路径和文件名
|
||||
const savePath = `${scriptDir}/${saveFile}/screenshot_${timestamp}.png`;
|
||||
const tempFilePath = `${scriptDir}/${saveFile}`;
|
||||
|
||||
// 检查临时文件是否存在,如果不存在则创建目录
|
||||
try {
|
||||
// 尝试读取临时文件
|
||||
file.readPathSync(tempFilePath);
|
||||
log.info("目录存在,继续执行保存图像操作");
|
||||
} catch (error) {
|
||||
log.error(`确保目录存在时出错: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存图像
|
||||
try {
|
||||
mat.saveImage(savePath);
|
||||
// log.info(`图像已成功保存到: ${savePath}`);
|
||||
} catch (error) {
|
||||
log.error(`保存图像失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取脚本目录
|
||||
function getScriptDirPath() {
|
||||
try {
|
||||
file.readTextSync(`temp-${Math.random()}.txt`);
|
||||
} catch (e) {
|
||||
const match = e.toString().match(/'([^']+)'/);
|
||||
return match ? match[1].replace(/\\[^\\]+$/, "") : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "背包统计采集系统",
|
||||
"version": "2.42",
|
||||
"name": "背包统计采集",
|
||||
"version": "2.5",
|
||||
"bgi_version": "0.44.8",
|
||||
"description": "模板匹配材料,OCR识别数量;\n支持背包材料的数量统计+路径CD管理,自动优选路径;\n具体支持看材料CD文件,可自行增减材料CD。",
|
||||
"saved_files": [
|
||||
"history_record/*.txt",
|
||||
"overwrite_record/*.txt",
|
||||
"pathing/",
|
||||
"history_record/",
|
||||
"overwrite_record/",
|
||||
"latest_record.txt",
|
||||
"pathing_record/*.txt",
|
||||
"pathing_record/noRecord/*.txt"
|
||||
"pathing_record/"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
|
||||
2
repo/js/背包材料统计/materialsCD/怪物.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
4点:丘丘人,丘丘萨满,丘丘人射手,丘丘暴徒,丘丘王,丘丘游侠,愚人众先遣队,萤术士,债务处理人,冬国仕女,愚人众风役人,愚人众特辖队,盗宝团,野伏众,海乱鬼,镀金旅团,黑蛇众,黯色空壳,部族龙形武士,遗迹机械,元能构装体,遗迹机兵,遗迹龙兽,发条机关,秘源机兵,巡陆艇,史莱姆,骗骗花,飘浮灵,蕈兽,浊水幻灵,原海异种,隙境原体,魔像禁卫,大灵显化身,熔岩游像,龙蜥,圣骸兽,玄文兽,纳塔龙众,炉壳山鼬,蕴光异兽,深渊法师,深渊使徒,兽境群狼,深邃拟覆叶,荒野狂猎,霜夜灵嗣,地脉花,
|
||||
3
repo/js/背包材料统计/materialsCD/狗粮.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
12小时:12h狗粮,
|
||||
|
||||
24小时:24h狗粮,
|
||||
@@ -1,20 +1,20 @@
|
||||
12小时:晶蝶,野猪,松鼠,狐狸,鼬,鸟,鱼,鸭子,路边闪光点,兽肉,禽肉,神秘的肉,鱼肉,鳗肉,螃蟹,青蛙,发光髓,蜥蜴尾巴,晶核,鳅鳅宝玉,吉光虫,燃素蜜虫,固晶甲虫,月萤虫,
|
||||
12小时:12h狗粮,晶蝶,野猪,松鼠,狐狸,鼬,鸟,鱼,鸭子,路边闪光点,兽肉,禽肉,神秘的肉,鱼肉,鳗肉,螃蟹,青蛙,发光髓,蜥蜴尾巴,晶核,鳅鳅宝玉,吉光虫,燃素蜜虫,固晶甲虫,月萤虫,
|
||||
|
||||
24小时:沉玉仙茗,狗粮,
|
||||
24小时:24h狗粮,沉玉仙茗,
|
||||
|
||||
46小时:小灯草,嘟嘟莲,落落莓,塞西莉亚花,慕风蘑菇,蒲公英籽,钩钩果,风车菊,霓裳花,清心,琉璃袋,琉璃百合,夜泊石,绝云椒椒,星螺,石珀,清水玉,海灵芝,鬼兜虫,绯樱绣球,鸣草,珊瑚真珠,晶化骨髓,血斛,天云草实,幽灯蕈,沙脂蛹,月莲,帕蒂沙兰,树王圣体菇,圣金虫,万相石,悼灵花,劫波莲,赤念果,苍晶螺,海露花,柔灯铃,子探测单元,湖光铃兰,幽光星星,虹彩蔷薇,初露之源,浪沫羽鳃,灼灼彩菊,肉龙掌,青蜜莓,枯叶紫英,微光角菌,云岩裂叶,琉鳞石,奇异的「牙齿」,冰雾花花朵,烈焰花花蕊,便携轴承,霜盏花,月落银,
|
||||
46小时:小灯草,嘟嘟莲,落落莓,塞西莉亚花,慕风蘑菇,蒲公英籽,钩钩果,风车菊,霓裳花,清心,琉璃袋,琉璃百合,夜泊石,绝云椒椒,星螺,石珀,清水玉,海灵芝,鬼兜虫,绯樱绣球,鸣草,珊瑚真珠,晶化骨髓,血斛,天云草实,幽灯蕈,沙脂蛹,月莲,帕蒂沙兰,树王圣体菇,圣金虫,万相石,悼灵花,劫波莲,赤念果,苍晶螺,海露花,柔灯铃,子探测单元,湖光铃兰,幽光星星,虹彩蔷薇,初露之源,浪沫羽鳃,灼灼彩菊,肉龙掌,青蜜莓,枯叶紫英,微光角菌,云岩裂叶,琉鳞石,奇异的「牙齿」,冰雾花花朵,烈焰花花蕊,便携轴承,霜盏花,月落银
|
||||
|
||||
72小时:
|
||||
|
||||
1次0点:铁块,甜甜花,胡萝卜,蘑菇,松茸,松果,金鱼草,莲蓬,薄荷,鸟蛋,马尾,树莓,白萝卜,苹果,日落果,竹笋,海草,堇瓜,星蕈,墩墩桃,须弥蔷薇,香辛果,枣椰,泡泡桔,汐藻,茉洁草,久雨莲,颗粒果,烛伞蘑菇,澄晶实,红果果菇,苦种,烬芯花,夏槲果,白灵果,寒涌石,宿影花,
|
||||
1次0点:铁块,甜甜花,胡萝卜,蘑菇,松茸,松果,金鱼草,莲蓬,薄荷,鸟蛋,树莓,白萝卜,苹果,马尾,日落果,竹笋,海草,堇瓜,星蕈,墩墩桃,须弥蔷薇,香辛果,枣椰,泡泡桔,汐藻,茉洁草,久雨莲,颗粒果,烛伞蘑菇,澄晶实,红果果菇,苦种,烬芯花,夏槲果,白灵果,寒涌石,宿影花,
|
||||
|
||||
2次0点:白铁块,星银矿石,
|
||||
2次0点:白铁块,星银矿石,电气水晶,
|
||||
|
||||
3次0点:水晶块,紫晶块,萃凝晶,魔晶块,钓鱼点,虹滴晶,
|
||||
|
||||
4点:盐,胡椒,洋葱,牛奶,番茄,卷心菜,土豆,小麦,稻米,虾仁,豆腐,杏仁,发酵果实汁,咖啡豆,秃秃豆,面粉,奶油,熏禽肉,黄油,火腿,糖,香辛料,蟹黄,果酱,奶酪,培根,香肠,「冷鲜肉」,黑麦,黑麦粉,酸奶油,
|
||||
4点:盐,胡椒,洋葱,牛奶,番茄,卷心菜,土豆,小麦,稻米,虾仁,豆腐,杏仁,发酵果实汁,咖啡豆,秃秃豆,面粉,奶油,熏禽肉,黄油,火腿,糖,香辛料,蟹黄,果酱,奶酪,培根,香肠,「冷鲜肉」,黑麦,黑麦粉,酸奶油,冷鲜肉,
|
||||
|
||||
6点:
|
||||
|
||||
即时刷新:蝴蝶,萤火虫,蝴蝶的翅膀,发光髓
|
||||
即时刷新:蝴蝶,萤火虫,蝴蝶的翅膀,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"name": "TargetCount",
|
||||
"type": "input-text",
|
||||
"label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 名刀镡/\n 📄 旅行者的名刀镡.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数"
|
||||
"label": "js目录下默认扫描的文件结构:\n./📁BetterGI/📁User/📁JsScript/\n📁背包材料统计/\n 📁pathing/\n 📁 薄荷/\n 📄 薄荷1.json\n 📁 薄荷效率/\n 📄 薄荷-吉吉喵.json\n 📁 苹果/\n 📄 旅行者的果园.json\n----------------------------------\n目标数量,默认5000\n给📁pathing下材料设定的目标数"
|
||||
},
|
||||
{
|
||||
"name": "TargetresourceName",
|
||||
@@ -22,12 +22,12 @@
|
||||
{
|
||||
"name": "noRecord",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n取消扫描数量。默认:否\n勾选将不进行单路径的扫描,但保留时间记录\n(推荐路径记录炼成后启用)"
|
||||
"label": "----------------------------------\n取消扫描数量。默认:否\n勾选将不进行单路径的扫描,但保留时间记录\n(推荐路径记录炼成后启用)"
|
||||
},
|
||||
{
|
||||
"name": "Pathing",
|
||||
"type": "select",
|
||||
"label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:兼并",
|
||||
"label": "====================\n扫描📁pathing下的\n或勾选【材料分类】的材料。默认:仅📁pathing材料",
|
||||
"options": [
|
||||
"1.兼并:📁pathing材料+【材料分类】",
|
||||
"2.仅📁pathing材料",
|
||||
@@ -37,7 +37,7 @@
|
||||
{
|
||||
"name": "Smithing",
|
||||
"type": "checkbox",
|
||||
"label": "刷怪、采集推荐:2.仅📁pathing材料\n----------------------------------\n【锻造素材】"
|
||||
"label": "\n----------------------------------\n【锻造素材】"
|
||||
},
|
||||
{
|
||||
"name": "Drops",
|
||||
@@ -72,7 +72,7 @@
|
||||
{
|
||||
"name": "CharAscension",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【角色突破素材】"
|
||||
"label": "----------------------------------\n\n【角色培养素材】世界BOSS树脂材料"
|
||||
},
|
||||
{
|
||||
"name": "Fishing",
|
||||
@@ -87,7 +87,7 @@
|
||||
{
|
||||
"name": "Talent",
|
||||
"type": "checkbox",
|
||||
"label": "----------------------------------\n\n【角色天赋素材】"
|
||||
"label": "----------------------------------\n\n【角色天赋素材】天赋书"
|
||||
},
|
||||
{
|
||||
"name": "WeaponAscension",
|
||||
@@ -95,9 +95,18 @@
|
||||
"label": "----------------------------------\n\n【武器突破素材】"
|
||||
},
|
||||
{
|
||||
"name": "ImageDelay",
|
||||
"name": "PopupNames",
|
||||
"type": "input-text",
|
||||
"label": "数字太小可能无法识别,用?代替\n====================\n识图延迟时间(默认:10 毫秒)"
|
||||
"label": "数字太小可能无法识别,用?代替\n====================\n弹窗名(默认:全部)"
|
||||
},
|
||||
{
|
||||
"name": "PopupClickDelay",
|
||||
"type": "input-text",
|
||||
"label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔(默认:5 秒)"
|
||||
},
|
||||
{
|
||||
"name": "CDCategories",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n\n采用的CD分类(默认:全部)"
|
||||
}
|
||||
]
|
||||
|
||||
]
|
||||
3
repo/js/背包材料统计/targetText/交互.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
狗粮:冒险家头带,冒险家金杯,冒险家怀表,冒险家尾羽,冒险家之花,幸运儿银冠,幸运儿之杯,幸运儿沙漏,幸运儿鹰羽,幸运儿绿花,游医的方巾,游医的药壶,游医的怀钟,游医的枭羽,游医的银莲,感别之冠,异国之盏,逐光之石,归乡之羽,故人之心,奇迹耳坠,奇迹之杯,奇迹之沙,奇迹之羽,奇迹之花,战狂的鬼面,战狂的骨杯,战狂的时计,战狂的翎羽,战狂的蔷薇,教官的帽子,教官的茶杯,教官的怀表,教官的羽饰,教官的胸花,流放者头冠,流放者之杯,流放者怀表,流放者之羽,流放者之花,守护束带,守护之皿,守护座钟,守护徽印,守护之花,勇士的勋章,勇士的期许,勇士的坚毅,勇士的壮行,勇士的冠冕,武人的红花,武人的羽饰,武人的水漏,武人的酒杯,武人的头巾,赌徒的胸花,赌徒的羽饰,赌徒的怀表,赌徒的骰蛊,赌徒的耳环,学士的书签,学士的羽笔,学士的时钟,学士的墨杯,学士的镜片,祭雷礼冠,祭火礼冠,祭水礼冠,祭冰礼冠,
|
||||
|
||||
交互:触摸,调查,拾取,
|
||||
4
repo/js/背包材料统计/targetText/宝箱.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
武器:无锋剑,银剑,旅行剑,吃虎鱼刀,训练大剑,佣兵重剑,飞天大御剑,白铁大剑,新手长枪,铁尖枪,钺矛,白缨枪,猎弓,历练的猎弓,信使,反曲弓,学徒笔记,口袋魔导书,甲级宝珏,异世界行记,
|
||||
|
||||
养成:精锻用,的教导,的经验
|
||||
|
||||
2
repo/js/背包材料统计/targetText/掉落.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
掉落:偏光棱镜,地脉的旧枝,大英雄的经验,特工祭刀,冒险家的经验,水晶棱镜,混沌炉心,猎兵祭刀,流浪者的经验,混沌回路,石化的骨片,黯淡棱镜,混沌装置,结实的骨片,隐兽鬼爪,黑晶号角,脆弱的骨片,隐兽利爪,雾虚灯芯,黑铜号角,沉重号角,混沌真眼,隐兽指爪,雾虚草囊,地脉的新芽,幽邃刻像,混沌枢纽,雾虚花粉,地脉的枯叶,夤夜刻像,混沌机关,督察长祭刀,初生的浊水幻灵,晦暗刻像,混浊棱晶,老旧的役人怀表,浊水的一掬,渊光鳍翅,破缺棱晶,茁壮菌核,休眠菌核,月色鳍翅,浊水的一滴,锲纹的横脊,失活菌核,密固的横脊,异界生命核,羽状鳍翅,外世突触,未熄的剑柄,残毁的横脊,混沌锚栓,混沌模块,漫游者的盛放之花,裂断的剑柄,隙间之核,何人所珍藏之花,役人的时时刻刻,残毁的剑柄,混沌容器,役人的制式怀表,意志巡游的符像,来自何处的待放之花,辉光棱晶,史莱姆凝液,意志明晰的寄偶,繁光躯外骸,迷光的蜷叶之心,不祥的面具,惑光的阔叶,意志破碎的残片,稀光遗骼,失光块骨,折光的胚芽,污秽的面具,聚燃的游像眼,幽雾兜盔,破损的面具,聚燃的命种,明燃的棱状壳,蕴热的背壳,冷裂壳块,幽雾片甲,禁咒绘卷,聚燃的石块,封魔绘卷,幽雾化形,秘源真芯,霜夜的煌荣,史莱姆原浆,导能绘卷,秘源机鞘,霜夜的柔辉,历战的箭簇,史莱姆清,秘源轴,霜夜的残照,原素花蜜,异海之块,浮游干核,锐利的箭簇,孢囊晶尘,异海凝珠,微光花蜜,牢固的箭簇,奇械机芯齿轮,尉官的徽记,荧光孢粉,骗骗花蜜,名刀镡,士官的徽记,机关正齿轮,蕈兽孢子,啮合齿轮,影打刀镡,新兵的徽记,织金红绸,攫金鸦印,横行霸者的利齿,破旧的刀镡,镶边红绸,浮游晶化核,老练的坚齿,藏银鸦印,褪色红绸,寻宝鸦印,异色结晶石,浮游幽核,稚嫩的尖齿,磨损的执凭,龙冠武士的金哨,战士的铁哨,卫从的木哨,精制机轴,加固机轴,毁损机轴,霜镌的执凭,精致的执凭
|
||||
|
||||
10
repo/js/背包材料统计/targetText/采集.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
小动物:蜜虫,甲虫,陆鳗,海鳗,沙鳗,青蛙,泥蛙,蓝蛙,树蛙,金虫,角蜥,髓蜥,尾蜥,蝴蝶,鳅鳅,晶蝶,金蟹,太阳蟹,海蓝蟹,将军蟹,薄红蟹,光虫,子探测,黑背鲈,蓝鳍鲈,黄金鲈,火虫,月萤虫,
|
||||
|
||||
素材:塞西莉亚花,霓裳花,夜泊石,落落莓,绝云椒椒,钩钩果,蒲公英籽,嘟嘟莲,小灯草,慕风蘑菇,风车菊,初露之源,树王圣体菇,绯樱绣球,万相石,幽灯蕈,鬼兜虫,天云草实,悼灵花,清水玉,湖光铃兰,幽光星星,沙脂蛹,珊瑚真珠,石珀,海灵芝,琉璃袋,虹彩蔷薇,赤念果,劫波莲,星螺,柔灯铃,鸣草,月莲,海露花,清心,血斛,帕蒂沙兰,晶化骨髓,琉璃百合,苍晶螺,琉鳞石,蜥蜴尾巴,云岩裂叶,枯叶紫英,肉龙掌,烈焰花,冰雾花,青蜜莓,灼灼彩菊,浪沫羽鳃,奇异的「牙齿」,便携轴承,霜盏花,月落银,芯花,微光角菌,苦种,久雨莲,毗波耶,马尾,寒涌石,宿影花,电气水晶
|
||||
|
||||
食材:苹果,日落果,星蕈,泡泡桔,烛伞蘑菇,胡萝卜,白萝卜,金鱼草,薄荷,小麦,卷心菜,松果,蘑菇,甜甜花,莲蓬,树莓,海草,稻米,兽肉,堇瓜,冷鲜肉,番茄,香辛果,鸟蛋,土豆,墩墩桃,松茸,禽肉,须弥蔷薇,鱼肉,枣椰,神秘的肉,茉洁草,汐藻,洋葱,竹笋,沉玉仙茗,颗粒果,澄晶实,红果,黑麦,黑麦粉,酸奶油,夏槲果,白灵果,
|
||||
|
||||
矿石:萃凝晶,紫晶块,星银矿石,魔晶块,水晶块,白铁块,铁块,虹滴晶,
|
||||
|
||||
狗粮:冒险家头带,冒险家金杯,冒险家怀表,冒险家尾羽,冒险家之花,幸运儿银冠,幸运儿之杯,幸运儿沙漏,幸运儿鹰羽,幸运儿绿花,游医的方巾,游医的药壶,游医的怀钟,游医的枭羽,游医的银莲,感别之冠,异国之盏,逐光之石,归乡之羽,故人之心,奇迹耳坠,奇迹之杯,奇迹之沙,奇迹之羽,奇迹之花,战狂的鬼面,战狂的骨杯,战狂的时计,战狂的翎羽,战狂的蔷薇,教官的帽子,教官的茶杯,教官的怀表,教官的羽饰,教官的胸花,流放者头冠,流放者之杯,流放者怀表,流放者之羽,流放者之花,守护束带,守护之皿,守护座钟,守护徽印,守护之花,勇士的勋章,勇士的期许,勇士的坚毅,勇士的壮行,勇士的冠冕,武人的红花,武人的羽饰,武人的水漏,武人的酒杯,武人的头巾,赌徒的胸花,赌徒的羽饰,赌徒的怀表,赌徒的骰蛊,赌徒的耳环,学士的书签,学士的羽笔,学士的时钟,学士的墨杯,学士的镜片,祭雷礼冠,祭火礼冠,祭水礼冠,祭冰礼冠,
|
||||
|
||||