From 5f12c8ec3621e5a09033a106ab4d673305bd1662 Mon Sep 17 00:00:00 2001 From: ddaodan <40017293+ddaodan@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:57:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=84=9A=E6=9C=AC=E5=8A=9F=E8=83=BD=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=EF=BC=9B=E4=BF=AE=E6=94=B9=20main.js=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=8A=A8=E8=AF=86=E5=88=AB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E8=B7=B3=E8=BF=87=E7=AD=89=E5=BE=85=E6=8C=89?= =?UTF-8?q?=E9=94=AE=E8=AE=BE=E7=BD=AE=EF=BC=9B=E6=9B=B4=E6=96=B0=20manife?= =?UTF-8?q?st.json=20=E7=89=88=E6=9C=AC=E5=8F=B7=E8=87=B3=202.0=EF=BC=9B?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20settings.json=EF=BC=8C=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B9=E3=80=82=20(#2756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- repo/js/猜角色辅助/README.md | 3 +- repo/js/猜角色辅助/main.js | 234 +++++++++++++++++++++++++++---- repo/js/猜角色辅助/manifest.json | 5 +- repo/js/猜角色辅助/settings.json | 26 +++- 4 files changed, 231 insertions(+), 37 deletions(-) diff --git a/repo/js/猜角色辅助/README.md b/repo/js/猜角色辅助/README.md index 355afda2b..e591edabd 100644 --- a/repo/js/猜角色辅助/README.md +++ b/repo/js/猜角色辅助/README.md @@ -1,5 +1,6 @@ # 千星奇域-猜角色辅助 ## 使用说明 -- 脚本仅有框架,不提供台词数据,请自行解决 +- 脚本仅有框架,不提供数据,请自行解决 +- 支持元素战技,元素爆发,宝箱,倒下,入队台词,命座,天赋 - 需要在奇域内启动 - 退出奇域后记得关闭 \ No newline at end of file diff --git a/repo/js/猜角色辅助/main.js b/repo/js/猜角色辅助/main.js index cb594aa32..e111116ae 100644 --- a/repo/js/猜角色辅助/main.js +++ b/repo/js/猜角色辅助/main.js @@ -1,11 +1,42 @@ -const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 }; +const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 }; (async function () { const data = loadData(); + const kmHook = new KeyMouseHook(); + const skipKey = (typeof settings !== "undefined" && settings && settings.skipKey) ? settings.skipKey : "R"; + const autoRecognize = (typeof settings !== "undefined" && settings && typeof settings.autoRecognize !== "undefined") ? settings.autoRecognize : true; + let skipWait = false; + kmHook.OnKeyDown((key) => { + if (key === skipKey) { + skipWait = true; + } + }); + const sleepOrSkip = async (ms) => { + const step = 100; + const endTime = Date.now() + ms; + while (Date.now() < endTime) { + if (skipWait) { + skipWait = false; + return true; + } + await sleep(Math.min(step, endTime - Date.now())); + } + return false; + }; + log.info("按 {0} 可跳过等待并立刻识别", skipKey); + log.info("自动识别: {0}", autoRecognize ? "开启" : "关闭"); + try { let emptyCount = 0; let lastTextKey = ""; let lastResultKey = ""; while (true) { + // 自动识别关闭时:仅在按下跳过按键后才进行一次识别 + if (!autoRecognize) { + let triggered = await sleepOrSkip(24 * 60 * 60 * 1000); + if (!triggered) { + continue; + } + } let texts = ocr(ocrRegion1.x, ocrRegion1.y, ocrRegion1.width, ocrRegion1.height); if (!texts || texts.length === 0) { emptyCount++; @@ -14,7 +45,7 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 }; log.warn("连续未识别达到上限,退出循环"); break; } - await sleep(5000); + await sleepOrSkip(1000); continue; } @@ -22,7 +53,7 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 }; // 仅在识别结果发生变化时输出,避免刷屏 let textKey = texts.join(" | ").trim(); if (textKey === lastTextKey) { - await sleep(5000); + await sleepOrSkip(5000); continue; } if (lastResultKey) { @@ -30,13 +61,13 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 }; } lastTextKey = textKey; lastResultKey = textKey; - log.info(`识别到文本: ${textKey}`); + log.debug(`识别到文本: ${textKey}`); // 解析 OCR 文本,提取冒号后的台词内容 let parsedList = parseOcrTexts(texts); if (parsedList.length === 0) { log.info("未获取到可用台词内容,继续识别"); - await sleep(5000); + await sleepOrSkip(1000); continue; } @@ -51,7 +82,10 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 }; logMatches(matches); } - await sleep(5000); + await sleepOrSkip(5000); + } + } finally { + kmHook.Dispose(); } })(); @@ -126,26 +160,57 @@ function parseOcrTexts(texts) { let content = cleaned.slice(colonIndex + 1).trim(); if (!content) { continue; - } + } let category = detectCategory(prefix); // 没有“前两字/首字”标记时,认为是完整台词 let isFull = prefix.indexOf("(前两字)") < 0 && prefix.indexOf("(前两字)") < 0 && prefix.indexOf("(首字)") < 0 && prefix.indexOf("(首字)") < 0; - parsed.push({ raw, content, isFull, prefix }); + parsed.push({ raw, content, isFull, prefix, category }); } return parsed; } +function detectCategory(prefix) { + let p = String(prefix || "").replace(/\s+/g, ""); + if (p.includes("元素战技")) { + return "elementSkill"; + } + if (p.includes("元素爆发")) { + return "elementBurst"; + } + if (p.includes("入队语音") || p.includes("加入队伍")) { + return "joinVoice"; + } + if (p.includes("倒下语音") || p === "倒下") { + return "fallVoice"; + } + if (p.includes("宝箱语音") || p.includes("打开宝箱") || p.includes("宝箱")) { + return "chestVoice"; + } + if (p.includes("命之座")) { + return "constellation"; + } + if (p.includes("天赋")) { + return "talent"; + } + return "unknown"; +} + function normalizeText(text) { return String(text || "") .replace(/\s+/g, "") .replace(/[\"“”]/g, "") + .replace(/[\p{P}\p{S}]/gu, "") .trim(); } function isDialogueKey(key) { - return key.indexOf("台词") >= 0 || key.indexOf("鍙拌瘝") >= 0; + return key.indexOf("台词") >= 0 + || key.indexOf("语音") >= 0 + || key.indexOf("命之座") >= 0 + || key.indexOf("天赋") >= 0 + || key.indexOf("鍙拌瘝") >= 0; } function getName(item) { @@ -178,7 +243,13 @@ function isMatch(content, value, isFull) { } // 完整台词严格匹配,前两字/首字走前缀/包含匹配 if (isFull) { - return v === c; + if (v === c) { + return true; + } + if (shouldUseLooseMatch(v)) { + return isLooseMatch(c, v, 2); + } + return false; } if (c.length <= 2) { return v.indexOf(c) === 0; @@ -186,30 +257,133 @@ function isMatch(content, value, isFull) { return v.indexOf(c) >= 0 || c.indexOf(v) >= 0; } +function shouldUseLooseMatch(text) { + const specialChars = ["貘"]; + for (let i = 0; i < specialChars.length; i++) { + if (text.indexOf(specialChars[i]) >= 0) { + return true; + } + } + return false; +} + +function isLooseMatch(shortText, fullText, maxMissing) { + let a = shortText; + let b = fullText; + if (a.length > b.length) { + let tmp = a; + a = b; + b = tmp; + } + if (b.length - a.length > maxMissing) { + return false; + } + return lcsLength(a, b) >= b.length - maxMissing; +} + +function lcsLength(a, b) { + let m = a.length; + let n = b.length; + let prev = new Array(n + 1).fill(0); + let curr = new Array(n + 1).fill(0); + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (a[i - 1] === b[j - 1]) { + curr[j] = prev[j - 1] + 1; + } else { + curr[j] = curr[j - 1] > prev[j] ? curr[j - 1] : prev[j]; + } + } + let temp = prev; + prev = curr; + curr = temp; + for (let k = 0; k <= n; k++) { + curr[k] = 0; + } + } + return prev[n]; +} function findCharactersByLine(parsed, data) { let matches = []; if (!parsed || !parsed.content || !data) { return matches; } - for (let i = 0; i < data.length; i++) { - let item = data[i]; - for (let key in item) { - if (!isDialogueKey(key)) { - continue; + let allowKey = (key) => { + if (!key || !isDialogueKey(key)) { + return false; + } + switch (parsed.category) { + case "elementSkill": + return key.indexOf("元素战技台词") >= 0; + case "elementBurst": + return key.indexOf("元素爆发台词") >= 0; + case "joinVoice": + return key.indexOf("入队语音") >= 0; + case "fallVoice": + return key.indexOf("倒下语音") >= 0; + case "chestVoice": + return key.indexOf("宝箱语音") >= 0; + case "constellation": + return key.indexOf("命之座") >= 0; + case "talent": + return key.indexOf("天赋") >= 0; + default: + return true; + } + }; + + let collectMatches = (isFullFlag) => { + let out = []; + for (let i = 0; i < data.length; i++) { + let item = data[i]; + for (let key in item) { + if (!allowKey(key)) { + continue; + } + let val = item[key]; + if (typeof val !== "string" || !val) { + continue; + } + if (isMatch(parsed.content, val, isFullFlag)) { + out.push({ + name: getName(item), + info: buildCharacterInfo(item) + }); + break; + } } - let val = item[key]; - if (typeof val !== "string" || !val) { - continue; - } - if (isMatch(parsed.content, val, parsed.isFull)) { - matches.push({ - name: getName(item), - info: buildCharacterInfo(item) - }); - break; + } + return out; + }; + + // 先按“完整匹配”尝试;若完全匹配不到,再降级为“只用前几个字/部分内容匹配”(解决长文本OCR截断问题) + matches = collectMatches(parsed.isFull); + if (matches.length > 0) { + return matches; + } + + if (parsed.isFull) { + let normalized = normalizeText(parsed.content); + // 太短的内容降级会产生大量误匹配,这里做个下限 + if (normalized.length >= 3) { + let relaxed = collectMatches(false); + if (relaxed.length > 0) { + // 去重(同一角色可能在多个字段命中) + let seen = {}; + let uniq = []; + for (let i = 0; i < relaxed.length; i++) { + let name = relaxed[i].name; + if (seen[name]) { + continue; + } + seen[name] = true; + uniq.push(relaxed[i]); + } + return uniq; } } } + return matches; } @@ -219,7 +393,7 @@ function logMatches(matches) { } // “只显示角色名”时仅输出角色名 - if (settings && settings.onlyName) { + if (typeof settings !== "undefined" && settings && settings.onlyName) { for (let i = 0; i < matches.length; i++) { let m = matches[i]; log.info("角色名:{0}", m.name); @@ -286,3 +460,11 @@ function formatBriefInfo(info, name) { } return parts.join(","); } + + + + + + + + diff --git a/repo/js/猜角色辅助/manifest.json b/repo/js/猜角色辅助/manifest.json index f047de92c..23a784ae2 100644 --- a/repo/js/猜角色辅助/manifest.json +++ b/repo/js/猜角色辅助/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "千星奇域-猜角色辅助", - "version": "1.0", + "version": "2.0", "bgi_version": "0.54.0", "description": "千星奇域-猜角色辅助", "authors": [ @@ -13,7 +13,6 @@ "settings_ui": "settings.json", "main": "main.js", "saved_files": [ - "需要保留的文件.txt", - "支持正则表达式与通配符.json" + "data.json" ] } \ No newline at end of file diff --git a/repo/js/猜角色辅助/settings.json b/repo/js/猜角色辅助/settings.json index 91e4ffbc4..4c139a054 100644 --- a/repo/js/猜角色辅助/settings.json +++ b/repo/js/猜角色辅助/settings.json @@ -1,7 +1,19 @@ -[ - { - "name": "onlyName", - "type": "checkbox", - "label": "只显示角色名" - } -] \ No newline at end of file +[ + { + "name": "autoRecognize", + "type": "checkbox", + "label": "自动识别", + "default": true + }, + { + "name": "onlyName", + "type": "checkbox", + "label": "只显示角色名" + }, + { + "name": "skipKey", + "type": "input-text", + "label": "跳过等待立刻识别的按键", + "default": "R" + } +]