diff --git a/repo/js/OCRArtifacts/README.md b/repo/js/OCRArtifacts/README.md new file mode 100644 index 000000000..443c92f49 --- /dev/null +++ b/repo/js/OCRArtifacts/README.md @@ -0,0 +1,7 @@ +# 圣遗物解析 - OCRArtifacts + +使用ocr识别背包中的圣遗物并解析为json格式 + +tips: 暂时没有适配识别非满星圣遗物, 如: 猎人套的4星圣遗物识别可能会有问题 + 推荐在1920*1080分辨率下使用 + diff --git a/repo/js/OCRArtifacts/assets/a.txt b/repo/js/OCRArtifacts/assets/a.txt new file mode 100644 index 000000000..33cdeb84c --- /dev/null +++ b/repo/js/OCRArtifacts/assets/a.txt @@ -0,0 +1 @@ +占位 diff --git a/repo/js/OCRArtifacts/lib/Artifact.js b/repo/js/OCRArtifacts/lib/Artifact.js new file mode 100644 index 000000000..6cbf9a517 --- /dev/null +++ b/repo/js/OCRArtifacts/lib/Artifact.js @@ -0,0 +1,73 @@ + + +class _Artifact { + constructor(arr) { + this.number = parseInt(arr.shift()) + this.name = arr.shift() + this.slot = arr.shift() + this.main = arr.shift() + this.mainVal = arr.shift().replace(',', '') + const last = arr.pop() + if (last.includes('已装备')) { + this.equip = last.replace('已装备', '') + } + const index = findIndex(arr, '2件套') + arr = arr.slice(0, index) + this.setType = arr.pop().replace(/[::]/, '') + this.getStar() + this.getSource(arr) + this.getLv(arr) + this.getAttr(arr) + } + + getStar() { + const { setType } = this + if (STAR_3_SET_TYPE.includes(setType)) this.star = 3 + else if (STAR_4_SET_TYPE.includes(setType)) this.star = 4 + else this.star = 5 + } + getSource(arr) { + if (findIndex(arr, '祝圣之霜定义') !== -1) this.source = 'SEDefinition' + } + + getLv(arr) { + for (var i = 0; i < arr.length; i++) { + if (arr[i].startsWith('+')) { + let str = arr.splice(i, 1)[0] + this.lv = str.replace('+', '') || 0 + } + } + } + + getAttr(arr) { + let r = [] + for (let i = 0; i < arr.length; i++) { + let str = arr[i] + for (let n of ATTR_NAMES) { + if (str.includes(n)) { + let t = {} + if (str.includes('待激活')) { + t['active'] = false + } + if (str.includes('%') && ['攻击力', '防御力', '生命值'].includes(n)) { + t['isPercent'] = true + } + t[n] = str.match(/\+([.0-9%]+)/)[1] + r.push(t) + } + } + } + this.attr = r + } +} + + +function findIndex(arr, target) { + for (let i = 0; i < arr.length; i++) { + if (arr[i].includes(target)) return i + } + return -1 +} + +var Artifact = _Artifact + diff --git a/repo/js/OCRArtifacts/lib/Constants.js b/repo/js/OCRArtifacts/lib/Constants.js new file mode 100644 index 000000000..afaaa0b74 --- /dev/null +++ b/repo/js/OCRArtifacts/lib/Constants.js @@ -0,0 +1,15 @@ +var SET_TYPES = ["冒险家", "悠古的磐岩", "战狂", "冰风迷途的勇士", "染血的骑士道", + "勇士之心", "炽烈的炎之魔女", "深林的记忆", "守护之心", "沙上楼阁史话", + "来歆余响", "绝缘之旗印", "深廊终曲", "乐园遗落之花", "谐律异想断章", + "赌徒", "饰金之梦", "角斗士的终幕礼", "黄金剧团", "沉沦之心", "华馆梦醒形骸记", + "教官", "渡过烈火的贤人", "长夜之誓", "幸运儿", "被怜爱的少女", "逐影猎人", "武人", + "穹境示现之夜", "回声之林夜话", "昔日宗室之仪", "水仙之梦", "黑曜秘典", "海染砗磲", + "苍白之火", "祭水之人", "祭火之人", "祭雷之人", "祭冰之人", "行者之心", "逆飞的流星", + "学士", "烬城勇者绘卷", "追忆之注连", "纺月的夜歌", "昔时之歌", "千岩牢固", "流放者", + "如雷的盛怒", "平息鸣雷的尊者", "奇迹", "游医", "未竟的遐思", "辰砂往生录", "翠绿之影", + "花海甘露之光", "流浪大地的乐团"] + +var ATTR_NAMES = ['生命值', '元素精通', '暴击率', '暴击伤害', '防御力', '攻击力', '元素充能效率'] + +var STAR_3_SET_TYPE = ["冒险家", "幸运儿", "游医"] +var STAR_4_SET_TYPE = ["祭水之人", "祭火之人", "祭雷之人", "祭冰之人", "学士", "赌徒", "武人", "战狂", "流放者", "行者之心", "奇迹", "守护之心", "勇士之心"] diff --git a/repo/js/OCRArtifacts/lib/Parser.js b/repo/js/OCRArtifacts/lib/Parser.js new file mode 100644 index 000000000..77be9c84e --- /dev/null +++ b/repo/js/OCRArtifacts/lib/Parser.js @@ -0,0 +1,16 @@ +async function parse(filePath) { + let content = file.readTextSync(filePath) + content = content.replace(/=+第(\d*)个圣遗物信息:/g, ';;;;;$1') + content = content.replace(/=+.*/g, '') + const arr = content.split(';;;;;') + const total = arr.shift().match(/圣遗物数量: (\d*),/)[1] + log.info(`total: ${total}`) + let result = [] + while (arr.length > 0) { + result.push(new Artifact(arr.shift().trim().split('\n'))) + } + let name = filePath.replace('artifact_log', 'parsed_result').replace('.txt', '.json') + file.WriteTextSync(name, JSON.stringify(result, null, 2), true) + log.info(`解析完成,请查看文件: ${name}`) + return new Promise((res, rej) => { res() }) +} diff --git a/repo/js/OCRArtifacts/main.js b/repo/js/OCRArtifacts/main.js new file mode 100644 index 000000000..72b963824 --- /dev/null +++ b/repo/js/OCRArtifacts/main.js @@ -0,0 +1,196 @@ +// 加载额外模块 +eval(file.readTextSync("lib/Constants.js")) +eval(file.readTextSync("lib/Artifact.js")) +eval(file.readTextSync("lib/Parser.js")) + +// 每行圣遗物数量 +const COUNT_PER_LINE = 8 +// 每页圣遗物行数 +const LINE_PER_PAGE = 5 +// 左上方第一个圣遗物图标中心点坐标 +const centerX = 180 +const centerY = 253 +// 每次向右移动鼠标的偏移量 +const offsetX = 145 +// 每次向下移动鼠标的偏移量 +const offsetY = 166 + +// 翻页 使用模拟鼠标滚轮向下滚动实现 +let _cnt = 0 +async function scrollPage() { + let count = 49 + for (let i = 0; i < count; i++) { + await verticalScroll(-1) + } + if (++_cnt === 3) { + await verticalScroll(1) + _cnt = 0 + } +} + +// 在指定区域进行 OCR 识别并输出识别内容 +async function recognizeTextInRegion(ocrRegion, timeout = 5000) { + let startTime = Date.now() + let retryCount = 0 // 重试计数 + while (Date.now() - startTime < timeout) { + try { + // 在指定区域进行 OCR 识别 + let ocrResult = captureGameRegion().find( + RecognitionObject.ocr( + ocrRegion.x, + ocrRegion.y, + ocrRegion.width, + ocrRegion.height + ) + ) + if (ocrResult) { + return ocrResult.text // 返回识别到的内容 + } else { + log.warn(`OCR 识别区域未找到内容`) + return null // 如果 OCR 未识别到内容,返回 null + } + } catch (error) { + retryCount++ // 增加重试计数 + log.warn(`OCR识别失败,正在进行第 ${retryCount} 次重试...`) + } + await sleep(500) // 短暂延迟,避免过快循环 + } + log.warn(`经过多次尝试,仍然无法在指定区域识别到文字`) + return null // 如果未识别到文字,返回 null +} + +// 识别右上方的圣遗物总数 +async function recognizedArtifactCount() { + let ocrRegion = { x: 1500, y: 0, width: 300, height: 80 } + let text = await recognizeTextInRegion(ocrRegion) + log.info('圣遗物数量识别结果: ', text) + let res = text.match(/(\d+)\/(\d+)/) + log.info('圣遗物数量:', res[1]) + return res[1] +} + +// 识别左侧的圣遗物文本 +async function recognizedArtifactText() { + let ocrRegion = { x: 1306, y: 122, width: 496, height: 845 } + return await recognizeTextInRegion(ocrRegion) +} + +// 遍历当前页 +async function traversePage(n, totalCount) { + let x = centerX + let y = centerY + let num = n * LINE_PER_PAGE * COUNT_PER_LINE + let startRow = 0 + const sy = totalCount - num + if (sy < LINE_PER_PAGE * COUNT_PER_LINE) { + let rowNum = Math.floor(sy / COUNT_PER_LINE) + let last = sy - rowNum * COUNT_PER_LINE + if (last > 0) rowNum++ + startRow = LINE_PER_PAGE - rowNum - 1 + if (startRow < 0) startRow = 0 + log.info( + `当前为最后一页,开始行:${startRow},行数:${rowNum},最后一行有${last}个` + ) + } + let content = `开始遍历第${n + 1}页 +===================` + for (let j = startRow; j < LINE_PER_PAGE; j++) { + y = centerY + offsetY * j + for (let i = 0; i < COUNT_PER_LINE; i++) { + x = centerX + offsetX * i + await moveMouseTo(x, y) + await click(x, y) + await sleep(50) + let text = await recognizedArtifactText() + content += `第${++num}个圣遗物信息: +${text} +=====================` + if (num == totalCount) return content + } + } + return content +} + +// 同步写入文件 +async function writeFileSync( + filePath, + content, + succLogFn = null, + errorLogFn = null +) { + result = file.WriteTextSync(filePath, `${content}`, true) + if (result) { + if (succLogFn) succLogFn() + else log.info(`成功写入日志文件`) + } else { + if (errorLogFn) errorLogFn() + else log.error(`写入日志文件失败`) + } +} + +// 识别圣遗物 +async function doSearch(startTimeStamp) { + const filePath = `records/artifact_log_${startTimeStamp}.txt` + await genshin.returnMainUi() + // 按下 B 键 + keyPress('B') + await sleep(1000) + // 点击圣遗物菜单的图标 + await click(675, 50) + await sleep(1500) + let content = `开始识别圣遗物,时间:${new Date()}` + // 识别圣遗物数量 + const artifactCount = await recognizedArtifactCount() + const lineCount = Math.ceil(artifactCount / COUNT_PER_LINE) + const pageCount = Math.ceil(artifactCount / (COUNT_PER_LINE * LINE_PER_PAGE)) + let tips = ` +圣遗物数量: ${artifactCount}, 页数: ${pageCount}, 行数: ${lineCount} +=============================` + log.info(tips) + content += tips + writeFileSync(filePath, content) + for (let n = 0; n < pageCount; n++) { + let res = await traversePage(n, artifactCount) + writeFileSync( + filePath, + res, + () => log.info(`第${n + 1}页成功写入日志文件`), + () => log.error(`第${n + 1}页写入日志文件失败`) + ) + await moveMouseTo(centerX, centerY) + await sleep(100) + if (n < pageCount - 1) { + await scrollPage() + } + await sleep(200) + } + await sleep(500) + const searchEndTime = new Date().getTime() + content = `识别结束,时间:${new Date()},识别耗时:${searchEndTime - startTimeStamp}` + writeFileSync(filePath, content) + return searchEndTime +} + +async function doParse(filePath) { + await parse(filePath) +} + +; (async function () { + setGameMetrics(1920, 1080, 1) + const startTimeStamp = new Date().getTime() + const filePath = `records/artifact_log_${startTimeStamp}.txt` + try { + await doSearch(startTimeStamp) + await doParse(filePath) + await genshin.returnMainUi() + const endTimeStamp = new Date().getTime() + content = `任务结束,时间:${new Date()},总耗时:${endTimeStamp - startTimeStamp}` + writeFileSync(filePath, content) + } catch (e) { + log.error(e.stack) + writeFileSync(filePath, ` + 错误调用堆栈: + ${e.stack}`) + } + +})() diff --git a/repo/js/OCRArtifacts/manifest.json b/repo/js/OCRArtifacts/manifest.json new file mode 100644 index 000000000..6819872a3 --- /dev/null +++ b/repo/js/OCRArtifacts/manifest.json @@ -0,0 +1,18 @@ +{ + "manifest_version": 1, + "name": "OCRArtifacts", + "version": "1.0.0", + "description": "读取背包圣遗物并解析为json格式", + "authors": [ + { + "name": "zlh", + "links": "https://github.com/coderzhlh" + } + ], + "settings_ui": "settings.json", + "main": "main.js", + "saved_files": [ + "records/*.txt", + "records/*.json" + ] +} diff --git a/repo/js/OCRArtifacts/records/a.txt b/repo/js/OCRArtifacts/records/a.txt new file mode 100644 index 000000000..a13a1c8f7 --- /dev/null +++ b/repo/js/OCRArtifacts/records/a.txt @@ -0,0 +1 @@ +站位 diff --git a/repo/js/OCRArtifacts/settings.json b/repo/js/OCRArtifacts/settings.json new file mode 100644 index 000000000..34886b73a --- /dev/null +++ b/repo/js/OCRArtifacts/settings.json @@ -0,0 +1,7 @@ +[ + { + "name": "notify", + "type": "checkbox", + "label": "通知" + } +]