Skip to content

Commit 5f12c8e

Browse files
authored
更新 README.md,添加脚本功能说明;修改 main.js,增加自动识别功能及跳过等待按键设置;更新 manifest.json 版本号至 2.0;重构 settings.json,完善设置项。 (#2756)
1 parent 9ba0beb commit 5f12c8e

File tree

4 files changed

+231
-37
lines changed

4 files changed

+231
-37
lines changed

repo/js/猜角色辅助/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 千星奇域-猜角色辅助
22
## 使用说明
3-
- 脚本仅有框架,不提供台词数据,请自行解决
3+
- 脚本仅有框架,不提供数据,请自行解决
4+
- 支持元素战技,元素爆发,宝箱,倒下,入队台词,命座,天赋
45
- 需要在奇域内启动
56
- 退出奇域后记得关闭

repo/js/猜角色辅助/main.js

Lines changed: 208 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
1-
const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
1+
const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
22

33
(async function () {
44
const data = loadData();
5+
const kmHook = new KeyMouseHook();
6+
const skipKey = (typeof settings !== "undefined" && settings && settings.skipKey) ? settings.skipKey : "R";
7+
const autoRecognize = (typeof settings !== "undefined" && settings && typeof settings.autoRecognize !== "undefined") ? settings.autoRecognize : true;
8+
let skipWait = false;
9+
kmHook.OnKeyDown((key) => {
10+
if (key === skipKey) {
11+
skipWait = true;
12+
}
13+
});
14+
const sleepOrSkip = async (ms) => {
15+
const step = 100;
16+
const endTime = Date.now() + ms;
17+
while (Date.now() < endTime) {
18+
if (skipWait) {
19+
skipWait = false;
20+
return true;
21+
}
22+
await sleep(Math.min(step, endTime - Date.now()));
23+
}
24+
return false;
25+
};
26+
log.info("按 {0} 可跳过等待并立刻识别", skipKey);
27+
log.info("自动识别: {0}", autoRecognize ? "开启" : "关闭");
28+
try {
529
let emptyCount = 0;
630
let lastTextKey = "";
731
let lastResultKey = "";
832
while (true) {
33+
// 自动识别关闭时:仅在按下跳过按键后才进行一次识别
34+
if (!autoRecognize) {
35+
let triggered = await sleepOrSkip(24 * 60 * 60 * 1000);
36+
if (!triggered) {
37+
continue;
38+
}
39+
}
940
let texts = ocr(ocrRegion1.x, ocrRegion1.y, ocrRegion1.width, ocrRegion1.height);
1041
if (!texts || texts.length === 0) {
1142
emptyCount++;
@@ -14,29 +45,29 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
1445
log.warn("连续未识别达到上限,退出循环");
1546
break;
1647
}
17-
await sleep(5000);
48+
await sleepOrSkip(1000);
1849
continue;
1950
}
2051

2152
emptyCount = 0;
2253
// 仅在识别结果发生变化时输出,避免刷屏
2354
let textKey = texts.join(" | ").trim();
2455
if (textKey === lastTextKey) {
25-
await sleep(5000);
56+
await sleepOrSkip(5000);
2657
continue;
2758
}
2859
if (lastResultKey) {
2960
log.info("==== 识别结果 ====");
3061
}
3162
lastTextKey = textKey;
3263
lastResultKey = textKey;
33-
log.info(`识别到文本: ${textKey}`);
64+
log.debug(`识别到文本: ${textKey}`);
3465

3566
// 解析 OCR 文本,提取冒号后的台词内容
3667
let parsedList = parseOcrTexts(texts);
3768
if (parsedList.length === 0) {
3869
log.info("未获取到可用台词内容,继续识别");
39-
await sleep(5000);
70+
await sleepOrSkip(1000);
4071
continue;
4172
}
4273

@@ -51,7 +82,10 @@ const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
5182
logMatches(matches);
5283
}
5384

54-
await sleep(5000);
85+
await sleepOrSkip(5000);
86+
}
87+
} finally {
88+
kmHook.Dispose();
5589
}
5690
})();
5791

@@ -126,26 +160,57 @@ function parseOcrTexts(texts) {
126160
let content = cleaned.slice(colonIndex + 1).trim();
127161
if (!content) {
128162
continue;
129-
}
163+
} let category = detectCategory(prefix);
130164
// 没有“前两字/首字”标记时,认为是完整台词
131165
let isFull = prefix.indexOf("(前两字)") < 0
132166
&& prefix.indexOf("(前两字)") < 0
133167
&& prefix.indexOf("(首字)") < 0
134168
&& prefix.indexOf("(首字)") < 0;
135-
parsed.push({ raw, content, isFull, prefix });
169+
parsed.push({ raw, content, isFull, prefix, category });
136170
}
137171
return parsed;
138172
}
139173

