mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-23 04:39:51 +08:00
feat: 新增自动配置圣遗物锁定方案 (#2850)
* feat: 新增自动配置圣遗物锁定方案 * fix: clean * fix: 截图资源释放 * chore: library 清理 * chore: README 删除冒号 * fix: 区分套装配置失败与跳过状态,修正结果汇总误报 * fix: 以已完成套装数提前结束,避免跳过后继续滚动列表 * feat: 分辨率检测,自动返回主界面打开背包,README 修改 * chore: 默认延迟系数调整为 1.2
This commit is contained in:
62
repo/js/AutoArtifactLockingSettings/README.md
Normal file
62
repo/js/AutoArtifactLockingSettings/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
<h1 align="center">🔒 自动配置圣遗物锁定方案</h1>
|
||||
|
||||
## ✨ 脚本简介
|
||||
|
||||
本脚本用于**自动配置圣遗物锁定方案**。
|
||||
|
||||
锁定方案来源于 哔哩哔哩 @酸儒书生 的 [这期视频](https://www.bilibili.com/video/BV13YHQzMEAT),脚本会自动识别游戏界面并完成所有配置操作,省去手动逐个设置的繁琐过程。
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
- 🎯 **自动识别套装** - 通过 OCR 自动识别当前界面的圣遗物套装
|
||||
- ⚙️ **批量配置方案** - 支持一次性配置多个套装的锁定方案
|
||||
- 🔄 **覆盖已有方案** - 可选择是否覆盖已存在的锁定方案
|
||||
- ⏱️ **延迟系数调节** - 支持自定义延迟系数,适配不同性能设备
|
||||
|
||||
## 📖 使用步骤
|
||||
|
||||
### 1️⃣ 前置要求
|
||||
|
||||
- 游戏分辨率设置为 **16:9** (2560x1440 或 1920x1080)
|
||||
|
||||
- 游戏语言设置为**简体中文**
|
||||
|
||||
- 圣遗物「锁定辅助」页面的「通用锁定方案」勾选上1、3、4项,如图所示
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
BIN
repo/js/AutoArtifactLockingSettings/assets/global-lock.jpg
Normal file
BIN
repo/js/AutoArtifactLockingSettings/assets/global-lock.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
96
repo/js/AutoArtifactLockingSettings/lib.js
Normal file
96
repo/js/AutoArtifactLockingSettings/lib.js
Normal 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("拖动完成");
|
||||
};
|
||||
621
repo/js/AutoArtifactLockingSettings/main.js
Normal file
621
repo/js/AutoArtifactLockingSettings/main.js
Normal 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();
|
||||
22
repo/js/AutoArtifactLockingSettings/manifest.json
Normal file
22
repo/js/AutoArtifactLockingSettings/manifest.json
Normal 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": [
|
||||
"."
|
||||
]
|
||||
}
|
||||
63
repo/js/AutoArtifactLockingSettings/settings.json
Normal file
63
repo/js/AutoArtifactLockingSettings/settings.json
Normal 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": [
|
||||
"全选",
|
||||
"风起之日",
|
||||
"晨星与月的晓歌",
|
||||
"纺月的夜歌",
|
||||
"穹境示现之夜",
|
||||
"深廊终曲",
|
||||
"长夜之誓",
|
||||
"黑曜秘典",
|
||||
"烬城勇者绘卷",
|
||||
"未竟的遐思",
|
||||
"谐律异想断章",
|
||||
"回声之林夜话",
|
||||
"昔时之歌",
|
||||
"黄金剧团",
|
||||
"逐影猎人",
|
||||
"花海甘露之光",
|
||||
"水仙之梦",
|
||||
"乐园遗落之花",
|
||||
"沙上楼阁史话",
|
||||
"饰金之梦",
|
||||
"深林的记忆",
|
||||
"来歆余响",
|
||||
"辰砂往生录",
|
||||
"海染砗磲",
|
||||
"华馆梦醒形骸记",
|
||||
"绝缘之旗印",
|
||||
"追忆之注连",
|
||||
"苍白之火",
|
||||
"千岩牢固",
|
||||
"沉沦之心",
|
||||
"冰风迷途的勇士",
|
||||
"炽烈的炎之魔女",
|
||||
"渡过烈火的贤人",
|
||||
"如雷的盛怒",
|
||||
"平息鸣雷的尊者",
|
||||
"逆飞的流星",
|
||||
"悠古的磐岩",
|
||||
"翠绿之影",
|
||||
"被怜爱的少女",
|
||||
"染血的骑士道",
|
||||
"昔日宗室之仪",
|
||||
"流浪大地的乐团",
|
||||
"角斗士的终幕礼"
|
||||
]
|
||||
}
|
||||
]
|
||||
225
repo/js/AutoArtifactLockingSettings/ui_coordinates.json
Normal file
225
repo/js/AutoArtifactLockingSettings/ui_coordinates.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
5872
repo/js/AutoArtifactLockingSettings/圣遗物锁定方案信息.json
Normal file
5872
repo/js/AutoArtifactLockingSettings/圣遗物锁定方案信息.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user