自动体力计划 重构+适配 (#3235)

This commit is contained in:
云端客
2026-05-29 02:36:47 +08:00
committed by GitHub
parent 53112deab8
commit a53b96874d
10 changed files with 1674 additions and 1595 deletions

View File

@@ -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
- 限制单例模式配置

View File

@@ -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
// }

View File

@@ -141,6 +141,15 @@
]
}
,
{
"name": "山风的荆冕",
"type": "圣遗物",
"hasOrder": false,
"list": [
"天之美赐",
"影中沉凝的幻灭"
]
},
{
"name": "月童的库藏",
"type": "圣遗物",

View File

@@ -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) {

View File

@@ -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": [
{

View File

@@ -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
}

View 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()
}
}
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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,
}