Files
bettergi-scripts-list/repo/js/ArtifactsGroupPurchasing/main.js
mno 6b5c41945a js:匹配拾取中滚轮改为仅在必要时触发 (#2930)
* js:AAA狗粮批发2.2.0

1.滚轮改为仅在必要时触发
2.更新readme

* js:采集cd管理

滚轮仅在必要时触发

* js:锄地一条龙

移除材料图片zip
滚轮改为仅必要时触发

* js:联机团购

滚轮改为仅必要时触发

* js:锄地一条龙

优化吃药流程
2026-02-26 14:27:47 +08:00

1682 lines
63 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.
const runExtra = settings.runExtra || false;
const leaveTeamRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/leaveTeam.png"));
const scrollRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/拾取滚轮.png"), 1017, 496, 1093 - 581, 581 - 496);
let targetItems;
let pickupDelay = 100;
let timeMove = 1000;
let findFInterval = (+settings.findFInterval || 100);
if (findFInterval < 16) {
findFInterval = 16;
}
if (findFInterval > 200) {
findFInterval = 200;
}
let lastRoll = new Date();
let checkDelay = Math.round(findFInterval / 2);
let timeMoveUp = Math.round(timeMove * 0.45);
let timeMoveDown = Math.round(timeMove * 0.55);
let rollingDelay = 50;
let state;
let gameRegion;
let TMthreshold = +settings.TMthreshold || 0.9;
let doRunExtra = false;
let expGain;
let skipRunning = false;
let runnedEnding = false;
(async function () {
setGameMetrics(1920, 1080, 1);
dispatcher.AddTrigger(new RealtimeTimer("AutoSkip"));
if (settings.logName) {
expGain = await processArtifacts();
moraGain = await mora();
}
await genshin.tpToStatueOfTheSeven();
await switchPartyIfNeeded(settings.partyName);
targetItems = await loadTargetItems();
if (settings.groupMode != "按照下列配置自动进入并运行") {
await genshin.clearPartyCache();
await runGroupPurchasing(runExtra);
}
if (settings.groupMode != "手动进入后运行") {
//解析与输出自定义配置
const raw = settings.runningOrder || "1234";
if (!/^[1-4]+$/.test(raw)) {
throw new Error('runningOrder 只能由 1-4 的数字组成,检测到非法字符。');
}
if (new Set(raw).size !== raw.length) {
throw new Error('runningOrder 中出现了重复数字。');
}
const enteringIndex = raw.split('').map(Number);
const msg = '将依次进入' + enteringIndex.map(i => `${i}`).join('') + '的世界';
log.info(msg);
const yourIndex = Number(settings.yourIndex);
if (!yourIndex || yourIndex < 1 || yourIndex > 4) {
throw new Error('yourIndex 必须是 1-4 之间的数字。');
}
const pos = enteringIndex.indexOf(yourIndex) + 1; // 第几个执行
log.info(`你的序号是${yourIndex}号,将在第${pos}个执行`);
let loopCnt = 0;
// 按 runningOrder 依次进入世界并执行联机收尾
for (const idx of enteringIndex) {
if (skipRunning) {
break;
}
await genshin.clearPartyCache();
if (settings.usingCharacter) { await sleep(1000); keyPress(`${settings.usingCharacter}`); }
//构造加入idx号世界的autoEnter的settings
let autoEnterSettings;
if (idx === yourIndex) {
settings.forceGroupNumber = 1;//将房主强制指定为房主
// 1. 先收集真实存在的白名单
const permits = {};
let permitIndex = 1;
for (const otherIdx of [1, 2, 3, 4]) {
if (otherIdx !== yourIndex) {
const pName = settings[`p${otherIdx}Name`];
if (pName) { // 过滤掉空/undefined
permits[`nameToPermit${permitIndex}`] = pName;
permitIndex++;
}
}
}
// 2. 用真正写进去的人数作为 maxEnterCount
autoEnterSettings = {
enterMode: "等待他人进入",
permissionMode: "白名单",
timeout: loopCnt++ === 0 ? 10 : 5, // ← 第一次 10之后 5
maxEnterCount: Object.keys(permits).length
};
Object.assign(autoEnterSettings, permits);
log.info(`等待他人进入自己世界,目标人数:${autoEnterSettings.maxEnterCount}`);
notification.send(`等待他人进入自己世界,目标人数:${autoEnterSettings.maxEnterCount}`);
} else {
settings.forceGroupNumber = 0;//取消强制指定
// 构造队员配置
autoEnterSettings = {
enterMode: "进入他人世界",
enteringUID: settings[`p${idx}UID`],
timeout: loopCnt++ === 0 ? 10 : 5, // ← 第一次 10之后 5
};
log.info(`将要进入序号${idx}uid为${settings[`p${idx}UID`]}的世界`);
notification.send(`将要进入序号${idx}uid为${settings[`p${idx}UID`]},名称为${settings[`p${idx}Name`]}的世界`);
}
let attempts = 0;
while (attempts < 5) {
attempts++;
await autoEnter(autoEnterSettings);
//队员加入后要检查房主名称
if (autoEnterSettings.enterMode === "进入他人世界" && attempts != 5) {
if (await checkP1Name(settings[`p${idx}Name`])) {
notification.send(`成功进入序号${idx}uid为${settings[`p${idx}UID`]},名称为${settings[`p${idx}Name`]}的世界`);
break;
} else {
//进入了错误的世界,退出世界并重新加入,最后一次不检查
await sleep(1000);
let leaveAttempts = 0;
while (leaveAttempts < 10) {
leaveAttempts++;
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();
}
}
} else {
break;
}
}
//执行对应的联机狗粮
await runGroupPurchasing(false);
settings.forceGroupNumber = 0;//解除强制指定
}
//如果勾选了额外,且本次自动运行当过房主成功进人,在结束后再执行一次额外路线
if (settings.runExtra && doRunExtra) {
await runGroupPurchasing(runExtra);
}
}
await genshin.tpToStatueOfTheSeven();
if (skipRunning && !runnedEnding) {
log.info(`本次运行启用并触发了强迫症模式,且未完成收尾路线需要重新上线`);
// 按中文分号分割字符串
const segments = settings.onlyRunPerfectly.split('');
// 逐段输出每段间隔1秒
for (const segment of segments) {
if (segment.trim()) { // 跳过空段落
log.info(segment.trim());
await sleep(1000);
}
}
await sleep(10000);
return;
}
if (settings.logName) {
expGain = await processArtifacts() - expGain;
moraGain = await mora() - moraGain;
log.info(`${settings.logName}:联机狗粮分解获得经验${expGain}`);
notification.send(`${settings.logName}:联机狗粮分解获得经验${expGain}`);
log.info(`${settings.logName}:联机狗粮获得摩拉${moraGain}`);
notification.send(`${settings.logName}:联机狗粮获得摩拉${moraGain}`);
}
{
log.info(`本次运行未启用或未触发强迫症模式,正常结束`);
if (settings.normalEnding) {
// 按中文分号分割字符串
const segments = settings.normalEnding.split('');
// 逐段输出每段间隔1秒
for (const segment of segments) {
if (segment.trim()) { // 跳过空段落
log.info(segment.trim());
await sleep(1000);
}
}
}
}
}
)();
async function checkP1Name(p1Name) {
//log.info("禁用了房主名称校验,直接视为通过");
//强制禁用房主检测
return true;
}
/**
* 群收尾 / 额外路线统一入口
*
*/
async function runGroupPurchasing(runExtra) {
// ===== 1. 读取配置 =====
const p1EndingRoute = settings.p1EndingRoute || "枫丹高塔";
const p2EndingRoute = "度假村";
const p3EndingRoute = "智障厅";
const p4EndingRoute = "踏鞴砂";
const forceGroupNumber = settings.forceGroupNumber || 0;
// ===== 2. 图标模板 =====
const kickAllRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/kickAll.png"));
const confirmKickRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/confirmKick.png"));
// ===== 3. 初始化变量 =====
let _infoPoints = null;
let running = true;
// ===== 4. 主流程 =====
log.info("开始识别队伍编号");
let groupNumBer = await getPlayerSign();
if (groupNumBer !== 0) log.info(`在队伍中编号为${groupNumBer}`);
else log.info(`不处于联机模式或识别异常`);
if (forceGroupNumber != 0) {
groupNumBer = forceGroupNumber;
log.info(`将自己在队伍中的编号强制指定为${groupNumBer}`);
}
if (groupNumBer === 1) {
log.info("是1p检测当前总人数");
const totalNumber = await findTotalNumber();
await waitForReady(totalNumber);
if (skipRunning) {
log.info(`强迫症模式启用中,队友不齐或未及时到位,跳过所有路线`);
notification.send(`强迫症模式启用中,队友不齐或未及时到位,跳过所有路线`);
await sleep(10000);
} else {
for (let i = 1; i <= totalNumber; i++) await runEndingPath(i);
}
let kickAttempts = 0;
while (kickAttempts < 10) {
kickAttempts++;
await genshin.returnMainUi();
await keyPress("F2");
await sleep(2000);
await findAndClick(kickAllRo);
await sleep(500);
await findAndClick(confirmKickRo);
await waitForMainUI(true);
await genshin.returnMainUi();
if (await getPlayerSign() === 0) {
await genshin.returnMainUi();
break;
}
await genshin.returnMainUi();
await sleep(10000);
}
} else if (groupNumBer > 1) {
await goToTarget(groupNumBer);
let start = new Date();
while (new Date() - start < 2 * 60 * 60 * 1000) {
if (await waitForMainUI(false, 60 * 60 * 1000)) {
await waitForMainUI(true);
await genshin.returnMainUi();
if (await getPlayerSign() === 0) {
break;
}
}
}
if (new Date() - start > 2 * 60 * 60 * 1000) {
log.warn("超时仍未回到主界面,主动退出");
}
let leaveAttempts = 0;
while (leaveAttempts < 10) {
if (await getPlayerSign() === 0) {
break;
}
await keyPress("F2");
await sleep(1000);
await findAndClick(leaveTeamRo);
await waitForMainUI(true);
await genshin.returnMainUi();
}
} else if (groupNumBer === 0) {
if (runExtra) {
log.info("请确保联机收尾已结束,将开始运行额外路线");
await runExtraPath();
} else {
log.warn("处于单人模式,不执行任何路线");
}
} else {
log.warn("角色编号识别异常")
}
running = false;
/**
* 等待所有队友2P/3P/4P就位
* @param {number} totalNumber 联机总人数(包含自己)
* @param {number} timeOut 最长等待毫秒
*/
async function waitForReady(totalNumber, timeOut = 300000) {
// 实际需要检测的队友编号2 ~ totalNumber
const needCheck = totalNumber - 1; // 队友人数
const readyFlags = new Array(needCheck).fill(false); // 下标 0 代表 2P1 代表 3P …
const startTime = Date.now();
while (Date.now() - startTime < timeOut) {
let allReady = true;
await genshin.returnMainUi();
await keyPress("M"); // 打开多人地图/界面
await sleep(2000); // 给 UI 一点加载时间
for (let i = 0; i < needCheck; i++) {
// 已就绪的队友跳过
if (readyFlags[i]) continue;
const playerIndex = i + 2; // 2P/3P/4P
const ready = await checkReady(playerIndex);
if (ready) {
log.info(`玩家 ${playerIndex}P 已就绪`);
readyFlags[i] = true;
} else {
allReady = false; // 还有没就绪的
}
}
if (allReady) {
log.info("所有队友已就绪");
if (settings.runDebug) await sleep(10000);
return true;
}
// 每轮检测后稍等,防止刷屏
await sleep(500);
}
log.warn("等待队友就绪超时");
if (settings.onlyRunPerfectly) {
skipRunning = true;
doRunExtra = false;
}
return false;
}
async function checkReady(i) {
try {
/* 1. 先把地图移到目标点位point 来自 info.json */
const point = await getPointByPlayer(i);
if (!point) return false;
// 把路径封装在函数内部
const map = {
2: "assets/RecognitionObject/2pInBigMap.png",
3: "assets/RecognitionObject/3pInBigMap.png",
4: "assets/RecognitionObject/4pInBigMap.png"
};
const tplPath = map[i];
if (!tplPath) {
log.error(`无效玩家编号: ${i}`);
return null;
}
const template = file.ReadImageMatSync(tplPath);
const recognitionObj = RecognitionObject.TemplateMatch(template, 0, 0, 1920, 1080);
if (await findAndClick(recognitionObj, false, 2000)) await sleep(1000);
await genshin.moveMapTo(Math.round(point.x), Math.round(point.y));
/* 2. 取图标屏幕坐标 */
const pos = await getPlayerIconPos(i);
if (!pos || !pos.found) return false;
/* 3. 屏幕坐标 → 地图坐标(图标)*/
const mapZoomLevel = 2.0;
await genshin.setBigMapZoomLevel(mapZoomLevel);
const mapScaleFactor = 2.361;
const center = genshin.getPositionFromBigMap(); // 仅用于坐标系转换
const iconScreenX = pos.x;
const iconScreenY = pos.y;
const iconMapX = (960 - iconScreenX) * mapZoomLevel / mapScaleFactor + center.x;
const iconMapY = (540 - iconScreenY) * mapZoomLevel / mapScaleFactor + center.y;
/* 4. 计算“图标地图坐标”与“目标点位”的距离 */
const dx = iconMapX - point.x;
const dy = iconMapY - point.y;
const dist = Math.sqrt(dx * dx + dy * dy);
/* 5. 打印两种坐标及距离 */
log.info(`玩家 ${i}P`);
log.info(`├─ 屏幕坐标: (${iconScreenX}, ${iconScreenY})`);
log.info(`├─ 图标地图坐标: (${iconMapX.toFixed(2)}, ${iconMapY.toFixed(2)})`);
log.info(`├─ 目标点位坐标: (${point.x}, ${point.y})`);
log.info(`└─ 图标与目标点位距离: ${dist.toFixed(2)} m`);
return dist <= 10; // 10 m 阈值,可按需调整
} catch (error) {
log.error(error.message);
return false;
}
}
/**
* 根据玩家编号返回该路线在 assets/info.json 中记录的点位坐标
* @param {number} playerIndex 1 | 2 | 3 | 4
* @returns {{x:number,y:number}|null}
*/
async function getPointByPlayer(playerIndex) {
// 1. 只读一次:第一次调用时加载并缓存
if (_infoPoints === null) {
try {
const jsonStr = file.ReadTextSync('assets/info.json');
_infoPoints = JSON.parse(jsonStr);
if (!Array.isArray(_infoPoints)) {
log.error('assets/info.json 不是数组格式');
_infoPoints = []; // 防止后续再读
return null;
}
} catch (err) {
log.error(`读取或解析 assets/info.json 失败: ${err.message}`);
_infoPoints = [];
return null;
}
}
// 2. 外部已准备好的路线名称映射
const routeMap = {
1: p1EndingRoute,
2: p2EndingRoute,
3: p3EndingRoute,
4: p4EndingRoute
};
const routeName = routeMap[playerIndex];
if (!routeName) {
log.error(`无效玩家编号: ${playerIndex}`);
return null;
}
// 3. 遍历缓存数组
for (let i = 0; i < _infoPoints.length; i++) {
const p = _infoPoints[i];
if (p && p.name === routeName) {
return { x: p.position.x, y: p.position.y };
}
}
log.warn(`在 info.json 中找不到 name 为 "${routeName}" 的点`);
return null;
}
/**
* 根据玩家编号获取 2/3/4P 图标在屏幕上的坐标
* @param {number} playerIndex 2 | 3 | 4
* @param {number} timeout 最长查找毫秒,默认 2000
* @returns {Promise<{x:number,y:number,width:number,height:number,found:boolean}|null>}
* found=true 时坐标有效;未找到/取消/异常返回 null
*/
async function getPlayerIconPos(playerIndex, timeout = 2000) {
// 把路径封装在函数内部
const map = {
2: "assets/RecognitionObject/2pInBigMap.png",
3: "assets/RecognitionObject/3pInBigMap.png",
4: "assets/RecognitionObject/4pInBigMap.png"
};
const tplPath = map[playerIndex];
if (!tplPath) {
log.error(`无效玩家编号: ${playerIndex}`);
return null;
}
const template = file.ReadImageMatSync(tplPath);
const recognitionObj = RecognitionObject.TemplateMatch(template, 0, 0, 1920, 1080); // 全屏查找,可自行改区域
const start = Date.now();
while (Date.now() - start < timeout) {
let gameRegion = null;
try {
gameRegion = captureGameRegion();
const res = gameRegion.find(recognitionObj);
if (res.isExist()) {
log.info(`${playerIndex}P在屏幕上的坐标为(${res.x + 10},${res.y + 10})`);//图标大小为20*20
return {
x: res.x + 10,
y: res.y + 10,
found: true
};
}
} catch (e) {
log.error(`模板匹配异常: ${e.message}`);
return null;
} finally {
if (gameRegion) gameRegion.dispose();
}
await sleep(100);
}
return null;
}
/**
* 根据玩家编号执行执行路线的全部 JSON 文件
* @param {number} i 1 | 2 | 3 | 4
*/
async function runEndingPath(i) {
const routeMap = {
1: p1EndingRoute,
2: p2EndingRoute,
3: p3EndingRoute,
4: p4EndingRoute
};
const folderName = routeMap[i];
if (!folderName) {
log.error(`无效玩家编号: ${i}`);
return;
}
const folderPath = `assets/ArtifactsPath/${folderName}/执行`;
const files = await readFolder(folderPath, ".json");
if (files.length === 0) {
log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`);
return;
}
runnedEnding = true;
if (!settings.runDebug) {
for (const { fullPath } of files) {
await runPath(fullPath, 1);
}
log.info(`${folderName} 的全部路线已完成`);
} else {
log.info("当前为调试模式,跳过执行路线");
}
}
/**
* 执行额外路线
*/
async function runExtraPath() {
const folderPath = `assets/ArtifactsPath/额外/执行`;
const files = await readFolder(folderPath, ".json");
if (files.length === 0) {
log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`);
return;
}
if (skipRunning) {
log.info(`强迫症模式启用中,队友不齐或未及时到位,跳过所有路线`);
notification.send(`强迫症模式启用中,队友不齐或未及时到位,跳过所有路线`);
await sleep(10000);
return;
}
if (!settings.runDebug) {
for (const { fullPath } of files) {
await runPath(fullPath, 1);
}
log.info(`额外 的全部路线已完成`);
} else {
log.info("当前为调试模式,跳过执行路线");
}
}
/**
* 根据玩家编号执行占位路线的全部 JSON 文件
* @param {number} i 1 | 2 | 3 | 4
*/
async function goToTarget(i) {
const routeMap = {
1: p1EndingRoute,
2: p2EndingRoute,
3: p3EndingRoute,
4: p4EndingRoute
};
const folderName = routeMap[i];
if (!folderName) {
log.error(`无效玩家编号: ${i}`);
return;
}
const folderPath = `assets/ArtifactsPath/${folderName}/占位`;
const files = await readFolder(folderPath, ".json");
if (files.length === 0) {
log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`);
}
for (const { fullPath } of files) {
await runPath(fullPath, 1);
}
try {
const pos = await genshin.getPositionFromMap();
const dist = Math.hypot(pos.x - 2297.3, pos.y + 823.8);
if (dist && dist < 100) {
log.warn("仍然在七天神像附近,尝试重跑至多一次");
for (const { fullPath } of files) {
await runPath(fullPath, 1);
}
}
} catch (error) {
}
log.info(`${folderName} 的全部路线已完成`);
}
}
/**
* 自动联机脚本(整体打包为一个函数)
* @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;
let enteredPlayers = [];
// ===== 初始化 =====
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)) {
doRunExtra = true;
await waitForMainUI(true, 20 * 1000);
enterCount++;
break;
}
} else {
const result = await recognizeRequest();
if (result) {
if (await findAndClick(allowEnterRo)) {
await waitForMainUI(true, 20 * 1000);
enterCount++;
enteredPlayers = [...new Set([...enteredPlayers, result])];
log.info(`允许 ${result} 加入`);
notification.send(`允许 ${result} 加入`);
doRunExtra = true;
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(`超时未达到预定人数`);
if (settings.onlyRunPerfectly) {
skipRunning = true;
doRunExtra = false;
}
}
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; }
}
}
//切换队伍
async function switchPartyIfNeeded(partyName) {
if (!partyName) {
await genshin.returnMainUi();
return;
}
try {
log.info("正在尝试切换至" + partyName);
if (!await genshin.switchParty(partyName)) {
log.info("切换队伍失败,前往七天神像重试");
await genshin.tpToStatueOfTheSeven();
await genshin.switchParty(partyName);
}
} catch {
log.error("队伍切换失败,可能处于联机模式或其他不可切换状态");
notification.error(`队伍切换失败,可能处于联机模式或其他不可切换状态`);
await genshin.returnMainUi();
}
}
/**
* 通用找图/找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;
}
// fakeLog 函数,使用方法:将本函数放在主函数前,调用时请务必使用await否则可能出现v8白框报错
//在js开头处伪造该js结束运行的日志信息如 await fakeLog("js脚本", true, true, 0);
//在js结尾处伪造该js开始运行的日志信息如 await fakeLog("js脚本", true, false, 2333);
//duration项目仅在伪造结束信息时有效且无实际作用可以任意填写当你需要在日志中输出特定值时才需要单位为毫秒
//在调用地图追踪前伪造该地图追踪开始运行的日志信息,如 await fakeLog(`地图追踪.json`, false, true, 0);
//在调用地图追踪后伪造该地图追踪结束运行的日志信息,如 await fakeLog(`地图追踪.json`, false, false, 0);
//如此便可以在js运行过程中伪造地图追踪的日志信息可以在日志分析等中查看
async function fakeLog(name, isJs, isStart, duration) {
await sleep(10);
const currentTime = Date.now();
// 参数检查
if (typeof name !== 'string') {
log.error("参数 'name' 必须是字符串类型!");
return;
}
if (typeof isJs !== 'boolean') {
log.error("参数 'isJs' 必须是布尔型!");
return;
}
if (typeof isStart !== 'boolean') {
log.error("参数 'isStart' 必须是布尔型!");
return;
}
if (typeof currentTime !== 'number' || !Number.isInteger(currentTime)) {
log.error("参数 'currentTime' 必须是整数!");
return;
}
if (typeof duration !== 'number' || !Number.isInteger(duration)) {
log.error("参数 'duration' 必须是整数!");
return;
}
// 将 currentTime 转换为 Date 对象并格式化为 HH:mm:ss.sss
const date = new Date(currentTime);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
const formattedTime = `${hours}:${minutes}:${seconds}.${milliseconds}`;
// 将 duration 转换为分钟和秒,并保留三位小数
const durationInSeconds = duration / 1000; // 转换为秒
const durationMinutes = Math.floor(durationInSeconds / 60);
const durationSeconds = (durationInSeconds % 60).toFixed(3); // 保留三位小数
// 使用四个独立的 if 语句处理四种情况
if (isJs && isStart) {
// 处理 isJs = true 且 isStart = true 的情况
const logMessage = `正在伪造js开始的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 开始执行JS脚本: "${name}"`;
log.debug(logMessage);
}
if (isJs && !isStart) {
// 处理 isJs = true 且 isStart = false 的情况
const logMessage = `正在伪造js结束的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}${durationSeconds}\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------`;
log.debug(logMessage);
}
if (!isJs && isStart) {
// 处理 isJs = false 且 isStart = true 的情况
const logMessage = `正在伪造地图追踪开始的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 开始执行地图追踪任务: "${name}"`;
log.debug(logMessage);
}
if (!isJs && !isStart) {
// 处理 isJs = false 且 isStart = false 的情况
const logMessage = `正在伪造地图追踪结束的日志记录\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`→ 脚本执行结束: "${name}", 耗时: ${durationMinutes}${durationSeconds}\n\n` +
`[${formattedTime}] [INF] BetterGenshinImpact.Service.ScriptService\n` +
`------------------------------`;
log.debug(logMessage);
}
}
/**
* 递归读取目录下所有文件
* @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 runPath(fullPath, targetItemPath) {
state = { running: true };
/* ---------- 主任务 ---------- */
const pathingTask = (async () => {
await genshin.returnMainUi();
log.info(`开始执行路线: ${fullPath}`);
await fakeLog(fullPath, false, true, 0);
await pathingScript.runFile(fullPath);
await fakeLog(fullPath, false, false, 0);
state.running = false;
})();
/* ---------- 伴随任务 ---------- */
const pickupTask = (async () => {
await recognizeAndInteract();
})();
const errorProcessTask = (async () => {
const revivalRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/复苏.png"));
const readingRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/readingUI.png"), 72, 22, 133 - 72, 79 - 22);
const dialogueRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/dialogueUI.png"), 187, 26, 233 - 130, 69);
let errorCheckCount = 9;
while (state.running) {
await sleep(100);
errorCheckCount++;
if (errorCheckCount > 50) {
errorCheckCount = 0;
if (await findAndClick(revivalRo, true, 2, 3)) {
log.info("识别到复苏按钮,点击复苏");
errorCheckCount = 50;
}
if (await findAndClick(readingRo, false, 2, 3)) {
log.info("识别到阅读界面esc脱离");
await genshin.returnMainUi();
errorCheckCount = 50;
}
if (await findAndClick(dialogueRo, false, 2, 3)) {
log.info("识别到对话界面,点击进行对话");
click(960, 540);
errorCheckCount = 50;
}
}
}
})();
/* ---------- 并发等待 ---------- */
await Promise.allSettled([pathingTask, pickupTask, errorProcessTask]);
}
//加载拾取物图片
async function loadTargetItems() {
const targetItemPath = 'assets/targetItems'; // 固定目录
const items = await readFolder(targetItemPath, ".png");
// 统一预加载模板
for (const it of items) {
it.template = file.ReadImageMatSync(it.fullPath);
it.itemName = it.fileName.replace(/\.png$/i, '');
}
return items;
}
// 定义一个函数用于拾取
async function recognizeAndInteract() {
//log.info("调试-开始执行图像识别与拾取任务");
let lastcenterYF = 0;
let lastItemName = "";
let fIcontemplate = file.ReadImageMatSync('assets/F_Dialogue.png');
let mainUITemplate = file.ReadImageMatSync("assets/MainUI.png");
let thisMoveUpTime = 0;
let lastMoveDown = 0;
gameRegion = captureGameRegion();
//主循环
while (state.running) {
gameRegion.dispose();
gameRegion = captureGameRegion();
let centerYF = await findFIcon();
if (!centerYF) {
if (new Date() - lastRoll >= 200) {
lastRoll = new Date();
if (await hasScroll()) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
}
}
continue;
}
//log.info(`调试-成功找到f图标,centerYF为${centerYF}`);
let foundTarget = false;
let itemName = await performTemplateMatch(centerYF);
if (itemName) {
//log.info(`调试-识别到物品${itemName}`);
if (Math.abs(lastcenterYF - centerYF) <= 20 && lastItemName === itemName) {
//log.info("调试-相同物品名和相近y坐标本次不拾取");
await sleep(2 * pickupDelay);
lastcenterYF = -20;
lastItemName = null;
} else {
keyPress("F");
log.info(`交互或拾取:"${itemName}"`);
lastcenterYF = centerYF;
lastItemName = itemName;
await sleep(pickupDelay);
}
} else {
/*
log.info("识别失败,尝试截图");
await refreshTargetItems(centerYF);
lastItemName = "";
*/
}
if (!foundTarget) {
//log.info(`调试-执行滚轮动作`);
const currentTime = new Date().getTime();
if (currentTime - lastMoveDown > timeMoveUp) {
await keyMouseScript.runFile(`assets/滚轮下翻.json`);
if (thisMoveUpTime === 0) thisMoveUpTime = currentTime;
if (currentTime - thisMoveUpTime >= timeMoveDown) {
lastMoveDown = currentTime;
thisMoveUpTime = 0;
}
} else {
await keyMouseScript.runFile(`assets/滚轮上翻.json`);
}
await sleep(rollingDelay);
}
}
async function performTemplateMatch(centerYF) {
try {
let result;
let itemName = null;
for (const targetItem of targetItems) {
//log.info(`正在尝试匹配${targetItem.itemName}`);
const cnLen = Math.min([...targetItem.itemName].filter(c => c >= '\u4e00' && c <= '\u9fff').length, 5);
const recognitionObject = RecognitionObject.TemplateMatch(
targetItem.template,
1219,
centerYF - 15,
12 + 28 * cnLen + 2,
30
);
recognitionObject.Threshold = TMthreshold;
recognitionObject.InitTemplate();
result = gameRegion.find(recognitionObject);
if (result.isExist()) {
itemName = targetItem.itemName;
break;
}
}
return itemName;
} catch (error) {
log.error(`模板匹配时发生异常: ${error.message}`);
return null;
}
}
async function findFIcon() {
let recognitionObject = RecognitionObject.TemplateMatch(fIcontemplate, 1102, 335, 34, 400);
recognitionObject.Threshold = 0.95;
recognitionObject.InitTemplate();
try {
let result = gameRegion.find(recognitionObject);
if (result.isExist()) {
return Math.round(result.y + result.height / 2);
}
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
if (!state.running)
return null;
}
await sleep(checkDelay);
return null;
}
async function isMainUI() {
const recognitionObject = RecognitionObject.TemplateMatch(mainUITemplate, 0, 0, 150, 150);
const maxAttempts = 1;
let attempts = 0;
while (attempts < maxAttempts && state.running) {
try {
const result = gameRegion.find(recognitionObject);
if (result.isExist()) return true;
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
if (!state.running) break;
return false;
}
attempts++;
await sleep(checkDelay);
}
return false;
}
}
async function processArtifacts() {
const decomposeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/decompose.png"));
const quickChooseRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/quickChoose.png"));
const confirmRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/confirm.png"));
const outDatedRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/ConfirmButton.png"), 760, 700, 100, 100);
await genshin.returnMainUi();
await sleep(500);
let result = 0;
try {
result = await decomposeArtifacts();
} catch (error) {
log.error(`处理狗粮分解时发生异常: ${error.message}`);
}
await genshin.returnMainUi();
return result;
async function decomposeArtifacts() {
keyPress("B");
if (await findAndClick(outDatedRo, true, 1500)) {
log.info("检测到过期物品弹窗,处理");
await sleep(1000);
}
let enterAttempts = 0;
await sleep(500);
while (enterAttempts < 10) {
const type = "圣遗物";
const clicked = await findAndClick([
`assets/RecognitionObject/背包界面/${type}1.png`,
`assets/RecognitionObject/背包界面/${type}2.png`
]);
if (clicked) break; // 找到并点击成功就退出循环
await sleep(750);
enterAttempts++;
await genshin.returnMainUi();
await sleep(100);
keyPress("B");
}
if (enterAttempts >= 10) {
log.warn("尝试十次未能成功进入背包界面");
notification.Send("尝试十次未能成功进入背包界面");
await genshin.returnMainUi();
return 0;
}
if (!await findAndClick(decomposeRo)) {
await genshin.returnMainUi();
return 0;
}
await sleep(1000);
// 识别已储存经验1570-880-1650-930
const digits = await numberTemplateMatch("assets/已储存经验数字", 1573, 885, 74, 36);
let initialValue = 0;
if (digits >= 0) {
initialValue = digits;
log.info(`已储存经验识别成功: ${initialValue}`);
} else {
log.warn(`在指定区域未识别到有效数字: ${initialValue}`);
}
if (!await findAndClick(quickChooseRo)) {
await genshin.returnMainUi();
return 0;
}
moveMouseTo(960, 540);
await sleep(1000);
// 点击“确认选择”按钮
if (!await findAndClick(confirmRo)) {
await genshin.returnMainUi();
return 0;
}
await sleep(2000);
// 当前总经验1470-880-205-70
const digits2 = await numberTemplateMatch("assets/分解可获得经验数字", 1469, 899, 180, 37, 0.95, 0.85, 5, 1);
let newValue = 0;
if (digits2 >= 0) {
newValue = digits2
log.info(`当前总经验识别成功: ${newValue}`);
} else {
log.warn(`在指定区域未识别到有效数字: ${newValue}`);
}
// 7. 计算分解获得经验=总经验-上次剩余
const resinExperience = Math.max(newValue - initialValue, 0);
log.info(`分解可获得经验: ${resinExperience}`);
let resultExperience = resinExperience;
if (resultExperience === 0) {
resultExperience = initialValue;
}
const result = resultExperience;
await genshin.returnMainUi();
return result;
}
}
async function mora() {
let result = 0;
let tryTimes = 0;
while (result === 0 && tryTimes < 3) {
log.info("开始尝试识别摩拉");
let enterAttempts = 0;
await genshin.returnMainUi();
await sleep(100);
keyPress("B");
await sleep(500);
while (enterAttempts < 10) {
const type = "养成道具";
const clicked = await findAndClick([
`assets/RecognitionObject/背包界面/${type}1.png`,
`assets/RecognitionObject/背包界面/${type}2.png`
]);
if (clicked) break; // 找到并点击成功就退出循环
await sleep(750);
enterAttempts++;
await genshin.returnMainUi();
await sleep(100);
keyPress("B");
}
if (enterAttempts >= 10) {
log.warn("尝试十次未能成功进入背包界面");
notification.Send("尝试十次未能成功进入背包界面");
await genshin.returnMainUi();
return 0;
}
let moraRes = 0;
await sleep(1000);
if (settings.notify) {
notification.Send(`当前摩拉如图`);
}
const moraRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/mora.png"));
const gameRegion = captureGameRegion();
let moraX = 336;
let moraY = 1004;
try {
const result = gameRegion.find(moraRo);
if (result.isExist()) {
moraX = result.x;
moraY = result.y;
}
} catch (err) {
} finally {
gameRegion.dispose();
}
moraRes = await numberTemplateMatch("assets/背包摩拉数字", moraX, moraY, 300, 40, 0.95, 0.85, 10);
if (moraRes >= 0) {
log.info(`成功识别到摩拉数值: ${moraRes}`);
result = moraRes;
} else {
log.warn("未能识别到摩拉数值。");
}
await sleep(500);
tryTimes++;
await genshin.returnMainUi();
}
return result;
}
/**
* 在指定区域内,用 0-9 的 PNG 模板做「多阈值 + 非极大抑制」数字识别,
* 最终把检测到的数字按左右顺序拼成一个整数返回。
*
* @param {string} numberPngFilePath - 存放 0.png ~ 9.png 的文件夹路径(不含文件名)
* @param {number} x - 待识别区域的左上角 x 坐标,默认 0
* @param {number} y - 待识别区域的左上角 y 坐标,默认 0
* @param {number} w - 待识别区域的宽度,默认 1920
* @param {number} h - 待识别区域的高度,默认 1080
* @param {number} maxThreshold - 模板匹配起始阈值,默认 0.95(最高可信度)
* @param {number} minThreshold - 模板匹配最低阈值,默认 0.8(最低可信度)
* @param {number} splitCount - 在 maxThreshold 与 minThreshold 之间做几次等间隔阈值递减,默认 3
* @param {number} maxOverlap - 非极大抑制时允许的最大重叠像素,默认 2只要 x 或 y 方向重叠大于该值即视为重复框
*
* @returns {number} 识别出的整数;若没有任何有效数字框则返回 -1
*
* @example
* const mora = await numberTemplateMatch('摩拉数字', 860, 70, 200, 40);
* if (mora >= 0) console.log(`当前摩拉:${mora}`);
*/
async function numberTemplateMatch(
numberPngFilePath,
x = 0, y = 0, w = 1920, h = 1080,
maxThreshold = 0.95,
minThreshold = 0.8,
splitCount = 3,
maxOverlap = 2
) {
let ros = [];
for (let i = 0; i <= 9; i++) {
ros[i] = RecognitionObject.TemplateMatch(
file.ReadImageMatSync(`${numberPngFilePath}/${i}.png`), x, y, w, h);
}
function setThreshold(roArr, newThreshold) {
for (let i = 0; i < roArr.length; i++) {
roArr[i].Threshold = newThreshold;
roArr[i].InitTemplate();
}
}
const gameRegion = captureGameRegion();
const allCandidates = [];
/* 1. splitCount 次等间隔阈值递减 */
for (let k = 0; k < splitCount; k++) {
const curThr = maxThreshold - (maxThreshold - minThreshold) * k / Math.max(splitCount - 1, 1);
setThreshold(ros, curThr);
/* 2. 0-9 每个模板跑一遍,所有框都收 */
for (let digit = 0; digit <= 9; digit++) {
const res = gameRegion.findMulti(ros[digit]);
if (res.count === 0) continue;
for (let i = 0; i < res.count; i++) {
const box = res[i];
allCandidates.push({
digit: digit,
x: box.x,
y: box.y,
w: box.width,
h: box.height,
thr: curThr
});
}
}
}
gameRegion.dispose();
/* 3. 无结果提前返回 -1 */
if (allCandidates.length === 0) {
return -1;
}
/* 4. 非极大抑制(必须 x、y 两个方向重叠都 > maxOverlap 才视为重复) */
const adopted = [];
for (const c of allCandidates) {
let overlap = false;
for (const a of adopted) {
const xOverlap = Math.max(0, Math.min(c.x + c.w, a.x + a.w) - Math.max(c.x, a.x));
const yOverlap = Math.max(0, Math.min(c.y + c.h, a.y + a.h) - Math.max(c.y, a.y));
if (xOverlap > maxOverlap && yOverlap > maxOverlap) {
overlap = true;
break;
}
}
if (!overlap) {
adopted.push(c);
//log.info(`在 [${c.x},${c.y},${c.w},${c.h}] 找到数字 ${c.digit},匹配阈值=${c.thr}`);
}
}
/* 5. 按 x 排序,拼整数;仍无有效框时返回 -1 */
if (adopted.length === 0) return -1;
adopted.sort((a, b) => a.x - b.x);
return adopted.reduce((num, item) => num * 10 + item.digit, 0);
}
/**
* 判断当前是否存在拾取滚轮图标
* @param {number} maxDuration 最大允许耗时(毫秒)
*/
async function hasScroll(maxDuration = 10) {
const start = Date.now();
let dodispose = false;
while (Date.now() - start < maxDuration) {
if (!gameRegion) {
gameRegion = captureGameRegion();
dodispose = true;
}
try {
const result = gameRegion.find(scrollRo);
if (result.isExist()) return true;
} catch (error) {
log.error(`识别图像时发生异常: ${error.message}`);
return false; // 一旦出现异常直接退出,不再重试
}
await sleep(checkDelay); // 识别间隔
if (dodispose) {
gameRegion.dispose();
dodispose = false; // 已经释放,标记避免重复 dispose
}
}
/* 超时仍未识别到,返回失败 */
return false;
}