[全自动地脉] (#2485)

* feat(树脂): 1.树脂耗尽模式基础上新增==>刷取次数取小值<==功能
2.优化原粹树脂识别速度

* feat(AutoLeyLineOutcrop): 添加最小替换次数功能

* feat(physical): 添加运行次数统计和OCR识别区域优化

* refactor: 将info日志级别调整为debug级别

* refactor: 调整操作延迟时间从800ms到1000ms

* refactor: 优化模板匹配配置和错误信息输出

* fix: 修复错误处理和通知设置引用问题

* refactor: 修改变量声明为let

* refactor: 优化区域对象创建和资源释放

* fix(utils): 修改原粹树脂识别函数的默认参数

- 将 opToMainUi 参数的默认值从 true 改为 false
- 确保函数在不切换到主界面的情况下也能正常执行
- 避免不必要的界面跳转提升用户体验

* fix(utils): 修复图像识别逻辑中的区域查找问题

- 修正了 captureGameRegion 的调用方式,确保正确获取游戏区域
- 更新了模板匹配按钮查找逻辑,使用 region.find 替代 captureGameRegion().find
- 添加了资源释放逻辑,确保 regionA 在使用后正确 Dispose
- 移除了重复和注释掉的代码,提升代码可读性
- 保留了错误处理机制,确保路径错误时能正确抛出异常并记录日志

* fix(utils): 调整资源释放逻辑以防止内存泄漏

- 将 region.Dispose() 移至 await sleep(ms) 之后确保区域对象使用后正确释放
- 统一所有图像识别后的资源清理操作顺序
- 避免因提前释放导致的潜在空引用异常
- 确保每次识别操作结束后及时回收内存资源
- 优化错误处理流程中的资源管理时机
- 提高脚本运行稳定性与性能表现
This commit is contained in:
云端客
2025-12-21 16:47:49 +08:00
committed by GitHub
parent d4d2770d6e
commit 5db53d18f8
9 changed files with 431 additions and 113 deletions

View File

@@ -79,6 +79,7 @@
## 更新日志
### 4.4
- 新增树脂耗尽模式
- 新增树脂耗尽模式下刷取次数取小值
- 新增更新提醒
- 新增一条龙模式
- BGI最低版本要求改为0.52.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -17,9 +17,9 @@ let recheckCount = 0; // 树脂重新检查次数(防止无限递归)
const MAX_RECHECK_COUNT = 3; // 最大重新检查次数
let consecutiveFailureCount = 0; // 连续战斗失败次数
const MAX_CONSECUTIVE_FAILURES = 5; // 最大连续失败次数,超过后终止脚本
const ocrRegion1 = { x: 800, y: 200, width: 300, height: 100 }; // 中心区域
const ocrRegion2 = { x: 0, y: 200, width: 300, height: 300 }; // 追踪任务区域
const ocrRegion3 = { x: 1200, y: 520, width: 300, height: 300 }; // 拾取区域
const ocrRegion1 = {x: 800, y: 200, width: 300, height: 100}; // 中心区域
const ocrRegion2 = {x: 0, y: 200, width: 300, height: 300}; // 追踪任务区域
const ocrRegion3 = {x: 1200, y: 520, width: 300, height: 300}; // 拾取区域
// 预定义识别对象
const openRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/open.png"));
@@ -37,22 +37,20 @@ const ocrRoThis = RecognitionObject.ocrThis;
(async function () {
try {
await runLeyLineOutcropScript();
}
catch (error) {
} catch (error) {
// 全局错误捕获,记录并发送错误日志
log.error("出错了: {error}", error.message);
if (isNotification) {
notification.error(`出错了: ${error.message}`);
}
}
finally {
} finally {
// 确保退出奖励界面(如果在奖励界面)
try {
await ensureExitRewardPage();
} catch (exitError) {
log.warn(`退出奖励界面时出错: ${exitError.message}`);
}
if (!marksStatus) {
// 任何时候都确保自定义标记处于打开状态
await openCustomMarks();
@@ -68,10 +66,10 @@ const ocrRoThis = RecognitionObject.ocrThis;
async function runLeyLineOutcropScript() {
// 初始化加载配置和设置并校验
initialize();
// 处理树脂耗尽模式(如果开启)
let runTimesValue = await handleResinExhaustionMode();
if(runTimesValue <= 0) {
if (runTimesValue <= 0) {
throw new Error("树脂耗尽,脚本将结束运行");
}
@@ -79,7 +77,7 @@ async function runLeyLineOutcropScript() {
// 执行地脉花挑战
await runLeyLineChallenges();
// 如果是树脂耗尽模式,执行完毕后再次检查是否还有树脂
if (settings.isResinExhaustionMode) {
await recheckResinAndContinue();
@@ -102,13 +100,14 @@ async function initialize() {
"loadSettings.js",
"processLeyLineOutcrop.js",
"recognizeTextInRegion.js",
"physical.js",
"calCountByResin.js"
];
];
for (const fileName of utils) {
eval(file.readTextSync(`utils/${fileName}`));
}
} catch (error) {
throw new Error(`JS文件缺失: ${error.message}`);
throw new Error(`JS文件缺失: ${error.message}`);
}
// 2. 加载配置文件
try {
@@ -169,17 +168,17 @@ async function handleResinExhaustionMode() {
if (!settings.isResinExhaustionMode) {
return settings.timesValue;
}
log.info("树脂耗尽模式已开启,开始统计可刷取次数");
try {
// 调用树脂统计函数
const resinResult = await calCountByResin();
if (!resinResult || typeof resinResult.count !== 'number') {
throw new Error("树脂统计返回结果无效");
}
// 检查统计到的次数是否有效
if (resinResult.count <= 0) {
log.warn("统计到的可刷取次数为0脚本将不会执行任何刷取操作");
@@ -187,27 +186,32 @@ async function handleResinExhaustionMode() {
notification.send("树脂耗尽模式统计到的可刷取次数为0脚本将结束运行");
}
}
// 使用统计到的次数替换设置中的刷取次数
settings.timesValue = resinResult.count;
if (physical.OpenModeCountMin) {
settings.timesValue = Math.min(resinResult.count, settings.timesValue);
log.info(`当前开启模式刷取数量: {key}`, settings.timesValue);
} else {
// 使用统计到的次数替换设置中的刷取次数
settings.timesValue = resinResult.count;
}
physical.NeedRunsCount = settings.timesValue;
log.info(`树脂统计成功:`);
log.info(` 原粹树脂可刷取: ${resinResult.originalResinTimes}`);
log.info(` 浓缩树脂可刷取: ${resinResult.condensedResinTimes}`);
log.info(` 须臾树脂可刷取: ${resinResult.transientResinTimes}${settings.useTransientResin ? '' : '(未开启使用)'}`);
log.info(` 脆弱树脂可刷取: ${resinResult.fragileResinTimes}${settings.useFragileResin ? '' : '(未开启使用)'}`);
log.info(` 总计可刷取次数: ${resinResult.count}`);
log.info(` 总计可刷取次数: {count} 次,最小替换:{key}`, (physical.OpenModeCountMin ? settings.timesValue : resinResult.count), (physical.OpenModeCountMin ? "开启" : "未开启"));
// 发送通知
if (isNotification) {
const notificationText =
const notificationText =
`全自动地脉花脚本已启用树脂耗尽模式\n\n` +
`树脂统计结果(当前可刷取次数)\n` +
`原粹树脂: ${resinResult.originalResinTimes}\n` +
`浓缩树脂: ${resinResult.condensedResinTimes}\n` +
`须臾树脂: ${resinResult.transientResinTimes}${settings.useTransientResin ? '' : '(未开启)'}\n` +
`脆弱树脂: ${resinResult.fragileResinTimes}${settings.useFragileResin ? '' : '(未开启)'}\n\n` +
`总计可刷取: ${resinResult.count}\n`;
`总计可刷取: ${physical.OpenModeCountMin ? settings.timesValue : resinResult.count}\n最小替换:${(physical.OpenModeCountMin ? "开启" : "未开启")}\n`;
notification.send(notificationText);
}
@@ -216,7 +220,7 @@ async function handleResinExhaustionMode() {
// 统计失败,使用设置中的刷取次数
log.error(`树脂统计失败: ${error.message}`);
log.warn(`将使用设置中的刷取次数: ${settings.timesValue}`);
if (isNotification) {
notification.send(`树脂耗尽模式:统计失败,将使用设置中的刷取次数 ${settings.timesValue}\n错误信息: ${error.message}`);
}
@@ -231,7 +235,14 @@ async function handleResinExhaustionMode() {
async function recheckResinAndContinue() {
// 递归深度检查,防止无限循环
recheckCount++;
if (physical.OpenModeCountMin) {
physical.AlreadyRunsCount++;
if (physical.NeedRunsCount<=physical.AlreadyRunsCount){
log.info(`[已开启取小值]树脂耗尽模式:任务已完成,已经运行{count}次`,physical.AlreadyRunsCount);
return;
}
}
if (recheckCount > MAX_RECHECK_COUNT) {
log.warn(`已达到最大重新检查次数限制 (${MAX_RECHECK_COUNT} 次),停止继续检查`);
if (isNotification) {
@@ -239,27 +250,27 @@ async function recheckResinAndContinue() {
}
return;
}
log.info("=".repeat(50));
log.info(`树脂耗尽模式:任务已完成,开始检查树脂状态...`);
log.info("=".repeat(50));
try {
// 重新统计树脂
const resinResult = await calCountByResin();
if (!resinResult || typeof resinResult.count !== 'number') {
log.warn("树脂统计返回结果无效,结束运行");
return;
}
log.info(`树脂检查结果:`);
log.info(` 原粹树脂可刷取: ${resinResult.originalResinTimes}`);
log.info(` 浓缩树脂可刷取: ${resinResult.condensedResinTimes}`);
log.info(` 须臾树脂可刷取: ${resinResult.transientResinTimes}${settings.useTransientResin ? '' : '(未开启使用)'}`);
log.info(` 脆弱树脂可刷取: ${resinResult.fragileResinTimes}${settings.useFragileResin ? '' : '(未开启使用)'}`);
log.info(` 总计可刷取次数: ${resinResult.count}`);
// 安全检查:如果检测到的次数异常多,可能是识别错误
if (resinResult.count > 50) {
log.warn(`检测到异常的可刷取次数 (${resinResult.count}),为安全起见停止运行`);
@@ -268,23 +279,23 @@ async function recheckResinAndContinue() {
}
return;
}
// 如果还有树脂可用,继续执行
if (resinResult.count > 0) {
log.info(`检测到还有 ${resinResult.count} 次可刷取,继续执行地脉花挑战...`);
log.info(`(这是第 ${recheckCount} 次额外检查并继续执行)`);
if (isNotification) {
notification.send(`树脂耗尽模式:检测到还有 ${resinResult.count} 次可刷取,继续执行(第 ${recheckCount} 次额外执行)`);
}
// 重置运行次数并更新目标次数
currentRunTimes = 0;
settings.timesValue = resinResult.count;
// 递归调用继续执行地脉花挑战和重新检查
await runLeyLineChallenges();
// 执行完后再次检查(递归)
await recheckResinAndContinue();
} else {
@@ -333,7 +344,7 @@ async function prepareForLeyLineRun() {
setGameMetrics(1920, 1080, 1); // 看起来没什么用
// 1. 开局传送到七天神像
if (!oneDragonMode) {
await genshin.tpToStatueOfTheSeven();
await genshin.tpToStatueOfTheSeven();
}
// 2. 切换战斗队伍
@@ -425,7 +436,7 @@ async function loadNodeData() {
try {
const nodeDataText = await file.readText("LeyLineOutcropData.json");
const rawData = JSON.parse(nodeDataText);
// 适配数据结构:将原始数据转换为代码期望的格式
return adaptNodeData(rawData);
} catch (error) {
@@ -444,7 +455,7 @@ function adaptNodeData(rawData) {
node: [],
indexes: rawData.indexes
};
// 添加传送点设置type为"teleport"
if (rawData.teleports) {
for (const teleport of rawData.teleports) {
@@ -456,7 +467,7 @@ function adaptNodeData(rawData) {
});
}
}
// 添加地脉花节点设置type为"blossom"
if (rawData.blossoms) {
for (const blossom of rawData.blossoms) {
@@ -468,13 +479,13 @@ function adaptNodeData(rawData) {
});
}
}
// 根据edges构建next和prev关系
if (rawData.edges) {
for (const edge of rawData.edges) {
const sourceNode = adaptedData.node.find(node => node.id === edge.source);
const targetNode = adaptedData.node.find(node => node.id === edge.target);
if (sourceNode && targetNode) {
sourceNode.next.push({
target: edge.target,
@@ -484,9 +495,9 @@ function adaptNodeData(rawData) {
}
}
}
log.debug(`适配数据完成:传送点 ${rawData.teleports ? rawData.teleports.length : 0} 个,地脉花 ${rawData.blossoms ? rawData.blossoms.length : 0} 个,边缘 ${rawData.edges ? rawData.edges.length : 0}`);
return adaptedData;
}
@@ -532,7 +543,7 @@ function findPathsToTarget(nodeData, targetNode) {
/**
* 如果需要,尝试查找反向路径(从目标节点的前置节点到传送点再到目标)
* @param {Object} nodeData - 节点数据
* @param {Object} nodeData - 节点数据
* @param {Object} targetNode - 目标节点
* @param {Object} nodeMap - 节点映射
* @param {Array} existingPaths - 已找到的路径
@@ -628,13 +639,13 @@ async function executePath(path) {
const routePath = path.routes[path.routes.length - 1];
const targetPath = routePath.replace('assets/pathing/', 'assets/pathing/target/').replace('-rerun', '');
await processLeyLineOutcrop(settings.timeout, targetPath);
// 尝试领取奖励,如果失败则抛出异常停止执行
const rewardSuccess = await attemptReward();
if (!rewardSuccess) {
throw new Error("无法领取奖励,树脂不足或其他原因");
}
// 成功完成地脉花挑战,重置连续失败计数器
consecutiveFailureCount = 0;
}
@@ -677,14 +688,14 @@ async function handleNoStrategyFound() {
log.error("未找到对应的地脉花策略,请再次运行脚本");
log.error("如果仍然不行,请截图{1}游戏界面,并反馈给作者!", "*完整的*");
log.error("完整的游戏界面!完整的游戏界面!完整的游戏界面!");
// 确保退出奖励界面 TODO: 可能会影响debug先不执行ensureExitRewardPage
// try {
// await ensureExitRewardPage();
// } catch (exitError) {
// log.warn(`退出奖励界面时出错: ${exitError.message}`);
// }
if (isNotification) {
notification.error("未找到对应的地脉花策略");
await genshin.returnMainUi();
@@ -789,7 +800,7 @@ async function autoFight(timeout) {
logFightResult = fightResult ? "成功" : "失败";
log.info(`战斗结束,战斗结果:${logFightResult}`);
cts.cancel();
try {
await fightTask;
} catch (error) {
@@ -800,7 +811,7 @@ async function autoFight(timeout) {
log.warn(`战斗任务结束时出现异常: ${error.message}`);
}
}
return fightResult;
}
@@ -993,7 +1004,7 @@ async function adjustViewForReward(boxIconRo, token) {
captureRegion.dispose();
if (!iconRes.isExist()) {
log.warn("未找到图标,等待一下");
await sleep(1000);
await sleep(1000);
continue; // 没有找到图标等一秒再继续
// throw new Error('未找到图标,没有地脉花');
}

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "全自动地脉花",
"version": "4.4.6",
"version": "4.4.7",
"tags": ["地脉花"],
"bgi_version": "0.52.0",
"description": "基于OCR图像识别的全自动刷取地脉花。\n💡更多信息请查看README! \n\n----------注意事项----------\n●仅支持BetterGI 0.52.0 及以上版本!\n●部分地脉花因特殊原因不支持全自动具体的点位请在手册中查看。\n●树脂使用的优先级2倍原粹树脂 > 浓缩树脂 > 原粹树脂。\n●运行时会传送到七天神像设置中设置的七天神像需要关闭七天神像设置中的“是否就近七天神像恢复血量”并指定七天神像。\n●战斗策略注意调度器设置中地图追踪行走配置里的“允许在JsSpript中使用”和“覆盖JS中的自动战斗配置”只有在都打开的情况下脚本才会使用下面的战斗配置否则会使用独立任务中的战斗策略。战斗超时时间不能大于脚本自定义配置中的时间。\n\n如果遇到问题请先参照README中的方法进行解决。",
@@ -33,6 +33,10 @@
{
"name": "羊汪汪",
"links": "https://github.com/ColinXHL"
},
{
"name": "云端客",
"links": "https://github.com/Kirito520Asuna"
}
],
"settings_ui": "settings.json",

View File

@@ -28,6 +28,16 @@
],
"default": "稻妻"
},
{
"name": "isResinExhaustionMode",
"type": "checkbox",
"label": "树脂耗尽模式\n开启后会自动统计所有可用树脂并计算可刷取次数替换上方设置的刷取次数\n注意请同时设置树脂刷取次数将在统计失败时使用"
},
{
"name": "openModeCountMin",
"type": "checkbox",
"label": "刷取次数取小值\n开启后树脂耗尽模式中计算可刷取次数和上方设置的刷取次数取小值"
},
{
"name": "count",
"type": "input-text",
@@ -77,11 +87,6 @@
"type": "checkbox",
"label": "一条龙模式\n开启后会跳过开始的前往七天神像以及强制更新"
},
{
"name": "isResinExhaustionMode",
"type": "checkbox",
"label": "树脂耗尽模式\n开启后会自动统计所有可用树脂并计算可刷取次数替换上方设置的刷取次数\n注意请同时设置树脂刷取次数将在统计失败时使用"
},
{
"name": "isGoToSynthesizer",
"type": "select",

View File

@@ -17,20 +17,20 @@ const RESIN_ICONS = {
// 普通数字识别对象1-4
const NUMBER_ICONS = [
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1.png")), value: 1 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2.png")), value: 2 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3.png")), value: 3 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4.png")), value: 4 }
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1.png")), value: 1},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2.png")), value: 2},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3.png")), value: 3},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4.png")), value: 4}
];
// 白色数字识别对象0-5用于浓缩树脂
const WHITE_NUMBER_ICONS = [
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num0_white.png")), value: 0 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1_white.png")), value: 1 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2_white.png")), value: 2 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3_white.png")), value: 3 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4_white.png")), value: 4 },
{ ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num5_white.png")), value: 5 }
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num0_white.png")), value: 0},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num1_white.png")), value: 1},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num2_white.png")), value: 2},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num3_white.png")), value: 3},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num4_white.png")), value: 4},
{ro: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/num5_white.png")), value: 5}
];
// 配置常量
@@ -39,19 +39,19 @@ const CONFIG = {
SLEEP_INTERVAL: 500, // 循环间隔时间(毫秒)
UI_DELAY: 1500, // UI操作延迟时间毫秒
MAP_ZOOM_LEVEL: 6, // 地图缩放级别
// 点击坐标
COORDINATES: {
MAP_SWITCH: { x: 1840, y: 1020 }, // 地图右下角切换按钮
MONDSTADT: { x: 1420, y: 180 }, // 蒙德选择按钮
AVOID_SELECTION: { x: 1090, y: 450 } // 避免选中效果的点击位置
MAP_SWITCH: {x: 1840, y: 1020}, // 地图右下角切换按钮
MONDSTADT: {x: 1420, y: 180}, // 蒙德选择按钮
AVOID_SELECTION: {x: 1090, y: 450} // 避免选中效果的点击位置
},
// OCR识别区域配置
OCR_REGIONS: {
ORIGINAL_RESIN: { width: 200, height: 40 },
CONDENSED_RESIN: { width: 90, height: 40 },
OTHER_RESIN: { width: 0, height: 60 } // width会根据图标宽度动态设置
ORIGINAL_RESIN: {width: 200, height: 40},
CONDENSED_RESIN: {width: 90, height: 40},
OTHER_RESIN: {width: 0, height: 60} // width会根据图标宽度动态设置
}
};
@@ -73,7 +73,7 @@ let resinCounts = {
*/
async function recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TIMEOUT) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 直接链式调用不保存gameRegion变量避免内存管理问题
@@ -86,7 +86,7 @@ async function recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TI
}
await sleep(CONFIG.SLEEP_INTERVAL);
}
log.warn(`经过多次尝试,仍然无法识别图像`);
return null;
}
@@ -143,10 +143,10 @@ async function recognizeWhiteNumberByImage(ocrRegion) {
* @returns {boolean} 是否在区域内
*/
function isPointInRegion(point, region) {
return point.x >= region.x &&
point.x <= region.x + region.width &&
point.y >= region.y &&
point.y <= region.y + region.height;
return point.x >= region.x &&
point.x <= region.x + region.width &&
point.y >= region.y &&
point.y <= region.y + region.height;
}
/**
@@ -162,17 +162,17 @@ async function recognizeNumberByOCR(ocrRegion, pattern) {
const ocrRo = RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
captureRegion = captureGameRegion();
resList = captureRegion.findMulti(ocrRo);
if (!resList || resList.length === 0) {
log.warn("OCR未识别到任何文本");
return null;
}
for (const res of resList) {
if (!res || !res.text) {
continue;
}
const numberMatch = res.text.match(pattern);
if (numberMatch) {
const number = parseInt(numberMatch[1] || numberMatch[0]);
@@ -201,7 +201,26 @@ async function recognizeNumberByOCR(ocrRegion, pattern) {
* 统计原粹树脂数量
* @returns {number} 原粹树脂数量
*/
async function countOriginalResin() {
/**
* 统计原粹树脂数量
* @returns {number} 原粹树脂数量
*/
async function countOriginalResin(tryOriginalMode,opToMainUi) {
if (tryOriginalMode) {
log.info("尝试使用原始模式");
return await countOriginalResinBackup()
} else {
let ocrPhysical = await physical.ocrPhysical(opToMainUi);
await sleep(600)
if (ocrPhysical && ocrPhysical.ok) {
return ocrPhysical.remainder;
} else {
throw new Error("ocrPhysical error");
}
}
}
async function countOriginalResinBackup() {
const originalResin = await recognizeImage(RESIN_ICONS.ORIGINAL);
if (!originalResin) {
log.warn(`未找到原粹树脂图标`);
@@ -269,7 +288,7 @@ async function countCondensedResin() {
// OCR识别整个界面的文本
let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height);
textList = captureRegion.findMulti(ocrRo);
for (const res of textList) {
if (res.text.includes("当前拥有")) {
const match = res.text.match(/当前拥有\s*([0-5ss])/);
@@ -371,20 +390,33 @@ async function openMap() {
log.info("打开地图界面");
keyPress("M");
await sleep(CONFIG.UI_DELAY);
// 切换到国家选择界面
click(CONFIG.COORDINATES.MAP_SWITCH.x, CONFIG.COORDINATES.MAP_SWITCH.y);
await sleep(CONFIG.UI_DELAY);
// click(CONFIG.COORDINATES.MAP_SWITCH.x, CONFIG.COORDINATES.MAP_SWITCH.y);
// await sleep(CONFIG.UI_DELAY);
// 选择蒙德
click(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y);
await sleep(CONFIG.UI_DELAY);
// click(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y);
// await sleep(CONFIG.UI_DELAY);
// await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y)
// 设置地图缩放级别,排除识别干扰
await genshin.setBigMapZoomLevel(CONFIG.MAP_ZOOM_LEVEL);
log.info("地图界面设置完成");
}
/**
* 切换到国家选择界面的异步函数
* 通过点击指定坐标并等待界面加载来完成切换操作
*/
async function switchtoCountrySelection(x, y) {
// 切换到国家选择界面
click(CONFIG.COORDINATES.MAP_SWITCH.x, CONFIG.COORDINATES.MAP_SWITCH.y);
await sleep(CONFIG.UI_DELAY);
click(x, y);
await sleep(CONFIG.UI_DELAY);
}
/**
* 打开补充树脂界面
*/
@@ -405,7 +437,7 @@ async function openReplenishResinUi() {
*/
function displayResults(results) {
const resultText = `原粹:${results.original} 浓缩:${results.condensed} 须臾:${results.transient} 脆弱:${results.fragile}`;
log.info(`============ 树脂统计结果 ============`);
log.info(`原粹树脂数量: ${results.original}`);
log.info(`浓缩树脂数量: ${results.condensed}`);
@@ -420,42 +452,55 @@ function displayResults(results) {
* 统计所有树脂数量的主函数
* @returns {Object} 包含所有树脂数量的对象
*/
this.countAllResin = async function() {
this.countAllResin = async function () {
try {
setGameMetrics(1920, 1080, 1);
log.info("开始统计树脂数量");
// 返回主界面
await genshin.returnMainUi();
let toMainUi=true
// await genshin.returnMainUi();
// await sleep(CONFIG.UI_DELAY);
let tryPass = true;
try {
resinCounts.original = await countOriginalResin(false,toMainUi);
} catch (e) {
tryPass = false
}
await sleep(CONFIG.UI_DELAY);
// 打开地图界面统计原粹/浓缩树脂
await openMap();
await sleep(CONFIG.UI_DELAY);
log.info("开始统计地图界面中的树脂");
resinCounts.original = await countOriginalResin();
if (!tryPass){
// 如果第一次尝试失败,则切换到蒙德
await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y)
resinCounts.original = await countOriginalResin(!tryPass);
}
resinCounts.condensed = await countCondensedResin();
// 打开补充树脂界面统计须臾/脆弱树脂
await openReplenishResinUi();
await sleep(CONFIG.UI_DELAY);
// 点击避免选中效果影响统计
click(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y);
await sleep(500);
log.info("开始统计补充树脂界面中的树脂");
resinCounts.transient = await countTransientResin();
resinCounts.fragile = await countFragileResin();
// 显示结果
displayResults(resinCounts);
// 返回主界面
await genshin.returnMainUi();
await sleep(CONFIG.UI_DELAY);
log.info("树脂统计完成");
return {
originalResinCount: resinCounts.original,
@@ -463,7 +508,7 @@ function displayResults(results) {
transientResinCount: resinCounts.transient,
fragileResinCount: resinCounts.fragile
};
} catch (error) {
log.error(`统计树脂数量时发生异常: ${error.message}`);
throw error;
@@ -480,7 +525,7 @@ function displayResults(results) {
* fragileResinTimes: number
* }
*/
this.calCountByResin = async function() {
this.calCountByResin = async function () {
try {
let countResult = await this.countAllResin();
let count = 0;
@@ -508,7 +553,7 @@ this.calCountByResin = async function() {
// 2. 浓缩树脂每个计算为1次
let condensedResinTimes = countResult.condensedResinCount;
log.info(`浓缩树脂可刷取次数: ${condensedResinTimes}`);
// 3. 须臾树脂:检查设置是否开启
let transientResinTimes = 0;
if (settings.useTransientResin) {
@@ -517,7 +562,7 @@ this.calCountByResin = async function() {
} else {
log.info(`须臾树脂未开启使用,跳过计算`);
}
// 4. 脆弱树脂:检查设置是否开启
let fragileResinTimes = 0;
if (settings.useFragileResin) {
@@ -526,7 +571,7 @@ this.calCountByResin = async function() {
} else {
log.info(`脆弱树脂未开启使用,跳过计算`);
}
// 计算总次数
count = originalResinTimes + condensedResinTimes + transientResinTimes + fragileResinTimes;
log.info(`总计可刷取次数: ${count}`);

View File

@@ -0,0 +1,252 @@
const commonPath = 'assets/icon/'
const commonMap = new Map([
['main_ui', {
path: `${commonPath}`,
name: 'paimon_menu',
type: '.png',
}],
['yue', {
path: `${commonPath}`,
name: 'yue',
type: '.png',
}],
['200', {
path: `${commonPath}`,
name: '200',
type: '.png',
}],
['add_button', {
path: `${commonPath}`,
name: 'add_button',
type: '.jpg',
}],
])
//====================================================
const genshinJson = {
width: 1920,//genshin.width,
height: 1080,//genshin.height,
}
const MinPhysical = settings.minPhysical?parseInt(settings.minPhysical+''):parseInt(20+'')
const OpenModeCountMin = settings.openModeCountMin
let AlreadyRunsCount=0
let NeedRunsCount=0
const TemplateOrcJson={x: 1568, y: 16, width: 225, height: 60,}
//====================================================
/**
* 根据键值获取JSON路径
* @param {string} key - 要查找的键值
* @returns {any} 返回与键值对应的JSON路径值
*/
function getJsonPath(key) {
return commonMap.get(key); // 通过commonMap的get方法获取指定键对应的值
}
/**
* 从字符串中提取数字并组合成一个整数
* @param {string} str - 包含数字的字符串
* @returns {number} - 由字符串中所有数字组合而成的整数
*/
async function saveOnlyNumber(str) {
// 使用正则表达式匹配字符串中的所有数字
// \d+ 匹配一个或多个数字
// .join('') 将匹配到的数字数组连接成一个字符串
// parseInt 将连接后的字符串转换为整数
return parseInt(str.match(/\d+/g).join(''));
}
/**
* 识别原粹树脂(体力)的函数
* @param {boolean} [opToMainUi=false] - 是否操作到主界面
* @returns {Promise<Object>} 返回一个包含识别结果的Promise对象
* - ok {boolean}: 是否可执行(体力是否足够)
* - min {number}: 最小可执行体力值
* - remainder {number}: 当前剩余体力值
*/
async function ocrPhysical(opToMainUi = false) {
// 检查是否启用体力识别功能,如果未启用则直接返回默认结果
if (!settings.isResinExhaustionMode) {
log.info(`===未启用===`)
return {
ok: true,
min: 0,
remainder: 0,
}
}
log.info(`===开始识别原粹树脂===`)
let ms = 1000 // 定义操作延迟时间(毫秒)
if (opToMainUi) {
await toMainUi(); // 切换到主界面
}
//设置最小可执行体力值
let minPhysical = MinPhysical
//打开地图界面
await keyPress('M')
await sleep(ms)
log.debug(`===[点击+]===`)
//点击+ 按钮 x=1264,y=39,width=18,height=19
let add_buttonJSON = getJsonPath('add_button');
let add_objJson = {
path: `${add_buttonJSON.path}${add_buttonJSON.name}${add_buttonJSON.type}`,
x: 1264,
y: 39,
width: genshinJson.width - 1264,
height: 60,
}
let templateMatchAddButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${add_objJson.path}`), add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height);
let regionA = captureGameRegion()
// let buttonA = captureGameRegion().find(templateMatchAddButtonRo);
let buttonA = region.find(templateMatchAddButtonRo);
regionA.Dispose()
await sleep(ms)
if (!buttonA.isExist()) {
log.error(`未找到${add_objJson.path}请检查路径是否正确`)
throwError(`未找到${add_objJson.path}请检查路径是否正确`)
}
await buttonA.click()
// let add_obj = {
// x: 1264,
// y: 39,
// }
// await click(add_obj.x, add_obj.y)
await sleep(ms)
log.debug(`===[定位原粹树脂]===`)
//定位月亮
let jsonPath = getJsonPath('yue');
let tmJson = {
path: `${jsonPath.path}${jsonPath.name}${jsonPath.type}`,
x: TemplateOrcJson.x,
y: TemplateOrcJson.y,
width: TemplateOrcJson.width,
height: TemplateOrcJson.height,
}
let templateMatchButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${tmJson.path}`), tmJson.x, tmJson.y, tmJson.width, tmJson.height);
let region =captureGameRegion()
// let button = captureGameRegion().find(templateMatchButtonRo);
let button = region.find(templateMatchButtonRo);
region.Dispose()
await sleep(ms)
if (!button.isExist()) {
log.error(`${tmJson.path} 匹配异常`)
throwError(`${tmJson.path} 匹配异常`)
}
log.debug(`===[定位/200]===`)
//定位200
let jsonPath2 = getJsonPath('200');
let tmJson2 = {
path: `${jsonPath2.path}${jsonPath2.name}${jsonPath2.type}`,
x: TemplateOrcJson.x,
y: TemplateOrcJson.y,
width: TemplateOrcJson.width,
height: TemplateOrcJson.height,
}
let templateMatchButtonRo2 = RecognitionObject.TemplateMatch(file.ReadImageMatSync(`${tmJson2.path}`), tmJson2.x, tmJson2.y, tmJson2.width, tmJson2.height);
let region2 = captureGameRegion()
let button2 = region2.find(templateMatchButtonRo2);
region2.Dispose()
await sleep(ms)
if (!button2.isExist()) {
log.error(`${tmJson2.path} 匹配异常`)
throwError(`${tmJson2.path} 匹配异常`)
}
log.debug(`===[识别原粹树脂]===`)
//识别体力 x=1625,y=31,width=79,height=30 / x=1689,y=35,width=15,height=26
let ocr_obj = {
// x: 1623,
x: button.x + button.width-20,
// y: 32,
y: button.y,
// width: 61,
width: Math.abs(button2.x - button.x - button.width+20),
height: button2.height
}
log.debug(`ocr_obj: x={x},y={y},width={width},height={height}`, ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height)
try {
let recognitionObjectOcr = RecognitionObject.Ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height);
let region3 = captureGameRegion()
let res = region3.find(recognitionObjectOcr);
region3.Dispose()
log.info(`[OCR原粹树脂]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`);
let remainder = await saveOnlyNumber(res.text)
let execute = (remainder - minPhysical) >= 0
log.info(`最小可执行原粹树脂:{min},原粹树脂:{key}`, minPhysical, remainder,)
await keyPress('VK_ESCAPE')
return {
ok: execute,
min: minPhysical,
remainder: remainder,
}
} catch (e) {
throwError(`识别失败,err:${e.message}`)
} finally {
if (opToMainUi) {
await toMainUi(); // 切换到主界面
}
}
}
/**
* 抛出错误函数
* 该函数用于显示错误通知并抛出错误对象
* @param {string} msg - 错误信息,将用于通知和错误对象
*/
function throwError(msg) {
// 使用notification组件显示错误通知
// notification.error(`${msg}`);
if (setting.isNotification) {
notification.error(`${msg}`);
}
// 抛出一个包含错误信息的Error对象
throw new Error(`${msg}`);
}
// 判断是否在主界面的函数
const isInMainUI = () => {
// let name = '主界面'
let main_ui = getJsonPath('main_ui');
// 定义识别对象
let paimonMenuRo = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(`${main_ui.path}${main_ui.name}${main_ui.type}`),
0,
0,
genshinJson.width / 3.0,
genshinJson.width / 5.0
);
let captureRegion = captureGameRegion();
let res = captureRegion.find(paimonMenuRo);
captureRegion.Dispose()
return !res.isEmpty();
};
async function toMainUi() {
let ms = 300
let index = 1
await sleep(ms);
while (!isInMainUI()) {
await sleep(ms);
await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面
await sleep(ms);
if (index > 3) {
throwError(`多次尝试返回主界面失败`);
}
index += 1
}
}
this.physical = {
ocrPhysical,
MinPhysical,
OpenModeCountMin,
AlreadyRunsCount,
NeedRunsCount,
}