mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-25 04:59:52 +08:00
* js:AAA狗粮批发2.2.0 1.滚轮改为仅在必要时触发 2.更新readme * js:采集cd管理 滚轮仅在必要时触发 * js:锄地一条龙 移除材料图片zip 滚轮改为仅必要时触发 * js:联机团购 滚轮改为仅必要时触发 * js:锄地一条龙 优化吃药流程
1682 lines
63 KiB
JavaScript
1682 lines
63 KiB
JavaScript
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 代表 2P,1 代表 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;
|
||
} |