174+
function detectCategory(prefix) {
175+
let p = String(prefix || "").replace(/\s+/g, "");
176+
if (p.includes("元素战技")) {
177+
return "elementSkill";
178+
}
179+
if (p.includes("元素爆发")) {
180+
return "elementBurst";
181+
}
182+
if (p.includes("入队语音") || p.includes("加入队伍")) {
183+
return "joinVoice";
184+
}
185+
if (p.includes("倒下语音") || p === "倒下") {
186+
return "fallVoice";
187+
}
188+
if (p.includes("宝箱语音") || p.includes("打开宝箱") || p.includes("宝箱")) {
189+
return "chestVoice";
190+
}
191+
if (p.includes("命之座")) {
192+
return "constellation";
193+
}
194+
if (p.includes("天赋")) {
195+
return "talent";
196+
}
197+
return "unknown";
198+
}
199+
140200
function normalizeText(text) {
141201
return String(text || "")
142202
.replace(/\s+/g, "")
143203
.replace(/[\"]/g, "")
204+
.replace(/[\p{P}\p{S}]/gu, "")
144205
.trim();
145206
}
146207

147208
function isDialogueKey(key) {
148-
return key.indexOf("台词") >= 0 || key.indexOf("鍙拌瘝") >= 0;
209+
return key.indexOf("台词") >= 0
210+
|| key.indexOf("语音") >= 0
211+
|| key.indexOf("命之座") >= 0
212+
|| key.indexOf("天赋") >= 0
213+
|| key.indexOf("鍙拌瘝") >= 0;
149214
}
150215

151216
function getName(item) {
@@ -178,38 +243,147 @@ function isMatch(content, value, isFull) {
178243
}
179244
// 完整台词严格匹配,前两字/首字走前缀/包含匹配
180245
if (isFull) {
181-
return v === c;
246+
if (v === c) {
247+
return true;
248+
}
249+
if (shouldUseLooseMatch(v)) {
250+
return isLooseMatch(c, v, 2);
251+
}
252+
return false;
182253
}
183254
if (c.length <= 2) {
184255
return v.indexOf(c) === 0;
185256
}
186257
return v.indexOf(c) >= 0 || c.indexOf(v) >= 0;
187258
}
188259

260+
function shouldUseLooseMatch(text) {
261+
const specialChars = ["貘"];
262+
for (let i = 0; i < specialChars.length; i++) {
263+
if (text.indexOf(specialChars[i]) >= 0) {
264+
return true;
265+
}
266+
}
267+
return false;
268+
}
269+
270+
function isLooseMatch(shortText, fullText, maxMissing) {
271+
let a = shortText;
272+
let b = fullText;
273+
if (a.length > b.length) {
274+
let tmp = a;
275+
a = b;
276+
b = tmp;
277+
}
278+
if (b.length - a.length > maxMissing) {
279+
return false;
280+
}
281+
return lcsLength(a, b) >= b.length - maxMissing;
282+
}
283+
284+
function lcsLength(a, b) {
285+
let m = a.length;
286+
let n = b.length;
287+
let prev = new Array(n + 1).fill(0);
288+
let curr = new Array(n + 1).fill(0);
289+
for (let i = 1; i <= m; i++) {
290+
for (let j = 1; j <= n; j++) {
291+
if (a[i - 1] === b[j - 1]) {
292+
curr[j] = prev[j - 1] + 1;
293+
} else {
294+
curr[j] = curr[j - 1] > prev[j] ? curr[j - 1] : prev[j];
295+
}
296+
}
297+
let temp = prev;
298+
prev = curr;
299+
curr = temp;
300+
for (let k = 0; k <= n; k++) {
301+
curr[k] = 0;
302+
}
303+
}
304+
return prev[n];
305+
}
189306
function findCharactersByLine(parsed, data) {
190307
let matches = [];
191308
if (!parsed || !parsed.content || !data) {
192309
return matches;
193310
}
194-
for (let i = 0; i < data.length; i++) {
195-
let item = data[i];
196-
for (let key in item) {
197-
if (!isDialogueKey(key)) {
198-
continue;
199-
}
200-
let val = item[key];
201-
if (typeof val !== "string" || !val) {
202-
continue;
311+
let allowKey = (key) => {
312+
if (!key || !isDialogueKey(key)) {
313+
return false;
314+
}
315+
switch (parsed.category) {
316+
case "elementSkill":
317+
return key.indexOf("元素战技台词") >= 0;
318+
case "elementBurst":
319+
return key.indexOf("元素爆发台词") >= 0;
320+
case "joinVoice":
321+
return key.indexOf("入队语音") >= 0;
322+
case "fallVoice":
323+
return key.indexOf("倒下语音") >= 0;
324+
case "chestVoice":
325+
return key.indexOf("宝箱语音") >= 0;
326+
case "constellation":
327+
return key.indexOf("命之座") >= 0;
328+
case "talent":
329+
return key.indexOf("天赋") >= 0;
330+
default:
331+
return true;
332+
}
333+
};
334+
335+
let collectMatches = (isFullFlag) => {
336+
let out = [];
337+
for (let i = 0; i < data.length; i++) {
338+
let item = data[i];
339+
for (let key in item) {
340+
if (!allowKey(key)) {
341+
continue;
342+
}
343+
let val = item[key];
344+
if (typeof val !== "string" || !val) {
345+
continue;
346+
}
347+
if (isMatch(parsed.content, val, isFullFlag)) {
348+
out.push({
349+
name: getName(item),
350+
info: buildCharacterInfo(item)
351+
});
352+
break;
353+
}
203354
}
204-
if (isMatch(parsed.content, val, parsed.isFull)) {
205-
matches.push({
206-
name: getName(item),
207-
info: buildCharacterInfo(item)
208-
});
209-
break;
355+
}
356+
return out;
357+
};
358+
359+
// 先按“完整匹配”尝试;若完全匹配不到,再降级为“只用前几个字/部分内容匹配”(解决长文本OCR截断问题)
360+
matches = collectMatches(parsed.isFull);
361+
if (matches.length > 0) {
362+
return matches;
363+
}
364+
365+
if (parsed.isFull) {
366+
let normalized = normalizeText(parsed.content);
367+
// 太短的内容降级会产生大量误匹配,这里做个下限
368+
if (normalized.length >= 3) {
369+
let relaxed = collectMatches(false);
370+
if (relaxed.length > 0) {
371+
// 去重(同一角色可能在多个字段命中)
372+
let seen = {};
373+
let uniq = [];
374+
for (let i = 0; i < relaxed.length; i++) {
375+
let name = relaxed[i].name;
376+
if (seen[name]) {
377+
continue;
378+
}
379+
seen[name] = true;
380+
uniq.push(relaxed[i]);
381+
}
382+
return uniq;
210383
}
211384
}
212385
}
386+
213387
return matches;
214388
}
215389

@@ -219,7 +393,7 @@ function logMatches(matches) {
219393
}
220394

221395
// “只显示角色名”时仅输出角色名
222-
if (settings && settings.onlyName) {
396+
if (typeof settings !== "undefined" && settings && settings.onlyName) {
223397
for (let i = 0; i < matches.length; i++) {
224398
let m = matches[i];
225399
log.info("角色名:{0}", m.name);
@@ -286,3 +460,11 @@ function formatBriefInfo(info, name) {
286460
}
287461
return parts.join(",");
288462
}
463+
464+
465+
466+
467+
468+
469+
470+

repo/js/猜角色辅助/manifest.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 1,
33
"name": "千星奇域-猜角色辅助",
4-
"version": "1.0",
4+
"version": "2.0",
55
"bgi_version": "0.54.0",
66
"description": "千星奇域-猜角色辅助",
77
"authors": [
@@ -13,7 +13,6 @@
1313
"settings_ui": "settings.json",
1414
"main": "main.js",
1515
"saved_files": [
16-
"需要保留的文件.txt",
17-
"支持正则表达式与通配符.json"
16+
"data.json"
1817
]
1918
}
Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
[
2-
{
3-
"name": "onlyName",
4-
"type": "checkbox",
5-
"label": "只显示角色名"
6-
}
7-
]
1+
[
2+
{
3+
"name": "autoRecognize",
4+
"type": "checkbox",
5+
"label": "自动识别",
6+
"default": true
7+
},
8+
{
9+
"name": "onlyName",
10+
"type": "checkbox",
11+
"label": "只显示角色名"
12+
},
13+
{
14+
"name": "skipKey",
15+
"type": "input-text",
16+
"label": "跳过等待立刻识别的按键",
17+
"default": "R"
18+
}
19+
]

0 commit comments

Comments
 (0)