新增定时终止,修复bug,对 不规范命名的路径文件夹 适配 (#2188)

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update 怪物.txt
This commit is contained in:
吉吉喵
2025-10-20 13:05:42 +08:00
committed by GitHub
parent f86749088f
commit 3c3ab47233
11 changed files with 445 additions and 155 deletions

View File

@@ -1,4 +1,4 @@
# 背包材料统计 v2.52
# 背包材料统计 v2.54
作者:吉吉喵
<!-- 新增:全局图片样式,控制连续图片同行显示 -->
@@ -118,9 +118,10 @@
| 6. 仅 pathing 材料 | 仅扫描 `pathing` 文件夹内的材料,跳过其他分类,大幅缩短扫描时间 | 路径配置完成后开启,提升脚本运行效率 |
| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏) |
| 8. 采用的 CD 分类 | 不填则默认执行 `materialsCD` 文件夹内配置的CD分类填写则仅执行指定CD分类 | 新增材料时需在该文件夹同步配置CD规则操作见「四、问题解答Q2」 |
| 9. 采用的识别名单 | 不填则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时需符合配置规则操作见「四、问题解答Q4」 |
| 10. 超量阈值 | 首次扫描后超量的路径材料将从识别名单中剔除默认5000 | 不推荐9999怪物材料有几千就够了采用默认数值可自动避免爆背包 |
| 11. 拖动距离 | 解决非1080p分辨率下“划页过头”问题需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”大于1080p屏可适当减小数值 |
| 9. 终止时刻 | 不填则不执行定时终止路径无时间记录时会预判路径耗时5分钟且预留2分钟空闲 | 填写需要按24小时格式4:10 |
| 10. 采用的识别名单 | 不填则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时需符合配置规则操作见「四、问题解答Q4」 |
| 11. 超量阈值 | 首次扫描后超量的路径材料将从识别名单中剔除默认5000 | 不推荐9999怪物材料有几千就够了采用默认数值可自动避免爆背包 |
| 12. 拖动距离 | 解决非1080p分辨率下“划页过头”问题需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”大于1080p屏可适当减小数值 |
## 四、注意事项
@@ -129,7 +130,7 @@
3. **食物识别强制要求**:背包食物界面**第一行必须包含8种食物**(苹果、日落果、星蕈、活化的星蕈、枯焦的星蕈、泡泡桔、烛伞蘑菇、美味的宝石闪闪),缺少则这些食物无法识别;
4. **关键文件备份**:建议不定期备份 `pathing` 文件夹(路径文件)和 `pathing_record` 文件夹(路径运行记录),便于丢失后或记录被污染后恢复如初;
5. **OCR配置**默认最新调整识别名单时用的是V5Auto
6. **手动终止运行**如果要终止JS运行推荐在当前路径采集前或者采集完进入背包扫描时终止以保护当前记录如果是【取消扫描】模式不会储存当前记录的材料数目就随意。
6. **手动终止运行**如果要终止JS运行推荐在当前路径采集到当前材料前,或者采集完进入背包扫描时终止(会在扫描结束后终止),以保护当前记录;如果是【取消扫描】模式,不会储存当前记录的材料数目,就随意。
## 五、问题解答
@@ -149,9 +150,18 @@ A1. 打开 `materialsCD` 文件夹(脚本路径:`BetterGI\User\JsScript\
<img src="assets/Pic/Pic08.png" alt="添加新材料操作截图2" class="img-row-item">
</div>
### Q3如何识别不规范命名的路径文件夹如“纳塔食材一条龙”“果园.json”
A将不规范的文件夹/文件,放入**适配的材料文件夹**中即可路径CD由“所在材料文件夹”决定
例:“果园.json”放入“苹果”文件夹将按“苹果”的CD规则执行。
### Q3如何识别不规范命名的路径文件夹如“纳塔食材一条龙”“果园.json”
A1. 将不规范的文件夹/文件,放入**适配的材料文件夹**中即可路径CD由“所在材料文件夹”决定
2. 例看「四、问题解答Q2」① 把“纳塔食材一条龙”作为标准名选择一个CD② 在「JS 自定义设置」【优先级材料】里填入:纳塔食材一条龙,③将“纳塔食材一条龙”的文件夹放置到`pathing` 文件夹;锄地路径可放置到“锄地”文件夹里(没有就新建一个)**此方法无法使用 背包材料统计 的优选路径功能!**
3. 「JS 自定义设置」勾选【取消扫描】后,就可以运行了!**此项不勾将无CD记录**
4. 例:“果园.json”放入“苹果”文件夹将按“苹果”的CD规则执行。
操作参考截图:
<div class="img-row-container">
<img src="assets/Pic/Pic14.png" alt="添加新路径文件夹操作截图1" class="img-row-item">
<img src="assets/Pic/Pic15.png" alt="添加新路径文件夹操作截图2" class="img-row-item">
<img src="assets/Pic/Pic16.png" alt="添加新路径文件夹操作截图2" class="img-row-item">
</div>
### Q4如何自定义识别名单
A1. 打开 `targetText` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\targetText`
@@ -213,4 +223,5 @@ A记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
| v2.42 | 新增“无路径间扫描”“noRecord模式”适合成熟路径新增怪物材料CD文件 |
| v2.50 | 新增独立名单拾取、弹窗模块;支持怪物名识别 |
| v2.51 | 自定义设置新增“拖动距离/拖动点”新增月卡弹窗识别路径材料超量自动上黑名单修复怪物0收获记录 |
| v2.52 | 自定义设置新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 |
| v2.52 | 自定义设置新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 |
| v2.54 | 自定义设置新增“终止时刻”修复bug新增“添加不规范命名的路径文件夹”说明新增一个“锄地”的怪物路径CD |

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +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}}
{"macroEvents":[
{"type":6,"mouseX":0,"mouseY":-120,"time":25}
],"info":{"name":"","description":"","x":0,"y":0,"width":1920,"height":1080,"recordDpi":1}}

