Files
bettergi-scripts-list/repo/js/背包材料统计/main.js
JJMdzh 6c832ff0a6 js: 背包统计采集系统 (#1151)
+ v2.30 更改路径专注模式默认值,加log提示;去除注释掉的调试log;背包材料统计更改名为背包统计采集系统
2025-06-21 20:29:09 +08:00

1522 lines
64 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))); // 设定的目标数量
const OCRdelay = Math.min(50, Math.max(0, Math.floor(Number(settings.OcrDelay) || 10))); // OCR基准时长
const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长
const timeCost = Math.min(300, Math.max(0, Math.floor(Number(settings.TimeCost) || 30))); // 耗时和材料数量的比值,即一个材料多少秒
// 定义映射表"unselected": "反选材料分类",
const material_mapping = {
"General": "一般素材",
"Drops": "怪物掉落素材",
"CookingIngs": "烹饪食材",
"ForagedFood": "采集食物",
"Weekly": "周本素材",
"Wood": "木材",
"CharAscension": "角色突破素材",
"Fishing": "鱼饵鱼类",
"Smithing": "锻造素材",
"Gems": "宝石",
"Talent": "角色天赋素材",
"WeaponAscension": "武器突破素材"
}
const isOnlyPathing = settings.onlyPathing === "是" ? true : false;
if (isOnlyPathing) {
log.warn("已开启路径专注模式,将忽略勾选的分类");
}
// 初始化 settings将 material_mapping 中的所有键设置为 false
const initialSettings = Object.keys(material_mapping).reduce((acc, key) => {
acc[key] = false;
return acc;
}, {});
// 合并初始设置和实际的 settings实际的 settings 会覆盖初始设置
const finalSettings = { ...initialSettings, ...settings };
// 检查是否启用反选功能
const isUnselected = finalSettings.unselected === true;
// 根据反选功能生成选中的材料分类数组
const selected_materials_array = Object.keys(finalSettings)
.filter(key => key !== "unselected") // 排除 "unselected" 键
.filter(key => {
// 确保 finalSettings[key] 是布尔值
if (typeof finalSettings[key] !== 'boolean') {
console.warn(`非布尔值的键: ${key}, 值: ${finalSettings[key]}`);
return false;
}
return isUnselected ? !finalSettings[key] : finalSettings[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,
};
// 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 numberReplaceMap = {
"O": "0", "o": "0", "Q": "0", "": "0",
"I": "1", "l": "1", "i": "1", "": "1", "一": "1",
"Z": "2", "z": "2", "": "2", "二": "2",
"E": "3", "e": "3", "": "3", "三": "3",
"A": "4", "a": "4", "": "4",
"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 => numberReplaceMap[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);
}
function filterMaterialsByPriority(materialsCategory) {
// 获取当前材料分类的优先级
const currentPriority = materialPriority[materialsCategory];
if (currentPriority === undefined) {
throw new Error(`Invalid materialsCategory: ${materialsCategory}`);
}
// 获取当前材料分类的 materialTypeMap 对应值
const currentType = materialTypeMap[materialsCategory];
if (currentType === undefined) {
throw new Error(`Invalid materialTypeMap for: ${materialsCategory}`);
}
// 获取所有优先级更高的材料分类(前位材料)
const frontPriorityMaterials = Object.keys(materialPriority)
.filter(mat => materialPriority[mat] < currentPriority && materialTypeMap[mat] === currentType);
// 获取所有优先级更低的材料分类(后位材料)
const backPriorityMaterials = Object.keys(materialPriority)
.filter(mat => materialPriority[mat] > currentPriority && materialTypeMap[mat] === currentType);
// 合并当前和后位材料分类
const finalFilteredMaterials = [...backPriorityMaterials,materialsCategory ];// 当前材料
return finalFilteredMaterials
}
// 扫描材料
async function scanMaterials(materialsCategory, materialCategoryMap) {
// 获取当前+后位材料名单
const priorityMaterialNames = [];
const finalFilteredMaterials = await filterMaterialsByPriority(materialsCategory);
for (const category of finalFilteredMaterials) {
const materialIconDir = `assets/images/${category}`;
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
priorityMaterialNames.push({ category, name });
}
}
// 根据材料分类获取对应的材料图片文件夹路径
const materialIconDir = `assets/images/${materialsCategory}`;
// 使用 ReadPathSync 读取所有材料图片路径
const materialIconFilePaths = file.ReadPathSync(materialIconDir);
// 创建材料种类集合
const materialCategories = [];
const allMaterials = new Set(); // 用于记录所有需要扫描的材料名称
const materialImages = {}; // 用于缓存加载的图片
// 检查 materialCategoryMap 中当前分类的数组是否为空
const categoryMaterials = materialCategoryMap[materialsCategory] || [];
const shouldScanAllMaterials = categoryMaterials.length === 0; // 如果为空,则扫描所有材料
for (const filePath of materialIconFilePaths) {
const name = basename(filePath).replace(".png", ""); // 去掉文件扩展名
// 如果 materialCategoryMap 中当前分类的数组不为空
// 且当前材料名称不在指定的材料列表中,则跳过加载
if (isOnlyPathing && !shouldScanAllMaterials && !categoryMaterials.includes(name)) {
continue;
}
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
materialCategories.push({ name, filePath });
allMaterials.add(name); // 将材料名称添加到集合中
materialImages[name] = mat; // 缓存图片
}
// 已识别的材料集合,避免重复扫描
const recognizedMaterials = new Set();
const unmatchedMaterialNames = new Set(); // 未匹配的材料名称
const materialInfo = []; // 存储材料名称和数量
// 扫描参数
const tolerance = 1;
const startX = 117;
const startY = 121;
const OffsetWidth = 147;
const columnWidth = 123;
const columnHeight = 750;
const maxColumns = 8;
// 扫描状态
let hasFoundFirstMaterial = false;
let lastFoundTime = null;
let shouldEndScan = false;
let foundPriorityMaterial = false;
// 俏皮话逻辑
const scanPhrases = [
"扫描中... 太好啦,有这么多素材!",
"扫描中... 不错的珍宝!",
"扫描中... 侦查骑士,发现目标!",
"扫描中... 嗯哼,意外之喜!",
"扫描中... 嗯?",
"扫描中... 很好,没有放过任何角落!",
"扫描中... 会有烟花材料嘛?",
"扫描中... 嗯,这是什么?",
"扫描中... 这些宝藏积灰了,先清洗一下",
"扫描中... 哇!都是好东西!",
"扫描中... 不虚此行!",
"扫描中... 瑰丽的珍宝,令人欣喜。",
"扫描中... 是对长高有帮助的东西吗?",
"扫描中... 嗯!品相卓越!",
"扫描中... 虽无法比拟黄金,但终有价值。",
"扫描中... 收获不少,可以拿去换几瓶好酒啦。",
"扫描中... 房租和伙食费,都有着落啦!",
"扫描中... 还不赖。",
"扫描中... 荒芜的世界,竟藏有这等瑰宝。",
"扫描中... 运气还不错。",
];
let tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5); // 打乱数组顺序,确保随机性
let phrasesStartTime = Date.now();
// 扫描背包中的材料
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
if (!foundPriorityMaterial) {
for (const { category, name } of priorityMaterialNames) {
if (recognizedMaterials.has(name)) {
continue; // 如果已经识别过,跳过
}
const filePath = `assets/images/${category}/${name}.png`;
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载材料图库失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, 1146, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.8; // 设置识别阈值
const result = captureGameRegion().find(recognitionObject);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
foundPriorityMaterial = true; // 标记找到前位材料
log.info(`发现当前或后位材料: ${name},开始全列扫描`);
break; // 发现前位材料后,退出当前循环
}
}
}
if (foundPriorityMaterial) {
for (let column = 0; column < maxColumns; column++) {
const scanX = startX + column * OffsetWidth;
for (let i = 0; i < materialCategories.length; i++) {
const { name } = materialCategories[i];
if (recognizedMaterials.has(name)) {
continue; // 如果已经识别过,跳过
}
const mat = materialImages[name];
const recognitionObject = RecognitionObject.TemplateMatch(mat, scanX, startY, columnWidth, columnHeight);
recognitionObject.threshold = 0.85;
const result = captureGameRegion().find(recognitionObject);
await sleep(imageDelay);
if (result.isExist() && result.x !== 0 && result.y !== 0) {
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);
materialInfo.push({ name, count: ocrResult.success ? ocrResult.text : "?" });
if (!hasFoundFirstMaterial) {
hasFoundFirstMaterial = true;
lastFoundTime = Date.now();
} else {
lastFoundTime = Date.now();
}
}
}
}
}
// 每2秒输出一句俏皮话
const phrasesTime = Date.now();
if (phrasesTime - phrasesStartTime >= 5000) {
const selectedPhrase = tempPhrases.shift();
log.info(selectedPhrase);
if (tempPhrases.length === 0) {
tempPhrases = [...scanPhrases];
tempPhrases.sort(() => Math.random() - 0.5);
}
phrasesStartTime = phrasesTime;
}
// 检查是否结束扫描
if (recognizedMaterials.size === allMaterials.size) {
log.info("所有材料均已识别!");
shouldEndScan = true;
break;
}
if (hasFoundFirstMaterial && Date.now() - lastFoundTime > 5000) {
log.info("未发现新的材料,结束扫描");
shouldEndScan = true;
break;
}
// 检查是否到达最后一页
const sliderBottomRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/SliderBottom.png"), 1284, 916, 9, 26);
sliderBottomRo.threshold = 0.8;
const sliderBottomResult = captureGameRegion().find(sliderBottomRo);
if (sliderBottomResult.isExist()) {
log.info("已到达最后一页!");
shouldEndScan = true;
break;
}
// 滑动到下一页
if (scroll < pageScrollCount) {
await scrollPage(680, 10, 5);
await sleep(10);
}
}
// 处理未匹配的材料
for (const name of allMaterials) {
if (!recognizedMaterials.has(name)) {
unmatchedMaterialNames.add(name);
}
}
// 日志记录
const now = new Date();
const formattedTime = now.toLocaleString();
const scanMode = shouldScanAllMaterials ? "全材料扫描" : "指定材料扫描";
const logContent = `
${formattedTime}
${scanMode} - ${materialsCategory} 种类: ${recognizedMaterials.size} 数量:
${materialInfo.map(item => `${item.name}: ${item.count}`).join(",")}
未匹配的材料 种类: ${unmatchedMaterialNames.size} 数量:
${Array.from(unmatchedMaterialNames).join(",")}
`;
// 写入历史记录文件
const categoryFilePath = `history_record/${materialsCategory}.txt`;
const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`;
const latestFilePath = "latest_record.txt";
await writeLog(categoryFilePath, logContent);
await writeLog(overwriteFilePath, logContent);
await writeLog(latestFilePath, logContent);
// 返回结果
return materialInfo;
}
async function writeLog(filePath, logContent) {
try {
const existingContent = file.readTextSync(filePath);
const records = existingContent.split("\n\n");
const latestRecords = records.slice(-365).join("\n\n");
const finalContent = `${logContent}\n\n${latestRecords}`;
const result = file.WriteTextSync(filePath, finalContent, false);
if (result) {
log.info(`成功将日志写入文件 ${filePath}`);
} else {
log.error(`写入文件 ${filePath} 失败`);
}
} catch (error) {
log.warn(`文件 ${filePath} 不存在,将创建新文件`);
const result = file.WriteTextSync(filePath, logContent, false);
if (result) {
log.info(`成功创建并写入文件 ${filePath}`);
} else {
log.error(`创建文件 ${filePath} 失败`);
}
}
}
// 定义所有图标的图像识别对象,每个图片都有自己的识别区域
const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38);
const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38);
const CultivationItemsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/CultivationItems.png"), 749, 30, 38, 38);
const FoodRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Food.png"), 845, 31, 38, 38);
// 定义一个函数用于识别图像
async function recognizeImage(recognitionObject, timeout = 5000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 尝试识别图像
const imageResult = captureGameRegion().find(recognitionObject);
if (imageResult.isExist() && imageResult.x !== 0 && imageResult.y !== 0) {
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(pathingMaterialCounts, materialCategoryMap) {
// 将 materialCategoryMap 中的所有材料名提取出来
const allMaterials = Object.values(materialCategoryMap).flat();
// 筛选 pathingMaterialCounts 中的材料,只保留 materialCategoryMap 中定义的材料,并且数量低于 targetCount 或 count 为 "?" 或 name 在 specialMaterials 中
return pathingMaterialCounts
.filter(item =>
allMaterials.includes(item.name) &&
(item.count < targetCount || item.count === "?")
)
.map(item => {
// 如果 name 在 specialMaterials 数组中
if (specialMaterials.includes(item.name)) {
// 如果 count 是 "?",直接保留
if (item.count === "?") {
return item;
}
// 否则,将 count 除以 10 并向下取整
item.count = Math.floor(item.count / 10);
}
return item;
});
}
function dynamicMaterialGrouping(materialCategoryMap) {
// 初始化动态分组对象
const dynamicMaterialGroups = {};
// 遍历 materialCategoryMap 的 entries
for (const category in materialCategoryMap) {
const type = materialTypeMap[category]; // 获取材料分类对应的组编号3、4、5
if (!dynamicMaterialGroups[type]) {
dynamicMaterialGroups[type] = []; // 初始化组
}
dynamicMaterialGroups[type].push(category); // 将分类加入对应组
}
// 对每组内的材料分类按照 materialPriority 排序
for (const type in dynamicMaterialGroups) {
dynamicMaterialGroups[type].sort((a, b) => materialPriority[a] - materialPriority[b]);
}
// 将分组结果转换为数组并按类型排序3, 4, 5
const sortedGroups = Object.entries(dynamicMaterialGroups)
.map(([type, categories]) => ({ type: parseInt(type), categories }))
.sort((a, b) => a.type - b.type);
// 返回分组结果
return sortedGroups;
}
// 主逻辑函数
async function MaterialPath(materialCategoryMap) {
const maxStage = 4; // 最大阶段数
let stage = 0; // 当前阶段
let currentGroupIndex = 0; // 当前处理的分组索引
let currentCategoryIndex = 0; // 当前处理的分类索引
let materialsCategory = ""; // 当前处理的材料分类名称
const allLowCountMaterials = []; // 用于存储所有识别到的低数量材料信息
const sortedGroups = dynamicMaterialGrouping(materialCategoryMap);
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: // 打开背包界面
keyPress("B"); // 打开背包界面
await sleep(1000);
await imageClick()
let backpackResult = await recognizeImage(BagpackRo, 2000);
if (backpackResult.success) {
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);
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) {
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'], includeDirs = false) {
if (!pathExists(dirPath)) {
log.error(`目录 ${dirPath} 不存在`);
return [];
}
try {
const entries = file.readPathSync(dirPath); // 读取目录内容,返回的是完整路径
const filePaths = [];
for (const entry of entries) {
const isDirectory = pathExists(entry); // 如果路径存在且返回的是数组,则认为是目录
if (isDirectory) {
if (includeDirs) {
filePaths.push(entry); // 添加目录路径
}
if (currentDepth < maxDepth) {
filePaths.push(...readAllFilePaths(entry, currentDepth + 1, maxDepth, includeExtensions, includeDirs)); // 递归读取子目录
}
} else {
const fileExtension = entry.substring(entry.lastIndexOf('.'));
if (includeExtensions.includes(fileExtension.toLowerCase())) {
filePaths.push(entry); // 添加文件路径
} else {
}
}
}
return filePaths;
} catch (error) {
log.error(`读取目录 ${dirPath} 时发生错误: ${error}`);
return [];
}
}
// 解析文件内容,提取材料信息
function parseMaterialContent(content) {
if (!content) {
log.warn(`文件内容为空`);
return {}; // 如果内容为空,直接返回空对象
}
const lines = content.split('\n').map(line => line.trim());
const materialCDInfo = {};
lines.forEach(line => {
if (!line.includes('')) {
return;
}
const [refreshCD, materials] = line.split('');
if (!refreshCD || !materials) {
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 !== '');
});
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) {
const materialFilePaths = readAllFilePaths(materialDir, 0, 1, ['.txt']);
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);
}
return materialCategories;
}
// 获取当前时间(以小时为单位)
function getCurrentTimeInHours() {
const now = new Date();
return now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600;
}
// 辅助函数:写入内容到文件
function writeContentToFile(filePath, content) {
try {
// 读取文件现有内容
let existingContent = '';
try {
existingContent = file.readTextSync(filePath); // 读取文件内容
} catch (readError) {
// 如果文件不存在或读取失败existingContent 保持为空字符串
log.warn(`文件读取失败或文件不存在: ${filePath}`);
}
// 将新的记录内容插入到最前面
const updatedContent = content + existingContent;
// 将更新后的内容写回文件
const result = file.writeTextSync(filePath, updatedContent, false); // 覆盖写入
if (result) {
log.info(`记录成功: ${filePath}`);
} else {
log.error(`记录失败: ${filePath}`);
}
} catch (error) {
log.error(`记录失败: ${error}`);
}
}
function checkPathNameFrequency(recordDir, resourceName, pathName) {
const recordPath = `${recordDir}/${resourceName}-0.txt`; // 记录文件路径,以 resourceName-0.txt 命名
try {
const content = file.readTextSync(recordPath); // 同步读取记录文件
const lines = content.split('\n');
let totalCount = 0; // 用于记录路径名出现的总次数
// 从文件内容的开头开始查找
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('路径名: ')) {
const currentPathName = lines[i].split('路径名: ')[1];
if (currentPathName === pathName) {
totalCount++; // 如果当前路径名匹配计数加1
}
}
}
// 如果路径名出现次数超过3次返回 false
if (totalCount >= 3) {
log.info(`路径文件: ${pathName}, 多次0采集请检查后删除记录再执行`);
return false;
}
// 如果路径名出现次数不超过3次返回 true
return true;
} catch (error) {
log.warn(`读取文件时发生错误: ${recordPath}`, error);
return true; // 如果文件不存在或读取失败认为路径名出现次数不超过3次
}
}
function recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences = {}, finalCumulativeDistance) {
const recordPath = `${recordDir}/${resourceName}.txt`; // 正常记录文件路径
const normalContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
try {
// 只有当运行时间大于或等于3秒时才记录运行时间
if (runTime >= 3) {
// 检查 materialCountDifferences 中是否存在材料数目为 0 的情况
for (const [material, count] of Object.entries(materialCountDifferences)) {
if (count === 0) {
// 如果材料数目为 0记录到单独的文件
const zeroMaterialPath = `${recordDir}/${material}-0.txt`; // 材料数目为0的记录文件路径
const zeroMaterialContent = `路径名: ${pathName}\n开始时间: ${startTime}\n结束时间: ${endTime}\n运行时间: ${runTime}\n数量变化: ${JSON.stringify(materialCountDifferences)}\n\n`;
writeContentToFile(zeroMaterialPath, zeroMaterialContent); // 写入材料数目为0的记录
log.warn(`材料数目为0已写入单独文件: ${zeroMaterialPath}`);
}
}
// 检查是否需要记录正常内容
const hasZeroMaterial = Object.values(materialCountDifferences).includes(0);
const isFinalCumulativeDistanceZero = finalCumulativeDistance === 0;
if (!(hasZeroMaterial && isFinalCumulativeDistanceZero)) {
// 写入正常记录的内容
writeContentToFile(recordPath, normalContent);
log.info(`正常记录已写入: ${recordPath}`);
} else {
if (hasZeroMaterial) {
log.warn(`存在材料数目为0的情况: ${JSON.stringify(materialCountDifferences)}`);
}
if (isFinalCumulativeDistanceZero) {
log.warn(`累计距离为0: finalCumulativeDistance=${finalCumulativeDistance}`);
}
log.warn(`未写入正常记录: ${recordPath}`);
}
} else {
log.warn(`运行时间小于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 = 0; i < lines.length; 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 calculatePerTime(resourceName, pathName, recordDir) {
const recordPath = `${recordDir}/${resourceName}.txt`; // 记录文件路径,以材料名命名
try {
const content = file.readTextSync(recordPath); // 同步读取记录文件
const lines = content.split('\n');
const completeRecords = []; // 用于存储完整的记录
// 从文件内容的开头开始查找
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('路径名: ')) {
const currentPathName = lines[i].split('路径名: ')[1];
if (currentPathName === 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) {
let perTime;
if (quantityChange[resourceName] !== 0) {
// 保留两位小数
perTime = parseFloat((runTime / quantityChange[resourceName]).toFixed(2));
} else {
perTime = Infinity; // 数量变化为 0 时,设置为 Infinity
}
completeRecords.push(perTime);
}
}
}
}
}
// 如果完整记录少于3条返回 null
if (completeRecords.length < 3) {
log.warn(` ${pathName}有效记录不足3条无法计算平均时间成本: ${recordPath}`);
return null;
}
// 只考虑最近的5条记录 过滤掉 Infinity 和 NaN 值
const recentRecords = completeRecords.slice(-5).filter(record => !isNaN(record) && record !== Infinity);
// 打印最近的记录
log.info(` ${pathName}最近的记录: ${JSON.stringify(recentRecords)}`);
// 计算平均值和标准差
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);// 使用1倍标准差作为过滤条件
// 如果过滤后没有剩余数据,返回 null
if (filteredRecords.length === 0) {
log.warn(` ${pathName}记录数据差异过大,无法计算有效的时间成本: ${recordPath}`);
return null;
}
// 计算平均时间成本
const averagePerTime = parseFloat((filteredRecords.reduce((acc, val) => acc + val, 0) / filteredRecords.length).toFixed(2));
return averagePerTime;
} catch (error) {
log.warn(`缺失耗时或者数量变化,无法计算 ${pathName}时间成本: ${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()}`);
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, ['.png']);
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];
}
}
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)) {
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, 3, ['.json']);
// 将路径和资源名绑定,避免重复提取
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 中的分类被初始化为空数组
if (Object.keys(selected_materials_array).length === 0) {
log.warn("==================\n 未选择【材料分类】!\n ==================");
} else {
selected_materials_array.forEach(selectedCategory => {
if (!materialCategoryMap[selectedCategory]) {
materialCategoryMap[selectedCategory] = [];
}
});
}
// 如果 isOnlyPathing 为 true移除 materialCategoryMap 中的空数组
if (isOnlyPathing) {
Object.keys(materialCategoryMap).forEach(category => {
if (materialCategoryMap[category].length === 0) {
delete materialCategoryMap[category];
}
});
}
// 调用背包材料统计
const pathingMaterialCounts = await MaterialPath(materialCategoryMap);
// 调用 filterLowCountMaterials 过滤材料信息,先将嵌套数组展平,然后再进行筛选
const lowCountMaterialsFiltered = filterLowCountMaterials(pathingMaterialCounts.flat(), materialCategoryMap);
// 展平数组并按数量从小到大排序
let flattenedLowCountMaterials = lowCountMaterialsFiltered
.flat()
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10));
// 提取低数量材料的名称
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;
});
// 合并优先路径和普通路径
const allPaths = prioritizedPaths.concat(normalPaths);
dispatcher.addTimer(new RealtimeTimer("AutoPick", { "forceInteraction": false }));
// 假设 flattenedLowCountMaterials 是一个全局变量或在外部定义的变量
let currentMaterialName = null; // 用于记录当前材料名
// 遍历所有路径文件
for (const { path: pathingFilePath, resourceName } of allPaths) {
const pathName = basename(pathingFilePath); // 假设路径文件名即为材料路径
// 查找材料对应的CD分类
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);
// 计算效率
const perTime = calculatePerTime(resourceName, pathName, recordDir);
log.info(`路径文件:${pathName} 单个材料耗时:${perTime}`);
// 判断是否可以运行脚本
if (
canRunPathingFile(currentTime, lastEndTime, refreshCD, pathName) &&
checkPathNameFrequency(recordDir, resourceName, pathName) &&
(perTime === null || perTime <= timeCost)
) {
log.info(`可调用路径文件:${pathName}`);
// 根据 materialCategoryMap 构建 resourceCategoryMap
const resourceCategoryMap = {};
for (const [materialCategory, materialList] of Object.entries(materialCategoryMap)) {
if (materialList.includes(resourceName)) {
resourceCategoryMap[materialCategory] = [resourceName];
break;
}
}
// 输出 resourceCategoryMap 以供调试
log.info(`resourceCategoryMap: ${JSON.stringify(resourceCategoryMap, null, 2)}`);
// 如果材料名发生变化,更新 flattenedLowCountMaterials
if (currentMaterialName !== resourceName) {
currentMaterialName = resourceName; // 更新当前材料名
// 调用背包材料统计(获取当前材料数量)
const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap);
// 展平数组并按数量从小到大排序
flattenedLowCountMaterials = updatedLowCountMaterials
.flat()
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10));
log.info(`材料名变更,更新了 flattenedLowCountMaterials`);
}
// 记录开始时间
const startTime = new Date().toLocaleString();
// 在路径执行前执行一次位移监测
const initialPosition = genshin.getPositionFromMap();
let initialCumulativeDistance = 0;
// 调用路径文件
await pathingScript.runFile(pathingFilePath);
// 在路径执行后执行一次位移监测
const finalPosition = genshin.getPositionFromMap();
const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition);
// 记录结束时间
const endTime = new Date().toLocaleString();
// 计算运行时间
const runTime = (new Date(endTime) - new Date(startTime)) / 1000; // 秒
// 调用背包材料统计(获取调用路径文件后的材料数量)
const updatedLowCountMaterials = await MaterialPath(resourceCategoryMap);
// 展平数组并按数量从小到大排序
const flattenedUpdatedMaterialCounts = updatedLowCountMaterials
.flat()
.sort((a, b) => parseInt(a.count, 10) - parseInt(b.count, 10));
// 提取更新后的低数量材料的名称
const updatedLowCountMaterialNames = flattenedUpdatedMaterialCounts.map(material => material.name);
// 创建一个映射,用于存储更新前后的数量差值
const materialCountDifferences = {};
// 遍历更新后的材料数量,计算差值
flattenedUpdatedMaterialCounts.forEach(updatedMaterial => {
const originalMaterial = flattenedLowCountMaterials.find(material => material.name === updatedMaterial.name);
if (originalMaterial) {
const originalCount = parseInt(originalMaterial.count, 10);
const updatedCount = parseInt(updatedMaterial.count, 10);
const difference = updatedCount - originalCount;
materialCountDifferences[updatedMaterial.name] = difference;
}
});
// 更新 flattenedLowCountMaterials 为最新的材料数量
flattenedLowCountMaterials = flattenedLowCountMaterials.map(material => {
// 找到对应的更新后的材料数量
const updatedMaterial = flattenedUpdatedMaterialCounts.find(updated => updated.name === material.name);
if (updatedMaterial) {
return { ...material, count: updatedMaterial.count }; // 更新数量
}
return material;
});
// 打印数量差值
log.info(`数量变化: ${JSON.stringify(materialCountDifferences, null, 2)}`);
// 记录运行时间到材料对应的文件中
recordRunTime(resourceName, pathName, startTime, endTime, runTime, recordDir, materialCountDifferences, finalCumulativeDistance);
log.info(`当前材料名: ${JSON.stringify(resourceName, null, 2)}`);
categoryFound = true;
break;
} else {
if (perTime !== null && perTime > timeCost) {
log.info(`路径文件 ${pathName} 的单个材料耗时大于 ${timeCost} ,不执行`);
} else {
log.info(`路径文件 ${pathName} 未能执行!`);
}
}
}
}
if (categoryFound) break;
}
}
} catch (error) {
log.error(`操作失败: ${error}`);
}
})();
// 辅助函数:计算两点之间的距离
function calculateDistance(initialPosition, finalPosition) {
const deltaX = finalPosition.X - initialPosition.X;
const deltaY = finalPosition.Y - initialPosition.Y;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
// 修改后的位移监测函数
async function monitorDisplacement(monitoring, resolve) {
// 获取对象的实际初始位置
let lastPosition = genshin.getPositionFromMap();
let cumulativeDistance = 0; // 初始化累计位移量
let lastUpdateTime = Date.now(); // 记录上一次位置更新的时间
while (monitoring) {
const currentPosition = genshin.getPositionFromMap(); // 获取当前位置
const currentTime = Date.now(); // 获取当前时间
// 计算位移量
const deltaX = currentPosition.X - lastPosition.X;
const deltaY = currentPosition.Y - lastPosition.Y;
let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 如果位移量小于0.5则视为0
if (distance < 0.5) {
distance = 0;
}
// 如果有位移,更新累计位移量和最后更新时间
if (distance > 0) {
cumulativeDistance += distance; // 累计位移量
lastUpdateTime = currentTime; // 更新最后更新时间
}
// 检测是否超过5秒没有位移
if (currentTime - lastUpdateTime >= 5000) {
// 触发跳跃
keyPress(VK_SPACE);
lastUpdateTime = currentTime; // 重置最后更新时间
}
// 输出位移信息和累计位移量
log.info(`时间:${(currentTime - lastUpdateTime) / 1000}秒,位移信息: X=${currentPosition.X}, Y=${currentPosition.Y}, 当前位移量=${distance.toFixed(2)}, 累计位移量=${cumulativeDistance.toFixed(2)}`);
// 更新最后位置
lastPosition = currentPosition;
// 等待1秒再次检查
await sleep(1000);
}
// 当监测结束时,返回累计位移量
resolve(cumulativeDistance);
}
// 识图点击主逻辑
async function imageClick() {
// 定义包含多个文件夹的根目录
const rootDir = "assets/imageClick";
// 获取根目录下的所有子目录路径,深度为 1
const subDirs = readAllFilePaths(rootDir, 0, 0, [], true);
// 遍历子目录
for (const subDir of subDirs) {
// 从 subDir 中找到 icon 和 Picture 文件夹
const entries = readAllFilePaths(subDir, 0, 1, [], true); // 获取当前子目录下的所有条目
// 筛选出 icon 和 Picture 文件夹
const iconDir = entries.find(entry => entry.endsWith('\icon'));
const pictureDir = entries.find(entry => entry.endsWith('\Picture'));
if (!iconDir) {
continue;
}
if (!pictureDir) {
continue;
}
// 读取 icon 文件夹下的所有文件路径
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
// 读取 Picture 文件夹下的所有文件路径
const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']);
// 创建图标的 RecognitionObject
const iconRecognitionObjects = [];
for (const filePath of iconFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图标失败:${filePath}`);
continue; // 跳过当前文件
}
const recognitionObject = RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080);
iconRecognitionObjects.push({ name: basename(filePath), ro: recognitionObject });
}
// 创建图库的 ImageRegion以获取图标的XYWH
const pictureRegions = [];
for (const filePath of pictureFilePaths) {
const mat = file.readImageMatSync(filePath);
if (mat.empty()) {
log.error(`加载图库失败:${filePath}`);
continue; // 跳过当前文件
}
pictureRegions.push({ name: basename(filePath), region: new ImageRegion(mat, 0, 0) });
}
// 在每张图片中查找图标的位置信息
const foundRegions = [];
for (const picture of pictureRegions) {
for (const icon of iconRecognitionObjects) {
const foundRegion = picture.region.find(icon.ro);
if (foundRegion.isExist()) {
foundRegions.push({
pictureName: picture.name,
iconName: icon.name,
region: foundRegion
});
}
}
}
// 在屏幕上查找并点击图标
for (const foundRegion of foundRegions) {
const tolerance = 1; // 容错区间
const iconMat = file.readImageMatSync(`${iconDir}/${foundRegion.iconName}`);
const recognitionObject = RecognitionObject.TemplateMatch(iconMat, foundRegion.region.x - tolerance, foundRegion.region.y - tolerance, foundRegion.region.width + 2 * tolerance, foundRegion.region.height + 2 * tolerance);
recognitionObject.threshold = 0.9; // 设置识别阈值为 0.9
const result = captureGameRegion().find(recognitionObject);
if (result.isExist()) {
const x = Math.round(foundRegion.region.x + foundRegion.region.width / 2);
const y = Math.round(foundRegion.region.y + foundRegion.region.height / 2);
log.info(`即将点击图标:${foundRegion.iconName},位置: (${x}, ${y})`);
await click(x, y); // 假设 click 是一个可用的点击函数
log.info(`点击 ${foundRegion.iconName}成功,位置: (${x}, ${y})`);
await sleep(500); // 等待一段时间
} else {
// log.info(`无过期材料弹窗:${foundRegion.iconName},正常跳过`);
}
}
}
}