mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-21 04:19:51 +08:00
* js:联机狗粮团购 增加额外的检测点 * js:联机团购增加保留拾取图片 * 修复autoenter以适配更新后的ui --------- Co-authored-by: 起个名字好难的喵 <25520958+MisakaAldrich@users.noreply.github.com>
463 lines
18 KiB
JavaScript
463 lines
18 KiB
JavaScript
const leaveTeamRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/leaveTeam.png"));
|
||
|
||
/**
|
||
* 递归读取目录下所有文件
|
||
* @param {string} folderPath 起始目录
|
||
* @param {string} [ext=''] 需要的文件后缀,空字符串表示不限制;例如 'json' 或 '.json' 均可
|
||
* @returns {Array<{fullPath:string, fileName:string, folderPathArray:string[]}>}
|
||
*/
|
||
async function readFolder(folderPath, ext = '') {
|
||
// 统一后缀格式:确保前面有一个点,且全小写
|
||
const targetExt = ext ? (ext.startsWith('.') ? ext : `.${ext}`).toLowerCase() : '';
|
||
|
||
const folderStack = [folderPath];
|
||
const files = [];
|
||
|
||
while (folderStack.length > 0) {
|
||
const currentPath = folderStack.pop();
|
||
const filesInSubFolder = file.ReadPathSync(currentPath); // 同步读取当前目录
|
||
const subFolders = [];
|
||
|
||
for (const filePath of filesInSubFolder) {
|
||
if (file.IsFolder(filePath)) {
|
||
subFolders.push(filePath); // 子目录稍后处理
|
||
} else {
|
||
// 后缀过滤
|
||
if (targetExt) {
|
||
const fileExt = filePath.toLowerCase().slice(filePath.lastIndexOf('.'));
|
||
if (fileExt !== targetExt) continue;
|
||
}
|
||
|
||
const fileName = filePath.split('\\').pop();
|
||
const folderPathArray = filePath.split('\\').slice(0, -1);
|
||
files.push({ fullPath: filePath, fileName, folderPathArray });
|
||
}
|
||
}
|
||
|
||
// 保持同层顺序,reverse 后仍按原顺序入栈
|
||
folderStack.push(...subFolders.reverse());
|
||
}
|
||
|
||
return files;
|
||
}
|
||
|
||
(async function () {
|
||
await autoEnter(settings);
|
||
}
|
||
)();
|
||
|
||
/**
|
||
* 自动联机脚本(整体打包为一个函数)
|
||
* @param {Object} autoEnterSettings 配置对象
|
||
* enterMode: "进入他人世界" | "等待他人进入"
|
||
* enteringUID: string | null
|
||
* permissionMode: "无条件通过" | "白名单"
|
||
* nameToPermit1/2/3: string | null
|
||
* timeout: 分钟
|
||
* maxEnterCount: number
|
||
*/
|
||
async function autoEnter(autoEnterSettings) {
|
||
// ===== 配置解析 =====
|
||
const enterMode = autoEnterSettings.enterMode || "进入他人世界";
|
||
const enteringUID = autoEnterSettings.enteringUID;
|
||
const permissionMode = autoEnterSettings.permissionMode || "无条件通过";
|
||
const timeout = +autoEnterSettings.timeOut || 5;
|
||
const maxEnterCount = +autoEnterSettings.maxEnterCount || 3;
|
||
|
||
// 白名单
|
||
const targetList = [];
|
||
[autoEnterSettings.nameToPermit1, autoEnterSettings.nameToPermit2, autoEnterSettings.nameToPermit3]
|
||
.forEach(v => v && targetList.push(v));
|
||
|
||
// ===== 模板 / 路径 =====
|
||
const enterUIDRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/enterUID.png"));
|
||
const searchRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/search.png"));
|
||
const requestEnterRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/requestEnter.png"));
|
||
const requestEnter2Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/requestEnter.png"), 1480, 300, 280, 600);
|
||
const yUIRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/yUI.png"));
|
||
const allowEnterRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/allowEnter.png"));
|
||
const targetsPath = "targets";
|
||
|
||
// ===== 状态 =====
|
||
let enterCount = 0;
|
||
let targetsRo = [];
|
||
let checkToEnd = false;
|
||
|
||
// ===== 初始化 =====
|
||
setGameMetrics(1920, 1080, 1);
|
||
const start = new Date();
|
||
log.info(`当前模式为:${enterMode}`);
|
||
|
||
// 加载目标 PNG
|
||
const targetPngs = await readFolder(targetsPath, ".png");
|
||
for (const f of targetPngs) {
|
||
const mat = file.ReadImageMatSync(f.fullPath);
|
||
const ro = RecognitionObject.TemplateMatch(mat, 664, 481, 1355 - 668, 588 - 484);
|
||
const baseName = f.fileName.replace(/\.png$/i, '');
|
||
targetsRo.push({ ro, baseName });
|
||
}
|
||
log.info(`加载完成共 ${targetsRo.length} 个目标`);
|
||
|
||
// ===== 主循环 =====
|
||
while (new Date() - start < timeout * 60 * 1000) {
|
||
if (enterMode === "进入他人世界") {
|
||
const playerSign = await getPlayerSign();
|
||
await sleep(500);
|
||
if (playerSign > 1) {
|
||
log.info(`加入成功,队伍编号 ${playerSign}`);
|
||
break;
|
||
} else if (playerSign === -1) {
|
||
log.warn("队伍编号识别异常,尝试按0p处理");
|
||
}
|
||
log.info('不处于多人世界,开始尝试加入');
|
||
await genshin.returnMainUi(); await sleep(500);
|
||
|
||
if (!enteringUID) { log.error('未填写有效 UID'); break; }
|
||
|
||
await keyPress("F2"); await sleep(2000);
|
||
if (!await findAndClick(enterUIDRo)) { await genshin.returnMainUi(); continue; }
|
||
await sleep(1000); inputText(enteringUID);
|
||
await sleep(1000);
|
||
if (!await findAndClick(searchRo)) { await genshin.returnMainUi(); continue; }
|
||
await sleep(500);
|
||
if (!await confirmSearchResult()) { await genshin.returnMainUi(); log.warn("无搜索结果"); continue; }
|
||
|
||
await sleep(500);
|
||
if (!await findAndClick(requestEnterRo)) { await genshin.returnMainUi(); continue; }
|
||
await waitForMainUI(true, 20 * 1000);
|
||
|
||
} else { // 等待他人进入
|
||
const playerSign = await getPlayerSign();
|
||
if (playerSign > 1) {
|
||
log.warn("处于他人世界,先尝试退出");
|
||
let leaveAttempts = 0;
|
||
while (leaveAttempts < 10) {
|
||
if (await getPlayerSign() === 0) {
|
||
break;
|
||
}
|
||
await keyPress("F2");
|
||
await sleep(1000);
|
||
await findAndClick(leaveTeamRo);
|
||
await sleep(1000);
|
||
keyPress("VK_ESCAPE");
|
||
await waitForMainUI(true);
|
||
await genshin.returnMainUi();
|
||
}
|
||
}
|
||
if (enterCount >= maxEnterCount) break;
|
||
if (await isYUI()) keyPress("VK_ESCAPE"); await sleep(500);
|
||
await genshin.returnMainUi();
|
||
keyPress("Y"); await sleep(250);
|
||
|
||
if (!await isYUI()) continue;
|
||
log.info("处于 Y 界面,开始识别");
|
||
|
||
let attempts = 0;
|
||
while (attempts++ < 5) {
|
||
if (permissionMode === "无条件通过") {
|
||
if (await findAndClick(allowEnterRo)) {
|
||
await waitForMainUI(true, 20 * 1000);
|
||
enterCount++;
|
||
break;
|
||
}
|
||
} else {
|
||
const result = await recognizeRequest();
|
||
if (result) {
|
||
if (await findAndClick(allowEnterRo)) {
|
||
await waitForMainUI(true, 20 * 1000);
|
||
enterCount++;
|
||
log.info(`允许 ${result} 加入`);
|
||
notification.send(`允许 ${result} 加入`);
|
||
if (await isYUI()) { keyPress("VK_ESCAPE"); await sleep(500); await genshin.returnMainUi(); }
|
||
break;
|
||
} else {
|
||
if (await isYUI()) { keyPress("VK_ESCAPE"); await sleep(500); await genshin.returnMainUi(); }
|
||
}
|
||
}
|
||
}
|
||
await sleep(500);
|
||
}
|
||
|
||
if (await isYUI()) { keyPress("VK_ESCAPE"); await genshin.returnMainUi(); }
|
||
|
||
if (enterCount >= maxEnterCount || checkToEnd) {
|
||
checkToEnd = true;
|
||
await sleep(20000);
|
||
if (await findTotalNumber() === maxEnterCount + 1) {
|
||
notification.send(`已达到预定人数:${maxEnterCount + 1}`);
|
||
break;
|
||
}
|
||
else enterCount--;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (new Date() - start >= timeout * 60 * 1000) {
|
||
log.warn("超时未达到预定人数");
|
||
notification.error(`超时未达到预定人数`);
|
||
}
|
||
|
||
async function confirmSearchResult() {
|
||
for (let i = 0; i < 4; i++) {
|
||
const gameRegion = captureGameRegion();
|
||
const res = gameRegion.find(requestEnter2Ro);
|
||
gameRegion.dispose();
|
||
if (res.isExist()) return false;
|
||
if (i < 4) await sleep(250);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function isYUI() {
|
||
for (let i = 0; i < 5; i++) {
|
||
const gameRegion = captureGameRegion();
|
||
const res = gameRegion.find(yUIRo);
|
||
gameRegion.dispose();
|
||
if (res.isExist()) return true;
|
||
await sleep(250);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function recognizeRequest() {
|
||
try {
|
||
const gameRegion = captureGameRegion();
|
||
for (const { ro, baseName } of targetsRo) {
|
||
if (gameRegion.find(ro).isExist()) { gameRegion.dispose(); return baseName; }
|
||
}
|
||
gameRegion.dispose();
|
||
} catch { }
|
||
try {
|
||
const gameRegion = captureGameRegion();
|
||
const resList = gameRegion.findMulti(RecognitionObject.ocr(664, 481, 1355 - 668, 588 - 484));
|
||
gameRegion.dispose();
|
||
let hit = null;
|
||
for (const res of resList) {
|
||
const txt = res.text.trim();
|
||
if (targetList.includes(txt)) { hit = txt; break; }
|
||
}
|
||
if (!hit) resList.forEach(r => log.warn(`识别到"${r.text.trim()}",不在白名单`));
|
||
return hit;
|
||
} catch { return null; }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通用找图/找RO并可选点击(支持单图片文件路径、单RO、图片文件路径数组、RO数组)
|
||
* @param {string|string[]|RecognitionObject|RecognitionObject[]} target
|
||
* @param {boolean} [doClick=true] 是否点击
|
||
* @param {number} [timeout=3000] 识别时间上限(ms)
|
||
* @param {number} [interval=50] 识别间隔(ms)
|
||
* @param {number} [retType=0] 0-返回布尔;1-返回 Region 结果
|
||
* @param {number} [preClickDelay=50] 点击前等待
|
||
* @param {number} [postClickDelay=50] 点击后等待
|
||
* @returns {boolean|Region} 根据 retType 返回是否成功或最终 Region
|
||
*/
|
||
async function findAndClick(target,
|
||
doClick = true,
|
||
timeout = 3000,
|
||
interval = 50,
|
||
retType = 0,
|
||
preClickDelay = 50,
|
||
postClickDelay = 50) {
|
||
try {
|
||
// 1. 统一转成 RecognitionObject 数组
|
||
let ros = [];
|
||
if (Array.isArray(target)) {
|
||
ros = target.map(t =>
|
||
(typeof t === 'string')
|
||
? RecognitionObject.TemplateMatch(file.ReadImageMatSync(t))
|
||
: t
|
||
);
|
||
} else {
|
||
ros = [(typeof target === 'string')
|
||
? RecognitionObject.TemplateMatch(file.ReadImageMatSync(target))
|
||
: target];
|
||
}
|
||
|
||
const start = Date.now();
|
||
let found = null;
|
||
|
||
while (Date.now() - start <= timeout) {
|
||
const gameRegion = captureGameRegion();
|
||
try {
|
||
// 依次尝试每一个 ro
|
||
for (const ro of ros) {
|
||
const res = gameRegion.find(ro);
|
||
if (!res.isEmpty()) { // 找到
|
||
found = res;
|
||
if (doClick) {
|
||
await sleep(preClickDelay);
|
||
res.click();
|
||
await sleep(postClickDelay);
|
||
}
|
||
break; // 成功即跳出 for
|
||
}
|
||
}
|
||
if (found) break; // 成功即跳出 while
|
||
} finally {
|
||
gameRegion.dispose();
|
||
}
|
||
await sleep(interval); // 没找到时等待
|
||
}
|
||
|
||
// 3. 按需返回
|
||
return retType === 0 ? !!found : (found || null);
|
||
|
||
} catch (error) {
|
||
log.error(`执行通用识图时出现错误:${error.message}`);
|
||
return retType === 0 ? false : null;
|
||
}
|
||
}
|
||
|
||
//等待主界面状态
|
||
async function waitForMainUI(requirement, timeOut = 60 * 1000) {
|
||
log.info(`等待至多${timeOut}毫秒`)
|
||
const startTime = Date.now();
|
||
let logcount = 0;
|
||
while (Date.now() - startTime < timeOut) {
|
||
const mainUIState = await isMainUI();
|
||
logcount++;
|
||
if (mainUIState === requirement) return true;
|
||
const elapsed = Date.now() - startTime;
|
||
const min = Math.floor(elapsed / 60000);
|
||
const sec = Math.floor((elapsed % 60000) / 1000);
|
||
const ms = elapsed % 1000;
|
||
if (logcount >= 50) {
|
||
logcount = 0;
|
||
log.info(`已等待 ${min}分 ${sec}秒 ${ms}毫秒`);
|
||
}
|
||
await sleep(200);
|
||
}
|
||
log.error("超时仍未到达指定状态");
|
||
return false;
|
||
}
|
||
|
||
|
||
//检查是否在主界面
|
||
async function isMainUI() {
|
||
// 修改后的图像路径
|
||
const imagePath = "assets/RecognitionObject/MainUI.png";
|
||
// 修改后的识别区域(左上角区域)
|
||
const xMin = 0;
|
||
const yMin = 0;
|
||
const width = 150; // 识别区域宽度
|
||
const height = 150; // 识别区域高度
|
||
let template = file.ReadImageMatSync(imagePath);
|
||
let recognitionObject = RecognitionObject.TemplateMatch(template, xMin, yMin, width, height);
|
||
|
||
// 尝试次数设置为 5 次
|
||
const maxAttempts = 5;
|
||
|
||
let attempts = 0;
|
||
while (attempts < maxAttempts) {
|
||
try {
|
||
|
||
let gameRegion = captureGameRegion();
|
||
let result = gameRegion.find(recognitionObject);
|
||
gameRegion.dispose();
|
||
if (result.isExist()) {
|
||
//log.info("处于主界面");
|
||
return true; // 如果找到图标,返回 true
|
||
}
|
||
} catch (error) {
|
||
log.error(`识别图像时发生异常: ${error.message}`);
|
||
|
||
return false; // 发生异常时返回 false
|
||
}
|
||
attempts++; // 增加尝试次数
|
||
await sleep(250); // 每次检测间隔 250 毫秒
|
||
}
|
||
return false; // 如果尝试次数达到上限或取消,返回 false
|
||
}
|
||
|
||
//获取联机世界的当前玩家标识
|
||
async function getPlayerSign() {
|
||
let attempts = 0;
|
||
while (attempts < 10) {
|
||
attempts++;
|
||
const picDic = {
|
||
"0P": "assets/RecognitionObject/0P.png",
|
||
"1P": "assets/RecognitionObject/1P.png",
|
||
"2P": "assets/RecognitionObject/2P.png",
|
||
"3P": "assets/RecognitionObject/3P.png",
|
||
"4P": "assets/RecognitionObject/4P.png"
|
||
}
|
||
await genshin.returnMainUi();
|
||
await sleep(500);
|
||
const p0Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["0P"]), 200, 10, 400, 70);
|
||
p0Ro.Threshold = 0.95;
|
||
p0Ro.InitTemplate();
|
||
const p1Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["1P"]), 200, 10, 400, 70);
|
||
p1Ro.Threshold = 0.95;
|
||
p1Ro.InitTemplate();
|
||
const p2Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["2P"]), 200, 10, 400, 70);
|
||
p2Ro.Threshold = 0.95;
|
||
p2Ro.InitTemplate();
|
||
const p3Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["3P"]), 200, 10, 400, 70);
|
||
p3Ro.Threshold = 0.95;
|
||
p3Ro.InitTemplate();
|
||
const p4Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["4P"]), 200, 10, 400, 70);
|
||
p4Ro.Threshold = 0.95;
|
||
p4Ro.InitTemplate();
|
||
moveMouseTo(1555, 860); // 移走鼠标,防止干扰识别
|
||
const gameRegion = captureGameRegion();
|
||
// 当前页面模板匹配
|
||
let p0 = gameRegion.Find(p0Ro);
|
||
let p1 = gameRegion.Find(p1Ro);
|
||
let p2 = gameRegion.Find(p2Ro);
|
||
let p3 = gameRegion.Find(p3Ro);
|
||
let p4 = gameRegion.Find(p4Ro);
|
||
gameRegion.dispose();
|
||
if (p0.isExist()) { log.info("识别结果为0P"); return 0; }
|
||
if (p1.isExist()) { log.info("识别结果为1P"); return 1; }
|
||
if (p2.isExist()) { log.info("识别结果为2P"); return 2; }
|
||
if (p3.isExist()) { log.info("识别结果为3P"); return 3; }
|
||
if (p4.isExist()) { log.info("识别结果为4P"); return 4; }
|
||
await genshin.returnMainUi();
|
||
await sleep(250);
|
||
}
|
||
log.warn("超时仍未识别到队伍编号");
|
||
return -1;
|
||
}
|
||
|
||
async function findTotalNumber() {
|
||
await genshin.returnMainUi();
|
||
await keyPress("F2");
|
||
await sleep(2000);
|
||
|
||
// 定义模板
|
||
const kick2pRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/kickButton.png"), 1520, 277, 230, 120);
|
||
const kick3pRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/kickButton.png"), 1520, 400, 230, 120);
|
||
const kick4pRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/kickButton.png"), 1520, 527, 230, 120);
|
||
|
||
moveMouseTo(1555, 860); // 防止鼠标干扰
|
||
const gameRegion = captureGameRegion();
|
||
await sleep(200);
|
||
|
||
let count = 1; // 先算上自己
|
||
|
||
// 依次匹配 2P
|
||
if (gameRegion.Find(kick2pRo).isExist()) {
|
||
log.info("发现 2P");
|
||
count++;
|
||
}
|
||
|
||
// 依次匹配 3P
|
||
if (gameRegion.Find(kick3pRo).isExist()) {
|
||
log.info("发现 3P");
|
||
count++;
|
||
}
|
||
|
||
// 依次匹配 4P
|
||
if (gameRegion.Find(kick4pRo).isExist()) {
|
||
log.info("发现 4P");
|
||
count++;
|
||
}
|
||
|
||
gameRegion.dispose();
|
||
|
||
log.info(`当前联机世界玩家总数(含自己):${count}`);
|
||
return count;
|
||
}
|