mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-31 05:59:51 +08:00
新增定时终止,修复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:
@@ -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 @@ A:1. 打开 `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”)?
|
||||
A:1. 将不规范的文件夹/文件,放入**适配的材料文件夹**中即可(路径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:如何自定义识别名单?
|
||||
A:1. 打开 `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 |
|
||||
BIN
repo/js/背包材料统计/assets/Pic/Pic14.png
Normal file
BIN
repo/js/背包材料统计/assets/Pic/Pic14.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
repo/js/背包材料统计/assets/Pic/Pic15.png
Normal file
BIN
repo/js/背包材料统计/assets/Pic/Pic15.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
repo/js/背包材料统计/assets/Pic/Pic16.png
Normal file
BIN
repo/js/背包材料统计/assets/Pic/Pic16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
repo/js/背包材料统计/assets/Scroll.png
Normal file
BIN
repo/js/背包材料统计/assets/Scroll.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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}}
|
||||
@@ -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;
|
||||
|
||||
@@ -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]; // 映射顺序:[最高级, 中级, 最低级]
|
||||
// 新比例:最高级×3(1最高级=3中级),中级×1(本身),最低级×1/3(3最低级=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 // 传递终止时间
|
||||
);
|
||||
|
||||
// 汇总结果
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
|
||||
4点:丘丘人,丘丘萨满,丘丘人射手,丘丘暴徒,丘丘王,丘丘游侠,愚人众先遣队,萤术士,债务处理人,冬国仕女,愚人众风役人,愚人众特辖队,盗宝团,野伏众,海乱鬼,镀金旅团,黑蛇众,黯色空壳,部族龙形武士,遗迹机械,元能构装体,遗迹机兵,遗迹龙兽,发条机关,秘源机兵,巡陆艇,史莱姆,骗骗花,飘浮灵,蕈兽,浊水幻灵,原海异种,隙境原体,魔像禁卫,大灵显化身,熔岩游像,龙蜥,圣骸兽,玄文兽,纳塔龙众,炉壳山鼬,蕴光异兽,深渊法师,深渊使徒,兽境群狼,深邃拟覆叶,荒野狂猎,霜夜灵嗣,地脉花,
|
||||
4点:丘丘人,丘丘萨满,丘丘人射手,丘丘暴徒,丘丘王,丘丘游侠,愚人众先遣队,萤术士,债务处理人,冬国仕女,愚人众风役人,愚人众特辖队,盗宝团,野伏众,海乱鬼,镀金旅团,黑蛇众,黯色空壳,部族龙形武士,遗迹机械,元能构装体,遗迹机兵,遗迹龙兽,发条机关,秘源机兵,巡陆艇,史莱姆,骗骗花,飘浮灵,蕈兽,浊水幻灵,原海异种,隙境原体,魔像禁卫,大灵显化身,熔岩游像,龙蜥,圣骸兽,玄文兽,纳塔龙众,炉壳山鼬,蕴光异兽,深渊法师,深渊使徒,兽境群狼,深邃拟覆叶,荒野狂猎,霜夜灵嗣,地脉花,锄地
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user