体力计划[支持幽境危战] (#2978)

This commit is contained in:
云端客
2026-03-11 13:13:52 +08:00
committed by GitHub
parent 40c9d4b95a
commit 0b3b8bc74a
6 changed files with 763 additions and 83 deletions

View File

@@ -42,29 +42,35 @@
**字段详解**
| 位置 | 字段 | 是否必填 | 说明 | 示例 |
|:--:|:-----------:|:------:|:--------------------------------------:|:-----------------------:|
| 1 | 类型 | **必填** | 秘境/地脉(后期支持地脉冗余字段) | 秘境/地脉 |
| 2 | 周几执行(0-6) | 可选 | 0=周日,1=周一,...,6=周六;可多选按`/`分割,不填=每天都可执行 | 0/3,3 |
| 3 | 执行顺序 | 可选 | 数字越大越优先执行(同时间点先跑优先级高的) | 9 / 5 / 1 |
| | | | ***`秘境类型后几位参数说明`***
| 4 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称(用于切换队伍) | 速刷 / 雷国 / 国家队 |
| 5 | 秘境名称/刷取物品名称 | **必填** | 与 BetterGI 自动秘境识别的名称保持一致 | 苍白的遗荣 / 炽烈的炎之魔女 / 辰砂往生录 |
| 6 | 刷几轮 | 可选 | 整数,执行几轮(每轮 = 1次完整秘境 | 3 / 5 / 10 |
| 7 | 限时/周日 | 可选 | 和本体的1-3一致 | 1 / 2 / 3 |
| 8 | 树脂使用顺序 | 可选 | 原粹树脂,浓缩树脂,须臾树脂,脆弱树脂`/`分割,不填=默认执行 原粹树脂 | 原粹树脂/浓缩树脂,须臾树脂 |
| | | | ***`地脉类型后几位参数说明`***
| 4 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称(用于切换队伍) | 速刷 / 雷国 / 国家队 |
| 5 | 国家 | **必填** | 识别国家(用于切换国家) | 纳塔 ... |
| 6 | 刷几轮 | **必填** | 刷几轮 | 1 |
| 7 | 地脉类型 | **必填** | 启示之花/藏金之花 | 启示之花 / 藏金之花 |
| 8 | 好感队 | 可选 | | |
| 9 | 使用脆弱树脂 | 可选 | 启用随便填个值 |
| 10 | 使用须臾树脂 | 可选 | 启用随便填个值 |
| 11 | 合成浓缩树脂 | 可选 | 启用随便填个值 |
| 12 | 使用冒险家之书 | 可选 | 启用随便填个值 |
| 13 | 详细通知 | 可选 | 启用随便填个值 |
| 14 | 战斗超时 | 可选 | 可选,默认 120 |
| 位置 | 字段 | 是否必填 | 说明 | 示例 |
|:--:|:-----------:|:------:|:-------------------------------------------:|:-----------------------:|
| 1 | 类型 | **必填** | 秘境/地脉/幽境 | 秘境/地脉/幽境 |
| 2 | 周几执行(0-6) | 可选 | 0=周日,1=周一,...,6=周六;可多选按`/`分割,不填=每天都可执行 | 0/3,3 |
| 3 | 执行顺序 | 可选 | 数字越大越优先执行(同时间点先跑优先级高的) | 9 / 5 / 1 |
| | | | ***`秘境类型后几位参数说明`***
| 4 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称(用于切换队伍) | 速刷 / 雷国 / 国家队 |
| 5 | 秘境名称/刷取物品名称 | **必填** | 与 BetterGI 自动秘境识别的名称保持一致 | 苍白的遗荣 / 炽烈的炎之魔女 / 辰砂往生录 |
| 6 | 刷几轮 | 可选 | 整数,执行几轮(每轮 = 1次完整秘境 | 3 / 5 / 10 |
| 7 | 限时/周日 | 可选 | 和本体的1-3一致 | 1 / 2 / 3 |
| 8 | 树脂使用顺序 | 可选 | 原粹树脂,浓缩树脂,须臾树脂,脆弱树脂`/`分割,不填=默认执行 原粹树脂 | 原粹树脂/浓缩树脂,须臾树脂 |
| | | | ***`地脉类型后几位参数说明`***
| 4 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称(用于切换队伍) | 速刷 / 雷国 / 国家队 |
| 5 | 国家 | **必填** | 识别国家(用于切换国家) | 纳塔 ... |
| 6 | 刷几轮 | **必填** | 刷几轮 | 1 |
| 7 | 地脉类型 | **必填** | 启示之花/藏金之花 | 启示之花 / 藏金之花 |
| 8 | 好感队 | 可选 | | |
| 9 | 使用脆弱树脂 | 可选 | 启用随便填个值 |
| 10 | 使用须臾树脂 | 可选 | 启用随便填个值 |
| 11 | 合成浓缩树脂 | 可选 | 启用随便填个值 |
| 12 | 使用冒险家之书 | 可选 | 启用随便填个值 |
| 13 | 详细通知 | 可选 | 启用随便填个值 |
| 14 | 战斗超时 | 可选 | 可选,默认 120 |
| | | | ***`幽境类型后几位参数说明`***
| 4 | 指定刷取战场 | 可选 | (1-3)不填使用BetterGI默认的配置 | 1 |
| 5 | 队伍名称 | 可选 | BetterGI 中已保存的队伍名称用于切换队伍不填使用BetterGI默认的配置 | 速刷 / 雷国 / 国家队 |
| 6 | 是否指定使用自定义树脂 | 可选 | 启用随便填个值 不填使用BetterGI默认的配置 | |
| 7 | 自定义树脂顺序 | 可选 | 原粹树脂,浓缩树脂,须臾树脂,脆弱树脂`/`分割 不填使用BetterGI默认的配置 | 原粹树脂 / 浓缩树脂 / 须臾树脂 |
| 8 | 自定义树脂次数 | 可选 | 和自定义树脂顺序对应 `/`分割 不填使用BetterGI默认的配置 | 1 / 2 / 3 |
**配置示例**
@@ -72,7 +78,9 @@
秘境|0/3|9|速刷|苍白的遗荣|3|1, # 优先级最高,周日,周三刷3轮遗荣
秘境||5|国家队|炽烈的炎之魔女|5||, # 优先级次之每天刷5轮魔女
秘境|0|2|雷国|无想之刃狭间|2|2|浓缩树脂/原粹树脂, # 只在周日刷,优先使用浓缩树脂后使用原粹树脂,优先级较低
地脉||1||蒙德|1|启示之花|||||||120 #优先级较低 刷1轮蒙德经验书
地脉||1||蒙德|1|启示之花|||||||120, #优先级较低 刷1轮蒙德经验书
幽境||1|||, #幽境 默认本体配置
幽境||1|1|队伍|1|浓缩树脂/原粹树脂|1/1 #幽境 默认自定义树脂配置 切换到 队伍 运行->浓缩树脂 1次->原粹树脂 1次
```
(注意:最后一条也可以不带逗号)
@@ -85,15 +93,17 @@
"uid",
[
{
"order": 1,
// 顺序值
"order": 1,
// 执行日期
"days": [
0
],
// 执行日期
// 类型支持。 秘境、地脉、幽境
"runType": "秘境",
// 预留地脉 类型支持。 秘境、地脉
// 秘境信息对象,
"autoFight": {
//树脂开启和使用顺序
"physical": [
{
"order": 0,
@@ -116,41 +126,76 @@
"open": false
}
],
//树脂开启和使用顺序
"domainName": undefined,
//秘境名称
"domainName": undefined,
//队伍名称
"partyName": undefined,
//队伍名称
"sundaySelectedValue": undefined,
//周日|限时选择的值
"domainRoundNum": undefined//副本轮数
}
// 秘境信息对象,
"autoLeyLineOutcrop": {
"count": 0,
//刷几轮
"country": "",
//国家
"leyLineOutcropType": "启示之花",
//地脉类型 启示之花/藏金之花
"useAdventurerHandbook": false,
//使用冒险家之书
"friendshipTeam": "",
//好感队伍名称
"team": "",
//队伍名称
"timeout": 120,
//战斗超时
"isGoToSynthesizer": false,
//合成浓缩树脂
"useFragileResin": false,
//使用脆弱树脂
"useTransientResin": false,
//使用须臾树脂
"isNotification": false
//详细通知
}
"sundaySelectedValue": undefined,
//副本轮数
"domainRoundNum": undefined
},
//地脉信息对象
"autoLeyLineOutcrop": {
//刷几轮
"count": 0,
//国家
"country": "",
//地脉类型 启示之花/藏金之花
"leyLineOutcropType": "启示之花",
//使用冒险家之书
"useAdventurerHandbook": false,
//好感队伍名称
"friendshipTeam": "",
//队伍名称
"team": "",
//战斗超时
"timeout": 120,
//合成浓缩树脂
"isGoToSynthesizer": false,
//使用脆弱树脂
"useFragileResin": false,
//使用须臾树脂
"useTransientResin": false,
//详细通知
"isNotification": false
},
//危战信息对象
"autoStygianOnslaught": {
//自定义树脂
"physical": [
{
"order": 0,
"name": "原粹树脂",
"open": true,
"count": 0
},
{
"order": 1,
"name": "浓缩树脂",
"open": false,
"count": 0
},
{
"order": 2,
"name": "须臾树脂",
"open": false,
"count": 0
},
{
"order": 3,
"name": "脆弱树脂",
"open": false,
"count": 0
}
],
// 是否指定使用自定义树脂
"specifyResinUse": false,
//指定刷取战场 1-3
"bossNum": undefined,
//队伍名称
"fightTeamName": ""
}
}
]
]
@@ -220,7 +265,7 @@
]
```
### 如果你不想研究语法 请部署[bettergi-scripts-tools](https://github.com/Kirito520Asuna/bettergi-scripts-tools) v0.0.4以上版本
### 如果你不想研究语法 请部署[bettergi-scripts-tools](https://github.com/Kirito520Asuna/bettergi-scripts-tools)(点击前往部署) v0.0.4以上版本
***`话不多说直接上图:`***
@@ -253,9 +298,11 @@
| 版本 | 密钥 |
|-------|---------------------|
| 0.0.1 | oiJbmjU2R0NniiwiZxh |
| 0.0.2 | oiJbmjU2R0NniiwiZxh |
## 版本历史(简要)
### 0.0.2 2026.03.07
- 新增幽境支持
### 0.0.1 2026.01.30
- 基本功能完成

View File

@@ -34,7 +34,7 @@ const config = {
current: 0,//当前体力
names: ["原粹树脂", "浓缩树脂", "须臾树脂", "脆弱树脂"]
},
runTypes: ['秘境', '地脉']
runTypes: ['秘境', '地脉', '幽境']
},
//
path: {

View File

@@ -1,8 +1,9 @@
import {config, initConfig, initSettings, LoadType} from './config/config';
import {ocrUid} from './utils/uid';
import {getDayOfWeek, outDomainUI, throwError} from './utils/tool';
import {getDayOfWeek, outDomainUI, outStygianOnslaughtUI, throwError,toMainUi} from './utils/tool';
import {pullJsonConfig, pushAllCountryConfig, pushAllJsonConfig} from './utils/bgi_tools';
import {ocrPhysical} from "./utils/physical";
import {findStygianOnslaught} from "./utils/activity";
/**
* 自动执行秘境任务的异步函数
@@ -12,13 +13,17 @@ import {ocrPhysical} from "./utils/physical";
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: "原粹树脂", count: 1, open: true},
// {order: 1, name: "浓缩树脂", count: 0, open: false},
// {order: 2, name: "须臾树脂", count: 0, open: false},
// {order: 3, name: "脆弱树脂", count: 0, open: false},
// {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) {
@@ -31,10 +36,10 @@ async function autoDomain(autoFight) {
physical_domain.sort((a, b) => a.order - b.order)
// 不包含原粹树脂的和
const noOriginalSum = physical_domain.filter(item => item?.name.trim() !== "原粹树脂")
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() === "原粹树脂")
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())
// /** 树脂使用优先级列表 */
@@ -53,15 +58,13 @@ async function autoDomain(autoFight) {
config.user.physical.current = physicalOcr.current
config.user.physical.min = physicalOcr.min
const physical = config.user.physical
if (physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) {
if (domainParam.specifyResinUse && physical.current < physical.min && noOriginalSum <= 0 && originalSum > 0) {
throwError(`体力不足,当前体力${physical.current},最低体力${physical.min},请手动补充体力后重试`)
}
// 创建秘境参数对象初始化值为0
let domainParam = new AutoDomainParam();
//关闭分解
domainParam.autoArtifactSalvage = false
//关闭榨干原粹树脂
domainParam.specifyResinUse = true
//配置树脂使用优先级
if (resinPriorityList.length > 0) {
domainParam.SetResinPriorityList(...resinPriorityList)
@@ -117,7 +120,6 @@ async function autoDomain(autoFight) {
* @returns {Promise<void>}
*/
async function autoLeyLineOutcrop(autoLeyLineOutcrop) {
//todo :地脉花
// autoLeyLineOutcrop = {
// "count": 0,
// "country": "country_cb3d792be8db",
@@ -175,6 +177,117 @@ async function autoLeyLineOutcrop(autoLeyLineOutcrop) {
}
}
/**
* 自动执行幽境危战任务的异步函数
* @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 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;
}
throw e;
}
}
} finally {
log.info(`{0}`, "执行完成")
// 退出危战
await outStygianOnslaughtUI()
}
}
/**
* 自动执行列表处理函数
* @param {Array} autoRunOrderList - 包含自动配置的数组
@@ -187,6 +300,8 @@ async function autoRunList(autoRunOrderList) {
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);
}
}
}
@@ -246,13 +361,15 @@ async function loadMode(Load, autoOrderSet, runConfig) {
runType: runType, // 运行类型
days: days, // 执行日期(数组)
autoFight: undefined, // 秘境信息对象
autoLeyLineOutcrop: undefined // 地脉信息对象
autoLeyLineOutcrop: undefined, // 地脉信息对象
autoStygianOnslaught: undefined // 幽境信息对象
}
if (!config.user.runTypes.includes(runType)) {
throwError(`运行类型${runType}输入错误`)
} else if (config.user.runTypes[0] === runType) {
}
else if (config.user.runTypes[0] === runType) {
// 创建秘境信息对象
let autoFight = {
domainName: undefined,//秘境名称
@@ -297,7 +414,8 @@ async function loadMode(Load, autoOrderSet, runConfig) {
autoFight.sundaySelectedValue = sundaySelectedValue // 周日|限时选择的值
autoOrder.autoFight = autoFight // 将秘境信息对象添加到秘境顺序对象中
} else if (config.user.runTypes[1] === runType) {
}
else if (config.user.runTypes[1] === runType) {
//"|队伍名称|国家|刷几轮|花类型|好感队|是否使用脆弱树脂|是否使用须臾树脂|是否前往合成台合成浓缩树脂|是否使用冒险之证|发送详细通知|战斗超时时间,..."
let autoLeyLineOutcrop = {
count: 0, // 刷几次0=自动/无限)
@@ -338,13 +456,78 @@ async function loadMode(Load, autoOrderSet, runConfig) {
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)
@@ -397,7 +580,7 @@ async function loadMode(Load, autoOrderSet, runConfig) {
break
default:
throw new Error("请先配置加载方式");
// break;
// break;
}
}
@@ -479,10 +662,40 @@ async function main() {
let runConfig = config.run.config;
//"队伍名称|秘境名称/刷取物品名称|刷几轮|限时/周日|周几执行(0-6)不填默认执行|执行顺序,..."
const autoRunOrderList = await initRunOrderList(runConfig);
const list = autoRunOrderList.filter(item =>
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[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[1] && 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()
}
}
if (list?.length > 0) {
//循环跑
while (true) {

View File

@@ -1,10 +1,10 @@
{
"name": "自动体力计划",
"version": "0.0.1",
"version": "0.0.2",
"description": "",
"settings_ui": "settings.json",
"main": "main.js",
"bgi_version": "0.57.2",
"bgi_version": "0.58.0",
"key": "oiJbmjU2R0NniiwiZxh",
"authors": [
{

View File

@@ -0,0 +1,369 @@
const ocrRegionConfig = {
activity: {x: 267, y: 197, width: 226, height: 616},//活动识别区域坐标和尺寸
remainingTime: {x: 497, y: 202, width: 1417, height: 670},//剩余时间识别区域坐标和尺寸
}
const xyConfig = {
top: {x: 344, y: 273},
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 = config.scrollPageCount) {
// 根据滚动方向设置坐标位置
// 如果是向上滚动,使用顶部坐标;否则使用底部坐标
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();
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();
// 4. 主循环:逐页向下扫描
while (scannedPages < maxPages) {
scannedPages++;
log.info(`正在扫描第 ${scannedPages}`);
await moveMouseTo(0, 20);
let captureRegion = null;
try {
captureRegion = captureGameRegion();
const ocrObject = RecognitionObject.Ocr(
ocrRegionConfig.activity.x,
ocrRegionConfig.activity.y,
ocrRegionConfig.activity.width,
ocrRegionConfig.activity.height
);
let resList = captureRegion.findMulti(ocrObject);
if (resList.length === 0) {
log.info("当前页未识别到任何活动,视为已到页面底部");
break;
}
// ============ 重复页检测 ============
const currentPageNames = new Set();
for (let res of resList) {
currentPageNames.add(res.text.trim());
}
if (previousPageActivities.size > 0) {
let overlapCount = 0;
for (let actName of currentPageNames) {
if (previousPageActivities.has(actName)) overlapCount++;
}
const overlapRatio = overlapCount / previousPageActivities.size;
if (overlapRatio >= 0.7) {
log.info(`检测到当前页与上一页高度重复(重合率 ${Math.round(overlapRatio * 100)}%),已到达底部,停止扫描`);
break;
}
}
previousPageActivities = currentPageNames;
// ===================================
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);
foundTarget = true;
// 如果没有指定 key找到活动名称就直接返回成功
if (!key) {
log.info(`已找到指定活动 [${activityName}],无需匹配关键字`);
findActivity.key = true;
break;
}
// 如果指定了 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)
}
if (findActivity.value) {
log.info(`已找到指定活动 [${activityName}] 且包含关键字 [${key}] 和值 [${value}]`);
}
} else {
log.info(`活动 [${activityName}] 不包含关键字 [${key}],继续查找`);
}
break;
}
}
// 如果找到目标活动,直接退出主循环
if (foundTarget) {
break;
}
// 5. 判断是否已到达页面底部(单一判断逻辑)
if (currentPageBottomName && currentPageBottomName === lastPageBottomName) {
sameBottomCount++;
if (sameBottomCount >= sameBottomCountMax) {
log.info(`连续 ${sameBottomCountMax} 次检测到相同底部活动,已确认到达页面最底部,扫描结束`);
break;
}
} else {
sameBottomCount = 0;
}
lastPageBottomName = currentPageBottomName;
// 6. 向下滑动一页,继续下一轮
await scrollPagesByActivity(false);
await sleep(ms);
} finally {
if (captureRegion) {
captureRegion.dispose();
}
}
}
return findActivity;
}
async function findStygianOnslaught() {
const findActivity = {
name: "幽境危战",
key: "紊乱爆发期",
value: "已结束",
}
const findResult = await scrollFindActivity(findActivity.name, findActivity.key, findActivity.value);
if (findResult.name && findResult.key && findResult.value) {
// 幽境危战 紊乱爆发期 已结束
return false
}
//正常模式
return true
}
export {
findStygianOnslaught
}

View File

@@ -213,6 +213,55 @@ 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() {
log.info(`{0}`,"退出挑战");
const ocrRegion = {
x: 509,
y: 259,
w: 901,
h: 563
}
let ms = 300
let index = 1
let tryMax = false
let inMainUI = false
await sleep(ms);
while (!await isInOutStygianOnslaughtUI()) {
if (isInMainUI()) {
inMainUI = true
break
}
await sleep(ms);
await keyPress("ESCAPE");
await sleep(ms * 2);
if (index > 3) {
log.error(`多次尝试匹配退出秘境界面失败 假定已经退出处理`);
tryMax = true
break
}
index += 1
}
if ((!tryMax) && (!inMainUI) && await isInOutStygianOnslaughtUI()) {
try {
//点击确认按钮
await findTextAndClick('退出挑战', ocrRegion.x, ocrRegion.y, ocrRegion.w, ocrRegion.h)
} catch (e) {
// log.error(`多次尝试点击确认失败 假定已经退出处理`);
}
}
}
/**
* 在指定区域内查找文本内容
* @param {string} text - 要查找的文本内容
@@ -343,6 +392,8 @@ export {
toMainUi,
isInOutDomainUI,
outDomainUI,
isInOutStygianOnslaughtUI,
outStygianOnslaughtUI,
findTextAndClick,
throwError,
}