添加通过ocr识别圣遗物并解析为json格式的js脚本 (#2119)

* Create README.md

* Create settings.json

* Create manifest.json

* Create main.js

* Create Artifact.js

* Create Constants.js

* Create Parser.js

* Create a.txt

* Delete repo/js/OCRArtifacts/records/a.txt

* Create a.txt

* Create a.txt
This commit is contained in:
coderzhlh
2025-10-11 20:00:00 +08:00
committed by GitHub
parent b969b5d15d
commit 8cc27e288c
9 changed files with 334 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
# 圣遗物解析 - OCRArtifacts
使用ocr识别背包中的圣遗物并解析为json格式
tips: 暂时没有适配识别非满星圣遗物, 如: 猎人套的4星圣遗物识别可能会有问题
推荐在1920*1080分辨率下使用

View File

@@ -0,0 +1 @@
占位

View File

@@ -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

View File

@@ -0,0 +1,15 @@
var SET_TYPES = ["冒险家", "悠古的磐岩", "战狂", "冰风迷途的勇士", "染血的骑士道",
"勇士之心", "炽烈的炎之魔女", "深林的记忆", "守护之心", "沙上楼阁史话",
"来歆余响", "绝缘之旗印", "深廊终曲", "乐园遗落之花", "谐律异想断章",
"赌徒", "饰金之梦", "角斗士的终幕礼", "黄金剧团", "沉沦之心", "华馆梦醒形骸记",
"教官", "渡过烈火的贤人", "长夜之誓", "幸运儿", "被怜爱的少女", "逐影猎人", "武人",
"穹境示现之夜", "回声之林夜话", "昔日宗室之仪", "水仙之梦", "黑曜秘典", "海染砗磲",
"苍白之火", "祭水之人", "祭火之人", "祭雷之人", "祭冰之人", "行者之心", "逆飞的流星",
"学士", "烬城勇者绘卷", "追忆之注连", "纺月的夜歌", "昔时之歌", "千岩牢固", "流放者",
"如雷的盛怒", "平息鸣雷的尊者", "奇迹", "游医", "未竟的遐思", "辰砂往生录", "翠绿之影",
"花海甘露之光", "流浪大地的乐团"]
var ATTR_NAMES = ['生命值', '元素精通', '暴击率', '暴击伤害', '防御力', '攻击力', '元素充能效率']
var STAR_3_SET_TYPE = ["冒险家", "幸运儿", "游医"]
var STAR_4_SET_TYPE = ["祭水之人", "祭火之人", "祭雷之人", "祭冰之人", "学士", "赌徒", "武人", "战狂", "流放者", "行者之心", "奇迹", "守护之心", "勇士之心"]

View File

@@ -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() })
}

View File

@@ -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}`)
}
})()

View File

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

View File

@@ -0,0 +1 @@
站位

View File

@@ -0,0 +1,7 @@
[
{
"name": "notify",
"type": "checkbox",
"label": "通知"
}
]