mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-06-03 00:45:57 +08:00
自动体力计划 重构+适配 (#3235)
This commit is contained in:
@@ -310,6 +310,15 @@
|
||||
## 版本历史(简要)
|
||||
|
||||
---
|
||||
### 0.0.7 2026.05.27
|
||||
|
||||
- 重构配置模块:使用新的 UID 获取方法 `genshin.uid()` 替代 OCR 识别
|
||||
- 移除对 `ocrUid`、`getDayOfWeek`、`parseInteger`、`pullJsonConfig` 和 `findStygianOnslaught` 的导入
|
||||
- 添加对 `toMainUi` 的导入,在初始化时调用确保回到主界面
|
||||
- 更新最低 BetterGI 版本要求从 0.58.0 到 0.61.0
|
||||
- 删除 utils/uid.js 文件中的 `ocrUid` 函数实现
|
||||
- 优化 UID 获取逻辑,提升稳定性和准确性
|
||||
|
||||
### 0.0.6 2026.04.23
|
||||
|
||||
- 限制单例模式配置
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ocrUid} from "../utils/uid";
|
||||
|
||||
let config = {
|
||||
import {toMainUi} from "../utils/tool";
|
||||
/*===========================================[config]===========================================*/
|
||||
export let config = {
|
||||
//setting设置放在这个json
|
||||
run: {
|
||||
exclude_run_exception: false,//忽略运行异常
|
||||
@@ -58,7 +58,8 @@ let config = {
|
||||
domainItemsMap: new Map(),
|
||||
}
|
||||
|
||||
const LoadType = Object.freeze({
|
||||
/*===========================================[enum]===========================================*/
|
||||
export const LoadType = Object.freeze({
|
||||
uid: 'uid',//uid加载
|
||||
input: 'input',//input加载
|
||||
bgi_tools: 'bgi_tools',//bgi_tools加载
|
||||
@@ -66,11 +67,70 @@ const LoadType = Object.freeze({
|
||||
return Object.keys(this).find(key => this[key] === value);
|
||||
}
|
||||
})
|
||||
const LoadMap = new Map([
|
||||
export const LoadMap = new Map([
|
||||
['UID加载', LoadType.uid],
|
||||
['输入加载', LoadType.input],
|
||||
['bgi_tools加载', LoadType.bgi_tools],
|
||||
])
|
||||
|
||||
/*===========================================[tool]===========================================*/
|
||||
/**
|
||||
* 获取多复选框的映射表
|
||||
* 该函数会从初始化的设置中提取所有类型为"multi-checkbox"的条目,
|
||||
* 并将这些条目的名称和对应的选项值存储在一个Map对象中返回
|
||||
* @returns {Promise<Map>} 返回一个Promise对象,解析为包含多复选框配置的Map
|
||||
*/
|
||||
export async function getMultiCheckboxMap() {
|
||||
// 如果configSettings存在则使用它,否则调用initSettings()函数获取
|
||||
const settingsJson = config.info.settings ? config.info.settings : await initSettings();
|
||||
// 创建一个新的Map对象用于存储多复选框的配置
|
||||
// Map结构为: {名称: 选项数组}
|
||||
let multiCheckboxMap = new Map();
|
||||
// 遍历设置JSON中的每个条目
|
||||
settingsJson.forEach((entry) => {
|
||||
// 如果条目没有name属性或者类型不是"multi-checkbox",则跳过该条目
|
||||
if (!entry.name || entry.type !== "multi-checkbox") return;
|
||||
// 解构条目中的name和label属性,便于后续使用
|
||||
const {name, label} = entry;
|
||||
// 获取当前name对应的设置值,如果存在则转换为数组,否则使用空数组
|
||||
const options = settings[name] ? Array.from(settings[name]) : [];
|
||||
// 记录调试信息,包含名称、标签、选项和选项数量
|
||||
log.debug("name={key1},label={key2},options={key3},length={key4}", name, label, JSON.stringify(options), options.length);
|
||||
// 将名称和对应的选项数组存入Map
|
||||
multiCheckboxMap.set(name, options);
|
||||
})
|
||||
// 返回包含多复选框配置的Map
|
||||
return multiCheckboxMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据复选框组名称获取对应的值
|
||||
* 这是一个异步函数,用于从复选框映射中获取指定名称的值
|
||||
* @param {string} name - 复选框组的名称
|
||||
* @returns {Promise<any>} 返回一个Promise,解析为复选框组对应的值
|
||||
*/
|
||||
export async function getValueByMultiCheckboxName(name) {
|
||||
// 获取复选框映射表,这是一个异步操作
|
||||
let multiCheckboxMap = await getMultiCheckboxMap()
|
||||
// log.debug("multiCheckboxMap={key}", JSON.stringify(multiCheckboxMap))
|
||||
// multiCheckboxMap.entries().forEach(([name, options]) => {
|
||||
// log.debug("name={key1},options={key2}", name, JSON.stringify(options))
|
||||
// })
|
||||
// 从映射表中获取并返回指定名称对应的值
|
||||
let values = multiCheckboxMap.get(name);
|
||||
log.debug("values={key}", JSON.stringify(values))
|
||||
return values
|
||||
}
|
||||
/*===========================================[check]===========================================*/
|
||||
/**
|
||||
* 检查密钥是否正确
|
||||
*/
|
||||
export async function checkKey(key = "") {
|
||||
if (config?.info?.manifest?.key !== key?.trim()) {
|
||||
throw new Error("密钥错误");
|
||||
}
|
||||
}
|
||||
/*===========================================[init]===========================================*/
|
||||
/**
|
||||
* 构建初始化配置设置函数
|
||||
* 该函数用于创建并返回一个包含各种配置项的对象
|
||||
@@ -141,7 +201,7 @@ export async function buildInitConfigSettings(){
|
||||
* 从配置文件中读取设置信息并返回
|
||||
* @returns {Object} 返回解析后的JSON设置对象
|
||||
*/
|
||||
async function initSettings() {
|
||||
export async function initSettings() {
|
||||
// 默认设置文件路径
|
||||
let settings_ui = "settings.json";
|
||||
try {
|
||||
@@ -173,77 +233,20 @@ async function initSettings() {
|
||||
// 返回设置对象
|
||||
return settingsJson
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多复选框的映射表
|
||||
* 该函数会从初始化的设置中提取所有类型为"multi-checkbox"的条目,
|
||||
* 并将这些条目的名称和对应的选项值存储在一个Map对象中返回
|
||||
* @returns {Promise<Map>} 返回一个Promise对象,解析为包含多复选框配置的Map
|
||||
*/
|
||||
async function getMultiCheckboxMap() {
|
||||
// 如果configSettings存在则使用它,否则调用initSettings()函数获取
|
||||
const settingsJson = config.info.settings ? config.info.settings : await initSettings();
|
||||
// 创建一个新的Map对象用于存储多复选框的配置
|
||||
// Map结构为: {名称: 选项数组}
|
||||
let multiCheckboxMap = new Map();
|
||||
// 遍历设置JSON中的每个条目
|
||||
settingsJson.forEach((entry) => {
|
||||
// 如果条目没有name属性或者类型不是"multi-checkbox",则跳过该条目
|
||||
if (!entry.name || entry.type !== "multi-checkbox") return;
|
||||
// 解构条目中的name和label属性,便于后续使用
|
||||
const {name, label} = entry;
|
||||
// 获取当前name对应的设置值,如果存在则转换为数组,否则使用空数组
|
||||
const options = settings[name] ? Array.from(settings[name]) : [];
|
||||
// 记录调试信息,包含名称、标签、选项和选项数量
|
||||
log.debug("name={key1},label={key2},options={key3},length={key4}", name, label, JSON.stringify(options), options.length);
|
||||
// 将名称和对应的选项数组存入Map
|
||||
multiCheckboxMap.set(name, options);
|
||||
})
|
||||
// 返回包含多复选框配置的Map
|
||||
return multiCheckboxMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据复选框组名称获取对应的值
|
||||
* 这是一个异步函数,用于从复选框映射中获取指定名称的值
|
||||
* @param {string} name - 复选框组的名称
|
||||
* @returns {Promise<any>} 返回一个Promise,解析为复选框组对应的值
|
||||
*/
|
||||
async function getValueByMultiCheckboxName(name) {
|
||||
// 获取复选框映射表,这是一个异步操作
|
||||
let multiCheckboxMap = await getMultiCheckboxMap()
|
||||
// log.debug("multiCheckboxMap={key}", JSON.stringify(multiCheckboxMap))
|
||||
// multiCheckboxMap.entries().forEach(([name, options]) => {
|
||||
// log.debug("name={key1},options={key2}", name, JSON.stringify(options))
|
||||
// })
|
||||
// 从映射表中获取并返回指定名称对应的值
|
||||
let values = multiCheckboxMap.get(name);
|
||||
log.debug("values={key}", JSON.stringify(values))
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查密钥是否正确
|
||||
*/
|
||||
async function checkKey(key = "") {
|
||||
if (config?.info?.manifest?.key !== key?.trim()) {
|
||||
throw new Error("密钥错误");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化秘境配置
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initConfig() {
|
||||
export async function initConfig() {
|
||||
config.info.key = settings.key || config.info.key
|
||||
await checkKey(config.info.key)
|
||||
// //流程->返回主页 打开地图 返回主页
|
||||
// const physical = await ocrPhysical(true, true)
|
||||
// config.user.physical.current = physical.current
|
||||
// config.user.physical.min = physical.min
|
||||
await toMainUi()
|
||||
// 初始化uid
|
||||
config.user.uid = await ocrUid()
|
||||
config.user.uid = await genshin.uid()
|
||||
// config.run.retry_count = (settings.retry_count ? parseInt(settings.retry_count) : config.run.retry_count)
|
||||
|
||||
const retryCount = Number.parseInt(String(settings.retry_count ?? ""), 10);
|
||||
@@ -335,12 +338,11 @@ async function initConfig() {
|
||||
config.run.loads = loads
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
config, LoadType, LoadMap,
|
||||
checkKey,
|
||||
initSettings,
|
||||
getMultiCheckboxMap,
|
||||
getValueByMultiCheckboxName,
|
||||
initConfig
|
||||
}
|
||||
// export {
|
||||
// config, LoadType, LoadMap,
|
||||
// checkKey,
|
||||
// initSettings,
|
||||
// getMultiCheckboxMap,
|
||||
// getValueByMultiCheckboxName,
|
||||
// initConfig
|
||||
// }
|
||||
@@ -141,6 +141,15 @@
|
||||
]
|
||||
}
|
||||
,
|
||||
{
|
||||
"name": "山风的荆冕",
|
||||
"type": "圣遗物",
|
||||
"hasOrder": false,
|
||||
"list": [
|
||||
"天之美赐",
|
||||
"影中沉凝的幻灭"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "月童的库藏",
|
||||
"type": "圣遗物",
|
||||
|
||||
@@ -1,652 +1,17 @@
|
||||
import {buildInitConfigSettings, config, initConfig, initSettings, LoadType} from './config/config';
|
||||
import {ocrUid} from './utils/uid';
|
||||
import {getDayOfWeek, outDomainUI, outStygianOnslaughtUI, throwError,toMainUi} from './utils/tool';
|
||||
import {
|
||||
buildInitConfigSettings,
|
||||
config,
|
||||
initConfig,
|
||||
initSettings,
|
||||
} from './config/config';
|
||||
import {outDomainUI} from './utils/tool';
|
||||
import {pullJsonConfig, pushAllCountryConfig, pushAllJsonConfig} from './utils/bgi_tools';
|
||||
import {countOriginalResin, ocrPhysical,countAllResin} from "./utils/physical";
|
||||
import {findStygianOnslaught} from "./utils/activity";
|
||||
|
||||
/**
|
||||
* 自动执行秘境任务的异步函数
|
||||
* @param {Object} autoFight - 包含秘境自动配置参数的对象
|
||||
* @returns {Promise<void>} - 执行完成后返回的Promise
|
||||
*/
|
||||
async function autoDomain(autoFight) {
|
||||
log.info(`{0}`, "开始执行秘境任务")
|
||||
log.warn(`{0}`, "非体力耗尽情况下(受本体限制),等待退出秘境时间较长")
|
||||
// 创建秘境参数对象,初始化值为0
|
||||
let domainParam = new AutoDomainParam();
|
||||
//关闭榨干原粹树脂
|
||||
domainParam.specifyResinUse = true
|
||||
//定死做预留冗余 先不实现 不能指定次数 只能指定启用
|
||||
let physical_domain = autoFight?.physical
|
||||
// || [
|
||||
// {order: 0, name: config.user.physical.names[0], count: 1, open: true},
|
||||
// {order: 1, name: config.user.physical.names[1], count: 0, open: false},
|
||||
// {order: 2, name: config.user.physical.names[2], count: 0, open: false},
|
||||
// {order: 3, name: config.user.physical.names[3], count: 0, open: false},
|
||||
// ]
|
||||
|
||||
if ((!physical_domain) || physical_domain.filter(item => item?.open).length === 0) {
|
||||
const names = config.user.physical.names;
|
||||
physical_domain = []
|
||||
names.forEach((name, index) => {
|
||||
physical_domain.push({order: index, name: name, open: index === 0})
|
||||
})
|
||||
}
|
||||
|
||||
physical_domain.sort((a, b) => a.order - b.order)
|
||||
// 不包含原粹树脂的和
|
||||
const noOriginalSum = physical_domain.filter(item => item?.name.trim() !== config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;//求和
|
||||
// 只包含原粹树脂的和
|
||||
const originalSum = physical_domain.filter(item => item?.name?.trim() === config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;
|
||||
const resinPriorityList = physical_domain.filter(item => item?.open).map(item => item?.name?.trim())
|
||||
// /** 树脂使用优先级列表 */
|
||||
// resinPriorityList: string[];
|
||||
// /** 使用原粹树脂次数 */
|
||||
// originalResinUseCount: number;
|
||||
// /** 使用浓缩树脂次数 */
|
||||
// condensedResinUseCount: number;
|
||||
// /** 使用须臾树脂次数 */
|
||||
// transientResinUseCount: number;
|
||||
// /** 使用脆弱树脂次数 */
|
||||
// fragileResinUseCount: number;
|
||||
await sleep(1000)
|
||||
//流程->返回主页 打开地图 返回主页
|
||||
// const physicalOcr = await ocrPhysical(true, true)
|
||||
// config.user.physical.current = physicalOcr.current
|
||||
// config.user.physical.min = physicalOcr.min
|
||||
|
||||
const currentPhysical = await countAllResin()
|
||||
config.user.physical.current = currentPhysical.originalResinCount;
|
||||
|
||||
const physical = config.user.physical
|
||||
if (domainParam.specifyResinUse && physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) {
|
||||
throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`)
|
||||
}
|
||||
|
||||
//关闭分解
|
||||
domainParam.autoArtifactSalvage = false
|
||||
|
||||
//配置树脂使用优先级
|
||||
if (resinPriorityList.length > 0) {
|
||||
domainParam.SetResinPriorityList(...resinPriorityList)
|
||||
}
|
||||
// log.debug(`开始执行秘境任务`)
|
||||
//秘境名称
|
||||
domainParam.DomainName = autoFight.domainName || domainParam.DomainName;
|
||||
log.debug(`秘境名称:${domainParam.DomainName}`)
|
||||
|
||||
//队伍名称
|
||||
domainParam.PartyName = autoFight.partyName || domainParam.PartyName;
|
||||
log.debug(`队伍名称:${domainParam.PartyName}`)
|
||||
|
||||
if (autoFight.sundaySelectedValue) {
|
||||
//周日|限时选择的值
|
||||
domainParam.SundaySelectedValue = "" + (autoFight.sundaySelectedValue || domainParam.SundaySelectedValue);
|
||||
}
|
||||
log.debug(`周日|限时选择的值:${domainParam.SundaySelectedValue}`)
|
||||
//副本轮数
|
||||
try {
|
||||
domainParam.DomainRoundNum = parseInt((autoFight.domainRoundNum || domainParam.DomainRoundNum) + "");
|
||||
} catch (e) {
|
||||
log.debug(`副本轮数:${autoFight.domainRoundNum}`)
|
||||
throwError(e.message)
|
||||
}
|
||||
log.debug(`副本轮数:${domainParam.DomainRoundNum}`)
|
||||
try {
|
||||
// 复活重试
|
||||
for (let i = 0; i < config.run.retry_count; i++) {
|
||||
try {
|
||||
await dispatcher.RunAutoDomainTask(domainParam);
|
||||
// 其他场景不重试
|
||||
break;
|
||||
} catch (e) {
|
||||
const errorMessage = e.message
|
||||
// 只有选择了秘境的时候才会重试
|
||||
if (errorMessage.includes("复活") && domainParam.DomainName) {
|
||||
continue;
|
||||
}
|
||||
if (!config.run.exclude_run_exception||config.run.loop_plan) {//排除异常 与循环计划互斥
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
log.info(`{0}`, "执行完成")
|
||||
// 退出秘境
|
||||
await outDomainUI()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动执行地脉花任务的异步函数
|
||||
* @param autoLeyLineOutcrop
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function autoLeyLineOutcrop(autoLeyLineOutcrop) {
|
||||
// autoLeyLineOutcrop = {
|
||||
// "count": 0,
|
||||
// "country": "country_cb3d792be8db",
|
||||
// "leyLineOutcropType": "leyLineOutcropType_f259b77fabcb",
|
||||
// // "isResinExhaustionMode": true,
|
||||
// // "openModeCountMin": true,
|
||||
// "useAdventurerHandbook": false,
|
||||
// "friendshipTeam": "friendshipTeam_7122cab56b16",
|
||||
// "team": "team_d0798ca3aa27",
|
||||
// "timeout": 0,
|
||||
// "isGoToSynthesizer": false,
|
||||
// "useFragileResin": false,
|
||||
// "useTransientResin": false,
|
||||
// "isNotification": false
|
||||
// }
|
||||
|
||||
|
||||
log.info(`{0}`, "开始执行地脉任务")
|
||||
// if (true) {
|
||||
// log.warn("地脉 暂不支持")
|
||||
// return
|
||||
// }
|
||||
let param = new AutoLeyLineOutcropParam(parseInteger(autoLeyLineOutcrop.count + ""), autoLeyLineOutcrop.country, autoLeyLineOutcrop.leyLineOutcropType);
|
||||
// let param = new AutoLeyLineOutcropParam();
|
||||
// param.count = parseInteger(autoLeyLineOutcrop.count+"");
|
||||
// param.country = autoLeyLineOutcrop.country;
|
||||
// param.leyLineOutcropType = autoLeyLineOutcrop.leyLineOutcropType;
|
||||
//和本体保持一致
|
||||
param.useAdventurerHandbook = !autoLeyLineOutcrop.useAdventurerHandbook;
|
||||
param.friendshipTeam = autoLeyLineOutcrop.friendshipTeam;
|
||||
param.team = autoLeyLineOutcrop.team;
|
||||
param.timeout = autoLeyLineOutcrop.timeout;
|
||||
param.isGoToSynthesizer = autoLeyLineOutcrop.isGoToSynthesizer;
|
||||
param.useFragileResin = autoLeyLineOutcrop.useFragileResin;
|
||||
param.useTransientResin = autoLeyLineOutcrop.useTransientResin;
|
||||
param.isNotification = autoLeyLineOutcrop.isNotification;
|
||||
|
||||
param.isResinExhaustionMode = true;
|
||||
param.openModeCountMin = true;
|
||||
await sleep(1000)
|
||||
// 复活重试
|
||||
for (let i = 0; i < config.run.retry_count; i++) {
|
||||
try {
|
||||
await dispatcher.RunAutoLeyLineOutcropTask(param);
|
||||
// 其他场景不重试
|
||||
break;
|
||||
} catch (e) {
|
||||
const errorMessage = e.message
|
||||
// 只有选择了秘境的时候才会重试
|
||||
if (errorMessage.includes("复活")) {
|
||||
continue;
|
||||
}
|
||||
if (!config.run.exclude_run_exception||config.run.loop_plan) {//排除异常 与循环计划互斥
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动执行幽境危战任务的异步函数
|
||||
* @param autoStygianOnslaught
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function autoStygianOnslaught(autoStygianOnslaught) {
|
||||
// autoStygianOnslaught = {
|
||||
// /**boss 名字 1~3 */
|
||||
// bossNum: 1,
|
||||
// /**结束后是否自动分解圣遗物*/
|
||||
// autoArtifactSalvage: false,
|
||||
// /**指定树脂的使用次数*/
|
||||
// specifyResinUse: false,
|
||||
// /**自定义使用树脂优先级*/
|
||||
// resinPriorityList: [""],
|
||||
// /** 使用原粹树脂刷取副本次数*/
|
||||
// originalResinUseCount: 0,
|
||||
// /** 使用浓缩树脂刷取副本次数*/
|
||||
// condensedResinUseCount: 0,
|
||||
// /** 使用须臾树脂刷取副本次数*/
|
||||
// transientResinUseCount: 0,
|
||||
// /** 使用脆弱树脂刷取副本次数*/
|
||||
// fragileResinUseCount: 0,
|
||||
// /**指定战斗队伍*/
|
||||
// fightTeamName: undefined
|
||||
// }
|
||||
log.debug(`autoStygianOnslaught ={0}`, JSON.stringify(autoStygianOnslaught))
|
||||
log.info(`{0}`, "开始执行幽境任务")
|
||||
let param = new AutoStygianOnslaughtParam()
|
||||
param.specifyResinUse = autoStygianOnslaught?.specifyResinUse || param.specifyResinUse
|
||||
|
||||
//定死做预留冗余 先不实现 不能指定次数 只能指定启用
|
||||
let physical_domain = autoStygianOnslaught?.physical||[]
|
||||
// || [
|
||||
// {order: 0, name: config.user.physical.names[0], count: 1, open: true},
|
||||
// {order: 1, name: config.user.physical.names[1], count: 0, open: false},
|
||||
// {order: 2, name: config.user.physical.names[2], count: 0, open: false},
|
||||
// {order: 3, name: config.user.physical.names[3], count: 0, open: false},
|
||||
// ]
|
||||
|
||||
if (param.specifyResinUse && ((!physical_domain) || physical_domain.filter(item => item?.open).length === 0)) {
|
||||
const names = config.user.physical.names;
|
||||
physical_domain = []
|
||||
names.forEach((name, index) => {
|
||||
physical_domain.push({order: index, name: name, open: index === 0, count: 1})
|
||||
})
|
||||
}
|
||||
|
||||
physical_domain?.sort((a, b) => a.order - b.order)
|
||||
// 不包含原粹树脂的和
|
||||
const noOriginalSum = physical_domain.filter(item => item?.name.trim() !== config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;//求和
|
||||
// 只包含原粹树脂的和
|
||||
const originalSum = physical_domain.filter(item => item?.name?.trim() === config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;
|
||||
const physical_domain_filter = physical_domain.filter(item => item?.open);
|
||||
const resinPriorityList = physical_domain_filter.map(item => item?.name?.trim())
|
||||
// /** 树脂使用优先级列表 */
|
||||
// resinPriorityList: string[];
|
||||
// /** 使用原粹树脂次数 */
|
||||
// originalResinUseCount: number;
|
||||
// /** 使用浓缩树脂次数 */
|
||||
// condensedResinUseCount: number;
|
||||
// /** 使用须臾树脂次数 */
|
||||
// transientResinUseCount: number;
|
||||
// /** 使用脆弱树脂次数 */
|
||||
// fragileResinUseCount: number;
|
||||
await sleep(1000)
|
||||
//流程->返回主页 打开地图 返回主页
|
||||
// const physicalOcr = await ocrPhysical(true, true)
|
||||
// config.user.physical.current = physicalOcr.current
|
||||
// config.user.physical.min = physicalOcr.min
|
||||
const currentPhysical = await countAllResin()
|
||||
config.user.physical.current = currentPhysical.originalResinCount;
|
||||
|
||||
const physical = config.user.physical
|
||||
if (physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) {
|
||||
throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`)
|
||||
}
|
||||
|
||||
|
||||
param.bossNum = autoStygianOnslaught?.bossNum > 0 && autoStygianOnslaught?.bossNum <= 3 ? autoStygianOnslaught.bossNum : param.bossNum
|
||||
param.fightTeamName = autoStygianOnslaught?.fightTeamName?.trim() !== "" ? autoStygianOnslaught.fightTeamName.trim() : param.fightTeamName
|
||||
if (resinPriorityList.length > 0) {
|
||||
param.SetResinPriorityList(...resinPriorityList)
|
||||
param.originalResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[0] && item?.open)?.count || 0
|
||||
param.condensedResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[1] && item?.open)?.count || 0
|
||||
param.transientResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[2] && item?.open)?.count || 0
|
||||
param.fragileResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[3] && item?.open)?.count || 0
|
||||
}
|
||||
|
||||
await sleep(1000)
|
||||
try {
|
||||
// 复活重试
|
||||
for (let i = 0; i < config.run.retry_count; i++) {
|
||||
try {
|
||||
await dispatcher.RunAutoStygianOnslaughtTask(param)
|
||||
// 其他场景不重试
|
||||
break;
|
||||
} catch (e) {
|
||||
const errorMessage = e.message
|
||||
if (errorMessage.includes("复活")) {
|
||||
continue;
|
||||
}
|
||||
if (!config.run.exclude_run_exception||config.run.loop_plan) {//排除异常 与循环计划互斥
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
log.info(`{0}`, "执行完成")
|
||||
// 退出危战
|
||||
await outStygianOnslaughtUI()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动执行列表处理函数
|
||||
* @param {Array} autoRunOrderList - 包含自动配置的数组
|
||||
*/
|
||||
async function autoRunList(autoRunOrderList) {
|
||||
//计划执行
|
||||
for (const item of autoRunOrderList) {
|
||||
await sleep(3000)
|
||||
if (item.runType === config.user.runTypes[0]) {
|
||||
await autoDomain(item.autoFight);
|
||||
} else if (item.runType === config.user.runTypes[1]) {
|
||||
await autoLeyLineOutcrop(item.autoLeyLineOutcrop);
|
||||
} else if (item.runType === config.user.runTypes[2]) {
|
||||
await autoStygianOnslaught(item.autoStygianOnslaught);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:安全地解析 day 字段
|
||||
function parseInteger(day) {
|
||||
if (day == null || String(day).trim() === "") {
|
||||
return undefined; // 空值或无效值返回 undefined
|
||||
}
|
||||
const parsedDay = parseInt(String(day).trim(), 10);
|
||||
return isNaN(parsedDay) ? undefined : parsedDay; // 非法数字返回 undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据不同的加载方式加载秘境配置
|
||||
* @param {string} Load - 加载方式类型,如uid或input
|
||||
* @param {Set} autoOrderSet - 用于存储秘境顺序的Set集合
|
||||
* @param {string} runConfig - 输入的配置字符串,仅在Load为input时使用
|
||||
*/
|
||||
async function loadMode(Load, autoOrderSet, runConfig) {
|
||||
switch (Load) {
|
||||
case LoadType.input:
|
||||
// 通过输入字符串方式加载配置
|
||||
if (runConfig) {
|
||||
// 处理输入字符串:去除首尾空格,将中文逗号替换为英文逗号,然后按逗号分割
|
||||
runConfig.trim().replaceAll(',', ',').split(",").forEach(
|
||||
item => {
|
||||
// 将当前项按"|"分割成数组
|
||||
let arr = item.split("|")
|
||||
// 类型|执行日期|执行顺序
|
||||
let index = 0
|
||||
let runType = arr[index]; // 解析运行类型
|
||||
index++
|
||||
const rawDays = arr[index];
|
||||
let days = (rawDays != null && String(rawDays).trim() !== "")
|
||||
? String(rawDays).split('/').map(d => parseInt(d.trim(), 10)).filter(d => !isNaN(d))
|
||||
: [];
|
||||
// let days = arr[index].trim() !== ""
|
||||
// ? arr[index].split('/').map(d => parseInt(d.trim())).filter(d => !isNaN(d))
|
||||
// : [];
|
||||
index++
|
||||
// 解析顺序值,处理可能的无效值
|
||||
let order = (() => {
|
||||
const rawOrder = arr[index]; // 获取原始值
|
||||
if (rawOrder == null || String(rawOrder).trim() === "") {
|
||||
return 0; // 若为空或无效值,默认返回 0
|
||||
}
|
||||
const parsedOrder = parseInt(String(rawOrder).trim(), 10); // 转换为整数
|
||||
return isNaN(parsedOrder) ? 0 : parsedOrder; // 若转换失败,返回默认值 0
|
||||
})();
|
||||
index++
|
||||
|
||||
// 创建秘境顺序对象
|
||||
let autoOrder = {
|
||||
order: order, // 顺序值
|
||||
// day: day,// 执行日期
|
||||
runType: runType, // 运行类型
|
||||
days: days, // 执行日期(数组)
|
||||
autoFight: undefined, // 秘境信息对象
|
||||
autoLeyLineOutcrop: undefined, // 地脉信息对象
|
||||
autoStygianOnslaught: undefined // 幽境信息对象
|
||||
}
|
||||
|
||||
|
||||
if (!config.user.runTypes.includes(runType)) {
|
||||
throwError(`运行类型${runType}输入错误`)
|
||||
}
|
||||
else if (config.user.runTypes[0] === runType) {
|
||||
// 创建秘境信息对象
|
||||
let autoFight = {
|
||||
domainName: undefined,//秘境名称
|
||||
partyName: undefined,//队伍名称
|
||||
sundaySelectedValue: 1,//周日|限时选择的值
|
||||
domainRoundNum: 0,//副本轮数
|
||||
}
|
||||
|
||||
//"|队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日,..."
|
||||
let partyName = arr[index]; // 解析队伍名称
|
||||
index++
|
||||
let domainName = arr[index]; // 解析秘境名称
|
||||
index++
|
||||
let domainRoundNum = arr[index]; // 解析副本轮数
|
||||
index++
|
||||
let sundaySelectedValue = "1"
|
||||
if (index <= arr.length - 1)
|
||||
sundaySelectedValue = arr[index]; // 解析周日|限时选择的值
|
||||
|
||||
// 检查秘境名称是否有效
|
||||
if (!config.domainNames.has(domainName)) {
|
||||
//秘境名称没有记录 查询是否是物品名称
|
||||
if (config.itemNames.has(domainName)) {
|
||||
const domainNameTemp = config.domainItemsMap.get(domainName);
|
||||
if (!domainNameTemp) {
|
||||
throw new Error(`${domainName} 输入错误`);
|
||||
}
|
||||
if (index <= arr.length - 1) {
|
||||
const domainSelectedValue = parseInt(config.domainOrderMap.get(domainName) + "");
|
||||
sundaySelectedValue = domainSelectedValue
|
||||
}
|
||||
domainName = domainNameTemp
|
||||
} else {
|
||||
throw new Error(`${domainName} 输入错误`);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置秘境信息的各个属性
|
||||
autoFight.partyName = partyName // 队伍名称
|
||||
autoFight.domainName = domainName // 秘境名称
|
||||
autoFight.domainRoundNum = domainRoundNum // 副本轮数
|
||||
autoFight.sundaySelectedValue = sundaySelectedValue // 周日|限时选择的值
|
||||
|
||||
autoOrder.autoFight = autoFight // 将秘境信息对象添加到秘境顺序对象中
|
||||
}
|
||||
else if (config.user.runTypes[1] === runType) {
|
||||
//"|队伍名称|国家|刷几轮|花类型|好感队|是否使用脆弱树脂|是否使用须臾树脂|是否前往合成台合成浓缩树脂|是否使用冒险之证|发送详细通知|战斗超时时间,..."
|
||||
let autoLeyLineOutcrop = {
|
||||
count: 0, // 刷几次(0=自动/无限)
|
||||
country: undefined, // 国家地区
|
||||
leyLineOutcropType: undefined, // 需映射为经验/摩拉
|
||||
useAdventurerHandbook: false, // 是否使用冒险之证
|
||||
friendshipTeam: "", // 好感队伍ID
|
||||
team: "", // 主队伍ID
|
||||
timeout: 120, // 超时时间(秒)
|
||||
isGoToSynthesizer: false, // 是否前往合成台
|
||||
useFragileResin: false, // 使用脆弱树脂
|
||||
useTransientResin: false, // 使用须臾树脂(须臾=Transient)
|
||||
isNotification: false // 是否通知
|
||||
}
|
||||
autoLeyLineOutcrop.team = arr[index]
|
||||
index++
|
||||
autoLeyLineOutcrop.country = arr[index]
|
||||
index++
|
||||
autoLeyLineOutcrop.count = arr[index]
|
||||
index++
|
||||
autoLeyLineOutcrop.leyLineOutcropType = arr[index]
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.friendshipTeam = arr[index]
|
||||
index++
|
||||
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.useFragileResin = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.useTransientResin = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.isGoToSynthesizer = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.useAdventurerHandbook = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.isNotification = (arr[index] != null && arr[index].trim() !== "")
|
||||
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.timeout = parseInteger(arr[index])
|
||||
|
||||
autoOrder.autoLeyLineOutcrop = autoLeyLineOutcrop // 将地脉信息对象添加到顺序对象中
|
||||
}
|
||||
else if (config.user.runTypes[2] === runType) {
|
||||
let autoStygianOnslaught = {
|
||||
bossNum: undefined,//boss1-3
|
||||
fightTeamName: "",//队伍名称
|
||||
specifyResinUse: undefined,//自定义树脂使用
|
||||
physical: [
|
||||
{order: 0, name: config.user.physical.names[1], open: true, count: 1},
|
||||
{order: 1, name: config.user.physical.names[0], open: true, count: 1},
|
||||
{order: 2, name: config.user.physical.names[2], open: false, count: 1},
|
||||
{order: 3, name: config.user.physical.names[3], open: false, count: 1}
|
||||
],//副本轮数
|
||||
}
|
||||
if (index <= arr.length - 1) {
|
||||
const bossNum = parseInteger(arr[index]);
|
||||
if (bossNum && bossNum > 0 && bossNum <= 3) {
|
||||
autoStygianOnslaught.bossNum = bossNum
|
||||
}
|
||||
}
|
||||
index++
|
||||
if (index <= arr.length - 1) {
|
||||
const fightTeamName = arr[index];
|
||||
if (fightTeamName && fightTeamName.trim() !== "") {
|
||||
autoStygianOnslaught.fightTeamName = fightTeamName
|
||||
}
|
||||
}
|
||||
index++
|
||||
if (index <= arr.length - 1) {
|
||||
autoStygianOnslaught.specifyResinUse = arr[index].trim() !== ""
|
||||
}
|
||||
if (autoStygianOnslaught.specifyResinUse) {
|
||||
index++
|
||||
let line = 0
|
||||
if (index <= arr.length - 1) {
|
||||
if (arr[index]?.trim() !== "") {
|
||||
const physical = []
|
||||
const physicals = arr[index].trim().split("/");
|
||||
for (let i = 0; i < physicals.length; i++) {
|
||||
const item = physicals[i];
|
||||
physical.push({order: i, name: item, open: true, count: 1})
|
||||
}
|
||||
line = physical.length
|
||||
autoStygianOnslaught.physical = physical
|
||||
}
|
||||
}
|
||||
index++
|
||||
if (index <= arr.length - 1) {
|
||||
if (line > 0 && arr[index]?.trim() !== "") {
|
||||
const counts = arr[index].trim().split("/")
|
||||
.map(item => {
|
||||
let count = parseInteger(item) || 1;
|
||||
return count
|
||||
});
|
||||
autoStygianOnslaught.physical.forEach((item, index) => {
|
||||
try {
|
||||
item.count = counts[index] || 1;
|
||||
} catch (e) {
|
||||
log.warn(`解析${item.name}数量失败`)
|
||||
throwError(`解析${item.name}数量失败`)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
autoOrder.autoStygianOnslaught = autoStygianOnslaught
|
||||
}
|
||||
|
||||
// 将秘境顺序对象添加到列表中
|
||||
autoOrderSet.add(autoOrder)
|
||||
}
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case LoadType.uid:
|
||||
|
||||
// 通过UID方式加载配置
|
||||
const uid = config.user.uid || (await ocrUid()) // 获取用户UID,如果未配置则通过OCR识别获取
|
||||
// const configAutoFightOrderMap = JSON.parse(file.readTextSync(config.path.runConfig)) || new Map() // 读取本地配置文件并转换为Map对象
|
||||
// const uidConfigList = configAutoFightOrderMap.get(uid + "") || []; // 获取当前UID对应的配置列表
|
||||
|
||||
const configAutoFightOrderMap = JSON.parse(file.readTextSync(config.path.runConfig)) || {} // 读取本地配置文件
|
||||
const uidConfigList = configAutoFightOrderMap[uid + ""] || []; // 获取当前UID对应的配置列表
|
||||
if (uidConfigList?.length > 0) {
|
||||
// 如果配置列表不为空,遍历并添加到结果集合中
|
||||
uidConfigList.forEach(item => {
|
||||
// 将秘境顺序对象添加到列表中
|
||||
// 主逻辑优化
|
||||
// if (item.day !== undefined) {
|
||||
// item.day = parseInteger(item.day);
|
||||
// }
|
||||
if (item.days && item.days.length > 0) {
|
||||
item.days = item.days.map(day => parseInteger(day))
|
||||
// item.day = parseInteger(item.day);
|
||||
}
|
||||
autoOrderSet.add(item)
|
||||
})
|
||||
}
|
||||
break
|
||||
case LoadType.bgi_tools:
|
||||
// 通过bgi_tools方式加载配置
|
||||
log.info(`开始拉取bgi_tools配置`)
|
||||
const uidConfigListBgiTools = await pullJsonConfig(config.user.uid + '', config.bgi_tools.api.httpPullJsonConfig) || []
|
||||
if (uidConfigListBgiTools?.length > 0) {
|
||||
// 如果配置列表不为空,遍历并添加到结果集合中
|
||||
uidConfigListBgiTools.forEach(item => {
|
||||
// 将秘境顺序对象添加到列表中
|
||||
// 主逻辑优化
|
||||
if (item.days && item.days.length > 0) {
|
||||
item.days = item.days.map(day => parseInteger(day))
|
||||
// item.day = parseInteger(item.day);
|
||||
}
|
||||
autoOrderSet.add(item)
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error("请先配置加载方式");
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化执行顺序列表
|
||||
* @param {string} domainConfig - 输入的字符串,包含秘境顺序信息
|
||||
* @returns {Array} 返回处理后的秘境顺序列表
|
||||
*/
|
||||
async function initRunOrderList(domainConfig) {
|
||||
const autoFightOrderSet = new Set() // 存储秘境顺序列表的数组
|
||||
/* let te = {
|
||||
order: 1, // 顺序值
|
||||
day: 0,// 执行日期
|
||||
autoFight: {
|
||||
domainName: undefined,//秘境名称
|
||||
partyName: undefined,//队伍名称
|
||||
sundaySelectedValue: undefined,//周日|限时选择的值
|
||||
domainRoundNum: undefined,//副本轮数
|
||||
} // 秘境信息对象
|
||||
}*/
|
||||
// let Load = LoadType.uid
|
||||
|
||||
for (const Load of config.run.loads) {
|
||||
await loadMode(Load.load, autoFightOrderSet, domainConfig);
|
||||
}
|
||||
|
||||
// 检查是否已配置秘境
|
||||
if (!autoFightOrderSet || autoFightOrderSet.size <= 0) {
|
||||
throw new Error("请先配置体力配置");
|
||||
}
|
||||
// 返回处理后的秘境顺序列表
|
||||
let from = Array.from(autoFightOrderSet);
|
||||
let dayOfWeek = await getDayOfWeek();
|
||||
log.debug(`old-from:{0}`, JSON.stringify(from))
|
||||
from = from
|
||||
//过滤掉不执行的秘境
|
||||
.filter(item => config.user.runTypes.includes(item.runType))
|
||||
.filter(item => {
|
||||
// if (item.day) {
|
||||
// return item.day === dayOfWeek.day
|
||||
// }
|
||||
log.debug(`[{1}]item.days.length:{0}`, dayOfWeek.day, item?.days?.length || 0)
|
||||
if (item.days && item.days.length > 0) {
|
||||
const includes = item.days.includes(dayOfWeek.day);
|
||||
log.debug(`[{1}]item.days:{0}`, dayOfWeek.day, JSON.stringify(item.days))
|
||||
return includes;
|
||||
}
|
||||
return true
|
||||
})
|
||||
from.sort((a, b) => b.order - a.order)
|
||||
log.debug(`from:{0}`, JSON.stringify(from))
|
||||
return from;
|
||||
}
|
||||
import {countAllResin, Physical} from "./utils/physical";
|
||||
import {
|
||||
autoRunList,
|
||||
initRunOrderList,
|
||||
checkAndFilterStygianOnslaught
|
||||
} from './utils/load_check_run'
|
||||
|
||||
/**
|
||||
* 初始化函数
|
||||
@@ -658,8 +23,16 @@ async function init() {
|
||||
// 这是一个异步初始化配置的步骤
|
||||
await initSettings()
|
||||
await initConfig();
|
||||
|
||||
if (config.bgi_tools.open.open_push) {
|
||||
log.info(`开始推送bgi_tools配置`)
|
||||
await pushAllJsonConfig(JSON.parse(file.readTextSync(config.path.domain)), config.bgi_tools.api.httpPushAllJsonConfig, config.bgi_tools.token)
|
||||
await pushAllCountryConfig(JSON.parse(file.readTextSync(config.path.countryList)), config.bgi_tools.api.httpPushAllCountryConfig, config.bgi_tools.token)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 主函数,用于执行秘境自动刷取任务
|
||||
* @async
|
||||
@@ -667,48 +40,16 @@ async function init() {
|
||||
async function main() {
|
||||
// 初始化配置
|
||||
await init();
|
||||
if (config.bgi_tools.open.open_push) {
|
||||
log.info(`开始推送bgi_tools配置`)
|
||||
await pushAllJsonConfig(JSON.parse(file.readTextSync(config.path.domain)), config.bgi_tools.api.httpPushAllJsonConfig, config.bgi_tools.token)
|
||||
await pushAllCountryConfig(JSON.parse(file.readTextSync(config.path.countryList)), config.bgi_tools.api.httpPushAllCountryConfig, config.bgi_tools.token)
|
||||
}
|
||||
// 获取配置
|
||||
let runConfig = config.run.config;
|
||||
//"队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日|周几执行(0-6)不填默认执行|执行顺序,..."
|
||||
const autoRunOrderList = await initRunOrderList(runConfig);
|
||||
let list = autoRunOrderList.filter(item =>
|
||||
(item.runType === config.user.runTypes[0] && item?.autoFight.domainRoundNum > 0)
|
||||
|| (item.runType === config.user.runTypes[1] && item?.autoLeyLineOutcrop.count > 0)||(item.runType === config.user.runTypes[2])
|
||||
|| (item.runType === config.user.runTypes[1] && item?.autoLeyLineOutcrop.count > 0) || (item.runType === config.user.runTypes[2])
|
||||
)
|
||||
|
||||
const hasStygianOnslaught = list.some(item => item.runType === config.user.runTypes[2]);
|
||||
if (hasStygianOnslaught) {
|
||||
log.info(`{0}`,`检查幽境危战紊乱爆发期开放`)
|
||||
try {
|
||||
await toMainUi()
|
||||
const isStygianOnslaught = await findStygianOnslaught();
|
||||
if (isStygianOnslaught) {
|
||||
//圣遗物秘境名称
|
||||
const holyRelicDomainNames = config.domainList.filter(item => !item.hasOrder).map(item => item.name);
|
||||
const filter = list.find(item => item.runType === config.user.runTypes[0] && holyRelicDomainNames.includes(item.autoFight?.domainName));
|
||||
if (filter) {
|
||||
// 幽境危战添加秘境顺序前
|
||||
list.forEach(item => {
|
||||
if (item.runType === config.user.runTypes[2]) {
|
||||
item.order = Math.max(filter.order + 1, item.order)
|
||||
}
|
||||
})
|
||||
list.sort((item1, item2) => item2.order - item1.order)
|
||||
}
|
||||
log.info(`{0}`,`幽境危战紊乱爆发期已开启`)
|
||||
} else {
|
||||
log.info(`{0}`,`幽境危战紊乱爆发期已结束`)
|
||||
list = list.filter(item => item.runType !== config.user.runTypes[2])
|
||||
}
|
||||
} finally {
|
||||
await toMainUi()
|
||||
}
|
||||
}
|
||||
list = await checkAndFilterStygianOnslaught(list)
|
||||
|
||||
if (list?.length > 0) {
|
||||
//循环跑
|
||||
@@ -717,7 +58,7 @@ async function main() {
|
||||
if (config.run.loop_plan) {
|
||||
// 重新获取当前体力值
|
||||
// const physicalOcr = await ocrPhysical(true, true);
|
||||
const currentPhysical = await countAllResin()
|
||||
const currentPhysical = await Physical.countAllResin()
|
||||
config.user.physical.current = currentPhysical.originalResinCount;
|
||||
//循环
|
||||
if (config.user.physical.current < config.user.physical.min) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "自动体力计划",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "",
|
||||
"settings_ui": "settings.json",
|
||||
"main": "main.js",
|
||||
"bgi_version": "0.58.0",
|
||||
"bgi_version": "0.61.0",
|
||||
"key": "oiJbmjU2R0NniiwiZxh",
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@@ -7,347 +7,368 @@ const xyConfig = {
|
||||
bottom: {x: 342, y: 791},
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 滚动页面的异步函数
|
||||
* @param {number} totalDistance - 总滚动距离
|
||||
* @param {boolean} [isUp=false] - 是否向上滚动,默认为false(向下滚动)
|
||||
* @param {number} [waitCount=6] - 每隔多少步等待一次
|
||||
* @param {number} [stepDistance=30] - 每步滚动的距离
|
||||
* @param {number} [delayMs=1] - 等待的延迟时间(毫秒)
|
||||
*/
|
||||
async function scrollPage(totalDistance, isUp = false, waitCount = 6, stepDistance = 30, delayMs = 1000) {
|
||||
let ms = 600
|
||||
await sleep(ms);
|
||||
leftButtonDown(); // 按下左键
|
||||
await sleep(ms);
|
||||
// 计算总步数
|
||||
let steps = Math.floor(totalDistance / stepDistance);
|
||||
// 开始循环滚动
|
||||
for (let j = 0; j < steps; j++) {
|
||||
// 计算剩余距离
|
||||
let remainingDistance = totalDistance - j * stepDistance;
|
||||
// 确定本次移动距离
|
||||
let moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance;
|
||||
// 如果是向上滚动,则移动距离取反
|
||||
if (isUp) {
|
||||
//向上活动
|
||||
moveDistance = -moveDistance
|
||||
}
|
||||
// 执行鼠标移动
|
||||
moveMouseBy(0, -moveDistance);
|
||||
// 取消注释后会在每一步后等待
|
||||
// await sleep(delayMs);
|
||||
// 每隔waitCount步等待一次
|
||||
if (j % waitCount === 0) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
// 滚动完成后释放左键
|
||||
await sleep(ms);
|
||||
leftButtonUp();
|
||||
await sleep(ms);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据活动页面进行滚动操作
|
||||
* @param {boolean} isUp - 滚动方向,true表示向上滚动,false表示向下滚动
|
||||
* @param {number} total - 滚动总量
|
||||
* @param {number} waitCount - 等待次数
|
||||
* @param {number} stepDistance - 每次滚动的步长距离
|
||||
* @param {number} scrollPageCount - 滚动页面次数,默认从config中获取
|
||||
*/
|
||||
async function scrollPagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30, scrollPageCount = 4) {
|
||||
// 根据滚动方向设置坐标位置
|
||||
// 如果是向上滚动,使用顶部坐标;否则使用底部坐标
|
||||
let x = isUp ? xyConfig.top.x : xyConfig.bottom.x; // 根据滚动方向获取x坐标
|
||||
let y = isUp ? xyConfig.top.y : xyConfig.bottom.y; // 根据滚动方向获取y坐标
|
||||
// 记录滑动方向
|
||||
log.info(`活动页面-${isUp ? '向上' : '向下'}滑动`);
|
||||
// 注释:坐标信息已注释掉,避免日志过多
|
||||
// log.info(`坐标:${x},${y}`);
|
||||
// 根据配置的滑动次数执行循环
|
||||
for (let i = 0; i < scrollPageCount; i++) {
|
||||
// 移动到坐标位置
|
||||
await moveMouseTo(x, y)
|
||||
//80 18次滑动偏移量 46次测试未发现偏移
|
||||
await scrollPage(total, isUp, waitCount, stepDistance)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到活动页面最顶部(优化版)
|
||||
* 通过连续检测顶部活动名称相同来确认已到顶,更加健壮
|
||||
* @param {Object} ocrRegion - OCR识别区域,默认为活动列表区域
|
||||
* @throws {Error} 如果超过最大尝试次数仍未检测到稳定顶部,则抛出错误
|
||||
*/
|
||||
async function scrollPagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) {
|
||||
let ms = 800; // 等待时间,单位毫秒
|
||||
let topActivityName = null; // 上一次检测到的顶部活动名称
|
||||
let sameTopCount = 0; // 连续出现相同顶部名称的次数
|
||||
const requiredSameCount = 1; // 需要连续几次相同才确认到顶(推荐 2~3)
|
||||
let attemptIndex = 0; // 总尝试次数计数器
|
||||
const maxAttempts = config.toTopCount; // 可配置,默认为15次
|
||||
|
||||
log.info("开始滚动到活动页面顶部...");
|
||||
|
||||
while (attemptIndex < maxAttempts) {
|
||||
attemptIndex++;
|
||||
log.info(`第 {attemptIndex} 次尝试回顶`, attemptIndex);
|
||||
|
||||
// 移动鼠标到安全位置,避免干扰截图
|
||||
await moveMouseTo(0, 20);
|
||||
|
||||
// 截图 + OCR 识别活动列表区域
|
||||
let captureRegion = null;
|
||||
try {
|
||||
captureRegion = captureGameRegion();
|
||||
const ocrObject = RecognitionObject.Ocr(
|
||||
ocrRegion.x,
|
||||
ocrRegion.y,
|
||||
ocrRegion.width,
|
||||
ocrRegion.height
|
||||
);
|
||||
// 可选:提升识别率
|
||||
// ocrObject.threshold = 0.8;
|
||||
|
||||
let resList = captureRegion.findMulti(ocrObject);
|
||||
// captureRegion.dispose();
|
||||
|
||||
// 如果完全没识别到任何活动,可能是页面异常或已在顶(极少情况)
|
||||
if (resList.length === 0) {
|
||||
log.warn("顶部OCR未识别到任何活动条目,可能是页面为空或识别失败");
|
||||
// 再尝试一次向上滚大距离
|
||||
// await scrollPagesByActivity(true); // true = 向上
|
||||
await scrollPagesByActivity(true, 80 * 4, 6, 60, 1);
|
||||
await sleep(ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 取当前识别到的最顶部活动名称(resList[0] 通常是列表最上面的)
|
||||
const currentTopName = resList[0].text.trim();
|
||||
|
||||
log.info(`当前检测到的顶部活动: {currentTopName}`, currentTopName);
|
||||
|
||||
// 判断是否与上一次相同
|
||||
if (currentTopName === topActivityName) {
|
||||
sameTopCount++;
|
||||
log.debug(`顶部活动连续相同 ${sameTopCount} 次`);
|
||||
|
||||
if (sameTopCount >= requiredSameCount) {
|
||||
log.info(`已连续 {sameTopCount} 次检测到相同顶部活动,确认回到页面最顶部!`, sameTopCount);
|
||||
return; // 成功回到顶部
|
||||
}
|
||||
} else {
|
||||
// 顶部名称变了,说明还在向上滚动,重置计数
|
||||
topActivityName = currentTopName;
|
||||
sameTopCount = 1; // 这次算第一次
|
||||
}
|
||||
|
||||
// 未达到稳定状态,继续向上滚动一页(可根据实际情况调整滚动距离)
|
||||
// 这里使用更大滚动距离确保能快速回顶
|
||||
// await scrollPagesByActivity(true); // true = 向上
|
||||
await scrollPagesByActivity(true, 80 * 4, 6, 60, 1);
|
||||
|
||||
await sleep(ms); // 给页面滚动和渲染留时间
|
||||
} finally {
|
||||
// 确保资源被正确释放
|
||||
if (captureRegion) {
|
||||
captureRegion.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 超过最大尝试次数仍未稳定
|
||||
throw new Error(`回到活动页面顶部失败:尝试 ${attemptIndex} 次后仍未检测到稳定顶部活动`);
|
||||
}
|
||||
|
||||
/**
|
||||
* OCR识别活动剩余时间的函数
|
||||
* @param {Object} ocrRegion - OCR识别的区域坐标和尺寸
|
||||
* @param {string} activityName - 活动名称
|
||||
* @param {string} key - 要识别的关键词,默认为
|
||||
* @returns {string|null} 返回识别到的剩余时间文本,若未识别到则返回null
|
||||
*/
|
||||
async function OcrKey(activityName, key, ocrRegion = ocrRegionConfig.remainingTime) {
|
||||
if (!key) {
|
||||
return null
|
||||
}
|
||||
let captureRegion = captureGameRegion(); // 获取游戏区域截图
|
||||
try {
|
||||
let list = new Array()
|
||||
const ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); // 创建OCR识别对象
|
||||
// ocrObject.threshold = 1.0;
|
||||
let resList = captureRegion.findMulti(ocrObject); // 在指定区域进行OCR识别
|
||||
for (let res of resList) {
|
||||
log.debug(`[info][{key}]{activityName}--{time}`, key, activityName, res.text); // 记录日志
|
||||
if (res.text.includes(key)) { // 检查识别结果是否包含关键词
|
||||
log.debug(`[{key}][命中]{activityName}--{time}`, key, activityName, res.text); // 记录日志
|
||||
list.push(res.text.trim())
|
||||
// return res.text // 返回识别到的文本
|
||||
}
|
||||
}
|
||||
if (list.length > 0) {
|
||||
return list.join('<-->')
|
||||
}
|
||||
// 没有识别到剩余时间
|
||||
return null;
|
||||
|
||||
} finally {
|
||||
captureRegion.dispose(); // 释放截图资源
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ... existing code ...
|
||||
|
||||
async function scrollFindActivity(name, key, value,activityKey = "F5") {
|
||||
const ms = 1000;
|
||||
// 1. 打开活动页面(默认 F5)
|
||||
await keyPress(activityKey);
|
||||
await sleep(ms * 2);
|
||||
// 2. 先强制滚动到最顶部(非常重要!)
|
||||
try {
|
||||
await scrollPagesByActivityToTop();
|
||||
export class scroll {
|
||||
/**
|
||||
* 模拟页面滚动功能
|
||||
* @param {number} totalDistance - 总滚动距离
|
||||
* @param {boolean} [isUp=false] - 是否向上滚动,默认为向下滚动
|
||||
* @param {number} [waitCount=6] - 每隔多少步等待一次,默认为6步
|
||||
* @param {number} [stepDistance=30] - 每次滚动的步长距离,默认为30像素
|
||||
* @param {number} [delayMs=1000] - 每次等待的毫秒数,默认为1000毫秒
|
||||
* @returns {Promise<void>} - 返回一个Promise,表示滚动操作完成
|
||||
*/
|
||||
static async page(totalDistance, isUp = false, waitCount = 6, stepDistance = 30, delayMs = 1000) {
|
||||
let ms = 600 // 初始延迟时间,单位毫秒
|
||||
await sleep(ms); // 等待初始延迟时间
|
||||
leftButtonDown(); // 按下左键
|
||||
await sleep(ms);
|
||||
// 计算总步数
|
||||
let steps = Math.floor(totalDistance / stepDistance);
|
||||
// 开始循环滚动
|
||||
for (let j = 0; j < steps; j++) {
|
||||
// 计算剩余距离
|
||||
let remainingDistance = totalDistance - j * stepDistance;
|
||||
// 确定本次移动距离
|
||||
let moveDistance = remainingDistance < stepDistance ? remainingDistance : stepDistance;
|
||||
// 如果是向上滚动,则移动距离取反
|
||||
if (isUp) {
|
||||
//向上活动
|
||||
moveDistance = -moveDistance
|
||||
}
|
||||
// 执行鼠标移动
|
||||
moveMouseBy(0, -moveDistance);
|
||||
// 取消注释后会在每一步后等待
|
||||
// await sleep(delayMs);
|
||||
// 每隔waitCount步等待一次
|
||||
if (j % waitCount === 0) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
// 滚动完成后释放左键
|
||||
await sleep(ms);
|
||||
leftButtonUp();
|
||||
await sleep(ms);
|
||||
} catch (e) {
|
||||
log.warn("回到顶部失败,但继续尝试执行");
|
||||
}
|
||||
let findActivity = {
|
||||
name: false,
|
||||
key: false,
|
||||
value: false
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
log.warn("未指定活动名称,无法查找");
|
||||
return findActivity;
|
||||
}
|
||||
|
||||
let lastPageBottomName = null;
|
||||
let sameBottomCount = 0;
|
||||
const sameBottomCountMax = 1;
|
||||
let scannedPages = 0;
|
||||
const maxPages = 25;
|
||||
let previousPageActivities = new Set();
|
||||
/**
|
||||
* 根据活动滑动页面
|
||||
* @param {boolean} isUp - 滑动方向,true表示向上滑动,false表示向下滑动
|
||||
* @param {number} total - 滑动总量
|
||||
* @param {number} waitCount - 等待次数
|
||||
* @param {number} stepDistance - 每次滑动的步长距离
|
||||
* @param {number} scrollPageCount - 滑动页面的次数
|
||||
* @returns {Promise<void>} - 返回一个Promise,表示异步操作的完成
|
||||
*/
|
||||
static async pagesByActivity(isUp = false, total = 90, waitCount = 6, stepDistance = 30, scrollPageCount = 4) {
|
||||
// 根据滚动方向设置坐标位置
|
||||
// 如果是向上滚动,使用顶部坐标;否则使用底部坐标
|
||||
let x = isUp ? xyConfig.top.x : xyConfig.bottom.x; // 根据滚动方向获取x坐标
|
||||
let y = isUp ? xyConfig.top.y : xyConfig.bottom.y; // 根据滚动方向获取y坐标
|
||||
// 记录滑动方向
|
||||
log.info(`活动页面-${isUp ? '向上' : '向下'}滑动`);
|
||||
// 注释:坐标信息已注释掉,避免日志过多
|
||||
// log.info(`坐标:${x},${y}`);
|
||||
// 根据配置的滑动次数执行循环
|
||||
for (let i = 0; i < scrollPageCount; i++) {
|
||||
// 移动到坐标位置
|
||||
await moveMouseTo(x, y)
|
||||
//80 18次滑动偏移量 46次测试未发现偏移
|
||||
await scroll.page(total, isUp, waitCount, stepDistance)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 主循环:逐页向下扫描
|
||||
while (scannedPages < maxPages) {
|
||||
scannedPages++;
|
||||
log.info(`正在扫描第 ${scannedPages} 页`);
|
||||
/**
|
||||
* 滚动到活动页面顶部的异步方法
|
||||
* @param {Object} ocrRegion - OCR识别区域配置,默认为ocrRegionConfig.activity
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} 当超过最大尝试次数仍未回到顶部时抛出错误
|
||||
*/
|
||||
static async pagesByActivityToTop(ocrRegion = ocrRegionConfig.activity) {
|
||||
let ms = 800; // 等待时间,单位毫秒
|
||||
let topActivityName = null; // 上一次检测到的顶部活动名称
|
||||
let sameTopCount = 0; // 连续出现相同顶部名称的次数
|
||||
const requiredSameCount = 1; // 需要连续几次相同才确认到顶(推荐 2~3)
|
||||
let attemptIndex = 0; // 总尝试次数计数器
|
||||
const maxAttempts = config.toTopCount; // 可配置,默认为15次
|
||||
|
||||
await moveMouseTo(0, 20);
|
||||
log.info("开始滚动到活动页面顶部...");
|
||||
|
||||
let captureRegion = null;
|
||||
try {
|
||||
captureRegion = captureGameRegion();
|
||||
while (attemptIndex < maxAttempts) {
|
||||
attemptIndex++;
|
||||
log.info(`第 {attemptIndex} 次尝试回顶`, attemptIndex);
|
||||
|
||||
const ocrObject = RecognitionObject.Ocr(
|
||||
ocrRegionConfig.activity.x,
|
||||
ocrRegionConfig.activity.y,
|
||||
ocrRegionConfig.activity.width,
|
||||
ocrRegionConfig.activity.height
|
||||
);
|
||||
let resList = captureRegion.findMulti(ocrObject);
|
||||
// 移动鼠标到安全位置,避免干扰截图
|
||||
await moveMouseTo(0, 20);
|
||||
|
||||
if (resList.length === 0) {
|
||||
log.info("当前页未识别到任何活动,视为已到页面底部");
|
||||
break;
|
||||
}
|
||||
// 截图 + OCR 识别活动列表区域
|
||||
let captureRegion = null;
|
||||
try {
|
||||
captureRegion = captureGameRegion();
|
||||
const ocrObject = RecognitionObject.Ocr(
|
||||
ocrRegion.x,
|
||||
ocrRegion.y,
|
||||
ocrRegion.width,
|
||||
ocrRegion.height
|
||||
);
|
||||
// 可选:提升识别率
|
||||
// ocrObject.threshold = 0.8;
|
||||
|
||||
// ============ 重复页检测 ============
|
||||
const currentPageNames = new Set();
|
||||
for (let res of resList) {
|
||||
currentPageNames.add(res.text.trim());
|
||||
}
|
||||
let resList = captureRegion.findMulti(ocrObject);
|
||||
// captureRegion.dispose();
|
||||
|
||||
if (previousPageActivities.size > 0) {
|
||||
let overlapCount = 0;
|
||||
for (let actName of currentPageNames) {
|
||||
if (previousPageActivities.has(actName)) overlapCount++;
|
||||
// 如果完全没识别到任何活动,可能是页面异常或已在顶(极少情况)
|
||||
if (resList.length === 0) {
|
||||
log.warn("顶部OCR未识别到任何活动条目,可能是页面为空或识别失败");
|
||||
// 再尝试一次向上滚大距离
|
||||
// await scrollPagesByActivity(true); // true = 向上
|
||||
await scroll.pagesByActivity(true, 80 * 4, 6, 60, 1);
|
||||
await sleep(ms);
|
||||
continue;
|
||||
}
|
||||
const overlapRatio = overlapCount / previousPageActivities.size;
|
||||
|
||||
if (overlapRatio >= 0.7) {
|
||||
log.info(`检测到当前页与上一页高度重复(重合率 ${Math.round(overlapRatio * 100)}%),已到达底部,停止扫描`);
|
||||
// 取当前识别到的最顶部活动名称(resList[0] 通常是列表最上面的)
|
||||
const currentTopName = resList[0].text.trim();
|
||||
|
||||
log.info(`当前检测到的顶部活动: {currentTopName}`, currentTopName);
|
||||
|
||||
// 判断是否与上一次相同
|
||||
if (currentTopName === topActivityName) {
|
||||
sameTopCount++;
|
||||
log.debug(`顶部活动连续相同 ${sameTopCount} 次`);
|
||||
|
||||
if (sameTopCount >= requiredSameCount) {
|
||||
log.info(`已连续 {sameTopCount} 次检测到相同顶部活动,确认回到页面最顶部!`, sameTopCount);
|
||||
return; // 成功回到顶部
|
||||
}
|
||||
} else {
|
||||
// 顶部名称变了,说明还在向上滚动,重置计数
|
||||
topActivityName = currentTopName;
|
||||
sameTopCount = 1; // 这次算第一次
|
||||
}
|
||||
|
||||
// 未达到稳定状态,继续向上滚动一页(可根据实际情况调整滚动距离)
|
||||
// 这里使用更大滚动距离确保能快速回顶
|
||||
// await scrollPagesByActivity(true); // true = 向上
|
||||
await scroll.pagesByActivity(true, 80 * 4, 6, 60, 1);
|
||||
|
||||
await sleep(ms); // 给页面滚动和渲染留时间
|
||||
} finally {
|
||||
// 确保资源被正确释放
|
||||
if (captureRegion) {
|
||||
captureRegion.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 超过最大尝试次数仍未稳定
|
||||
throw new Error(`回到活动页面顶部失败:尝试 ${attemptIndex} 次后仍未检测到稳定顶部活动`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查找指定活动的静态异步方法
|
||||
* @param {string} name - 要查找的活动名称
|
||||
* @param {string} key - 活动中要匹配的关键字
|
||||
* @param {string} value - 关键字对应的值
|
||||
* @param {string} activityKey - 默认为"F5",用于打开活动页面的按键
|
||||
* @returns {Object} 返回一个包含查找结果的对象,包含name、key、value三个布尔值属性
|
||||
*/
|
||||
static async findActivity(name, key, value, activityKey = "F5") {
|
||||
const ms = 1000; // 定义时间单位,1秒
|
||||
// 1. 打开活动页面(默认 F5)
|
||||
await keyPress(activityKey); // 模拟按键打开活动页面
|
||||
await sleep(ms * 2); // 等待2秒确保页面加载完成
|
||||
// 2. 先强制滚动到最顶部(非常重要!)
|
||||
try {
|
||||
await scroll.pagesByActivityToTop(); // 滚动到页面顶部
|
||||
await sleep(ms); // 等待1秒确保滚动完成
|
||||
} catch (e) {
|
||||
log.warn("回到顶部失败,但继续尝试执行"); // 捕获并处理异常
|
||||
}
|
||||
// 初始化查找结果对象
|
||||
let findActivity = {
|
||||
name: false,
|
||||
key: false,
|
||||
value: false
|
||||
};
|
||||
|
||||
// 如果未指定活动名称,直接返回失败结果
|
||||
if (!name) {
|
||||
log.warn("未指定活动名称,无法查找");
|
||||
return findActivity;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 初始化底部检测相关变量
|
||||
let lastPageBottomName = null; // 记录上一页底部活动名称
|
||||
let sameBottomCount = 0; // 连续相同底部活动计数
|
||||
const sameBottomCountMax = 1; // 最大连续相同次数
|
||||
let scannedPages = 0; // 已扫描页数计数
|
||||
const maxPages = 25; // 最大扫描页数限制
|
||||
let previousPageActivities = new Set(); // 存储上一页所有活动名称
|
||||
|
||||
// 4. 主循环:逐页向下扫描
|
||||
while (scannedPages < maxPages) {
|
||||
scannedPages++;
|
||||
log.info(`正在扫描第 ${scannedPages} 页`);
|
||||
|
||||
await moveMouseTo(0, 20); // 移动鼠标到页面顶部
|
||||
|
||||
let captureRegion = null; // 初始化屏幕捕获区域
|
||||
try {
|
||||
captureRegion = captureGameRegion(); // 捕获游戏屏幕区域
|
||||
|
||||
// 创建OCR识别对象
|
||||
const ocrObject = RecognitionObject.Ocr(
|
||||
ocrRegionConfig.activity.x,
|
||||
ocrRegionConfig.activity.y,
|
||||
ocrRegionConfig.activity.width,
|
||||
ocrRegionConfig.activity.height
|
||||
);
|
||||
let resList = captureRegion.findMulti(ocrObject); // 使用OCR识别活动
|
||||
|
||||
// 如果当前页没有识别到任何活动,视为已到底部
|
||||
if (resList.length === 0) {
|
||||
log.info("当前页未识别到任何活动,视为已到页面底部");
|
||||
break;
|
||||
}
|
||||
}
|
||||
previousPageActivities = currentPageNames;
|
||||
// ===================================
|
||||
|
||||
let currentPageBottomName = null;
|
||||
let foundTarget = false;
|
||||
// ============ 重复页检测 ============
|
||||
const currentPageNames = new Set(); // 存储当前页所有活动名称
|
||||
for (let res of resList) {
|
||||
currentPageNames.add(res.text.trim()); // 添加活动名称到集合
|
||||
}
|
||||
|
||||
// 遍历当前页所有识别到的活动条目
|
||||
for (let res of resList) {
|
||||
const activityName = res.text.trim();
|
||||
currentPageBottomName = activityName; // 更新底部活动名
|
||||
// 检测当前页与上一页的重合度
|
||||
if (previousPageActivities.size > 0) {
|
||||
let overlapCount = 0;
|
||||
for (let actName of currentPageNames) {
|
||||
if (previousPageActivities.has(actName)) overlapCount++;
|
||||
}
|
||||
const overlapRatio = overlapCount / previousPageActivities.size;
|
||||
|
||||
// 【关键修改】检查是否是目标活动名称(精确匹配)
|
||||
if (activityName.includes(name)) {
|
||||
findActivity.name = true;
|
||||
log.info(`找到目标活动:${activityName}`);
|
||||
await click(res.x, res.y);
|
||||
await sleep(ms);
|
||||
foundTarget = true;
|
||||
|
||||
// 如果没有指定 key,找到活动名称就直接返回成功
|
||||
if (!key) {
|
||||
log.info(`已找到指定活动 [${activityName}],无需匹配关键字`);
|
||||
findActivity.key = true;
|
||||
// 如果重合度超过70%,认为已到底部
|
||||
if (overlapRatio >= 0.7) {
|
||||
log.info(`检测到当前页与上一页高度重复(重合率 ${Math.round(overlapRatio * 100)}%),已到达底部,停止扫描`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
previousPageActivities = currentPageNames; // 更新上一页活动集合
|
||||
// ===================================
|
||||
|
||||
// 如果指定了 key,进行关键字匹配
|
||||
const text = await OcrKey(activityName, key);
|
||||
if (text && text.includes(key)) {
|
||||
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}]`);
|
||||
findActivity.key = true;
|
||||
if (value) {
|
||||
findActivity.value = text.includes(value)
|
||||
let currentPageBottomName = null; // 当前页底部活动名称
|
||||
let foundTarget = false; // 是否找到目标活动标志
|
||||
|
||||
// 遍历当前页所有识别到的活动条目
|
||||
for (let res of resList) {
|
||||
const activityName = res.text.trim(); // 获取活动名称
|
||||
currentPageBottomName = activityName; // 更新底部活动名
|
||||
|
||||
// 【关键修改】检查是否是目标活动名称(精确匹配)
|
||||
if (activityName.includes(name)) {
|
||||
findActivity.name = true; // 找到活动名称
|
||||
log.info(`找到目标活动:${activityName}`);
|
||||
await click(res.x, res.y); // 点击活动
|
||||
await sleep(ms); // 等待1秒
|
||||
foundTarget = true; // 设置找到目标标志
|
||||
|
||||
// 如果没有指定 key,找到活动名称就直接返回成功
|
||||
if (!key) {
|
||||
log.info(`已找到指定活动 [${activityName}],无需匹配关键字`);
|
||||
findActivity.key = true;
|
||||
break;
|
||||
}
|
||||
if (findActivity.value) {
|
||||
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}] 和值 [${value}]`);
|
||||
|
||||
// 如果指定了 key,进行关键字匹配
|
||||
const text = await Ocr.key(activityName, key); // OCR识别关键字
|
||||
if (text && text.includes(key)) {
|
||||
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}]`);
|
||||
findActivity.key = true; // 找到关键字
|
||||
if (value) {
|
||||
findActivity.value = text.includes(value) // 检查值
|
||||
}
|
||||
if (findActivity.value) {
|
||||
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}] 和值 [${value}]`);
|
||||
}
|
||||
} else {
|
||||
log.info(`活动 [${activityName}] 不包含关键字 [${key}],继续查找`);
|
||||
}
|
||||
} else {
|
||||
log.info(`活动 [${activityName}] 不包含关键字 [${key}],继续查找`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到目标活动,直接退出主循环
|
||||
if (foundTarget) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到目标活动,直接退出主循环
|
||||
if (foundTarget) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. 判断是否已到达页面底部(单一判断逻辑)
|
||||
if (currentPageBottomName && currentPageBottomName === lastPageBottomName) {
|
||||
sameBottomCount++;
|
||||
if (sameBottomCount >= sameBottomCountMax) {
|
||||
log.info(`连续 ${sameBottomCountMax} 次检测到相同底部活动,已确认到达页面最底部,扫描结束`);
|
||||
break;
|
||||
// 5. 判断是否已到达页面底部(单一判断逻辑)
|
||||
if (currentPageBottomName && currentPageBottomName === lastPageBottomName) {
|
||||
sameBottomCount++;
|
||||
if (sameBottomCount >= sameBottomCountMax) {
|
||||
log.info(`连续 ${sameBottomCountMax} 次检测到相同底部活动,已确认到达页面最底部,扫描结束`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
sameBottomCount = 0;
|
||||
}
|
||||
} else {
|
||||
sameBottomCount = 0;
|
||||
}
|
||||
lastPageBottomName = currentPageBottomName;
|
||||
lastPageBottomName = currentPageBottomName;
|
||||
|
||||
// 6. 向下滑动一页,继续下一轮
|
||||
await scrollPagesByActivity(false);
|
||||
await sleep(ms);
|
||||
} finally {
|
||||
if (captureRegion) {
|
||||
captureRegion.dispose();
|
||||
// 6. 向下滑动一页,继续下一轮
|
||||
await scroll.pagesByActivity(false);
|
||||
await sleep(ms);
|
||||
} finally {
|
||||
if (captureRegion) {
|
||||
captureRegion.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
return findActivity;
|
||||
}
|
||||
return findActivity;
|
||||
}
|
||||
|
||||
export class Ocr{
|
||||
/**
|
||||
* 在指定区域进行OCR识别并匹配关键词
|
||||
* @param {string} activityName - 活动名称,用于日志记录
|
||||
* @param {string} key - 要匹配的关键词
|
||||
* @param {Object} ocrRegion - OCR识别区域配置,默认为剩余时间区域
|
||||
* @param {number} ocrRegion.x - 识别区域左上角X坐标
|
||||
* @param {number} ocrRegion.y - 识别区域左上角Y坐标
|
||||
* @param {number} ocrRegion.width - 识别区域宽度
|
||||
* @param {number} ocrRegion.height - 识别区域高度
|
||||
* @returns {string|null} 匹配到的文本列表(用'<-->'连接),未匹配到则返回null
|
||||
*/
|
||||
static key(activityName, key, ocrRegion = ocrRegionConfig.remainingTime) {
|
||||
if (!key) {
|
||||
return null
|
||||
}
|
||||
let captureRegion = captureGameRegion();
|
||||
try {
|
||||
let list = new Array()
|
||||
const ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
|
||||
let resList = captureRegion.findMulti(ocrObject);
|
||||
|
||||
for (let res of resList) {
|
||||
log.debug(`[info][{key}]{activityName}--{time}`, key, activityName, res.text);
|
||||
if (res.text.includes(key)) {
|
||||
log.debug(`[{key}][命中]{activityName}--{time}`, key, activityName, res.text);
|
||||
list.push(res.text.trim())
|
||||
}
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
return list.join('<-->')
|
||||
}
|
||||
return null;
|
||||
|
||||
} finally {
|
||||
captureRegion.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findStygianOnslaught() {
|
||||
const findActivity = {
|
||||
@@ -355,8 +376,8 @@ async function findStygianOnslaught() {
|
||||
key: "紊乱爆发期",
|
||||
value: "已结束",
|
||||
}
|
||||
const findResult = await scrollFindActivity(findActivity.name, findActivity.key, findActivity.value);
|
||||
if((!findResult.name)||(findResult.name && findResult.key && findResult.value) ){
|
||||
const findResult = await scroll.findActivity(findActivity.name, findActivity.key, findActivity.value);
|
||||
if ((!findResult.name) || (findResult.name && findResult.key && findResult.value)) {
|
||||
// 幽境危战 紊乱爆发期 已结束
|
||||
return false
|
||||
}
|
||||
|
||||
736
repo/js/AutoPlan/utils/load_check_run.js
Normal file
736
repo/js/AutoPlan/utils/load_check_run.js
Normal file
@@ -0,0 +1,736 @@
|
||||
import {config, LoadType} from "../config/config";
|
||||
import {Physical} from "./physical";
|
||||
import {getDayOfWeek, outDomainUI, outStygianOnslaughtUI, parseInteger, throwError,toMainUi} from "./tool";
|
||||
import {findStygianOnslaught} from "./activity";
|
||||
import {pullJsonConfig} from "./bgi_tools";
|
||||
|
||||
/*===========================================[check]===========================================*/
|
||||
/**
|
||||
* 检查并过滤幽境危战紊乱爆发期的任务列表
|
||||
* @param {Array} list - 任务列表
|
||||
* @returns {Promise<Array>} - 处理后的任务列表
|
||||
*/
|
||||
export async function checkAndFilterStygianOnslaught(list) {
|
||||
// 检查列表中是否存在幽境危战任务
|
||||
const hasStygianOnslaught = list.some(item => item.runType === config.user.runTypes[2]);
|
||||
if (hasStygianOnslaught) {
|
||||
// 记录日志:检查幽境危战紊乱爆发期开放
|
||||
log.info(`{0}`, `检查幽境危战紊乱爆发期开放`)
|
||||
try {
|
||||
// 切换到主界面
|
||||
await toMainUi()
|
||||
// 查找当前是否有幽境危战
|
||||
const isStygianOnslaught = await findStygianOnslaught();
|
||||
if (isStygianOnslaught) {
|
||||
// 获取所有没有排序的圣遗物秘境名称
|
||||
//圣遗物秘境名称
|
||||
const holyRelicDomainNames = config.domainList.filter(item => !item.hasOrder).map(item => item.name);
|
||||
const filter = list.find(item => item.runType === config.user.runTypes[0] && holyRelicDomainNames.includes(item.autoFight?.domainName));
|
||||
if (filter) {
|
||||
// 幽境危战添加秘境顺序前
|
||||
list.forEach(item => {
|
||||
if (item.runType === config.user.runTypes[2]) {
|
||||
item.order = Math.max(filter.order + 1, item.order)
|
||||
}
|
||||
})
|
||||
list.sort((item1, item2) => item2.order - item1.order)
|
||||
}
|
||||
log.info(`{0}`, `幽境危战紊乱爆发期已开启`)
|
||||
} else {
|
||||
log.info(`{0}`, `幽境危战紊乱爆发期已结束`)
|
||||
list = list.filter(item => item.runType !== config.user.runTypes[2])
|
||||
}
|
||||
return list
|
||||
} finally {
|
||||
await toMainUi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================[load]===========================================*/
|
||||
/**
|
||||
* 根据不同的加载方式加载秘境配置
|
||||
* @param {string} Load - 加载方式类型,如uid或input
|
||||
* @param {Set} autoOrderSet - 用于存储秘境顺序的Set集合
|
||||
* @param {string} runConfig - 输入的配置字符串,仅在Load为input时使用
|
||||
*/
|
||||
export async function loadMode(Load, autoOrderSet, runConfig) {
|
||||
switch (Load) {
|
||||
case LoadType.input:
|
||||
// 通过输入字符串方式加载配置
|
||||
if (runConfig) {
|
||||
// 处理输入字符串:去除首尾空格,将中文逗号替换为英文逗号,然后按逗号分割
|
||||
runConfig.trim().replaceAll(',', ',').split(",").forEach(
|
||||
item => {
|
||||
let {arr, index, runType, autoOrder} = Base.buildOrder(item);
|
||||
|
||||
if (!config.user.runTypes.includes(runType)) {
|
||||
throwError(`运行类型${runType}输入错误`)
|
||||
} else if (config.user.runTypes[0] === runType) {
|
||||
const __ret = Domain.build(arr, index);
|
||||
let autoFight = __ret.autoFight;
|
||||
index = __ret.index;
|
||||
autoOrder.autoFight = autoFight // 将秘境信息对象添加到秘境顺序对象中
|
||||
} else if (config.user.runTypes[1] === runType) {
|
||||
const __ret = LeyLineOutcrop.build(arr, index);
|
||||
let autoLeyLineOutcrop = __ret.autoLeyLineOutcrop;
|
||||
index = __ret.index;
|
||||
|
||||
autoOrder.autoLeyLineOutcrop = autoLeyLineOutcrop // 将地脉信息对象添加到顺序对象中
|
||||
} else if (config.user.runTypes[2] === runType) {
|
||||
const __ret = StygianOnslaught.build(arr,index);
|
||||
let autoStygianOnslaught = __ret.autoStygianOnslaught;
|
||||
index = __ret.index;
|
||||
autoOrder.autoStygianOnslaught = autoStygianOnslaught
|
||||
}
|
||||
|
||||
// 将秘境顺序对象添加到列表中
|
||||
autoOrderSet.add(autoOrder)
|
||||
}
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case LoadType.uid:
|
||||
await toMainUi()
|
||||
// 通过UID方式加载配置
|
||||
const uid = config.user.uid || (await genshin.uid()) // 获取用户UID,如果未配置则通过OCR识别获取
|
||||
// const configAutoFightOrderMap = JSON.parse(file.readTextSync(config.path.runConfig)) || new Map() // 读取本地配置文件并转换为Map对象
|
||||
// const uidConfigList = configAutoFightOrderMap.get(uid + "") || []; // 获取当前UID对应的配置列表
|
||||
|
||||
const configAutoFightOrderMap = JSON.parse(file.readTextSync(config.path.runConfig)) || {} // 读取本地配置文件
|
||||
const uidConfigList = configAutoFightOrderMap[uid + ""] || []; // 获取当前UID对应的配置列表
|
||||
if (uidConfigList?.length > 0) {
|
||||
// 如果配置列表不为空,遍历并添加到结果集合中
|
||||
uidConfigList.forEach(item => {
|
||||
// 将秘境顺序对象添加到列表中
|
||||
// 主逻辑优化
|
||||
// if (item.day !== undefined) {
|
||||
// item.day = parseInteger(item.day);
|
||||
// }
|
||||
if (item.days && item.days.length > 0) {
|
||||
item.days = item.days.map(day => parseInteger(day))
|
||||
// item.day = parseInteger(item.day);
|
||||
}
|
||||
autoOrderSet.add(item)
|
||||
})
|
||||
}
|
||||
break
|
||||
case LoadType.bgi_tools:
|
||||
// 通过bgi_tools方式加载配置
|
||||
log.info(`开始拉取bgi_tools配置`)
|
||||
const uidConfigListBgiTools = await pullJsonConfig(config.user.uid + '', config.bgi_tools.api.httpPullJsonConfig) || []
|
||||
if (uidConfigListBgiTools?.length > 0) {
|
||||
// 如果配置列表不为空,遍历并添加到结果集合中
|
||||
uidConfigListBgiTools.forEach(item => {
|
||||
// 将秘境顺序对象添加到列表中
|
||||
// 主逻辑优化
|
||||
if (item.days && item.days.length > 0) {
|
||||
item.days = item.days.map(day => parseInteger(day))
|
||||
// item.day = parseInteger(item.day);
|
||||
}
|
||||
autoOrderSet.add(item)
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error("请先配置加载方式");
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化执行顺序列表
|
||||
* @param {string} domainConfig - 输入的字符串,包含秘境顺序信息
|
||||
* @returns {Array} 返回处理后的秘境顺序列表
|
||||
*/
|
||||
export async function initRunOrderList(domainConfig) {
|
||||
const autoFightOrderSet = new Set() // 存储秘境顺序列表的数组
|
||||
/* let te = {
|
||||
order: 1, // 顺序值
|
||||
day: 0,// 执行日期
|
||||
autoFight: {
|
||||
domainName: undefined,//秘境名称
|
||||
partyName: undefined,//队伍名称
|
||||
sundaySelectedValue: undefined,//周日|限时选择的值
|
||||
domainRoundNum: undefined,//副本轮数
|
||||
} // 秘境信息对象
|
||||
}*/
|
||||
// let Load = LoadType.uid
|
||||
|
||||
for (const Load of config.run.loads) {
|
||||
await loadMode(Load.load, autoFightOrderSet, domainConfig);
|
||||
}
|
||||
|
||||
// 检查是否已配置秘境
|
||||
if (!autoFightOrderSet || autoFightOrderSet.size <= 0) {
|
||||
throw new Error("请先配置体力配置");
|
||||
}
|
||||
// 返回处理后的秘境顺序列表
|
||||
let from = Array.from(autoFightOrderSet);
|
||||
let dayOfWeek = await getDayOfWeek();
|
||||
log.debug(`old-from:{0}`, JSON.stringify(from))
|
||||
from = from
|
||||
//过滤掉不执行的秘境
|
||||
.filter(item => config.user.runTypes.includes(item.runType))
|
||||
.filter(item => {
|
||||
// if (item.day) {
|
||||
// return item.day === dayOfWeek.day
|
||||
// }
|
||||
log.debug(`[{1}]item.days.length:{0}`, dayOfWeek.day, item?.days?.length || 0)
|
||||
if (item.days && item.days.length > 0) {
|
||||
const includes = item.days.includes(dayOfWeek.day);
|
||||
log.debug(`[{1}]item.days:{0}`, dayOfWeek.day, JSON.stringify(item.days))
|
||||
return includes;
|
||||
}
|
||||
return true
|
||||
})
|
||||
from.sort((a, b) => b.order - a.order)
|
||||
log.debug(`from:{0}`, JSON.stringify(from))
|
||||
return from;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动执行列表处理函数
|
||||
* @param {Array} autoRunOrderList - 包含自动配置的数组
|
||||
*/
|
||||
export async function autoRunList(autoRunOrderList) {
|
||||
//计划执行
|
||||
for (const item of autoRunOrderList) {
|
||||
await sleep(3000)
|
||||
if (item.runType === config.user.runTypes[0]) {
|
||||
await Domain.run(item.autoFight);
|
||||
} else if (item.runType === config.user.runTypes[1]) {
|
||||
await LeyLineOutcrop.run(item.autoLeyLineOutcrop);
|
||||
} else if (item.runType === config.user.runTypes[2]) {
|
||||
await StygianOnslaught.run(item.autoStygianOnslaught);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*===========================================[class]===========================================*/
|
||||
class Base{
|
||||
static buildOrder(item) {
|
||||
// 将当前项按"|"分割成数组
|
||||
let arr = item.split("|")
|
||||
// 类型|执行日期|执行顺序
|
||||
let index = 0
|
||||
let runType = arr[index]; // 解析运行类型
|
||||
index++
|
||||
const rawDays = arr[index];
|
||||
let days = (rawDays != null && String(rawDays).trim() !== "")
|
||||
? String(rawDays).split('/').map(d => parseInt(d.trim(), 10)).filter(d => !isNaN(d))
|
||||
: [];
|
||||
// let days = arr[index].trim() !== ""
|
||||
// ? arr[index].split('/').map(d => parseInt(d.trim())).filter(d => !isNaN(d))
|
||||
// : [];
|
||||
index++
|
||||
// 解析顺序值,处理可能的无效值
|
||||
let order = (() => {
|
||||
const rawOrder = arr[index]; // 获取原始值
|
||||
if (rawOrder == null || String(rawOrder).trim() === "") {
|
||||
return 0; // 若为空或无效值,默认返回 0
|
||||
}
|
||||
const parsedOrder = parseInt(String(rawOrder).trim(), 10); // 转换为整数
|
||||
return isNaN(parsedOrder) ? 0 : parsedOrder; // 若转换失败,返回默认值 0
|
||||
})();
|
||||
index++
|
||||
|
||||
// 创建秘境顺序对象
|
||||
let autoOrder = {
|
||||
order: order, // 顺序值
|
||||
// day: day,// 执行日期
|
||||
runType: runType, // 运行类型
|
||||
days: days, // 执行日期(数组)
|
||||
autoFight: undefined, // 秘境信息对象
|
||||
autoLeyLineOutcrop: undefined, // 地脉信息对象
|
||||
autoStygianOnslaught: undefined // 幽境信息对象
|
||||
}
|
||||
return {arr, index, runType, autoOrder};
|
||||
}
|
||||
static build(arr, index) {
|
||||
throw new Error("未实现build方法");
|
||||
}
|
||||
|
||||
static async run(object) {
|
||||
throw new Error("未实现run方法");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Domain类,用于处理秘境相关的操作
|
||||
*/
|
||||
class Domain extends Base{
|
||||
/**
|
||||
* 构建秘境信息对象
|
||||
* @param {Array} arr - 包含秘境信息的数组
|
||||
* @param {number} index - 当前解析的位置索引
|
||||
* @returns {Object} 返回包含秘境信息对象和更新后的索引
|
||||
*/
|
||||
static build(arr, index) {
|
||||
// 创建秘境信息对象,初始化默认值
|
||||
let autoFight = {
|
||||
domainName: undefined,//秘境名称
|
||||
partyName: undefined,//队伍名称
|
||||
sundaySelectedValue: 1,//周日|限时选择的值,默认为1
|
||||
domainRoundNum: 0,//副本轮数,默认为0
|
||||
}
|
||||
|
||||
//"|队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日,..."
|
||||
let partyName = arr[index]; // 解析队伍名称
|
||||
index++
|
||||
let domainName = arr[index]; // 解析秘境名称
|
||||
index++
|
||||
let domainRoundNum = arr[index]; // 解析副本轮数
|
||||
index++
|
||||
let sundaySelectedValue = "1"
|
||||
if (index <= arr.length - 1)
|
||||
sundaySelectedValue = arr[index]; // 解析周日|限时选择的值
|
||||
|
||||
// 检查秘境名称是否有效
|
||||
if (!config.domainNames.has(domainName)) {
|
||||
//秘境名称没有记录 查询是否是物品名称
|
||||
if (config.itemNames.has(domainName)) {
|
||||
const domainNameTemp = config.domainItemsMap.get(domainName);
|
||||
if (!domainNameTemp) {
|
||||
throw new Error(`${domainName} 输入错误`);
|
||||
}
|
||||
if (index <= arr.length - 1) {
|
||||
const domainSelectedValue = parseInt(config.domainOrderMap.get(domainName) + "");
|
||||
sundaySelectedValue = domainSelectedValue
|
||||
}
|
||||
domainName = domainNameTemp
|
||||
} else {
|
||||
throw new Error(`${domainName} 输入错误`);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置秘境信息的各个属性
|
||||
autoFight.partyName = partyName // 队伍名称
|
||||
autoFight.domainName = domainName // 秘境名称
|
||||
autoFight.domainRoundNum = domainRoundNum // 副本轮数
|
||||
autoFight.sundaySelectedValue = sundaySelectedValue // 周日|限时选择的值
|
||||
return {autoFight, index};
|
||||
}
|
||||
/**
|
||||
* 执行秘境任务
|
||||
* @param {Object} autoFight - 包含秘境信息的对象
|
||||
*/
|
||||
static async run(autoFight) {
|
||||
log.info(`{0}`, "开始执行秘境任务")
|
||||
log.warn(`{0}`, "非体力耗尽情况下(受本体限制),等待退出秘境时间较长")
|
||||
// 创建秘境参数对象,初始化值为0
|
||||
let domainParam = new AutoDomainParam();
|
||||
//关闭榨干原粹树脂
|
||||
domainParam.specifyResinUse = true
|
||||
//定死做预留冗余 先不实现 不能指定次数 只能指定启用
|
||||
let physical_domain = autoFight?.physical
|
||||
// || [
|
||||
// {order: 0, name: config.user.physical.names[0], count: 1, open: true},
|
||||
// {order: 1, name: config.user.physical.names[1], count: 0, open: false},
|
||||
// {order: 2, name: config.user.physical.names[2], count: 0, open: false},
|
||||
// {order: 3, name: config.user.physical.names[3], count: 0, open: false},
|
||||
// ]
|
||||
|
||||
if ((!physical_domain) || physical_domain.filter(item => item?.open).length === 0) {
|
||||
const names = config.user.physical.names;
|
||||
physical_domain = []
|
||||
names.forEach((name, index) => {
|
||||
physical_domain.push({order: index, name: name, open: index === 0})
|
||||
})
|
||||
}
|
||||
|
||||
physical_domain.sort((a, b) => a.order - b.order)
|
||||
// 不包含原粹树脂的和
|
||||
const noOriginalSum = physical_domain.filter(item => item?.name.trim() !== config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;//求和
|
||||
// 只包含原粹树脂的和
|
||||
const originalSum = physical_domain.filter(item => item?.name?.trim() === config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;
|
||||
const resinPriorityList = physical_domain.filter(item => item?.open).map(item => item?.name?.trim())
|
||||
// /** 树脂使用优先级列表 */
|
||||
// resinPriorityList: string[];
|
||||
// /** 使用原粹树脂次数 */
|
||||
// originalResinUseCount: number;
|
||||
// /** 使用浓缩树脂次数 */
|
||||
// condensedResinUseCount: number;
|
||||
// /** 使用须臾树脂次数 */
|
||||
// transientResinUseCount: number;
|
||||
// /** 使用脆弱树脂次数 */
|
||||
// fragileResinUseCount: number;
|
||||
await sleep(1000)
|
||||
//流程->返回主页 打开地图 返回主页
|
||||
// const physicalOcr = await ocrPhysical(true, true)
|
||||
// config.user.physical.current = physicalOcr.current
|
||||
// config.user.physical.min = physicalOcr.min
|
||||
|
||||
const currentPhysical = await Physical.countAllResin()
|
||||
config.user.physical.current = currentPhysical.originalResinCount;
|
||||
|
||||
const physical = config.user.physical
|
||||
if (domainParam.specifyResinUse && physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) {
|
||||
throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`)
|
||||
}
|
||||
|
||||
//关闭分解
|
||||
domainParam.autoArtifactSalvage = false
|
||||
|
||||
//配置树脂使用优先级
|
||||
if (resinPriorityList.length > 0) {
|
||||
domainParam.SetResinPriorityList(...resinPriorityList)
|
||||
}
|
||||
// log.debug(`开始执行秘境任务`)
|
||||
//秘境名称
|
||||
domainParam.DomainName = autoFight.domainName || domainParam.DomainName;
|
||||
log.debug(`秘境名称:${domainParam.DomainName}`)
|
||||
|
||||
//队伍名称
|
||||
domainParam.PartyName = autoFight.partyName || domainParam.PartyName;
|
||||
log.debug(`队伍名称:${domainParam.PartyName}`)
|
||||
|
||||
if (autoFight.sundaySelectedValue) {
|
||||
//周日|限时选择的值
|
||||
domainParam.SundaySelectedValue = "" + (autoFight.sundaySelectedValue || domainParam.SundaySelectedValue);
|
||||
}
|
||||
log.debug(`周日|限时选择的值:${domainParam.SundaySelectedValue}`)
|
||||
//副本轮数
|
||||
try {
|
||||
domainParam.DomainRoundNum = parseInt((autoFight.domainRoundNum || domainParam.DomainRoundNum) + "");
|
||||
} catch (e) {
|
||||
log.debug(`副本轮数:${autoFight.domainRoundNum}`)
|
||||
throwError(e.message)
|
||||
}
|
||||
log.debug(`副本轮数:${domainParam.DomainRoundNum}`)
|
||||
try {
|
||||
// 复活重试
|
||||
for (let i = 0; i < config.run.retry_count; i++) {
|
||||
try {
|
||||
await dispatcher.RunAutoDomainTask(domainParam);
|
||||
// 其他场景不重试
|
||||
break;
|
||||
} catch (e) {
|
||||
const errorMessage = e.message
|
||||
// 只有选择了秘境的时候才会重试
|
||||
if (errorMessage.includes("复活") && domainParam.DomainName) {
|
||||
continue;
|
||||
}
|
||||
if (!config.run.exclude_run_exception || config.run.loop_plan) {//排除异常 与循环计划互斥
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
log.info(`{0}`, "执行完成")
|
||||
// 退出秘境
|
||||
await outDomainUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 地脉刷取任务类
|
||||
* 用于处理地脉刷取任务的构建和执行
|
||||
*/
|
||||
class LeyLineOutcrop extends Base{
|
||||
/**
|
||||
* 构建地脉刷取任务参数
|
||||
* @param {Array} arr - 输入参数数组,包含队伍名称、国家、刷取轮数等信息
|
||||
* @param {number} index - 当前处理的参数索引
|
||||
* @returns {Object} 返回包含构建好的参数对象和新的索引
|
||||
*/
|
||||
static build(arr, index) {
|
||||
// 注释解释了输入参数的格式
|
||||
//"|队伍名称|国家|刷几轮|花类型|好感队|是否使用脆弱树脂|是否使用须臾树脂|是否前往合成台合成浓缩树脂|是否使用冒险之证|发送详细通知|战斗超时时间,..."
|
||||
// 初始化地脉刷取任务参数对象
|
||||
let autoLeyLineOutcrop = {
|
||||
count: 0, // 刷几次(0=自动/无限)
|
||||
country: undefined, // 国家地区
|
||||
leyLineOutcropType: undefined, // 需映射为经验/摩拉
|
||||
useAdventurerHandbook: false, // 是否使用冒险之证
|
||||
friendshipTeam: "", // 好感队伍ID
|
||||
team: "", // 主队伍ID
|
||||
timeout: 120, // 超时时间(秒)
|
||||
isGoToSynthesizer: false, // 是否前往合成台
|
||||
useFragileResin: false, // 使用脆弱树脂
|
||||
useTransientResin: false, // 使用须臾树脂(须臾=Transient)
|
||||
isNotification: false // 是否通知
|
||||
}
|
||||
autoLeyLineOutcrop.team = arr[index]
|
||||
index++
|
||||
autoLeyLineOutcrop.country = arr[index]
|
||||
index++
|
||||
autoLeyLineOutcrop.count = arr[index]
|
||||
index++
|
||||
autoLeyLineOutcrop.leyLineOutcropType = arr[index]
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.friendshipTeam = arr[index]
|
||||
index++
|
||||
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.useFragileResin = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.useTransientResin = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.isGoToSynthesizer = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.useAdventurerHandbook = (arr[index] != null && arr[index].trim() !== "")
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.isNotification = (arr[index] != null && arr[index].trim() !== "")
|
||||
|
||||
index++
|
||||
if (index <= arr.length - 1)
|
||||
autoLeyLineOutcrop.timeout = parseInteger(arr[index])
|
||||
return {autoLeyLineOutcrop, index};
|
||||
}
|
||||
static async run(autoLeyLineOutcrop) {
|
||||
// autoLeyLineOutcrop = {
|
||||
// "count": 0,
|
||||
// "country": "country_cb3d792be8db",
|
||||
// "leyLineOutcropType": "leyLineOutcropType_f259b77fabcb",
|
||||
// // "isResinExhaustionMode": true,
|
||||
// // "openModeCountMin": true,
|
||||
// "useAdventurerHandbook": false,
|
||||
// "friendshipTeam": "friendshipTeam_7122cab56b16",
|
||||
// "team": "team_d0798ca3aa27",
|
||||
// "timeout": 0,
|
||||
// "isGoToSynthesizer": false,
|
||||
// "useFragileResin": false,
|
||||
// "useTransientResin": false,
|
||||
// "isNotification": false
|
||||
// }
|
||||
|
||||
|
||||
log.info(`{0}`, "开始执行地脉任务")
|
||||
// if (true) {
|
||||
// log.warn("地脉 暂不支持")
|
||||
// return
|
||||
// }
|
||||
let param = new AutoLeyLineOutcropParam(parseInteger(autoLeyLineOutcrop.count + ""), autoLeyLineOutcrop.country, autoLeyLineOutcrop.leyLineOutcropType);
|
||||
// let param = new AutoLeyLineOutcropParam();
|
||||
// param.count = parseInteger(autoLeyLineOutcrop.count+"");
|
||||
// param.country = autoLeyLineOutcrop.country;
|
||||
// param.leyLineOutcropType = autoLeyLineOutcrop.leyLineOutcropType;
|
||||
//和本体保持一致
|
||||
param.useAdventurerHandbook = !autoLeyLineOutcrop.useAdventurerHandbook;
|
||||
param.friendshipTeam = autoLeyLineOutcrop.friendshipTeam;
|
||||
param.team = autoLeyLineOutcrop.team;
|
||||
param.timeout = autoLeyLineOutcrop.timeout;
|
||||
param.isGoToSynthesizer = autoLeyLineOutcrop.isGoToSynthesizer;
|
||||
param.useFragileResin = autoLeyLineOutcrop.useFragileResin;
|
||||
param.useTransientResin = autoLeyLineOutcrop.useTransientResin;
|
||||
param.isNotification = autoLeyLineOutcrop.isNotification;
|
||||
|
||||
param.isResinExhaustionMode = true;
|
||||
param.openModeCountMin = true;
|
||||
await sleep(1000)
|
||||
// 复活重试
|
||||
for (let i = 0; i < config.run.retry_count; i++) {
|
||||
try {
|
||||
await dispatcher.RunAutoLeyLineOutcropTask(param);
|
||||
// 其他场景不重试
|
||||
break;
|
||||
} catch (e) {
|
||||
const errorMessage = e.message
|
||||
// 只有选择了秘境的时候才会重试
|
||||
if (errorMessage.includes("复活")) {
|
||||
continue;
|
||||
}
|
||||
if (!config.run.exclude_run_exception || config.run.loop_plan) {//排除异常 与循环计划互斥
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* StygianOnslaught 类,用于处理幽境任务的相关操作
|
||||
*/
|
||||
class StygianOnslaught extends Base{
|
||||
/**
|
||||
* 构建幽境任务参数
|
||||
* @param {Array} arr - 输入参数数组
|
||||
* @param {number} index - 当前处理的参数索引
|
||||
* @returns {Object} 包含构建的参数和更新后的索引
|
||||
*/
|
||||
static build(arr,index) {
|
||||
// 初始化幽境任务配置对象
|
||||
let autoStygianOnslaught = {
|
||||
bossNum: undefined,//boss1-3
|
||||
fightTeamName: "",//队伍名称
|
||||
specifyResinUse: undefined,//自定义树脂使用
|
||||
physical: [
|
||||
{order: 0, name: config.user.physical.names[1], open: true, count: 1},
|
||||
{order: 1, name: config.user.physical.names[0], open: true, count: 1},
|
||||
{order: 2, name: config.user.physical.names[2], open: false, count: 1},
|
||||
{order: 3, name: config.user.physical.names[3], open: false, count: 1}
|
||||
],//副本轮数
|
||||
}
|
||||
if (index <= arr.length - 1) {
|
||||
const bossNum = parseInteger(arr[index]);
|
||||
if (bossNum && bossNum > 0 && bossNum <= 3) {
|
||||
autoStygianOnslaught.bossNum = bossNum
|
||||
}
|
||||
}
|
||||
index++
|
||||
if (index <= arr.length - 1) {
|
||||
const fightTeamName = arr[index];
|
||||
if (fightTeamName && fightTeamName.trim() !== "") {
|
||||
autoStygianOnslaught.fightTeamName = fightTeamName
|
||||
}
|
||||
}
|
||||
index++
|
||||
if (index <= arr.length - 1) {
|
||||
autoStygianOnslaught.specifyResinUse = arr[index].trim() !== ""
|
||||
}
|
||||
if (autoStygianOnslaught.specifyResinUse) {
|
||||
index++
|
||||
let line = 0
|
||||
if (index <= arr.length - 1) {
|
||||
if (arr[index]?.trim() !== "") {
|
||||
const physical = []
|
||||
const physicals = arr[index].trim().split("/");
|
||||
for (let i = 0; i < physicals.length; i++) {
|
||||
const item = physicals[i];
|
||||
physical.push({order: i, name: item, open: true, count: 1})
|
||||
}
|
||||
line = physical.length
|
||||
autoStygianOnslaught.physical = physical
|
||||
}
|
||||
}
|
||||
index++
|
||||
if (index <= arr.length - 1) {
|
||||
if (line > 0 && arr[index]?.trim() !== "") {
|
||||
const counts = arr[index].trim().split("/")
|
||||
.map(item => {
|
||||
let count = parseInteger(item) || 1;
|
||||
return count
|
||||
});
|
||||
autoStygianOnslaught.physical.forEach((item, index) => {
|
||||
try {
|
||||
item.count = counts[index] || 1;
|
||||
} catch (e) {
|
||||
log.warn(`解析${item.name}数量失败`)
|
||||
throwError(`解析${item.name}数量失败`)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {autoStygianOnslaught, index};
|
||||
}
|
||||
static async run(autoStygianOnslaught) {
|
||||
// autoStygianOnslaught = {
|
||||
// /**boss 名字 1~3 */
|
||||
// bossNum: 1,
|
||||
// /**结束后是否自动分解圣遗物*/
|
||||
// autoArtifactSalvage: false,
|
||||
// /**指定树脂的使用次数*/
|
||||
// specifyResinUse: false,
|
||||
// /**自定义使用树脂优先级*/
|
||||
// resinPriorityList: [""],
|
||||
// /** 使用原粹树脂刷取副本次数*/
|
||||
// originalResinUseCount: 0,
|
||||
// /** 使用浓缩树脂刷取副本次数*/
|
||||
// condensedResinUseCount: 0,
|
||||
// /** 使用须臾树脂刷取副本次数*/
|
||||
// transientResinUseCount: 0,
|
||||
// /** 使用脆弱树脂刷取副本次数*/
|
||||
// fragileResinUseCount: 0,
|
||||
// /**指定战斗队伍*/
|
||||
// fightTeamName: undefined
|
||||
// }
|
||||
log.debug(`autoStygianOnslaught ={0}`, JSON.stringify(autoStygianOnslaught))
|
||||
log.info(`{0}`, "开始执行幽境任务")
|
||||
let param = new AutoStygianOnslaughtParam()
|
||||
param.specifyResinUse = autoStygianOnslaught?.specifyResinUse || param.specifyResinUse
|
||||
|
||||
//定死做预留冗余 先不实现 不能指定次数 只能指定启用
|
||||
let physical_domain = autoStygianOnslaught?.physical || []
|
||||
// || [
|
||||
// {order: 0, name: config.user.physical.names[0], count: 1, open: true},
|
||||
// {order: 1, name: config.user.physical.names[1], count: 0, open: false},
|
||||
// {order: 2, name: config.user.physical.names[2], count: 0, open: false},
|
||||
// {order: 3, name: config.user.physical.names[3], count: 0, open: false},
|
||||
// ]
|
||||
|
||||
if (param.specifyResinUse && ((!physical_domain) || physical_domain.filter(item => item?.open).length === 0)) {
|
||||
const names = config.user.physical.names;
|
||||
physical_domain = []
|
||||
names.forEach((name, index) => {
|
||||
physical_domain.push({order: index, name: name, open: index === 0, count: 1})
|
||||
})
|
||||
}
|
||||
|
||||
physical_domain?.sort((a, b) => a.order - b.order)
|
||||
// 不包含原粹树脂的和
|
||||
const noOriginalSum = physical_domain.filter(item => item?.name.trim() !== config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;//求和
|
||||
// 只包含原粹树脂的和
|
||||
const originalSum = physical_domain.filter(item => item?.name?.trim() === config.user.physical.names[0])
|
||||
.filter(item => item?.open).length;
|
||||
const physical_domain_filter = physical_domain.filter(item => item?.open);
|
||||
const resinPriorityList = physical_domain_filter.map(item => item?.name?.trim())
|
||||
// /** 树脂使用优先级列表 */
|
||||
// resinPriorityList: string[];
|
||||
// /** 使用原粹树脂次数 */
|
||||
// originalResinUseCount: number;
|
||||
// /** 使用浓缩树脂次数 */
|
||||
// condensedResinUseCount: number;
|
||||
// /** 使用须臾树脂次数 */
|
||||
// transientResinUseCount: number;
|
||||
// /** 使用脆弱树脂次数 */
|
||||
// fragileResinUseCount: number;
|
||||
await sleep(1000)
|
||||
//流程->返回主页 打开地图 返回主页
|
||||
// const physicalOcr = await ocrPhysical(true, true)
|
||||
// config.user.physical.current = physicalOcr.current
|
||||
// config.user.physical.min = physicalOcr.min
|
||||
const currentPhysical = await Physical.countAllResin()
|
||||
config.user.physical.current = currentPhysical.originalResinCount;
|
||||
|
||||
const physical = config.user.physical
|
||||
if (physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) {
|
||||
throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`)
|
||||
}
|
||||
|
||||
|
||||
param.bossNum = autoStygianOnslaught?.bossNum > 0 && autoStygianOnslaught?.bossNum <= 3 ? autoStygianOnslaught.bossNum : param.bossNum
|
||||
param.fightTeamName = autoStygianOnslaught?.fightTeamName?.trim() !== "" ? autoStygianOnslaught.fightTeamName.trim() : param.fightTeamName
|
||||
if (resinPriorityList.length > 0) {
|
||||
param.SetResinPriorityList(...resinPriorityList)
|
||||
param.originalResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[0] && item?.open)?.count || 0
|
||||
param.condensedResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[1] && item?.open)?.count || 0
|
||||
param.transientResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[2] && item?.open)?.count || 0
|
||||
param.fragileResinUseCount = physical_domain_filter.find(item => item?.name?.trim() === config.user.physical.names[3] && item?.open)?.count || 0
|
||||
}
|
||||
|
||||
await sleep(1000)
|
||||
try {
|
||||
// 复活重试
|
||||
for (let i = 0; i < config.run.retry_count; i++) {
|
||||
try {
|
||||
await dispatcher.RunAutoStygianOnslaughtTask(param)
|
||||
// 其他场景不重试
|
||||
break;
|
||||
} catch (e) {
|
||||
const errorMessage = e.message
|
||||
if (errorMessage.includes("复活")) {
|
||||
continue;
|
||||
}
|
||||
if (!config.run.exclude_run_exception || config.run.loop_plan) {//排除异常 与循环计划互斥
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
log.info(`{0}`, "执行完成")
|
||||
// 退出危战
|
||||
await outStygianOnslaughtUI()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,427 +43,407 @@ const CONFIG = {
|
||||
};
|
||||
|
||||
//====================================================
|
||||
|
||||
|
||||
/**
|
||||
* 从字符串中提取数字并组合成一个整数
|
||||
* @param {string} str - 包含数字的字符串
|
||||
* @returns {number} - 由字符串中所有数字组合而成的整数
|
||||
*/
|
||||
async function saveOnlyNumber(str, defaultValue = 0) {
|
||||
// 使用正则表达式匹配字符串中的所有数字
|
||||
// \d+ 匹配一个或多个数字
|
||||
// .join('') 将匹配到的数字数组连接成一个字符串
|
||||
// parseInt 将连接后的字符串转换为整数
|
||||
try {
|
||||
return parseInt(str.match(/\d+/g).join(''));
|
||||
} catch (e) {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别原粹树脂(体力)的函数
|
||||
* @param {boolean} [opToMainUi=false] - 是否操作到主界面
|
||||
* @param {boolean} [openMap=false] - 是否打开地图界面
|
||||
* @param {number} [minPhysical=20] - 最小可执行体力值
|
||||
* @param {boolean} [isResinExhaustionMode=true] - 是否启用体力识别功能
|
||||
* @returns {Promise<Object>} 返回一个包含识别结果的Promise对象
|
||||
* - ok {boolean}: 是否可执行(体力是否足够)
|
||||
* - min {number}: 最小可执行体力值
|
||||
* - current {number}: 当前剩余体力值
|
||||
*/
|
||||
async function ocrPhysical(opToMainUi = false, openMap = false, minPhysical = 20, isResinExhaustionMode = true) {
|
||||
// 检查是否启用体力识别功能,如果未启用则直接返回默认结果
|
||||
if (!isResinExhaustionMode) {
|
||||
log.info(`===未启用===`)
|
||||
return {
|
||||
ok: true,
|
||||
min: 0,
|
||||
current: 0,
|
||||
export class Physical {
|
||||
/**
|
||||
* 从字符串中提取数字并转换为整数
|
||||
* @param {string} str - 需要处理的字符串
|
||||
* @param {number} [defaultValue=0] - 当无法提取数字时的默认返回值
|
||||
* @returns {number} 返回提取到的数字,如果无法提取则返回默认值
|
||||
*/
|
||||
static async saveOnlyNumber(str, defaultValue = 0) {
|
||||
// 使用正则表达式匹配字符串中的所有数字
|
||||
// \d+ 匹配一个或多个数字
|
||||
// .join('') 将匹配到的数字数组连接成一个字符串
|
||||
// parseInt 将连接后的字符串转换为整数
|
||||
try {
|
||||
return parseInt(str.match(/\d+/g).join(''));
|
||||
} catch (e) {
|
||||
// 如果发生异常(例如输入不是字符串或无法匹配数字),返回默认值
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
log.debug(`===开始识别原粹树脂===`)
|
||||
let ms = 1000 // 定义操作延迟时间(毫秒)
|
||||
if (opToMainUi) {
|
||||
await sleep(ms)
|
||||
await toMainUi(); // 切换到主界面
|
||||
}
|
||||
|
||||
if (openMap) {
|
||||
await sleep(ms)
|
||||
//打开地图界面
|
||||
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: 1373,
|
||||
y: 22,
|
||||
width: 52,
|
||||
height: 49,
|
||||
}
|
||||
//
|
||||
// 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 deriveCrop = regionA.DeriveCrop(add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height);
|
||||
// try {
|
||||
// let buttonA = regionA.find(templateMatchAddButtonRo);
|
||||
//
|
||||
// await sleep(ms)
|
||||
// if (!buttonA.isExist()) {
|
||||
// log.error(`${add_objJson.path}匹配异常`)
|
||||
// throwError(`${add_objJson.path}匹配异常`)
|
||||
// }
|
||||
// await buttonA.click()
|
||||
// } finally {
|
||||
// // deriveCrop.dispose()
|
||||
// regionA.dispose()
|
||||
// }
|
||||
const addClick = await findImgAndClick(`${add_objJson.path}`, 1248, 21, 50, 50);
|
||||
if (addClick === null) {
|
||||
log.error(`${add_objJson.path}匹配异常`)
|
||||
return undefined
|
||||
}
|
||||
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
|
||||
try {
|
||||
button = region.find(templateMatchButtonRo);
|
||||
await sleep(ms)
|
||||
if ((!button) || !button.isExist()) {
|
||||
log.error(`${tmJson.path} 匹配异常`)
|
||||
// throwError(`${tmJson.path} 匹配异常`)
|
||||
return undefined
|
||||
/**
|
||||
* 识别游戏中原粹树脂(体力)的功能函数
|
||||
* @param {boolean} [opToMainUi=false] - 是否操作返回主界面
|
||||
* @param {boolean} [openMap=false] - 是否打开地图界面
|
||||
* @param {number} [minPhysical=20] - 最小可执行体力值
|
||||
* @param {boolean} [isResinExhaustionMode=true] - 是否启用体力识别模式
|
||||
* @returns {Promise<Object|undefined>} 返回包含识别结果的对象或undefined
|
||||
*/
|
||||
static async ocrPhysical(opToMainUi = false, openMap = false, minPhysical = 20, isResinExhaustionMode = true) {
|
||||
// 检查是否启用体力识别功能,如果未启用则直接返回默认结果
|
||||
if (!isResinExhaustionMode) {
|
||||
log.info(`===未启用===`)
|
||||
return {
|
||||
ok: true,
|
||||
min: 0,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
region.dispose()
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
// y: 32,
|
||||
y: button.y,
|
||||
// width: 61,
|
||||
width: Math.abs(genshinJson.width - button.x - button.width),
|
||||
height: 26
|
||||
}
|
||||
|
||||
log.debug(`ocr_obj: x={x},y={y},width={width},height={height}`, ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height)
|
||||
let region3 = captureGameRegion()
|
||||
|
||||
try {
|
||||
let recognitionObjectOcr = RecognitionObject.ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height);
|
||||
let res = region3.find(recognitionObjectOcr);
|
||||
|
||||
log.debug(`[OCR原粹树脂]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`);
|
||||
let text = res.text.split('/')[0]
|
||||
let current = await saveOnlyNumber(text)
|
||||
let execute = (current - minPhysical) >= 0
|
||||
log.debug(`最小可执行原粹树脂:{min},原粹树脂:{key}`, minPhysical, current,)
|
||||
|
||||
// await keyPress('VK_ESCAPE')
|
||||
return {
|
||||
ok: execute,
|
||||
min: minPhysical,
|
||||
current: current,
|
||||
}
|
||||
} catch (e) {
|
||||
// throwError(`识别失败,err:${e.message}`)
|
||||
log.error(`识别失败,err:${e.message}`)
|
||||
return undefined
|
||||
} finally {
|
||||
region3.dispose()
|
||||
//返回地图操作
|
||||
log.debug(`===开始识别原粹树脂===`)
|
||||
let ms = 1000 // 定义操作延迟时间(毫秒)
|
||||
// 如果需要操作返回主界面
|
||||
if (opToMainUi) {
|
||||
await sleep(ms)
|
||||
await toMainUi(); // 切换到主界面
|
||||
}
|
||||
|
||||
// 如果需要打开地图界面
|
||||
if (openMap) {
|
||||
await sleep(ms)
|
||||
//打开地图界面
|
||||
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: 1373,
|
||||
y: 22,
|
||||
width: 52,
|
||||
height: 49,
|
||||
}
|
||||
//
|
||||
// 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 deriveCrop = regionA.DeriveCrop(add_objJson.x, add_objJson.y, add_objJson.width, add_objJson.height);
|
||||
// try {
|
||||
// let buttonA = regionA.find(templateMatchAddButtonRo);
|
||||
//
|
||||
// await sleep(ms)
|
||||
// if (!buttonA.isExist()) {
|
||||
// log.error(`${add_objJson.path}匹配异常`)
|
||||
// throwError(`${add_objJson.path}匹配异常`)
|
||||
// }
|
||||
// await buttonA.click()
|
||||
// } finally {
|
||||
// // deriveCrop.dispose()
|
||||
// regionA.dispose()
|
||||
// }
|
||||
const addClick = await findImgAndClick(`${add_objJson.path}`, 1248, 21, 50, 50);
|
||||
if (addClick === null) {
|
||||
log.error(`${add_objJson.path}匹配异常`)
|
||||
return undefined
|
||||
}
|
||||
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
|
||||
try {
|
||||
button = region.find(templateMatchButtonRo);
|
||||
await sleep(ms)
|
||||
if ((!button) || !button.isExist()) {
|
||||
log.error(`${tmJson.path} 匹配异常`)
|
||||
// throwError(`${tmJson.path} 匹配异常`)
|
||||
return undefined
|
||||
}
|
||||
} finally {
|
||||
region.dispose()
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
// y: 32,
|
||||
y: button.y,
|
||||
// width: 61,
|
||||
width: Math.abs(genshinJson.width - button.x - button.width),
|
||||
height: 26
|
||||
}
|
||||
|
||||
log.debug(`ocr_obj: x={x},y={y},width={width},height={height}`, ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height)
|
||||
let region3 = captureGameRegion()
|
||||
|
||||
try {
|
||||
let recognitionObjectOcr = RecognitionObject.ocr(ocr_obj.x, ocr_obj.y, ocr_obj.width, ocr_obj.height);
|
||||
let res = region3.find(recognitionObjectOcr);
|
||||
|
||||
log.debug(`[OCR原粹树脂]识别结果: ${res.text}, 原始坐标: x=${res.x}, y=${res.y},width:${res.width},height:${res.height}`);
|
||||
let text = res.text.split('/')[0]
|
||||
let current = await Physical.saveOnlyNumber(text)
|
||||
let execute = (current - minPhysical) >= 0
|
||||
log.debug(`最小可执行原粹树脂:{min},原粹树脂:{key}`, minPhysical, current,)
|
||||
|
||||
// await keyPress('VK_ESCAPE')
|
||||
return {
|
||||
ok: execute,
|
||||
min: minPhysical,
|
||||
current: current,
|
||||
}
|
||||
} catch (e) {
|
||||
// throwError(`识别失败,err:${e.message}`)
|
||||
log.error(`识别失败,err:${e.message}`)
|
||||
return undefined
|
||||
} finally {
|
||||
region3.dispose()
|
||||
//返回地图操作
|
||||
if (opToMainUi) {
|
||||
await toMainUi(); // 切换到主界面
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ==================== UI操作函数 ====================
|
||||
|
||||
/**
|
||||
* 打开并设置地图界面
|
||||
*/
|
||||
async function openMap() {
|
||||
log.info("打开地图界面");
|
||||
await 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("地图界面设置完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计所有树脂数量的主函数
|
||||
* @returns {Object} 包含所有树脂数量的对象
|
||||
*/
|
||||
async function countAllResin() {
|
||||
let shouldRestoreMainUi = false
|
||||
try {
|
||||
// setGameMetrics(1920, 1080, 1);
|
||||
// log.info("开始统计树脂数量");
|
||||
let resinCounts = {
|
||||
original: 0,
|
||||
transient: undefined,
|
||||
fragile: undefined,
|
||||
condensed: undefined
|
||||
}
|
||||
await toMainUi();
|
||||
/**
|
||||
* 打开地图界面的静态异步方法
|
||||
* 该方法用于在游戏中打开地图并进行相关设置
|
||||
*/
|
||||
static async openMap() {
|
||||
// 记录日志信息,表示正在打开地图界面
|
||||
log.info("打开地图界面");
|
||||
// 模拟按下M键打开地图
|
||||
await keyPress("M");
|
||||
// 等待UI界面加载完成,等待时间由配置文件中的UI_DELAY决定
|
||||
await sleep(CONFIG.UI_DELAY);
|
||||
shouldRestoreMainUi = true
|
||||
// 打开地图界面统计原粹/浓缩树脂
|
||||
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();
|
||||
|
||||
// 切换到国家选择界面
|
||||
// click(CONFIG.COORDINATES.MAP_SWITCH.x, CONFIG.COORDINATES.MAP_SWITCH.y);
|
||||
// await sleep(CONFIG.UI_DELAY);
|
||||
|
||||
// 点击避免选中效果影响统计
|
||||
// click(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y);
|
||||
// await sleep(500);
|
||||
// 选择蒙德
|
||||
// click(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y);
|
||||
// await sleep(CONFIG.UI_DELAY);
|
||||
// await switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y)
|
||||
|
||||
// log.info("开始统计补充树脂界面中的树脂");
|
||||
// resinCounts.transient = await countTransientResin();
|
||||
// resinCounts.fragile = await countFragileResin();
|
||||
// }
|
||||
// 显示结果
|
||||
displayResults(resinCounts);
|
||||
// 设置地图缩放级别,排除识别干扰
|
||||
await genshin.setBigMapZoomLevel(CONFIG.MAP_ZOOM_LEVEL);
|
||||
log.info("地图界面设置完成");
|
||||
}
|
||||
|
||||
// 返回主界面
|
||||
await genshin.returnMainUi();
|
||||
/**
|
||||
* 统计游戏中的所有树脂类型的数量
|
||||
* 包括原粹树脂、浓缩树脂、须臾树脂和脆弱树脂
|
||||
* @returns {Promise<Object>} 返回包含各种树脂数量的对象
|
||||
*/
|
||||
static async countAllResin() {
|
||||
let shouldRestoreMainUi = false // 标记是否需要恢复主界面
|
||||
try {
|
||||
// setGameMetrics(1920, 1080, 1); // 设置游戏显示参数
|
||||
// log.info("开始统计树脂数量"); // 记录开始统计的日志
|
||||
let resinCounts = { // 存储各种树脂数量的对象
|
||||
original: 0, // 原粹树脂数量
|
||||
transient: undefined, // 须臾树脂数量
|
||||
fragile: undefined, // 脆弱树脂数量
|
||||
condensed: undefined // 浓缩树脂数量
|
||||
}
|
||||
await toMainUi(); // 切换到主界面
|
||||
await sleep(CONFIG.UI_DELAY); // 等待界面加载
|
||||
shouldRestoreMainUi = true // 设置需要恢复主界面的标志
|
||||
// 打开地图界面统计原粹/浓缩树脂
|
||||
await Physical.openMap(); // 打开地图界面
|
||||
await sleep(CONFIG.UI_DELAY); // 等待界面加载
|
||||
let tryPass = true; // 标记第一次尝试是否成功
|
||||
try {
|
||||
// log.info("[开始]统计补充树脂界面中的树脂"); // 记录开始统计的日志
|
||||
resinCounts.original = await Physical.countOriginalResin(false, false); // 统计原粹树脂数量
|
||||
moveMouseTo(CONFIG.COORDINATES.AVOID_SELECTION.x, CONFIG.COORDINATES.AVOID_SELECTION.y) // 移动鼠标到指定位置
|
||||
await sleep(500); // 等待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 Physical.switchtoCountrySelection(CONFIG.COORDINATES.MONDSTADT.x, CONFIG.COORDINATES.MONDSTADT.y) // 切换到蒙德
|
||||
resinCounts.original = await Physical.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); // 等待500毫秒
|
||||
|
||||
// log.info("开始统计补充树脂界面中的树脂"); // 记录开始统计的日志
|
||||
// resinCounts.transient = await countTransientResin(); // 统计须臾树脂数量
|
||||
// resinCounts.fragile = await countFragileResin(); // 统计脆弱树脂数量
|
||||
// }
|
||||
// 显示结果
|
||||
Physical.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; // 抛出异常
|
||||
} finally { // 无论是否发生异常都会执行
|
||||
if (shouldRestoreMainUi) { // 如果需要恢复主界面
|
||||
await toMainUi(); // 切换到主界面
|
||||
await sleep(CONFIG.UI_DELAY); // 等待界面加载
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到国家选择界面的异步方法
|
||||
* @param {number} x - 目标位置的x坐标
|
||||
* @param {number} y - 目标位置的y坐标
|
||||
* @returns {Promise<void>} - 返回一个Promise,表示异步操作的完成
|
||||
*/
|
||||
static async 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);
|
||||
}
|
||||
|
||||
log.info("树脂统计完成");
|
||||
return {
|
||||
originalResinCount: resinCounts.original,
|
||||
condensedResinCount: resinCounts.condensed,
|
||||
transientResinCount: resinCounts.transient,
|
||||
fragileResinCount: resinCounts.fragile
|
||||
static 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(`====================================`);
|
||||
}
|
||||
|
||||
static async countOriginalResin(tryOriginalMode, opToMainUi, openMap) {
|
||||
if (tryOriginalMode) {
|
||||
log.info("尝试使用原始模式");
|
||||
return await Physical.countOriginalResinBackup()
|
||||
} else {
|
||||
log.info('尝试使用优化模式');
|
||||
let ocr_physical = await Physical.ocrPhysical(opToMainUi, openMap);
|
||||
|
||||
log.debug(`ocrPhysical: {0}`, JSON.stringify(ocr_physical))
|
||||
await sleep(600)
|
||||
// ocrPhysical = false//模拟异常
|
||||
if (ocr_physical/* && ocrPhysical.ok*/) {
|
||||
return ocr_physical?.current;
|
||||
} else {
|
||||
//异常 退出至地图 尝试使用原始模式
|
||||
// await keyPress("VK_ESCAPE")
|
||||
log.error(`ocrPhysical error`);
|
||||
throw new Error("ocrPhysical error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async countOriginalResinBackup() {
|
||||
const originalResin = await Physical.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
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log.error(`统计树脂数量时发生异常: ${error.message}`);
|
||||
throw error;
|
||||
} finally {
|
||||
if (shouldRestoreMainUi) {
|
||||
await toMainUi();
|
||||
await sleep(CONFIG.UI_DELAY);
|
||||
// 匹配 xxx/200 格式中的第一个数字(1-3位)
|
||||
const count = await Physical.recognizeNumberByOCR(ocrRegion, /(\d{1,3})\/\d+/);
|
||||
if (count !== null) {
|
||||
log.info(`原粹树脂数量: ${count}`);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到国家选择界面的异步函数
|
||||
* 通过点击指定坐标并等待界面加载来完成切换操作
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
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 {number} 原粹树脂数量
|
||||
*/
|
||||
async function countOriginalResin(tryOriginalMode, opToMainUi, openMap) {
|
||||
if (tryOriginalMode) {
|
||||
log.info("尝试使用原始模式");
|
||||
return await countOriginalResinBackup()
|
||||
} else {
|
||||
log.info('尝试使用优化模式');
|
||||
let ocr_physical = await ocrPhysical(opToMainUi, openMap);
|
||||
|
||||
log.debug(`ocrPhysical: {0}`, JSON.stringify(ocr_physical))
|
||||
await sleep(600)
|
||||
// ocrPhysical = false//模拟异常
|
||||
if (ocr_physical/* && ocrPhysical.ok*/) {
|
||||
return ocr_physical?.current;
|
||||
} 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(`未找到原粹树脂图标`);
|
||||
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
|
||||
};
|
||||
static async recognizeImage(recognitionObject, timeout = CONFIG.RECOGNITION_TIMEOUT) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 匹配 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;
|
||||
}
|
||||
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 通用图像识别函数
|
||||
* @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) {
|
||||
let gameRegion = undefined
|
||||
try {
|
||||
gameRegion = captureGameRegion();
|
||||
// 直接链式调用,不保存gameRegion变量,避免内存管理问题
|
||||
const imageResult = gameRegion.find(recognitionObject);
|
||||
if (imageResult.isExist()) {
|
||||
return imageResult;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`识别图像时发生异常: ${error.message}`);
|
||||
} finally {
|
||||
if (gameRegion) {
|
||||
gameRegion.dispose();
|
||||
}
|
||||
}
|
||||
await sleep(CONFIG.SLEEP_INTERVAL);
|
||||
}
|
||||
|
||||
log.warn(`经过多次尝试,仍然无法识别图像`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过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;
|
||||
while (Date.now() - startTime < timeout) {
|
||||
let gameRegion = undefined
|
||||
try {
|
||||
gameRegion = captureGameRegion();
|
||||
// 直接链式调用,不保存gameRegion变量,避免内存管理问题
|
||||
const imageResult = gameRegion.find(recognitionObject);
|
||||
if (imageResult.isExist()) {
|
||||
return imageResult;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`识别图像时发生异常: ${error.message}`);
|
||||
} finally {
|
||||
if (gameRegion) {
|
||||
gameRegion.dispose();
|
||||
}
|
||||
}
|
||||
await sleep(CONFIG.SLEEP_INTERVAL);
|
||||
}
|
||||
|
||||
log.warn(`经过多次尝试,仍然无法识别图像`);
|
||||
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();
|
||||
}
|
||||
|
||||
static 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ocrPhysical,
|
||||
countOriginalResin,
|
||||
countAllResin,
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* @param {number} h - 区域高度,默认为1080
|
||||
* @returns {Promise<string|null>} 返回识别到的文本内容,如果识别失败则返回null
|
||||
*/
|
||||
async function ocrRegion(x = 0,
|
||||
export async function ocrRegion(x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080) {
|
||||
@@ -34,7 +34,7 @@ async function ocrRegion(x = 0,
|
||||
* @param {boolean} [calibrationGameRefreshTime=true] 是否进行游戏刷新时间校准
|
||||
* @returns {Object} 返回包含星期数字和星期名称的对象
|
||||
*/
|
||||
async function getDayOfWeek(calibrationGameRefreshTime = true) {
|
||||
export async function getDayOfWeek(calibrationGameRefreshTime = true) {
|
||||
// 获取当前日期对象
|
||||
let today = new Date();//4点刷新 所以要减去4小时
|
||||
if (calibrationGameRefreshTime) {
|
||||
@@ -55,8 +55,8 @@ async function getDayOfWeek(calibrationGameRefreshTime = true) {
|
||||
}
|
||||
}
|
||||
|
||||
const commonPath = 'assets/'
|
||||
const commonMap = new Map([
|
||||
export const commonPath = 'assets/'
|
||||
export const commonMap = new Map([
|
||||
['main_ui', {
|
||||
path: `${commonPath}`,
|
||||
name: 'paimon_menu',
|
||||
@@ -83,7 +83,7 @@ const commonMap = new Map([
|
||||
type: '.jpg',
|
||||
}],
|
||||
])
|
||||
const genshinJson = {
|
||||
export const genshinJson = {
|
||||
width: 1920,//genshin.width,
|
||||
height: 1080,//genshin.height,
|
||||
}
|
||||
@@ -93,37 +93,103 @@ const genshinJson = {
|
||||
* @param {string} key - 要查找的键值
|
||||
* @returns {any} 返回与键值对应的JSON路径值
|
||||
*/
|
||||
function getJsonPath(key) {
|
||||
export function getJsonPath(key) {
|
||||
return commonMap.get(key); // 通过commonMap的get方法获取指定键对应的值
|
||||
}
|
||||
|
||||
// 判断是否在主界面的函数
|
||||
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();
|
||||
try {
|
||||
let res = captureRegion.find(paimonMenuRo);
|
||||
return !res.isEmpty();
|
||||
} finally {
|
||||
captureRegion.dispose()
|
||||
class UI{
|
||||
/**
|
||||
* 检查当前是否在主界面
|
||||
* @returns {boolean} 如果在主界面则返回true,否则返回false
|
||||
*/
|
||||
static 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, // 起始点x坐标
|
||||
0, // 起始点y坐标
|
||||
genshinJson.width / 3.0, // 匹配区域的宽度
|
||||
genshinJson.width / 5.0 // 匹配区域的高度
|
||||
);
|
||||
let captureRegion = captureGameRegion(); // 获取游戏区域的截图
|
||||
try {
|
||||
// 在捕获的区域中查找模板匹配的结果
|
||||
let res = captureRegion.find(paimonMenuRo);
|
||||
return !res.isEmpty(); // 如果找到匹配项则返回true,否则返回false
|
||||
} finally {
|
||||
// 确保释放资源
|
||||
captureRegion.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* 检查当前是否在秘境界面
|
||||
* @returns {Promise<boolean>} 返回是否在秘境界面
|
||||
*/
|
||||
static async isInOutDomainUI() {
|
||||
//509, 259, 901, 563
|
||||
const text = "退出秘境";
|
||||
const ocrRegion = {
|
||||
x: 509,
|
||||
y: 259,
|
||||
w: 901,
|
||||
h: 563
|
||||
}
|
||||
const find = await findText(text, ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
|
||||
log.debug("识别结果:{1}", find)
|
||||
return find && find.includes(text)
|
||||
}
|
||||
|
||||
async function toMainUi() {
|
||||
/**
|
||||
* 判断当前UI是否在秘境界面
|
||||
* 通过OCR技术在指定区域识别"退出秘境"文本
|
||||
* @returns {Promise<boolean>} 返回是否在秘境界面,true表示在秘境界面,false表示不在
|
||||
*/
|
||||
static async isInOutDomainUI() {
|
||||
//509, 259, 901, 563
|
||||
const text = "退出秘境";
|
||||
const ocrRegion = {
|
||||
x: 509,
|
||||
y: 259,
|
||||
w: 901,
|
||||
h: 563
|
||||
}
|
||||
const find = await findText(text, ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
|
||||
log.debug("识别结果:{1}", find)
|
||||
return find && find.includes(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在Stygian Onslaught(冥河冲击)UI界面中
|
||||
* 通过OCR识别屏幕上的特定文本"退出挑战"来判断当前界面状态
|
||||
* @returns {Promise<boolean>} 返回是否在Stygian Onslaught UI界面中
|
||||
*/
|
||||
static async isInOutStygianOnslaughtUI() {
|
||||
// 定义要识别的文本内容
|
||||
const text = "退出挑战";
|
||||
// 定义OCR识别的区域坐标和尺寸
|
||||
const ocrRegion = {
|
||||
x: 509, // 区域左上角x坐标
|
||||
y: 259, // 区域左上角y坐标
|
||||
w: 901, // 区域宽度
|
||||
h: 563 // 区域高度
|
||||
}
|
||||
// 在指定区域内查找文本,并等待识别结果
|
||||
const find = await findText(text, ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
|
||||
// 输出识别结果到调试日志
|
||||
log.debug("识别结果:{1}", find)
|
||||
// 返回识别结果中是否包含目标文本
|
||||
return find && find.includes(text)
|
||||
}
|
||||
}
|
||||
|
||||
export async function toMainUi() {
|
||||
let ms = 300
|
||||
let index = 1
|
||||
await sleep(ms);
|
||||
while (!isInMainUI()) {
|
||||
while (!UI.isInMainUI()) {
|
||||
await sleep(ms);
|
||||
await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面
|
||||
await sleep(ms);
|
||||
@@ -135,42 +201,11 @@ async function toMainUi() {
|
||||
|
||||
}
|
||||
|
||||
const isInOutDomainUI = async () => {
|
||||
// // let name = '主界面'
|
||||
// let main_ui = getJsonPath('out_domain');
|
||||
// // 定义识别对象
|
||||
// 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();
|
||||
// try {
|
||||
// let res = captureRegion.find(paimonMenuRo);
|
||||
// return !res.isEmpty();
|
||||
// }finally {
|
||||
// captureRegion.dispose()
|
||||
// }
|
||||
//509, 259, 901, 563
|
||||
const text = "退出秘境";
|
||||
const ocrRegion = {
|
||||
x: 509,
|
||||
y: 259,
|
||||
w: 901,
|
||||
h: 563
|
||||
}
|
||||
const find = await findText(text, ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
|
||||
log.debug("识别结果:{1}", find)
|
||||
return find && find.includes(text)
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出秘境的UI处理函数
|
||||
* 该函数用于处理退出秘境界面的相关操作,包括点击确认按钮和检测界面状态
|
||||
*/
|
||||
async function outDomainUI() {
|
||||
export async function outDomainUI() {
|
||||
log.info(`{0}`,"退出秘境");
|
||||
const ocrRegion = {
|
||||
x: 509,
|
||||
@@ -186,8 +221,8 @@ async function outDomainUI() {
|
||||
//点击确认按钮
|
||||
await findTextAndClick('地脉异常')
|
||||
await sleep(ms);
|
||||
while (!await isInOutDomainUI()) {
|
||||
if (isInMainUI()) {
|
||||
while (!await UI.isInOutDomainUI()) {
|
||||
if (UI.isInMainUI()) {
|
||||
inMainUI = true
|
||||
break
|
||||
}
|
||||
@@ -213,19 +248,7 @@ async function outDomainUI() {
|
||||
|
||||
}
|
||||
|
||||
const isInOutStygianOnslaughtUI = async () =>{
|
||||
const text = "退出挑战";
|
||||
const ocrRegion = {
|
||||
x: 509,
|
||||
y: 259,
|
||||
w: 901,
|
||||
h: 563
|
||||
}
|
||||
const find = await findText(text, ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
|
||||
log.debug("识别结果:{1}", find)
|
||||
return find && find.includes(text)
|
||||
}
|
||||
async function outStygianOnslaughtUI() {
|
||||
export async function outStygianOnslaughtUI() {
|
||||
log.info(`{0}`,"退出挑战");
|
||||
const ocrRegion = {
|
||||
x: 509,
|
||||
@@ -238,8 +261,8 @@ async function outStygianOnslaughtUI() {
|
||||
let tryMax = false
|
||||
let inMainUI = false
|
||||
await sleep(ms);
|
||||
while (!await isInOutStygianOnslaughtUI()) {
|
||||
if (isInMainUI()) {
|
||||
while (!await UI.isInOutStygianOnslaughtUI()) {
|
||||
if (UI.isInMainUI()) {
|
||||
inMainUI = true
|
||||
break
|
||||
}
|
||||
@@ -253,7 +276,7 @@ async function outStygianOnslaughtUI() {
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
if ((!tryMax) && (!inMainUI) && await isInOutStygianOnslaughtUI()) {
|
||||
if ((!tryMax) && (!inMainUI) && await UI.isInOutStygianOnslaughtUI()) {
|
||||
try {
|
||||
//点击确认按钮
|
||||
await findTextAndClick('退出挑战', ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
|
||||
@@ -273,7 +296,7 @@ async function outStygianOnslaughtUI() {
|
||||
* @param {number} interval - 每次尝试之间的间隔时间(毫秒),默认为50
|
||||
* @returns {Promise<string>} 返回找到的文本内容,如果未找到则返回空字符串
|
||||
*/
|
||||
async function findText(
|
||||
export async function findText(
|
||||
text,
|
||||
x = 0,
|
||||
y = 0,
|
||||
@@ -327,7 +350,7 @@ async function findText(
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function findTextAndClick(
|
||||
export async function findTextAndClick(
|
||||
text,
|
||||
x = 0,
|
||||
y = 0,
|
||||
@@ -383,7 +406,7 @@ async function findTextAndClick(
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function findImgAndClick(
|
||||
export async function findImgAndClick(
|
||||
target,
|
||||
x = 0,
|
||||
y = 0,
|
||||
@@ -431,7 +454,7 @@ async function findImgAndClick(
|
||||
* 该函数用于显示错误通知并抛出错误对象
|
||||
* @param {string} msg - 错误信息,将用于通知和错误对象
|
||||
*/
|
||||
function throwError(msg, isNotification = false) {
|
||||
export function throwError(msg, isNotification = false) {
|
||||
// 使用notification组件显示错误通知
|
||||
// notification.error(`${msg}`);
|
||||
if (isNotification) {
|
||||
@@ -441,17 +464,11 @@ function throwError(msg, isNotification = false) {
|
||||
throw new Error(`${msg}`);
|
||||
}
|
||||
|
||||
export {
|
||||
ocrRegion,
|
||||
getDayOfWeek,
|
||||
getJsonPath,
|
||||
isInMainUI,
|
||||
toMainUi,
|
||||
isInOutDomainUI,
|
||||
outDomainUI,
|
||||
isInOutStygianOnslaughtUI,
|
||||
outStygianOnslaughtUI,
|
||||
findTextAndClick,
|
||||
findImgAndClick,
|
||||
throwError,
|
||||
}
|
||||
// 辅助函数:安全地解析 day 字段
|
||||
export function parseInteger(str) {
|
||||
if (str == null || String(str).trim() === "") {
|
||||
return undefined; // 空值或无效值返回 undefined
|
||||
}
|
||||
const parsedInt = parseInt(String(str).trim(), 10);
|
||||
return isNaN(parsedInt) ? undefined : parsedInt; // 非法数字返回 undefined
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import {ocrRegion} from './tool.js'
|
||||
|
||||
async function saveOnlyNumber(str) {
|
||||
str = str ? str : '';
|
||||
// 使用正则表达式匹配字符串中的所有数字
|
||||
// \d匹配一个或多个数字
|
||||
// .join('') 将匹配到的数字数组连接成一个字符串
|
||||
// parseInt 将连接后的字符串转换为整数
|
||||
// return parseInt(str.match(/\d+/g).join(''));
|
||||
const matches = str.match(/\d+/g);
|
||||
if (!matches) {
|
||||
return 0; // 或抛出错误
|
||||
}
|
||||
return parseInt(matches.join(''), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* OCR识别UID的异步函数
|
||||
* 该函数用于通过OCR技术识别屏幕上特定位置的UID文本
|
||||
* @returns {Promise<number>} - 异步函数,没有明确的返回值
|
||||
*/
|
||||
async function ocrUid() {
|
||||
// 定义OCR识别的坐标和尺寸参数
|
||||
let uid_json = {
|
||||
x: 1683, // OCR识别区域的左上角x坐标
|
||||
y: 1051, // OCR识别区域的左上角y坐标
|
||||
width: 234, // OCR识别区域的宽度
|
||||
height: 28, // OCR识别区域的高度
|
||||
}
|
||||
let text = await ocrRegion(uid_json.x, uid_json.y, uid_json.width, uid_json.height);
|
||||
return await saveOnlyNumber(text);
|
||||
}
|
||||
|
||||
export {
|
||||
ocrUid,
|
||||
}
|
||||
Reference in New Issue
Block a user