diff --git a/repo/js/猜角色辅助/README.md b/repo/js/猜角色辅助/README.md new file mode 100644 index 000000000..355afda2b --- /dev/null +++ b/repo/js/猜角色辅助/README.md @@ -0,0 +1,5 @@ +# 千星奇域-猜角色辅助 +## 使用说明 +- 脚本仅有框架,不提供台词数据,请自行解决 +- 需要在奇域内启动 +- 退出奇域后记得关闭 \ No newline at end of file diff --git a/repo/js/猜角色辅助/csv_to_json.js b/repo/js/猜角色辅助/csv_to_json.js new file mode 100644 index 000000000..ad56d4ed1 --- /dev/null +++ b/repo/js/猜角色辅助/csv_to_json.js @@ -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(); diff --git a/repo/js/猜角色辅助/data.json b/repo/js/猜角色辅助/data.json new file mode 100644 index 000000000..4c20aa954 --- /dev/null +++ b/repo/js/猜角色辅助/data.json @@ -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": "" + } +] \ No newline at end of file diff --git a/repo/js/猜角色辅助/main.js b/repo/js/猜角色辅助/main.js new file mode 100644 index 000000000..cb594aa32 --- /dev/null +++ b/repo/js/猜角色辅助/main.js @@ -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(","); +} diff --git a/repo/js/猜角色辅助/manifest.json b/repo/js/猜角色辅助/manifest.json new file mode 100644 index 000000000..f047de92c --- /dev/null +++ b/repo/js/猜角色辅助/manifest.json @@ -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" + ] + } \ No newline at end of file diff --git a/repo/js/猜角色辅助/settings.json b/repo/js/猜角色辅助/settings.json new file mode 100644 index 000000000..91e4ffbc4 --- /dev/null +++ b/repo/js/猜角色辅助/settings.json @@ -0,0 +1,7 @@ +[ + { + "name": "onlyName", + "type": "checkbox", + "label": "只显示角色名" + } +] \ No newline at end of file