Files
bettergi-scripts-list/repo/js/AutoArtifactLockingSettings/main.js
bling-yshs f9a6dd519f feat: 新增自动配置圣遗物锁定方案 (#2850)
* feat: 新增自动配置圣遗物锁定方案

* fix: clean

* fix: 截图资源释放

* chore: library 清理

* chore: README 删除冒号

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

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

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

* chore: 默认延迟系数调整为 1.2
2026-02-07 23:03:49 +08:00

622 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();