Skip to content

Commit

Permalink
ZO disc scanner (#2598)
Browse files Browse the repository at this point in the history
* zo disc scanner

* fix lint + feedback

* refactor substat key detection
  • Loading branch information
frzyc authored Jan 18, 2025
1 parent beb669d commit 4d74491
Show file tree
Hide file tree
Showing 27 changed files with 1,066 additions and 280 deletions.
8 changes: 4 additions & 4 deletions libs/common/img-util/src/processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function histogramAnalysis(
const height = imageData.height
const width = imageData.width
const p = imageData.data
return Array.from({ length: hori ? width : height }, (v, i) => {
return Array.from({ length: hori ? width : height }, (_, i) => {
let num = 0
for (let j = 0; j < (hori ? height : width); j++) {
const pixelIndex = hori
Expand Down Expand Up @@ -68,7 +68,7 @@ export function histogramContAnalysis(
const left = max * leftRange
const right = max * rightRange

return Array.from({ length: max }, (v, i) => {
return Array.from({ length: max }, (_, i) => {
if (i < left || i > right) return 0
let longest = 0
let num = 0
Expand Down Expand Up @@ -142,7 +142,7 @@ export function preprocessImage(pixelData: ImageData) {
}

// from https://github.com/processing/p5.js/blob/main/src/image/filters.js
function thresholdFilter(pixels: Uint8ClampedArray, level: number) {
export function thresholdFilter(pixels: Uint8ClampedArray, level: number) {
if (level === undefined) {
level = 0.5
}
Expand All @@ -162,7 +162,7 @@ function thresholdFilter(pixels: Uint8ClampedArray, level: number) {
}
}
// from https://css-tricks.com/manipulating-pixels-using-canvas/
function invertColors(pixels: Uint8ClampedArray) {
export function invertColors(pixels: Uint8ClampedArray) {
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = pixels[i] ^ 255 // Invert Red
pixels[i + 1] = pixels[i + 1] ^ 255 // Invert Green
Expand Down
7 changes: 5 additions & 2 deletions libs/zzz/consts/src/disc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const subData = {
crit_dmg_: { B: 0.016, A: 0.032, S: 0.048 },
anomProf: { B: 3, A: 6, S: 9 },
} as const
export function getSubStatBaseVal(
export function getDiscSubStatBaseVal(
statKey: DiscSubStatKey,
rarity: DiscRarityKey
) {
Expand Down Expand Up @@ -140,9 +140,12 @@ const mainData = {
ether_dmg_: m123,
anomMas_: m123,
enerRegen_: { B: 0.2, A: 0.4, S: 0.6 },
impact: { B: 0.06, A: 0.12, S: 0.18 },
impact_: { B: 0.06, A: 0.12, S: 0.18 },
} as const

/**
* WARN: this only works for fully leveled discs
*/
export function getDiscMainStatVal(
rarity: DiscRarityKey,
mainStatKey: DiscMainStatKey,
Expand Down
49 changes: 26 additions & 23 deletions libs/zzz/db/src/Database/DataManagers/DiscDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ export class DiscDataManager extends DataManager<
level,
slotKey,
mainStatKey,
substats: substats.map((substat) => ({
key: substat.key,
value: substat.value,
substats: substats.map(({ key, upgrades }) => ({
key,
upgrades,
})),
location,
lock,
Expand Down Expand Up @@ -238,7 +238,7 @@ export class DiscDataManager extends DataManager<
(substat, i) =>
!candidate.substats[i].key || // Candidate doesn't have anything on this slot
(substat.key === candidate.substats[i].key && // Or editor simply has better substat
substat.value >= candidate.substats[i].value)
substat.upgrades >= candidate.substats[i].upgrades)
)
)

Expand All @@ -254,15 +254,15 @@ export class DiscDataManager extends DataManager<
i // Has no extra roll
) =>
substat.key === candidate.substats[i].key &&
substat.value === candidate.substats[i].value
substat.upgrades === candidate.substats[i].upgrades
)
: substats.some(
(
substat,
i // Has extra rolls
) =>
candidate.substats[i].key
? substat.value > candidate.substats[i].value // Extra roll to existing substat
? substat.upgrades > candidate.substats[i].upgrades // Extra roll to existing substat
: substat.key // Extra roll to new substat
))
)
Expand All @@ -280,7 +280,7 @@ export class DiscDataManager extends DataManager<
candidate.substats.some(
(candidateSubstat) =>
substat.key === candidateSubstat.key && // Or same slot
substat.value === candidateSubstat.value
substat.upgrades === candidateSubstat.upgrades
)
)
)
Expand All @@ -297,11 +297,19 @@ export function validateDisc(
sortSubs = true
): IDisc | undefined {
if (!obj || typeof obj !== 'object') return undefined
const { setKey } = obj as IDisc
let { rarity, slotKey, level, mainStatKey, substats, location, lock, trash } =
obj as IDisc
let {
setKey,
rarity,
slotKey,
level,
mainStatKey,
substats,
location,
lock,
trash,
} = obj as IDisc

if (!allDiscSetKeys.includes(setKey)) return undefined // non-recoverable
if (!allDiscSetKeys.includes(setKey)) setKey = allDiscSetKeys[0]
if (!allDiscSlotKeys.includes(slotKey)) slotKey = '1'
if (!discSlotToMainStatKeys[slotKey].includes(mainStatKey))
mainStatKey = discSlotToMainStatKeys[slotKey][0]
Expand Down Expand Up @@ -344,23 +352,18 @@ function parseSubstats(
): ISubstat[] {
if (!Array.isArray(obj)) return []
const substats = (obj as ISubstat[])
.map(({ key = '', value = 0 }) => {
.map(({ key, upgrades = 0 }) => {
if (!key) return null
if (
!allDiscSubStatKeys.includes(key as DiscSubStatKey) ||
typeof value !== 'number' ||
!isFinite(value)
typeof upgrades !== 'number' ||
!Number.isFinite(upgrades)
)
return null
if (key) {
value = key.endsWith('_')
? Math.round(value * 1000) / 1000
: Math.round(value)
// TODO:
// const { low, high } = getSubstatRange(rarity, key)
// value = clamp(value, allowZeroSub ? 0 : low, high)
} else value = 0
return { key, value }

upgrades = Math.round(upgrades)

return { key, upgrades }
})
.filter(notEmpty) as ISubstat[]

Expand Down
9 changes: 1 addition & 8 deletions libs/zzz/db/src/Interfaces/IDbDisc.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import type { IDisc, ISubstat } from './IDisc'
import type { IDisc } from './IDisc'

export interface ICachedDisc extends IDisc {
id: string
mainStatVal: number
substats: ICachedSubstat[]
}

export interface ICachedSubstat extends ISubstat {
rolls: number
accurateValue: number
}
4 changes: 2 additions & 2 deletions libs/zzz/db/src/Interfaces/IDisc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import type {

export interface ISubstat {
key: DiscSubStatKey
value: number // TODO: should this be the # of rolls?
upgrades: number // This is the number of upgrades this sub receives.
}
export interface IDisc {
setKey: DiscSetKey
slotKey: DiscSlotKey
level: number
level: number // 0-15
rarity: DiscRarityKey
mainStatKey: DiscMainStatKey
location: string // TODO: CharacterKey
Expand Down
13 changes: 13 additions & 0 deletions libs/zzz/disc-scanner/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage",
"importSource": "@emotion/react"
}
]
],
"plugins": ["@emotion/babel-plugin"]
}
21 changes: 21 additions & 0 deletions libs/zzz/disc-scanner/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"extends": ["plugin:@nx/react", "../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"parserOptions": {
"project": "libs/zzz/disc-scanner/tsconfig.eslint.json"
},
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
7 changes: 7 additions & 0 deletions libs/zzz/disc-scanner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# zzz-disc-scanner

This library was generated with [Nx](https://nx.dev).

## Running unit tests

Run `nx test zzz-disc-scanner` to execute the unit tests via [Jest](https://jestjs.io).
8 changes: 8 additions & 0 deletions libs/zzz/disc-scanner/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "zzz-disc-scanner",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/zzz/disc-scanner/src",
"projectType": "library",
"tags": [],
"targets": {}
}
1 change: 1 addition & 0 deletions libs/zzz/disc-scanner/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/ScanningQueue'
73 changes: 73 additions & 0 deletions libs/zzz/disc-scanner/src/lib/ScanningQueue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Outstanding, Processed } from './processImg'
import { processEntry } from './processImg'

export type ScanningData = {
processedNum: number
outstandingNum: number
scanningNum: number
}
export type { Processed }

const maxProcessingCount = 3,
maxProcessedCount = 16

type textsFromImageFunc = (
imageData: ImageData,
options?: object | undefined
) => Promise<string[]>

export class ScanningQueue {
private debug: boolean
private textsFromImage: textsFromImageFunc
constructor(textsFromImage: textsFromImageFunc, debug = false) {
this.textsFromImage = textsFromImage
this.debug = debug
}
private processed = [] as Processed[]
private outstanding = [] as Outstanding[]
private scanning = [] as Promise<Processed>[]
callback = (() => {}) as (data: ScanningData) => void

addFiles(files: Outstanding[]) {
this.outstanding.push(...files)
this.processQueue()
}
processQueue() {
const numProcessing = Math.min(
maxProcessedCount - this.processed.length - this.scanning.length,
maxProcessingCount - this.scanning.length,
this.outstanding.length
)
numProcessing &&
this.outstanding.splice(0, numProcessing).map((p) => {
const prom = processEntry(p, this.textsFromImage, this.debug)
this.scanning.push(prom)
prom.then((procesResult) => {
const index = this.scanning.indexOf(prom)
if (index === -1) return // probably because the queue has been cleared.
this.scanning.splice(index, 1)
this.processed.push(procesResult)
this.processQueue()
})
})
this.callCB()
}
private callCB() {
this.callback({
processedNum: this.processed.length,
outstandingNum: this.outstanding.length,
scanningNum: this.scanning.length,
})
}
shiftProcessed(): Processed | undefined {
const procesesd = this.processed.shift()
if (procesesd) this.processQueue()
return procesesd
}
clearQueue() {
this.processed = []
this.outstanding = []
this.scanning = []
this.callCB()
}
}
14 changes: 14 additions & 0 deletions libs/zzz/disc-scanner/src/lib/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Color } from '@genshin-optimizer/common/img-util'

export const misreadCharactersInSubstatMap = [
{
pattern: /#/,
replacement: '+',
},
]

export const blackColor: Color = {
r: 0,
g: 0,
b: 0,
}
31 changes: 31 additions & 0 deletions libs/zzz/disc-scanner/src/lib/enStringMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Store the english translations to use with scanner

const elementalData: Record<string, string> = {
electric: 'Electric',
fire: 'Fire',
ice: 'Ice',
frost: 'Frost',
physical: 'Physical',
ether: 'Ether',
} as const

export const statMapEngMap = {
hp: 'HP',
hp_: 'HP',
atk: 'ATK',
atk_: 'ATK',
def: 'DEF',
def_: 'DEF',
pen: 'PEN',
pen_: 'PEN Ratio',
crit_: 'CRIT Rate',
crit_dmg_: 'CRIT DMG',
enerRegen_: 'Energy Regen',
impact_: 'Impact',
anomMas_: 'Anomaly Mastery',
anomProf: 'Anomaly Proficiency',
} as Record<string, string>

Object.entries(elementalData).forEach(([e, name]) => {
statMapEngMap[`${e}_dmg_`] = `${name} DMG Bonus`
})
Loading

0 comments on commit 4d74491

Please sign in to comment.