(async function () { // ======================================== // 扫码上号 v3.1 // 流程: 状态判定 → (已登录则先退出) → A0检测tap → A1选号 → A2等登录+点中心 → A3完成 // 状态文件: status.txt (登录中 / 已登录) // ======================================== setGameMetrics(1920, 1080, 1); // ---------- 状态文件 ---------- const STATUS_FILE = "status.txt"; function writeStatus(text) { try { file.writeTextSync(STATUS_FILE, text); } catch (e) { log.warn("写入状态文件失败: " + e); } } // 脚本启动 → 写入"登录中" writeStatus("登录中"); function settingNumber(name, fallback) { const value = typeof settings !== "undefined" && settings ? Number(settings[name]) : NaN; return Number.isFinite(value) ? value : fallback; } function settingBoolean(name, fallback) { if (typeof settings === "undefined" || !settings || settings[name] === undefined) return fallback; return settings[name] !== false && String(settings[name]).toLowerCase() !== "false"; } const useExitDomainOcr = settingBoolean("useOcr", true); let confirmExitX = settingNumber("confirmExitX", 1164); let confirmExitY = settingNumber("confirmExitY", 757); if (confirmExitX === 830 && confirmExitY === 600) { confirmExitX = 1164; confirmExitY = 757; log.info("检测到旧版退出秘境坐标,已自动修正为 (1164, 757)"); } // ---------- 加载图像资源 ---------- const tapMat = file.readImageMatSync("assets/tap.png"); const a0PhoneMat = file.readImageMatSync("assets/a0_phone.png"); const loggedInMat = file.readImageMatSync("assets/btn_logged_in_real.png"); const paimonMat = file.readImageMatSync("assets/paimon_menu.png"); const exitDoorMat = file.readImageMatSync("assets/btn_exit_door.png"); const preLoginNoticeMat = file.readImageMatSync("assets/pre_login_notice.png"); // ---------- 工具函数 ---------- /** * 在截屏中匹配指定图片, 返回识别结果(含 x/y/中心坐标) */ function findImageMatch(mat, x, y, w, h) { const cap = captureGameRegion(); try { const ro = RecognitionObject.TemplateMatch(mat, x || 0, y || 0, w || 1920, h || 1080); const r = cap.find(ro); if (r.isExist()) { return { x: r.x, y: r.y, w: r.width, h: r.height }; } return null; } catch (e) { log.error("图像识别失败: " + e); return null; } finally { cap.dispose(); } } /** * 严格匹配:用于 A0 入口判定,避免地图页面/传送点/圆形 UI 被低阈值误识别。 */ function findImageMatchStrict(mat, x, y, w, h) { const thresholds = [0.88, 0.84]; for (const t of thresholds) { const cap = captureGameRegion(); try { const ro = RecognitionObject.TemplateMatch(mat, x || 0, y || 0, w || 1920, h || 1080); ro.threshold = t; ro.Use3Channels = true; const r = cap.find(ro); if (r.isExist()) { log.info(`严格找到图标 (阈值=${t}, x=${r.x}, y=${r.y})`); return { x: r.x, y: r.y, w: r.width, h: r.height }; } } catch (e) { // 忽略, 继续下一个阈值 } finally { cap.dispose(); } } return null; } /** * 多阈值尝试匹配, 提高识别率 */ function findImageMatchRobust(mat, x, y, w, h) { const thresholds = [0.7, 0.6, 0.5]; for (const t of thresholds) { const cap = captureGameRegion(); try { const ro = RecognitionObject.TemplateMatch(mat, x || 0, y || 0, w || 1920, h || 1080); ro.threshold = t; ro.Use3Channels = true; const r = cap.find(ro); if (r.isExist()) { log.info(`找到图标 (阈值=${t}, x=${r.x}, y=${r.y})`); return { x: r.x, y: r.y, w: r.width, h: r.height }; } } catch (e) { // 忽略, 继续下一个阈值 } finally { cap.dispose(); } } return null; } function clickImage(found) { click(Math.round(found.x + found.w / 2), Math.round(found.y + found.h / 2)); } async function waitForImage(mat, timeout, interval) { interval = interval || 500; const start = Date.now(); while (Date.now() - start < timeout) { const found = findImageMatch(mat); if (found) return found; await sleep(interval); } return null; } function normalizeOcrText(text) { return String(text || "").replace(/\s+/g, ""); } function readOcrResults(x, y, w, h) { const cap = captureGameRegion(); try { const results = cap.findMulti(RecognitionObject.ocr(x, y, w, h)); return Array.from(results).map((result) => ({ text: normalizeOcrText(result.text), x: Number(result.x || 0), y: Number(result.y || 0), w: Number(result.width || result.Width || 0), h: Number(result.height || result.Height || 0) })); } catch (e) { log.warn("退出秘境弹窗 OCR 失败: " + e); return []; } finally { cap.dispose(); } } async function handleExitDomainDialog() { const results = readOcrResults(480, 220, 960, 620); const combinedText = results.map((result) => result.text).join(""); if (!combinedText.includes("退出秘境")) return false; log.info("检测到退出秘境确认弹窗"); const confirmResult = results .filter((result) => result.text.includes("确认") && result.x >= 900) .sort((left, right) => right.x - left.x)[0]; if (useExitDomainOcr && confirmResult) { const x = Math.round(confirmResult.x + confirmResult.w / 2); const y = Math.round(confirmResult.y + confirmResult.h / 2); log.info(`OCR 定位确认按钮,点击 (${x}, ${y})`); click(x, y); } else { const reason = useExitDomainOcr ? "OCR 未定位到确认按钮" : "已关闭 OCR 按钮定位"; log.warn(`${reason},使用固定坐标 (${confirmExitX}, ${confirmExitY})`); click(confirmExitX, confirmExitY); } log.info("已点击退出秘境确认,等待返回主界面..."); await sleep(6000); return true; } // ======================================== // 前置处理:检测登录前的提示图标 // 检测到则点击 (1830, 985),1秒后点击 (1100, 675),再1秒后进入初始判定; // 未检测到则直接进入初始判定,不影响原流程。 // ======================================== log.info("===== 前置检测: pre_login_notice 图标 ====="); const preNoticeFound = findImageMatchRobust(preLoginNoticeMat); if (preNoticeFound) { log.info(`检测到 pre_login_notice 图标 (x=${preNoticeFound.x}, y=${preNoticeFound.y})`); log.info("点击 (1830, 985)..."); click(1830, 985); await sleep(1000); log.info("点击 (1100, 675)..."); click(1100, 675); await sleep(1000); } else { log.info("未检测到 pre_login_notice 图标,跳过前置处理"); } // ======================================== // 初始状态判定:先检测 tap + 手机图标 // 1. 同时存在:已经在开门页,直接进入 A0 选号 // 2. 不同时存在:检测派蒙头像;若无派蒙,则按 ESC 后重检,最多 8 次 // 3. 8 次仍无派蒙:再严格检测一次开门页;开门页也不存在才判定登录失败 // 4. 找到派蒙:说明已在游戏内,先退出到开门页,再进入 A0 // ======================================== log.info("===== 初始状态判定: 严格检测 tap 图标 + 手机图标 ====="); // A0 入口图标不能全屏低阈值搜索,否则地图/传送点等 UI 容易误判。 // 这里限定在开门页中下区域,并使用高阈值严格匹配。 const A0_X = 420; const A0_Y = 120; const A0_W = 1080; const A0_H = 820; let tapFound = findImageMatchStrict(tapMat, A0_X, A0_Y, A0_W, A0_H); let phoneFound = findImageMatchStrict(a0PhoneMat, A0_X, A0_Y, A0_W, A0_H); if (!tapFound || !phoneFound) { log.info("未同时检测到 tap 图标和手机图标 → 开始检测派蒙头像"); let paimonFound = findImageMatchRobust(paimonMat); if (!paimonFound && await handleExitDomainDialog()) { paimonFound = findImageMatchRobust(paimonMat); if (paimonFound) log.info("退出秘境后已检测到派蒙头像"); } for (let i = 0; !paimonFound && i < 8; i++) { log.info(`第 ${i + 1}/8 次未检测到派蒙头像,按 ESC 后重试...`); keyPress("Escape"); await sleep(1000); if (await handleExitDomainDialog()) { log.info("退出秘境弹窗已处理,重新检测主界面"); } paimonFound = findImageMatchRobust(paimonMat); if (paimonFound) log.info("已检测到派蒙头像,继续退出账号流程"); } if (!paimonFound) { log.info("按 ESC 检测 8 次后仍未检测到派蒙头像 → 再严格检测一次开门页"); tapFound = findImageMatchStrict(tapMat, A0_X, A0_Y, A0_W, A0_H); phoneFound = findImageMatchStrict(a0PhoneMat, A0_X, A0_Y, A0_W, A0_H); if (!tapFound || !phoneFound) { log.error("未检测到派蒙头像,最终也未同时检测到 tap 图标和手机图标,判定登录失败"); writeStatus("登录失败"); return; } log.info("最终严格检测到 tap 图标和手机图标 → 当前已在开门页,继续扫码流程"); } if (paimonFound) { // ======================================== // 已登录流程: 退出到开门页 // 找到派蒙→ESC关菜单→点55,1010→找exit_door→点击→等10s // ======================================== log.info("检测到派蒙头像 → 已登录状态, 执行退出流程"); log.info(`找到派蒙头像 (x=${paimonFound.x}, y=${paimonFound.y}), 按 ESC 打开菜单...`); keyPress("Escape"); await sleep(1000); log.info("点击 (55, 1010) 打开派蒙菜单..."); click(55, 1010); await sleep(1500); log.info("查找退出图标 btn_exit_door..."); const doorFound = await waitForImage(exitDoorMat, 10 * 1000, 500); if (doorFound) { log.info("找到退出图标, 点击退出..."); clickImage(doorFound); } else { log.warn("未找到退出图标, 继续后续流程"); } log.info("等待 8 秒, 等待退回到开门页..."); await sleep(8 * 1000); log.info("退出流程完成, 重新严格检测 tap 图标 + 手机图标"); tapFound = findImageMatchStrict(tapMat, A0_X, A0_Y, A0_W, A0_H); phoneFound = findImageMatchStrict(a0PhoneMat, A0_X, A0_Y, A0_W, A0_H); if (!tapFound || !phoneFound) { log.error("退出后仍未同时检测到 tap 图标和手机图标,判定登录失败"); writeStatus("登录失败"); return; } } } log.info("同时检测到 tap 图标和手机图标 → 准备进入扫码流程"); // ======================================== // A1: 进入扫码页 // 点击选号坐标 → 确认进入 // ======================================== log.info("===== A1: 进入扫码页 ====="); log.info("点击 (660, 250) 选择米游社账号..."); click(660, 250); await sleep(750); log.info("点击 (828, 691) 确认进入..."); click(828, 691); log.info("A1 完成, 进入等待登录..."); // ======================================== // A2: 等待登录完成 → 点击画面进入游戏 // 等待 btn_logged_in_real 出现 (80秒超时) // 找到后每秒点击 (960, 800), 每3秒检测派蒙头像 // ======================================== log.info("===== A2: 等待登录完成 ====="); log.info("等待扫码登录... 超时 80 秒"); const loginResult = await waitForImage(loggedInMat, 80 * 1000, 1000); if (!loginResult) { log.error("登录超时! 80 秒内未检测到登录完成图标"); writeStatus("登录失败"); return; } log.info(`检测到登录完成图标! (x=${loginResult.x}, y=${loginResult.y})`); log.info("点击画面进入游戏..."); const CENTER_X = 960; const CENTER_Y = 800; let enteredGame = false; for (let i = 0; i < 60; i++) { click(CENTER_X, CENTER_Y); await sleep(1000); // 每 3 秒检测一次是否进入游戏 (匹配派蒙头像) if (i % 3 === 2) { const found = findImageMatch(paimonMat); if (found) { log.info(`检测到派蒙头像! 已进入游戏 (x=${found.x}, y=${found.y})`); enteredGame = true; break; } } } if (!enteredGame) { log.error("60秒内未检测到最后的派蒙头像, 判定登录失败"); writeStatus("登录失败"); return; } // ======================================== // A3: 登录流程完成, 退出脚本 // ======================================== log.info("===== A3: 登录流程完成! ====="); log.info("扫码上号成功, 脚本退出"); // 写入"已登录"后按一次 ESC,收起菜单/退出可能残留的界面 writeStatus("已登录"); await sleep(500); keyPress("Escape"); })();