mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-15 03:23:22 +08:00
feat: 添加工具函数 (#2823)
This commit is contained in:
2
.github/workflows/build_release_branch.yml
vendored
2
.github/workflows/build_release_branch.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
git rm -rf .
|
||||
|
||||
echo "📦 检出 main 分支的内容到工作区"
|
||||
git checkout main -- repo .gitignore
|
||||
git checkout main -- repo packages .gitignore
|
||||
|
||||
echo "📋 复制生成的文件到 release 分支"
|
||||
cp ../repo.json .
|
||||
|
||||
189
build/dev_deploy.js
Normal file
189
build/dev_deploy.js
Normal file
@@ -0,0 +1,189 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ================= 使用说明 =================
|
||||
// 1. 请确保你是以bettergi-script-list完整仓库的环境运行此脚本
|
||||
// 2. 请确保你本地配置了node.js环境
|
||||
// 3. 运行: node dev_deploy.js 脚本文件夹名 BGI目录。例:node build/dev_deploy.js test E:\BetterGIProject\BetterGI
|
||||
// 4. 脚本自动导入,会删除原有packages后导入新的packages,其他文件覆盖式导入
|
||||
// ===========================================
|
||||
|
||||
// 脚本名称(repo/js 下的文件夹名)
|
||||
const SCRIPT_NAME = process.argv[2];
|
||||
|
||||
// BetterGI 软件根目录(包含 User 文件夹)
|
||||
const BETTERGI_ROOT = process.argv[3];
|
||||
|
||||
if (!SCRIPT_NAME || !BETTERGI_ROOT) {
|
||||
console.error('❌ 参数不足。');
|
||||
console.error('用法: node dev_deploy.js <script_folder_name> <bettergi_root_path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 路径定义
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
const SOURCE_SCRIPT_DIR = path.join(REPO_ROOT, 'repo', 'js', SCRIPT_NAME);
|
||||
const TARGET_SCRIPT_DIR = path.join(
|
||||
path.resolve(BETTERGI_ROOT),
|
||||
'User',
|
||||
'JsScript',
|
||||
SCRIPT_NAME
|
||||
);
|
||||
|
||||
const PROCESSED_PACKAGES = new Set();
|
||||
const PROCESSED_FILES = new Set(); // 避免循环扫描
|
||||
|
||||
function main() {
|
||||
console.log(`📂 源目录: ${SOURCE_SCRIPT_DIR}`);
|
||||
console.log(`📂 目标目录: ${TARGET_SCRIPT_DIR}`);
|
||||
|
||||
if (!fs.existsSync(SOURCE_SCRIPT_DIR)) {
|
||||
console.error(`❌ 错误: 找不到本地脚本目录: ${SOURCE_SCRIPT_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 复制主脚本目录
|
||||
if (!fs.existsSync(TARGET_SCRIPT_DIR)) {
|
||||
fs.mkdirSync(TARGET_SCRIPT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 清理旧的 packages 目录,防止残留
|
||||
const targetPackagesDir = path.join(TARGET_SCRIPT_DIR, 'packages');
|
||||
if (fs.existsSync(targetPackagesDir)) {
|
||||
console.log('🧹 清理旧依赖目录...');
|
||||
fs.rmSync(targetPackagesDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
copyDir(SOURCE_SCRIPT_DIR, TARGET_SCRIPT_DIR);
|
||||
|
||||
// 解析依赖
|
||||
console.log('🔍 解析依赖并注入 packages...');
|
||||
resolveDependenciesRecursively(TARGET_SCRIPT_DIR);
|
||||
|
||||
console.log('✅ 部署完成!可在 BetterGI 中运行测试。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归扫描目录中的 JS 文件并处理依赖
|
||||
* @param {string} dir
|
||||
*/
|
||||
function resolveDependenciesRecursively(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
resolveDependenciesRecursively(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
||||
processJsFile(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个 JS 文件
|
||||
* @param {string} filePath
|
||||
*/
|
||||
function processJsFile(filePath) {
|
||||
if (PROCESSED_FILES.has(filePath)) return;
|
||||
PROCESSED_FILES.add(filePath);
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 匹配 import
|
||||
const regex = /(import\s+(?:[\w\s{},*]*?from\s+)?['"]|require\s*\(\s*['"]|import\s+['"])([^'"]+)(['"])/g;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const importPath = match[2]; // Group 2 is the path
|
||||
|
||||
// 处理显式 packages/ 引用
|
||||
const packageIndex = importPath.indexOf('packages/');
|
||||
if (packageIndex >= 0) {
|
||||
let packagePath = importPath.substring(packageIndex);
|
||||
|
||||
// 复制且(如果是JS)递归处理
|
||||
copyPackageResource(packagePath);
|
||||
}
|
||||
else if (importPath.startsWith('.')) {
|
||||
// 处理 packages 内部的相对引用
|
||||
const targetPackagesDir = path.join(TARGET_SCRIPT_DIR, 'packages');
|
||||
if (filePath.startsWith(targetPackagesDir)) {
|
||||
const relToScript = path.relative(TARGET_SCRIPT_DIR, filePath);
|
||||
const relDir = path.dirname(relToScript);
|
||||
let depPackagePath = path.join(relDir, importPath);
|
||||
depPackagePath = depPackagePath.split(path.sep).join('/');
|
||||
|
||||
if (depPackagePath.startsWith('packages/')) {
|
||||
copyPackageResource(depPackagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从源仓库复制 package 资源到目标位置
|
||||
* @param {string} packagePath 相对路径,如 "packages/utils/test"
|
||||
*/
|
||||
function copyPackageResource(packagePath) {
|
||||
if (tryCopy(packagePath)) return;
|
||||
if (!packagePath.endsWith('.js') && tryCopy(packagePath + '.js')) return;
|
||||
console.warn(`⚠️ 警告: 未找到依赖资源 ${packagePath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试复制文件或目录
|
||||
* @param {string} relPath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function tryCopy(relPath) {
|
||||
const src = path.join(REPO_ROOT, relPath);
|
||||
|
||||
if (!fs.existsSync(src)) return false;
|
||||
|
||||
if (PROCESSED_PACKAGES.has(relPath)) return true;
|
||||
|
||||
const dest = path.join(TARGET_SCRIPT_DIR, relPath);
|
||||
const destDir = path.dirname(dest);
|
||||
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
}
|
||||
|
||||
const stat = fs.statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
copyDir(src, dest);
|
||||
resolveDependenciesRecursively(dest);
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
// 如果是 JS 文件,需要递归解析它的依赖
|
||||
if (dest.endsWith('.js')) {
|
||||
processJsFile(dest);
|
||||
}
|
||||
}
|
||||
|
||||
PROCESSED_PACKAGES.add(relPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录
|
||||
* @param {string} src
|
||||
* @param {string} dest
|
||||
*/
|
||||
function copyDir(src, dest) {
|
||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDir(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
2053
packages/assets/files/combat_avatar.json
Normal file
2053
packages/assets/files/combat_avatar.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
packages/assets/imgs/esc_settings.png
Normal file
BIN
packages/assets/imgs/esc_settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
packages/assets/imgs/girl_moon.png
Normal file
BIN
packages/assets/imgs/girl_moon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/assets/imgs/page_close_white.png
Normal file
BIN
packages/assets/imgs/page_close_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
packages/assets/imgs/paimon_menu.png
Normal file
BIN
packages/assets/imgs/paimon_menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/assets/imgs/primogem.png
Normal file
BIN
packages/assets/imgs/primogem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
packages/assets/imgs/welkin_moon_logo.png
Normal file
BIN
packages/assets/imgs/welkin_moon_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
561
packages/utils/tool.js
Normal file
561
packages/utils/tool.js
Normal file
@@ -0,0 +1,561 @@
|
||||
import paimon from '../assets/imgs/paimon_menu.png';
|
||||
import girl_moon from '../assets/imgs/girl_moon.png';
|
||||
import primogem from '../assets/imgs/primogem.png';
|
||||
import welkin_moon_logo from '../assets/imgs/welkin_moon_logo.png';
|
||||
|
||||
/**
|
||||
* 获取图片 Mat(支持单路径 / 路径数组)
|
||||
*
|
||||
* @param {string|string[]} path 图片路径或路径数组
|
||||
* @returns {Mat|Mat[]} OpenCV Mat 或 Mat 数组
|
||||
*/
|
||||
function getImgMat(path) {
|
||||
if (path == null) {
|
||||
throw new Error('getImgMat: path 不能为空');
|
||||
}
|
||||
|
||||
// 数组形式
|
||||
if (Array.isArray(path)) {
|
||||
return path.map((p, index) => {
|
||||
if (typeof p !== 'string' || !p) {
|
||||
throw new Error(`getImgMat: path[${index}] 不是有效字符串`);
|
||||
}
|
||||
return file.readImageMatSync(p);
|
||||
});
|
||||
}
|
||||
|
||||
// 单个路径
|
||||
if (typeof path !== 'string') {
|
||||
throw new Error('getImgMat: path 必须是字符串或字符串数组');
|
||||
}
|
||||
|
||||
return file.readImageMatSync(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用找图/找RO(支持图片文件路径、Mat)
|
||||
* @param {string|Mat} target 图片路径或已构造的 Mat
|
||||
* @param {number} [x=0] 识别区域左上角 X
|
||||
* @param {number} [y=0] 识别区域左上角 Y
|
||||
* @param {number} [w=1920] 识别区域宽度
|
||||
* @param {number} [h=1080] 识别区域高度
|
||||
* @param {number} [timeout=1000] 识别时间上限(毫秒)
|
||||
* @param {number} [interval=50] 每次识别之间的等待间隔(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function findImg(
|
||||
target,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
timeout = 1000,
|
||||
interval = 50
|
||||
) {
|
||||
const ro =
|
||||
typeof target === 'string'
|
||||
? RecognitionObject.TemplateMatch(
|
||||
file.readImageMatSync(target),
|
||||
x, y, w, h
|
||||
)
|
||||
: RecognitionObject.TemplateMatch(
|
||||
target,
|
||||
x, y, w, h
|
||||
);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start <= timeout) {
|
||||
const gameRegion = captureGameRegion();
|
||||
try {
|
||||
const res = gameRegion.find(ro);
|
||||
if (!res.isEmpty()) {
|
||||
return res;
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e.toString());
|
||||
} finally {
|
||||
gameRegion.dispose();
|
||||
}
|
||||
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用找图并点击(支持图片文件路径、Mat)
|
||||
* @param {string|Mat} target 图片路径或已构造的 Mat
|
||||
* @param {number} [x=0] 识别区域左上角 X
|
||||
* @param {number} [y=0] 识别区域左上角 Y
|
||||
* @param {number} [w=1920] 识别区域宽度
|
||||
* @param {number} [h=1080] 识别区域高度
|
||||
* @param {number} [timeout=1000] 识别时间上限(毫秒)
|
||||
* @param {number} [interval=50] 每次识别之间的等待间隔(毫秒)
|
||||
* @param {number} [preClickDelay=50] 点击前等待时间(毫秒)
|
||||
* @param {number} [postClickDelay=50] 点击后等待时间(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function findImgAndClick(
|
||||
target,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
timeout = 1000,
|
||||
interval = 50,
|
||||
preClickDelay = 50,
|
||||
postClickDelay = 50
|
||||
) {
|
||||
const ro =
|
||||
typeof target === 'string'
|
||||
? RecognitionObject.TemplateMatch(
|
||||
file.readImageMatSync(target),
|
||||
x, y, w, h
|
||||
)
|
||||
: RecognitionObject.TemplateMatch(
|
||||
target,
|
||||
x, y, w, h
|
||||
);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start <= timeout) {
|
||||
const gameRegion = captureGameRegion();
|
||||
try {
|
||||
const res = gameRegion.find(ro);
|
||||
if (!res.isEmpty()) {
|
||||
await sleep(preClickDelay);
|
||||
res.click();
|
||||
await sleep(postClickDelay);
|
||||
return res;
|
||||
}
|
||||
} finally {
|
||||
gameRegion.dispose();
|
||||
}
|
||||
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用找文本(OCR)
|
||||
* @param {string|string[]} text 目标文本(单个文本或文本列表,列表时需全部匹配)
|
||||
* @param {number} [x=0] OCR 区域左上角 X
|
||||
* @param {number} [y=0] OCR 区域左上角 Y
|
||||
* @param {number} [w=1920] OCR 区域宽度
|
||||
* @param {number} [h=1080] OCR 区域高度
|
||||
* @param {number} [attempts=5] OCR 尝试次数
|
||||
* @param {number} [interval=50] 每次 OCR 之间的等待间隔(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function findText(
|
||||
text,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
attempts = 5,
|
||||
interval = 50
|
||||
) {
|
||||
const keywords = (Array.isArray(text) ? text : [text])
|
||||
.map(t => t.toLowerCase());
|
||||
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
const gameRegion = captureGameRegion();
|
||||
try {
|
||||
const ro = RecognitionObject.Ocr(x, y, w, h);
|
||||
const results = gameRegion.findMulti(ro);
|
||||
|
||||
for (let j = 0; j < results.count; j++) {
|
||||
const res = results[j];
|
||||
if (!res.isExist() || !res.text) continue;
|
||||
|
||||
const ocrText = res.text.toLowerCase();
|
||||
const matched = keywords.every(k => ocrText.includes(k));
|
||||
if (matched) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
gameRegion.dispose();
|
||||
}
|
||||
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用找文本并点击(OCR)
|
||||
* @param {string|string[]} text 目标文本(单个文本或文本列表,列表时需全部匹配)
|
||||
* @param {number} [x=0] OCR 区域左上角 X
|
||||
* @param {number} [y=0] OCR 区域左上角 Y
|
||||
* @param {number} [w=1920] OCR 区域宽度
|
||||
* @param {number} [h=1080] OCR 区域高度
|
||||
* @param {number} [attempts=5] OCR 尝试次数
|
||||
* @param {number} [interval=50] 每次 OCR 之间的等待间隔(毫秒)
|
||||
* @param {number} [preClickDelay=50] 点击前等待时间(毫秒)
|
||||
* @param {number} [postClickDelay=50] 点击后等待时间(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function findTextAndClick(
|
||||
text,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
attempts = 5,
|
||||
interval = 50,
|
||||
preClickDelay = 50,
|
||||
postClickDelay = 50
|
||||
) {
|
||||
const keyword = text.toLowerCase();
|
||||
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
const gameRegion = captureGameRegion();
|
||||
try {
|
||||
const ro = RecognitionObject.Ocr(x, y, w, h);
|
||||
const results = gameRegion.findMulti(ro);
|
||||
|
||||
for (let j = 0; j < results.count; j++) {
|
||||
const res = results[j];
|
||||
if (
|
||||
res.isExist() &&
|
||||
res.text &&
|
||||
res.text.toLowerCase().includes(keyword)
|
||||
) {
|
||||
await sleep(preClickDelay);
|
||||
res.click();
|
||||
await sleep(postClickDelay);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
gameRegion.dispose();
|
||||
}
|
||||
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作直到图片出现
|
||||
* @param {string|Mat} target 目标图片路径或 Mat
|
||||
* @param {() => Promise<void>} action 执行的操作函数
|
||||
* @param {number} [x=0] 识别区域左上角 X
|
||||
* @param {number} [y=0] 识别区域左上角 Y
|
||||
* @param {number} [w=1920] 识别区域宽度
|
||||
* @param {number} [h=1080] 识别区域高度
|
||||
* @param {number} [timeout=5000] 超时时间(毫秒)
|
||||
* @param {number} [interval=50] 操作和识别间隔(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function waitUntilImgAppear(
|
||||
target,
|
||||
action,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
timeout = 5000,
|
||||
interval = 50
|
||||
) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start <= timeout) {
|
||||
await action();
|
||||
const res = await findImg(target, x, y, w, h, interval);
|
||||
if (res) return res;
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作直到图片消失
|
||||
* @param {string|Mat} target 目标图片路径或 Mat
|
||||
* @param {() => Promise<void>} action 执行的操作函数
|
||||
* @param {number} [x=0] 识别区域左上角 X
|
||||
* @param {number} [y=0] 识别区域左上角 Y
|
||||
* @param {number} [w=1920] 识别区域宽度
|
||||
* @param {number} [h=1080] 识别区域高度
|
||||
* @param {number} [timeout=5000] 超时时间(毫秒)
|
||||
* @param {number} [interval=50] 操作和识别间隔(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - true: 图片已消失, false: 超时
|
||||
*/
|
||||
async function waitUntilImgDisappear(
|
||||
target,
|
||||
action,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
timeout = 5000,
|
||||
interval = 50
|
||||
) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start <= timeout) {
|
||||
await action();
|
||||
const res = await findImg(target, x, y, w, h, interval);
|
||||
if (!res) return true;
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作直到文本出现
|
||||
* @param {string|string[]} text 目标文本(单个文本或文本列表,列表时需全部匹配)
|
||||
* @param {() => Promise<void>} action 执行的操作函数
|
||||
* @param {number} [x=0] OCR 区域左上角 X
|
||||
* @param {number} [y=0] OCR 区域左上角 Y
|
||||
* @param {number} [w=1920] OCR 区域宽度
|
||||
* @param {number} [h=1080] OCR 区域高度
|
||||
* @param {number} [attempts=5] OCR 尝试次数
|
||||
* @param {number} [interval=50] 操作和识别间隔(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - RecognitionResult | null
|
||||
*/
|
||||
async function waitUntilTextAppear(
|
||||
text,
|
||||
action,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
attempts = 5,
|
||||
interval = 50
|
||||
) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start <= attempts * interval) {
|
||||
await action();
|
||||
|
||||
const res = await findText(text, x, y, w, h, 1, interval);
|
||||
if (res) return res;
|
||||
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作直到文本消失
|
||||
* @param {string} text 目标文本
|
||||
* @param {() => Promise<void>} action 执行的操作函数
|
||||
* @param {number} [x=0] OCR 区域左上角 X
|
||||
* @param {number} [y=0] OCR 区域左上角 Y
|
||||
* @param {number} [w=1920] OCR 区域宽度
|
||||
* @param {number} [h=1080] OCR 区域高度
|
||||
* @param {number} [attempts=5] OCR 尝试次数
|
||||
* @param {number} [interval=50] 操作和识别间隔(毫秒)
|
||||
*
|
||||
* @returns
|
||||
* - true: 文本已消失, false: 超时
|
||||
*/
|
||||
async function waitUntilTextDisappear(
|
||||
text,
|
||||
action,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 1920,
|
||||
h = 1080,
|
||||
attempts = 5,
|
||||
interval = 50
|
||||
) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start <= attempts * interval) {
|
||||
await action();
|
||||
const res = await findText(text, x, y, w, h, 1, interval); // 每次只试 1 次 OCR
|
||||
if (!res) return true;
|
||||
await sleep(interval);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据派蒙图标判断当前是否位于主页面
|
||||
* @return {Promise<boolean>}
|
||||
* - true: 位于主页面, false: 不在主页面
|
||||
*/
|
||||
async function isInMainUI() {
|
||||
try {
|
||||
return !!(await findImg(paimon));
|
||||
} catch (e) {
|
||||
log.error("判断是否位于主页面时出错", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个后台任务,用于自动月卡点击
|
||||
*
|
||||
* 使用示例:
|
||||
*
|
||||
* const watcher = startMonthCardWatcher();
|
||||
*
|
||||
* // 执行你的操作
|
||||
*
|
||||
* await watcher.cancel();
|
||||
*
|
||||
* @return {function} cancel(): 取消监听方法,调用后可以停止检测事件运行(异步)
|
||||
*/
|
||||
function startMonthCardWatcher() {
|
||||
let cancelled = false;
|
||||
|
||||
const task = (async () => {
|
||||
try {
|
||||
while (!cancelled) {
|
||||
const [girl, common] = await Promise.all([
|
||||
findImg(girl_moon),
|
||||
findImg(welkin_moon_logo)
|
||||
]);
|
||||
|
||||
if (girl || common) {
|
||||
log.info("检测到月卡");
|
||||
await sleep(200);
|
||||
click(100, 100);
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
const stone = await findImg(primogem);
|
||||
if (stone) {
|
||||
log.info("点击原石");
|
||||
while (!cancelled) {
|
||||
await sleep(200);
|
||||
click(100, 100);
|
||||
if (await isInMainUI()) {
|
||||
log.info("已进入主页面");
|
||||
cancelled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(1000);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("月卡监听异常", e);
|
||||
} finally {
|
||||
log.info("月卡监听任务结束");
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
return task;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开背包(检测过期物品)
|
||||
*/
|
||||
async function openBag() {
|
||||
await genshin.returnMainUi();
|
||||
keyPress("B");
|
||||
await sleep(500);
|
||||
const expiredText = await findText("物品过期", 870, 280, 170, 40, 2);
|
||||
if (expiredText) {
|
||||
log.info("检测到过期物品,关闭弹窗");
|
||||
await sleep(500);
|
||||
click(980, 750);
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 修改分辨率为1080p(会导致截图器重启,任务全部清空,暂时无法使用,仅供参考)
|
||||
// * @return {Promise<void>}
|
||||
// */
|
||||
// async function changeTo1080P() {
|
||||
// await genshin.returnMainUi();
|
||||
// const settings_button = await waitUntilImgAppear(
|
||||
// esc_settings,
|
||||
// async () => {
|
||||
// keyPress("ESCAPE");
|
||||
// await sleep(1500);
|
||||
// }
|
||||
// );
|
||||
// if (settings_button) {
|
||||
// await sleep(500);
|
||||
// await waitUntilImgAppear(
|
||||
// page_close_white,
|
||||
// async () => {
|
||||
// settings_button.click();
|
||||
// await sleep(500);
|
||||
// }
|
||||
// );
|
||||
// await sleep(1000);
|
||||
// } else {
|
||||
// throw new Error("打开菜单超时");
|
||||
// }
|
||||
// await findTextAndClick("图像", 100, 200, 200, 300, 10);
|
||||
// const view_mode = await findText("显示模式", 450, 200, 200, 200, 10);
|
||||
// await sleep(500);
|
||||
// click(view_mode.x + 1100, view_mode.y + 20);
|
||||
// await sleep(200);
|
||||
// moveMouseBy(0, 100);
|
||||
// await sleep(200);
|
||||
// for (let count = 0; count < 20; count++) {
|
||||
// verticalScroll(100);
|
||||
// await sleep(50);
|
||||
// }
|
||||
// const text_1080p = await waitUntilTextAppear(
|
||||
// ["1920", "1080"],
|
||||
// () => {
|
||||
// verticalScroll(-100);
|
||||
// },
|
||||
// 1400,
|
||||
// 300,
|
||||
// 400,
|
||||
// 600,
|
||||
// 20,
|
||||
// 1000
|
||||
// );
|
||||
// await sleep(200);
|
||||
// log.info("已切换至1080P");
|
||||
// click(text_1080p.x + 100, text_1080p.y + 10);
|
||||
// }
|
||||
|
||||
export {
|
||||
getImgMat,
|
||||
findImg,
|
||||
findImgAndClick,
|
||||
findText,
|
||||
findTextAndClick,
|
||||
waitUntilImgAppear,
|
||||
waitUntilImgDisappear,
|
||||
waitUntilTextAppear,
|
||||
waitUntilTextDisappear,
|
||||
isInMainUI,
|
||||
startMonthCardWatcher,
|
||||
openBag
|
||||
};
|
||||
Reference in New Issue
Block a user