1522 lines
64 KiB
JavaScript
1522 lines
64 KiB
JavaScript
|
||
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": "0",
|
||
"I": "1", "l": "1", "i": "1", "1": "1", "一": "1",
|
||
"Z": "2", "z": "2", "2": "2", "二": "2",
|
||
"E": "3", "e": "3", "3": "3", "三": "3",
|
||
"A": "4", "a": "4", "4": "4",
|
||
"S": "5", "s": "5", "5": "5",
|
||
"G": "6", "b": "6", "6": "6",
|
||
"T": "7", "t": "7", "7": "7",
|
||
"B": "8", "θ": "8", "8": "8",
|
||
"g": "9", "q": "9", "9": "9",
|
||
};
|
||
|
||
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
|
||
let 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,以获取图标的X,Y,W,H
|
||
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},正常跳过`);
|
||
}
|
||
}
|
||
}
|
||
}
|