From c8fefb66d900a9869fe7508837a91a71e0a7ed53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BA=81=E5=8A=A8=E7=9A=84=E6=B0=A8=E6=B0=94?= <131591012+zaodonganqi@users.noreply.github.com> Date: Mon, 27 Oct 2025 00:02:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8D=83=E6=98=9F=E5=A5=87?= =?UTF-8?q?=E5=9F=9F=20(#2253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MiliastraExperienceAutomation/README.md | 10 +- .../assets/exit.png | Bin 0 -> 2863 bytes repo/js/MiliastraExperienceAutomation/main.js | 208 +++++++++++++----- .../manifest.json | 2 +- .../settings.json | 4 +- 5 files changed, 166 insertions(+), 58 deletions(-) create mode 100644 repo/js/MiliastraExperienceAutomation/assets/exit.png diff --git a/repo/js/MiliastraExperienceAutomation/README.md b/repo/js/MiliastraExperienceAutomation/README.md index e24488287..c0365d55d 100644 --- a/repo/js/MiliastraExperienceAutomation/README.md +++ b/repo/js/MiliastraExperienceAutomation/README.md @@ -11,17 +11,17 @@ ### 📖 使用方法 - 将本脚本添加至您的配置组中。 -- 右键点击脚本自定义配置项(如需要)。 +- 右键点击脚本自定义配置项(当同时配置了多个关卡id时,假设设置了20次循环,会以20次A,然后20次B这样的形式依次循环)。 - 点击运行按钮。 ### 🛠️ 脚本配置 -| 配置项 | 描述 | 默认值 | -| -------------- | ------------------------- | ---------- | +| 配置项 | 描述 | 默认值 | +| -------------- | ------------------------- |------------| | goToTeyvat | 完成后返回提瓦特大陆 | true | -| room | 奇域关卡关键词或关卡 GUID | 7015200164 | +| room | 奇域关卡关键词或关卡 GUID(支持多个id,用中文或英文逗号隔开) | 7070702264 | | force | 忽略本周经验值已达上限 | false | -| thisAttempts | 指定通关次数 | | +| thisAttempts | 指定通关次数 | 0 | | expWeeklyLimit | 每周可获取的经验值上限 | 5000 | | expPerAttempt | 每次通关获取的经验值数量 | 20 | diff --git a/repo/js/MiliastraExperienceAutomation/assets/exit.png b/repo/js/MiliastraExperienceAutomation/assets/exit.png new file mode 100644 index 0000000000000000000000000000000000000000..af8da74512575947d13297eecd677de5cbd40424 GIT binary patch literal 2863 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 N002ovPDHLkV1g { +const defaultMaxAttempts = 5; +const defaultRetryInterval = 1e3; +const waitForAction = async (condition, retryAction, options) => { const { maxAttempts = defaultMaxAttempts, retryInterval = defaultRetryInterval } = options || {}; for (let i = 0; i < maxAttempts; i++) { if (i === 0 && condition()) @@ -13,13 +13,13 @@ var waitForAction = async (condition, retryAction, options) => { } return false; }; -var waitForRegionAppear = async (regionProvider, retryAction, options) => { +const waitForRegionAppear = async (regionProvider, retryAction, options) => { return waitForAction(() => { const region = regionProvider(); return region != null && region.isExist(); }, retryAction, options); }; -var waitForRegionDisappear = async (regionProvider, retryAction, options) => { +const waitForRegionDisappear = async (regionProvider, retryAction, options) => { return waitForAction(() => { const region = regionProvider(); return !region || !region.isExist(); @@ -27,13 +27,13 @@ var waitForRegionDisappear = async (regionProvider, retryAction, options) => { }; // node_modules/.pnpm/@bettergi+utils@0.1.1/node_modules/@bettergi/utils/dist/asserts.js -var assertRegionAppearing = async (regionProvider, message, retryAction, options) => { +const assertRegionAppearing = async (regionProvider, message, retryAction, options) => { const isAppeared = await waitForRegionAppear(regionProvider, retryAction, options); if (!isAppeared) { throw new Error(message); } }; -var assertRegionDisappearing = async (regionProvider, message, retryAction, options) => { +const assertRegionDisappearing = async (regionProvider, message, retryAction, options) => { const isDisappeared = await waitForRegionDisappear(regionProvider, retryAction, options); if (!isDisappeared) { throw new Error(message); @@ -41,7 +41,7 @@ var assertRegionDisappearing = async (regionProvider, message, retryAction, opti }; // node_modules/.pnpm/@bettergi+utils@0.1.1/node_modules/@bettergi/utils/dist/ocr.js -var findFirst = (ir, ro, predicate) => { +const findFirst = (ir, ro, predicate) => { const candidates = ir.findMulti(ro); for (let i = 0; i < candidates.count; i++) { if (predicate(candidates[i])) @@ -49,7 +49,7 @@ var findFirst = (ir, ro, predicate) => { } return void 0; }; -var findImageWithinBounds = (path, x, y, w, h) => { +const findImageWithinBounds = (path, x, y, w, h) => { try { const ir = captureGameRegion(); const ro = RecognitionObject.templateMatch(file.readImageMatSync(path), x, y, w, h); @@ -58,7 +58,7 @@ var findImageWithinBounds = (path, x, y, w, h) => { err?.message && log.warn(`${err.message}`); } }; -var findText = (text, options) => { +const findText = (text, options) => { const { ignoreCase = true, contains = false } = options || {}; const searchText = ignoreCase ? text.toLowerCase() : text; const ir = captureGameRegion(); @@ -69,7 +69,7 @@ var findText = (text, options) => { return isMatch && region.isExist(); }); }; -var findTextWithinBounds = (text, x, y, w, h, options) => { +const findTextWithinBounds = (text, x, y, w, h, options) => { const { ignoreCase = true, contains = false } = options || {}; const searchText = ignoreCase ? text.toLowerCase() : text; const ir = captureGameRegion(); @@ -82,7 +82,7 @@ var findTextWithinBounds = (text, x, y, w, h, options) => { }; // node_modules/.pnpm/@bettergi+utils@0.1.1/node_modules/@bettergi/utils/dist/store.js -var useStore = (name) => { +const useStore = (name) => { const filePath = `store/${name}.json`; const obj = (() => { try { @@ -125,9 +125,9 @@ var useStore = (name) => { }; // src/misc.ts -var findHeaderTitle = (title, contains) => findTextWithinBounds(title, 0, 0, 300, 95, { contains }); -var findBottomButton = (text, contains) => findTextWithinBounds(text, 960, 980, 960, 100, { contains }); -var getNextMonday4AM = () => { +const findHeaderTitle = (title, contains) => findTextWithinBounds(title, 0, 0, 300, 95, { contains }); +const findBottomButton = (text, contains) => findTextWithinBounds(text, 960, 980, 960, 100, { contains }); +const getNextMonday4AM = () => { const now = /* @__PURE__ */ new Date(); const result = new Date(now); result.setHours(4, 0, 0, 0); @@ -143,11 +143,12 @@ var getNextMonday4AM = () => { }; // src/lobby.ts -var findMessageEnter = () => findImageWithinBounds("assets/Enter.png", 0, 1020, 960, 60); -var findMessageEnter2 = () => findImageWithinBounds("assets/Enter2.png", 0, 1020, 960, 60); -var findGotTeyvatButton = () => findTextWithinBounds("返回", 1500, 0, 300, 95, { contains: true }); -var isInLobby = () => findMessageEnter() !== void 0 || findMessageEnter2() !== void 0; -var goToLobby = async () => { +const findMessageEnter = () => findImageWithinBounds("assets/Enter.png", 0, 1020, 960, 60); +const findMessageEnter2 = () => findImageWithinBounds("assets/Enter2.png", 0, 1020, 960, 60); +const findExitButton = () => findImageWithinBounds("assets/Exit.png", 960, 0, 960, 540); +const findGotTeyvatButton = () => findTextWithinBounds("返回", 1500, 0, 300, 95, { contains: true }); +const isInLobby = () => findMessageEnter() !== void 0 || findMessageEnter2() !== void 0; +const goToLobby = async () => { const ok = await waitForAction( isInLobby, () => { @@ -157,7 +158,7 @@ var goToLobby = async () => { ); if (!ok) throw new Error("返回大厅超时"); }; -var goBackToTeyvat = async () => { +const goBackToTeyvat = async () => { log.info("打开当前大厅..."); await assertRegionAppearing( () => findHeaderTitle("大厅", true), @@ -180,7 +181,7 @@ var goBackToTeyvat = async () => { }; // src/room.ts -var createRoom = async (room) => { +const createRoom = async (room) => { log.info("打开人气奇域界面..."); await assertRegionAppearing( () => findHeaderTitle("人气", true), @@ -200,6 +201,7 @@ var createRoom = async (room) => { } ); log.info("粘贴奇域关卡文本: {room}", room); + await sleep(1000); await assertRegionAppearing( () => findTextWithinBounds("清除", 0, 120, 1920, 60), "粘贴关卡文本超时", @@ -253,7 +255,7 @@ var createRoom = async (room) => { { maxAttempts: 10 } ); }; -var enterRoom = async (room) => { +const enterRoom = async (room) => { const inLobby = isInLobby(); if (inLobby) { const enterButton = findTextWithinBounds("房间", 1580, 110, 320, 390, { @@ -274,7 +276,7 @@ var enterRoom = async (room) => { log.info("当前不在房间内,创建房间...", room); await createRoom(room); }; -var startGame = async () => { +const startGame = async () => { let outputCount = 0; await assertRegionAppearing( () => findBottomButton("大厅", true), @@ -304,8 +306,22 @@ var startGame = async () => { (async function() { setGameMetrics(1920, 1080, 1.5); await genshin.returnMainUi(); + + // 检查当前是否在房间内,如果是则先退回到大厅 + const inLobby = isInLobby(); + if (!inLobby) { + log.info("检测到当前不在大厅,正在返回大厅..."); + try { + await goToLobby(); + } catch (e) { + log.warn("返回大厅失败,继续执行: " + (e.message || e)); + } + } + const goToTeyvat = settings.goToTeyvat ?? true; - const room = settings.room || "7070702264"; + const roomStr = settings.room || "7070702264"; + // 支持中英文逗号分割多个房间号 + const rooms = roomStr.split(/[,,]/).map(r => r.trim()).filter(r => r); const force = settings.force ?? false; const thisAttempts = Math.max(0, Number(settings.thisAttempts || "0")); const expWeeklyLimit = Math.max(1, Number(settings.expWeeklyLimit || "5000")); @@ -318,38 +334,130 @@ var startGame = async () => { store.weekly = { expGained: 0, attempts: 0 }; store.nextWeek = getNextMonday4AM().getTime(); } - if (store.weekly.expGained >= expWeeklyLimit) { - if (force) { - log.warn("本周获取经验值已达上限,强制执行"); - } else { - log.warn("本周获取经验值已达上限,跳过执行"); - return; + + // 如果只有一个房间号,检查经验上限 + if (rooms.length === 1) { + if (store.weekly.expGained >= expWeeklyLimit) { + if (force) { + log.warn("本周获取经验值已达上限,强制执行"); + } else { + log.warn("本周获取经验值已达上限,跳过执行"); + return; + } } + } else { + log.info("检测到多个房间号,将忽略经验上限直接执行"); } + + // 如果指定了通关次数,不显示经验相关日志 + const isSpecifiedAttempts = thisAttempts > 0; + try { - const expRemain = expWeeklyLimit - store.weekly.expGained; - let attempts = Math.ceil( - (expRemain > 0 ? expRemain : expWeeklyLimit) / expPerAttempt - ); - if (thisAttempts > 0) attempts = thisAttempts; - for (let i = 0; i < attempts; i++) { - log.info( - "[{c}/{t}]: 开始本周第 {num} 次奇域挑战...", - i + 1, - attempts, - store.weekly.attempts + 1 + // 对每个房间号循环执行 + for (let roomIndex = 0; roomIndex < rooms.length; roomIndex++) { + const room = rooms[roomIndex]; + log.info("开始处理房间 " + room + " (" + (roomIndex + 1) + "/" + rooms.length + ")"); + + const expRemain = expWeeklyLimit - store.weekly.expGained; + let attempts = Math.ceil( + (expRemain > 0 ? expRemain : expWeeklyLimit) / expPerAttempt ); - await enterRoom(room); - await startGame(); - store.weekly.attempts += 1; - store.weekly.expGained += expPerAttempt; - if (store.weekly.expGained >= expWeeklyLimit && !force) { - log.warn("本周获取经验值已达上限,停止执行"); - break; + if (thisAttempts > 0) attempts = thisAttempts; + + // 对该房间执行指定次数 + for (let i = 0; i < attempts; i++) { + // 多房间模式时忽略经验上限检查 + if (rooms.length === 1 && !isSpecifiedAttempts) { + // 单房间模式且未指定次数:检查是否达到经验上限(仅第一次跳过,其他由内部判断) + if (i === 0 && store.weekly.expGained >= expWeeklyLimit && !force) { + log.warn("本周获取经验值已达上限,跳过该房间"); + break; + } + } + + // 首次执行时,先退出房间 + if (i === 0) { + const inLobby = isInLobby(); + if (inLobby) { + const enterButton = findTextWithinBounds("房间", 1580, 110, 320, 390, { + contains: true + }); + if (enterButton) { + log.info("首次执行,先退出已有房间..."); + await sleep(2000); + // 进入房间 + keyPress("P"); + await sleep(3000); + // 等待房间界面出现 + await assertRegionAppearing( + () => findHeaderTitle("房间", true), + "等待进入房间超时" + ); + // 点击退出按钮 + const exitBtn = findExitButton(); + if (exitBtn) { + log.info("找到退出按钮,点击退出..."); + exitBtn.click(); + await sleep(2000); + // 等待弹窗出现并点击"确认" + const confirmBtn = findText("确认"); + if (confirmBtn && confirmBtn.isExist && confirmBtn.isExist()) { + confirmBtn.click(); + await sleep(1000); + } else if (confirmBtn) { + confirmBtn.click(); + await sleep(1000); + } + } else { + log.warn("未找到退出按钮"); + } + // 确认已返回大厅 + const backToLobby = await waitForAction( + isInLobby, + null, + {maxAttempts: 30, retryInterval: 500} + ); + if (backToLobby) { + log.info("已成功退出已有房间"); + } else { + log.warn("退出房间超时,但继续执行"); + } + } + } + } + + if (isSpecifiedAttempts) { + log.info( + "房间 {room}: [{c}/{t}] 开始第 {num} 次奇域挑战...", + room, + i + 1, + attempts, + i + 1 + ); + } else { + log.info( + "房间 {room}: [{c}/{t}] 开始本周第 {num} 次奇域挑战...", + room, + i + 1, + attempts, + store.weekly.attempts + 1 + ); + } + + await enterRoom(room); + await startGame(); + store.weekly.attempts += 1; + store.weekly.expGained += expPerAttempt; + + // 单房间模式且未指定次数时检查经验上限 + if (rooms.length === 1 && !isSpecifiedAttempts && store.weekly.expGained >= expWeeklyLimit && !force) { + log.warn("本周获取经验值已达上限,停止执行"); + break; + } } } } catch (e) { - log.error("脚本执行出错: {error}", { error: e.message || e }); + log.error("脚本执行出错: " + (e.message || e)); await genshin.returnMainUi(); } if (goToTeyvat) { diff --git a/repo/js/MiliastraExperienceAutomation/manifest.json b/repo/js/MiliastraExperienceAutomation/manifest.json index 7c4c63642..1113895a1 100644 --- a/repo/js/MiliastraExperienceAutomation/manifest.json +++ b/repo/js/MiliastraExperienceAutomation/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "千星奇域每周刷取经验值", - "version": "0.1.3", + "version": "0.2", "bgi_version": "0.52.0", "description": "千星奇域每周刷取经验值", "authors": [ diff --git a/repo/js/MiliastraExperienceAutomation/settings.json b/repo/js/MiliastraExperienceAutomation/settings.json index cc3f32649..19895862c 100644 --- a/repo/js/MiliastraExperienceAutomation/settings.json +++ b/repo/js/MiliastraExperienceAutomation/settings.json @@ -8,7 +8,7 @@ { "type": "input-text", "name": "room", - "label": "奇域关卡关键词或关卡GUID", + "label": "奇域关卡关键词或关卡GUID\n(支持多个id,用中文或英文逗号隔开)", "default": "7070702264" }, { @@ -21,7 +21,7 @@ "type": "input-text", "name": "thisAttempts", "label": "指定通关次数", - "default": "" + "default": "0" }, { "type": "input-text",