[AutoLeyLineOutcrop]4.4版本更新,新增树脂耗尽模式 (#2178)

- 更新settings.json中树脂耗尽项的name
- 完善树脂耗尽模式,执行结束后再次检查树脂情况,有限次递归调用确保树脂耗尽(常规情况下不会执行到递归逻辑)
- 引入树脂统计模块,能够统计可供消耗的树脂
- 修复用书找地脉花检测大地图图像识别失败的问题
This commit is contained in:
Colin Xu
2025-10-19 20:30:24 +08:00
committed by GitHub
parent 61588f5e6e
commit 8c5c03a163
26 changed files with 715 additions and 18 deletions

View File

@@ -27,8 +27,9 @@
- **全自动运行**:配置好之后,只需要点击运行,就可以全部打完,无需任何其他操作。
- **自动寻找地脉花**:通过大地图定位地脉花位置的方式来寻找地脉花,并执行后续的路线。
- **自动开启地脉花奖励,优先使用浓缩树脂**:领取奖励时优先选择浓缩树脂,当存在双倍奖励活动(地脉移涌或星之归还)时会优先选择双倍奖励。
- **树脂耗尽模式**根据设置统计当前的树脂情况计算出需要运行的次数运行结束后再次判断若结束后统计到未消耗的树脂则继续执行最多3次
- **树脂不足时自动停止**:如果领取奖励时树脂不够了,脚本会直接停止,可以配置是否使用须臾/脆弱。
- **好感队领取奖励**:领取奖励前切换到好感队,还可以领取到好感度。
- **树脂不足时自动停止**:如果领取奖励时树脂不够了,脚本会直接停止,不会使用脆弱树脂。
## 常见问题
### 一运行就报错退出了
根据错误提示自行排查原因
@@ -48,6 +49,8 @@
打开大地图,点击左下角设置,开启自定义标记。
脚本非正常结束运行时会出现该问题。
## 更新日志
### 4.4
- 新增树脂耗尽模式
### 4.3
- 适配新的奖励领取界面
- 修复挪德卡莱识别错误及通过冒险之证寻找地脉花总会选择挪德卡莱的bug

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View File

@@ -13,6 +13,8 @@ let marksStatus = true; // 自定义标记状态
let currentRunTimes = 0; // 当前运行次数
let isNotification = false; // 是否发送通知
let config = {}; // 全局配置对象
let recheckCount = 0; // 树脂重新检查次数(防止无限递归)
const MAX_RECHECK_COUNT = 3; // 最大重新检查次数
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 }; // 拾取区域
@@ -64,10 +66,22 @@ const ocrRoThis = RecognitionObject.ocrThis;
async function runLeyLineOutcropScript() {
// 初始化加载配置和设置并校验
initialize();
// 处理树脂耗尽模式(如果开启)
let runTimesValue = await handleResinExhaustionMode();
if(runTimesValue <= 0) {
throw new Error("树脂耗尽,脚本将结束运行");
}
await prepareForLeyLineRun();
// 执行地脉花挑战
await runLeyLineChallenges();
// 如果是树脂耗尽模式,执行完毕后再次检查是否还有树脂
if (settings.isResinExhaustionMode) {
await recheckResinAndContinue();
}
}
/**
@@ -85,7 +99,8 @@ function initialize() {
"findLeyLineOutcropByBook.js",
"loadSettings.js",
"processLeyLineOutcrop.js",
"recognizeTextInRegion.js"
"recognizeTextInRegion.js",
"calCountByResin.js"
];
for (const fileName of utils) {
eval(file.readTextSync(`utils/${fileName}`));
@@ -102,6 +117,160 @@ function initialize() {
}
}
/**
* 处理树脂耗尽模式
* 如果开启了树脂耗尽模式,则统计可刷取次数并替换设置中的刷取次数
* 如果统计失败,则使用设置中的刷取次数
* @returns {Promise<number>} 返回可刷取次数
*/
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脚本将不会执行任何刷取操作");
if (isNotification) {
notification.send("树脂耗尽模式统计到的可刷取次数为0脚本将结束运行");
}
}
// 使用统计到的次数替换设置中的刷取次数
settings.timesValue = resinResult.count;
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 (isNotification) {
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`;
notification.send(notificationText);
}
return settings.timesValue;
} catch (error) {
// 统计失败,使用设置中的刷取次数
log.error(`树脂统计失败: ${error.message}`);
log.warn(`将使用设置中的刷取次数: ${settings.timesValue}`);
if (isNotification) {
notification.send(`树脂耗尽模式:统计失败,将使用设置中的刷取次数 ${settings.timesValue}\n错误信息: ${error.message}`);
}
return settings.timesValue;
}
}
/**
* 树脂耗尽模式结束后再次检查树脂并继续执行
* @returns {Promise<void>}
*/
async function recheckResinAndContinue() {
// 递归深度检查,防止无限循环
recheckCount++;
if (recheckCount > MAX_RECHECK_COUNT) {
log.warn(`已达到最大重新检查次数限制 (${MAX_RECHECK_COUNT} 次),停止继续检查`);
if (isNotification) {
notification.send(`树脂耗尽模式:已达到最大检查次数 ${MAX_RECHECK_COUNT},脚本结束`);
}
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}),为安全起见停止运行`);
if (isNotification) {
notification.send(`树脂耗尽模式:检测到异常次数 ${resinResult.count},已停止运行`);
}
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 {
// 正常结束情况
if (recheckCount === 1) {
log.info("树脂已完全耗尽,脚本正常执行完毕");
if (isNotification) {
notification.send(`树脂耗尽模式:树脂已完全耗尽,脚本正常执行完毕`);
}
} else {
// 异常重试情况
log.info("树脂已完全耗尽,脚本执行完毕");
log.info(`(本次运行触发了 ${recheckCount - 1} 次额外的树脂检查和执行)`);
if (isNotification) {
notification.send(`树脂耗尽模式:树脂已完全耗尽,脚本执行完毕(触发了 ${recheckCount - 1} 次额外执行)`);
}
}
}
} catch (error) {
log.error(`重新检查树脂时出错: ${error.message}`);
if (isNotification) {
notification.error(`重新检查树脂时出错: ${error.message}`);
}
// 出错时也要停止递归
return;
}
}
/**
* 执行地脉花挑战前的准备工作

View File

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

View File

@@ -73,9 +73,9 @@
"label": "通过BGI通知系统发送详细通知"
},
{
"name": "isRecognizeResin",
"name": "isResinExhaustionMode",
"type": "checkbox",
"label": "树脂耗尽模式\n开启后脚本会在树脂耗尽时停止运行【开发中】"
"label": "树脂耗尽模式\n开启后会自动统计所有可用树脂并计算可刷取次数,替换上方设置的刷取次数\n注意请同时设置树脂刷取次数将在统计失败时使用"
},
{
"name": "isGoToSynthesizer",

View File

@@ -431,7 +431,7 @@ async function trySwitch20To40Resin() {
currentCaptureRegion = captureGameRegion();
// 检测切换按钮
switchButtonIcon = file.ReadImageMatSync("RecognitionObject/switch_button.png");
switchButtonIcon = file.ReadImageMatSync("assets/icon/switch_button.png");
switchButtonRo = RecognitionObject.TemplateMatch(switchButtonIcon);
switchButtonRo.threshold = 0.7; // 设置合适的阈值

View File

@@ -0,0 +1,529 @@
/**
* OCR树脂数量统计脚本
* 功能:自动识别并统计原神中各种树脂的数量
* 支持:原粹树脂、浓缩树脂、须臾树脂、脆弱树脂
*/
// ==================== 常量定义 ====================
// 树脂图标识别对象
const RESIN_ICONS = {
ORIGINAL: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/original_resin.png")),
CONDENSED: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/condensed_resin.png")),
FRAGILE: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/fragile_resin.png")),
TRANSIENT: RecognitionObject.TemplateMatch(file.ReadImageMatSync("RecognitionObject/transient_resin.png")),
REPLENISH_BUTTON: RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/icon/replenish_resin_button.png"))
};
// 普通数字识别对象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 }
];
// 白色数字识别对象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 }
];
// 配置常量
const CONFIG = {
RECOGNITION_TIMEOUT: 2000, // 图像识别超时时间(毫秒)
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 } // 避免选中效果的点击位置
},
// OCR识别区域配置
OCR_REGIONS: {
ORIGINAL_RESIN: { width: 200, height: 40 },
CONDENSED_RESIN: { width: 90, height: 40 },
OTHER_RESIN: { width: 0, height: 60 } // width会根据图标宽度动态设置
}
};
// 树脂数量存储
let resinCounts = {
original: 0, // 原粹树脂数量
condensed: 0, // 浓缩树脂数量
transient: 0, // 须臾树脂数量
fragile: 0 // 脆弱树脂数量
};
// ==================== 工具函数 ====================
/**
* 通用图像识别函数
* @param {Object} recognitionObject - 识别对象
* @param {number} timeout - 超时时间(毫秒)
* @returns {Object|null} 识别结果或null
*/
async function recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TIMEOUT) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// 直接链式调用不保存gameRegion变量避免内存管理问题
const imageResult = captureGameRegion().find(recognitionObject);
if (imageResult.isExist()) {
return imageResult;
}
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
}
await sleep(CONFIG.SLEEP_INTERVAL);
}
log.warn(`经过多次尝试,仍然无法识别图像`);
return null;
}
/**
* 在指定区域内识别数字图片
* @param {Object} ocrRegion - OCR识别区域
* @param {Array} numberIcons - 数字图标数组
* @param {string} logPrefix - 日志前缀
* @returns {number|null} 识别到的数字或null
*/
async function recognizeNumberInRegion(ocrRegion, numberIcons, logPrefix = "") {
try {
for (const numObj of numberIcons) {
try {
// 直接链式调用,避免内存管理问题
const numResult = captureGameRegion().find(numObj.ro);
if (numResult && isPointInRegion(numResult, ocrRegion)) {
log.info(`${logPrefix}通过图片识别到数字: ${numObj.value}`);
return numObj.value;
}
} catch (error) {
log.error(`${logPrefix}识别数字图片时发生异常: ${error.message}`);
}
}
} catch (error) {
log.error(`${logPrefix}识别数字区域时发生异常: ${error.message}`);
}
return null;
}
/**
* 普通数字图片识别函数
* @param {Object} ocrRegion - OCR识别区域
* @returns {number|null} 识别到的数字或null
*/
async function recognizeNumberByImage(ocrRegion) {
return await recognizeNumberInRegion(ocrRegion, NUMBER_ICONS, "普通数字 - ");
}
/**
* 白色数字图片识别函数(用于浓缩树脂)
* @param {Object} ocrRegion - OCR识别区域
* @returns {number|null} 识别到的数字或null
*/
async function recognizeWhiteNumberByImage(ocrRegion) {
return await recognizeNumberInRegion(ocrRegion, WHITE_NUMBER_ICONS, "白色数字 - ");
}
/**
* 检查点是否在指定区域内
* @param {Object} point - 点坐标 {x, y}
* @param {Object} region - 区域 {x, y, width, height}
* @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;
}
/**
* 通过OCR识别数字
* @param {Object} ocrRegion - OCR识别区域
* @param {RegExp} pattern - 匹配模式
* @returns {number|null} 识别到的数字或null
*/
async function recognizeNumberByOCR(ocrRegion, pattern) {
try {
// 直接链式调用,避免内存管理问题
const ocrRo = RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
const resList = captureGameRegion().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]);
if (!isNaN(number)) {
return number;
}
}
}
} catch (error) {
log.error(`OCR识别时发生异常: ${error.message}`);
}
return null;
}
// ==================== 树脂计数函数 ====================
/**
* 统计原粹树脂数量
* @returns {number} 原粹树脂数量
*/
async function countOriginalResin() {
const originalResin = await recognizeImage(RESIN_ICONS.ORIGINAL);
if (!originalResin) {
log.warn(`未找到原粹树脂图标`);
return 0;
}
const ocrRegion = {
x: originalResin.x,
y: originalResin.y,
width: CONFIG.OCR_REGIONS.ORIGINAL_RESIN.width,
height: CONFIG.OCR_REGIONS.ORIGINAL_RESIN.height
};
// 匹配 xxx/200 格式中的第一个数字1-3位
const count = await recognizeNumberByOCR(ocrRegion, /(\d{1,3})\/\d+/);
if (count !== null) {
log.info(`原粹树脂数量: ${count}`);
return count;
}
log.warn(`未能识别原粹树脂数量`);
return 0;
}
/**
* 统计浓缩树脂数量
* @returns {number} 浓缩树脂数量
*/
async function countCondensedResin() {
const condensedResin = await recognizeImage(RESIN_ICONS.CONDENSED);
if (!condensedResin) {
log.warn(`未找到浓缩树脂图标`);
return 0;
}
const ocrRegion = {
x: condensedResin.x,
y: condensedResin.y,
width: CONFIG.OCR_REGIONS.CONDENSED_RESIN.width,
height: CONFIG.OCR_REGIONS.CONDENSED_RESIN.height
};
// 首先尝试OCR识别
const ocrCount = await recognizeNumberByOCR(ocrRegion, /\d+/);
if (ocrCount !== null) {
log.info(`浓缩树脂数量: ${ocrCount}`);
return ocrCount;
}
// OCR识别失败尝试白色数字图片识别
log.info(`OCR识别浓缩树脂失败尝试白色数字图片识别`);
const imageCount = await recognizeWhiteNumberByImage(ocrRegion);
if (imageCount !== null) {
log.info(`浓缩树脂数量(白色数字图片识别): ${imageCount}`);
return imageCount;
}
log.info(`白色数字图片识别识别浓缩树脂失败,尝试在说明界面获取`);
// 点击浓缩树脂打开说明界面统计
condensedResin.click();
await sleep(CONFIG.UI_DELAY);
let captureRegion = captureGameRegion();
// OCR识别整个界面的文本
let ocrRo = RecognitionObject.Ocr(0, 0, captureRegion.width, captureRegion.height);
let textList = captureRegion.findMulti(ocrRo);
captureRegion.dispose();
for (const res of textList) {
if (res.text.includes("当前拥有")) {
const match = res.text.match(/当前拥有\s*([0-5ss])/);
if (match && match[1]) {
const count = parseInt(match[1]);
log.info(`浓缩树脂数量(说明界面): ${count}`);
keyPress("ESCAPE");
await sleep(CONFIG.UI_DELAY);
return count;
}
}
}
log.warn(`未能识别浓缩树脂数量`);
return 0;
}
/**
* 统计须臾树脂数量
* @returns {number} 须臾树脂数量
*/
async function countTransientResin() {
const transientResin = await recognizeImage(RESIN_ICONS.TRANSIENT);
if (!transientResin) {
log.warn(`未找到须臾树脂图标`);
return 0;
}
const ocrRegion = {
x: transientResin.x,
y: transientResin.y + transientResin.height,
width: transientResin.width,
height: CONFIG.OCR_REGIONS.OTHER_RESIN.height
};
return await countResinWithFallback(ocrRegion, "须臾树脂", recognizeNumberByImage);
}
/**
* 统计脆弱树脂数量
* @returns {number} 脆弱树脂数量
*/
async function countFragileResin() {
const fragileResin = await recognizeImage(RESIN_ICONS.FRAGILE);
if (!fragileResin) {
log.warn(`未找到脆弱树脂图标`);
return 0;
}
const ocrRegion = {
x: fragileResin.x,
y: fragileResin.y + fragileResin.height,
width: fragileResin.width,
height: CONFIG.OCR_REGIONS.OTHER_RESIN.height
};
return await countResinWithFallback(ocrRegion, "脆弱树脂", recognizeNumberByImage);
}
/**
* 通用树脂计数函数(带图片识别回退)
* @param {Object} ocrRegion - OCR识别区域
* @param {string} resinType - 树脂类型名称
* @param {Function} fallbackFunction - 回退识别函数
* @returns {number} 树脂数量
*/
async function countResinWithFallback(ocrRegion, resinType, fallbackFunction) {
// 首先尝试OCR识别
const ocrCount = await recognizeNumberByOCR(ocrRegion, /\d+/);
if (ocrCount !== null) {
log.info(`${resinType}数量: ${ocrCount}`);
return ocrCount;
}
// OCR识别失败尝试图片识别
log.info(`OCR识别${resinType}失败,尝试图片识别`);
const imageCount = await fallbackFunction(ocrRegion);
if (imageCount !== null) {
log.info(`${resinType}数量(图片识别): ${imageCount}`);
return imageCount;
}
log.warn(`未能识别${resinType}数量`);
return 0;
}
// ==================== UI操作函数 ====================
/**
* 打开并设置地图界面
*/
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.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y);
await sleep(CONFIG.UI_DELAY);
// 设置地图缩放级别,排除识别干扰
await genshin.setBigMapZoomLevel(CONFIG.MAP_ZOOM_LEVEL);
log.info("地图界面设置完成");
}
/**
* 打开补充树脂界面
*/
async function openReplenishResinUi() {
log.info("尝试打开补充树脂界面");
const replenishResinButton = await recognizeImage(RESIN_ICONS.REPLENISH_BUTTON);
if (replenishResinButton) {
replenishResinButton.Click();
log.info("成功打开补充树脂界面");
} else {
log.warn("未找到补充树脂按钮");
}
}
/**
* 显示统计结果并发送通知
* @param {Object} results - 统计结果对象
*/
function displayResults(results) {
const resultText = `原粹:${results.original} 浓缩:${results.condensed} 须臾:${results.transient} 脆弱:${results.fragile}`;
log.info(`============ 树脂统计结果 ============`);
log.info(`原粹树脂数量: ${results.original}`);
log.info(`浓缩树脂数量: ${results.condensed}`);
log.info(`须臾树脂数量: ${results.transient}`);
log.info(`脆弱树脂数量: ${results.fragile}`);
log.info(`====================================`);
}
// ==================== 主要功能函数 ====================
/**
* 统计所有树脂数量的主函数
* @returns {Object} 包含所有树脂数量的对象
*/
this.countAllResin = async function() {
try {
setGameMetrics(1920, 1080, 1);
log.info("开始统计树脂数量");
// 返回主界面
await genshin.returnMainUi();
await sleep(CONFIG.UI_DELAY);
// 打开地图界面统计原粹/浓缩树脂
await openMap();
await sleep(CONFIG.UI_DELAY);
log.info("开始统计地图界面中的树脂");
resinCounts.original = await countOriginalResin();
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,
condensedResinCount: resinCounts.condensed,
transientResinCount: resinCounts.transient,
fragileResinCount: resinCounts.fragile
};
} catch (error) {
log.error(`统计树脂数量时发生异常: ${error.message}`);
throw error;
}
}
/**
* @returns {Object} 包含可刷取次数的对象
* {
* count: number,
* originalResinTimes: number,
* condensedResinTimes: number,
* transientResinTimes: number,
* fragileResinTimes: number
* }
*/
this.calCountByResin = async function() {
try {
let countResult = await this.countAllResin();
let count = 0;
// 计算可刷取次数
// 1. 原粹树脂优先消耗40/次不满40消耗20/次不满20不消耗
let originalResinTimes = 0;
let remainingOriginalResin = countResult.originalResinCount;
// 先计算40树脂的次数
if (remainingOriginalResin >= 40) {
const times40 = Math.floor(remainingOriginalResin / 40);
originalResinTimes += times40;
remainingOriginalResin = remainingOriginalResin - (times40 * 40);
}
// 再计算20树脂的次数
if (remainingOriginalResin >= 20) {
const times20 = Math.floor(remainingOriginalResin / 20);
originalResinTimes += times20;
remainingOriginalResin = remainingOriginalResin - (times20 * 20);
}
log.info(`原粹树脂可刷取次数: ${originalResinTimes}`);
// 2. 浓缩树脂每个计算为1次
let condensedResinTimes = countResult.condensedResinCount;
log.info(`浓缩树脂可刷取次数: ${condensedResinTimes}`);
// 3. 须臾树脂:检查设置是否开启
let transientResinTimes = 0;
if (settings.useTransientResin) {
transientResinTimes = countResult.transientResinCount;
log.info(`须臾树脂可刷取次数: ${transientResinTimes}`);
} else {
log.info(`须臾树脂未开启使用,跳过计算`);
}
// 4. 脆弱树脂:检查设置是否开启
let fragileResinTimes = 0;
if (settings.useFragileResin) {
fragileResinTimes = countResult.fragileResinCount;
log.info(`脆弱树脂可刷取次数: ${fragileResinTimes}`);
} else {
log.info(`脆弱树脂未开启使用,跳过计算`);
}
// 计算总次数
count = originalResinTimes + condensedResinTimes + transientResinTimes + fragileResinTimes;
log.info(`总计可刷取次数: ${count}`);
let result = {
count: count,
originalResinTimes: originalResinTimes,
condensedResinTimes: condensedResinTimes,
transientResinTimes: transientResinTimes,
fragileResinTimes: fragileResinTimes
}
return result;
} catch (error) {
log.error(`统计树脂数量时发生异常: ${error.message}`);
throw error;
}
}

View File

@@ -79,15 +79,8 @@ this.findLeyLineOutcropByBook = async function (country, type) {
this.checkBigMapOpened = async function() {
let captureRegion = captureGameRegion();
try {
// 只在左下角十六分之一区域查找齿轮图标(提高识别效率和准确性)
const searchWidth = genshin.width / 4;
const searchHeight = genshin.height / 4;
const searchX = 0;
const searchY = genshin.height - searchHeight;
let croppedRegion = captureRegion.DeriveCrop(searchX, searchY, searchWidth, searchHeight);
let result = croppedRegion.Find(mapSettingButtonRo);
return !result.isEmpty();
const imageResult = captureGameRegion().find(mapSettingButtonRo);
return imageResult.isExist();
} finally {
captureRegion.dispose();
}

View File

@@ -58,8 +58,11 @@ function () {
if (settings.friendshipTeam) {
log.info(`好感队:${settings.friendshipTeam}`);
}
log.info(`刷取次数:${settings.timesValue}`);
if (settings.isResinExhaustionMode) {
log.warn("树脂耗尽模式已开启,若统计成功将覆盖设置的刷取次数");
} else {
log.info(`刷取次数:${settings.timesValue}`);
}
// 设置通知状态
isNotification = settings.isNotification;