diff --git a/repo/js/AAA-Artifacts-Bulk-Supply/README.md b/repo/js/AAA-Artifacts-Bulk-Supply/README.md index 1bb58407b..6f5465503 100644 --- a/repo/js/AAA-Artifacts-Bulk-Supply/README.md +++ b/repo/js/AAA-Artifacts-Bulk-Supply/README.md @@ -82,6 +82,8 @@ https://www.kdocs.cn/wo/sl/v13uXscL ## 更新日志 +### 1.2.3(2025.09.01) +1.修复了日期和时间计算的一个bug,该bug曾导致每个月1号时断点续跑不生效 ### 1.2.3(2025.08.31) 1.修几条路线640,642,激活路线度假村/智障厅 ### 1.2.2(2025.08.30) diff --git a/repo/js/AAA-Artifacts-Bulk-Supply/main.js b/repo/js/AAA-Artifacts-Bulk-Supply/main.js index 82c29120d..511771781 100644 --- a/repo/js/AAA-Artifacts-Bulk-Supply/main.js +++ b/repo/js/AAA-Artifacts-Bulk-Supply/main.js @@ -245,12 +245,30 @@ async function readRecord(accountName) { /* ---------- 判断今日是否运行(北京时间 04:00 分界,手动拼接 UTC 20 点) ---------- */ if (record.lastRunDate) { - const [y, m, d] = record.lastRunDate.split("/").map(Number); - // 东八区 04:00 对应 UTC 20:00 - const lastRun4AM = new Date(`${y}-${String(m).padStart(2, '0')}-${String(d - 1).padStart(2, '0')}T20:00:00.000Z`).getTime(); + const [y, m, d] = record.lastRunDate.split('/').map(Number); + + // 1. 用 UTC 构造记录日期 00:00:00 + const recordUtc = Date.UTC(y, m - 1, d); // 毫秒 + + // 2. 减 24 小时得到“前一天” + const prevUtc = recordUtc - 24 * 60 * 60 * 1000; + + // 3. 从毫秒时间戳里取出 UTC 年月日 + const prev = new Date(prevUtc); + const yy = prev.getUTCFullYear(); + const mm = prev.getUTCMonth() + 1; // 1-based + const dd = prev.getUTCDate(); + + // 4. 严格按模板字符串拼成合法日期 + const lastRun4AM = new Date( + `${yy}-${String(mm).padStart(2, '0')}-${String(dd).padStart(2, '0')}T20:00:00.000Z` + ).getTime(); + //log.info(`lastRun4AM = ${new Date(lastRun4AM).toISOString()}`); - const now = Date.now(); + const now = Date.now(); // 当前毫秒时间戳 + //log.info(`时间差为 ${now - lastRun4AM} ms`); + if (now - lastRun4AM < 24 * 60 * 60 * 1000) { log.info("今日已经运行过狗粮"); state.runnedToday = true; diff --git a/repo/js/AAA-Artifacts-Bulk-Supply/manifest.json b/repo/js/AAA-Artifacts-Bulk-Supply/manifest.json index 0fa07c2fd..0d2cd69c7 100644 --- a/repo/js/AAA-Artifacts-Bulk-Supply/manifest.json +++ b/repo/js/AAA-Artifacts-Bulk-Supply/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "AAA狗粮批发", - "version": "1.2.3", + "version": "1.2.4", "tags": [ "狗粮" ], diff --git a/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/allowEnter.png b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/allowEnter.png new file mode 100644 index 000000000..8c6cd7632 Binary files /dev/null and b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/allowEnter.png differ diff --git a/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/enterUID.png b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/enterUID.png new file mode 100644 index 000000000..d1041e548 Binary files /dev/null and b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/enterUID.png differ diff --git a/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/requestEnter.png b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/requestEnter.png new file mode 100644 index 000000000..0b2e42bf4 Binary files /dev/null and b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/requestEnter.png differ diff --git a/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/search.png b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/search.png new file mode 100644 index 000000000..d53060012 Binary files /dev/null and b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/search.png differ diff --git a/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/yUI.png b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/yUI.png new file mode 100644 index 000000000..4e365bb76 Binary files /dev/null and b/repo/js/ArtifactsGroupPurchasing/assets/RecognitionObject/yUI.png differ diff --git a/repo/js/ArtifactsGroupPurchasing/main.js b/repo/js/ArtifactsGroupPurchasing/main.js index daf317efd..4d9ae5a44 100644 --- a/repo/js/ArtifactsGroupPurchasing/main.js +++ b/repo/js/ArtifactsGroupPurchasing/main.js @@ -1,56 +1,101 @@ -// 初始化自定义配置并赋予默认值 -let decomposeMode = settings.decomposeMode || "保留";//狗粮分解模式 -let keep4Star = settings.keep4Star;//保留四星 -let autoSalvage = settings.autoSalvage;//启用自动分解 -let notify = settings.notify;//启用通知 -let p1EndingRoute = settings.p1EndingRoute || "枫丹高塔"; -let p2EndingRoute = settings.p2EndingRoute || "度假村"; -let p3EndingRoute = settings.p3EndingRoute || "智障厅"; -let p4EndingRoute = settings.p4EndingRoute || "踏鞴砂"; -let accountName = settings.accountName || "默认账户"; -let runExtra = settings.runExtra || false; -let forceGroupNumber = settings.forceGroupNumber || 0; - - -//文件路径 -//摧毁狗粮 -const ArtifactsButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/ArtifactsButton.png")); -const DeleteButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/DeleteButton.png")); -const AutoAddButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/AutoAddButton.png")); -const ConfirmButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/ConfirmButton.png")); -const DestoryButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/DestoryButton.png")); -const MidDestoryButtonRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/DestoryButton.png"), 900, 600, 500, 300); -const CharacterMenuRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/CharacterMenu.png"), 60, 991, 38, 38); -//分解狗粮 -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 doDecomposeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/doDecompose.png")); -const doDecompose2Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/doDecompose2.png")); -//联机图标 -const p2InBigMapRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/2pInBigMap.png")); -const p3InBigMapRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/3pInBigMap.png")); -const p4InBigMapRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/4pInBigMap.png")); -const kickAllRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/kickAll.png")); -const confirmKickRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/confirmKick.png")); -const leaveTeamRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/leaveTeam.png")); - -//初始化变量 -let artifactExperienceDiff = 0; -let moraDiff = 0; -let failcount = 0; -let autoSalvageCount = 0; -let furinaState = "unknown"; -let _infoPoints = null; // 缓存 assets/info.json 解析后的数组 - (async function () { + if (settings.groupMode != "按照下列配置自动进入并运行") await runGroupPurchasing(); + if (settings.groupMode != "手动进入后运行") { + await switchPartyIfNeeded(settings.partyName) + //解析与输出自定义配置 + 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}个执行`); + + // 按 runningOrder 依次进入世界并执行联机收尾 + for (const idx of enteringIndex) { + if (settings.usingCharacter) keyPress(`${settings.usingCharacter}`); + //构造加入idx号世界的autoEnter的settings + let autoEnterSettings; + if (idx === yourIndex) { + // 构造房主配置 + autoEnterSettings = { + enterMode: "等待他人进入", + permissionMode: "白名单", + timeout: 5, + maxEnterCount: enteringIndex.length - 1 + }; + const permits = {}; + let permitIndex = 1; + for (const otherIdx of enteringIndex) { + if (otherIdx !== yourIndex) { + const pName = settings[`p${otherIdx}Name`]; + if (pName) { + permits[`nameToPermit${permitIndex}`] = pName; + permitIndex++; + } + } + } + // 把构造的permits合并到autoEnterSettings中 + Object.assign(autoEnterSettings, permits); + log.info(`等待他人进入自己世界`); + } else { + // 构造队员配置 + autoEnterSettings = { + enterMode: "进入他人世界", + enteringUID: settings[`p${idx}UID`], + timeout: 5 + }; + log.info(`将要进入序号${idx},uid为${settings[`p${idx}UID`]}的世界`); + } + await autoEnter(autoEnterSettings); + //执行对应的联机狗粮 + await runGroupPurchasing(); + } + //如果勾选了额外,在结束后再执行一次额外路线 + if (settings.runExtra) await runGroupPurchasing(); + } +} +)(); + +/** + * 群收尾 / 额外路线统一入口 + * + */ +async function runGroupPurchasing() { + // ===== 1. 读取配置 ===== + const p1EndingRoute = settings.p1EndingRoute || "枫丹高塔"; + const p2EndingRoute = settings.p2EndingRoute || "度假村"; + const p3EndingRoute = settings.p3EndingRoute || "智障厅"; + const p4EndingRoute = settings.p4EndingRoute || "踏鞴砂"; + const runExtra = settings.runExtra || false; + const forceGroupNumber = settings.forceGroupNumber || 0; + + // ===== 2. 图标模板 ===== + const p2InBigMapRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/2pInBigMap.png")); + const p3InBigMapRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/3pInBigMap.png")); + const p4InBigMapRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/4pInBigMap.png")); + const kickAllRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/kickAll.png")); + const confirmKickRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/confirmKick.png")); + const leaveTeamRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/RecognitionObject/leaveTeam.png")); + + // ===== 3. 初始化变量 ===== + let _infoPoints = null; + let running = true; + + // ===== 4. 主流程 ===== setGameMetrics(1920, 1080, 1); let groupNumBer = await getPlayerSign(); - if (groupNumBer != 0) { - log.info(`在队伍中编号为${groupNumBer}`); - } else { - log.info(`不处于联机模式或识别异常`); - } + if (groupNumBer !== 0) log.info(`在队伍中编号为${groupNumBer}`); + else log.info(`不处于联机模式或识别异常`); if (forceGroupNumber != 0) { groupNumBer = forceGroupNumber; @@ -58,61 +103,22 @@ let _infoPoints = null; // 缓存 assets/info.json 解析后的数组 } if (groupNumBer === 1) { - // 启用自动拾取的实时任务 dispatcher.addTimer(new RealtimeTimer("AutoPick")); - //自己是房主,检测总人数 log.info("是1p,检测当前总人数"); const totalNumber = await findTotalNumber(); - - //预处理 - /*await readRecord(accountName);//读取记录文件 - - //运行前按自定义配置清理狗粮 - if (settings.decomposeMode === "分解(经验瓶)") { - await processArtifacts(21); - } else { - artifactExperienceDiff -= await processArtifacts(21); - } - - moraDiff -= await mora();*/ - - //循环检测,直到其他人所有人到位 await waitForReady(totalNumber); + for (let i = 1; i <= totalNumber; i++) await runEndingPath(i); - //根据人数决定执行路线 - for (let i = 1; i <= totalNumber; i++) { - //执行第i条收尾路线 - await runEndingPath(i); - } - - //运行结束,解散队伍? await genshin.returnMainUi(); await keyPress("F2"); await sleep(2000); await findAndClick(kickAllRo); await sleep(500); await findAndClick(confirmKickRo); - await waitForMainUI(true);//等待直到回到主界面 + await waitForMainUI(true); await genshin.returnMainUi(); - - //运行后按自定义配置清理狗粮 - /*artifactExperienceDiff += await processArtifacts(21); - moraDiff += await mora(); - log.info(`狗粮路线获取摩拉: ${moraDiff}`); - log.info(`狗粮路线获取狗粮经验: ${artifactExperienceDiff}`); - //修改records - for (let i = record.records.length - 1; i > 0; i--) { - record.records[i] = record.records[i - 1]; - } - record.records[0] = `日期:${record.lastRunDate},狗粮经验${artifactExperienceDiff},摩拉${moraDiff}`; - if (settings.notify) { - notification.Send(`日期:${record.lastRunDate},狗粮经验${artifactExperienceDiff},摩拉${moraDiff}`); - } - await writeRecord(accountName);//修改记录文件*/ } else if (groupNumBer > 1) { - //自己是队员,前往对应的占位点 await goToTarget(groupNumBer); - //等待到房主解散队伍并返回主界面? if (await waitForMainUI(false, 2 * 60 * 60 * 1000)) { await waitForMainUI(true); await genshin.returnMainUi(); @@ -121,963 +127,498 @@ let _infoPoints = null; // 缓存 assets/info.json 解析后的数组 } } else if (runExtra) { log.info("请确保联机收尾已结束,将开始运行额外路线"); - // 启用自动拾取的实时任务 dispatcher.addTimer(new RealtimeTimer("AutoPick")); await runExtraPath(); } - /* - for (i = 0; i < 3; i++) { - //确保回到单机模式 - const finalPlayerSign = await getPlayerSign(); - if (finalPlayerSign != 0) { - await genshin.returnMainUi(); - await keyPress("F2"); - await sleep(2000); - if (finalPlayerSign === 1) { - await findAndClick(kickAllRo); - await sleep(500); - await findAndClick(confirmKickRo); - await waitForMainUI(true);//等待直到回到主界面 - await genshin.returnMainUi(); - } else { - await findAndClick(leaveTeamRo); - await sleep(500); - await waitForMainUI(true);//等待直到回到主界面 - await genshin.returnMainUi(); - } - } else { - log.info("已成功回到单人模式"); - break; - } - } - */ -} -)(); + running = false; + + //等待主界面状态 + async function waitForMainUI(requirement, timeOut = 60 * 1000) { + log.info(`等待至多${timeOut}毫秒`) + const startTime = Date.now(); + while (Date.now() - startTime < timeOut) { + const mainUIState = await isMainUI(); + 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; + log.info(`已等待 ${min}分 ${sec}秒 ${ms}毫秒`); -//等待主界面状态 -async function waitForMainUI(requirement, timeOut = 60 * 1000) { - log.info(`等待至多${timeOut}毫秒`) - const startTime = Date.now(); - while (Date.now() - startTime < timeOut) { - const mainUIState = await isMainUI(); - 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; - log.info(`已等待 ${min}分 ${sec}秒 ${ms}毫秒`); - - await sleep(1000); - } - 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(50); // 每次检测间隔 50 毫秒 - } - return false; // 如果尝试次数达到上限或取消,返回 false -} - - -//获取联机世界的当前玩家标识 -async function getPlayerSign() { - const picDic = { - "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 p1Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["1P"]), 344, 22, 45, 45); - const p2Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["2P"]), 344, 22, 45, 45); - const p3Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["3P"]), 344, 22, 45, 45); - const p4Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["4P"]), 344, 22, 45, 45); - moveMouseTo(1555, 860); // 移走鼠标,防止干扰识别 - const gameRegion = captureGameRegion(); - // 当前页面模板匹配 - let p1 = gameRegion.Find(p1Ro); - let p2 = gameRegion.Find(p2Ro); - let p3 = gameRegion.Find(p3Ro); - let p4 = gameRegion.Find(p4Ro); - gameRegion.dispose(); - if (p1.isExist()) return 1; - if (p2.isExist()) return 2; - if (p3.isExist()) return 3; - if (p4.isExist()) return 4; - return 0; -} - -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; -} - -/** - * 等待所有队友(2P/3P/4P)就位 - * @param {number} totalNumber 联机总人数(包含自己) - * @param {number} timeOut 最长等待毫秒 - */ -async function waitForReady(totalNumber, timeOut = 300000) { - await genshin.tpToStatueOfTheSeven(); - - // 实际需要检测的队友编号: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("所有队友已就绪"); - return true; - } - - // 每轮检测后稍等,防止刷屏 - await sleep(500); - } - - log.warn("等待队友就绪超时"); - return false; -} - -async function checkReady(i) { - /* 1. 先把地图移到目标点位(point 来自 info.json) */ - const point = await getPointByPlayer(i); - if (!point) return false; - 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 <= 20; // 20 m 阈值,可按需调整 -} - - -/** - * 根据玩家编号返回该路线在 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, true); - - if (files.length === 0) { - log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`); - return; - } - - for (const { fullPath } of files) { - log.info(`开始执行路线: ${fullPath}`); - await pathingScript.runFile(fullPath); - } - - log.info(`${folderName} 的全部路线已跑完`); -} - -/** - * 执行额外路线 - */ -async function runExtraPath() { - - const folderPath = `assets/ArtifactsPath/额外/执行`; - const files = await readFolder(folderPath, true); - - if (files.length === 0) { - log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`); - return; - } - - for (const { fullPath } of files) { - log.info(`开始执行路线: ${fullPath}`); - await pathingScript.runFile(fullPath); - } - - 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, true); - - if (files.length === 0) { - log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`); - return; - } - - for (const { fullPath } of files) { - log.info(`开始执行路线: ${fullPath}`); - await pathingScript.runFile(fullPath); - } - - log.info(`${folderName} 的全部路线已跑完`); -} - -async function readRecord(accountName) { - /* ---------- 文件名合法性校验 ---------- */ - const illegalCharacters = /[\\/:*?"<>|]/; - const reservedNames = [ - "CON", "PRN", "AUX", "NUL", - "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", - "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" - ]; - - let finalAccountName = accountName; - - if (accountName === "" || - accountName.startsWith(" ") || - accountName.endsWith(" ") || - illegalCharacters.test(accountName) || - reservedNames.includes(accountName.toUpperCase()) || - accountName.length > 255 - ) { - log.error(`账户名 "${accountName}" 不合法,将使用默认值`); - finalAccountName = "默认账户"; - await sleep(5000); - } else { - log.info(`账户名 "${accountName}" 合法`); - } - - /* ---------- 读取记录文件 ---------- */ - const recordFolderPath = "records/"; - const recordFilePath = `records/${finalAccountName}.txt`; - - const filesInSubFolder = file.ReadPathSync(recordFolderPath); - let fileExists = false; - for (const filePath of filesInSubFolder) { - if (filePath === `records\\${accountName}.txt`) { - fileExists = true; - break; - } - } - - - - /* ---------- 初始化记录对象 ---------- */ - record = { - records: new Array(14).fill(""), - version: "" - }; - - let recordIndex = 0; - - if (fileExists) { - log.info(`记录文件 ${recordFilePath} 存在`); - } else { - log.warn(`无记录文件,将使用默认数据`); - return; - } - - const content = await file.readText(recordFilePath); - const lines = content.split("\n"); - - - - /* ---------- 逐行解析 ---------- */ - for (const rawLine of lines) { - const line = rawLine.trim(); - if (!line) continue; - - if (line.startsWith("日期") && recordIndex < record.records.length) { - record.records[recordIndex++] = line; - } - } - - log.info(`上次运行日期: ${record.lastRunDate}`); - log.info(`上次激活路线开始时间: ${record.lastActivateTime.toLocaleString()}`); - - /* ---------- 读取 manifest 版本 ---------- */ - try { - const manifest = JSON.parse(await file.readText("manifest.json")); - record.version = manifest.version; - log.info(`当前版本为${record.version}`); - } catch (err) { - log.error("读取或解析 manifest.json 失败:", err); - } - -} - -async function writeRecord(accountName) { - if (state.cancel) return; - const recordFilePath = `records/${accountName}.txt`; - - const lines = [ - ...record.records.filter(Boolean) - ]; - - const content = lines.join('\n'); - - try { - await file.writeText(recordFilePath, content, false); - log.info(`记录已写入 ${recordFilePath}`); - } catch (e) { - log.error(`写入 ${recordFilePath} 失败:`, e); - } -} - -async function findAndClick(target, maxAttempts = 20) { - for (let attempts = 0; attempts < maxAttempts; attempts++) { - const gameRegion = captureGameRegion(); - try { - const result = gameRegion.find(target); - if (result.isExist) { - result.click(); - return true; // 成功立刻返回 - } - log.warn(`识别失败,第 ${attempts + 1} 次重试`); - } catch (err) { - } finally { - gameRegion.dispose(); - } - if (attempts < maxAttempts - 1) { // 最后一次不再 sleep - await sleep(250); - } - } - log.error("已达到重试次数上限,仍未找到目标"); - return false; -} - -async function processArtifacts(times = 1) { - await genshin.returnMainUi(); - await sleep(500); - let result = 0; - try { - if (settings.decomposeMode === "销毁(摩拉)") { - result = await destroyArtifacts(times); - } else { - result = await decomposeArtifacts(); - } - } catch (error) { - log.error(`处理狗粮分解时发生异常: ${error.message}`); - } - await genshin.returnMainUi(); - return result; - - async function decomposeArtifacts() { - keyPress("B"); - await sleep(1000); - await click(670, 45); - await sleep(500); - if (!await findAndClick(decomposeRo)) { - await genshin.returnMainUi(); - return 0; - } - await sleep(1000); - - //识别已储存经验(1570-880-1650-930) - let regionToCheck1 = { x: 1570, y: 880, width: 80, height: 50 }; - let initialNum = await recognizeTextInRegion(regionToCheck1); - let initialValue = 0; - - if (initialNum && !isNaN(parseInt(initialNum, 10))) { - initialValue = parseInt(initialNum, 10); - log.info(`已储存经验识别成功: ${initialValue}`); - } else { - log.warn(`在指定区域未识别到有效数字: ${initialValue}`); - } - let regionToCheck3 = { x: 100, y: 885, width: 170, height: 50 }; - let decomposedNum = 0; - let firstNumber = 0; - let firstNumber2 = 0; - - if (settings.keep4Star) { - 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(1000); - - decomposedNum = await recognizeTextInRegion(regionToCheck3); - - // 使用正则表达式提取第一个数字 - const match = decomposedNum.match(/已选(\d+)/); - - // 检查是否匹配成功 - if (match) { - // 将匹配到的第一个数字转换为数字类型并存储在变量中 - firstNumber = Number(match[1]); - log.info(`1-4星总数量: ${firstNumber}`); - } else { - log.info("识别失败"); - } - keyPress("VK_ESCAPE"); - - await sleep(500); - if (!await findAndClick(decomposeRo)) { - await genshin.returnMainUi(); - return 0; - } - await sleep(500); - } - if (!await findAndClick(quickChooseRo)) { - await genshin.returnMainUi(); - return 0; - } - moveMouseTo(960, 540); - await sleep(1000); - - if (settings.keep4Star) { - await click(370, 370);//取消选择四星 await sleep(1000); } - // 点击“确认选择”按钮 - if (!await findAndClick(confirmRo)) { - await genshin.returnMainUi(); - return 0; - } - await sleep(1500); - - let decomposedNum2 = await recognizeTextInRegion(regionToCheck3); - - // 使用正则表达式提取第一个数字 - const match2 = decomposedNum2.match(/已选(\d+)/); - - // 检查是否匹配成功 - if (match2) { - // 将匹配到的第一个数字转换为数字类型并存储在变量中 - firstNumber2 = Number(match2[1]); - log.info(`分解总数是: ${firstNumber2}`); - } else { - log.info("识别失败"); - } - //识别当前总经验 - if (settings.notify) { - notification.Send(`当前经验如图`); - } - let regionToCheck2 = { x: 1470, y: 880, width: 205, height: 70 }; - let newNum = await recognizeTextInRegion(regionToCheck2); - let newValue = 0; - - if (newNum && !isNaN(parseInt(newNum, 10))) { - newValue = parseInt(newNum, 10); - log.info(`当前总经验识别成功: ${newValue}`); - } else { - log.warn(`在指定区域未识别到有效数字: ${newValue}`); - } - - if (settings.decomposeMode === "分解(经验瓶)") { - log.info(`用户选择了分解,执行分解`); - // 根据用户配置,分解狗粮 - await sleep(1000); - // 点击分解按钮 - if (!await findAndClick(doDecomposeRo)) { - await genshin.returnMainUi(); - return 0; - } - await sleep(500); - - // 4. "进行分解"按钮// 点击进行分解按钮 - if (!await findAndClick(doDecompose2Ro)) { - await genshin.returnMainUi(); - return 0; - } - await sleep(1000); - - // 5. 关闭确认界面 - await click(1340, 755); - await sleep(1000); - } - else { - log.info(`用户未选择分解,不执行分解`); - } - - // 7. 计算分解获得经验=总经验-上次剩余 - const resinExperience = Math.max(newValue - initialValue, 0); - log.info(`分解可获得经验: ${resinExperience}`); - let fourStarNum = firstNumber - firstNumber2; - if (settings.keep4Star) { - log.info(`保留的四星数量: ${fourStarNum}`); - } - let resultExperience = resinExperience; - if (resultExperience === 0) { - resultExperience = initialValue; - } - const result = resultExperience; - await genshin.returnMainUi(); - return result; + 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); - async function destroyArtifacts(times = 1) { - await genshin.returnMainUi(); - await sleep(250); - keyPress("B"); - await sleep(500); - await findAndClick(ArtifactsButtonRo, 5) - try { - for (let i = 0; i < times; i++) { - // 点击摧毁 - if (!await findAndClick(DeleteButtonRo)) { - await genshin.returnMainUi(); - return; - } - await sleep(600); - // 点击自动添加 - if (!await findAndClick(AutoAddButtonRo)) { - await genshin.returnMainUi(); - return; - } - await sleep(900); - click(150, 150); - await sleep(300); - click(150, 220); - await sleep(300); - click(150, 300); - if (!settings.keep4Star) { - await sleep(300); - click(150, 370); - } - // 点击快捷放入 - if (!await findAndClick(ConfirmButtonRo)) { - await genshin.returnMainUi(); - return; - } - await sleep(600); - // 点击摧毁 - if (!await findAndClick(DestoryButtonRo)) { - await genshin.returnMainUi(); - return; - } - await sleep(600); - // 弹出页面点击摧毁 - if (!await findAndClick(MidDestoryButtonRo)) { - await genshin.returnMainUi(); - return; - } - await sleep(600); - click(960, 1000);// 点击空白处 - await sleep(1000); - } - } catch (ex) { - log.info("背包里的圣遗物已摧毁完毕,提前结束") - } finally { - await genshin.returnMainUi(); - } + // 尝试次数设置为 5 次 + const maxAttempts = 5; - } -} - -async function mora() { - // 定义一个函数用于识别图像 - async function recognizeImage(recognitionObject, timeout = 5000) { - let startTime = Date.now(); - while (Date.now() - startTime < timeout) { + let attempts = 0; + while (attempts < maxAttempts) { try { - // 尝试识别图像 - const gameRegion = captureGameRegion(); - let imageResult = gameRegion.find(recognitionObject); - gameRegion.dispose; - if (imageResult) { - // log.info(`成功识别图像,坐标: x=${imageResult.x}, y=${imageResult.y}`); - // log.info(`图像尺寸: width=${imageResult.width}, height=${imageResult.height}`); - return { success: true, x: imageResult.x, y: imageResult.y }; + + 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 } - await sleep(500); // 短暂延迟,避免过快循环 + attempts++; // 增加尝试次数 + await sleep(50); // 每次检测间隔 50 毫秒 } - log.warn(`经过多次尝试,仍然无法识别图像`); - return { success: false }; + return false; // 如果尝试次数达到上限或取消,返回 false } - let result = 0; - let tryTimes = 0; - while (result === 0 && tryTimes < 3) { + + + //获取联机世界的当前玩家标识 + async function getPlayerSign() { + const picDic = { + "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(100); - log.info("开始尝试识别摩拉"); - // 按下 C 键 - keyPress("C"); - await sleep(1500); - let recognized = false; - // 识别“角色菜单”图标或“天赋”文字 - let startTime = Date.now(); - while (Date.now() - startTime < 5000) { - // 尝试识别“角色菜单”图标 - let characterMenuResult = await recognizeImage(CharacterMenuRo, 5000); - if (characterMenuResult.success) { - await click(177, 433); - await sleep(500); - recognized = true; - break; - } - - // 尝试识别“天赋”文字 - let targetText = "天赋"; - let ocrRegion = { x: 133, y: 395, width: 115, height: 70 }; // 设置对应的识别区域 - let talentResult = await recognizeTextAndClick(targetText, ocrRegion); - if (talentResult.success) { - log.info(`点击天赋文字,坐标: x=${talentResult.x}, y=${talentResult.y}`); - recognized = true; - break; - } - - await sleep(1000); // 短暂延迟,避免过快循环 - } - - let recognizedText = ""; - - // 如果识别到了“角色菜单”或“天赋”,则识别“摩拉数值” - if (recognized) { - if (settings.notify) { - notification.Send(`当前摩拉如图`); - } - let ocrRegionMora = { x: 1620, y: 25, width: 152, height: 46 }; // 设置对应的识别区域 - recognizedText = await recognizeTextInRegion(ocrRegionMora); - if (recognizedText) { - log.info(`成功识别到摩拉数值: ${recognizedText}`); - result = recognizedText; - } else { - log.warn("未能识别到摩拉数值。"); - } - } else { - log.warn("未能识别到角色菜单或天赋"); - } await sleep(500); - tryTimes++; + const p1Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["1P"]), 344, 22, 45, 45); + const p2Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["2P"]), 344, 22, 45, 45); + const p3Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["3P"]), 344, 22, 45, 45); + const p4Ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic["4P"]), 344, 22, 45, 45); + moveMouseTo(1555, 860); // 移走鼠标,防止干扰识别 + const gameRegion = captureGameRegion(); + // 当前页面模板匹配 + let p1 = gameRegion.Find(p1Ro); + let p2 = gameRegion.Find(p2Ro); + let p3 = gameRegion.Find(p3Ro); + let p4 = gameRegion.Find(p4Ro); + gameRegion.dispose(); + if (p1.isExist()) return 1; + if (p2.isExist()) return 2; + if (p3.isExist()) return 3; + if (p4.isExist()) return 4; + return 0; + } + + async function findTotalNumber() { await genshin.returnMainUi(); - } - return Number(result); -} + await keyPress("F2"); + await sleep(2000); -// 定义一个独立的函数用于在指定区域进行 OCR 识别并输出识别内容 -async function recognizeTextInRegion(ocrRegion, timeout = 5000) { - let startTime = Date.now(); - let retryCount = 0; // 重试计数 - while (Date.now() - startTime < timeout) { - try { - // 在指定区域进行 OCR 识别 - const gameRegion = captureGameRegion(); - let ocrResult = gameRegion.find(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height)); - gameRegion.dispose(); - if (ocrResult) { - let correctedText = ocrResult.text; - return correctedText; // 返回识别到的内容 - } else { - log.warn(`OCR 识别区域未找到内容`); - return null; // 如果 OCR 未识别到内容,返回 null - } - } catch (error) { - retryCount++; // 增加重试计数 - log.warn(`OCR 识别失败,正在进行第 ${retryCount} 次重试...`); + // 定义模板 + 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++; } - await sleep(500); // 短暂延迟,避免过快循环 - } - log.warn(`经过多次尝试,仍然无法在指定区域识别到文字`); - return null; // 如果未识别到文字,返回 null -} -// 定义一个函数用于识别文字并点击 -async function recognizeTextAndClick(targetText, ocrRegion, timeout = 3000) { - let startTime = Date.now(); - let retryCount = 0; // 重试计数 - while (Date.now() - startTime < timeout) { - try { - // 尝试 OCR 识别 - const gameRegion = captureGameRegion(); - let resList = gameRegion.findMulti(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height)); // 指定识别区域 - gameRegion.dispose(); - // 遍历识别结果,检查是否找到目标文本 - for (let res of resList) { - let correctedText = res.text; - if (correctedText.includes(targetText)) { - // 如果找到目标文本,计算并点击文字的中心坐标 - let centerX = Math.round(res.x + res.width / 2); - let centerY = Math.round(res.y + res.height / 2); - await click(centerX, centerY); - await sleep(500); // 确保点击后有足够的时间等待 - return { success: true, x: centerX, y: centerY }; + // 依次匹配 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; + } + + /** + * 等待所有队友(2P/3P/4P)就位 + * @param {number} totalNumber 联机总人数(包含自己) + * @param {number} timeOut 最长等待毫秒 + */ + async function waitForReady(totalNumber, timeOut = 300000) { + await genshin.tpToStatueOfTheSeven(); + + // 实际需要检测的队友编号: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; // 还有没就绪的 } } - } catch (error) { - retryCount++; // 增加重试计数 - log.warn(`页面标志识别失败,正在进行第 ${retryCount} 次重试...`); + + if (allReady) { + log.info("所有队友已就绪"); + if (settings.runDebug) await sleep(10000); + return true; + } + + // 每轮检测后稍等,防止刷屏 + await sleep(500); } - await sleep(1000); // 短暂延迟,避免过快循环 + + log.warn("等待队友就绪超时"); + return false; } - log.warn(`经过多次尝试,仍然无法识别文字: ${targetText},尝试点击默认中心位置`); - let centerX = Math.round(ocrRegion.x + ocrRegion.width / 2); - let centerY = Math.round(ocrRegion.y + ocrRegion.height / 2); - await click(centerX, centerY); - await sleep(1000); - return { success: false }; -} -// 定义 readFolder 函数 -async function readFolder(folderPath, onlyJson) { - // 新增一个堆栈,初始时包含 folderPath - const folderStack = [folderPath]; + async function checkReady(i) { + /* 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 files = []; + const template = file.ReadImageMatSync(tplPath); + const recognitionObj = RecognitionObject.TemplateMatch(template, 0, 0, 1920, 1080); // 全屏查找,可自行改区域 + if (await findAndClick(recognitionObj, 5)) await sleep(1000); - // 当堆栈不为空时,继续处理 - while (folderStack.length > 0) { - // 从堆栈中弹出一个路径 - const currentPath = folderStack.pop(); + await genshin.moveMapTo(Math.round(point.x), Math.round(point.y)); - // 读取当前路径下的所有文件和子文件夹路径 - const filesInSubFolder = file.ReadPathSync(currentPath); + /* 2. 取图标屏幕坐标 */ + const pos = await getPlayerIconPos(i); + if (!pos || !pos.found) return false; - // 临时数组,用于存储子文件夹路径 - const subFolders = []; - for (const filePath of filesInSubFolder) { - if (file.IsFolder(filePath)) { - // 如果是文件夹,先存储到临时数组中 - subFolders.push(filePath); - } else { - // 如果是文件,根据 onlyJson 判断是否存储 - if (onlyJson) { - if (filePath.endsWith(".json")) { + /* 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 <= 20; // 20 m 阈值,可按需调整 + } + + + /** + * 根据玩家编号返回该路线在 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, true); + + if (files.length === 0) { + log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`); + return; + } + if (!settings.runDebug) { + for (const { fullPath } of files) { + log.info(`开始执行路线: ${fullPath}`); + await fakeLog(`${fullPath}`, false, true, 0); + await pathingScript.runFile(fullPath); + await fakeLog(`${fullPath}`, false, false, 0); + } + + log.info(`${folderName} 的全部路线已完成`); + } else { + log.info("当前为调试模式,跳过执行路线"); + } + } + + /** + * 执行额外路线 + */ + async function runExtraPath() { + + const folderPath = `assets/ArtifactsPath/额外/执行`; + const files = await readFolder(folderPath, true); + + if (files.length === 0) { + log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`); + return; + } + + if (!settings.runDebug) { + for (const { fullPath } of files) { + log.info(`开始执行路线: ${fullPath}`); + await fakeLog(`${fullPath}`, false, true, 0); + await pathingScript.runFile(fullPath); + await fakeLog(`${fullPath}`, false, false, 0); + } + + 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, true); + + if (files.length === 0) { + log.warn(`文件夹 ${folderPath} 下未找到任何 JSON 路线文件`); + return; + } + + for (const { fullPath } of files) { + log.info(`开始执行路线: ${fullPath}`); + await fakeLog(`${fullPath}`, false, true, 0); + await pathingScript.runFile(fullPath); + await fakeLog(`${fullPath}`, false, false, 0); + } + + log.info(`${folderName} 的全部路线已完成`); + } + + async function findAndClick(target, maxAttempts = 20) { + for (let attempts = 0; attempts < maxAttempts; attempts++) { + const gameRegion = captureGameRegion(); + try { + const result = gameRegion.find(target); + if (result.isExist()) { + result.click(); + return true; // 成功立刻返回 + } + log.warn(`识别失败,第 ${attempts + 1} 次重试`); + } catch (err) { + } finally { + gameRegion.dispose(); + } + if (attempts < maxAttempts - 1) { // 最后一次不再 sleep + await sleep(250); + } + } + //log.error("已达到重试次数上限,仍未找到目标"); + return false; + } + + // 定义 readFolder 函数 + async function readFolder(folderPath, onlyJson) { + // 新增一个堆栈,初始时包含 folderPath + 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 { + // 如果是文件,根据 onlyJson 判断是否存储 + if (onlyJson) { + if (filePath.endsWith(".json")) { + const fileName = filePath.split('\\').pop(); // 提取文件名 + const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组 + files.push({ + fullPath: filePath, + fileName: fileName, + folderPathArray: folderPathArray + }); + //log.info(`找到 JSON 文件:${filePath}`); + } + } else { const fileName = filePath.split('\\').pop(); // 提取文件名 const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组 files.push({ @@ -1085,211 +626,379 @@ async function readFolder(folderPath, onlyJson) { fileName: fileName, folderPathArray: folderPathArray }); - //log.info(`找到 JSON 文件:${filePath}`); + //log.info(`找到文件:${filePath}`); + } + } + } + // 将临时数组中的子文件夹路径按原顺序压入堆栈 + folderStack.push(...subFolders.reverse()); // 反转子文件夹路径 + } + + return files; + } + + // 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 {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"), 1250, 300, 150, 130); + 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, false); + for (const f of targetPngs) { + if (!f.fullPath.endsWith('.png')) continue; + const mat = file.ReadImageMatSync(f.fullPath); + const ro = RecognitionObject.TemplateMatch(mat, 650, 320, 350, 60); + 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 !== 0) { + log.info(`加入成功,队伍编号 ${playerSign}`); + break; + } + log.error('不处于多人世界,开始尝试加入'); + 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; } + if (await confirmSearchResult()) { await genshin.returnMainUi(); continue; } + + await sleep(500); + if (!await findAndClick(requestEnterRo)) { await genshin.returnMainUi(); continue; } + await waitForMainUI(true, 20 * 1000); + + } else { // 等待他人进入 + if (enterCount >= maxEnterCount) break; + + if (await isYUI()) keyPress("VK_ESCAPE"); + await genshin.returnMainUi(); + keyPress("Y"); await sleep(250); + + if (!await isYUI()) continue; + log.info("处于 Y 界面,开始识别"); + + let attempts = 0; + while (attempts++ < 5) { + if (permissionMode === "无条件通过") { + if (await findAndClick(allowEnterRo)) { + await waitForMainUI(true, 20 * 1000); + enterCount++; + break; } } else { - const fileName = filePath.split('\\').pop(); // 提取文件名 - const folderPathArray = filePath.split('\\').slice(0, -1); // 提取文件夹路径数组 - files.push({ - fullPath: filePath, - fileName: fileName, - folderPathArray: folderPathArray - }); - //log.info(`找到文件:${filePath}`); + const result = await recognizeRequest(); + if (result) { + if (await findAndClick(allowEnterRo)) { + await waitForMainUI(true, 20 * 1000); + enterCount++; + enteredPlayers = [...new Set([...enteredPlayers, result])]; + log.info(`允许 ${result} 加入`); + if (await isYUI()) { keyPress("VK_ESCAPE"); await genshin.returnMainUi(); } + break; + } else { + if (await isYUI()) { keyPress("VK_ESCAPE"); 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) break; + else enterCount--; } } - // 将临时数组中的子文件夹路径按原顺序压入堆栈 - folderStack.push(...subFolders.reverse()); // 反转子文件夹路径 } - return files; -} - -async function runPaths(folderFilePath) { - let Paths = await readFolder(folderFilePath, true); - for (let i = 0; i < Paths.length; i++) { - let skiprecord = false; - const Path = Paths[i]; - let success = true; - if (settings.autoSalvage && autoSalvageCount >= 4) { - autoSalvageCount = 0; - if (settings.decomposeMode === "分解(经验瓶)") { - artifactExperienceDiff += await processArtifacts(1); - } else { - await processArtifacts(1); - } - } else { - autoSalvageCount++; + // ===== 以下工具函数保持不变 ===== + async function waitForMainUI(requirement, timeOut = 60 * 1000) { + const startTime = Date.now(); + while (Date.now() - startTime < timeOut) { + if (await isMainUI() === requirement) return true; + await sleep(1000); } - await fakeLog(Path.fileName, false, true, 0); - const pathInfo = await parsePathing(Path.fullPath); + log.error("超时仍未到达指定状态"); + return false; + } + + async function isMainUI() { + const template = file.ReadImageMatSync("assets/RecognitionObject/MainUI.png"); + const ro = RecognitionObject.TemplateMatch(template, 0, 0, 150, 150); try { - log.info(`当前进度:${Path.fileName}为${folderFilePath}第${i + 1}/${Paths.length}个`); - await pathingScript.runFile(Path.fullPath); - await sleep(1); - } catch (error) { - skiprecord = true; - log.error(`执行路径文件时发生错误:${error.message}`); - if (error.message === "A task was canceled.") { - log.warn("任务取消"); - state.cancel = true; - } - success = false; - break; + const gameRegion = captureGameRegion(); + const result = gameRegion.find(ro); + gameRegion.dispose(); + return result.isExist(); + } catch { return false; } + } + + async function getPlayerSign() { + const picDic = { + "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 gameRegion = captureGameRegion(); + const x = 344, y = 22, w = 45, h = 45; + for (let i = 1; i <= 4; i++) { + const ro = RecognitionObject.TemplateMatch(file.ReadImageMatSync(picDic[i + "P"]), x, y, w, h); + if (gameRegion.Find(ro).isExist()) { gameRegion.dispose(); return i; } } - await fakeLog(Path.fileName, false, false, 0); - if (pathInfo.ok) { - //回到主界面 - await genshin.returnMainUi(); - await sleep(100); - try { - // 获取当前人物在指定地图上的坐标 - const currentPosition = await genshin.getPositionFromMap(pathInfo.map_name); + gameRegion.dispose(); + return 0; + } - // 计算与最后一个非 orientation 点的距离 - const distToLast = Math.hypot( - currentPosition.x - pathInfo.x, - currentPosition.y - pathInfo.y - ); + async function confirmSearchResult() { + for (let i = 0; i < 5; i++) { + const gameRegion = captureGameRegion(); + const res = gameRegion.find(requestEnter2Ro); + gameRegion.dispose(); + if (res.isExist()) return false; + if (i < 4) await sleep(250); + } + return true; + } - // 距离超过 50 认为路线没有正常完成(卡死或未开图等) - if (distToLast >= 50) { - failcount++; - skiprecord = true; - log.warn(`路线${Path.fileName}没有正常完成`); - await sleep(5000); + async function findAndClick(target, maxAttempts = 20) { + for (let i = 0; i < maxAttempts; i++) { + const gameRegion = captureGameRegion(); + const res = gameRegion.find(target); + if (res.isExist()) { res.click(); return true; } + gameRegion.dispose(); + if (i < maxAttempts - 1) await sleep(250); + } + return false; + } + + 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(650, 320, 350, 60)); + 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 findTotalNumber() { + await genshin.returnMainUi(); await keyPress("F2"); await sleep(2000); + const template = file.ReadImageMatSync("assets/RecognitionObject/kickButton.png"); + const positions = [ + { x: 1520, y: 277, w: 230, h: 120 }, + { x: 1520, y: 400, w: 230, h: 120 }, + { x: 1520, y: 527, w: 230, h: 120 } + ]; + const gameRegion = captureGameRegion(); + let count = 1; + for (const { x, y, w, h } of positions) { + if (gameRegion.Find(RecognitionObject.TemplateMatch(template, x, y, w, h)).isExist()) count++; + } + gameRegion.dispose(); + log.info(`当前联机世界玩家总数(含自己):${count}`); + return count; + } + + async function readFolder(folderPath, onlyJson) { + const stack = [folderPath]; + const files = []; + while (stack.length) { + const cur = stack.pop(); + const items = file.ReadPathSync(cur); + const folders = []; + for (const p of items) { + if (file.IsFolder(p)) folders.push(p); + else if (!onlyJson || p.endsWith('.json')) { + const fileName = p.split('\\').pop(); + const folderPathArray = p.split('\\').slice(0, -1); + files.push({ fullPath: p, fileName, folderPathArray }); } - } catch (error) { - log.error(`发生错误:${error.message}`); - skiprecord = true; } + stack.push(...folders.reverse()); } + return files; } } -async function parsePathing(pathFilePath) { +//切换队伍 +async function switchPartyIfNeeded(partyName) { + if (!partyName) { + await genshin.returnMainUi(); + return; + } try { - const raw = await file.readText(pathFilePath); - const json = JSON.parse(raw); - - // 只要 positions 不是数组就直接失败 - if (!Array.isArray(json.positions)) { - log.error("文件positions字段异常"); - return { ok: false }; + log.info("正在尝试切换至" + partyName); + if (!await genshin.switchParty(partyName)) { + log.info("切换队伍失败,前往七天神像重试"); + await genshin.tpToStatueOfTheSeven(); + await genshin.switchParty(partyName); } - - // map_name 不存在时兜底为 "Teyvat" - const map_name = - typeof json.map_name === 'string' && json.map_name.trim() !== '' - ? json.map_name - : 'Teyvat'; - - // 从后往前找第一个 type !== "orientation" 的点 - for (let i = json.positions.length - 1; i >= 0; i--) { - const p = json.positions[i]; - if ( - p.type !== 'orientation' && - typeof p.x === 'number' && - typeof p.y === 'number' - ) { - return { - ok: true, - x: p.x, - y: p.y, - map_name, - }; - } - } - return { ok: false }; - } catch (err) { - log.error(`解析路径文件失败: ${err.message}`); - return { ok: false }; - } -} - - -// 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); + } catch { + log.error("队伍切换失败,可能处于联机模式或其他不可切换状态"); + notification.error(`队伍切换失败,可能处于联机模式或其他不可切换状态`); + await genshin.returnMainUi(); } } \ No newline at end of file diff --git a/repo/js/ArtifactsGroupPurchasing/manifest.json b/repo/js/ArtifactsGroupPurchasing/manifest.json index 550aee1f2..69411d66e 100644 --- a/repo/js/ArtifactsGroupPurchasing/manifest.json +++ b/repo/js/ArtifactsGroupPurchasing/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "AAA狗粮联机团购", - "version": "1.0.7", + "version": "1.1.0", "tags": [ "狗粮" ], diff --git a/repo/js/ArtifactsGroupPurchasing/settings.json b/repo/js/ArtifactsGroupPurchasing/settings.json index ce3dec888..92b5e00dd 100644 --- a/repo/js/ArtifactsGroupPurchasing/settings.json +++ b/repo/js/ArtifactsGroupPurchasing/settings.json @@ -1,8 +1,95 @@ [ + { + "name": "runExtra", + "type": "checkbox", + "label": "运行额外路线,运行完联机收尾后单人运行时勾选\n勾选后处于单人世界时将运行额外路线\n如果选择自动进入模式,则会在任务完成后运行额外路线" + }, + { + "name": "groupMode", + "type": "select", + "label": "####################################################################\n联机运行配置\n\n运行模式", + "options": [ + "手动进入后运行", + "按照下列配置自动进入并运行" + ], + "default": "手动进入后运行" + }, + { + "name": "yourIndex", + "type": "input-text", + "label": "该部分配置请仔细阅读readme后填写\n强烈建议联机运行的所有成员以下配置除你的序号外【完全相同】\n你的序号" + }, + { + "name": "runningOrder", + "type": "input-text", + "label": "运行次序,将依次进入对应序号的成员的世界进行联机狗粮", + "default": "1234" + }, + { + "name": "p1UID", + "type": "input-text", + "label": "1号的uid" + }, + { + "name": "p1Name", + "type": "input-text", + "label": "1号的名称(以游戏内实际显示为准,有备注时填备注名)" + }, + { + "name": "p2UID", + "type": "input-text", + "label": "2号的uid" + }, + { + "name": "p2Name", + "type": "input-text", + "label": "2号的名称(以游戏内实际显示为准,有备注时填备注名)" + }, + { + "name": "p3UID", + "type": "input-text", + "label": "3号的uid" + }, + { + "name": "p3Name", + "type": "input-text", + "label": "3号的名称(以游戏内实际显示为准,有备注时填备注名)" + }, + { + "name": "p4UID", + "type": "input-text", + "label": "4号的uid" + }, + { + "name": "p4Name", + "type": "input-text", + "label": "4号的名称(以游戏内实际显示为准,有备注时填备注名)" + }, + { + "name": "notify", + "type": "checkbox", + "label": "####################################################################\n其他配置\n\n是否通知信息" + }, + { + "name": "forceGroupNumber", + "type": "input-text", + "label": "强制指定自己在队伍中的编号,仅在识别异常时使用,填0不指定", + "default": "0" + }, + { + "name": "partyName", + "type": "input-text", + "label": "运行时使用队伍,将在进入联机模式前切换" + }, + { + "name": "usingCharacter", + "type": "input-text", + "label": "运行时使用角色的序号\n将在进入联机模式前尝试切换到对应序号的角色" + }, { "name": "p1EndingRoute", "type": "select", - "label": "####################################################################\n收尾路线配置\n请确保每个人在该部分的选项【完全一致】\n\n1p收尾路线", + "label": "####################################################################\n收尾路线配置\n请确保每个人在该部分的选项【完全一致】\n非必要不建议更改\n\n1p收尾路线", "options": [ "枫丹高塔", "度假村", @@ -48,46 +135,8 @@ "default": "踏鞴砂" }, { - "name": "runExtra", + "name": "runDebug", "type": "checkbox", - "label": "运行额外路线,运行完联机收尾后单人运行时勾选" - }, - { - "name": "decomposeMode", - "type": "select", - "label": "####################################################################\n狗粮分解配置\n\n狗粮分解模式", - "options": [ - "保留", - "分解(经验瓶)", - "销毁(摩拉)" - ], - "default": "保留" - }, - { - "name": "keep4Star", - "type": "checkbox", - "label": "是否保留四星(默认不保留)" - }, - { - "name": "autoSalvage", - "type": "checkbox", - "label": "是否沿路自动分解(默认:否)" - }, - { - "name": "notify", - "type": "checkbox", - "label": "####################################################################\n其他配置\n\n是否通知信息" - }, - { - "name": "accountName", - "type": "input-text", - "label": "账户名称\n用于多账户运行时区分不同账户\n单账户无需修改", - "default": "默认账户" - }, - { - "name": "forceGroupNumber", - "type": "input-text", - "label": "强制指定自己在队伍中的编号,在识别异常时使用,填0不指定", - "default": "0" + "label": "调试时勾选,跳过路线执行逻辑" } ] \ No newline at end of file diff --git a/repo/js/ArtifactsGroupPurchasing/targets/火山老师.png b/repo/js/ArtifactsGroupPurchasing/targets/火山老师.png new file mode 100644 index 000000000..6c047f973 Binary files /dev/null and b/repo/js/ArtifactsGroupPurchasing/targets/火山老师.png differ