View File

@@ -158,29 +158,42 @@ async function findFIcon(recognitionObject, timeout = 10, ra = null) {
return { success: true, x: result.x, y: result.y, width: result.width, height: result.height };
}
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
log.error(`识别图异常: ${error.message}`);
if (state.cancelRequested) {
break; // 如果请求了取消,则退出循环
break;
}
return null;
}
await sleep(5); // 每次检测间隔 5 毫秒
}
if (state.cancelRequested) {
log.info("图识别任务已取消");
log.info("图识别任务已取消");
}
return null;
}
// 对齐并交互目标
async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame=null) {
// 定义Scroll.png识别对象按需求使用TemplateMatch包含指定范围
const ScrollRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync("assets/Scroll.png"),
1055, 521, 15, 35 // 识别范围x=1055, y=521, width=15, height=35
);
/**
* 对齐并交互目标直接用findFIcon识别Scroll.png
* @param {string[]} targetTexts - 待匹配的目标文本列表
* @param {Object} fDialogueRo - F图标的识别对象
* @param {Object} textxRange - 文本识别的X轴范围 { min: number, max: number }
* @param {number} texttolerance - 文本与F图标Y轴对齐的容差
* @param {Object} cachedFrame - 缓存的图像帧(可选)
*/
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秒记录一次日志
if (currentTime - lastLogTime >= 10000) {
log.info("检测中...");
lastLogTime = currentTime;
}
@@ -191,13 +204,15 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
// 尝试找到 F 图标
let fRes = await findFIcon(fDialogueRo, 10, cachedFrame);
if (!fRes) {
continue;
const scrollRes = await findFIcon(ScrollRo, 10, cachedFrame); // 复用findFIcon函数
if (scrollRes) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`); // 调用翻滚脚本
}
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);
// 检查所有目标文本是否在当前页面中
@@ -206,31 +221,25 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
let targetResult = ocrResults.find(res => res.text.includes(targetText));
if (targetResult) {
// 生成唯一标识并更新识别计数(文本+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"); // 执行交互操作
keyPress("F");
log.info(`交互或拾取: ${targetText}`);
// F键后清除计数确保单次交互
recognitionCount.delete(materialId);
}
foundTarget = true;
break; // 成功交互后退出当前循环,但继续检测
break;
}
}
}
// 如果在当前页面中没有找到任何目标文本,则滚动到下一页
if (!foundTarget) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
// verticalScroll(-20);
}
if (state.cancelRequested) {
break;

View File

@@ -51,6 +51,9 @@ var state = { completed: false, cancelRequested: false };
const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30)));
const notify = settings.notify || false;
const noRecord = settings.noRecord || false;
const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量
const exceedCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.ExceedCount) || 5000))); // 设定的超量目标数量
const endTimeStr = settings.CurrentTime ? settings.CurrentTime : null;
// 解析需要处理的CD分类
const allowedCDCategories = (settings.CDCategories || "")
@@ -326,6 +329,32 @@ function getCurrentTimeInHours() {
return now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600;
}
/**
* 计算当前时间到指定终止时间的剩余分钟数(处理跨天,单向倒计时)
* @param {string} endTimeStr - 指定终止时间(格式"HH:mm",如"4:00"
* @returns {number} 剩余分钟数(负数表示已过终止时间),无效格式返回-1
*/
function getRemainingMinutesToEndTime(endTimeStr) {
// 1. 解析终止时间
const [endHours, endMinutes] = endTimeStr.split(/[:]/).map(Number);
if (isNaN(endHours) || isNaN(endMinutes) || endHours < 0 || endHours >= 24 || endMinutes < 0 || endMinutes >= 60) {
log.error(`${CONSTANTS.LOG_MODULES.MAIN}无效终止时间格式:${endTimeStr},需为"HH:mm"(如"14:30"`);
return -1; // 无效格式视为“已过时间”
}
// 2. 转换为时间戳(当天终止时间 & 次日终止时间,处理跨天)
const now = new Date();
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), endHours, endMinutes);
const tomorrowEnd = new Date(todayEnd.getTime() + 24 * 60 * 60 * 1000); // 加1天
// 3. 确定有效终止时间(若当天已过,取次日)
const targetEndTime = now <= todayEnd ? todayEnd : tomorrowEnd;
// 4. 计算剩余分钟数(毫秒转分钟,保留整数)
const remainingMs = targetEndTime - now;
return Math.floor(remainingMs / (1000 * 60));
}
// ==============================================
// 记录管理
// ==============================================
@@ -403,9 +432,6 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco
try {
if (runTime >= 3) { // 运行时间≥3秒才处理记录
// ==============================================
// 新增怪物路径专用逻辑判断对应材料总数量是否为0
// ==============================================
const isMonsterPath = monsterToMaterials.hasOwnProperty(resourceName); // 是否为怪物路径
if (isMonsterPath) {
// 1. 获取当前怪物对应的所有目标材料(从已有映射中取)
@@ -424,9 +450,6 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco
}
}
// ==============================================
// 原有普通材料0记录逻辑完全保留不做修改
// ==============================================
for (const [material, count] of Object.entries(materialCountDifferences)) {
if (material === resourceName && count === 0) {
const zeroMaterialPath = `${recordDir}/${material}${CONSTANTS.ZERO_COUNT_SUFFIX}`;
@@ -436,9 +459,6 @@ function recordRunTime(resourceName, pathName, startTime, endTime, runTime, reco
}
}
// ==============================================
// 原有:正常记录生成逻辑(完全保留,不做修改)
// ==============================================
const hasZeroMaterial = Object.values(materialCountDifferences).includes(0);
const isFinalCumulativeDistanceZero = finalCumulativeDistance === 0;
@@ -504,102 +524,194 @@ function getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir) {
* @param {string} recordDir - 记录目录
* @returns {number|null} 时间成本(秒/中级单位null=无法计算
*/
function calculatePerTime(resourceName, pathName, recordDir) {
const recordPath = `${recordDir}/${resourceName}.txt`;
function getHistoricalPathRecords(resourceKey, pathName, recordDir, noRecordDir, isFood = false, cache = {}) {
// 1. 生成唯一缓存键(确保不同路径/不同文件的记录不混淆)
const isFoodSuffix = isFood ? CONSTANTS.FOOD_EXP_RECORD_SUFFIX : ".txt";
const recordFile = `${recordDir}/${resourceKey}${isFoodSuffix}`;
const cacheKey = `${recordFile}|${pathName}`; // 键格式:文件路径|路径名
// 2. 优先从缓存获取,命中则直接返回(不读文件)
if (cache[cacheKey]) {
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}从缓存复用记录:${cacheKey}`);
return cache[cacheKey];
}
// 3. 缓存未命中,才读取文件
const records = [];
let targetFile = recordFile;
let content = "";
// 读主目录→读备用目录
try {
const content = file.readTextSync(recordPath);
const lines = content.split('\n');
const completeRecords = [];
content = file.readTextSync(targetFile);
} catch (mainErr) {
targetFile = `${noRecordDir}/${resourceKey}${isFoodSuffix}`;
try {
content = file.readTextSync(targetFile);
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}从备用目录读取记录:${targetFile}`);
} catch (backupErr) {
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}${resourceKey}的历史记录:${targetFile}`);
// 空记录也写入缓存,避免下次重复尝试读文件
cache[cacheKey] = records;
return records;
}
}
// ==============================================
// 怪物路径改为以中级材料为基准最低级÷3
// ==============================================
if (monsterToMaterials.hasOwnProperty(resourceName)) {
const monsterMaterials = monsterToMaterials[resourceName]; // 映射顺序:[最高级, 中级, 最低级]
// 新比例最高级×31最高级=3中级中级×1本身最低级×1/33最低级=1中级 → 最低级÷3
const gradeRatios = [3, 1, 1/3];
// 解析记录按原有格式提取runTime和quantityChange
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) {
const runTimeLine = lines[i + 3];
const quantityChangeLine = lines[i + 4] || "";
let runTime = 0;
let quantityChange = {};
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) {
const runTimeLine = lines[i + 3];
const quantityChangeLine = lines[i + 4];
if (runTimeLine?.startsWith('运行时间: ') && quantityChangeLine?.startsWith('数量变化: ')) {
// 1. 提取运行时间
const runTime = parseInt(runTimeLine.split('运行时间: ')[1].split('秒')[0], 10);
if (isNaN(runTime) || runTime <= 0) continue;
// 2. 提取数量变化
const quantityChange = JSON.parse(quantityChangeLine.split('数量变化: ')[1]);
// 3. 按新比例计算“总中级单位数量”最低级÷3
let totalMiddleCount = 0; // 变量名改为中级单位
monsterMaterials.forEach((mat, index) => {
const count = quantityChange[mat] || 0;
const ratio = gradeRatios[index] || 1;
totalMiddleCount += count * ratio; // 最低级此处等价于 count ÷ 3
});
// 保留两位小数处理1/3导致的无限小数
totalMiddleCount = parseFloat(totalMiddleCount.toFixed(2));
// 4. 过滤无效数据
if (totalMiddleCount <= 0) continue;
// 5. 计算时间成本(秒/中级单位)
const perTime = parseFloat((runTime / totalMiddleCount).toFixed(2));
completeRecords.push(perTime);
// 日志更新为中级单位
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}怪物【${resourceName}】路径${pathName}${runTime}秒/${totalMiddleCount}中级单位 → ${perTime}秒/单位`);
}
// 提取运行时间(秒)
if (runTimeLine?.startsWith('运行时间: ')) {
runTime = parseInt(runTimeLine.split('运行时间: ')[1].split('秒')[0], 10) || 0;
}
// 提取数量变化JSON格式
if (quantityChangeLine.startsWith('数量变化: ')) {
try {
quantityChange = JSON.parse(quantityChangeLine.split('数量变化: ')[1]) || {};
} catch (e) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}解析数量变化失败:${quantityChangeLine}`);
}
}
}
// ==============================================
// 普通材料:完全保留原有逻辑
// ==============================================
else {
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('路径名: ') && lines[i].split('路径名: ')[1] === pathName) {
const runTimeLine = lines[i + 3];
const quantityChangeLine = lines[i + 4];
if (runTimeLine?.startsWith('运行时间: ') && quantityChangeLine?.startsWith('数量变化: ')) {
const runTime = parseInt(runTimeLine.split('运行时间: ')[1].split('秒')[0], 10);
const quantityChange = JSON.parse(quantityChangeLine.split('数量变化: ')[1]);
if (quantityChange[resourceName] !== undefined && quantityChange[resourceName] !== 0) {
completeRecords.push(parseFloat((runTime / quantityChange[resourceName]).toFixed(2)));
}
}
}
if (runTime > 0) {
records.push({ runTime, quantityChange });
}
}
}
// ==============================================
// 统一的异常值过滤和平均值计算(不变)
// ==============================================
if (completeRecords.length < 3) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}有效记录不足3条无法计算时间成本`);
return null;
}
// 4. 将读取到的记录写入缓存,供后续复用
cache[cacheKey] = records;
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}读取记录并缓存:${cacheKey}${records.length}条)`);
return records;
}
const recentRecords = completeRecords.slice(-5).filter(record => !isNaN(record) && record !== Infinity);
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}最近记录: ${JSON.stringify(recentRecords)}`);
/**
* 基于历史runTime预估路径总耗时默认5分钟
* @param {Object} entry - 路径条目
* @param {string} recordDir - 记录目录
* @param {string} noRecordDir - 备用目录
* @param {Object} cache - 缓存对象
* @returns {number} 预估耗时(秒)
*/
function estimatePathTotalTime(entry, recordDir, noRecordDir, cache = {}) {
const { resourceName, monsterName, path: pathingFilePath } = entry;
const pathName = basename(pathingFilePath);
const isFood = resourceName && isFoodResource(resourceName);
let resourceKey = isFood ? resourceName : (monsterName || resourceName);
const mean = recentRecords.reduce((acc, val) => acc + val, 0) / recentRecords.length;
const stdDev = Math.sqrt(recentRecords.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / recentRecords.length);
const filteredRecords = recentRecords.filter(record => Math.abs(record - mean) <= 1 * stdDev);
// 无资源关联时默认5分钟300秒
if (!resourceKey) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}无资源关联默认按300秒5分钟预估`);
return 300;
}
// 调用公共函数获取记录(复用缓存)
const historicalRecords = getHistoricalPathRecords(
resourceKey,
pathName,
recordDir,
noRecordDir,
isFood,
cache
);
// 无记录时默认5分钟300秒
if (historicalRecords.length === 0) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}无有效runTime记录默认按300秒5分钟预估`);
return 300;
}
// 取最近5条记录计算平均值
const recentRecords = [...historicalRecords].reverse().slice(0, 5);
const avgRunTime = Math.round(
recentRecords.reduce((sum, record) => sum + record.runTime, 0) / recentRecords.length
);
log.debug(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}历史runTime最近5条${recentRecords.map(r => r.runTime)}秒,预估耗时:${avgRunTime}`);
return avgRunTime;
}
/**
* 计算单次时间成本(秒/单位材料)(复用缓存)
* @param {string} resourceName - 资源名
* @param {string} pathName - 路径名
* @param {string} recordDir - 记录目录
* @param {string} noRecordDir - 备用目录
* @param {boolean} isFood - 是否为狗粮路径
* @param {Object} cache - 缓存对象
* @returns {number|null} 时间成本
*/
function calculatePerTime(resourceName, pathName, recordDir, noRecordDir, isFood = false, cache = {}) {
const isMonster = monsterToMaterials.hasOwnProperty(resourceName);
// 调用公共函数获取记录(复用缓存)
const historicalRecords = getHistoricalPathRecords(
resourceName,
pathName,
recordDir,
noRecordDir,
isFood,
cache
);
// 有效记录不足3条返回null
if (historicalRecords.length < 3) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}有效记录不足3条无法计算时间成本`);
return null;
}
const completeRecords = [];
if (isMonster) {
// 怪物路径:按中级单位计算
const monsterMaterials = monsterToMaterials[resourceName];
const gradeRatios = [3, 1, 1/3]; // 最高级×3中级×1最低级×1/3
historicalRecords.forEach(record => {
const { runTime, quantityChange } = record;
let totalMiddleCount = 0;
monsterMaterials.forEach((mat, index) => {
const count = quantityChange[mat] || 0;
totalMiddleCount += count * (gradeRatios[index] || 1);
});
totalMiddleCount = parseFloat(totalMiddleCount.toFixed(2));
if (totalMiddleCount > 0) {
completeRecords.push(parseFloat((runTime / totalMiddleCount).toFixed(2)));
}
});
} else {
// 普通材料路径:直接按材料数量计算
historicalRecords.forEach(record => {
const { runTime, quantityChange } = record;
if (quantityChange[resourceName] !== undefined && quantityChange[resourceName] !== 0) {
completeRecords.push(parseFloat((runTime / quantityChange[resourceName]).toFixed(2)));
}
});
}
// 异常值过滤与平均值计算(原有逻辑不变)
if (completeRecords.length < 3) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}有效效率记录不足3条无法计算时间成本`);
return null;
}
const recentRecords = completeRecords.slice(-5).filter(r => !isNaN(r) && r !== Infinity);
const mean = recentRecords.reduce((acc, val) => acc + val, 0) / recentRecords.length;
const stdDev = Math.sqrt(recentRecords.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / recentRecords.length);
const filteredRecords = recentRecords.filter(r => Math.abs(r - mean) <= 1 * stdDev);
if (filteredRecords.length === 0) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}记录数据差异过大,无法计算有效时间成本`);
return null;
}
return parseFloat((filteredRecords.reduce((acc, val) => acc + val, 0) / filteredRecords.length).toFixed(2));
} catch (error) {
log.warn(`${CONSTANTS.LOG_MODULES.RECORD}路径${pathName}无有效记录,无法计算时间成本`);
}
return null;
return parseFloat((filteredRecords.reduce((acc, val) => acc + val, 0) / filteredRecords.length).toFixed(2));
}
// ==============================================
@@ -708,7 +820,7 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
const result = imageMapCache.get(imagesDir)[processedName] ?? null;
if (result) {
log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}资源${resourceName}匹配分类:${result}`);
// log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}资源${resourceName}匹配分类:${result}`);
} else {
log.debug(`${CONSTANTS.LOG_MODULES.MATERIAL}资源${resourceName}未匹配到分类`);
}
@@ -720,6 +832,54 @@ function matchImageAndGetCategory(resourceName, imagesDir) {
return result;
}
// ==============================================
// 特殊材料与超量判断(核心新增逻辑)
// ==============================================
const specialMaterials = [
"水晶块", "魔晶块", "星银矿石", "紫晶块", "萃凝晶", "虹滴晶", "铁块", "白铁块",
"精锻用魔矿", "精锻用良矿", "精锻用杂矿"
];
let excessMaterialNames = []; // 超量材料名单
// 筛选低数量材料(保留原逻辑+修正超量判断)
function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) {
// 超量阈值普通材料9999矿石处理后也是9999
const EXCESS_THRESHOLD = exceedCount;
// 临时存储本次超量材料
const tempExcess = [];
// 提取所有需要扫描的材料(含怪物材料)
const allMaterials = Object.values(materialCategoryMap).flat();
const filteredMaterials = pathingMaterialCounts
.filter(item =>
allMaterials.includes(item.name) &&
(item.count < targetCount || item.count === "?")
)
.map(item => {
// 矿石数量÷10
let processedCount = item.count;
if (specialMaterials.includes(item.name) && item.count !== "?") {
processedCount = Math.floor(Number(item.count) / 10);
}
// 判断是否超量(用处理后数量对比阈值)
if (item.count !== "?" && processedCount >= EXCESS_THRESHOLD) {
tempExcess.push(item.name); // 记录超量材料名
}
return { ...item, count: processedCount };
});
tempExcess.push("OCR启动"); // 添加特殊标记用于终止OCR等待
// 更新全局超量名单(去重)
excessMaterialNames = [...new Set(tempExcess)];
log.info(`【超量材料更新】共${excessMaterialNames.length}种:${excessMaterialNames.join("、")}`);
return filteredMaterials;
}
// ==============================================
// 路径处理(拆分巨型函数)
// ==============================================
@@ -785,8 +945,16 @@ async function processMonsterPathEntry(entry, context) {
CDCategories, timeCost, recordDir, noRecordDir, imagesDir,
materialCategoryMap, flattenedLowCountMaterials,
currentMaterialName: prevMaterialName,
materialAccumulatedDifferences, globalAccumulatedDifferences
materialAccumulatedDifferences, globalAccumulatedDifferences,
pathRecordCache // 新增:从上下文取缓存
} = context;
const monsterMaterials = monsterToMaterials[monsterName] || [];
const allExcess = monsterMaterials.every(mat => excessMaterialNames.includes(mat));
if (allExcess) {
log.warn(`${CONSTANTS.LOG_MODULES.MONSTER}怪物【${monsterName}】所有材料已超量,跳过路径:${pathName}`);
await sleep(1);
return context;
}
// 用怪物名查CD
let refreshCD = null;
@@ -811,7 +979,14 @@ async function processMonsterPathEntry(entry, context) {
const currentTime = getCurrentTimeInHours();
const lastEndTime = getLastRunEndTime(monsterName, pathName, recordDir, noRecordDir);
const isPathValid = checkPathNameFrequency(monsterName, pathName, recordDir);
const perTime = noRecord ? null : calculatePerTime(monsterName, pathName, recordDir);
const perTime = noRecord ? null : calculatePerTime(
monsterName,
pathName,
recordDir,
noRecordDir,
false,
pathRecordCache // 新增:传递缓存
);
log.info(`${CONSTANTS.LOG_MODULES.PATH}怪物路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`);
@@ -930,7 +1105,8 @@ async function processNormalPathEntry(entry, context) {
CDCategories, timeCost, recordDir, noRecordDir,
materialCategoryMap, flattenedLowCountMaterials,
currentMaterialName: prevMaterialName,
materialAccumulatedDifferences, globalAccumulatedDifferences
materialAccumulatedDifferences, globalAccumulatedDifferences,
pathRecordCache // 新增:从上下文取缓存
} = context;
// 用材料名查CD
@@ -956,7 +1132,14 @@ async function processNormalPathEntry(entry, context) {
const currentTime = getCurrentTimeInHours();
const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir, noRecordDir);
const isPathValid = checkPathNameFrequency(resourceName, pathName, recordDir);
const perTime = noRecord ? null : calculatePerTime(resourceName, pathName, recordDir);
const perTime = noRecord ? null : calculatePerTime(
resourceName,
pathName,
recordDir,
noRecordDir,
false,
pathRecordCache // 新增:传递缓存
);
log.info(`${CONSTANTS.LOG_MODULES.PATH}材料路径${pathName} 单个材料耗时:${perTime ?? '忽略'}`);
@@ -1066,42 +1249,84 @@ async function processNormalPathEntry(entry, context) {
* @param {string} recordDir - 记录目录
* @param {string} noRecordDir - 无记录目录
* @param {string} imagesDir - 图像目录
* @param {string} endTimeStr - 指定终止时间
* @returns {Object} 处理结果
*/
async function processAllPaths(allPaths, CDCategories, materialCategoryMap, timeCost, flattenedLowCountMaterials, currentMaterialName, recordDir, noRecordDir, imagesDir) {
async function processAllPaths(allPaths, CDCategories, materialCategoryMap, timeCost, flattenedLowCountMaterials, currentMaterialName, recordDir, noRecordDir, imagesDir, endTimeStr) {
try {
// 初始化累加器
let foodExpAccumulator = {};
const globalAccumulatedDifferences = {};
const materialAccumulatedDifferences = {};
// 新增:单路径处理周期内的记录缓存(处理完所有路径后自动释放)
const pathRecordCache = {};
let context = {
CDCategories, timeCost, recordDir, noRecordDir, imagesDir,
materialCategoryMap, flattenedLowCountMaterials,
currentMaterialName, materialAccumulatedDifferences,
globalAccumulatedDifferences
globalAccumulatedDifferences,
pathRecordCache // 上下文加入缓存,供子函数使用
};
for (const entry of allPaths) {
if (state.cancelRequested) break;
// 优先响应手动终止指令(原有逻辑保留)
if (state.cancelRequested) {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到手动终止指令,停止路径处理`);
break;
}
// 核心修改仅当endTimeStr有值时才执行定时终止判断默认不执行
let skipPath = false;
if (endTimeStr) { // 只有用户显式配置了终止时间,才进入判断
const isValidEndTime = /^\d{1,2}[:]\d{1,2}$/.test(endTimeStr);
if (isValidEndTime) {
const remainingMinutes = getRemainingMinutesToEndTime(endTimeStr);
if (remainingMinutes <= 0) {
log.warn(`${CONSTANTS.LOG_MODULES.MAIN}已过指定终止时间(${endTimeStr}),停止路径处理`);
state.cancelRequested = true;
break;
}
const pathTotalTimeSec = estimatePathTotalTime(
entry,
recordDir,
noRecordDir,
pathRecordCache
);
const pathTotalTimeMin = pathTotalTimeSec / 60;
const requiredMin = pathTotalTimeMin + 2;
if (remainingMinutes <= requiredMin) {
log.warn(`${CONSTANTS.LOG_MODULES.MAIN}时间不足:剩余${remainingMinutes}分钟,需${requiredMin}分钟含2分钟空闲`);
state.cancelRequested = true;
skipPath = true;
break;
} else {
log.debug(`${CONSTANTS.LOG_MODULES.MAIN}时间充足:剩余${remainingMinutes}分钟,需${requiredMin}分钟`);
}
} else {
log.warn(`${CONSTANTS.LOG_MODULES.MAIN}终止时间格式无效(${endTimeStr}),跳过定时判断`);
}
} // 若endTimeStr为null默认则完全跳过定时终止逻辑
if (skipPath) break;
// 原有路径处理逻辑(仅新增缓存传递)
const { path: pathingFilePath, resourceName, monsterName } = entry;
log.info(`${CONSTANTS.LOG_MODULES.PATH}开始处理路径:${basename(pathingFilePath)}`);
try {
const { path: pathingFilePath, resourceName, monsterName } = entry;
log.info(`${CONSTANTS.LOG_MODULES.PATH}开始处理路径:${basename(pathingFilePath)}`);
// 区分路径类型并处理
if (resourceName && isFoodResource(resourceName)) {
// 狗粮路径
const result = await processFoodPathEntry(entry, {
foodExpAccumulator,
currentMaterialName: context.currentMaterialName
currentMaterialName: context.currentMaterialName,
pathRecordCache // 传递缓存
}, recordDir, noRecordDir);
foodExpAccumulator = result.foodExpAccumulator;
context.currentMaterialName = result.currentMaterialName;
} else if (monsterName) {
// 怪物路径
context = await processMonsterPathEntry(entry, context);
} else if (resourceName) {
// 普通材料路径
context = await processNormalPathEntry(entry, context);
} else {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}跳过无效路径条目:${JSON.stringify(entry)}`);
@@ -1109,6 +1334,10 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
} catch (singleError) {
log.error(`${CONSTANTS.LOG_MODULES.PATH}处理路径出错,已跳过:${singleError.message}`);
await sleep(1);
if (state.cancelRequested) {
log.warn(`${CONSTANTS.LOG_MODULES.PATH}检测到终止指令,停止处理`);
break;
}
}
}
@@ -1153,9 +1382,22 @@ async function processAllPaths(allPaths, CDCategories, materialCategoryMap, time
function classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMaterialNames, cdMaterialNames) {
const pathingFilePaths = readAllFilePaths(pathingDir, 0, 3, ['.json']);
const pathEntries = pathingFilePaths.map(path => {
const { materialName } = extractResourceNameFromPath(path, cdMaterialNames);
return { path, resourceName: materialName };
}).filter(entry => entry.resourceName);
const { materialName, monsterName } = extractResourceNameFromPath(path, cdMaterialNames);
return { path, resourceName: materialName, monsterName }; // 新增monsterName字段
}).filter(entry => {
// 新增:过滤超量材料对应的路径(包括怪物材料)
if (entry.monsterName) {
// 怪物路径:检查其所有材料是否都超量
const monsterMaterials = monsterToMaterials[entry.monsterName] || [];
const allExcess = monsterMaterials.every(mat => excessMaterialNames.includes(mat));
return !allExcess; // 若所有材料超量则过滤该路径
}
if (entry.resourceName) {
// 普通材料路径:检查自身是否超量
return !excessMaterialNames.includes(entry.resourceName);
}
return false;
});
if (pathEntries.length > 0) {
log.info(`${CONSTANTS.LOG_MODULES.PATH}\n===== 匹配到的材料路径列表 =====`);
@@ -1170,16 +1412,28 @@ function classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMateri
const prioritizedPaths = [];
const normalPaths = [];
for (const entry of pathEntries) {
if (targetResourceNames.includes(entry.resourceName)) {
prioritizedPaths.push(entry);
} else if (lowCountMaterialNames.includes(entry.resourceName)) {
normalPaths.push(entry);
if (entry.monsterName) {
// 怪物路径:检查是否包含有效目标材料
const monsterMaterials = monsterToMaterials[entry.monsterName] || [];
const hasValidTarget = monsterMaterials.some(mat => targetResourceNames.includes(mat) || lowCountMaterialNames.includes(mat));
if (hasValidTarget) {
prioritizedPaths.push(entry);
} else {
normalPaths.push(entry);
}
} else if (entry.resourceName) {
// 普通材料路径
if (targetResourceNames.includes(entry.resourceName)) {
prioritizedPaths.push(entry);
} else if (lowCountMaterialNames.includes(entry.resourceName)) {
normalPaths.push(entry);
}
}
}
// 按低数量排序
normalPaths.sort((a, b) => {
const indexA = lowCountMaterialNames.indexOf(a.resourceName);
const indexB = lowCountMaterialNames.indexOf(b.resourceName);
const indexA = lowCountMaterialNames.indexOf(a.resourceName) || lowCountMaterialNames.indexOf(a.monsterName ? monsterToMaterials[a.monsterName]?.[0] : '');
const indexB = lowCountMaterialNames.indexOf(b.resourceName) || lowCountMaterialNames.indexOf(b.monsterName ? monsterToMaterials[b.monsterName]?.[0] : '');
return indexA - indexB;
});
return prioritizedPaths.concat(normalPaths);
@@ -1237,14 +1491,25 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
let processedMonsterPaths = monsterPaths;
let pathingMaterialCounts = [];
if (normalPaths.length > 0) {
log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 执行背包扫描→低数量筛选`);
pathingMaterialCounts = await MaterialPath(materialCategoryMap);
if (normalPaths.length > 0 || monsterPaths.length > 0) { // 包含怪物路径时也需要扫描
// 优化:一次扫描获取全量材料数量,同时服务于怪物和普通材料
log.info(`${CONSTANTS.LOG_MODULES.PATH}[材料扫描] 执行一次全量背包扫描(服务于怪物+普通路径)`);
const allMaterialCounts = await MaterialPath(materialCategoryMap); // 仅一次扫描
pathingMaterialCounts = allMaterialCounts; // 普通材料直接复用扫描结果
// 1. 怪物材料筛选(复用全量扫描结果)
log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 基于全量扫描结果筛选有效材料`);
const filteredMonsterMaterials = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 复用结果
const validMonsterMaterialNames = filteredMonsterMaterials.map(m => m.name);
log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 筛选后有效材料:${validMonsterMaterialNames.join('、')}`);
// 2. 普通材料筛选(同样复用全量扫描结果,无需再次扫描)
if (pathingMode.onlyCategory) {
state.cancelRequested = true;
return { allPaths: [], pathingMaterialCounts };
}
const lowCountMaterialsFiltered = filterLowCountMaterials(pathingMaterialCounts.flat(), materialCategoryMap);
}
log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 基于全量扫描结果筛选低数量材料`);
const lowCountMaterialsFiltered = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 复用结果
const flattenedLowCountMaterials = lowCountMaterialsFiltered.flat().sort((a, b) => a.count - b.count);
const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name);
@@ -1283,7 +1548,8 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames
source: processedMonsterPaths,
filter: e => {
const materials = monsterToMaterials[e.monsterName] || [];
return !materials.some(mat => targetResourceNames.includes(mat));
return !materials.some(mat => targetResourceNames.includes(mat)) &&
materials.some(mat => !excessMaterialNames.includes(mat)); // 排除所有材料超量的怪物
}
},
// 6. 剩余普通材料
@@ -1358,9 +1624,6 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
=========================================\n\n`;
writeContentToFile(summaryPath, content);
log.info(`${CONSTANTS.LOG_MODULES.RECORD}最终汇总已记录至 ${summaryPath}`);
// ==============================================
// 新增:汇总后强制触发结束指令,确保程序终止
// ==============================================
state.completed = true; // 标记任务完全完成
state.cancelRequested = true; // 终止所有后台任务如图像点击、OCR
}
@@ -1504,7 +1767,8 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
currentMaterialName,
CONSTANTS.RECORD_DIR,
CONSTANTS.NO_RECORD_DIR,
CONSTANTS.IMAGES_DIR
CONSTANTS.IMAGES_DIR,
endTimeStr // 传递终止时间
);
// 汇总结果

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "背包统计采集系统",
"version": "2.53",
"version": "2.54",
"bgi_version": "0.44.8",
"description": "可统计背包养成道具、部分食物、素材的数量根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势\n+ 1. 自动判断材料CD不需要管材料CD有没有好\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别不会交互路边的npc或是神像可自定义识别名单具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料自动屏蔽更多详细内容查看readme.md",
"saved_files": [

View File

@@ -1,2 +1,3 @@
4点丘丘人丘丘萨满丘丘人射手丘丘暴徒丘丘王丘丘游侠愚人众先遣队萤术士债务处理人冬国仕女愚人众风役人愚人众特辖队盗宝团野伏众海乱鬼镀金旅团黑蛇众黯色空壳部族龙形武士遗迹机械元能构装体遗迹机兵遗迹龙兽发条机关秘源机兵巡陆艇史莱姆骗骗花飘浮灵蕈兽浊水幻灵原海异种隙境原体魔像禁卫大灵显化身熔岩游像龙蜥圣骸兽玄文兽纳塔龙众炉壳山鼬蕴光异兽深渊法师深渊使徒兽境群狼深邃拟覆叶荒野狂猎霜夜灵嗣地脉花
4点丘丘人丘丘萨满丘丘人射手丘丘暴徒丘丘王丘丘游侠愚人众先遣队萤术士债务处理人冬国仕女愚人众风役人愚人众特辖队盗宝团野伏众海乱鬼镀金旅团黑蛇众黯色空壳部族龙形武士遗迹机械元能构装体遗迹机兵遗迹龙兽发条机关秘源机兵巡陆艇史莱姆骗骗花飘浮灵蕈兽浊水幻灵原海异种隙境原体魔像禁卫大灵显化身熔岩游像龙蜥圣骸兽玄文兽纳塔龙众炉壳山鼬蕴光异兽深渊法师深渊使徒兽境群狼深邃拟覆叶荒野狂猎霜夜灵嗣地脉花锄地

View File

@@ -109,10 +109,15 @@
"type": "input-text",
"label": "====================\n\n采用的CD分类默认全部) 举例:采集,怪物,木材"
},
{
"name": "CurrentTime",
"type": "input-text",
"label": "根据CD分类来加载路径文件具体看materialsCD目录\n====================\n\n终止时刻默认不执行) 例4:00"
},
{
"name": "PickCategories",
"type": "input-text",
"label": "根据CD分类来加载路径文件具体看materialsCD目录\n====================\n\n采用的识别名单默认全部) 举例:交互,采集,宝箱"
"label": "====================\n\n采用的识别名单默认全部) 举例:交互,采集,宝箱"
},
{
"name": "ExceedCount",