Files
bettergi-scripts-list/repo/js/AutoLeyLineOutcrop/utils/calCountByResin.js
云端客 697b044600 fix(AutoLeyLineOutcrop): 修复OCR物理识别和区域匹配问题 (#2708)
- 添加调试日志输出ocrPhysical数据
- 移除ocrPhysical.ok验证条件以提高兼容性
- 使用可选链操作符安全访问remainder属性
- 更新版本号从4.5到4.5.1
- 调整添加按钮识别区域坐标和尺寸参数
2026-01-17 21:00:53 +08:00

602 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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) {
let resList = null;
let captureRegion = null;
try {
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]);
if (!isNaN(number)) {
return number;
}
}
}
return null;
} catch (error) {
log.error(`OCR识别时发生异常: ${error.message}`);
return null;
} finally {
if (resList && typeof resList.dispose === 'function') {
resList.dispose();
}
if (captureRegion && typeof captureRegion.dispose === 'function') {
captureRegion.dispose();
}
}
}
// ==================== 树脂计数函数 ====================
/**
* 统计原粹树脂数量
* @returns {number} 原粹树脂数量
*/
/**
* 统计原粹树脂数量
* @returns {number} 原粹树脂数量
*/
async function countOriginalResin(tryOriginalMode, opToMainUi, openMap) {
if (tryOriginalMode) {
log.info("尝试使用原始模式");
return await countOriginalResinBackup()
} else {
log.info('尝试使用优化模式');
let ocrPhysical = await physical.ocrPhysical(opToMainUi, openMap);
log.debug(`ocrPhysical: {0}`,JSON.stringify(ocrPhysical))
await sleep(600)
// ocrPhysical = false//模拟异常
if (ocrPhysical/* && ocrPhysical.ok*/) {
return ocrPhysical?.remainder;
} else {
//异常 退出至地图 尝试使用原始模式
await keyPress("VK_ESCAPE")
log.error(`ocrPhysical error`);
throw new Error("ocrPhysical error");
}
}
}
async function countOriginalResinBackup() {
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();
let textList = null;
try {
// 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])/);
if (match && match[1]) {
const count = parseInt(match[1]);
log.info(`浓缩树脂数量(说明界面): ${count}`);
keyPress("ESCAPE");
await sleep(CONFIG.UI_DELAY);
return count;
}
}
}
} finally {
if (textList && typeof textList.dispose === 'function') {
textList.dispose();
}
captureRegion.dispose();
}
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 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);
}
/**
* 打开补充树脂界面
*/
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);
let tryPass = true;
try {
log.info("[开始]统计补充树脂界面中的树脂");
resinCounts.original = await countOriginalResin(false, false);
moveMouseTo(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y)
await sleep(500);
resinCounts.transient = await countTransientResin();
resinCounts.fragile = await countFragileResin();
log.info("[完成]统计补充树脂界面中的树脂");
// 点击避免选中效果影响统计
click(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y);
} catch (e) {
tryPass = false
}
await sleep(CONFIG.UI_DELAY);
log.info("开始统计地图界面中的树脂");
if (!tryPass) {
// 如果第一次尝试失败,则切换到蒙德
await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y)
resinCounts.original = await countOriginalResin(!tryPass);
}
resinCounts.condensed = await countCondensedResin();
if (!tryPass) {
// 打开补充树脂界面统计须臾/脆弱树脂
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;
}
}