mirror of
https://github.com/babalae/bettergi-scripts-list.git
synced 2026-03-16 03:33:25 +08:00
猜角色辅助1.0 (#2650)
This commit is contained in:
5
repo/js/猜角色辅助/README.md
Normal file
5
repo/js/猜角色辅助/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 千星奇域-猜角色辅助
|
||||
## 使用说明
|
||||
- 脚本仅有框架,不提供台词数据,请自行解决
|
||||
- 需要在奇域内启动
|
||||
- 退出奇域后记得关闭
|
||||
62
repo/js/猜角色辅助/csv_to_json.js
Normal file
62
repo/js/猜角色辅助/csv_to_json.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function splitCsvLine(line) {
|
||||
// Basic CSV split on commas (no quoted fields in source file).
|
||||
return line.split(",");
|
||||
}
|
||||
|
||||
function csvToJson(csvText) {
|
||||
const lines = csvText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
||||
if (lines.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const headerLine = lines[0].trim();
|
||||
if (!headerLine) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const headers = splitCsvLine(headerLine);
|
||||
const rows = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
if (!line || !line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const values = splitCsvLine(line);
|
||||
const hasAnyValue = values.some((value) => value && value.trim() !== "");
|
||||
if (!hasAnyValue) {
|
||||
continue;
|
||||
}
|
||||
const row = {};
|
||||
|
||||
for (let h = 0; h < headers.length; h += 1) {
|
||||
const key = headers[h];
|
||||
row[key] = values[h] !== undefined ? values[h] : "";
|
||||
}
|
||||
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const inputPath = process.argv[2] || "千星奇域-猜角色辅助-工作表1.csv";
|
||||
const outputPath = process.argv[3] || "data.json";
|
||||
|
||||
const csvText = fs.readFileSync(path.resolve(inputPath), "utf8");
|
||||
const data = csvToJson(csvText);
|
||||
fs.writeFileSync(
|
||||
path.resolve(outputPath),
|
||||
JSON.stringify(data, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
console.log(`Wrote ${data.length} rows to ${outputPath}`);
|
||||
}
|
||||
|
||||
run();
|
||||
62
repo/js/猜角色辅助/data.json
Normal file
62
repo/js/猜角色辅助/data.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"角色名": "安柏",
|
||||
"性别": "女",
|
||||
"星级": "4",
|
||||
"武器类型": "弓",
|
||||
"国家": "蒙德",
|
||||
"元素类型": "火",
|
||||
"元素战绩台词1": "靠你咯。",
|
||||
"元素战绩台词2": "兔兔伯爵,出击!",
|
||||
"元素战绩台词3": "♪哼哼~哼哼哼~",
|
||||
"元素战绩台词4": "",
|
||||
"元素战绩台词5": "",
|
||||
"元素战绩台词6": "",
|
||||
"元素爆发台词1": "百发百中!",
|
||||
"元素爆发台词2": "箭如…雨下!",
|
||||
"元素爆发台词3": "你没有退路了!",
|
||||
"元素爆发台词4": "",
|
||||
"元素爆发台词5": "",
|
||||
"元素爆发台词6": ""
|
||||
},
|
||||
{
|
||||
"角色名": "砂糖",
|
||||
"性别": "女",
|
||||
"星级": "4",
|
||||
"武器类型": "法器",
|
||||
"国家": "蒙德",
|
||||
"元素类型": "风",
|
||||
"元素战绩台词1": "吸附力测试。",
|
||||
"元素战绩台词2": "陆叁零捌式风单元!",
|
||||
"元素战绩台词3": "确认…安全距离!",
|
||||
"元素战绩台词4": "",
|
||||
"元素战绩台词5": "",
|
||||
"元素战绩台词6": "",
|
||||
"元素爆发台词1": "超扩散态!",
|
||||
"元素爆发台词2": "柒伍式超级风模块!",
|
||||
"元素爆发台词3": "无相之风…拟造!",
|
||||
"元素爆发台词4": "",
|
||||
"元素爆发台词5": "",
|
||||
"元素爆发台词6": ""
|
||||
},
|
||||
{
|
||||
"角色名": "芭芭拉",
|
||||
"性别": "女",
|
||||
"星级": "4",
|
||||
"武器类型": "法器",
|
||||
"国家": "蒙德",
|
||||
"元素类型": "水",
|
||||
"元素战绩台词1": "我会保护大家!",
|
||||
"元素战绩台词2": "打起精神来哟!",
|
||||
"元素战绩台词3": "演唱,开始!",
|
||||
"元素战绩台词4": "",
|
||||
"元素战绩台词5": "",
|
||||
"元素战绩台词6": "",
|
||||
"元素爆发台词1": "♪哼哼哼~哼哼~",
|
||||
"元素爆发台词2": "准备好了吗~",
|
||||
"元素爆发台词3": "大家加油喔!",
|
||||
"元素爆发台词4": "",
|
||||
"元素爆发台词5": "",
|
||||
"元素爆发台词6": ""
|
||||
}
|
||||
]
|
||||
288
repo/js/猜角色辅助/main.js
Normal file
288
repo/js/猜角色辅助/main.js
Normal file
@@ -0,0 +1,288 @@
|
||||
const ocrRegion1 = { x: 0, y: 230, width: 500, height: 100 };
|
||||
|
||||
(async function () {
|
||||
const data = loadData();
|
||||
let emptyCount = 0;
|
||||
let lastTextKey = "";
|
||||
let lastResultKey = "";
|
||||
while (true) {
|
||||
let texts = ocr(ocrRegion1.x, ocrRegion1.y, ocrRegion1.width, ocrRegion1.height);
|
||||
if (!texts || texts.length === 0) {
|
||||
emptyCount++;
|
||||
log.info(`未识别到内容,计数 ${emptyCount}/50`);
|
||||
if (emptyCount >= 50) {
|
||||
log.warn("连续未识别达到上限,退出循环");
|
||||
break;
|
||||
}
|
||||
await sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
emptyCount = 0;
|
||||
// 仅在识别结果发生变化时输出,避免刷屏
|
||||
let textKey = texts.join(" | ").trim();
|
||||
if (textKey === lastTextKey) {
|
||||
await sleep(5000);
|
||||
continue;
|
||||
}
|
||||
if (lastResultKey) {
|
||||
log.info("==== 识别结果 ====");
|
||||
}
|
||||
lastTextKey = textKey;
|
||||
lastResultKey = textKey;
|
||||
log.info(`识别到文本: ${textKey}`);
|
||||
|
||||
// 解析 OCR 文本,提取冒号后的台词内容
|
||||
let parsedList = parseOcrTexts(texts);
|
||||
if (parsedList.length === 0) {
|
||||
log.info("未获取到可用台词内容,继续识别");
|
||||
await sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < parsedList.length; i++) {
|
||||
let parsed = parsedList[i];
|
||||
// 在数据集中查找匹配台词的角色
|
||||
let matches = findCharactersByLine(parsed, data);
|
||||
if (matches.length === 0) {
|
||||
log.info(`未命中台词: ${parsed.content}`);
|
||||
continue;
|
||||
}
|
||||
logMatches(matches);
|
||||
}
|
||||
|
||||
await sleep(5000);
|
||||
}
|
||||
})();
|
||||
|
||||
/* 加载数据
|
||||
* @returns {Object} - 返回解析后的数据对象
|
||||
*/
|
||||
function loadData() {
|
||||
try {
|
||||
const data = JSON.parse(file.readTextSync('data.json'));
|
||||
return data;
|
||||
} catch (error) {
|
||||
log.error(`加载数据失败: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* OCR 区域识别
|
||||
* @param {number} x - X坐标
|
||||
* @param {number} y - Y坐标
|
||||
* @param {number} width - 宽度
|
||||
* @param {number} height - 高度
|
||||
* @returns {string[]} texts - 识别到的文本数组
|
||||
*/
|
||||
function ocr(x, y, width, height) {
|
||||
let captureRegion = null;
|
||||
try {
|
||||
captureRegion = captureGameRegion();
|
||||
let ocrRo = RecognitionObject.ocr(x, y, width, height);
|
||||
let resList = captureRegion.findMulti(ocrRo);
|
||||
let texts = [];
|
||||
for (let i = 0; i < resList.count; i++) {
|
||||
let res = resList[i];
|
||||
if (res && res.text) {
|
||||
texts.push(res.text);
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
} catch (error) {
|
||||
log.error(`OCR 失败: ${error}`);
|
||||
return [];
|
||||
} finally {
|
||||
if (captureRegion) {
|
||||
captureRegion.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 按台词分类识别结果
|
||||
* @param {string[]} texts
|
||||
* @param {Object[]} data
|
||||
* @returns {{matches: Array<{name: string, type: string, text: string}>}}
|
||||
*/
|
||||
function parseOcrTexts(texts) {
|
||||
let parsed = [];
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
let raw = texts[i];
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
let cleaned = raw.replace(/[\"“”]/g, "").trim();
|
||||
let colonIndex = cleaned.indexOf(":");
|
||||
if (colonIndex < 0) {
|
||||
colonIndex = cleaned.indexOf(":");
|
||||
}
|
||||
if (colonIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
let prefix = cleaned.slice(0, colonIndex).trim();
|
||||
let content = cleaned.slice(colonIndex + 1).trim();
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
// 没有“前两字/首字”标记时,认为是完整台词
|
||||
let isFull = prefix.indexOf("(前两字)") < 0
|
||||
&& prefix.indexOf("(前两字)") < 0
|
||||
&& prefix.indexOf("(首字)") < 0
|
||||
&& prefix.indexOf("(首字)") < 0;
|
||||
parsed.push({ raw, content, isFull, prefix });
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeText(text) {
|
||||
return String(text || "")
|
||||
.replace(/\s+/g, "")
|
||||
.replace(/[\"“”]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isDialogueKey(key) {
|
||||
return key.indexOf("台词") >= 0 || key.indexOf("鍙拌瘝") >= 0;
|
||||
}
|
||||
|
||||
function getName(item) {
|
||||
let keys = Object.keys(item || {});
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let k = keys[i];
|
||||
if (k.indexOf("角色") >= 0 || k.indexOf("瑙掕壊") >= 0) {
|
||||
return item[k];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildCharacterInfo(item) {
|
||||
let info = {};
|
||||
for (let key in item) {
|
||||
if (isDialogueKey(key)) {
|
||||
continue;
|
||||
}
|
||||
info[key] = item[key];
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
function isMatch(content, value, isFull) {
|
||||
let c = normalizeText(content);
|
||||
let v = normalizeText(value);
|
||||
if (!c || !v) {
|
||||
return false;
|
||||
}
|
||||
// 完整台词严格匹配,前两字/首字走前缀/包含匹配
|
||||
if (isFull) {
|
||||
return v === c;
|
||||
}
|
||||
if (c.length <= 2) {
|
||||
return v.indexOf(c) === 0;
|
||||
}
|
||||
return v.indexOf(c) >= 0 || c.indexOf(v) >= 0;
|
||||
}
|
||||
|
||||
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 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 matches;
|
||||
}
|
||||
|
||||
function logMatches(matches) {
|
||||
if (!matches || matches.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// “只显示角色名”时仅输出角色名
|
||||
if (settings && settings.onlyName) {
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
let m = matches[i];
|
||||
log.info("角色名:{0}", m.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches.length < 3) {
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
let m = matches[i];
|
||||
log.info("命中角色:{0}", m.name);
|
||||
log.info(formatFullInfo(m.info, m.name), m.name);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
let m = matches[i];
|
||||
log.info(formatBriefInfo(m.info, m.name), m.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length >= 2) {
|
||||
log.info(`命中数量: ${matches.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isNameKey(key) {
|
||||
return key.indexOf("角色") >= 0 || key.indexOf("瑙掕壊") >= 0;
|
||||
}
|
||||
|
||||
function formatFullInfo(info, name) {
|
||||
let parts = [];
|
||||
let hasName = false;
|
||||
for (let key in info) {
|
||||
if (isNameKey(key)) {
|
||||
parts.push(`${key}:{0}`);
|
||||
hasName = true;
|
||||
continue;
|
||||
}
|
||||
parts.push(`${key}:${info[key]}`);
|
||||
}
|
||||
if (!hasName && name) {
|
||||
parts.unshift("角色名:{0}");
|
||||
}
|
||||
return parts.join(",");
|
||||
}
|
||||
|
||||
function formatBriefInfo(info, name) {
|
||||
let parts = [];
|
||||
let hasName = false;
|
||||
for (let key in info) {
|
||||
if (isNameKey(key)) {
|
||||
parts.push("{0}");
|
||||
hasName = true;
|
||||
continue;
|
||||
}
|
||||
let val = info[key];
|
||||
if (val === undefined || val === null || String(val).trim() === "") {
|
||||
continue;
|
||||
}
|
||||
parts.push(String(val));
|
||||
}
|
||||
if (!hasName && name) {
|
||||
parts.unshift("{0}");
|
||||
}
|
||||
return parts.join(",");
|
||||
}
|
||||
19
repo/js/猜角色辅助/manifest.json
Normal file
19
repo/js/猜角色辅助/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "千星奇域-猜角色辅助",
|
||||
"version": "1.0",
|
||||
"bgi_version": "0.54.0",
|
||||
"description": "千星奇域-猜角色辅助",
|
||||
"authors": [
|
||||
{
|
||||
"name": "ddaodan",
|
||||
"link": "https://github.com/ddaodan"
|
||||
}
|
||||
],
|
||||
"settings_ui": "settings.json",
|
||||
"main": "main.js",
|
||||
"saved_files": [
|
||||
"需要保留的文件.txt",
|
||||
"支持正则表达式与通配符.json"
|
||||
]
|
||||
}
|
||||
7
repo/js/猜角色辅助/settings.json
Normal file
7
repo/js/猜角色辅助/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "onlyName",
|
||||
"type": "checkbox",
|
||||
"label": "只显示角色名"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user