猜角色辅助1.0 (#2650)

This commit is contained in:
ddaodan
2026-01-09 21:10:30 +08:00
committed by GitHub
parent b6d7410f31
commit e5b4378a60
6 changed files with 443 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# 千星奇域-猜角色辅助
## 使用说明
- 脚本仅有框架,不提供台词数据,请自行解决
- 需要在奇域内启动
- 退出奇域后记得关闭

View 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();

View 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": ""
}
]

View 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(",");
}

View 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"
]
}

View File

@@ -0,0 +1,7 @@
[
{
"name": "onlyName",
"type": "checkbox",
"label": "只显示角色名"
}
]