diff --git a/repo/js/AutoPlan/README.md b/repo/js/AutoPlan/README.md index 307397102..d112f4d1b 100644 --- a/repo/js/AutoPlan/README.md +++ b/repo/js/AutoPlan/README.md @@ -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 - 基本功能完成 diff --git a/repo/js/AutoPlan/config/config.js b/repo/js/AutoPlan/config/config.js index 9512d3084..a36524d3d 100644 --- a/repo/js/AutoPlan/config/config.js +++ b/repo/js/AutoPlan/config/config.js @@ -34,7 +34,7 @@ const config = { current: 0,//当前体力 names: ["原粹树脂", "浓缩树脂", "须臾树脂", "脆弱树脂"] }, - runTypes: ['秘境', '地脉'] + runTypes: ['秘境', '地脉', '幽境'] }, // path: { diff --git a/repo/js/AutoPlan/main.js b/repo/js/AutoPlan/main.js index 5d3689cd8..c70d9c7ee 100644 --- a/repo/js/AutoPlan/main.js +++ b/repo/js/AutoPlan/main.js @@ -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} */ async function autoLeyLineOutcrop(autoLeyLineOutcrop) { - //todo :地脉花 // autoLeyLineOutcrop = { // "count": 0, // "country": "country_cb3d792be8db", @@ -175,6 +177,117 @@ async function autoLeyLineOutcrop(autoLeyLineOutcrop) { } } +/** + * 自动执行幽境危战任务的异步函数 + * @param autoStygianOnslaught + * @returns {Promise} + */ +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) { diff --git a/repo/js/AutoPlan/manifest.json b/repo/js/AutoPlan/manifest.json index 16a5860ba..feb9d4c92 100644 --- a/repo/js/AutoPlan/manifest.json +++ b/repo/js/AutoPlan/manifest.json @@ -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": [ { diff --git a/repo/js/AutoPlan/utils/activity.js b/repo/js/AutoPlan/utils/activity.js new file mode 100644 index 000000000..3e35f9b5f --- /dev/null +++ b/repo/js/AutoPlan/utils/activity.js @@ -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 +} \ No newline at end of file diff --git a/repo/js/AutoPlan/utils/tool.js b/repo/js/AutoPlan/utils/tool.js index ed88b2548..02d2bb8ff 100644 --- a/repo/js/AutoPlan/utils/tool.js +++ b/repo/js/AutoPlan/utils/tool.js @@ -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, } \ No newline at end of file