From 4084566ea287811cb96d4bb736169068585430b3 Mon Sep 17 00:00:00 2001 From: Bread Grocery Date: Fri, 24 Oct 2025 20:35:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8D=83=E6=98=9F=E5=A5=87=E5=9F=9F?= =?UTF-8?q?=E6=AF=8F=E5=91=A8=E5=88=B7=E5=8F=96=E7=BB=8F=E9=AA=8C=E5=80=BC?= =?UTF-8?q?=20(#2225)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 千星奇域每周刷取经验值 * fix: pr 调整 --- .../MiliastraExperienceAutomation/README.md | 31 ++ .../assets/Enter.png | Bin 0 -> 1692 bytes .../assets/Enter2.png | Bin 0 -> 1171 bytes repo/js/MiliastraExperienceAutomation/main.js | 354 ++++++++++++++++++ .../manifest.json | 18 + .../settings.json | 38 ++ 6 files changed, 441 insertions(+) create mode 100644 repo/js/MiliastraExperienceAutomation/README.md create mode 100644 repo/js/MiliastraExperienceAutomation/assets/Enter.png create mode 100644 repo/js/MiliastraExperienceAutomation/assets/Enter2.png create mode 100644 repo/js/MiliastraExperienceAutomation/main.js create mode 100644 repo/js/MiliastraExperienceAutomation/manifest.json create mode 100644 repo/js/MiliastraExperienceAutomation/settings.json diff --git a/repo/js/MiliastraExperienceAutomation/README.md b/repo/js/MiliastraExperienceAutomation/README.md new file mode 100644 index 000000000..e24488287 --- /dev/null +++ b/repo/js/MiliastraExperienceAutomation/README.md @@ -0,0 +1,31 @@ +# MiliastraExperienceAutomation + +千星奇域每周刷取经验值 + +### 🌟 功能介绍 + +- 自动重复通关指定奇域关卡,高效刷取经验值。 +- 自动追踪每周经验获取进度,达到上限后再次执行将自动跳过,避免重复刷取。 +- 执行完毕后自动返回提瓦特大陆,不干扰其他自动化流程。 + +### 📖 使用方法 + +- 将本脚本添加至您的配置组中。 +- 右键点击脚本自定义配置项(如需要)。 +- 点击运行按钮。 + +### 🛠️ 脚本配置 + +| 配置项 | 描述 | 默认值 | +| -------------- | ------------------------- | ---------- | +| goToTeyvat | 完成后返回提瓦特大陆 | true | +| room | 奇域关卡关键词或关卡 GUID | 7015200164 | +| force | 忽略本周经验值已达上限 | false | +| thisAttempts | 指定通关次数 | | +| expWeeklyLimit | 每周可获取的经验值上限 | 5000 | +| expPerAttempt | 每次通关获取的经验值数量 | 20 | + +### ❗ 注意事项 + +- 请确保游戏窗口分辨率比例为 `16:9`,否则可能影响脚本正常运行。 +- 建议提前设置好快捷键,便于在需要时快速暂停或终止脚本。 diff --git a/repo/js/MiliastraExperienceAutomation/assets/Enter.png b/repo/js/MiliastraExperienceAutomation/assets/Enter.png new file mode 100644 index 0000000000000000000000000000000000000000..e53a33bf2b7bc0107f1bf3e36739bd397adbdc7c GIT binary patch literal 1692 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 literal 0 HcmV?d00001 diff --git a/repo/js/MiliastraExperienceAutomation/main.js b/repo/js/MiliastraExperienceAutomation/main.js new file mode 100644 index 000000000..d4ccba899 --- /dev/null +++ b/repo/js/MiliastraExperienceAutomation/main.js @@ -0,0 +1,354 @@ +// node_modules/.pnpm/@bettergi+utils@0.1.1/node_modules/@bettergi/utils/dist/workflow.js +var defaultMaxAttempts = 5; +var defaultRetryInterval = 1e3; +var waitForAction = async (condition, retryAction, options) => { + const { maxAttempts = defaultMaxAttempts, retryInterval = defaultRetryInterval } = options || {}; + for (let i = 0; i < maxAttempts; i++) { + if (i === 0 && condition()) + return true; + await retryAction?.(); + await sleep(retryInterval); + if (condition()) + return true; + } + return false; +}; +var waitForRegionAppear = async (regionProvider, retryAction, options) => { + return waitForAction(() => { + const region = regionProvider(); + return region != null && region.isExist(); + }, retryAction, options); +}; +var waitForRegionDisappear = async (regionProvider, retryAction, options) => { + return waitForAction(() => { + const region = regionProvider(); + return !region || !region.isExist(); + }, 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 isAppeared = await waitForRegionAppear(regionProvider, retryAction, options); + if (!isAppeared) { + throw new Error(message); + } +}; +var assertRegionDisappearing = async (regionProvider, message, retryAction, options) => { + const isDisappeared = await waitForRegionDisappear(regionProvider, retryAction, options); + if (!isDisappeared) { + throw new Error(message); + } +}; + +// node_modules/.pnpm/@bettergi+utils@0.1.1/node_modules/@bettergi/utils/dist/ocr.js +var findFirst = (ir, ro, predicate) => { + const candidates = ir.findMulti(ro); + for (let i = 0; i < candidates.count; i++) { + if (predicate(candidates[i])) + return candidates[i]; + } + return void 0; +}; +var findImageWithinBounds = (path, x, y, w, h) => { + try { + const ir = captureGameRegion(); + const ro = RecognitionObject.templateMatch(file.readImageMatSync(path), x, y, w, h); + return findFirst(ir, ro, (region) => region.isExist()); + } catch (err) { + err?.message && log.warn(`${err.message}`); + } +}; +var findText = (text, options) => { + const { ignoreCase = true, contains = false } = options || {}; + const searchText = ignoreCase ? text.toLowerCase() : text; + const ir = captureGameRegion(); + const ro = RecognitionObject.ocrThis; + return findFirst(ir, ro, (region) => { + const itemText = ignoreCase ? region.text.toLowerCase() : region.text; + const isMatch = contains ? itemText.includes(searchText) : itemText === searchText; + return isMatch && region.isExist(); + }); +}; +var findTextWithinBounds = (text, x, y, w, h, options) => { + const { ignoreCase = true, contains = false } = options || {}; + const searchText = ignoreCase ? text.toLowerCase() : text; + const ir = captureGameRegion(); + const ro = RecognitionObject.ocr(x, y, w, h); + return findFirst(ir, ro, (region) => { + const itemText = ignoreCase ? region.text.toLowerCase() : region.text; + const isMatch = contains ? itemText.includes(searchText) : itemText === searchText; + return isMatch && region.isExist(); + }); +}; + +// node_modules/.pnpm/@bettergi+utils@0.1.1/node_modules/@bettergi/utils/dist/store.js +var useStore = (name) => { + const filePath = `store/${name}.json`; + const obj = (() => { + try { + const text = file.readTextSync(filePath); + return JSON.parse(text); + } catch { + return {}; + } + })(); + const createProxy = (target, parentPath = []) => { + if (typeof target !== "object" || target === null) { + return target; + } + return new Proxy(target, { + get: (target2, key) => { + const value = Reflect.get(target2, key); + return typeof value === "object" && value !== null ? createProxy(value, [...parentPath, key]) : value; + }, + set: (target2, key, value) => { + const success = Reflect.set(target2, key, value); + if (success) { + Promise.resolve().then(() => { + file.writeTextSync(filePath, JSON.stringify(obj, null, 2)); + }); + } + return success; + }, + deleteProperty: (target2, key) => { + const success = Reflect.deleteProperty(target2, key); + if (success) { + Promise.resolve().then(() => { + file.writeTextSync(filePath, JSON.stringify(obj, null, 2)); + }); + } + return success; + } + }); + }; + return createProxy(obj); +}; + +// 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 now = /* @__PURE__ */ new Date(); + const result = new Date(now); + result.setHours(4, 0, 0, 0); + const currentDay = now.getDay(); + let daysUntilMonday; + if (currentDay === 1 && now.getHours() < 4) { + daysUntilMonday = 0; + } else { + daysUntilMonday = (8 - currentDay) % 7; + } + result.setDate(now.getDate() + daysUntilMonday); + return result; +}; + +// 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 ok = await waitForAction( + isInLobby, + () => { + findBottomButton("大厅", true)?.click(); + }, + { maxAttempts: 60 } + ); + if (!ok) throw new Error("返回大厅超时"); +}; +var goBackToTeyvat = async () => { + log.info("打开当前大厅..."); + await assertRegionAppearing( + () => findHeaderTitle("大厅", true), + "打开当前大厅超时", + () => { + keyPress("F2"); + }, + { maxAttempts: 10 } + ); + await assertRegionAppearing( + findMessageEnter, + "返回提瓦特大陆超时", + () => { + log.info("返回提瓦特大陆..."); + findGotTeyvatButton()?.click(); + findText("确认")?.click(); + }, + { maxAttempts: 120 } + ); +}; + +// src/room.ts +var createRoom = async (room) => { + log.info("打开人气奇域界面..."); + await assertRegionAppearing( + () => findHeaderTitle("人气", true), + "打开人气奇域界面超时", + () => { + keyPress("F6"); + } + ); + log.info("打开全部奇域界面..."); + await assertRegionAppearing( + () => findHeaderTitle("全部", true), + "打开全部奇域界面超时", + () => { + findTextWithinBounds("全部", 1320, 0, 600, 95, { + contains: true + })?.click(); + } + ); + log.info("粘贴奇域关卡文本: {room}", room); + await assertRegionAppearing( + () => findTextWithinBounds("清除", 0, 120, 1920, 60), + "粘贴关卡文本超时", + () => { + const ph = findTextWithinBounds("搜索", 0, 120, 1920, 60, { + contains: true + }); + if (ph) { + ph.click(); + inputText(room); + } + } + ); + log.info("搜索奇域关卡: {guid}", room); + const findSearchButton = () => findTextWithinBounds("搜索", 0, 120, 1920, 60); + const findTooFrequentText = () => findTextWithinBounds("过于频繁", 0, 0, 1920, 300, { contains: true }); + await assertRegionAppearing( + findTooFrequentText, + "搜索关卡超时", + () => { + findSearchButton()?.click(); + }, + { maxAttempts: 50, retryInterval: 200 } + ); + log.info("打开奇域介绍..."); + const findCreateRoomButton = () => findTextWithinBounds("房间", 960, 140, 960, 70, { contains: true }); + await assertRegionAppearing( + findCreateRoomButton, + "打开奇域介绍超时", + () => { + const lobbyButton = findTextWithinBounds("大厅", 880, 840, 1040, 110, { + contains: true + }); + if (lobbyButton) { + log.info("当前不在大厅,前往大厅..."); + lobbyButton.click(); + } else { + log.info("选择第一个奇域关卡..."); + click(355, 365); + } + }, + { maxAttempts: 30 } + ); + log.info("创建并进入房间..."); + await assertRegionAppearing( + () => findHeaderTitle("房间", true), + "创建并进入房间超时", + () => { + findCreateRoomButton()?.click(); + }, + { maxAttempts: 10 } + ); +}; +var enterRoom = async (room) => { + const inLobby = isInLobby(); + if (inLobby) { + const enterButton = findTextWithinBounds("房间", 1580, 110, 320, 390, { + contains: true + }); + if (enterButton) { + log.info("当前已存在房间,进入房间...", room); + await assertRegionAppearing( + () => findHeaderTitle("房间", true), + "进入房间超时", + () => { + keyPress("P"); + } + ); + return; + } + } + log.info("当前不在房间内,创建房间...", room); + await createRoom(room); +}; +var startGame = async () => { + await assertRegionAppearing( + () => findBottomButton("大厅", true), + "等待游戏结束超时", + async () => { + findBottomButton("开始游戏")?.click(); + findBottomButton("准备", true)?.click(); + const prepare = () => findText("加入准备", { contains: true }); + if (prepare()) { + log.info("加入准备区..."); + await assertRegionDisappearing(prepare, "等待加入准备区提示消失超时"); + click(770, 275); + } else { + log.info("等待本次关卡结束..."); + } + }, + { maxAttempts: 120 } + ); + log.info("返回大厅..."); + await goToLobby(); +}; + +// main.ts +(async function() { + setGameMetrics(1920, 1080, 1.5); + await genshin.returnMainUi(); + const goToTeyvat = settings.goToTeyvat ?? true; + const room = settings.room || "7015200164"; + const force = settings.force ?? false; + const thisAttempts = Math.max(0, Number(settings.thisAttempts || "0")); + const expWeeklyLimit = Math.max(1, Number(settings.expWeeklyLimit || "5000")); + const expPerAttempt = Math.max(1, Number(settings.expPerAttempt || "20")); + const store = useStore("data"); + store.weekly = store.weekly || { expGained: 0, attempts: 0 }; + store.nextWeek = store.nextWeek || getNextMonday4AM().getTime(); + if (Date.now() >= store.nextWeek) { + log.info("新的一周,重置本周经验值数据"); + store.weekly = { expGained: 0, attempts: 0 }; + store.nextWeek = getNextMonday4AM().getTime(); + } + if (store.weekly.expGained >= expWeeklyLimit) { + if (force) { + log.warn("本周获取经验值已达上限,强制执行"); + } else { + log.warn("本周获取经验值已达上限,跳过执行"); + return; + } + } + 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 + ); + await enterRoom(room); + await startGame(); + store.weekly.attempts += 1; + store.weekly.expGained += expPerAttempt; + if (store.weekly.expGained >= expWeeklyLimit && !force) { + log.warn("本周获取经验值已达上限,停止执行"); + break; + } + } + } catch (e) { + log.error("脚本执行出错: {error}", { error: e.message || e }); + await genshin.returnMainUi(); + } + if (goToTeyvat) { + await goBackToTeyvat(); + } +})(); diff --git a/repo/js/MiliastraExperienceAutomation/manifest.json b/repo/js/MiliastraExperienceAutomation/manifest.json new file mode 100644 index 000000000..eab60d90c --- /dev/null +++ b/repo/js/MiliastraExperienceAutomation/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 1, + "name": "千星奇域每周刷取经验值", + "version": "0.1.2", + "bgi_version": "0.48.0", + "description": "千星奇域每周刷取经验值", + "authors": [ + { + "name": "breadgrocery", + "link": "https://github.com/breadgrocery" + } + ], + "main": "main.js", + "settings_ui": "settings.json", + "saved_files": [ + "store/*.json" + ] +} diff --git a/repo/js/MiliastraExperienceAutomation/settings.json b/repo/js/MiliastraExperienceAutomation/settings.json new file mode 100644 index 000000000..96b00ca70 --- /dev/null +++ b/repo/js/MiliastraExperienceAutomation/settings.json @@ -0,0 +1,38 @@ +[ + { + "type": "checkbox", + "name": "goToTeyvat", + "label": "完成后返回提瓦特大陆", + "default": true + }, + { + "type": "input-text", + "name": "room", + "label": "奇域关卡关键词或关卡GUID", + "default": "7015200164" + }, + { + "type": "checkbox", + "name": "force", + "label": "忽略本周经验值已达上限", + "default": false + }, + { + "type": "input-text", + "name": "thisAttempts", + "label": "指定通关次数", + "default": "" + }, + { + "type": "input-text", + "name": "expWeeklyLimit", + "label": "每周可获取的经验值上限", + "default": "5000" + }, + { + "type": "input-text", + "name": "expPerAttempt", + "label": "每次通关获取的经验值数量", + "default": "20" + } +]