Files
bettergi-scripts-list/repo/js/背包材料统计/main.js
JJMdzh 8adfeea5ce js,背包材料统计修复bug (#875)
* Add files via upload

* Delete repo/js/背包材料统计/pathing directory

* Add files via upload
2025-05-20 01:28:01 +08:00

1147 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.targetCount) || 5000))); // OCR基准时长
const OCRdelay = Math.min(50, Math.max(0, Math.floor(Number(settings.OcrDelay) || 10))); // OCR基准时长
// 定义映射表"unselected": "反选材料分类",
const material_mapping = {
"General": "一般素材",
"Drops": "怪物掉落素材",
"CookingIngs": "烹饪食材",
"ForagedFood": "采集食物",
"Weekly": "周本素材",
"Wood": "木材",
"CharAscension": "角色突破素材",
"Fishing": "鱼饵鱼类",
"Smithing": "锻造素材",
"Gems": "宝石",
"Talent": "角色天赋素材",
"WeaponAscension": "武器突破素材"
}
// 检查是否启用反选功能
const isUnselected = settings.unselected === true;
// 根据反选功能生成选中的材料分类数组
const selected_materials_array = Object.keys(settings)
.filter(key => key !== "unselected") // 排除 "unselected" 键
.filter(key => {
// 确保 settings[key] 是布尔值
if (typeof settings[key] !== 'boolean') {
console.warn(`非布尔值的键: ${key}, 值: ${settings[key]}`);
return false;
}
return isUnselected ? !settings[key] : settings[key];
})
.map(name => {
// 确保 material_mapping 中存在对应的键
if (!material_mapping[name]) {
console.warn(`material_mapping 中缺失的键: ${name}`);
return null;
}
return material_mapping[name];
})
.filter(name => name !== null); // 过滤掉 null 值
// 初始化游戏窗口大小和返回主界面
setGameMetrics(1920, 1080, 1);
// 配置参数
const pageScrollCount = 22; // 最多滑页次数
// 材料分类映射表
const materialTypeMap = {
"锻造素材": "5",
"怪物掉落素材": "3",
"一般素材": "5",
"周本素材": "3",
"烹饪食材": "5",
"角色突破素材": "3",
"木材": "5",
"宝石": "3",
"鱼饵鱼类": "5",
"角色天赋素材": "3",
"武器突破素材": "3",
"采集食物": "4",
"料理": "4",
};
// 材料前位定义
const materialPriority = {
"锻造素材": 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 = 10000, retryInterval = 20, maxAttempts = 10, maxFailures = 3) {
let startTime = Date.now();
let retryCount = 0;
let failureCount = 0; // 用于记录连续失败的次数
// const results = [];
const frequencyMap = {}; // 用于记录每个结果的出现次数
const replacementMap = {
"O": "0", "o": "0", "Q": "0", "": "0",
"I": "1", "l": "1", "i": "1", "": "1",
"Z": "2", "z": "2", "": "2",
"E": "3", "e": "3", "": "3",
"A": "4", "a": "4", "": "4",
"S": "5", "s": "5", "": "5",
"G": "6", "b": "6", "": "6",
"T": "7", "t": "7", "": "7",
"B": "8", "θ": "8", "": "8",
"g": "9", "q": "9", "": "9",
};
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
let captureRegion = captureGameRegion();
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.85; // 适当降低阈值以提高速度
let resList = captureRegion.findMulti(ocrObject);
if (resList.count === 0) {
failureCount++;
if (failureCount >= maxFailures) {
ocrRegion.x += 3; // 每次缩小6像素
ocrRegion.width -= 6; // 每次缩小6像素
retryInterval += 10;
if (ocrRegion.width <= 12) {
return { success: false };
}
}
retryCount++;
await sleep(retryInterval);
continue;
}
for (let res of resList) {
let text = res.text;
text = text.split('').map(char => replacementMap[char] || char).join('');
// results.push(text);
if (!frequencyMap[text]) {
frequencyMap[text] = 0;
}
frequencyMap[text]++;
if (frequencyMap[text] >= 2) {
return { success: true, text: text };
}
}
await sleep(retryInterval);
}
const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]);
return sortedResults.length > 0 ? { success: true, text: sortedResults[0] } : { success: false };
}
// 滚动页面
async function scrollPage(totalDistance, stepDistance = 10, delayMs = 5) {
moveMouseTo(999, 750);
await sleep(50);
leftButtonDown();
const steps = Math.ceil(totalDistance / stepDistance);
for (let j = 0; j < steps; j++) {
const remainingDistance = totalDistance - j * stepDistance;
const moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance;
moveMouseBy(0, -moveDistance);
await sleep(delayMs);
}
await sleep(700);
leftButtonUp();
await sleep(100);
}
// 扫描材料
async function scanMaterials(materialsCategory, materialCategoryMap) {
// 根据材料分类获取对应的材料图片文件夹路径
const materialIconDir = `assets/images/${materialsCategory}`;
// 使用 ReadPathSync 读取所有材料图片路径
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
// 创建材料种类集合
const materialCategories = [];
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
for (const filePath of materialIconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
materialCategories.push({ name: name, filePath: filePath });
allMaterials.add(name); // 将材料名称添加到集合中
}
// 已识别的材料集合,避免重复扫描
const recognizedMaterials = new Set();
// 扫描背包中的材料
const tolerance = 1; // 容错区间
const startX = 117;
const startY = 121;
const OffsetWidth = 147;
const columnWidth = 123;
const columnHeight = 750;
const maxColumns = 8;
// 用于存储图片名和材料数量的数组
const materialInfo = [];
const unmatchedMaterialNames = new Set();// 使用 Set 来存储未匹配的材料名称,确保不重复
// 是否已经开始计时
let hasFoundFirstMaterial = false;
// 记录上一次发现材料的时间
let lastFoundTime = null;
// 初始化标志变量,确保在整个扫描过程中保持状态
// let foundPriorityMaterial = false;
let shouldEndScan = false;
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
// log.info(`第 ${scroll+1} 页`);
// 随机选择一句俏皮话
const scanPhrases = [
"扫描中... 太好啦,有这么多素材!",
"扫描中... 不错的珍宝!",
"扫描中... 侦查骑士,发现目标!",
"扫描中... 嗯哼,意外之喜!",
"扫描中... 嗯?",
"扫描中... 很好,没有放过任何角落!",
"扫描中... 会有烟花材料嘛?",
"扫描中... 嗯,这是什么?",
"扫描中... 这些宝藏积灰了,先清洗一下",
"扫描中... 哇!都是好东西!",
"扫描中... 不虚此行!",
"扫描中... 瑰丽的珍宝,令人欣喜。",
"扫描中... 是对长高有帮助的东西吗?",
"扫描中... 嗯!品相卓越!",
"扫描中... 虽无法比拟黄金,但终有价值。",
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
"扫描中... 房租和伙食费,都有着落啦!",
"扫描中... 还不赖。",
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
"扫描中... 运气还不错。",
];
// 创建一个数组,用于存储未使用的俏皮话
let tempPhrases = [...scanPhrases];
// 打乱数组顺序,确保随机性
tempPhrases.sort(() => Math.random() - 0.5);
// 记录扫描开始时间
let phrasesStartTime = Date.now();
for (let column = 0; column < maxColumns; column++) {
const scanX = startX + column * OffsetWidth;
for (let i = 0; i < materialCategories.length; i++) {
const { name, filePath } = materialCategories[i];
if (recognizedMaterials.has(name)) {
materialCategories.splice(i, 1); // 从数组中移除已识别的材料
i--; // 调整索引
continue; // 如果已经识别过,跳过
}
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载材料图库失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.85; // 设置识别阈值为 0.85
const result = captureGameRegion().find(recognitionObject);
if (result.isExist()) {
recognizedMaterials.add(name); // 标记为已识别
await moveMouseTo(result.x, result.y); // 移动鼠标至图片
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, OCRdelay, 10, 3);
if (ocrResult.success) {
materialInfo.push({ name: name, count: ocrResult.text });
} else {
log.warn(`芝麻大的数看不清(>ε<)`);
materialInfo.push({ name: name, count: "?" });
}
// 如果是第一次发现材料,开始计时
if (!hasFoundFirstMaterial) {
hasFoundFirstMaterial = true;
lastFoundTime = Date.now();
} else {
// 更新上一次发现材料的时间
lastFoundTime = Date.now();
}
}
}
}
// 每2秒输出一句俏皮话
const phrasesTime = Date.now();
if (phrasesTime - phrasesStartTime >= 2000) {
// 输出当前数组的第一句俏皮话
const selectedPhrase = tempPhrases.shift();
log.info(selectedPhrase);
// 如果数组为空,重新加载并打乱所有俏皮话
if (tempPhrases.length === 0) {
tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5);
}
phrasesStartTime = phrasesTime;
}
// 检查材料识别情况
if (recognizedMaterials.size === allMaterials.size) {
log.info("所有材料均已识别!");
shouldEndScan = true;
break; // 立即退出当前循环
}
// 如果已经发现过材料检查是否超过3秒未发现新的材料
if (hasFoundFirstMaterial) {
const currentTime = Date.now();
if (currentTime - lastFoundTime > 5000) {
log.info("未发现新的材料,结束扫描");
shouldEndScan = true;
break; // 立即退出当前循环
}
// 如果未超过3秒继续扫描无需额外操作
}
// 检查是否已经滑到最后一页
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
sliderBottomRo.threshold = 0.8;
const sliderBottomResult = captureGameRegion().find(sliderBottomRo);
if (sliderBottomResult.isExist()) {
log.info("已到达最后一页!");
shouldEndScan = true;
break; // 如果识别到滑动条底部,终止滑动
}
// 如果还没有到达最后一页,继续滑页
if (scroll < pageScrollCount) {
await scrollPage(680, 10, 5);
await sleep(10); // 滑动后等待10毫秒
}
}
const lowCountMaterials = filterLowCountMaterials(materialInfo, materialCategoryMap);
// log.info(`低于目标的导入材料名 ${JSON.stringify(lowCountMaterials, null, 2)}`);
// 检查是否需要结束扫描
if (shouldEndScan) {
// 输出识别到的材料数量
log.info(`共识别到 ${recognizedMaterials.size} 种材料`);
const now = new Date(); // 获取当前时间
const formattedTime = now.toLocaleString(); // 使用本地时间格式化
const allMaterialsArray = Array.from(allMaterials);
// 过滤 allMaterials找出不在 recognizedMaterials 中的材料名称
for (const name of allMaterials) {
if (!recognizedMaterials.has(name)) {
unmatchedMaterialNames.add(name); // 使用 Set 的 add 方法添加名称
}
}
const unmatchedMaterialNamesArray = Array.from(unmatchedMaterialNames);
// 准备日志内容
const logContent = `
${formattedTime}
${materialsCategory} 种类: ${recognizedMaterials.size} 数量:
${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")}
未匹配的材料 种类: ${unmatchedMaterialNamesArray.length} 数量:
${unmatchedMaterialNamesArray.join(",")}
`;
// 按材料分类类别分文件记录
const categoryFilePath = `history_record/${materialsCategory}.txt`;
let categoryLogContent = `${logContent}\n\n`; // 添加换行分隔
try {
// 读取现有文件内容
const existingContent = file.readTextSync(categoryFilePath);
// 按记录分隔,假设每条记录之间用两个换行符分隔
const records = existingContent.split("\n\n");
// 截取最新的365个记录
const latestRecords = records.slice(-365).join("\n\n");
categoryLogContent = `${logContent}\n\n${latestRecords}`; // 将新内容拼接到最前面
} catch (error) {
// 如果文件不存在,直接使用新内容
log.warn(`文件 ${categoryFilePath} 不存在,将创建新文件`);
}
// 写回文件
const categoryResult = file.WriteTextSync(categoryFilePath, categoryLogContent, false); // 覆盖模式
if (categoryResult) {
log.info(`成功将 ${materialsCategory} 的材料写入历史文件`);
} else {
log.error(`写入 ${materialsCategory} 的本地文件失败`);
}
// 按材料分类类别分文件覆写记录
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`;
const overwriteLogContent = `${logContent}
图库的材料 种类: ${allMaterialsArray.length} 数量:
${allMaterialsArray.join(",")}\n\n`; // 添加换行分隔
const overwriteResult = file.WriteTextSync(overwriteFilePath, overwriteLogContent, false); // 覆盖模式
if (overwriteResult) {
log.info(`成功将 ${materialsCategory} 的记录写入覆写文件`);
} else {
log.error(`覆写 ${materialsCategory} 到本地文件失败`);
}
// 最新的历史覆写记录
const latestFilePath = "latest_record.txt";
const latestLogContent = `${logContent}
图库的材料 种类: ${allMaterialsArray.length} 数量:
${allMaterialsArray.join(",")}\n\n`; // 添加换行分隔
const latestResult = file.WriteTextSync(latestFilePath, latestLogContent, false); // 覆盖模式
if (latestResult) {
log.info("成功将最新的历史记录写入js根目录");
} else {
log.error("写入最新的历史记录失败");
}
}
return lowCountMaterials;
}
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
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);
// 定义一个函数用于识别图像
async function recognizeImage(recognitionObject, timeout = 5000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 尝试识别图像
let imageResult = captureGameRegion().find(recognitionObject);
if (imageResult) {
// log.info(`成功识别图像,坐标: x=${imageResult.x}, y=${imageResult.y}`);
// log.info(`图像尺寸: width=${imageResult.width}, height=${imageResult.height}`);
return { success: true, x: imageResult.x, y: imageResult.y };
}
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
}
await sleep(500); // 短暂延迟,避免过快循环
}
log.warn(`经过多次尝试,仍然无法识别图像`);
return { success: false };
}
const specialMaterials = [
"水晶块", "魔晶块", "星银矿石", "紫晶块", "萃凝晶", "铁块", "白铁块",
"精锻用魔矿", "精锻用良矿", "精锻用杂矿"
];
function filterLowCountMaterials(materialInfo, materialCategoryMap) {
// 将 materialCategoryMap 中的所有材料名提取出来
const allMaterials = Object.values(materialCategoryMap).flat();
// 筛选 materialInfo 中的材料,只保留 materialCategoryMap 中定义的材料,并且数量低于 targetCount 或 count 为 "?" 或 name 在 specialMaterials 中
return materialInfo
.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) {
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);
let backpackResult = await recognizeImage(BagpackRo, 2000);
if (backpackResult.success) {
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);
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;
}
let CategoryResult = await recognizeImage(CategoryObject, 2000);
if (CategoryResult.success && CategoryResult.x !== 0 && CategoryResult.y !== 0) {
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("扫描流程结束");
// 返回所有识别到的材料信息
return allLowCountMaterials;
}
// 自定义 basename 函数
function basename(filePath) {
const lastSlashIndex = filePath.lastIndexOf('\\'); // 或者使用 '/',取决于你的路径分隔符
return filePath.substring(lastSlashIndex + 1);
}
// 检查路径是否存在
function pathExists(path) {
try {
const entries = file.readPathSync(path);
return entries !== undefined && entries.length >= 0;
} catch (error) {
return false;
}
}
// 递归读取目录下的所有文件路径,并排除特定后缀的文件
function readAllFilePaths(dirPath, currentDepth = 0, maxDepth = 3, includeExtensions = ['.png', '.json', '.txt']) {
// log.info(`开始递归读取目录:${dirPath},当前深度:${currentDepth}`);
if (!pathExists(dirPath)) {
log.error(`目录 ${dirPath} 不存在`);
return [];
}
try {
const entries = file.readPathSync(dirPath); // 读取目录内容,返回的是完整路径
// log.info(`目录 ${dirPath} 下的条目:${JSON.stringify(entries)}`);
const filePaths = [];
for (const entry of entries) {
const isDirectory = pathExists(entry); // 如果路径存在且返回的是数组,则认为是目录
// log.info(`处理条目:${entry},是否为目录:${isDirectory}`);
if (isDirectory && currentDepth < maxDepth) {
// log.info(`递归读取子目录:${entry}`);
filePaths.push(...readAllFilePaths(entry, currentDepth + 1, maxDepth, includeExtensions)); // 递归读取子目录
} else if (!isDirectory) {
const fileExtension = entry.substring(entry.lastIndexOf('.'));
if (includeExtensions.includes(fileExtension.toLowerCase())) {
// log.info(`添加文件路径:${entry}`);
filePaths.push(entry); // 添加文件路径
} else {
// log.info(`跳过文件(不在包含的后缀中):${entry}`);
}
}
}
// log.info(`完成目录 ${dirPath} 的递归读取,共找到 ${filePaths.length} 个文件`);
return filePaths;
} catch (error) {
log.error(`读取目录 ${dirPath} 时发生错误: ${error}`);
return [];
}
}
// 解析文件内容,提取材料信息
function parseMaterialContent(content) {
// log.info(`开始解析文件内容:\n${content}`);
if (!content) {
log.warn(`文件内容为空`);
return {}; // 如果内容为空,直接返回空对象
}
const lines = content.split('\n').map(line => line.trim());
const materialCDInfo = {};
lines.forEach(line => {
// log.info(`处理行:${line}`);
if (!line.includes('')) {
// log.warn(`跳过无效行:${line}`);
return;
}
const [refreshCD, materials] = line.split('');
if (!refreshCD || !materials) {
// log.warn(`跳过无效行:${line}`);
return;
}
// 处理特殊规则如“N次0点”和“即时刷新”
let refreshCDInHours;
if (refreshCD.includes('次0点')) {
const times = parseInt(refreshCD.split('次')[0], 10);
if (isNaN(times)) {
log.error(`无效的刷新时间格式:${refreshCD}`);
return;
}
refreshCDInHours = { type: 'midnight', times: times };
} else if (refreshCD.includes('点')) {
const hours = parseFloat(refreshCD.replace('点', ''));
if (isNaN(hours)) {
log.error(`无效的刷新时间格式:${refreshCD}`);
return;
}
refreshCDInHours = { type: 'specific', hour: hours };
} else if (refreshCD.includes('小时')) {
const hours = parseFloat(refreshCD.replace('小时', ''));
if (isNaN(hours)) {
log.error(`无效的刷新时间格式:${refreshCD}`);
return;
}
refreshCDInHours = hours;
} else if (refreshCD === '即时刷新') {
refreshCDInHours = { type: 'instant' };
} else {
log.error(`未知的刷新时间格式:${refreshCD}`);
return;
}
materialCDInfo[JSON.stringify(refreshCDInHours)] = materials.split('').map(material => material.trim()).filter(material => material !== '');
/* // 改进日志记录,更清晰地显示对象内容
if (typeof refreshCDInHours === 'object') {
if (refreshCDInHours.type === 'midnight') {
log.info(`解析结果:刷新时间 ${refreshCDInHours.type} ${refreshCDInHours.times}次,材料 ${materialList}`);
} else if (refreshCDInHours.type === 'specific') {
log.info(`解析结果:刷新时间 ${refreshCDInHours.type} ${refreshCDInHours.hour}点,材料 ${materialList}`);
} else if (refreshCDInHours.type === 'instant') {
log.info(`解析结果:刷新时间 ${refreshCDInHours.type},材料 ${materialList}`);
}
} else {
log.info(`解析结果:刷新时间 ${refreshCDInHours}小时,材料 ${materialList}`);
}*/
});
// log.info(`完成文件内容解析,结果:${JSON.stringify(materialCDInfo, null, 2)}`);
return materialCDInfo;
}
// 从路径中提取材料名
function extractResourceNameFromPath(filePath) {
const pathParts = filePath.split('\\'); // 或者使用 '/',取决于你的路径分隔符
if (pathParts.length < 3) {
log.warn(`路径格式不正确,无法提取材料名:${filePath}`);
return null; // 返回 null 表示无法提取材料名
}
// 第一层文件夹名即为材料名
return pathParts[1];
}
// 从 materials 文件夹中读取分类信息
function readMaterialCategories(materialDir) {
// log.info(`开始读取材料分类信息:${materialDir}`);
const materialFilePaths = readAllFilePaths(materialDir, 0, 1);
const materialCategories = {};
for (const filePath of materialFilePaths) {
const content = file.readTextSync(filePath); // 同步读取文本文件内容
if (!content) {
log.error(`加载文件失败:${filePath}`);
continue; // 跳过当前文件
}
const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名
materialCategories[sourceCategory] = parseMaterialContent(content);
}
// log.info(`完成材料分类信息读取,分类信息:${JSON.stringify(materialCategories, null, 2)}`);
return materialCategories;
}
// 获取当前时间(以小时为单位)
function getCurrentTimeInHours() {
const now = new Date();
return now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600;
}
// 记录运行时间到材料对应的文件中
function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir) {
const recordPath = `${recordDir}/${resourceName}.txt`; // 记录文件路径,以材料名命名
const content = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}\n\n`;
try {
// 只有当运行时间大于或等于3秒时才记录运行时间
if (runTime >= 3) {
// 读取文件现有内容
let existingContent = '';
try {
existingContent = file.readTextSync(recordPath); // 读取文件内容
} catch (readError) {
// 如果文件不存在或读取失败existingContent 保持为空字符串
log.warn(`文件读取失败或文件不存在: ${readError}`);
}
// 将新的记录内容插入到最前面
const updatedContent = content + existingContent;
// 将更新后的内容写回文件
const result = file.writeTextSync(recordPath, updatedContent, false); // 覆盖写入
if (result) {
log.info(`记录运行时间成功: ${recordPath}`);
} else {
log.error(`记录运行时间失败: ${recordPath}`);
}
} else {
log.info(`运行时间小于3秒请检查路径要求: ${recordPath}`);
}
} catch (error) {
log.error(`记录运行时间失败: ${error}`);
}
}
// 读取材料对应的文件,获取上次运行的结束时间
function getLastRunEndTime(resourceName, pathName, recordDir) {
const recordPath = `${recordDir}/${resourceName}.txt`; // 记录文件路径,以材料名命名
try {
const content = file.readTextSync(recordPath); // 同步读取记录文件
const lines = content.split('\n');
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith('路径名: ')) {
const currentPathName = lines[i].split('路径名: ')[1];
if (currentPathName === pathName) {
const endTimeLine = lines[i + 2]; // 假设结束时间在路径名后的第三行
if (endTimeLine.startsWith('结束时间: ')) {
return endTimeLine.split('结束时间: ')[1]; // 返回结束时间
}
}
}
}
} catch (error) {
log.warn(`未找到记录文件或记录文件中无结束时间: ${recordPath}`);
}
return null; // 如果未找到记录文件或结束时间,返回 null
}
// 判断是否可以运行脚本
function canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) {
if (!lastEndTime) {
return true; // 如果没有上次运行记录,直接可以运行
}
const lastEndTimeDate = new Date(lastEndTime);
const currentDate = new Date();
if (typeof refreshCD === 'object') {
if (refreshCD.type === 'midnight') {
// 处理“N次0点”这样的特殊规则
const times = refreshCD.times;
// 计算从上次运行时间到当前时间的天数差
let daysPassed = Math.floor((currentDate - lastEndTimeDate) / (1000 * 60 * 60 * 24));
// 计算下一个刷新时间
const nextRunTime = new Date(lastEndTimeDate);
nextRunTime.setDate(lastEndTimeDate.getDate() + times); // 在上次运行时间的基础上加上N天
nextRunTime.setHours(0, 0, 0, 0); // 将时间设置为午夜0点
// 判断是否可以运行
const canRun = currentDate >= nextRunTime;
log.info(`路径文件${pathName}上次运行时间:${lastEndTimeDate.toLocaleString()},下次运行时间:${nextRunTime.toLocaleString()}`);
// log.info(`是否可以运行:${canRun}`);
return canRun;
} else if (refreshCD.type === 'specific') {
// 处理“具体时间点”这样的特殊规则
const specificHour = refreshCD.hour;
const currentHour = currentDate.getHours();
// const lastEndHour = lastEndTimeDate.getHours();
// 如果当前时间等于指定时间点,且日期已经改变
if (currentHour === specificHour && currentDate.getDate() !== lastEndTimeDate.getDate()) {
return true;
}
const nextRunTime = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate(), specificHour);
if (currentHour >= specificHour) {
nextRunTime.setDate(nextRunTime.getDate() + 1);
}
log.info(`路径文件${pathName}上次运行时间:${lastEndTimeDate.toLocaleString()},下次运行时间:${nextRunTime.toLocaleString()}`);
return false;
} else if (refreshCD.type === 'instant') {
// 处理“即时刷新”这样的特殊规则
return true;
}
} else {
// 处理普通刷新时间
const nextRefreshTime = new Date(lastEndTimeDate.getTime() + refreshCD * 3600 * 1000);
log.info(`路径文件${pathName}上次运行时间:${lastEndTimeDate.toLocaleString()},下次运行时间:${nextRefreshTime.toLocaleString()}`);
return currentDate >= nextRefreshTime;
}
return false;
}
const MATERIAL_ALIAS = {
'晶蝶': '晶核',
'白铁矿': '白铁块',
'铁矿': '铁块',
// 添加更多别名映射...
};
const imageMapCache = new Map();
const createImageCategoryMap = (imagesDir) => {
const map = {};
const imageFiles = readAllFilePaths(imagesDir, 0, 1);
for (const imagePath of imageFiles) {
const pathParts = imagePath.split(/[\\/]/);
if (pathParts.length < 3) continue;
// 统一小写存储(新增逻辑)
const imageName = pathParts.pop()
.replace(/\.png$/i, '')
.trim()
.toLowerCase(); // 新增
if (!(imageName in map)) {
map[imageName] = pathParts[2];
}
}
// log.info(JSON.stringify({ dir: imagesDir, entries: map }, null, 2));
return map;
};
// 模块级去重集合(新增)
const loggedResources = new Set();
function matchImageAndGetCategory(resourceName, imagesDir) {
const processedName = (MATERIAL_ALIAS[resourceName] || resourceName)
.toLowerCase();
if (!imageMapCache.has(imagesDir)) {
imageMapCache.set(imagesDir, createImageCategoryMap(imagesDir));
}
const result = imageMapCache.get(imagesDir)[processedName] ?? null;
// Set 去重逻辑
if (!loggedResources.has(processedName)) {
// log.info(JSON.stringify({ entries: { [processedName]: result } }, null, 2));
loggedResources.add(processedName);
}
return result;
}
(async function () {
// 定义文件夹路径
const materialDir = "materialsCD"; // 存储材料信息的文件夹
const pathingDir = "pathing"; // 存储路径信息的文件夹
const recordDir = "pathing_record"; // 存储运行记录的文件夹
const imagesDir = "assets\\images"; // 存储图片的文件夹
// 从设置中获取目标材料名称
const targetResourceNamesStr = settings.targetresourceName || "";
// 使用正则表达式分割字符串,支持多种分隔符(如逗号、分号、空格等)
const targetResourceNames = targetResourceNamesStr
.split(/[,,、 \s]+/) // 使用正则表达式分割字符串
.map(name => name.trim()) // 去除每个元素的多余空格
.filter(name => name !== ""); // 过滤掉空字符串
// 打印目标材料名称数组
log.info(`优先材料名称数组: ${JSON.stringify(targetResourceNames)}`);
try {
// 读取材料分类信息
const materialCategories = readMaterialCategories(materialDir);
// 递归读取路径信息文件夹
const pathingFilePaths = readAllFilePaths(pathingDir, 0, 1);
// 将路径和资源名绑定,避免重复提取
const pathEntries = pathingFilePaths.map(path => ({
path,
resourceName: extractResourceNameFromPath(path)
}));
// 从路径文件中提取材料名
const resourceNames = pathEntries
?.map(entry => entry.resourceName)
.filter(name => name) || []; // 确保 resourceNames 是一个数组
// 生成材料与分类的映射对象
const materialCategoryMap = resourceNames.reduce((acc, resourceName) => {
const category = matchImageAndGetCategory(resourceName, imagesDir); // 获取材料的分类
if (category) {
// 初始化分类键(如果不存在)
if (!acc[category]) acc[category] = [];
// 将材料名加入对应分类数组(避免重复)
if (!acc[category].includes(resourceName)) {
acc[category].push(resourceName);
}
}
return acc;
}, {});
// 确保 selected_materials_array 中的分类被初始化为空数组
selected_materials_array.forEach(selectedCategory => {
if (!materialCategoryMap[selectedCategory]) {
materialCategoryMap[selectedCategory] = [];
}
});
// log.info(JSON.stringify(materialCategoryMap, null, 2));
// 调用背包材料统计
const lowCountMaterialsFiltered = await MaterialPath(materialCategoryMap);
// 展平数组并按数量从小到大排序
const flattenedLowCountMaterials = lowCountMaterialsFiltered
.flat()
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10));
// log.info(`筛选后的低数量材料信息: ${JSON.stringify(flattenedLowCountMaterials, null, 2)}`);
// 提取低数量材料的名称
const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name);
// 将路径文件按是否为目标材料分类
const prioritizedPaths = [];
const normalPaths = [];
for (const { path, resourceName } of pathEntries) {
if (!resourceName) {
log.warn(`无法提取材料名:${path}`);
continue;
}
// 检查当前 resourceName 是否在 targetResourceNames 中
if (targetResourceNames.includes(resourceName)) {
prioritizedPaths.push({ path, resourceName });
} else if (lowCountMaterialNames.includes(resourceName)) {
// 只有当 resourceName 不在 targetResourceNames 中时,才将其加入到 normalPaths
normalPaths.push({ path, resourceName });
}
}
// 按照 flattenedLowCountMaterials 的顺序对 normalPaths 进行排序
normalPaths.sort((a, b) => {
const indexA = lowCountMaterialNames.indexOf(a.resourceName);
const indexB = lowCountMaterialNames.indexOf(b.resourceName);
return indexA - indexB;
});
// log.info(`优先路径数组 (prioritizedPaths): ${JSON.stringify(prioritizedPaths, null, 2)}`);
// log.info(`普通路径数组 (normalPaths): ${JSON.stringify(normalPaths, null, 2)}`);
// 合并优先路径和普通路径
const allPaths = prioritizedPaths.concat(normalPaths);
log.info(`最终路径数组 (allPaths): ${JSON.stringify(allPaths, null, 2)}`);
dispatcher.addTimer(new RealtimeTimer("AutoPick", { "forceInteraction": false }));
// 遍历所有路径文件
for (const { path: pathingFilePath, resourceName } of allPaths) {
const pathName = basename(pathingFilePath); // 假设路径文件名即为材料路径
// log.info(`处理路径文件:${pathingFilePath},材料名:${resourceName},材料路径:${pathName}`);
// 查找材料对应的分类
let categoryFound = false;
for (const [category, materials] of Object.entries(materialCategories)) {
for (const [refreshCDKey, materialList] of Object.entries(materials)) {
const refreshCD = JSON.parse(refreshCDKey);
if (materialList.includes(resourceName)) {
// 获取当前时间
const currentTime = getCurrentTimeInHours();
// 读取上次运行的结束时间
const lastEndTime = getLastRunEndTime(resourceName, pathName, recordDir);
// 判断是否可以运行脚本
if (canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName)) {
log.info(`可调用路径文件:${pathName}`);
// 记录开始时间
const startTime = new Date().toLocaleString();
// 调用路径文件
await pathingScript.runFile(pathingFilePath);
await sleep(1000);
// 记录结束时间
const endTime = new Date().toLocaleString();
// 计算运行时间
const runTime = (new Date(endTime) - new Date(startTime)) / 1000; // 秒
// 记录运行时间到材料对应的文件中
recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir);
categoryFound = true;
break;
} else {
log.info(`路径文件 ${pathName} 还未到刷新时间`);
}
}
}
if (categoryFound) break;
}
}
} catch (error) {
log.error(`操作失败: ${error}`);
}
})();