feat: 新增自动配置圣遗物锁定方案 (#2850)

* feat: 新增自动配置圣遗物锁定方案

* fix: clean

* fix: 截图资源释放

* chore: library 清理

* chore: README 删除冒号

* fix: 区分套装配置失败与跳过状态,修正结果汇总误报

* fix: 以已完成套装数提前结束,避免跳过后继续滚动列表

* feat: 分辨率检测,自动返回主界面打开背包,README 修改

* chore: 默认延迟系数调整为 1.2
This commit is contained in:
bling-yshs
2026-02-07 23:03:49 +08:00
committed by GitHub
parent 0da57320ac
commit f9a6dd519f
8 changed files with 6961 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
<h1 align="center">🔒 自动配置圣遗物锁定方案</h1>
## ✨ 脚本简介
本脚本用于**自动配置圣遗物锁定方案**。
锁定方案来源于 哔哩哔哩 @酸儒书生 的 [这期视频](https://www.bilibili.com/video/BV13YHQzMEAT),脚本会自动识别游戏界面并完成所有配置操作,省去手动逐个设置的繁琐过程。
## 🚀 功能特性
- 🎯 **自动识别套装** - 通过 OCR 自动识别当前界面的圣遗物套装
- ⚙️ **批量配置方案** - 支持一次性配置多个套装的锁定方案
- 🔄 **覆盖已有方案** - 可选择是否覆盖已存在的锁定方案
- ⏱️ **延迟系数调节** - 支持自定义延迟系数,适配不同性能设备
## 📖 使用步骤
### 1⃣ 前置要求
- 游戏分辨率设置为 **16:9** (2560x1440 或 1920x1080)
- 游戏语言设置为**简体中文**
- 圣遗物「锁定辅助」页面的「通用锁定方案」勾选上1、3、4项如图所示
![global-lock](assets/global-lock.jpg)
### 2⃣ 配置选项
在 「调度器」 中打开脚本设置,配置以下选项:
| 选项 | 说明 |
|:---|:---|
| 覆盖已有方案 | 勾选后会删除已有方案并重新配置 |
| 延迟系数 | 默认 1.2,运行不稳定时可调高至 1.4 或更高 |
| 需要配置的圣遗物套装 | 勾选需要配置的套装,支持全选 |
### 3⃣ 运行脚本
1. 在「调度器」中运行此脚本
2. 等待脚本自动完成所有配置
> 运行期间请勿操作鼠标键盘,脚本会自动控制游戏界面
## 🛠️ 支持的套装
脚本目前支持 1.0~月之四 的圣遗物套装
## 💬 问题反馈流程
1. 找到日志文件,路径为 BetterGI 安装目录的 `log` 目录下,日志文件名为 `better-genshin-impact年月日.log`
2. 屏幕左下角的错误日志截个图
3. https://github.com/babalae/bettergi-scripts-list/issues 创建一个 issue**上传截图和日志**,并 @bling-yshs
## 🙏 致谢
- 锁定方案来源:[@酸儒书生](https://space.bilibili.com/10411008)
## 👥 作者
- [bling-yshs](https://github.com/bling-yshs)
- [Bedrockx](https://github.com/Bedrockx)

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -0,0 +1,96 @@
/**
* 在指定区域内查找特定文本并返回其中心坐标(屏幕绝对坐标)
* @param {number} x1 - 区域左上角 X 坐标
* @param {number} y1 - 区域左上角 Y 坐标
* @param {number} x2 - 区域右下角 X 坐标
* @param {number} y2 - 区域右下角 Y 坐标
* @param {string} targetText - 您要查找的目标文本
* @returns {{x: number, y: number, text: string} | null} - 如果找到,返回中心坐标和文本;否则返回 null
*/
export const findTextInRegion = (x1, y1, x2, y2, targetText) => {
// 获取游戏画面截图并裁剪指定区域
let screen = captureGameRegion();
let searchRegion = screen.deriveCrop(x1, y1, x2 - x1, y2 - y1);
try {
// 对区域进行 OCR获取所有文本行的列表
let ocrResultList = searchRegion.findMulti(RecognitionObject.ocrThis);
log.debug("OCR 识别到的文本行总数: {count}", ocrResultList.count);
// 遍历所有 OCR 结果
for (let i = 0; i < ocrResultList.count; i++) {
let currentResult = ocrResultList[i];
log.debug("识别到文本: '{text}',位置: ({x}, {y})", currentResult.text, currentResult.x, currentResult.y);
// 判断识别到的文本是否包含目标文本
if (currentResult.text && currentResult.text.includes(targetText)) {
log.debug("成功找到目标文本 '{target}'!", targetText);
// 计算中心坐标(局部坐标)
let localCenterX = currentResult.x + Math.floor(currentResult.width / 2);
let localCenterY = currentResult.y + Math.floor(currentResult.height / 2);
// 转换为屏幕绝对坐标
let screenX = x1 + localCenterX;
let screenY = y1 + localCenterY;
return {
x: screenX,
y: screenY,
text: currentResult.text
};
}
}
// 如果遍历完所有结果都未找到,返回 null
log.debug("在指定区域内未能找到目标文本: '{target}'", targetText);
return null;
} finally {
// 释放图像资源
searchRegion.dispose();
screen.dispose();
}
};
/**
* 平滑拖动列表(从起始坐标拖动到目标坐标)
* @param {number} startX - 起始 X 坐标
* @param {number} startY - 起始 Y 坐标
* @param {number} endX - 目标 X 坐标
* @param {number} endY - 目标 Y 坐标
* @param {number} stepDistance - 每步移动的距离(像素),默认 10
* @returns {Promise<void>}
*/
export const smoothDragVertical = async (startX, startY, endX, endY, stepDistance = 10) => {
log.debug("开始平滑拖动,从 ({x1}, {y1}) 到 ({x2}, {y2})", startX, startY, endX, endY);
// 移动到起始位置
moveMouseTo(startX, startY);
// 按住鼠标左键
leftButtonDown();
// 计算总距离和步数
const totalDistanceX = endX - startX;
const totalDistanceY = endY - startY;
const absDistanceY = Math.abs(totalDistanceY);
const steps = Math.floor(absDistanceY / stepDistance); // 完整的步数
// 分步移动鼠标,模拟自然拖动(使用绝对坐标)
for (let i = 1; i <= steps; i++) {
// 计算当前步的绝对坐标(线性插值)
const progress = i / steps;
const currentX = Math.round(startX + totalDistanceX * progress);
const currentY = Math.round(startY + totalDistanceY * (i * stepDistance / absDistanceY));
moveMouseTo(currentX, currentY);
await sleep(10); // 每次移动后延迟 10 毫秒
}
// 最后确保精确到达目标位置
moveMouseTo(endX, endY);
await sleep(10);
// 释放鼠标左键前稍作延迟
await sleep(700);
leftButtonUp();
await sleep(500);
log.debug("拖动完成");
};

View File

@@ -0,0 +1,621 @@
import { findTextInRegion, smoothDragVertical } from './lib.js';
// 延迟时间默认值(单位:毫秒)
const DEFAULT_DELAY_SHORT = 75;
const DEFAULT_DELAY_MEDIUM = 200;
const DEFAULT_DELAY_LONG = 1000;
const DEFAULT_DELAY_EXTRA_LONG = 3000;
// 从 settings 读取延迟系数,不填则使用默认值 1.2
const rawMultiplier = settings.delayMultiplier ? parseFloat(settings.delayMultiplier) : 1.2;
if (isNaN(rawMultiplier) || rawMultiplier <= 0) {
log.error("延迟系数配置错误,必须是大于 0 的数字,当前值: {value}", settings.delayMultiplier);
throw new Error("延迟系数配置错误,脚本已终止");
}
const DELAY_MULTIPLIER = rawMultiplier;
// 计算实际延迟值(默认值 × 延迟系数)
const DELAY_SHORT = Math.round(DEFAULT_DELAY_SHORT * DELAY_MULTIPLIER);
const DELAY_MEDIUM = Math.round(DEFAULT_DELAY_MEDIUM * DELAY_MULTIPLIER);
const DELAY_LONG = Math.round(DEFAULT_DELAY_LONG * DELAY_MULTIPLIER);
const DELAY_EXTRA_LONG = Math.round(DEFAULT_DELAY_EXTRA_LONG * DELAY_MULTIPLIER);
// 标记是否已检查过必须条件提示框
let hasCheckedNecessaryTip = false;
/**
* 检查 OCR 识别到的套装名称是否匹配配置中的套装
* @param {string} recognizedName - OCR 识别到的套装名称
* @param {Object} setConfig - 套装配置对象(包含 set_name 和可选的 alias 数组)
* @returns {boolean} - 是否匹配
*/
function matchSetName(recognizedName, setConfig) {
// 先精确匹配套装名称
if (recognizedName === setConfig.set_name) {
return true;
}
// 如果有别名,尝试模糊匹配(识别到的名称包含别名)
if (setConfig.alias && Array.isArray(setConfig.alias)) {
for (let alias of setConfig.alias) {
if (recognizedName.includes(alias)) {
return true;
}
}
}
return false;
}
/**
* 根据 OCR 识别到的套装名称查找对应的配置
* @param {string} recognizedName - OCR 识别到的套装名称
* @param {Array} config - 所有套装配置数组
* @returns {Object|null} - 匹配的套装配置,未找到返回 null
*/
function findSetConfig(recognizedName, config) {
for (let setConfig of config) {
if (matchSetName(recognizedName, setConfig)) {
return setConfig;
}
}
return null;
}
/**
* 检查用户选择的套装名称是否匹配配置(支持别名)
* @param {string} selectedName - 用户选择的套装名称
* @param {string} recognizedName - OCR 识别到的套装名称
* @param {Array} config - 所有套装配置数组
* @returns {boolean} - 是否匹配
*/
function isSelectedSetMatch(selectedName, recognizedName, config) {
// 精确匹配
if (selectedName === recognizedName) {
return true;
}
// 查找用户选择的套装对应的配置
let setConfig = config.find(s => s.set_name === selectedName);
if (setConfig) {
return matchSetName(recognizedName, setConfig);
}
return false;
}
/**
* 配置单个套装的所有方案
* @param {Object} setConfig - 套装配置对象
* @param {Object} uiCoords - UI坐标映射对象
* @param {boolean} overwriteExisting - 是否覆盖已有方案
* @returns {boolean|null} - true: 成功配置, false: 跳过(已有方案且未勾选覆盖), null: 错误(找不到按钮/无启用方案等)
*/
async function configureArtifactSet(setConfig, uiCoords, overwriteExisting) {
// 根据配置处理推荐方案开关
let shouldEnable = setConfig.enable_recommended_scheme;
log.debug("套装「{name}」的推荐方案配置: {value}", setConfig.set_name, shouldEnable);
// 检查当前界面状态:如果不包含"未生效",说明推荐方案是打开的
let statusResult = findTextInRegion(838, 294, 1136, 351, "未生效");
let isCurrentlyEnabled = (statusResult === null); // 不包含"未生效"说明是打开的
log.debug("当前推荐方案状态: {status}", isCurrentlyEnabled ? "已开启" : "未开启");
// 判断是否需要点击开关
if (shouldEnable !== isCurrentlyEnabled) {
log.info("步骤:切换推荐方案开关");
click(1786, 323);
await sleep(DELAY_MEDIUM);
} else {
log.debug("步骤:推荐方案状态已符合配置,无需操作");
}
// 查找并点击"编辑"按钮
let editResult = findTextInRegion(1686, 208, 1850, 256, "编辑");
if (editResult) {
log.debug("找到「编辑」按钮,坐标: ({x}, {y})", editResult.x, editResult.y);
log.info("步骤:点击编辑按钮");
click(editResult.x, editResult.y);
await sleep(DELAY_MEDIUM);
} else {
log.error("未找到「编辑」按钮");
return null;
}
// 获取所有启用的方案
let enabledPlans = setConfig.plans.filter(p => p.enabled);
if (enabledPlans.length === 0) {
log.warn("套装「{name}」未找到启用的方案,跳过", setConfig.set_name);
// 点击返回按钮
click(1840, 44);
await sleep(DELAY_MEDIUM);
return null;
}
log.debug("找到 {count} 个启用的方案", enabledPlans.length);
// 遍历所有启用的方案
for (let planIndex = 0; planIndex < enabledPlans.length; planIndex++) {
let plan = enabledPlans[planIndex];
log.debug("开始配置第 {index} 个方案", planIndex + 1);
// 如果不是第一个方案需要切换到方案2
if (planIndex > 0) {
log.info("步骤:切换到方案 {index}", planIndex + 1);
click(129, 265);
await sleep(DELAY_LONG);
}
// 检查是否存在旧方案
let noConfigResult = findTextInRegion(1004, 499, 1253, 549, "暂无方案配置");
if (noConfigResult === null) {
// 不包含"暂无方案配置",说明已经有旧方案
if (!overwriteExisting) {
// 未勾选覆盖选项,跳过该套装
log.info("套装「{name}」已有方案配置,且未勾选覆盖选项,跳过", setConfig.set_name);
// 点击返回按钮
click(1840, 44);
await sleep(DELAY_LONG);
return false;
}
// 点击删除按钮
log.info("步骤:点击删除按钮,坐标: (1147, 1022)");
click(1147, 1022);
// 等待一下让删除确认对话框出现
await sleep(DELAY_MEDIUM);
// 点击确认按钮
log.info("步骤:点击确认按钮,坐标: (1171, 758)");
click(1171, 758);
await sleep(DELAY_MEDIUM);
} else {
log.debug("未检测到旧方案(显示「暂无方案配置」),无需删除");
}
// 点击"新建锁定方案"按钮
let newPlanResult = findTextInRegion(1563, 986, 1876, 1051, "新建锁定方案");
if (newPlanResult) {
log.debug("找到「新建锁定方案」按钮,坐标: ({x}, {y})", newPlanResult.x, newPlanResult.y);
log.info("步骤:点击「新建锁定方案」按钮");
click(newPlanResult.x, newPlanResult.y);
await sleep(DELAY_MEDIUM);
} else {
log.error("未找到「新建锁定方案」按钮");
continue;
}
// 配置当前方案
await configurePlan(plan, uiCoords);
log.debug("第 {index} 个方案配置完成", planIndex + 1);
}
// 点击返回按钮
log.info("步骤:点击返回按钮,坐标: (1840, 44)");
click(1840, 44);
await sleep(DELAY_LONG);
log.debug("套装「{name}」配置完成", setConfig.set_name);
return true; // 返回 true 表示成功配置
}
/**
* 配置单个锁定方案
* @param {Object} plan - 方案配置对象
* @param {Object} uiCoords - UI坐标映射对象
*/
async function configurePlan(plan, uiCoords) {
log.debug("开始配置方案: {name}", plan.plan_name);
// 遍历配置中的所有部位不一定是5个
for (let positionConfig of plan.positions) {
// 根据部位名称找到对应的UI坐标
let positionUI = uiCoords.positions.find(p => p.name === positionConfig.position_name);
if (!positionUI) {
log.error("未找到部位「{name}」的UI坐标", positionConfig.position_name);
continue;
}
log.info("配置部位: {name}", positionConfig.position_name);
// 点击部位按钮
click(positionUI.x, positionUI.y);
await sleep(DELAY_SHORT);
// 如果部位未启用,跳过配置
if (!positionConfig.enabled) {
log.info("部位 {name} 未启用,跳过", positionConfig.position_name);
continue;
}
// 配置主属性
log.debug("点击主要属性按钮");
click(uiCoords.buttons.main_attr.x, uiCoords.buttons.main_attr.y);
await sleep(DELAY_SHORT);
// 点击所有需要的主属性
let positionMainAttrs = uiCoords.main_attrs_by_position[positionConfig.position_name];
if (!positionMainAttrs) {
log.error("未找到部位 {name} 的主属性坐标映射", positionConfig.position_name);
continue;
}
// 检查是否包含"元素伤害加成"
let hasElementalDamage = positionConfig.main_attrs.includes("元素伤害加成");
for (let mainAttr of positionConfig.main_attrs) {
// 特殊处理:元素伤害加成 = 使用"全选伤害加成"按钮(包含物理+所有7种元素伤害
if (mainAttr === "元素伤害加成") {
let coord = positionMainAttrs["全选伤害加成"];
log.debug("检测到「元素伤害加成」,使用全选按钮一次性选择所有伤害加成");
log.debug("点击全选伤害加成按钮,坐标: ({x}, {y})", coord.x, coord.y);
click(coord.x, coord.y);
await sleep(DELAY_SHORT);
} else if (mainAttr === "物理伤害加成" && hasElementalDamage) {
// 如果已经点击了"元素伤害加成"(全选),则跳过单独的"物理伤害加成"
log.debug("「物理伤害加成」已被全选包含,跳过");
} else {
// 普通主属性
if (positionMainAttrs[mainAttr]) {
let coord = positionMainAttrs[mainAttr];
log.debug("选择主属性: {attr}, 坐标: ({x}, {y})", mainAttr, coord.x, coord.y);
click(coord.x, coord.y);
await sleep(DELAY_SHORT);
} else {
log.warn("未找到主属性坐标: {attr} (部位: {pos})", mainAttr, positionConfig.position_name);
}
}
}
// 配置副属性
log.debug("点击追加属性按钮");
click(uiCoords.buttons.sub_attr.x, uiCoords.buttons.sub_attr.y);
await sleep(DELAY_SHORT);
// 点击所有需要的副属性
for (let subAttr of positionConfig.sub_attrs) {
if (uiCoords.sub_attrs[subAttr]) {
let coord = uiCoords.sub_attrs[subAttr];
log.debug("选择副属性: {attr}, 坐标: ({x}, {y})", subAttr, coord.x, coord.y);
click(coord.x, coord.y);
await sleep(DELAY_SHORT);
// 如果该副属性是必须条件,点击右侧的必须条件按钮
if (positionConfig.necessary_sub_attrs && positionConfig.necessary_sub_attrs.includes(subAttr)) {
// 必须条件按钮在副属性右侧X 坐标固定:第一列 1090第二列 1810
let necessaryX = coord.x < 800 ? 1090 : 1810;
log.debug("设置必须条件: {attr}, 坐标: ({x}, {y})", subAttr, necessaryX, coord.y);
click(necessaryX, coord.y);
await sleep(DELAY_SHORT);
// 第一次点击必须条件按钮后,检查是否弹出提示框
if (!hasCheckedNecessaryTip) {
hasCheckedNecessaryTip = true;
await sleep(DELAY_MEDIUM);
let tipResult = findTextInRegion(870, 261, 1069, 338, "提示");
if (tipResult) {
log.debug("检测到提示框,点击确认关闭");
click(964, 745);
await sleep(DELAY_MEDIUM);
}
}
}
} else {
log.warn("未找到副属性坐标: {attr}", subAttr);
}
}
// 配置副属性最小命中数
let minCount = positionConfig.min_count;
let countKey = `任意${minCount}`;
if (uiCoords.sub_attr_count[countKey]) {
let coord = uiCoords.sub_attr_count[countKey];
log.debug("选择副属性命中数: {count}, 坐标: ({x}, {y})", countKey, coord.x, coord.y);
click(coord.x, coord.y);
await sleep(DELAY_SHORT);
} else {
log.warn("未找到副属性命中数坐标: {count}", countKey);
}
log.debug("部位 {name} 配置完成", positionConfig.position_name);
}
// 点击保存按钮
log.info("步骤:点击保存按钮,坐标: (1733, 1017)");
click(1733, 1017);
await sleep(DELAY_LONG);
// 再点一次关闭弹窗
log.info("步骤:关闭保存按钮的弹窗");
click(1733, 1017);
await sleep(DELAY_LONG);
}
async function main() {
// 运行前检查游戏窗口信息,并验证分辨率是否为 16:9
const width = genshin.width;
const height = genshin.height;
if (!width || !height) {
log.error("无法获取游戏窗口信息,请确保原神正在运行,脚本已终止");
return;
}
const aspectRatio = width / height;
const targetRatio = 16 / 9;
const ratioTolerance = 0.01; // 允许极小的浮点误差
log.debug("环境检测:当前游戏分辨率 {w}x{h}", width, height);
if (Math.abs(aspectRatio - targetRatio) > ratioTolerance) {
log.error("当前分辨率不是 16:9检测到: {w}x{h}),脚本已终止", width, height);
return;
}
// 读取配置文件
const configText = await file.readText("圣遗物锁定方案信息.json");
const config = JSON.parse(configText);
// 读取UI坐标映射文件
const uiCoordsText = await file.readText("ui_coordinates.json");
const uiCoords = JSON.parse(uiCoordsText);
// 读取用户设置
let selectedSets = settings.artifactSets ? Array.from(settings.artifactSets) : []; // C# List<string> 对象,需要转换为 JS 数组
let overwriteExisting = settings.overwriteExisting; // 是否覆盖已有方案
log.debug("用户选择的套装数量: {count}", selectedSets.length);
log.debug("选择的套装: {sets}", JSON.stringify(selectedSets));
log.debug("是否覆盖已有方案: {value}", overwriteExisting);
// 如果选择了"全选",需要读取 settings.json 获取所有套装列表
if (selectedSets.includes("全选")) {
const settingsText = await file.readText("settings.json");
const settingsConfig = JSON.parse(settingsText);
const artifactSetsConfig = settingsConfig.find(s => s.name === "artifactSets");
if (artifactSetsConfig && artifactSetsConfig.options) {
// 包含所有套装(除了"全选"本身)
selectedSets = artifactSetsConfig.options.filter(name => name !== "全选");
log.debug("检测到「全选」,已展开为所有套装,共 {count} 个", selectedSets.length);
}
}
// 检查用户是否选择了至少一个套装
if (selectedSets.length === 0) {
log.error("未选择任何套装,请在设置中勾选需要配置的圣遗物套装");
return;
}
// 返回主界面
await genshin.returnMainUi();
// 打开背包
keyPress("B")
await sleep(DELAY_LONG);
log.info("即将配置 {count} 个套装", selectedSets.length);
// 第一步:点击圣遗物背包页面
log.info("步骤:点击圣遗物背包页面");
click(671, 46);
await sleep(DELAY_MEDIUM);
// 第二步:验证是否进入圣遗物页面
let verifyResult = findTextInRegion(131, 21, 231, 70, "圣遗物");
if (!verifyResult) {
log.error("未检测到「圣遗物」页面");
return;
}
// 第三步:点击进入圣遗物锁定方案入口
log.info("步骤:点击「锁定辅助」");
click(394, 1017);
await sleep(DELAY_LONG);
// 第四步:查找并点击 "套装锁定方案",来到套装选择界面
log.info("步骤:点击「套装锁定方案」");
let result = findTextInRegion(148, 696, 322, 760, "套装锁定方案");
if (!result) {
// 第一个位置没找到,尝试第二个位置
result = findTextInRegion(146, 290, 332, 366, "套装锁定方案");
}
if (result) {
log.debug("点击「套装锁定方案」,坐标: ({x}, {y})", result.x, result.y);
click(result.x, result.y);
await sleep(DELAY_LONG);
} else {
log.error("未找到「套装锁定方案」按钮");
return;
}
// 第五步:把滚动条拉到顶部
log.info("步骤:点击滚动条顶部");
click(795, 212);
await sleep(DELAY_MEDIUM);
// 第六步:遍历所有套装
let processedSets = new Set(); // 记录已处理的套装名称,避免重复
let configuredSets = []; // 记录成功配置的套装名称
let skippedSets = []; // 记录跳过的套装名称(已有方案且未勾选覆盖)
let notFoundSets = []; // 记录未找到的套装名称OCR 未识别到)
let failedSets = []; // 记录处理失败的套装名称(执行过程中出错)
let finishedSetNames = new Set(); // 记录已完成处理的套装名称(成功/跳过/失败)
while (true) {
log.info("步骤:开始识别当前屏幕的套装");
// 获取游戏画面截图
let screen = captureGameRegion();
let listRegion = null;
try {
// 裁剪套装列表区域 (142,209) 到 (384,954)
const cropStartX = 142;
const cropStartY = 209;
const cropEndX = 384;
const cropEndY = 954;
listRegion = screen.deriveCrop(cropStartX, cropStartY, cropEndX - cropStartX, cropEndY - cropStartY);
// 使用 findMulti 进行 OCR 识别
let ocrResultList = listRegion.findMulti(RecognitionObject.ocrThis);
log.debug("识别到 {count} 个文本", ocrResultList.count);
if (ocrResultList.count === 0) {
log.warn("未识别到任何文本,列表遍历完成");
break;
}
// 记录本轮识别到的套装名称
let currentScreenSets = [];
for (let i = 0; i < ocrResultList.count; i++) {
currentScreenSets.push(ocrResultList[i].text.trim());
}
// 检查是否所有套装都已处理过(说明列表已经到底了)
let allProcessed = currentScreenSets.every(name => processedSets.has(name));
if (allProcessed) {
log.info("当前屏幕所有套装都已处理过,列表遍历完成");
break;
}
// 遍历识别到的所有文本,处理套装
for (let i = 0; i < ocrResultList.count; i++) {
let ocrResult = ocrResultList[i];
let setName = ocrResult.text.trim();
// 跳过已处理的套装
if (processedSets.has(setName)) {
log.debug("套装「{name}」已处理过,跳过", setName);
continue;
}
log.debug("识别到套装名称: {name}", setName);
// 检查该套装是否在用户选择的列表中(支持别名匹配)
let matchedSelectedName = selectedSets.find(selected => isSelectedSetMatch(selected, setName, config));
if (!matchedSelectedName) {
log.debug("套装「{name}」不在用户选择列表中,跳过", setName);
processedSets.add(setName);
continue;
}
// 在配置中查找对应的套装(支持别名匹配)
let setConfig = findSetConfig(setName, config);
// 标记为已处理(无论是否有配置)
processedSets.add(setName);
if (setConfig) {
log.info(`开始处理套装: 「${setConfig.set_name}`);
// 计算文本左上角的屏幕绝对坐标
let absoluteX = cropStartX + ocrResult.x;
let absoluteY = cropStartY + ocrResult.y;
// 点击套装名称进入详情页
log.debug("点击套装,坐标: ({x}, {y})", absoluteX, absoluteY);
click(absoluteX, absoluteY);
await sleep(DELAY_MEDIUM);
// 配置该套装
let configured = await configureArtifactSet(setConfig, uiCoords, overwriteExisting);
if (configured === true) {
configuredSets.push(setConfig.set_name);
finishedSetNames.add(setConfig.set_name);
log.info("套装「{name}」处理完成,进度: {current}/{total}", setConfig.set_name, configuredSets.length, selectedSets.length);
// 快速结算:如果已完成数量达到用户选择的数量,提前结束
if (finishedSetNames.size >= selectedSets.length) {
log.info("已处理完所有选择的套装(含跳过/失败)");
break;
}
} else if (configured === false) {
skippedSets.push(setConfig.set_name);
finishedSetNames.add(setConfig.set_name);
log.info("套装「{name}」已跳过(已有方案且未勾选覆盖)", setConfig.set_name);
} else {
failedSets.push(setConfig.set_name);
finishedSetNames.add(setConfig.set_name);
log.error("套装「{name}」处理失败(执行过程中出现错误)", setConfig.set_name);
}
} else {
log.info("配置文件中未找到套装「{name}」,跳过", setName);
}
}
// 快速结算:如果已完成数量达到用户选择的数量,跳出外层循环
if (finishedSetNames.size >= selectedSets.length) {
break;
}
} finally {
// 释放图像资源
if (listRegion) {
listRegion.dispose();
}
screen.dispose();
}
await sleep(DELAY_LONG);
// 处理完当前屏幕的所有套装后,平滑拖动列表显示下一批
log.info("当前屏幕处理完成,拖动列表显示下一批套装");
// 从 (771,856) 移动到 (770,244)
await smoothDragVertical(771, 856, 770, 244);
}
// 检查哪些用户选择的套装未被识别到
for (let selectedName of selectedSets) {
// 检查是否已成功配置或已跳过
if (configuredSets.includes(selectedName) || skippedSets.includes(selectedName) || failedSets.includes(selectedName)) {
continue;
}
// 检查是否通过别名匹配到了
let foundByAlias = configuredSets.some(name => {
let cfg = config.find(c => c.set_name === name);
return cfg && cfg.set_name !== selectedName && matchSetName(selectedName, cfg);
}) || skippedSets.some(name => {
let cfg = config.find(c => c.set_name === name);
return cfg && cfg.set_name !== selectedName && matchSetName(selectedName, cfg);
}) || failedSets.some(name => {
let cfg = config.find(c => c.set_name === name);
return cfg && cfg.set_name !== selectedName && matchSetName(selectedName, cfg);
});
if (!foundByAlias) {
notFoundSets.push(selectedName);
}
}
// 输出结果汇总
log.info("========== 配置结果汇总 ==========");
log.info("成功配置: {count} 个套装", configuredSets.length);
if (skippedSets.length > 0) {
log.info("跳过(已有方案): {count} 个套装", skippedSets.length);
for (let name of skippedSets) {
log.info(" - {name}", name);
}
}
if (failedSets.length > 0) {
log.error("处理失败: {count} 个套装", failedSets.length);
for (let name of failedSets) {
log.error(" ✗ {name}", name);
}
}
if (notFoundSets.length > 0) {
log.warn("未能设置: {count} 个套装(未在列表中识别到)", notFoundSets.length);
for (let name of notFoundSets) {
log.warn(" ✗ {name}", name);
}
}
log.info("所有套装配置完成!");
}
main();

View File

@@ -0,0 +1,22 @@
{
"manifest_version": 1,
"name": "自动配置圣遗物锁定方案",
"version": "1.0.0",
"bgi_version": "0.56.2",
"description": "自动配置圣遗物锁定方案,方案来源于 哔哩哔哩 @酸儒书生 https://www.bilibili.com/video/BV13YHQzMEAT",
"authors": [
{
"name": "bling-yshs",
"link": "https://github.com/bling-yshs"
},
{
"name": "Bedrockx",
"link": "https://github.com/Bedrockx"
}
],
"settings_ui": "settings.json",
"main": "main.js",
"library": [
"."
]
}

View File

@@ -0,0 +1,63 @@
[
{
"name": "overwriteExisting",
"type": "checkbox",
"label": "覆盖已有方案",
"default": false
},
{
"name": "delayMultiplier",
"type": "input-text",
"label": "延迟系数留空为1.2如果发现运行不稳定可以尝试填1.4或者更高"
},
{
"name": "artifactSets",
"type": "multi-checkbox",
"label": "需要配置的圣遗物套装",
"options": [
"全选",
"风起之日",
"晨星与月的晓歌",
"纺月的夜歌",
"穹境示现之夜",
"深廊终曲",
"长夜之誓",
"黑曜秘典",
"烬城勇者绘卷",
"未竟的遐思",
"谐律异想断章",
"回声之林夜话",
"昔时之歌",
"黄金剧团",
"逐影猎人",
"花海甘露之光",
"水仙之梦",
"乐园遗落之花",
"沙上楼阁史话",
"饰金之梦",
"深林的记忆",
"来歆余响",
"辰砂往生录",
"海染砗磲",
"华馆梦醒形骸记",
"绝缘之旗印",
"追忆之注连",
"苍白之火",
"千岩牢固",
"沉沦之心",
"冰风迷途的勇士",
"炽烈的炎之魔女",
"渡过烈火的贤人",
"如雷的盛怒",
"平息鸣雷的尊者",
"逆飞的流星",
"悠古的磐岩",
"翠绿之影",
"被怜爱的少女",
"染血的骑士道",
"昔日宗室之仪",
"流浪大地的乐团",
"角斗士的终幕礼"
]
}
]

View File

@@ -0,0 +1,225 @@
{
"positions": [
{
"name": "生之花",
"index": 0,
"x": 550,
"y": 141
},
{
"name": "死之羽",
"index": 1,
"x": 842,
"y": 142
},
{
"name": "时之沙",
"index": 2,
"x": 1126,
"y": 143
},
{
"name": "空之杯",
"index": 3,
"x": 1417,
"y": 141
},
{
"name": "理之冠",
"index": 4,
"x": 1703,
"y": 144
}
],
"buttons": {
"main_attr": {
"x": 770,
"y": 221
},
"sub_attr": {
"x": 1492,
"y": 218
}
},
"main_attrs_by_position": {
"生之花": {
"生命值": {
"x": 440,
"y": 310
}
},
"死之羽": {
"攻击力": {
"x": 440,
"y": 310
}
},
"时之沙": {
"生命值百分比": {
"x": 440,
"y": 310
},
"攻击力百分比": {
"x": 1160,
"y": 310
},
"防御力百分比": {
"x": 440,
"y": 390
},
"元素精通": {
"x": 1160,
"y": 390
},
"元素充能效率": {
"x": 440,
"y": 470
}
},
"空之杯": {
"生命值百分比": {
"x": 440,
"y": 310
},
"攻击力百分比": {
"x": 1160,
"y": 310
},
"防御力百分比": {
"x": 440,
"y": 390
},
"元素精通": {
"x": 1160,
"y": 390
},
"全选伤害加成": {
"x": 440,
"y": 500,
"note": "一次性选择物理伤害加成和所有7种元素伤害加成"
},
"物理伤害加成": {
"x": 440,
"y": 580
},
"火元素伤害加成": {
"x": 1160,
"y": 580
},
"雷元素伤害加成": {
"x": 440,
"y": 660
},
"水元素伤害加成": {
"x": 1160,
"y": 660
},
"草元素伤害加成": {
"x": 440,
"y": 740
},
"风元素伤害加成": {
"x": 1160,
"y": 740
},
"岩元素伤害加成": {
"x": 440,
"y": 820
},
"冰元素伤害加成": {
"x": 1160,
"y": 820
}
},
"理之冠": {
"生命值百分比": {
"x": 440,
"y": 310
},
"攻击力百分比": {
"x": 1160,
"y": 310
},
"防御力百分比": {
"x": 440,
"y": 390
},
"元素精通": {
"x": 1160,
"y": 390
},
"暴击率": {
"x": 440,
"y": 470
},
"暴击伤害": {
"x": 1160,
"y": 470
},
"治疗加成": {
"x": 440,
"y": 550
}
}
},
"sub_attrs": {
"生命值": {
"x": 440,
"y": 360
},
"生命值百分比": {
"x": 1160,
"y": 360
},
"攻击力": {
"x": 440,
"y": 440
},
"攻击力百分比": {
"x": 1160,
"y": 440
},
"防御力": {
"x": 440,
"y": 520
},
"防御力百分比": {
"x": 1160,
"y": 520
},
"暴击率": {
"x": 440,
"y": 600
},
"暴击伤害": {
"x": 1160,
"y": 600
},
"元素精通": {
"x": 440,
"y": 680
},
"元素充能效率": {
"x": 1160,
"y": 680
}
},
"sub_attr_count": {
"任意1条": {
"x": 440,
"y": 810
},
"任意2条": {
"x": 1160,
"y": 810
},
"任意3条": {
"x": 440,
"y": 890
},
"任意4条": {
"x": 1160,
"y": 890
}
}
}

File diff suppressed because it is too large Load Diff