From e9f8659398ccab6e0b1fd15d694f213a8d9bbcac Mon Sep 17 00:00:00 2001 From: Van Nguyen <36019388+nguyentvan7@users.noreply.github.com> Date: Mon, 25 Dec 2023 21:57:06 -0700 Subject: [PATCH 01/17] Fix misc issues (#1383) * Add Hu Tao atk optimization target * Make Song of Days Past a teambuff * Update scanner versions * Remove Akasha scanner * Add Song of Days past as optimization target * Modify SyncRepo to take a parameter for branch name * Remove Song of Days Past opt target --- .../Data/Artifacts/SongOfDaysPast/index.tsx | 15 ++++++++++----- .../src/app/Data/Characters/HuTao/index.tsx | 1 + apps/frontend/src/app/PageScanner/index.tsx | 18 ++++++++---------- libs/dm/project.json | 3 ++- .../src/executors/sync-repo/executor.spec.ts | 3 +++ .../plugin/src/executors/sync-repo/executor.ts | 9 +++++++-- .../plugin/src/executors/sync-repo/schema.d.ts | 1 + .../plugin/src/executors/sync-repo/schema.json | 4 ++++ libs/sr-dm/project.json | 3 ++- 9 files changed, 38 insertions(+), 19 deletions(-) diff --git a/apps/frontend/src/app/Data/Artifacts/SongOfDaysPast/index.tsx b/apps/frontend/src/app/Data/Artifacts/SongOfDaysPast/index.tsx index fb30793fe6..5344ce98cf 100644 --- a/apps/frontend/src/app/Data/Artifacts/SongOfDaysPast/index.tsx +++ b/apps/frontend/src/app/Data/Artifacts/SongOfDaysPast/index.tsx @@ -34,11 +34,15 @@ const burst_dmgInc = { ...healing_dmgInc } export const data: Data = dataObjForArtifactSheet(key, { premod: { heal_: set2, - normal_dmgInc, - charged_dmgInc, - plunging_dmgInc, - skill_dmgInc, - burst_dmgInc, + }, + teamBuff: { + premod: { + normal_dmgInc, + charged_dmgInc, + plunging_dmgInc, + skill_dmgInc, + burst_dmgInc, + }, }, }) @@ -54,6 +58,7 @@ const sheet: IArtifactSheet = { value: condHealing, path: condHealingPath, name: trm('condName'), + teamBuff: true, states: objKeyMap(healingArr, (heal) => ({ name: `${heal}`, fields: [ diff --git a/apps/frontend/src/app/Data/Characters/HuTao/index.tsx b/apps/frontend/src/app/Data/Characters/HuTao/index.tsx index 84ba91c0fa..432de5470c 100644 --- a/apps/frontend/src/app/Data/Characters/HuTao/index.tsx +++ b/apps/frontend/src/app/Data/Characters/HuTao/index.tsx @@ -175,6 +175,7 @@ const dmgFormulas = { ), skill: { dmg: dmgNode('atk', dm.skill.dmg, 'skill'), + atk, }, burst: { dmg: dmgNode('atk', dm.burst.dmg, 'burst'), diff --git a/apps/frontend/src/app/PageScanner/index.tsx b/apps/frontend/src/app/PageScanner/index.tsx index a7673af13a..e505832dd7 100644 --- a/apps/frontend/src/app/PageScanner/index.tsx +++ b/apps/frontend/src/app/PageScanner/index.tsx @@ -1,9 +1,9 @@ import { AnvilIcon, DiscordIcon } from '@genshin-optimizer/svgicons' +import { CardThemed, SqBadge } from '@genshin-optimizer/ui-common' import { Backpack, Computer, Download, - EmojiEvents, Gamepad, InsertLink, PersonSearch, @@ -27,10 +27,8 @@ import ReactGA from 'react-ga4' import { Trans, useTranslation } from 'react-i18next' import { Link as RouterLink } from 'react-router-dom' import AdScanner from './AdeptiScanner.png' -import AkashaScanner from './AkashaScanner.png' -import Artiscan from './artiscan.png' import GIScanner from './GIScanner.png' -import { CardThemed, SqBadge } from '@genshin-optimizer/ui-common' +import Artiscan from './artiscan.png' export default function PageScanner() { const { t } = useTranslation('page_scanner') ReactGA.send({ hitType: 'pageview', page: '/scanner' }) @@ -98,7 +96,7 @@ export default function PageScanner() { sx={{ display: 'flex', alignItems: 'center' }} > - 4.2 + 4.3 @@ -156,7 +154,7 @@ export default function PageScanner() { sx={{ display: 'flex', alignItems: 'center' }} > - 4.2 + 4.3 @@ -243,7 +241,7 @@ export default function PageScanner() { sx={{ display: 'flex', alignItems: 'center' }} > - 4.2 + 4.3 @@ -295,7 +293,7 @@ export default function PageScanner() { - + {/* @@ -383,7 +381,7 @@ export default function PageScanner() { - + */} ) diff --git a/libs/dm/project.json b/libs/dm/project.json index 585fad7ffe..e8f5dc5f44 100644 --- a/libs/dm/project.json +++ b/libs/dm/project.json @@ -7,7 +7,8 @@ "load-dm": { "options": { "repoUrl": "https://gitlab.com/Dimbreath/AnimeGameData.git", - "outputPath": "libs/dm/GenshinData" + "outputPath": "libs/dm/GenshinData", + "branch": "origin/main" }, "inputs": [ { diff --git a/libs/plugin/src/executors/sync-repo/executor.spec.ts b/libs/plugin/src/executors/sync-repo/executor.spec.ts index ab0c6d4263..7dd27c402c 100644 --- a/libs/plugin/src/executors/sync-repo/executor.spec.ts +++ b/libs/plugin/src/executors/sync-repo/executor.spec.ts @@ -7,6 +7,7 @@ describe('SyncRepo Executor', () => { const repoUrl = 'https://github.com/chrislgarry/Apollo-11/' const outputPath = path.join(__dirname, 'TestDB') const hashPath = path.join(__dirname, 'TestDB.hash') + const branch = 'origin/master' if (fs.existsSync(outputPath)) fs.rmSync(outputPath, { recursive: true, force: true }) @@ -15,6 +16,7 @@ describe('SyncRepo Executor', () => { const output1 = await executor({ repoUrl, outputPath, + branch, prefixPath: false, }) @@ -28,6 +30,7 @@ describe('SyncRepo Executor', () => { const output2 = await executor({ repoUrl, outputPath, + branch, prefixPath: false, }) expect(output2.success).toBe(true) diff --git a/libs/plugin/src/executors/sync-repo/executor.ts b/libs/plugin/src/executors/sync-repo/executor.ts index be84571f90..05e0d41336 100644 --- a/libs/plugin/src/executors/sync-repo/executor.ts +++ b/libs/plugin/src/executors/sync-repo/executor.ts @@ -7,7 +7,12 @@ import * as path from 'path' export default async function runExecutor( options: SyncRepoExecutorSchema ): Promise<{ success: boolean }> { - const { outputPath, repoUrl: url, prefixPath: prefix = true } = options + const { + outputPath, + repoUrl: url, + prefixPath: prefix = true, + branch, + } = options const cwd = prefix ? path.join(workspaceRoot, outputPath) : outputPath const remoteHash = getRemoteRepoHash(url) const name = path.basename(cwd) @@ -23,7 +28,7 @@ Caution: if this is part of nx cache replay, const localHash = getLocalRepoHash(cwd) if (remoteHash !== localHash) { execSync(`git fetch --depth 1`, { cwd }) - execSync(`git reset --hard origin/master`, { cwd }) + execSync(`git reset --hard ${branch}`, { cwd }) } else console.log('Repo already existed with the latest commit') } else { // Clone diff --git a/libs/plugin/src/executors/sync-repo/schema.d.ts b/libs/plugin/src/executors/sync-repo/schema.d.ts index 0ebda2b084..c0f57dd67f 100644 --- a/libs/plugin/src/executors/sync-repo/schema.d.ts +++ b/libs/plugin/src/executors/sync-repo/schema.d.ts @@ -1,5 +1,6 @@ export interface SyncRepoExecutorSchema { repoUrl: string outputPath: string + branch: string prefixPath?: boolean } diff --git a/libs/plugin/src/executors/sync-repo/schema.json b/libs/plugin/src/executors/sync-repo/schema.json index ea63294e62..0d9577ada2 100644 --- a/libs/plugin/src/executors/sync-repo/schema.json +++ b/libs/plugin/src/executors/sync-repo/schema.json @@ -13,6 +13,10 @@ "type": "string", "description": "Local repo path (including the repo name)" }, + "branch": { + "type": "string", + "description": "Branch to sync (e.g. origin/master)" + }, "prefixPath": { "type": "boolean", "description": "Whether to prepend `localPath` with `{workspaceRoot}`. Default: true" diff --git a/libs/sr-dm/project.json b/libs/sr-dm/project.json index 3d15ad97e6..8abffb625a 100644 --- a/libs/sr-dm/project.json +++ b/libs/sr-dm/project.json @@ -7,7 +7,8 @@ "load-dm": { "options": { "repoUrl": "https://github.com/Dimbreath/StarRailData.git", - "outputPath": "libs/sr-dm/StarRailData" + "outputPath": "libs/sr-dm/StarRailData", + "branch": "origin/master" }, "inputs": [ { From 56ea4e68f2759e85844f7db79c33397c013f6854 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 05:05:41 +0000 Subject: [PATCH 02/17] 9.19.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4aebf13621..139347be61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "genshin-optimizer", - "version": "9.19.2", + "version": "9.19.3", "license": "MIT", "private": true, "scripts": { From eb8378ec33470ca121a6ddabd3ef6031eb447790 Mon Sep 17 00:00:00 2001 From: lantua <16190491+lantua@users.noreply.github.com> Date: Wed, 27 Dec 2023 17:27:04 -0500 Subject: [PATCH 03/17] Fix stat calculation stage and listing (#1389) - Restrict `base` stage to `atk/def/hp` - Move irrelevant `base` stats to `premod` - Update char/weapon bonus stat calculation` - Move `formula.listing` listing to `listing.formulas` - Add `listing.specialized` listing --- libs/gi-formula/src/data/char/util.ts | 46 +++++++++++++++---- libs/gi-formula/src/data/common/index.ts | 5 +- libs/gi-formula/src/data/common/prep.ts | 4 +- libs/gi-formula/src/data/common/reaction.ts | 2 +- libs/gi-formula/src/data/util/sheet.ts | 37 +++++++-------- libs/gi-formula/src/data/util/tag.ts | 12 +++-- libs/gi-formula/src/data/weapon/util.ts | 43 ++++++++++++++--- libs/gi-formula/src/debug.ts | 2 +- libs/gi-formula/src/example.test.ts | 14 +++--- .../src/executors/gen-desc/executor.ts | 4 +- libs/gi-formula/src/util.ts | 4 +- .../src/executors/gen-stats/src/weaponData.ts | 10 ++-- 12 files changed, 122 insertions(+), 61 deletions(-) diff --git a/libs/gi-formula/src/data/char/util.ts b/libs/gi-formula/src/data/char/util.ts index fe9b69c230..401cf5d209 100644 --- a/libs/gi-formula/src/data/char/util.ts +++ b/libs/gi-formula/src/data/char/util.ts @@ -12,12 +12,12 @@ import type { NumNode } from '@genshin-optimizer/pando' import { prod, subscript, sum } from '@genshin-optimizer/pando' import type { TagMapNodeEntries, FormulaArg, Stat } from '../util' import { - addStatCurve, - registerStatListing, allStatics, customDmg, customShield, + listingItem, percent, + readStat, self, selfBuff, } from '../util' @@ -126,32 +126,58 @@ export function fixedShield( ) } +const baseStats = new Set(['atk', 'def', 'hp']) + export function entriesForChar( - { ele, weaponType, region }: CharInfo, + { key, ele, weaponType, region }: CharInfo, { lvlCurves, ascensionBonus }: CharacterDataGen ): TagMapNodeEntries { - const specials = new Set(Object.keys(ascensionBonus)) - specials.delete('atk') - specials.delete('def') - specials.delete('hp') + const specialized = new Set( + Object.keys(ascensionBonus) as (keyof typeof ascensionBonus)[] + ) + specialized.delete('atk') + specialized.delete('def') + specialized.delete('hp') const { ascension } = self.char return [ // Stats ...lvlCurves.map(({ key, base, curve }) => - addStatCurve(key, prod(base, allStatics('static')[curve])) + selfBuff.base[key].add(prod(base, allStatics('static')[curve])) ), ...Object.entries(ascensionBonus).map(([key, values]) => - addStatCurve(key, subscript(ascension, values)) + (baseStats.has(key) + ? selfBuff.base[key as 'atk' | 'def' | 'hp'] + : readStat(selfBuff.premod, key as keyof typeof ascensionBonus) + ).add(subscript(ascension, values)) ), // Constants - ...[...specials].map((s) => registerStatListing(s)), selfBuff.common.weaponType.add(weaponType), selfBuff.char.ele.add(ele), // Counters selfBuff.common.count[ele].add(1), ...(region !== '' ? [selfBuff.common.count[region].add(1)] : []), + + // Listing (formulas) + selfBuff.listing.formulas.add(listingItem(self.final.hp)), + selfBuff.listing.formulas.add(listingItem(self.final.atk)), + selfBuff.listing.formulas.add(listingItem(self.final.def)), + selfBuff.listing.formulas.add(listingItem(self.final.eleMas)), + selfBuff.listing.formulas.add(listingItem(self.final.enerRech_)), + selfBuff.listing.formulas.add(listingItem(self.common.cappedCritRate_)), + selfBuff.listing.formulas.add(listingItem(self.final.critDMG_)), + selfBuff.listing.formulas.add(listingItem(self.final.heal_)), + selfBuff.listing.formulas.add(listingItem(self.final.dmg_[ele])), + selfBuff.listing.formulas.add(listingItem(self.final.dmg_.physical)), + + // Listing (specialized) + ...[...specialized].map((stat) => + selfBuff.listing.specialized.add( + // Sheet-specific data (i.e., `src:`) + listingItem(readStat(self.premod, stat).src(key)) + ) + ), ] } diff --git a/libs/gi-formula/src/data/common/index.ts b/libs/gi-formula/src/data/common/index.ts index e4bcd88031..7265571a9b 100644 --- a/libs/gi-formula/src/data/common/index.ts +++ b/libs/gi-formula/src/data/common/index.ts @@ -15,13 +15,16 @@ const data: TagMapNodeEntries = [ reader.withTag({ src: 'iso', et: 'self' }).reread(reader.src('custom')), reader.withTag({ src: 'agg', et: 'self' }).reread(reader.src('custom')), - // Final <= Premod <= Base + // Final <= Premod <= Base + WeaponRefinement reader .withTag({ src: 'agg', et: 'self', qt: 'final' }) .add(reader.with('qt', 'premod').sum), reader .withTag({ src: 'agg', et: 'self', qt: 'premod' }) .add(reader.with('qt', 'base').sum), + reader + .withTag({ src: 'agg', et: 'self', qt: 'premod' }) + .add(reader.with('qt', 'weaponRefinement').sum), // premod X += base X * premod X% ...(['atk', 'def', 'hp'] as const).map((s) => diff --git a/libs/gi-formula/src/data/common/prep.ts b/libs/gi-formula/src/data/common/prep.ts index 8c9aaddb6f..6910a5a757 100644 --- a/libs/gi-formula/src/data/common/prep.ts +++ b/libs/gi-formula/src/data/common/prep.ts @@ -15,10 +15,10 @@ const data: TagMapNodeEntries = [ }) ), selfBuff.formula.shield.add( - prod(self.formula.base, sum(percent(1), self.base.shield_)) + prod(self.formula.base, sum(percent(1), self.premod.shield_)) ), selfBuff.formula.heal.add( - prod(self.formula.base, sum(percent(1), self.base.heal_)) + prod(self.formula.base, sum(percent(1), self.premod.heal_)) ), // Transformative reactions diff --git a/libs/gi-formula/src/data/common/reaction.ts b/libs/gi-formula/src/data/common/reaction.ts index fdbe50dec4..a1d1cad8e4 100644 --- a/libs/gi-formula/src/data/common/reaction.ts +++ b/libs/gi-formula/src/data/common/reaction.ts @@ -364,7 +364,7 @@ const data: TagMapNodeEntries = [ return variants.flatMap((ele) => { const name = trans === 'swirl' ? `swirl_${ele}` : trans return [ - selfBuff.formula.listing.add( + selfBuff.listing.formulas.add( tag(cond, { trans, q, ele, src: 'static', name }) ), selfBuff.prep.ele.name(name).add(ele), diff --git a/libs/gi-formula/src/data/util/sheet.ts b/libs/gi-formula/src/data/util/sheet.ts index f6a1e98508..6898c1357d 100644 --- a/libs/gi-formula/src/data/util/sheet.ts +++ b/libs/gi-formula/src/data/util/sheet.ts @@ -7,9 +7,11 @@ import type { import type { NumNode, StrNode } from '@genshin-optimizer/pando' import { prod } from '@genshin-optimizer/pando' import type { Source, Stat } from './listing' +import type { Read } from './read' import { reader, tag } from './read' import { self, selfBuff, teamBuff } from './tag' import type { TagMapNodeEntries, TagMapNodeEntry } from './tagMapType' +import type { StatKey } from '@genshin-optimizer/dm' // Use `registerArt` for artifacts export function register( @@ -25,24 +27,6 @@ export function register( ) } -export function addStatCurve(key: string, value: NumNode): TagMapNodeEntry { - return ( - key.endsWith('_dmg_') - ? selfBuff.premod['dmg_'][key.slice(0, -5) as ElementWithPhyKey] - : selfBuff.base[key as Stat] - ).add(value) -} -export function registerStatListing(key: string): TagMapNodeEntry { - const tags = key.endsWith('_dmg_') - ? { - qt: 'premod', - q: 'dmg_', - ele: key.slice(0, -5) as ElementWithPhyKey, - } - : { qt: 'base', q: key } - return selfBuff.formula.listing.add(tag('sum', tags)) -} - export type FormulaArg = { team?: boolean // true if applies to every member, and false (default) if applies only to self cond?: string | StrNode @@ -121,9 +105,22 @@ function registerFormula( ...extra: TagMapNodeEntries ): TagMapNodeEntries { reader.name(name) // register name: - const buff = team ? teamBuff : selfBuff + const listing = (team ? teamBuff : selfBuff).listing.formulas return [ - buff.formula.listing.add(tag(cond, { name, q })), + listing.add(listingItem(reader.withTag({ name, qt: 'formula', q }), cond)), ...extra.map(({ tag, value }) => ({ tag: { ...tag, name }, value })), ] } + +export function listingItem(t: Read, cond?: string | StrNode) { + return tag(cond ?? t.ex ?? 'unique', t.tag) +} + +export function readStat( + list: Record, + key: StatKey +): Read { + return key.endsWith('_dmg_') + ? list['dmg_'][key.slice(0, -5) as ElementWithPhyKey] + : list[key as Stat] +} diff --git a/libs/gi-formula/src/data/util/tag.ts b/libs/gi-formula/src/data/util/tag.ts index 0830c6e534..d887cd3711 100644 --- a/libs/gi-formula/src/data/util/tag.ts +++ b/libs/gi-formula/src/data/util/tag.ts @@ -87,8 +87,9 @@ const stats: Record = { heal_: agg, } as const export const selfTag = { - base: { ...stats, shield_: agg }, - premod: stats, + base: { atk: agg, def: agg, hp: agg }, + weaponRefinement: { ...stats, shield_: agg }, + premod: { ...stats, shield_: agg }, final: stats, char: { lvl: iso, @@ -132,7 +133,6 @@ export const selfTag = { prep: { ele: prep, move: prep, amp: prep, cata: prep, trans: prep }, formula: { base: agg, - listing: aggStr, dmg: prep, shield: prep, heal: prep, @@ -140,6 +140,10 @@ export const selfTag = { transCrit: prep, swirl: prep, }, + listing: { + formulas: aggStr, + specialized: aggStr, + }, } as const export const enemyTag = { common: { @@ -159,7 +163,7 @@ export function convert>>( ): { [j in keyof V]: { [k in keyof V[j]]: Read } } { return reader.withTag(tag).withAll('qt', Object.keys(v), (r, qt) => r.withAll('q', Object.keys(v[qt]), (r, q) => { - if (!v[qt][q]) console.log(v, qt, q) + if (!v[qt][q]) console.error(`Invalid { qt:${qt} q:${q} }`) const { src, accu } = v[qt][q] // `tag.src` overrides `Desc` if (src && !tag.src) r = r.src(src) diff --git a/libs/gi-formula/src/data/weapon/util.ts b/libs/gi-formula/src/data/weapon/util.ts index 737154d63b..e5e61a107f 100644 --- a/libs/gi-formula/src/data/weapon/util.ts +++ b/libs/gi-formula/src/data/weapon/util.ts @@ -2,25 +2,54 @@ import { type WeaponKey } from '@genshin-optimizer/consts' import { allStats } from '@genshin-optimizer/gi-stats' import { prod, subscript } from '@genshin-optimizer/pando' import type { TagMapNodeEntries } from '../util' -import { addStatCurve, allStatics, registerStatListing, self } from '../util' +import { allStatics, listingItem, readStat, self, selfBuff } from '../util' export function entriesForWeapon(key: WeaponKey): TagMapNodeEntries { const gen = allStats.weapon.data[key] const { refinement, ascension } = self.weapon - const specials = new Set(Object.keys(gen.ascensionBonus)) + const primaryStat = 'atk' + const nonPrimaryStat = new Set(gen.lvlCurves.map(({ key }) => key)) + nonPrimaryStat.delete(primaryStat) return [ // Stats ...gen.lvlCurves.map(({ key, base, curve }) => - addStatCurve(key, prod(base, allStatics('static')[curve])) + (key == 'atk' ? selfBuff.base[key] : readStat(selfBuff.premod, key)).add( + prod(base, allStatics('static')[curve]) + ) ), ...Object.entries(gen.ascensionBonus).map(([key, values]) => - addStatCurve(key, subscript(ascension, values)) + (key == 'atk' + ? selfBuff.base[key] + : readStat(selfBuff.premod, key as keyof typeof gen.ascensionBonus) + ).add(subscript(ascension, values)) ), ...Object.entries(gen.refinementBonus).map(([key, values]) => - addStatCurve(key, subscript(refinement, values)) + readStat( + selfBuff.weaponRefinement, + key as keyof typeof gen.refinementBonus + ).add(subscript(refinement, values)) + ), + + // Listing (specialized) + // All items here are sheet-specific data (i.e., `src:`) + self.listing.specialized.add(listingItem(self.base[primaryStat].src(key))), + ...[...nonPrimaryStat].map((stat) => + self.listing.specialized.add( + listingItem(readStat(self.premod, stat).src(key)) + ) + ), + ...[...Object.keys(gen.refinementBonus)].map((stat) => + self.listing.specialized + .src(key) + .add( + listingItem( + readStat( + self.weaponRefinement, + stat as keyof typeof gen.refinementBonus + ) + ) + ) ), - // Listing - ...[...specials].map((key) => registerStatListing(key)), ] } diff --git a/libs/gi-formula/src/debug.ts b/libs/gi-formula/src/debug.ts index 8dac3b9c57..76c4fe1e77 100644 --- a/libs/gi-formula/src/debug.ts +++ b/libs/gi-formula/src/debug.ts @@ -161,7 +161,7 @@ export class DebugCalculator extends BaseCalculator { text: `expand ${tagStr(nTag!, ex)} (${tagStr(tag!)})`, deps: args.map(({ meta, entryTag }) => ({ ...meta, - text: `${entryTag!.map((tag) => tagStr(tag)).join(' <- ')} <= ${ + text: `${entryTag?.map((tag) => tagStr(tag)).join(' <- ')} <= ${ meta.text }`, })), diff --git a/libs/gi-formula/src/example.test.ts b/libs/gi-formula/src/example.test.ts index 57fc834374..98a42aba16 100644 --- a/libs/gi-formula/src/example.test.ts +++ b/libs/gi-formula/src/example.test.ts @@ -109,7 +109,7 @@ describe('example', () => { calc.compute(member1.final.eleMas).val ) }) - describe('list final formulas', () => { + describe('retrieve formulas in formula listing', () => { /** * Each entry in listing is a `Tag` in the shape of * ``` @@ -123,7 +123,9 @@ describe('example', () => { * } * ``` */ - const listing = calc.listFormulas(member0.formula.listing).map((x) => x.tag) + const listing = calc + .listFormulas(member0.listing.formulas) + .map((x) => x.tag) // Simple check that all tags are in the correct format const names: string[] = [] @@ -158,9 +160,9 @@ describe('example', () => { ]) expect(listing.filter((x) => x.src === 'static').length).toEqual(5) }) - test('calculate final formulas', () => { + test('calculate formulas in a listing', () => { const read = calc - .listFormulas(member0.formula.listing) + .listFormulas(member0.listing.formulas) .find((x) => x.tag.name === 'normal_0')! const tag = read.tag @@ -203,7 +205,7 @@ describe('example', () => { // Step 1: Pick formula(s); anything that `calc.compute` can handle will work const nodes = [ calc - .listFormulas(member0.formula.listing) + .listFormulas(member0.listing.formulas) .find((x) => x.tag.name === 'normal_0')!, member0.char.auto, member0.final.atk, @@ -247,7 +249,7 @@ describe('example', () => { test.skip('debug formula', () => { // Pick formula const normal0 = calc - .listFormulas(member1.formula.listing) + .listFormulas(member1.listing.formulas) .find((x) => x.tag.name === 'normal_0')! // Use `DebugCalculator` instead of `Calculator`, same constructor diff --git a/libs/gi-formula/src/executors/gen-desc/executor.ts b/libs/gi-formula/src/executors/gen-desc/executor.ts index 7b74327d42..f1b6a92fe8 100644 --- a/libs/gi-formula/src/executors/gen-desc/executor.ts +++ b/libs/gi-formula/src/executors/gen-desc/executor.ts @@ -34,8 +34,8 @@ export default async function runExecutor( // sheet-specific tag['src'] != 'agg' && // formula listing - tag['qt'] == 'formula' && - tag['q'] == 'listing' && + tag['qt'] == 'listing' && + tag['q'] == 'formulas' && // pattern from `registerFormula` value['op'] == 'tag' && 'name' in value.tag && diff --git a/libs/gi-formula/src/util.ts b/libs/gi-formula/src/util.ts index 501edca18b..fc570c6c2c 100644 --- a/libs/gi-formula/src/util.ts +++ b/libs/gi-formula/src/util.ts @@ -44,8 +44,8 @@ export function charData(data: ICharacter): TagMapNodeEntries { constellation.add(data.constellation), // Default char - selfBuff.base.critRate_.add(0.05), - selfBuff.base.critDMG_.add(0.5), + selfBuff.premod.critRate_.add(0.05), + selfBuff.premod.critDMG_.add(0.5), ] } diff --git a/libs/gi-stats/src/executors/gen-stats/src/weaponData.ts b/libs/gi-stats/src/executors/gen-stats/src/weaponData.ts index ac521a98d7..ea052de5e2 100644 --- a/libs/gi-stats/src/executors/gen-stats/src/weaponData.ts +++ b/libs/gi-stats/src/executors/gen-stats/src/weaponData.ts @@ -25,9 +25,9 @@ export type WeaponDataGen = { rarity: 1 | 2 | 3 | 4 | 5 mainStat: WeaponProp subStat?: WeaponProp | undefined - lvlCurves: { key: string; base: number; curve: WeaponGrowCurveKey }[] - refinementBonus: { [key in string]: number[] } - ascensionBonus: { [key in string]: number[] } + lvlCurves: { key: StatKey; base: number; curve: WeaponGrowCurveKey }[] + refinementBonus: { [key in StatKey]?: number[] } + ascensionBonus: { [key in StatKey]?: number[] } } export default function weaponData() { @@ -52,7 +52,7 @@ export default function weaponData() { if (!(key in refinementBonus)) refinementBonus[key] = [...emptyRefinement] // Refinement uses 1-based index, hence the +1 - refinementBonus[key][i + 1] += extrapolateFloat(value) + refinementBonus[key]![i + 1] += extrapolateFloat(value) } }) const ascensionBonus: WeaponDataGen['ascensionBonus'] = {} @@ -63,7 +63,7 @@ export default function weaponData() { const key = propTypeMap[propType] if (!(key in ascensionBonus)) ascensionBonus[key] = [...emptyAscension] - ascensionBonus[key][i] += extrapolateFloat(value) + ascensionBonus[key]![i] += extrapolateFloat(value) } }) From 66026ce2254303f3adcc4562865a072940c5b92b Mon Sep 17 00:00:00 2001 From: lantua <16190491+lantua@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:44:51 -0500 Subject: [PATCH 04/17] Minor gi-formula bug fix (#1391) * Minor bug fix --- libs/gi-formula/src/data/common/reaction.ts | 2 +- libs/gi-formula/src/data/weapon/util.ts | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/libs/gi-formula/src/data/common/reaction.ts b/libs/gi-formula/src/data/common/reaction.ts index a1d1cad8e4..610f9ec35e 100644 --- a/libs/gi-formula/src/data/common/reaction.ts +++ b/libs/gi-formula/src/data/common/reaction.ts @@ -365,7 +365,7 @@ const data: TagMapNodeEntries = [ const name = trans === 'swirl' ? `swirl_${ele}` : trans return [ selfBuff.listing.formulas.add( - tag(cond, { trans, q, ele, src: 'static', name }) + tag(cond, { trans, qt: 'formula', q, ele, src: 'static', name }) ), selfBuff.prep.ele.name(name).add(ele), ] diff --git a/libs/gi-formula/src/data/weapon/util.ts b/libs/gi-formula/src/data/weapon/util.ts index e5e61a107f..b735b6f377 100644 --- a/libs/gi-formula/src/data/weapon/util.ts +++ b/libs/gi-formula/src/data/weapon/util.ts @@ -40,16 +40,14 @@ export function entriesForWeapon(key: WeaponKey): TagMapNodeEntries { ) ), ...[...Object.keys(gen.refinementBonus)].map((stat) => - self.listing.specialized - .src(key) - .add( - listingItem( - readStat( - self.weaponRefinement, - stat as keyof typeof gen.refinementBonus - ) - ) + self.listing.specialized.add( + listingItem( + readStat( + self.weaponRefinement, + stat as keyof typeof gen.refinementBonus + ).src(key) ) + ) ), ] } From 7e432fef9ec74d0103f4f581938db5b685e801de Mon Sep 17 00:00:00 2001 From: lantua <16190491+lantua@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:20:37 -0500 Subject: [PATCH 05/17] Add weapon-only example (#1392) --- libs/gi-formula/src/example.test.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/libs/gi-formula/src/example.test.ts b/libs/gi-formula/src/example.test.ts index 98a42aba16..58027f1401 100644 --- a/libs/gi-formula/src/example.test.ts +++ b/libs/gi-formula/src/example.test.ts @@ -109,7 +109,7 @@ describe('example', () => { calc.compute(member1.final.eleMas).val ) }) - describe('retrieve formulas in formula listing', () => { + describe('retrieve formulas in a listing', () => { /** * Each entry in listing is a `Tag` in the shape of * ``` @@ -263,3 +263,25 @@ describe('example', () => { console.log(debugCalc.debug(normal0)) }) }) +describe('weapon-only example', () => { + const data: TagMapNodeEntries = [ + ...weaponData(rawData[1].weapon as IWeapon), + ...conditionalData(rawData[1].conditionals), + ], + calc = new Calculator(keys, values, compileTagMapValues(keys, data)) + + const self = convert(selfTag, { et: 'self' }) + + test('retrieve formulas in a listing', () => { + const listing = calc.listFormulas(self.listing.specialized) + expect(listing.length).toEqual(3) + }) + test('calculate formulas in a listing', () => { + // Some listings require character data (e.g., `formulas` listing) and will crash if used + const listing = calc.listFormulas(self.listing.specialized) + + expect(calc.compute(listing[0]).val).toBeCloseTo(337.96) // atk + expect(calc.compute(listing[1]).val).toBeCloseTo(0.458) // hp_ + expect(calc.compute(listing[2]).val).toBeCloseTo(0.3) // refinement hp_ + }) +}) From 467184a96adbde15a3654f868169080db78b62e6 Mon Sep 17 00:00:00 2001 From: lantua <16190491+lantua@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:10:01 -0500 Subject: [PATCH 06/17] Update `gi-formula` example (#1393) * Minor bug fix * Microscopic bug fix * Add weapon-only example * Add some comment * Replace `Calculator` constucture with more a appropriate function call * - Default `isActive` to 0 - Set `isActive` if `teamData` is used * Add test --- libs/gi-formula/src/data/util/tag.ts | 2 +- libs/gi-formula/src/example.test.ts | 8 +++++--- libs/gi-formula/src/util.ts | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/gi-formula/src/data/util/tag.ts b/libs/gi-formula/src/data/util/tag.ts index d887cd3711..4686437cab 100644 --- a/libs/gi-formula/src/data/util/tag.ts +++ b/libs/gi-formula/src/data/util/tag.ts @@ -103,7 +103,7 @@ export const selfTag = { }, weapon: { lvl: iso, refinement: iso, ascension: iso }, common: { - isActive: iso, + isActive: isoSum, weaponType: iso, critMode: fixed, cappedCritRate_: iso, diff --git a/libs/gi-formula/src/example.test.ts b/libs/gi-formula/src/example.test.ts index 58027f1401..2d63c581cf 100644 --- a/libs/gi-formula/src/example.test.ts +++ b/libs/gi-formula/src/example.test.ts @@ -7,7 +7,6 @@ import { detach, flatten, } from '@genshin-optimizer/pando' -import { Calculator } from './calculator' import { keys, values } from './data' import type { Tag, TagMapNodeEntries } from './data/util' import { @@ -28,6 +27,7 @@ import { weaponData, withMember, } from './util' +import { genshinCalculatorWithEntries } from './index' // This test acts as an example usage. It's mostly sufficient to test that the code // doesn't crash. Any test for correct values should go to `correctness` tests. @@ -65,7 +65,7 @@ describe('example', () => { enemyDebuff.common.preRes.add(0.1), selfBuff.common.critMode.add('avg'), ], - calc = new Calculator(keys, values, compileTagMapValues(keys, data)) + calc = genshinCalculatorWithEntries(data) const member0 = convert(selfTag, { member: 'member0', et: 'self' }) const member1 = convert(selfTag, { member: 'member1', et: 'self' }) @@ -81,6 +81,8 @@ describe('example', () => { } }) test('calculate stats', () => { + expect(calc.compute(member0.common.isActive).val).toBe(1) + expect(calc.compute(member1.common.isActive).val).toBe(0) expect(calc.compute(member1.final.hp).val).toBeCloseTo(9479.7, 1) expect(calc.compute(member0.final.atk).val).toBeCloseTo(346.21, 2) expect(calc.compute(member0.final.def).val).toBeCloseTo(124.15, 2) @@ -268,7 +270,7 @@ describe('weapon-only example', () => { ...weaponData(rawData[1].weapon as IWeapon), ...conditionalData(rawData[1].conditionals), ], - calc = new Calculator(keys, values, compileTagMapValues(keys, data)) + calc = genshinCalculatorWithEntries(data) const self = convert(selfTag, { et: 'self' }) diff --git a/libs/gi-formula/src/util.ts b/libs/gi-formula/src/util.ts index fc570c6c2c..492d67b42e 100644 --- a/libs/gi-formula/src/util.ts +++ b/libs/gi-formula/src/util.ts @@ -123,6 +123,9 @@ export function teamData( entry.reread(active.withTag({ dst, member: src })) ) }), + activeMembers.map((member) => + selfBuff.common.isActive.withTag({ member }).add(1) + ), // Team Buff members.flatMap((dst) => { const entry = self.with('member', dst) From 37ceb6a5d27280ea4ec1ec39773219bfcde765c8 Mon Sep 17 00:00:00 2001 From: frzyc Date: Thu, 28 Dec 2023 14:24:45 -0500 Subject: [PATCH 07/17] use exact mainstat value for calculations (#1395) --- apps/frontend/src/app/Formula/api.tsx | 14 +++++++------- libs/gi-util/src/artifact/artifact.ts | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/frontend/src/app/Formula/api.tsx b/apps/frontend/src/app/Formula/api.tsx index 6fe77c8049..134e5678be 100644 --- a/apps/frontend/src/app/Formula/api.tsx +++ b/apps/frontend/src/app/Formula/api.tsx @@ -6,7 +6,7 @@ import type { SubstatKey, } from '@genshin-optimizer/consts' import { allElementWithPhyKeys } from '@genshin-optimizer/consts' -import { getMainStatDisplayValue } from '@genshin-optimizer/gi-util' +import { getMainStatValue } from '@genshin-optimizer/gi-util' import { crawlObject, layeredAssignment, @@ -66,7 +66,7 @@ function dataObjForArtifact( art: ICachedArtifact, mainStatAssumptionLevel = 0 ): Data { - const mainStatVal = getMainStatDisplayValue( + const mainStatVal = getMainStatValue( art.mainStatKey, art.rarity, Math.max(Math.min(mainStatAssumptionLevel, art.rarity * 4), art.level) @@ -428,15 +428,15 @@ function compareInternal(data1: any | undefined, data2: any | undefined): any { } } -export type { NodeDisplay, UIData } export { + compareDisplayUIData, + compareTeamBuffUIData, + computeUIData, dataObjForArtifact, dataObjForCharacter, dataObjForWeapon, - mergeData, - computeUIData, inferInfoMut, + mergeData, uiDataForTeam, - compareTeamBuffUIData, - compareDisplayUIData, } +export type { NodeDisplay, UIData } diff --git a/libs/gi-util/src/artifact/artifact.ts b/libs/gi-util/src/artifact/artifact.ts index 7e261fbb80..58d5556f21 100644 --- a/libs/gi-util/src/artifact/artifact.ts +++ b/libs/gi-util/src/artifact/artifact.ts @@ -93,6 +93,21 @@ export function getSubstatValue( return toPercent(value, substatKey) } +/** + * Raw number from the datamine. + * @param rarity + * @param statKey + * @param level + * @returns + */ +export function getMainStatValue( + statKey: MainStatKey, + rarity: RarityKey, + level: number +) { + return allStats.art.main[rarity][statKey][level] +} + /** * NOTE: this gives the toPercent value of the main stat * @param rarity @@ -113,7 +128,8 @@ export function getMainStatDisplayValue( rarity: RarityKey, level: number ): number { - return getMainStatDisplayValues(rarity, key)[level] + const val = getMainStatValue(key, rarity, level) + return key === 'eleMas' ? Math.round(val) : toPercent(val, key) } export function getMainStatDisplayStr( From 6345011eef1e3494155a586030db8adf2c333004 Mon Sep 17 00:00:00 2001 From: eeeqeee <103794572+eeeqeee@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:29:30 -0500 Subject: [PATCH 08/17] Convert DMs to submodules (#1386) * convert to submodule * eslint pls * shallow by default * prettierignore * nx caching --- .github/workflows/ci.yml | 3 ++ .github/workflows/deploy-frontend.yml | 1 + .github/workflows/new-release.yml | 2 + .gitmodules | 8 ++++ .nxignore | 2 + .prettierignore | 1 + libs/dm/.eslintrc.json | 2 +- libs/dm/.gitignore | 1 - libs/dm/GenshinData | 1 + libs/dm/project.json | 6 +-- .../src/executors/sync-repo/executor.spec.ts | 45 ------------------- .../src/executors/sync-repo/executor.ts | 23 +++------- .../src/executors/sync-repo/schema.d.ts | 2 - .../src/executors/sync-repo/schema.json | 8 ---- libs/sr-dm/.eslintrc.json | 2 +- libs/sr-dm/.gitignore | 1 - libs/sr-dm/StarRailData | 1 + libs/sr-dm/project.json | 6 +-- package.json | 2 +- 19 files changed, 32 insertions(+), 85 deletions(-) create mode 100644 .gitmodules create mode 160000 libs/dm/GenshinData delete mode 100644 libs/plugin/src/executors/sync-repo/executor.spec.ts create mode 160000 libs/sr-dm/StarRailData diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83cc86a374..81a5413473 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false + submodules: true - uses: actions/setup-node@v3 with: node-version: 18 @@ -34,6 +35,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false + submodules: true - uses: actions/setup-node@v3 with: node-version: 18 @@ -52,6 +54,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false + submodules: true - uses: actions/setup-node@v3 with: node-version: 18 diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml index e2314c1201..9699317fb1 100644 --- a/.github/workflows/deploy-frontend.yml +++ b/.github/workflows/deploy-frontend.yml @@ -61,6 +61,7 @@ jobs: with: repository: ${{ inputs.repo_full_name }} ref: ${{ inputs.ref }} + submodules: true - uses: actions/setup-node@v3 with: node-version: 18 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 9322c132cc..5c42d90aec 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + submodules: true - uses: actions/setup-node@v3 with: node-version: 18 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..72326972a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "libs/dm/GenshinData"] + path = libs/dm/GenshinData + url = https://gitlab.com/Dimbreath/AnimeGameData.git + shallow = true +[submodule "libs/sr-dm/StarRailData"] + path = libs/sr-dm/StarRailData + url = https://github.com/Dimbreath/StarRailData.git + shallow = true diff --git a/.nxignore b/.nxignore index ee3110726c..fb78a04d0d 100644 --- a/.nxignore +++ b/.nxignore @@ -1,2 +1,4 @@ +libs/sr-dm/StarRailData !libs/dm/GenshinData.hash +libs/dm/GenshinData !libs/sr-dm/StarRailData.hash diff --git a/.prettierignore b/.prettierignore index f6e48d1fe8..5f08e61f1f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ /.yarn /libs/dm/GenshinData +/libs/sr-dm/StarRailData *_gen.json /libs/gi-stats/Data diff --git a/libs/dm/.eslintrc.json b/libs/dm/.eslintrc.json index 9d9c0db55b..99880fe204 100644 --- a/libs/dm/.eslintrc.json +++ b/libs/dm/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "GenshinData/"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], diff --git a/libs/dm/.gitignore b/libs/dm/.gitignore index a6d99446ff..5cae7b8ff2 100644 --- a/libs/dm/.gitignore +++ b/libs/dm/.gitignore @@ -1,3 +1,2 @@ -GenshinData GenshinData.hash Texture2D diff --git a/libs/dm/GenshinData b/libs/dm/GenshinData new file mode 160000 index 0000000000..8c799bf156 --- /dev/null +++ b/libs/dm/GenshinData @@ -0,0 +1 @@ +Subproject commit 8c799bf156bd08158f411bad8e375d4b255b25ee diff --git a/libs/dm/project.json b/libs/dm/project.json index e8f5dc5f44..6fe4ff3e96 100644 --- a/libs/dm/project.json +++ b/libs/dm/project.json @@ -6,13 +6,11 @@ "targets": { "load-dm": { "options": { - "repoUrl": "https://gitlab.com/Dimbreath/AnimeGameData.git", - "outputPath": "libs/dm/GenshinData", - "branch": "origin/main" + "outputPath": "libs/dm/GenshinData" }, "inputs": [ { - "runtime": "git ls-remote -q https://gitlab.com/Dimbreath/AnimeGameData.git HEAD" + "runtime": "git ls-tree --object-only HEAD libs/dm/GenshinData" } ] }, diff --git a/libs/plugin/src/executors/sync-repo/executor.spec.ts b/libs/plugin/src/executors/sync-repo/executor.spec.ts deleted file mode 100644 index 7dd27c402c..0000000000 --- a/libs/plugin/src/executors/sync-repo/executor.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import executor, { getLocalRepoHash } from './executor' -import * as path from 'path' -import * as fs from 'fs' - -describe('SyncRepo Executor', () => { - it('can run', async () => { - const repoUrl = 'https://github.com/chrislgarry/Apollo-11/' - const outputPath = path.join(__dirname, 'TestDB') - const hashPath = path.join(__dirname, 'TestDB.hash') - const branch = 'origin/master' - - if (fs.existsSync(outputPath)) - fs.rmSync(outputPath, { recursive: true, force: true }) - if (fs.existsSync(hashPath)) fs.unlinkSync(hashPath) - - const output1 = await executor({ - repoUrl, - outputPath, - branch, - prefixPath: false, - }) - - // Clone - expect(output1.success).toBe(true) - expect(fs.existsSync(outputPath)).toBe(true) - expect(fs.existsSync(hashPath)).toBe(true) - expect(getLocalRepoHash(outputPath)).toEqual(`${fs.readFileSync(hashPath)}`) - - // So much fun we try it again (Fetch) - const output2 = await executor({ - repoUrl, - outputPath, - branch, - prefixPath: false, - }) - expect(output2.success).toBe(true) - expect(fs.existsSync(outputPath)).toBe(true) - expect(fs.existsSync(hashPath)).toBe(true) - expect(getLocalRepoHash(outputPath)).toEqual(`${fs.readFileSync(hashPath)}`) - - // Clean up after oneself - fs.rmSync(outputPath, { recursive: true, force: true }) - fs.unlinkSync(hashPath) - }) -}) diff --git a/libs/plugin/src/executors/sync-repo/executor.ts b/libs/plugin/src/executors/sync-repo/executor.ts index 05e0d41336..b86326bc35 100644 --- a/libs/plugin/src/executors/sync-repo/executor.ts +++ b/libs/plugin/src/executors/sync-repo/executor.ts @@ -7,14 +7,9 @@ import * as path from 'path' export default async function runExecutor( options: SyncRepoExecutorSchema ): Promise<{ success: boolean }> { - const { - outputPath, - repoUrl: url, - prefixPath: prefix = true, - branch, - } = options + const { outputPath, prefixPath: prefix = true } = options const cwd = prefix ? path.join(workspaceRoot, outputPath) : outputPath - const remoteHash = getRemoteRepoHash(url) + const remoteHash = getRemoteRepoHash(cwd) const name = path.basename(cwd) console.log( @@ -23,18 +18,12 @@ Caution: if this is part of nx cache replay, no git command is actually executed.` + '\n ' ) - if (fs.existsSync(cwd)) { + { // Fetch & reset const localHash = getLocalRepoHash(cwd) if (remoteHash !== localHash) { - execSync(`git fetch --depth 1`, { cwd }) - execSync(`git reset --hard ${branch}`, { cwd }) + execSync(`git submodule update ${cwd}`) } else console.log('Repo already existed with the latest commit') - } else { - // Clone - const parent = path.dirname(cwd) - fs.mkdirSync(parent, { recursive: true }) - execSync(`git clone ${url} --depth 1 ${name}`, { cwd: parent }) } // Compute hash @@ -48,5 +37,5 @@ Caution: if this is part of nx cache replay, export const getLocalRepoHash = (cwd: string): string => `${execSync(`git rev-parse HEAD`, { cwd })}`.trimEnd() -export const getRemoteRepoHash = (url: string): string => - `${execSync(`git ls-remote ${url} HEAD`)}`.replace(/\s+HEAD\s*$/, '') +export const getRemoteRepoHash = (cwd: string): string => + `${execSync(`git ls-tree --object-only HEAD ${cwd}`)}`.trimEnd() diff --git a/libs/plugin/src/executors/sync-repo/schema.d.ts b/libs/plugin/src/executors/sync-repo/schema.d.ts index c0f57dd67f..168b5896eb 100644 --- a/libs/plugin/src/executors/sync-repo/schema.d.ts +++ b/libs/plugin/src/executors/sync-repo/schema.d.ts @@ -1,6 +1,4 @@ export interface SyncRepoExecutorSchema { - repoUrl: string outputPath: string - branch: string prefixPath?: boolean } diff --git a/libs/plugin/src/executors/sync-repo/schema.json b/libs/plugin/src/executors/sync-repo/schema.json index 0d9577ada2..ff9fcd8423 100644 --- a/libs/plugin/src/executors/sync-repo/schema.json +++ b/libs/plugin/src/executors/sync-repo/schema.json @@ -5,18 +5,10 @@ "description": "", "type": "object", "properties": { - "repoUrl": { - "type": "string", - "description": "Git repo URL" - }, "outputPath": { "type": "string", "description": "Local repo path (including the repo name)" }, - "branch": { - "type": "string", - "description": "Branch to sync (e.g. origin/master)" - }, "prefixPath": { "type": "boolean", "description": "Whether to prepend `localPath` with `{workspaceRoot}`. Default: true" diff --git a/libs/sr-dm/.eslintrc.json b/libs/sr-dm/.eslintrc.json index 9d9c0db55b..6cc7c44f06 100644 --- a/libs/sr-dm/.eslintrc.json +++ b/libs/sr-dm/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "StarRailData/"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], diff --git a/libs/sr-dm/.gitignore b/libs/sr-dm/.gitignore index 5119bba576..635ddfb058 100644 --- a/libs/sr-dm/.gitignore +++ b/libs/sr-dm/.gitignore @@ -1,3 +1,2 @@ -StarRailData StarRailData.hash assets diff --git a/libs/sr-dm/StarRailData b/libs/sr-dm/StarRailData new file mode 160000 index 0000000000..267db9b8cc --- /dev/null +++ b/libs/sr-dm/StarRailData @@ -0,0 +1 @@ +Subproject commit 267db9b8cc44face0f376075f0828c5e1dd20bff diff --git a/libs/sr-dm/project.json b/libs/sr-dm/project.json index 8abffb625a..530bd28653 100644 --- a/libs/sr-dm/project.json +++ b/libs/sr-dm/project.json @@ -6,13 +6,11 @@ "targets": { "load-dm": { "options": { - "repoUrl": "https://github.com/Dimbreath/StarRailData.git", - "outputPath": "libs/sr-dm/StarRailData", - "branch": "origin/master" + "outputPath": "libs/sr-dm/StarRailData" }, "inputs": [ { - "runtime": "git ls-remote -q https://github.com/Dimbreath/StarRailData.git HEAD" + "runtime": "git ls-tree --object-only HEAD libs/sr-dm/StarRailData" } ] }, diff --git a/package.json b/package.json index 139347be61..1fd31b4694 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "frontend": "nx serve frontend", "build-all": "nx run-many -t build", "mini-ci": "nx format:write && nx affected -t test,lint --max-warnings=0", - "reload-dm": "nx run-many -t load-dm --skip-nx-cache" + "reload-dm": "git submodule update --init" }, "devDependencies": { "@babel/core": "^7.14.5", From 1601b6598f43f63ed45512abf0455bdda7d20b19 Mon Sep 17 00:00:00 2001 From: Van Nguyen <36019388+nguyentvan7@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:16:14 -0700 Subject: [PATCH 09/17] Update char-cards README.md (#1388) * Update char-cards README.md * Format --- libs/char-cards/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/char-cards/README.md b/libs/char-cards/README.md index fb2abeceb2..aa6f9d1d51 100644 --- a/libs/char-cards/README.md +++ b/libs/char-cards/README.md @@ -3,3 +3,11 @@ This library was generated with [Nx](https://nx.dev). This is a library for the Character cards used by Genshin Optimizer. The cards needs to be manually added because it comes from their drip marketing on twitter. + +## Adding new cards + +1. Go to Twitter and search for `from:GenshinImpact `. +2. Download the character card from the drip marketing, such as https://twitter.com/GenshinImpact/status/1736687861444001829. You might need to rename the extension to `.jpg` or `.jpeg` +3. Resize it with your tool of choice to 350x700. Van uses [PowerToys Image Resizer](https://learn.microsoft.com/en-us/windows/powertoys/image-resizer). +4. Add the card to [./src/](https://github.com/frzyc/genshin-optimizer/tree/master/libs/char-cards/src). +5. Update [./src/index.ts](https://github.com/frzyc/genshin-optimizer/blob/master/libs/char-cards/src/index.ts) with the new card name, keeping it alphabetically sorted. From 33cdc01a22942f261c487cc6ea3a0e9262317234 Mon Sep 17 00:00:00 2001 From: Van Nguyen <36019388+nguyentvan7@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:16:35 -0700 Subject: [PATCH 10/17] Add SRO database managers (#1387) * Add sr-util * Add initial SRO Database manager code * Add some UI * Fix build * Fix build pt2 * Fix tsconfig discrepancy * Print actual DB contents * Add JSON import in UI (backend side broken) * Remove log * Fix to import * Fix to export * Add download button * Fix eslint * Add unit tests * Fix rerender * Fix lint * Fix substat calculation and tests * Update time display * Fix key collisions * Fix more errant keys * Fix rounding for clamp * Fix mainstat rounding and level validation * Fix lint * Move keys to constructor * Remove probability * Trim theme colors --- apps/sr-frontend/.eslintrc.json | 6 +- apps/sr-frontend/src/app/App.tsx | 57 +- apps/sr-frontend/src/app/Character.tsx | 2 +- apps/sr-frontend/src/app/Database.tsx | 150 ++++ apps/sr-frontend/src/app/Theme.tsx | 420 +--------- apps/sr-frontend/tsconfig.json | 3 +- libs/database/src/lib/DBLocalStorage.ts | 25 +- libs/database/src/lib/DBStorage.ts | 10 + libs/database/src/lib/DataManagerBase.ts | 4 +- libs/database/src/lib/SandboxStorage.ts | 29 +- libs/sr-consts/src/character.ts | 10 +- libs/sr-consts/src/lightCone.ts | 2 + libs/sr-consts/src/relic.ts | 44 +- libs/sr-db/project.json | 6 + .../src/Database/DataEntries/DBMetaEntry.ts | 56 ++ libs/sr-db/src/Database/DataEntry.test.ts | 30 + libs/sr-db/src/Database/DataEntry.ts | 25 + libs/sr-db/src/Database/DataManager.test.ts | 31 + libs/sr-db/src/Database/DataManager.ts | 38 + .../Database/DataManagers/BuildResultData.ts | 76 ++ .../Database/DataManagers/BuildSettingData.ts | 226 ++++++ .../src/Database/DataManagers/CharMetaData.ts | 66 ++ .../Database/DataManagers/CharacterData.ts | 390 ++++++++++ .../Database/DataManagers/LightConeData.ts | 261 +++++++ .../src/Database/DataManagers/RelicData.ts | 529 +++++++++++++ libs/sr-db/src/Database/Database.test.ts | 729 ++++++++++++++++++ libs/sr-db/src/Database/Database.ts | 185 +++++ libs/sr-db/src/Database/exim.ts | 53 ++ libs/sr-db/src/Database/index.ts | 1 + libs/sr-db/src/Database/migrate.ts | 60 ++ .../src/{ => Interfaces}/ISroCharacter.ts | 2 +- .../src/{ => Interfaces}/ISroDatabase.ts | 1 - libs/sr-db/src/Interfaces/ISroLightCone.ts | 5 + libs/sr-db/src/Interfaces/ISroRelic.ts | 13 + libs/sr-db/src/Interfaces/index.ts | 4 + libs/sr-db/src/index.ts | 4 +- libs/sr-db/tsconfig.json | 8 +- libs/sr-db/tsconfig.spec.json | 13 + libs/sr-db/vitest.config.ts | 19 + libs/sr-srod/src/IRelic.ts | 4 +- libs/sr-util/.eslintrc.json | 18 + libs/sr-util/README.md | 3 + libs/sr-util/project.json | 16 + libs/sr-util/src/index.ts | 2 + libs/sr-util/src/level.ts | 20 + libs/sr-util/src/relic.ts | 141 ++++ libs/sr-util/tsconfig.json | 19 + libs/sr-util/tsconfig.lib.json | 10 + .../src/components/GeneralAutocomplete.tsx | 2 +- libs/ui-common/tsconfig.json | 4 +- tsconfig.base.json | 1 + 51 files changed, 3389 insertions(+), 444 deletions(-) create mode 100644 apps/sr-frontend/src/app/Database.tsx create mode 100644 libs/sr-db/src/Database/DataEntries/DBMetaEntry.ts create mode 100644 libs/sr-db/src/Database/DataEntry.test.ts create mode 100644 libs/sr-db/src/Database/DataEntry.ts create mode 100644 libs/sr-db/src/Database/DataManager.test.ts create mode 100644 libs/sr-db/src/Database/DataManager.ts create mode 100644 libs/sr-db/src/Database/DataManagers/BuildResultData.ts create mode 100644 libs/sr-db/src/Database/DataManagers/BuildSettingData.ts create mode 100644 libs/sr-db/src/Database/DataManagers/CharMetaData.ts create mode 100644 libs/sr-db/src/Database/DataManagers/CharacterData.ts create mode 100644 libs/sr-db/src/Database/DataManagers/LightConeData.ts create mode 100644 libs/sr-db/src/Database/DataManagers/RelicData.ts create mode 100644 libs/sr-db/src/Database/Database.test.ts create mode 100644 libs/sr-db/src/Database/Database.ts create mode 100644 libs/sr-db/src/Database/exim.ts create mode 100644 libs/sr-db/src/Database/index.ts create mode 100644 libs/sr-db/src/Database/migrate.ts rename libs/sr-db/src/{ => Interfaces}/ISroCharacter.ts (96%) rename libs/sr-db/src/{ => Interfaces}/ISroDatabase.ts (93%) create mode 100644 libs/sr-db/src/Interfaces/ISroLightCone.ts create mode 100644 libs/sr-db/src/Interfaces/ISroRelic.ts create mode 100644 libs/sr-db/src/Interfaces/index.ts create mode 100644 libs/sr-db/tsconfig.spec.json create mode 100644 libs/sr-db/vitest.config.ts create mode 100644 libs/sr-util/.eslintrc.json create mode 100644 libs/sr-util/README.md create mode 100644 libs/sr-util/project.json create mode 100644 libs/sr-util/src/index.ts create mode 100644 libs/sr-util/src/level.ts create mode 100644 libs/sr-util/src/relic.ts create mode 100644 libs/sr-util/tsconfig.json create mode 100644 libs/sr-util/tsconfig.lib.json diff --git a/apps/sr-frontend/.eslintrc.json b/apps/sr-frontend/.eslintrc.json index 9d9c0db55b..e979009575 100644 --- a/apps/sr-frontend/.eslintrc.json +++ b/apps/sr-frontend/.eslintrc.json @@ -1,10 +1,12 @@ { - "extends": ["../../.eslintrc.json"], + "extends": ["plugin:@nx/react", "../../.eslintrc.json"], "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} + "rules": { + "@typescript-eslint/consistent-type-imports": "error" + } }, { "files": ["*.ts", "*.tsx"], diff --git a/apps/sr-frontend/src/app/App.tsx b/apps/sr-frontend/src/app/App.tsx index c77b1aac40..f8404e3860 100644 --- a/apps/sr-frontend/src/app/App.tsx +++ b/apps/sr-frontend/src/app/App.tsx @@ -1,14 +1,61 @@ -import { CssBaseline, StyledEngineProvider, ThemeProvider } from '@mui/material' -import React from 'react' -import { theme } from './Theme' +import { DBLocalStorage, SandboxStorage } from '@genshin-optimizer/database' +import type { DatabaseContextObj } from '@genshin-optimizer/sr-db' +import { DatabaseContext, SroDatabase } from '@genshin-optimizer/sr-db' +import { + CssBaseline, + Stack, + StyledEngineProvider, + ThemeProvider, +} from '@mui/material' +import { useCallback, useMemo, useState } from 'react' import Character from './Character' +import Database from './Database' +import { theme } from './Theme' + export default function App() { + const dbIndex = parseInt(localStorage.getItem('sro_dbIndex') || '1') + const [databases, setDatabases] = useState(() => { + localStorage.removeItem('SRONewTabDetection') + localStorage.setItem('SRONewTabDetection', 'debug') + return ([1, 2, 3, 4] as const).map((index) => { + if (index === dbIndex) { + return new SroDatabase(index, new DBLocalStorage(localStorage, 'sro')) + } else { + const dbName = `sro_extraDatabase_${index}` + const eDB = localStorage.getItem(dbName) + const dbObj = eDB ? JSON.parse(eDB) : {} + const db = new SroDatabase(index, new SandboxStorage(dbObj, 'sro')) + db.toExtraLocalDB() + return db + } + }) + }) + const setDatabase = useCallback( + (index: number, db: SroDatabase) => { + const dbs = [...databases] + dbs[index] = db + setDatabases(dbs) + }, + [databases, setDatabases] + ) + + const database = databases[dbIndex - 1] + const dbContextObj: DatabaseContextObj = useMemo( + () => ({ databases, setDatabases, database, setDatabase }), + [databases, setDatabases, database, setDatabase] + ) + return ( {/* https://mui.com/guides/interoperability/#css-injection-order-2 */} - - + + + + + + + ) diff --git a/apps/sr-frontend/src/app/Character.tsx b/apps/sr-frontend/src/app/Character.tsx index 3b9444ccb4..ade1af8ea5 100644 --- a/apps/sr-frontend/src/app/Character.tsx +++ b/apps/sr-frontend/src/app/Character.tsx @@ -17,7 +17,7 @@ import { Typography, } from '@mui/material' import { Container } from '@mui/system' -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' export default function Character() { const [charKey, setcharKey] = useState('March7th') diff --git a/apps/sr-frontend/src/app/Database.tsx b/apps/sr-frontend/src/app/Database.tsx new file mode 100644 index 0000000000..72f5550c47 --- /dev/null +++ b/apps/sr-frontend/src/app/Database.tsx @@ -0,0 +1,150 @@ +import { SandboxStorage } from '@genshin-optimizer/database' +import { DatabaseContext, SroDatabase } from '@genshin-optimizer/sr-db' +import { CardThemed, DropdownButton } from '@genshin-optimizer/ui-common' +import { range } from '@genshin-optimizer/util' +import { + Button, + CardContent, + Container, + Grid, + MenuItem, + Typography, +} from '@mui/material' +import type { ChangeEvent } from 'react' +import { useCallback, useContext, useEffect, useMemo, useState } from 'react' + +export default function Database() { + const { + database: mainDB, + databases, + setDatabase, + } = useContext(DatabaseContext) + const [index, setIndex] = useState(0) + const database = databases[index] + const current = database === mainDB + const [data, setData] = useState('') + // Need to update the dbMeta when database changes + const [{ name, lastEdit }, setDBMeta] = useState(database.dbMeta.get()) + useEffect( + () => database.dbMeta.follow((_r, dbMeta) => setDBMeta(dbMeta)), + [database] + ) + useEffect(() => setDBMeta(database.dbMeta.get()), [database]) + + const { importedDatabase } = + useMemo(() => { + if (!data) return undefined + let parsed: any + try { + parsed = JSON.parse(data) + console.log(parsed) + if (typeof parsed !== 'object') { + return undefined + } + } catch (e) { + return undefined + } + // Figure out the file format + if (parsed.format === 'SROD' || parsed.format === 'SRO') { + // Parse as SROD format + const copyStorage = new SandboxStorage(undefined, 'sro') + copyStorage.copyFrom(database.storage) + const importedDatabase = new SroDatabase( + (index + 1) as 1 | 2 | 3 | 4, + copyStorage + ) + const importResult = importedDatabase.importSROD(parsed, false, false) + if (!importResult) { + return undefined + } + + return { importResult, importedDatabase } + } + return undefined + }, [data, database, index]) ?? {} + + const onUpload = async (e: ChangeEvent) => { + const file = (e.target.files ?? [''])[0] + if (typeof file === 'string') return + const reader = new FileReader() + reader.onload = () => setData(reader.result as string) + reader.readAsText(file) + } + + const download = useCallback(() => { + const date = new Date() + const dateStr = date + .toISOString() + .split('.')[0] + .replace('T', '_') + .replaceAll(':', '-') + const JSONStr = JSON.stringify(database.exportSROD()) + const filename = `${name.trim().replaceAll(' ', '_')}_${dateStr}.json` + const contentType = 'application/json;charset=utf-8' + const a = document.createElement('a') + a.download = filename + a.href = `data:${contentType},${encodeURIComponent(JSONStr)}` + a.target = '_blank' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + }, [database, name]) + + const replaceDb = useCallback(() => { + if (!importedDatabase) return + importedDatabase.swapStorage(database) + setDatabase(index, importedDatabase) + importedDatabase.toExtraLocalDB() + }, [database, importedDatabase, index, setDatabase]) + + const swapDb = useCallback(() => { + if (current) return + mainDB.toExtraLocalDB() + database.swapStorage(mainDB) + setDatabase(index, database) + }, [current, database, index, mainDB, setDatabase]) + + const clearDb = useCallback(() => { + database.clear() + database.toExtraLocalDB() + }, [database]) + + return ( + + + + Database + + + {range(0, 3).map((i) => ( + setIndex(i)}>{`${ + mainDB === databases[i] ? '* ' : '' + }Database ${i + 1}`} + ))} + + + + + + + + + Last Edit: {new Date(lastEdit).toLocaleString()} + + + {JSON.stringify(database.exportSROD(), undefined, 2)} + + + + + ) +} diff --git a/apps/sr-frontend/src/app/Theme.tsx b/apps/sr-frontend/src/app/Theme.tsx index 39d80df7fb..aea1c5c90e 100644 --- a/apps/sr-frontend/src/app/Theme.tsx +++ b/apps/sr-frontend/src/app/Theme.tsx @@ -1,95 +1,24 @@ -import { createTheme, darkScrollbar } from '@mui/material' +import { createTheme } from '@mui/material' + +import { theme as commonTheme } from '@genshin-optimizer/ui-common' declare module '@mui/material/styles' { interface Palette { - warning: Palette['primary'] - contentDark: Palette['primary'] - contentDarker: Palette['primary'] - contentLight: Palette['primary'] roll1: Palette['primary'] roll2: Palette['primary'] roll3: Palette['primary'] roll4: Palette['primary'] roll5: Palette['primary'] roll6: Palette['primary'] - geo: Palette['primary'] - dendro: Palette['primary'] - pyro: Palette['primary'] - hydro: Palette['primary'] - cryo: Palette['primary'] - electro: Palette['primary'] - anemo: Palette['primary'] - physical: Palette['primary'] - vaporize: Palette['primary'] - melt: Palette['primary'] - spread: Palette['primary'] - aggravate: Palette['primary'] - overloaded: Palette['primary'] - superconduct: Palette['primary'] - electrocharged: Palette['primary'] - shattered: Palette['primary'] - swirl: Palette['primary'] - burning: Palette['primary'] - crystallize: Palette['primary'] - heal: Palette['primary'] - bloom: Palette['primary'] - burgeon: Palette['primary'] - hyperbloom: Palette['primary'] - - white: Palette['primary'] - red: Palette['primary'] - - discord: Palette['primary'] - patreon: Palette['primary'] - twitch: Palette['primary'] - twitter: Palette['primary'] - paypal: Palette['primary'] - keqing: Palette['primary'] } interface PaletteOptions { - warning?: PaletteOptions['primary'] - contentDark?: PaletteOptions['primary'] - contentDarker?: PaletteOptions['primary'] - contentLight?: PaletteOptions['primary'] roll1?: PaletteOptions['primary'] roll2?: PaletteOptions['primary'] roll3?: PaletteOptions['primary'] roll4?: PaletteOptions['primary'] roll5?: PaletteOptions['primary'] roll6?: PaletteOptions['primary'] - geo?: PaletteOptions['primary'] - dendro?: PaletteOptions['primary'] - pyro?: PaletteOptions['primary'] - hydro?: PaletteOptions['primary'] - cryo?: PaletteOptions['primary'] - electro?: PaletteOptions['primary'] - anemo?: PaletteOptions['primary'] - physical?: PaletteOptions['primary'] - vaporize?: PaletteOptions['primary'] - melt?: PaletteOptions['primary'] - spread?: PaletteOptions['primary'] - aggravate?: PaletteOptions['primary'] - overloaded?: PaletteOptions['primary'] - superconduct?: PaletteOptions['primary'] - electrocharged?: PaletteOptions['primary'] - shattered?: PaletteOptions['primary'] - swirl?: PaletteOptions['primary'] - burning?: PaletteOptions['primary'] - crystallize?: PaletteOptions['primary'] heal?: PaletteOptions['primary'] - bloom?: PaletteOptions['primary'] - burgeon?: PaletteOptions['primary'] - hyperbloom?: PaletteOptions['primary'] - - white?: PaletteOptions['primary'] - red?: PaletteOptions['primary'] - - discord?: PaletteOptions['primary'] - patreon?: PaletteOptions['primary'] - twitch?: PaletteOptions['primary'] - twitter?: PaletteOptions['primary'] - paypal?: PaletteOptions['primary'] - keqing?: PaletteOptions['primary'] } } @@ -102,394 +31,69 @@ declare module '@mui/material/Button' { roll4: true roll5: true roll6: true - geo: true - dendro: true - pyro: true - hydro: true - cryo: true - electro: true - anemo: true - physical: true - vaporize: true - melt: true - spread: true - aggravate: true - overloaded: true - superconduct: true - electrocharged: true - shattered: true - swirl: true - burning: true - crystallize: true heal: true - bloom: true - burgeon: true - hyperbloom: true - - white: true - red: true - - discord: true - patreon: true - twitch: true - twitter: true - paypal: true - keqing: true } } declare module '@mui/material/Chip' { interface ChipPropsColorOverrides { - warning: true roll1: true roll2: true roll3: true roll4: true roll5: true roll6: true - geo: true - dendro: true - pyro: true - hydro: true - cryo: true - electro: true - anemo: true - physical: true - vaporize: true - melt: true - spread: true - aggravate: true - overloaded: true - superconduct: true - electrocharged: true - shattered: true - swirl: true - burning: true - crystallize: true heal: true - bloom: true - burgeon: true - hyperbloom: true } } declare module '@mui/material/InputBase' { interface InputBasePropsColorOverrides { - warning: true roll1: true roll2: true roll3: true roll4: true roll5: true roll6: true - geo: true - dendro: true - pyro: true - hydro: true - cryo: true - electro: true - anemo: true - physical: true - vaporize: true - melt: true } } declare module '@mui/material/SvgIcon' { interface SvgIconPropsColorOverrides { - geo: true - dendro: true - pyro: true - hydro: true - cryo: true - electro: true - anemo: true - physical: true - vaporize: true - melt: true - spread: true - aggravate: true - overloaded: true - superconduct: true - electrocharged: true - shattered: true - swirl: true - burning: true - crystallize: true heal: true - bloom: true - burgeon: true - hyperbloom: true } } -const defaultTheme = createTheme({ - palette: { - mode: `dark`, - }, -}) export const theme = createTheme({ + ...commonTheme, palette: { - mode: 'dark', - primary: defaultTheme.palette.augmentColor({ - color: { main: '#1e78c8' }, - name: 'primary', - }), - secondary: defaultTheme.palette.augmentColor({ - color: { main: '#6c757d' }, - name: 'secondary', - }), - success: defaultTheme.palette.augmentColor({ - color: { main: '#46a046' }, - name: 'success', - }), - warning: defaultTheme.palette.augmentColor({ - color: { main: `#ffc107` }, - name: 'warning', - }), - error: defaultTheme.palette.augmentColor({ - color: { main: `#c83c3c` }, - name: 'error', - }), - background: { - default: '#0C1020', - paper: '#0C1020', - }, - info: defaultTheme.palette.augmentColor({ - color: { main: '#17a2b8' }, - name: 'info', - }), - text: { - primary: 'rgba(255,255,255,0.9)', - }, - contentDark: defaultTheme.palette.augmentColor({ - color: { main: '#1b263b' }, - name: 'contentDark', - }), - contentDarker: defaultTheme.palette.augmentColor({ - color: { main: '#172032' }, - name: 'contentDarker', - }), - contentLight: defaultTheme.palette.augmentColor({ - color: { main: '#2a364d' }, - name: 'contentLight', - }), - roll1: defaultTheme.palette.augmentColor({ + ...commonTheme.palette, + roll1: commonTheme.palette.augmentColor({ color: { main: '#a3a7a9' }, name: 'roll1', }), - roll2: defaultTheme.palette.augmentColor({ + roll2: commonTheme.palette.augmentColor({ color: { main: '#6fa376' }, name: 'roll2', }), - roll3: defaultTheme.palette.augmentColor({ + roll3: commonTheme.palette.augmentColor({ color: { main: '#8eea83' }, name: 'roll3', }), - roll4: defaultTheme.palette.augmentColor({ + roll4: commonTheme.palette.augmentColor({ color: { main: '#31e09d' }, name: 'roll4', }), - roll5: defaultTheme.palette.augmentColor({ + roll5: commonTheme.palette.augmentColor({ color: { main: '#27bbe4' }, name: 'roll5', }), - roll6: defaultTheme.palette.augmentColor({ + roll6: commonTheme.palette.augmentColor({ color: { main: '#de79f0' }, name: 'roll6', }), - geo: defaultTheme.palette.augmentColor({ - color: { main: '#f8ba4e', contrastText: '#fff' }, - name: 'geo', - }), - dendro: defaultTheme.palette.augmentColor({ - color: { main: '#a5c83b', contrastText: '#fff' }, - name: 'dendro', - }), - pyro: defaultTheme.palette.augmentColor({ - color: { main: '#bf2818' }, - name: 'pyro', - }), - hydro: defaultTheme.palette.augmentColor({ - color: { main: '#2f63d4' }, - name: 'hydro', - }), - cryo: defaultTheme.palette.augmentColor({ - color: { main: '#77a2e6', contrastText: '#fff' }, - name: 'cryo', - }), - electro: defaultTheme.palette.augmentColor({ - color: { main: '#b25dcd' }, - name: 'electro', - }), - anemo: defaultTheme.palette.augmentColor({ - color: { main: '#61dbbb', contrastText: '#fff' }, - name: 'anemo', - }), - physical: defaultTheme.palette.augmentColor({ - color: { main: '#aaaaaa' }, - name: 'physical', - }), - vaporize: defaultTheme.palette.augmentColor({ - color: { main: '#ffcb65' }, - name: 'vaporize', - }), - melt: defaultTheme.palette.augmentColor({ - color: { main: '#ffcb65' }, - name: 'melt', - }), - spread: defaultTheme.palette.augmentColor({ - color: { main: '#3bc8a7', contrastText: '#fff' }, - name: 'spread', - }), - aggravate: defaultTheme.palette.augmentColor({ - color: { main: '#3ba0c8', contrastText: '#fff' }, - name: 'aggravate', - }), - overloaded: defaultTheme.palette.augmentColor({ - color: { main: '#ff7e9a' }, - name: 'overloaded', - }), - superconduct: defaultTheme.palette.augmentColor({ - color: { main: '#b7b1ff' }, - name: 'superconduct', - }), - electrocharged: defaultTheme.palette.augmentColor({ - color: { main: '#e299fd' }, - name: 'electrocharged', - }), - shattered: defaultTheme.palette.augmentColor({ - color: { main: '#98fffd' }, - name: 'shattered', - }), - swirl: defaultTheme.palette.augmentColor({ - color: { main: '#66ffcb' }, - name: 'swirl', - }), - burning: defaultTheme.palette.augmentColor({ - color: { main: '#bf2818' }, - name: 'burning', - }), - crystallize: defaultTheme.palette.augmentColor({ - color: { main: '#f8ba4e' }, - name: 'crystallize', - }), - heal: defaultTheme.palette.augmentColor({ + heal: commonTheme.palette.augmentColor({ color: { main: '#c0e86c' }, name: 'heal', }), - bloom: defaultTheme.palette.augmentColor({ - color: { main: '#47c83b', contrastText: '#fff' }, - name: 'bloom', - }), - burgeon: defaultTheme.palette.augmentColor({ - color: { main: '#c8b33b', contrastText: '#fff' }, - name: 'burgeon', - }), - hyperbloom: defaultTheme.palette.augmentColor({ - color: { main: '#3b8dc8', contrastText: '#fff' }, - name: 'hyperbloom', - }), - - white: defaultTheme.palette.augmentColor({ - color: { main: '#FFFFFF' }, - name: 'white', - }), - red: defaultTheme.palette.augmentColor({ - color: { main: '#ff0000' }, - name: 'red', - }), - - discord: defaultTheme.palette.augmentColor({ - color: { main: '#5663F7' }, - name: 'discord', - }), - patreon: defaultTheme.palette.augmentColor({ - color: { main: '#f96854', contrastText: '#ffffff' }, - name: 'patreon', - }), - twitch: defaultTheme.palette.augmentColor({ - color: { main: '#6441a5' }, - name: 'twitch', - }), - twitter: defaultTheme.palette.augmentColor({ - color: { main: '#55acee', contrastText: '#ffffff' }, - name: 'twitter', - }), - paypal: defaultTheme.palette.augmentColor({ - color: { main: '#00457C' }, - name: 'paypal', - }), - keqing: defaultTheme.palette.augmentColor({ - color: { main: '#584862' }, - name: 'keqing', - }), - }, - typography: { - button: { - textTransform: 'none', - }, - }, - components: { - MuiCssBaseline: { - styleOverrides: { - body: defaultTheme.palette.mode === 'dark' ? darkScrollbar() : null, - }, - }, - MuiAppBar: { - defaultProps: { - enableColorOnDark: true, - }, - }, - MuiPaper: { - defaultProps: { - elevation: 0, - }, - }, - MuiButton: { - defaultProps: { - variant: 'contained', - }, - }, - MuiButtonGroup: { - defaultProps: { - variant: 'contained', - }, - }, - MuiList: { - styleOverrides: { - root: { - padding: 0, - marginTop: defaultTheme.spacing(1), - marginBottom: defaultTheme.spacing(1), - }, - }, - }, - MuiTypography: { - styleOverrides: { - root: { - '& ul': { - margin: 0, - paddingLeft: defaultTheme.spacing(3), - }, - }, - }, - }, - MuiCardContent: { - styleOverrides: { - root: { - [defaultTheme.breakpoints.down('sm')]: { - padding: defaultTheme.spacing(1), - '&:last-child': { - paddingBottom: defaultTheme.spacing(1), - }, - }, - [defaultTheme.breakpoints.up('sm')]: { - '&:last-child': { - paddingBottom: defaultTheme.spacing(2), - }, - }, - }, - }, - }, }, }) diff --git a/apps/sr-frontend/tsconfig.json b/apps/sr-frontend/tsconfig.json index 137333e4b7..617479b130 100644 --- a/apps/sr-frontend/tsconfig.json +++ b/apps/sr-frontend/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.json", "files": [], "compilerOptions": { - "jsx": "react", + "jsx": "react-jsx", "target": "ESNext", "useDefineForClassFields": true, "lib": ["ESNext", "DOM"], @@ -15,6 +15,7 @@ "noUnusedParameters": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true, + "exactOptionalPropertyTypes": false, "types": ["vite/client", "vitest"] }, "include": ["src"], diff --git a/libs/database/src/lib/DBLocalStorage.ts b/libs/database/src/lib/DBLocalStorage.ts index 57b369a876..202e507cf5 100644 --- a/libs/database/src/lib/DBLocalStorage.ts +++ b/libs/database/src/lib/DBLocalStorage.ts @@ -1,10 +1,23 @@ -import type { DBStorage } from './DBStorage' +import type { DbIndexKey, DbVersionKey } from './DBStorage' +import { type DBStorage, type StorageType } from './DBStorage' export class DBLocalStorage implements DBStorage { private storage: Storage + dbVersionKey: DbVersionKey + dbIndexKey: DbIndexKey - constructor(storage: Storage) { + constructor(storage: Storage, storageType: StorageType = 'go') { this.storage = storage + switch (storageType) { + case 'go': + this.dbVersionKey = 'db_ver' + this.dbIndexKey = 'dbIndex' + break + case 'sro': + this.dbVersionKey = 'sro_db_ver' + this.dbIndexKey = 'sro_dbIndex' + break + } } get keys(): string[] { @@ -52,15 +65,15 @@ export class DBLocalStorage implements DBStorage { } } getDBVersion(): number { - return parseInt(this.getString('db_ver') ?? '0') + return parseInt(this.getString(this.dbVersionKey) ?? '0') } setDBVersion(version: number): void { - this.setString('db_ver', version.toString()) + this.setString(this.dbVersionKey, version.toString()) } getDBIndex(): 1 | 2 | 3 | 4 { - return parseInt(this.getString('dbIndex') ?? '1') as 1 | 2 | 3 | 4 + return parseInt(this.getString(this.dbIndexKey) ?? '1') as 1 | 2 | 3 | 4 } setDBIndex(ind: 1 | 2 | 3 | 4) { - this.setString('dbIndex', ind.toString()) + this.setString(this.dbIndexKey, ind.toString()) } } diff --git a/libs/database/src/lib/DBStorage.ts b/libs/database/src/lib/DBStorage.ts index 03e3382deb..b399d3d7a2 100644 --- a/libs/database/src/lib/DBStorage.ts +++ b/libs/database/src/lib/DBStorage.ts @@ -1,6 +1,16 @@ +export const dbVersionKeys = ['db_ver', 'sro_db_ver'] as const +export type DbVersionKey = (typeof dbVersionKeys)[number] + +export const dbIndexKeys = ['dbIndex', 'sro_dbIndex'] as const +export type DbIndexKey = (typeof dbIndexKeys)[number] + +export type StorageType = 'go' | 'sro' + export interface DBStorage { keys: string[] entries: [key: string, value: string][] + dbVersionKey: DbVersionKey + dbIndexKey: DbIndexKey get(key: string): any | undefined set(key: string, value: any): void diff --git a/libs/database/src/lib/DataManagerBase.ts b/libs/database/src/lib/DataManagerBase.ts index 4f6b3a0900..92ddf6f4b1 100644 --- a/libs/database/src/lib/DataManagerBase.ts +++ b/libs/database/src/lib/DataManagerBase.ts @@ -53,10 +53,10 @@ export class DataManagerBase< } get keys() { - return Object.keys(this.data) + return Object.keys(this.data) as CacheKey[] } get values() { - return Object.values(this.data) + return Object.values(this.data) as CacheValue[] } get(key: CacheKey | '' | undefined): CacheValue | undefined { return key ? this.data[key] : undefined diff --git a/libs/database/src/lib/SandboxStorage.ts b/libs/database/src/lib/SandboxStorage.ts index c287c005be..48568c718f 100644 --- a/libs/database/src/lib/SandboxStorage.ts +++ b/libs/database/src/lib/SandboxStorage.ts @@ -1,10 +1,27 @@ -import type { DBStorage } from './DBStorage' +import type { + DBStorage, + DbIndexKey, + DbVersionKey, + StorageType, +} from './DBStorage' export class SandboxStorage implements DBStorage { protected storage: Record = {} + dbVersionKey: DbVersionKey + dbIndexKey: DbIndexKey - constructor(obj?: Record) { + constructor(obj?: Record, storageType: StorageType = 'go') { if (obj) this.storage = obj + switch (storageType) { + case 'go': + this.dbVersionKey = 'db_ver' + this.dbIndexKey = 'dbIndex' + break + case 'sro': + this.dbVersionKey = 'sro_db_ver' + this.dbIndexKey = 'sro_dbIndex' + break + } } get keys(): string[] { @@ -52,15 +69,15 @@ export class SandboxStorage implements DBStorage { ) } getDBVersion(): number { - return parseInt(this.getString('db_ver') ?? '0') + return parseInt(this.getString(this.dbVersionKey) ?? '0') } setDBVersion(version: number): void { - this.setString('db_ver', version.toString()) + this.setString(this.dbVersionKey, version.toString()) } getDBIndex(): 1 | 2 | 3 | 4 { - return parseInt(this.getString('dbIndex') ?? '1') as 1 | 2 | 3 | 4 + return parseInt(this.getString(this.dbIndexKey) ?? '1') as 1 | 2 | 3 | 4 } setDBIndex(ind: 1 | 2 | 3 | 4) { - this.setString('dbIndex', ind.toString()) + this.setString(this.dbIndexKey, ind.toString()) } } diff --git a/libs/sr-consts/src/character.ts b/libs/sr-consts/src/character.ts index 667c658f1b..fefbfdfd06 100644 --- a/libs/sr-consts/src/character.ts +++ b/libs/sr-consts/src/character.ts @@ -69,7 +69,7 @@ export type TrailblazerKey = (typeof allTrailblazerKeys)[number] export const allCharacterKeys = [ ...nonTrailblazerCharacterKeys, - ...allTrailblazerGenderedKeys, + ...allTrailblazerKeys, ] as const export type CharacterKey = (typeof allCharacterKeys)[number] @@ -87,3 +87,11 @@ export type BonusAbilityKey = (typeof allBonusAbilityKeys)[number] export const allStatBoostKeys = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const export type StatBoostKey = (typeof allStatBoostKeys)[number] + +export function charKeyToCharLocKey( + charKey: CharacterKey +): CharacterLocationKey { + return charKey.includes('Trailblazer') + ? 'Trailblazer' + : (charKey as CharacterLocationKey) +} diff --git a/libs/sr-consts/src/lightCone.ts b/libs/sr-consts/src/lightCone.ts index 02c870d81d..0b9389b815 100644 --- a/libs/sr-consts/src/lightCone.ts +++ b/libs/sr-consts/src/lightCone.ts @@ -73,3 +73,5 @@ export type LightConeKey = (typeof allLightConeKeys)[number] export const allSuperimposeKeys = [0, 1, 2, 3, 4, 5] as const export type SuperimposeKey = (typeof allSuperimposeKeys)[number] + +export const lightConeMaxLevel = 80 diff --git a/libs/sr-consts/src/relic.ts b/libs/sr-consts/src/relic.ts index d7b00b0687..0066bae8d3 100644 --- a/libs/sr-consts/src/relic.ts +++ b/libs/sr-consts/src/relic.ts @@ -42,7 +42,6 @@ export const allRelicSetKeys = [ ...allRelicCavernSetKeys, ...allRelicPlanarSetKeys, ] as const - export type RelicSetKey = (typeof allRelicSetKeys)[number] export const allRelicSubStatKeys = [ @@ -59,7 +58,6 @@ export const allRelicSubStatKeys = [ 'eff_res_', 'brEff_', ] as const - export type RelicSubStatKey = (typeof allRelicSubStatKeys)[number] export const allRelicMainStatKeys = [ @@ -83,5 +81,45 @@ export const allRelicMainStatKeys = [ 'brEff_', 'enerRegen_', ] as const - export type RelicMainStatKey = (typeof allRelicMainStatKeys)[number] + +export const allRelicRarityKeys = [2, 3, 4, 5] as const +export type RelicRarityKey = (typeof allRelicRarityKeys)[number] + +export const relicMaxLevel: Record = { + 2: 6, + 3: 9, + 4: 12, + 5: 15, +} as const + +export const relicSubstatRollData: Record< + RelicRarityKey, + { low: number; high: number; numUpgrades: number } +> = { + 2: { low: 0, high: 0, numUpgrades: 0 }, + 3: { low: 1, high: 2, numUpgrades: 1 }, + 4: { low: 2, high: 3, numUpgrades: 3 }, + 5: { low: 3, high: 4, numUpgrades: 5 }, +} as const + +export const relicSlotToMainStatKeys: Record = + { + head: ['hp'], + hand: ['atk'], + body: ['hp_', 'atk_', 'def_', 'eff_', 'heal_', 'crit_', 'crit_dmg_'], + feet: ['hp_', 'atk_', 'def_', 'spd'], + sphere: [ + 'hp_', + 'atk_', + 'def_', + 'physical_dmg_', + 'fire_dmg_', + 'ice_dmg_', + 'wind_dmg_', + 'lightning_dmg_', + 'quantum_dmg_', + 'imaginary_dmg_', + ], + rope: ['brEff_', 'enerRegen_'], + } diff --git a/libs/sr-db/project.json b/libs/sr-db/project.json index 74ac1693f9..820e2e56f9 100644 --- a/libs/sr-db/project.json +++ b/libs/sr-db/project.json @@ -10,6 +10,12 @@ "options": { "lintFilePatterns": ["libs/sr-db/**/*.ts"] } + }, + "test": { + "executor": "@nx/vite:test", + "options": { + "config": "libs/sr-db/vitest.config.ts" + } } }, "tags": [] diff --git a/libs/sr-db/src/Database/DataEntries/DBMetaEntry.ts b/libs/sr-db/src/Database/DataEntries/DBMetaEntry.ts new file mode 100644 index 0000000000..10cf5bce7b --- /dev/null +++ b/libs/sr-db/src/Database/DataEntries/DBMetaEntry.ts @@ -0,0 +1,56 @@ +import type { Database } from '@genshin-optimizer/database' +import type { GenderKey } from '@genshin-optimizer/sr-consts' +import { allGenderKeys } from '@genshin-optimizer/sr-consts' +import type { ISrObjectDescription } from '@genshin-optimizer/sr-srod' +import type { ISroDatabase } from '../../Interfaces' +import { DataEntry } from '../DataEntry' +import type { SroDatabase } from '../Database' +import type { ImportResult } from '../exim' + +interface IDBMeta { + name: string + lastEdit: number + gender: GenderKey +} + +function dbMetaInit(database: Database): IDBMeta { + return { + name: `Database ${database.storage.getDBIndex()}`, + lastEdit: 0, + gender: 'F', + } +} + +const storageKey = 'sro_dbMeta' +export class DBMetaEntry extends DataEntry< + typeof storageKey, + typeof storageKey, + IDBMeta, + IDBMeta +> { + constructor(database: SroDatabase) { + super(database, storageKey, dbMetaInit, storageKey) + } + override validate(obj: any): IDBMeta | undefined { + if (typeof obj !== 'object') return undefined + let { name, lastEdit, gender } = obj + if (typeof name !== 'string') + name = `Database ${this.database.storage.getDBIndex()}` + if (typeof lastEdit !== 'number') console.warn('lastEdit INVALID') + if (typeof lastEdit !== 'number') lastEdit = 0 + if (!allGenderKeys.includes(gender)) gender = 'F' + + return { name, lastEdit, gender } as IDBMeta + } + override importSROD( + sroDb: ISroDatabase & ISrObjectDescription, + _result: ImportResult + ): void { + const data = sroDb[this.goKey] + if (data) { + // Don't copy over lastEdit data + const { lastEdit, ...rest } = data as IDBMeta + this.set(rest) + } + } +} diff --git a/libs/sr-db/src/Database/DataEntry.test.ts b/libs/sr-db/src/Database/DataEntry.test.ts new file mode 100644 index 0000000000..8cdcb663bd --- /dev/null +++ b/libs/sr-db/src/Database/DataEntry.test.ts @@ -0,0 +1,30 @@ +import { DBLocalStorage } from '@genshin-optimizer/database' +import { SroDatabase } from './Database' + +const dbStorage = new DBLocalStorage(localStorage, 'sro') +const dbIndex = 1 +let database = new SroDatabase(dbIndex, dbStorage) + +describe('Database', () => { + beforeEach(() => { + dbStorage.clear() + database = new SroDatabase(dbIndex, dbStorage) + }) + test('initialValue', () => { + expect(database.dbMeta.get().gender).toEqual('F') + }) + test('DataEntry.set', () => { + expect(database.dbMeta.get().name).toEqual('Database 1') + + database.dbMeta.set({ name: 'test' }) + expect(database.dbMeta.get().name).toEqual('test') + + database.dbMeta.set((dbMeta) => { + dbMeta.name = `test ${dbMeta.name}` + }) + expect(database.dbMeta.get().name).toEqual('test test') + + database.dbMeta.set(({ name }) => ({ name: `test ${name}` })) + expect(database.dbMeta.get().name).toEqual('test test test') + }) +}) diff --git a/libs/sr-db/src/Database/DataEntry.ts b/libs/sr-db/src/Database/DataEntry.ts new file mode 100644 index 0000000000..a169a04840 --- /dev/null +++ b/libs/sr-db/src/Database/DataEntry.ts @@ -0,0 +1,25 @@ +import { DataEntryBase } from '@genshin-optimizer/database' +import type { ISrObjectDescription } from '@genshin-optimizer/sr-srod' +import type { ISroDatabase } from '../Interfaces' +import type { ImportResult } from './exim' + +export class DataEntry< + Key extends string, + SROKey extends string, + CacheValue, + StorageValue +> extends DataEntryBase { + exportSROD(sroDb: Partial) { + const key = this.goKey.replace('sro_', '') + sroDb[key] = this.data + } + importSROD( + sroDb: ISrObjectDescription & + ISroDatabase & { [k in SROKey]?: Partial | never }, + _result: ImportResult + ) { + const key = this.goKey.replace('sro_', '') + const data = sroDb[key] + if (data) this.set(data) + } +} diff --git a/libs/sr-db/src/Database/DataManager.test.ts b/libs/sr-db/src/Database/DataManager.test.ts new file mode 100644 index 0000000000..9b2fc2f23f --- /dev/null +++ b/libs/sr-db/src/Database/DataManager.test.ts @@ -0,0 +1,31 @@ +import { DBLocalStorage } from '@genshin-optimizer/database' +import { randomizeRelic } from '@genshin-optimizer/sr-util' +import { SroDatabase } from './Database' + +const dbStorage = new DBLocalStorage(localStorage, 'sro') +const dbIndex = 1 +let database = new SroDatabase(dbIndex, dbStorage) + +describe('Database', () => { + beforeEach(() => { + dbStorage.clear() + database = new SroDatabase(dbIndex, dbStorage) + }) + + test('DataManager.set', () => { + const invalid = database.relics.set('INVALID', () => ({ level: 0 })) + expect(invalid).toEqual(false) + expect(database.relics.values.length).toEqual(0) + const id = 'testid' + database.relics.set(id, randomizeRelic({ rarity: 4, level: 0 })) + expect(database.relics.get(id)?.level).toEqual(0) + + database.relics.set(id, (art) => { + art.level = art.level + 3 + }) + expect(database.relics.get(id)?.level).toEqual(3) + + database.relics.set(id, ({ level }) => ({ level: level + 3 })) + expect(database.relics.get(id)?.level).toEqual(6) + }) +}) diff --git a/libs/sr-db/src/Database/DataManager.ts b/libs/sr-db/src/Database/DataManager.ts new file mode 100644 index 0000000000..cd2f85b683 --- /dev/null +++ b/libs/sr-db/src/Database/DataManager.ts @@ -0,0 +1,38 @@ +import type { Database } from '@genshin-optimizer/database' +import { DataManagerBase } from '@genshin-optimizer/database' +import type { ISrObjectDescription } from '@genshin-optimizer/sr-srod' +import type { ISroDatabase } from '../' +import type { ImportResult } from './exim' +export class DataManager< + CacheKey extends string, + DataKey extends string, + CacheValue extends StorageValue, + StorageValue, + DatabaseType extends Database +> extends DataManagerBase< + CacheKey, + DataKey, + CacheValue, + StorageValue, + DatabaseType +> { + exportSROD(sro: Partial) { + const key = this.dataKey.replace('sro_', '') + sro[key] = (Object.entries(this.data) as [CacheKey, CacheValue][]).map( + ([id, value]) => ({ + ...this.deCache(value), + id, + }) + ) + } + importSROD(sro: ISrObjectDescription & ISroDatabase, _result: ImportResult) { + const entries = sro[this.dataKey] + if (entries && Array.isArray(entries)) + entries.forEach((ele) => ele.id && this.set(ele.id, ele)) + } + override get goKeySingle() { + const key = this.dataKey.replace('sro_', '') + if (key.endsWith('s')) return key.slice(0, -1) + return key + } +} diff --git a/libs/sr-db/src/Database/DataManagers/BuildResultData.ts b/libs/sr-db/src/Database/DataManagers/BuildResultData.ts new file mode 100644 index 0000000000..96124663ea --- /dev/null +++ b/libs/sr-db/src/Database/DataManagers/BuildResultData.ts @@ -0,0 +1,76 @@ +import type { CharacterKey } from '@genshin-optimizer/sr-consts' +import { + allCharacterKeys, + allRelicSlotKeys, +} from '@genshin-optimizer/sr-consts' +import { deepClone } from '@genshin-optimizer/util' +import { DataManager } from '../DataManager' +import type { SroDatabase } from '../Database' + +export interface IBuildResult { + builds: string[][] + buildDate: number +} + +const storageKey = 'sro_buildResults' +const storageHash = 'sro_buildResult_' +export class BuildResultDataManager extends DataManager< + CharacterKey, + typeof storageKey, + IBuildResult, + IBuildResult, + SroDatabase +> { + constructor(database: SroDatabase) { + super(database, storageKey) + for (const key of this.database.storage.keys) + if (key.startsWith(storageHash)) { + const charKey = key.split(storageHash)[1] as CharacterKey + if (!this.set(charKey, {})) this.database.storage.remove(key) + } + } + override toStorageKey(key: string): string { + return `${storageHash}${key}` + } + override validate(obj: unknown, key: CharacterKey): IBuildResult | undefined { + if (typeof obj !== 'object') return undefined + if (!allCharacterKeys.includes(key)) return undefined + let { builds, buildDate } = obj as IBuildResult + + if (!Array.isArray(builds)) { + builds = [] + buildDate = 0 + } else { + builds = builds + .map((build) => { + if (!Array.isArray(build)) return [] + const filteredBuild = build.filter((id) => + this.database.relics.get(id) + ) + // Check that builds has only 1 relic of each slot + if ( + allRelicSlotKeys.some( + (s) => + filteredBuild.filter( + (id) => this.database.relics.get(id)?.slotKey === s + ).length > 1 + ) + ) + return [] + return filteredBuild + }) + .filter((x) => x.length) + if (!Number.isInteger(buildDate)) buildDate = 0 + } + + return { builds, buildDate } + } + override get(key: CharacterKey) { + return super.get(key) ?? initialBuildResult + } +} + +const initialBuildResult: IBuildResult = deepClone({ + builds: [], + buildDate: 0, +}) diff --git a/libs/sr-db/src/Database/DataManagers/BuildSettingData.ts b/libs/sr-db/src/Database/DataManagers/BuildSettingData.ts new file mode 100644 index 0000000000..46fd6f7c3f --- /dev/null +++ b/libs/sr-db/src/Database/DataManagers/BuildSettingData.ts @@ -0,0 +1,226 @@ +import type { + CharacterKey, + CharacterLocationKey, + RelicMainStatKey, +} from '@genshin-optimizer/sr-consts' +import { + allCharacterKeys, + allCharacterLocationKeys, + allRelicSetKeys, + relicSlotToMainStatKeys, +} from '@genshin-optimizer/sr-consts' +import { deepClone, deepFreeze, validateArr } from '@genshin-optimizer/util' +import { DataManager } from '../DataManager' +import type { SroDatabase } from '../Database' + +export const maxBuildsToShowList = [1, 2, 3, 4, 5, 8, 10] as const +export const maxBuildsToShowDefault = 5 + +export const allAllowLocationsState = [ + 'unequippedOnly', + 'customList', + 'all', +] as const +export type AllowLocationsState = (typeof allAllowLocationsState)[number] + +export const allRelicSetExclusionKeys = [...allRelicSetKeys, 'rainbow'] as const +export type RelicSetExclusionKey = (typeof allRelicSetExclusionKeys)[number] + +export type RelicSetExclusion = Partial> + +export interface StatFilterSetting { + value: number + disabled: boolean +} +export type StatFilters = Record +export interface BuildSetting { + relicSetExclusion: RelicSetExclusion + statFilters: StatFilters + mainStatKeys: { + body: RelicMainStatKey[] + feet: RelicMainStatKey[] + sphere: RelicMainStatKey[] + rope: RelicMainStatKey[] + head?: never + hand?: never + } + excludedLocations: CharacterLocationKey[] + allowLocationsState: AllowLocationsState + relicExclusion: string[] + useExcludedRelics: boolean + optimizationTarget?: string[] + mainStatAssumptionLevel: number + allowPartial: boolean + maxBuildsToShow: number + plotBase?: string[] + compareBuild: boolean + levelLow: number + levelHigh: number +} + +const storageKey = 'sro_buildSettings' +const storageHash = 'sro_buildSetting_' +export class BuildSettingDataManager extends DataManager< + CharacterKey, + typeof storageKey, + BuildSetting, + BuildSetting, + SroDatabase +> { + constructor(database: SroDatabase) { + super(database, storageKey) + for (const key of this.database.storage.keys) + if ( + key.startsWith(storageHash) && + !this.set(key.split(storageHash)[1] as CharacterKey, {}) + ) + this.database.storage.remove(key) + } + override toStorageKey(key: string): string { + return `${storageHash}${key}` + } + override validate(obj: object, key: string): BuildSetting | undefined { + if (!allCharacterKeys.includes(key as CharacterKey)) return undefined + if (typeof obj !== 'object') return undefined + let { + relicSetExclusion, + relicExclusion, + useExcludedRelics, + statFilters, + mainStatKeys, + optimizationTarget, + mainStatAssumptionLevel, + excludedLocations, + allowLocationsState, + allowPartial, + maxBuildsToShow, + plotBase, + compareBuild, + levelLow, + levelHigh, + } = obj as BuildSetting + + if (typeof statFilters !== 'object') statFilters = {} + + if ( + !mainStatKeys || + !mainStatKeys.body || + !mainStatKeys.feet || + !mainStatKeys.sphere || + !mainStatKeys.rope + ) + mainStatKeys = deepClone(initialBuildSettings.mainStatKeys) + else { + // make sure the arrays are not empty + ;(['body', 'feet', 'sphere', 'rope'] as const).forEach((sk) => { + if (!mainStatKeys[sk].length) + mainStatKeys[sk] = [...relicSlotToMainStatKeys[sk]] + }) + } + + if (!optimizationTarget || !Array.isArray(optimizationTarget)) + optimizationTarget = undefined + if ( + typeof mainStatAssumptionLevel !== 'number' || + mainStatAssumptionLevel < 0 || + mainStatAssumptionLevel > 15 + ) + mainStatAssumptionLevel = 0 + + if (!relicExclusion || !Array.isArray(relicExclusion)) relicExclusion = [] + else + relicExclusion = [...new Set(relicExclusion)].filter((id) => + this.database.relics.keys.includes(id) + ) + + excludedLocations = validateArr( + excludedLocations, + allCharacterLocationKeys.filter((k) => k !== key), + [] // Remove self from list + ).filter( + (lk) => + this.database.chars.get(this.database.chars.LocationToCharacterKey(lk)) // Remove characters who do not exist in the DB + ) + if (!allowLocationsState) allowLocationsState = 'unequippedOnly' + + if ( + !maxBuildsToShowList.includes( + maxBuildsToShow as (typeof maxBuildsToShowList)[number] + ) + ) + maxBuildsToShow = maxBuildsToShowDefault + if (!plotBase || !Array.isArray(plotBase)) plotBase = undefined + if (compareBuild === undefined) compareBuild = false + if (levelLow === undefined) levelLow = 0 + if (levelHigh === undefined) levelHigh = 20 + if (!relicSetExclusion) relicSetExclusion = {} + if (useExcludedRelics === undefined) useExcludedRelics = false + if (!allowPartial) allowPartial = false + relicSetExclusion = Object.fromEntries( + Object.entries(relicSetExclusion as RelicSetExclusion) + .map(([k, r]) => [k, [...new Set(r)]]) + .filter(([_, a]) => a.length) + ) + return { + relicSetExclusion, + relicExclusion, + useExcludedRelics, + statFilters, + mainStatKeys, + optimizationTarget, + mainStatAssumptionLevel, + excludedLocations, + allowLocationsState, + allowPartial, + maxBuildsToShow, + plotBase, + compareBuild, + levelLow, + levelHigh, + } + } + override get(key: CharacterKey) { + return super.get(key) ?? initialBuildSettings + } +} + +const initialBuildSettings: BuildSetting = deepFreeze({ + relicSetExclusion: {}, + relicExclusion: [], + useExcludedRelics: false, + statFilters: {}, + mainStatKeys: { + body: relicSlotToMainStatKeys.body, + feet: relicSlotToMainStatKeys.feet, + sphere: relicSlotToMainStatKeys.sphere, + rope: relicSlotToMainStatKeys.rope, + }, + optimizationTarget: undefined, + mainStatAssumptionLevel: 0, + excludedLocations: [], + allowLocationsState: 'unequippedOnly', + allowPartial: false, + maxBuildsToShow: 5, + plotBase: undefined, + compareBuild: true, + levelLow: 0, + levelHigh: 20, +}) + +// TODO: Remove 4-set exclusion for planar relics +export function handleRelicSetExclusion( + currentRelicSetExclusion: RelicSetExclusion, + setKey: RelicSetExclusionKey, + num: 2 | 4 +) { + const relicSetExclusion = deepClone(currentRelicSetExclusion) + const setExclusion = relicSetExclusion[setKey] + if (!setExclusion) relicSetExclusion[setKey] = [num] + else if (!setExclusion.includes(num)) + relicSetExclusion[setKey] = [...setExclusion, num] + else { + relicSetExclusion[setKey] = setExclusion.filter((n) => n !== num) + if (!setExclusion.length) delete relicSetExclusion[setKey] + } + return relicSetExclusion +} diff --git a/libs/sr-db/src/Database/DataManagers/CharMetaData.ts b/libs/sr-db/src/Database/DataManagers/CharMetaData.ts new file mode 100644 index 0000000000..a6f559be68 --- /dev/null +++ b/libs/sr-db/src/Database/DataManagers/CharMetaData.ts @@ -0,0 +1,66 @@ +import type { + CharacterKey, + CharacterLocationKey, + RelicSubStatKey, +} from '@genshin-optimizer/sr-consts' +import { + allRelicSubStatKeys, + allTrailblazerKeys, +} from '@genshin-optimizer/sr-consts' +import { deepFreeze } from '@genshin-optimizer/util' +import { DataManager } from '../DataManager' +import type { SroDatabase } from '../Database' + +interface ICharMeta { + rvFilter: RelicSubStatKey[] + favorite: boolean +} +const initCharMeta: ICharMeta = deepFreeze({ + rvFilter: [...allRelicSubStatKeys], + favorite: false, +}) + +const storageKey = 'sro_charMetas' +const storageHash = 'sro_charMeta_' +export class CharMetaDataManager extends DataManager< + CharacterKey, + typeof storageKey, + ICharMeta, + ICharMeta, + SroDatabase +> { + constructor(database: SroDatabase) { + super(database, storageKey) + for (const key of this.database.storage.keys) + if ( + key.startsWith(storageHash) && + !this.set(key.split(storageHash)[1] as CharacterKey, {}) + ) + this.database.storage.remove(key) + } + override validate(obj: any): ICharMeta | undefined { + if (typeof obj !== 'object') return undefined + + let { rvFilter, favorite } = obj + if (!Array.isArray(rvFilter)) rvFilter = [] + else rvFilter = rvFilter.filter((k) => allRelicSubStatKeys.includes(k)) + if (typeof favorite !== 'boolean') favorite = false + return { rvFilter, favorite } + } + + override toStorageKey(key: CharacterKey): string { + return `${storageHash}${key}` + } + getTrailblazerCharacterKey(): CharacterKey { + return ( + allTrailblazerKeys.find((k) => this.keys.includes(k)) ?? + allTrailblazerKeys[0] + ) + } + LocationToCharacterKey(key: CharacterLocationKey): CharacterKey { + return key === 'Trailblazer' ? this.getTrailblazerCharacterKey() : key + } + override get(key: CharacterKey): ICharMeta { + return this.data[key] ?? initCharMeta + } +} diff --git a/libs/sr-db/src/Database/DataManagers/CharacterData.ts b/libs/sr-db/src/Database/DataManagers/CharacterData.ts new file mode 100644 index 0000000000..1b80f33447 --- /dev/null +++ b/libs/sr-db/src/Database/DataManagers/CharacterData.ts @@ -0,0 +1,390 @@ +import type { TriggerString } from '@genshin-optimizer/database' +import { validateLevelAsc } from '@genshin-optimizer/gi-util' +import type { + CharacterKey, + CharacterLocationKey, + RelicSlotKey, + TrailblazerKey, +} from '@genshin-optimizer/sr-consts' +import { + allBonusAbilityKeys, + allCharacterKeys, + allHitModeKeys, + allRelicSlotKeys, + allStatBoostKeys, + allTrailblazerKeys, + charKeyToCharLocKey, +} from '@genshin-optimizer/sr-consts' +import type { ISrObjectDescription } from '@genshin-optimizer/sr-srod' +import { clamp, deepClone, objKeyMap } from '@genshin-optimizer/util' +import type { + ICachedSroCharacter, + ISroCharacter, + ISroDatabase, +} from '../../Interfaces' +import { SroSource } from '../../Interfaces' +import { DataManager } from '../DataManager' +import type { SroDatabase } from '../Database' +import type { ImportResult } from '../exim' + +const storageKey = 'sro_characters' +const storageHash = 'sro_char_' +export class CharacterDataManager extends DataManager< + CharacterKey, + typeof storageKey, + ICachedSroCharacter, + ISroCharacter, + SroDatabase +> { + constructor(database: SroDatabase) { + super(database, storageKey) + for (const key of this.database.storage.keys) { + if ( + key.startsWith(storageHash) && + !this.set(key.split(storageHash)[1] as CharacterKey, {}) + ) + this.database.storage.remove(key) + } + } + override validate(obj: unknown): ISroCharacter | undefined { + if (!obj || typeof obj !== 'object') return undefined + const { + key: characterKey, + level: rawLevel, + ascension: rawAscension, + } = obj as ISroCharacter + let { + hitMode, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + eidolon, + team, + compareData, + } = obj as ISroCharacter + + if (!allCharacterKeys.includes(characterKey)) return undefined // non-recoverable + + if (!allHitModeKeys.includes(hitMode)) hitMode = 'avgHit' + if (typeof eidolon !== 'number' && eidolon < 0 && eidolon > 6) eidolon = 0 + + const { level, ascension } = validateLevelAsc(rawLevel, rawAscension) + + if (typeof bonusAbilities !== 'object') + bonusAbilities = objKeyMap(allBonusAbilityKeys, (_key) => false) + else { + bonusAbilities = objKeyMap(allBonusAbilityKeys, (key) => + typeof bonusAbilities[key] !== 'boolean' + ? false + : bonusAbilities[key] ?? false + ) + } + if (typeof statBoosts !== 'object') + statBoosts = objKeyMap(allStatBoostKeys, (_key) => false) + else { + statBoosts = objKeyMap(allStatBoostKeys, (key) => + typeof statBoosts[key] !== 'boolean' ? false : statBoosts[key] ?? false + ) + } + basic = typeof basic !== 'number' ? 1 : clamp(basic, 1, 6) + skill = typeof skill !== 'number' ? 1 : clamp(skill, 1, 10) + ult = typeof ult !== 'number' ? 1 : clamp(ult, 1, 10) + talent = typeof talent !== 'number' ? 1 : clamp(talent, 1, 10) + + if (!team || !Array.isArray(team)) team = ['', '', ''] + else + team = team.map((t, i) => + t && + allCharacterKeys.includes(t) && + !team.find((ot, j) => i > j && t === ot) + ? t + : '' + ) as ISroCharacter['team'] + + if (typeof compareData !== 'boolean') compareData = false + + const char: ISroCharacter = { + key: characterKey, + level, + ascension, + hitMode, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + eidolon, + team, + compareData, + } + return char + } + override toCache( + storageObj: ISroCharacter, + id: CharacterKey + ): ICachedSroCharacter { + const oldChar = this.get(id) + return { + equippedRelics: oldChar + ? oldChar.equippedRelics + : objKeyMap( + allRelicSlotKeys, + (sk) => + Object.values(this.database.relics?.data ?? {}).find( + (r) => + r?.location === charKeyToCharLocKey(id) && r.slotKey === sk + )?.id ?? '' + ), + equippedLightCone: oldChar + ? oldChar.equippedLightCone + : Object.values(this.database.lightCones?.data ?? {}).find( + (lc) => lc?.location === charKeyToCharLocKey(id) + )?.id ?? '', + ...storageObj, + } + } + override deCache(char: ICachedSroCharacter): ISroCharacter { + const { + key, + level, + ascension, + hitMode, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + eidolon, + team, + compareData, + } = char + const result: ISroCharacter = { + key, + level, + ascension, + hitMode, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + eidolon, + team, + compareData, + } + return result + } + override toStorageKey(key: CharacterKey): string { + return `${storageHash}${key}` + } + getTrailblazerCharacterKey(): CharacterKey { + return ( + allTrailblazerKeys.find((k) => this.keys.includes(k)) ?? + allTrailblazerKeys[0] + ) + } + LocationToCharacterKey(key: CharacterLocationKey): CharacterKey { + return key === 'Trailblazer' ? this.getTrailblazerCharacterKey() : key + } + getWithInitWeapon(key: CharacterKey): ICachedSroCharacter { + if (!this.keys.includes(key)) { + this.set(key, initialCharacter(key)) + } + return this.get(key) as ICachedSroCharacter + } + + override remove(key: CharacterKey) { + const char = this.get(key) + if (!char) return + for (const relicKey of Object.values(char.equippedRelics)) { + const relic = this.database.relics.get(relicKey) + // Only unequip relic from Trailblazer if there are no more "Trailblazer"s in the database + if ( + relic && + (relic.location === key || + (relic.location === 'Trailblazer' && + allTrailblazerKeys.includes(key as TrailblazerKey) && + !allTrailblazerKeys.find( + (t) => t !== key && this.keys.includes(t) + ))) + ) + this.database.relics.setCached(relicKey, { ...relic, location: '' }) + } + const lightCone = this.database.lightCones.get(char.equippedLightCone) + // Only unequip light cone from Trailblazer if there are no more "Trailblazer"s in the database + if ( + lightCone && + (lightCone.location === key || + (lightCone.location === 'Trailblazer' && + allTrailblazerKeys.includes(key as TrailblazerKey) && + !allTrailblazerKeys.find( + (t) => t !== key && this.keys.includes(t) + ))) && + char.equippedLightCone + ) + this.database.lightCones.setCached(char.equippedLightCone, { + ...lightCone, + location: '', + }) + super.remove(key) + } + + /** + * **Caution**: + * This does not update the `location` on relic + * This function should be use internally for database to maintain cache on ICachedSroCharacter. + */ + setEquippedRelic( + key: CharacterLocationKey, + slotKey: RelicSlotKey, + relicId: string + ) { + const setEq = (k: CharacterKey) => { + const char = super.get(k) + if (!char) return + const equippedRelics = deepClone(char.equippedRelics) + equippedRelics[slotKey] = relicId + super.setCached(k, { ...char, equippedRelics }) + } + if (key === 'Trailblazer') allTrailblazerKeys.forEach((k) => setEq(k)) + else setEq(key) + } + + /** + * **Caution**: + * This does not update the `location` on light cone + * This function should be use internally for database to maintain cache on ICachedSroCharacter. + */ + setEquippedLightCone( + key: CharacterLocationKey, + equippedLightCone: ICachedSroCharacter['equippedLightCone'] + ) { + const setEq = (k: CharacterKey) => { + const char = super.get(k) + if (!char) return + super.setCached(k, { ...char, equippedLightCone }) + } + if (key === 'Trailblazer') allTrailblazerKeys.forEach((k) => setEq(k)) + else setEq(key) + } + + hasDup(char: ISroCharacter, isSro: boolean) { + const db = this.getStorage(char.key) + if (!db) return false + if (isSro) { + return JSON.stringify(db) === JSON.stringify(char) + } else { + let { + key, + level, + eidolon, + ascension, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + } = db + const dbSr = { + key, + level, + eidolon, + ascension, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + } + ;({ + key, + level, + eidolon, + ascension, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + } = char) + const charSr = { + key, + level, + eidolon, + ascension, + basic, + skill, + ult, + talent, + bonusAbilities, + statBoosts, + } + return JSON.stringify(dbSr) === JSON.stringify(charSr) + } + } + triggerCharacter(key: CharacterLocationKey, reason: TriggerString) { + if (key === 'Trailblazer') + allTrailblazerKeys.forEach((ck) => this.trigger(ck, reason, this.get(ck))) + else this.trigger(key, reason, this.get(key)) + } + override importSROD( + sr: ISrObjectDescription & ISroDatabase, + result: ImportResult + ) { + result.characters.beforeMerge = this.values.length + + const source = sr.source ?? 'Unknown' + const characters = sr.characters + if (Array.isArray(characters) && characters?.length) { + result.characters.import = characters.length + const idsToRemove = new Set(this.keys) + characters.forEach((c) => { + if (!c.key) result.characters.invalid.push(c as ISroCharacter) + idsToRemove.delete(c.key) + if ( + this.hasDup( + { ...initialCharacter(c.key), ...c }, + source === SroSource + ) + ) + result.characters.unchanged.push(c as ISroCharacter) + else this.set(c.key, c) + }) + + const idtoRemoveArr = Array.from(idsToRemove) + if (result.keepNotInImport || result.ignoreDups) + result.characters.notInImport = idtoRemoveArr.length + else idtoRemoveArr.forEach((k) => this.remove(k)) + result.characters.unchanged = [] + } else result.characters.notInImport = this.values.length + } +} + +export function initialCharacter(key: CharacterKey): ICachedSroCharacter { + return { + key, + level: 1, + eidolon: 0, + ascension: 0, + basic: 1, + skill: 1, + ult: 1, + talent: 1, + bonusAbilities: {}, + statBoosts: {}, + hitMode: 'avgHit', + team: ['', '', ''], + compareData: false, + equippedRelics: objKeyMap(allRelicSlotKeys, () => ''), + equippedLightCone: '', + } +} diff --git a/libs/sr-db/src/Database/DataManagers/LightConeData.ts b/libs/sr-db/src/Database/DataManagers/LightConeData.ts new file mode 100644 index 0000000000..a48f79b469 --- /dev/null +++ b/libs/sr-db/src/Database/DataManagers/LightConeData.ts @@ -0,0 +1,261 @@ +import type { CharacterLocationKey } from '@genshin-optimizer/sr-consts' +import { + allCharacterLocationKeys, + allLightConeKeys, + charKeyToCharLocKey, + lightConeMaxLevel, +} from '@genshin-optimizer/sr-consts' +import type { + ILightCone, + ISrObjectDescription, +} from '@genshin-optimizer/sr-srod' +import { validateLevelAsc } from '@genshin-optimizer/sr-util' +import type { + ICachedLightCone, + ICachedSroCharacter, + ISroDatabase, +} from '../../Interfaces' +import { DataManager } from '../DataManager' +import type { SroDatabase } from '../Database' +import type { ImportResult } from '../exim' +import { initialCharacter } from './CharacterData' + +const storageKey = 'sro_lightCones' +const storageHash = 'sro_lightCone_' +export class LightConeDataManager extends DataManager< + string, + typeof storageKey, + ICachedLightCone, + ILightCone, + SroDatabase +> { + constructor(database: SroDatabase) { + super(database, storageKey) + for (const key of this.database.storage.keys) + if (key.startsWith(storageHash) && !this.set(key, {})) + this.database.storage.remove(key) + } + override validate(obj: unknown): ILightCone | undefined { + if (typeof obj !== 'object') return undefined + const { key, level: rawLevel, ascension: rawAscension } = obj as ILightCone + let { superimpose, location, lock } = obj as ILightCone + + if (!allLightConeKeys.includes(key)) return undefined + if (rawLevel > lightConeMaxLevel) return undefined + const { level, ascension } = validateLevelAsc(rawLevel, rawAscension) + if (typeof superimpose !== 'number' || superimpose < 1 || superimpose > 5) + superimpose = 1 + if (location && !allCharacterLocationKeys.includes(location)) location = '' + lock = !!lock + return { key, level, ascension, superimpose, location, lock } + } + override toCache( + storageObj: ILightCone, + id: string + ): ICachedLightCone | undefined { + const newLightCone = { ...storageObj, id } + const oldLightCone = super.get(id) + + // During initialization of the database, if you import lightCones with location without a corresponding character, the char will be generated here. + const getWithInit = (lk: CharacterLocationKey): ICachedSroCharacter => { + const cKey = this.database.chars.LocationToCharacterKey(lk) + if (!this.database.chars.keys.includes(cKey)) + this.database.chars.set(cKey, initialCharacter(cKey)) + return this.database.chars.get(cKey) as ICachedSroCharacter + } + if (newLightCone.location !== oldLightCone?.location) { + const prevChar = oldLightCone?.location + ? getWithInit(oldLightCone.location) + : undefined + const newChar = newLightCone.location + ? getWithInit(newLightCone.location) + : undefined + + // previously equipped light cone at new location + let prevLightCone = super.get(newChar?.equippedLightCone) + + //current prevLightCone <-> newChar && newLightCone <-> prevChar + //swap to prevLightCone <-> prevChar && newLightCone <-> newChar(outside of this if) + + if (prevLightCone) + super.setCached(prevLightCone.id, { + ...prevLightCone, + location: prevChar?.key ? charKeyToCharLocKey(prevChar.key) : '', + }) + else if (prevChar?.key) prevLightCone = undefined + + if (newChar) + this.database.chars.setEquippedLightCone( + charKeyToCharLocKey(newChar.key), + newLightCone.id + ) + if (prevChar) + this.database.chars.setEquippedLightCone( + charKeyToCharLocKey(prevChar.key), + prevLightCone?.id + ) + } else + newLightCone.location && + this.database.chars.triggerCharacter(newLightCone.location, 'update') + return newLightCone + } + override deCache(lightCone: ICachedLightCone): ILightCone { + const { key, level, ascension, superimpose, location, lock } = lightCone + return { key, level, ascension, superimpose, location, lock } + } + + new(value: ILightCone): string { + const id = this.generateKey() + this.set(id, value) + return id + } + override toStorageKey(key: string): string { + return `${storageHash}${key}` + } + override remove(key: string, notify = true) { + const lc = this.get(key) + if (!lc) return + lc.location && this.database.chars.setEquippedLightCone(lc.location, '') + super.remove(key, notify) + } + override importSROD( + srod: ISrObjectDescription & ISroDatabase, + result: ImportResult + ) { + result.lightCones.beforeMerge = this.values.length + + // Match lightCones for counter, metadata, and locations. + const lightCones = srod.lightCones + + if (!Array.isArray(lightCones) || !lightCones.length) { + result.lightCones.notInImport = this.values.length + return + } + + const takenIds = new Set(this.keys) + lightCones.forEach((a) => { + const id = (a as ICachedLightCone).id + if (!id) return + takenIds.add(id) + }) + + result.lightCones.import = lightCones.length + const idsToRemove = new Set(this.values.map((w) => w.id)) + const hasEquipment = lightCones.some((w) => w.location) + lightCones.forEach((w): void => { + const lightCone = this.validate(w) + if (!lightCone) { + result.lightCones.invalid.push(w) + return + } + + let importLightCone = lightCone + let importId: string | undefined = (w as ICachedLightCone).id + let foundDupOrUpgrade = false + if (!result.ignoreDups) { + const { duplicated, upgraded } = this.findDups( + lightCone, + Array.from(idsToRemove) + ) + if (duplicated[0] || upgraded[0]) { + foundDupOrUpgrade = true + // Favor upgrades with the same location, else use 1st dupe + let [match, isUpgrade] = + hasEquipment && + lightCone.location && + upgraded[0]?.location === lightCone.location + ? [upgraded[0], true] + : duplicated[0] + ? [duplicated[0], false] + : [upgraded[0], true] + if (importId) { + // favor exact id matches + const up = upgraded.find((w) => w.id === importId) + if (up) [match, isUpgrade] = [up, true] + const dup = duplicated.find((w) => w.id === importId) + if (dup) [match, isUpgrade] = [dup, false] + } + isUpgrade + ? result.lightCones.upgraded.push(lightCone) + : result.lightCones.unchanged.push(lightCone) + idsToRemove.delete(match.id) + + //Imported lightCone will be set to `importId` later, so remove the dup/upgrade now to avoid a duplicate + super.remove(match.id, false) // Do not notify, since this is a "replacement". Also use super to bypass the equipment check + if (!importId) importId = match.id // always resolve some id + importLightCone = { + ...lightCone, + location: hasEquipment ? lightCone.location : match.location, + } + } + } + if (importId) { + if (this.get(importId)) { + // `importid` already in use, get a new id + const newId = this.generateKey(takenIds) + takenIds.add(newId) + if (this.changeId(importId, newId)) { + // Sync the id in `idsToRemove` due to the `changeId` + if (idsToRemove.has(importId)) { + idsToRemove.delete(importId) + idsToRemove.add(newId) + } + } + } + this.set(importId, importLightCone, !foundDupOrUpgrade) + } else { + importId = this.generateKey(takenIds) + takenIds.add(importId) + } + this.set(importId, importLightCone, !foundDupOrUpgrade) + }) + + // Shouldn't remove Somnia's signature + const idtoRemoveArr = Array.from(idsToRemove) + if (result.keepNotInImport || result.ignoreDups) + result.lightCones.notInImport = idtoRemoveArr.length + else idtoRemoveArr.forEach((k) => this.remove(k)) + } + + findDups( + lightCone: ILightCone, + idList = this.keys + ): { duplicated: ICachedLightCone[]; upgraded: ICachedLightCone[] } { + const { key, level, ascension, superimpose } = lightCone + + const lightCones = idList + .map((id) => this.get(id)) + .filter((a) => a) as ICachedLightCone[] + const candidates = lightCones.filter( + (candidate) => + key === candidate.key && + level >= candidate.level && + ascension >= candidate.ascension && + superimpose >= candidate.superimpose + ) + + // Strictly upgraded lightCones + const upgraded = candidates + .filter( + (candidate) => + level > candidate.level || + ascension > candidate.ascension || + superimpose > candidate.superimpose + ) + .sort((candidates) => + candidates.location === lightCone.location ? -1 : 1 + ) + // Strictly duplicated lightCones + const duplicated = candidates + .filter( + (candidate) => + level === candidate.level && + ascension === candidate.ascension && + superimpose === candidate.superimpose + ) + .sort((candidates) => + candidates.location === lightCone.location ? -1 : 1 + ) + return { duplicated, upgraded } + } +} diff --git a/libs/sr-db/src/Database/DataManagers/RelicData.ts b/libs/sr-db/src/Database/DataManagers/RelicData.ts new file mode 100644 index 0000000000..f17730423c --- /dev/null +++ b/libs/sr-db/src/Database/DataManagers/RelicData.ts @@ -0,0 +1,529 @@ +import type { + RelicMainStatKey, + RelicRarityKey, + RelicSubStatKey, +} from '@genshin-optimizer/sr-consts' +import { + allCharacterLocationKeys, + allRelicMainStatKeys, + allRelicRarityKeys, + allRelicSetKeys, + allRelicSlotKeys, + allRelicSubStatKeys, + charKeyToCharLocKey, + relicMaxLevel, + relicSlotToMainStatKeys, +} from '@genshin-optimizer/sr-consts' +import type { + IRelic, + ISrObjectDescription, + ISubstat, +} from '@genshin-optimizer/sr-srod' +import { + getRelicMainStatDisplayVal, + getSubstatRange, +} from '@genshin-optimizer/sr-util' +import { clamp } from '@genshin-optimizer/util' +import type { + ICachedRelic, + ICachedSubstat, + ISroDatabase, +} from '../../Interfaces' +import { DataManager } from '../DataManager' +import type { SroDatabase } from '../Database' +import type { ImportResult } from '../exim' + +const storageKey = 'sro_relics' +const storageHash = 'sro_relic_' +export class RelicDataManager extends DataManager< + string, + typeof storageKey, + ICachedRelic, + IRelic, + SroDatabase +> { + constructor(database: SroDatabase) { + super(database, storageKey) + for (const key of this.database.storage.keys) + if (key.startsWith(storageHash) && !this.set(key, {})) + this.database.storage.remove(key) + } + override validate(obj: unknown): IRelic | undefined { + return validateRelic(obj) + } + override toCache(storageObj: IRelic, id: string): ICachedRelic | undefined { + // Generate cache fields + const newRelic = cachedRelic(storageObj, id).relic + + // Check relations and update equipment + const oldRelic = super.get(id) + if (newRelic.location !== oldRelic?.location) { + const slotKey = newRelic.slotKey + const prevChar = oldRelic?.location + ? this.database.chars.getWithInitWeapon( + this.database.chars.LocationToCharacterKey(oldRelic.location) + ) + : undefined + const newChar = newRelic.location + ? this.database.chars.getWithInitWeapon( + this.database.chars.LocationToCharacterKey(newRelic.location) + ) + : undefined + + // previously equipped relic at new location + const prevRelic = super.get(newChar?.equippedRelics[slotKey]) + + //current prevRelic <-> newChar && newRelic <-> prevChar + //swap to prevRelic <-> prevChar && newRelic <-> newChar(outside of this if) + + if (prevRelic) + super.setCached(prevRelic.id, { + ...prevRelic, + location: prevChar?.key ? charKeyToCharLocKey(prevChar.key) : '', + }) + if (newChar) + this.database.chars.setEquippedRelic( + charKeyToCharLocKey(newChar.key), + slotKey, + newRelic.id + ) + if (prevChar) + this.database.chars.setEquippedRelic( + charKeyToCharLocKey(prevChar.key), + slotKey, + prevRelic?.id ?? '' + ) + } else + newRelic.location && + this.database.chars.triggerCharacter(newRelic.location, 'update') + return newRelic + } + override deCache(relic: ICachedRelic): IRelic { + const { + setKey, + rarity, + level, + slotKey, + mainStatKey, + substats, + location, + lock, + } = relic + return { + setKey, + rarity, + level, + slotKey, + mainStatKey, + substats: substats.map((substat) => ({ + key: substat.key, + value: substat.value, + })), + location, + lock, + } + } + + new(value: IRelic): string { + const id = this.generateKey() + this.set(id, value) + return id + } + override toStorageKey(key: string): string { + return `${storageHash}${key}` + } + override remove(key: string, notify = true) { + const relic = this.get(key) + if (!relic) return + relic.location && + this.database.chars.setEquippedRelic(relic.location, relic.slotKey, '') + super.remove(key, notify) + } + override importSROD( + srod: ISrObjectDescription & ISroDatabase, + result: ImportResult + ) { + result.relics.beforeMerge = this.values.length + + // Match relics for counter, metadata, and locations + const relics = srod.relics + + if (!Array.isArray(relics) || !relics.length) { + result.relics.notInImport = this.values.length + return + } + + const takenIds = new Set(this.keys) + relics.forEach((r) => { + const id = (r as ICachedRelic).id + if (!id) return + takenIds.add(id) + }) + + result.relics.import = relics.length + const idsToRemove = new Set(this.values.map((r) => r.id)) + const hasEquipment = relics.some((r) => r.location) + relics.forEach((r): void => { + const relic = this.validate(r) + if (!relic) { + result.relics.invalid.push(r) + return + } + + let importRelic = relic + let importId: string | undefined = (r as ICachedRelic).id + let foundDupOrUpgrade = false + if (!result.ignoreDups) { + const { duplicated, upgraded } = this.findDups( + relic, + Array.from(idsToRemove) + ) + if (duplicated[0] || upgraded[0]) { + foundDupOrUpgrade = true + // Favor upgrades with the same location, else use 1st dupe + let [match, isUpgrade] = + hasEquipment && + relic.location && + upgraded[0]?.location === relic.location + ? [upgraded[0], true] + : duplicated[0] + ? [duplicated[0], false] + : [upgraded[0], true] + if (importId) { + // favor exact id matches + const up = upgraded.find((a) => a.id === importId) + if (up) [match, isUpgrade] = [up, true] + const dup = duplicated.find((a) => a.id === importId) + if (dup) [match, isUpgrade] = [dup, false] + } + isUpgrade + ? result.relics.upgraded.push(relic) + : result.relics.unchanged.push(relic) + idsToRemove.delete(match.id) + + //Imported relic will be set to `importId` later, so remove the dup/upgrade now to avoid a duplicate + this.remove(match.id, false) // Do not notify, since this is a "replacement" + if (!importId) importId = match.id // always resolve some id + importRelic = { + ...relic, + location: hasEquipment ? relic.location : match.location, + } + } + } + if (importId) { + if (this.get(importId)) { + // `importid` already in use, get a new id + const newId = this.generateKey(takenIds) + takenIds.add(newId) + if (this.changeId(importId, newId)) { + // Sync the id in `idsToRemove` due to the `changeId` + if (idsToRemove.has(importId)) { + idsToRemove.delete(importId) + idsToRemove.add(newId) + } + } + } + } else { + importId = this.generateKey(takenIds) + takenIds.add(importId) + } + this.set(importId, importRelic, !foundDupOrUpgrade) + }) + const idtoRemoveArr = Array.from(idsToRemove) + if (result.keepNotInImport || result.ignoreDups) + result.relics.notInImport = idtoRemoveArr.length + else idtoRemoveArr.forEach((k) => this.remove(k)) + } + findDups( + editorRelic: IRelic, + idList = this.keys + ): { duplicated: ICachedRelic[]; upgraded: ICachedRelic[] } { + const { setKey, rarity, level, slotKey, mainStatKey, substats } = + editorRelic + + const relics = idList + .map((id) => this.get(id)) + .filter((r) => r) as ICachedRelic[] + const candidates = relics.filter( + (candidate) => + setKey === candidate.setKey && + rarity === candidate.rarity && + slotKey === candidate.slotKey && + mainStatKey === candidate.mainStatKey && + level >= candidate.level && + substats.every( + (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) + ) + ) + + // Strictly upgraded relic + const upgraded = candidates + .filter( + (candidate) => + level > candidate.level && + (Math.floor(level / 3) === Math.floor(candidate.level / 3) // Check for extra rolls + ? substats.every( + ( + substat, + i // Has no extra roll + ) => + substat.key === candidate.substats[i].key && + substat.value === candidate.substats[i].value + ) + : substats.some( + ( + substat, + i // Has extra rolls + ) => + candidate.substats[i].key + ? substat.value > candidate.substats[i].value // Extra roll to existing substat + : substat.key // Extra roll to new substat + )) + ) + .sort((candidates) => + candidates.location === editorRelic.location ? -1 : 1 + ) + // Strictly duplicated relic + const duplicated = candidates + .filter( + (candidate) => + level === candidate.level && + substats.every( + (substat) => + !substat.key || // Empty slot + candidate.substats.some( + (candidateSubstat) => + substat.key === candidateSubstat.key && // Or same slot + substat.value === candidateSubstat.value + ) + ) + ) + .sort((candidates) => + candidates.location === editorRelic.location ? -1 : 1 + ) + return { duplicated, upgraded } + } +} + +export function cachedRelic( + flex: IRelic, + id: string +): { relic: ICachedRelic; errors: string[] } { + const { location, lock, setKey, slotKey, rarity, mainStatKey } = flex + const level = Math.round( + Math.min(Math.max(0, flex.level), relicMaxLevel[rarity]) + ) + const mainStatVal = getRelicMainStatDisplayVal(rarity, mainStatKey, level) + + const errors: string[] = [] + const substats: ICachedSubstat[] = flex.substats.map((substat) => ({ + ...substat, + rolls: [], + efficiency: 0, + accurateValue: substat.value, + })) + // Carry over the probability, since its a cached value calculated outside of the relic. + const validated: ICachedRelic = { + id, + setKey, + location, + slotKey, + lock, + mainStatKey, + rarity, + level, + substats, + mainStatVal, + } + + // TODO: Validate rolls + // const allPossibleRolls: { index: number; substatRolls: number[][] }[] = [] + // let totalUnambiguousRolls = 0 + + // function efficiency(value: number, key: RelicSubStatKey): number { + // return (value / getSubstatValue(rarity, key, 'high')) * 100 + // } + + // substats.forEach((substat, _index): void => { + // const { key, value } = substat + // if (!key) { + // substat.value = 0 + // return + // } + // substat.efficiency = efficiency(value, key) + + // const possibleRolls = getSubstatRolls(key, value, rarity) + + // if (possibleRolls.length) { + // // Valid Substat + // const possibleLengths = new Set(possibleRolls.map((roll) => roll.length)) + + // if (possibleLengths.size !== 1) { + // // Ambiguous Rolls + // allPossibleRolls.push({ index, substatRolls: possibleRolls }) + // } else { + // // Unambiguous Rolls + // totalUnambiguousRolls += possibleRolls[0].length + // } + + // substat.rolls = possibleRolls.reduce((best, current) => + // best.length < current.length ? best : current + // ) + // substat.efficiency = efficiency( + // substat.rolls.reduce((a, b) => a + b, 0), + // key + // ) + // substat.accurateValue = substat.rolls.reduce((a, b) => a + b, 0) + // } else { + // // Invalid Substat + // substat.rolls = [] + // // TODO: Translate + // errors.push(`Invalid substat ${substat.key}`) + // } + // }) + + // if (errors.length) return { relic: validated, errors } + + // const { low, high } = relicSubstatRollData[rarity], + // lowerBound = low + Math.floor(level / 3), + // upperBound = high + Math.floor(level / 3) + + // let highestScore = -Infinity // -Max(substats.rolls[i].length) over ambiguous rolls + // const tryAllSubstats = ( + // rolls: { index: number; roll: number[] }[], + // currentScore: number, + // total: number + // ) => { + // if (rolls.length === allPossibleRolls.length) { + // if ( + // total <= upperBound && + // total >= lowerBound && + // highestScore < currentScore + // ) { + // highestScore = currentScore + // for (const { index, roll } of rolls) { + // const key = substats[index].key as RelicSubStatKey + // const accurateValue = roll.reduce((a, b) => a + b, 0) + // substats[index].rolls = roll + // substats[index].accurateValue = accurateValue + // substats[index].efficiency = efficiency(accurateValue, key) + // } + // } + + // return + // } + + // const { index, substatRolls } = allPossibleRolls[rolls.length] + // for (const roll of substatRolls) { + // rolls.push({ index, roll }) + // const newScore = Math.min(currentScore, -roll.length) + // if (newScore >= highestScore) + // // Scores won't get better, so we can skip. + // tryAllSubstats(rolls, newScore, total + roll.length) + // rolls.pop() + // } + // } + + // tryAllSubstats([], Infinity, totalUnambiguousRolls) + + // const totalRolls = substats.reduce( + // (accu, { rolls }) => accu + rolls.length, + // 0 + // ) + + // if (totalRolls > upperBound) + // errors.push( + // `${rarity}-star relic (level ${level}) should have no more than ${upperBound} rolls. It currently has ${totalRolls} rolls.` + // ) + // else if (totalRolls < lowerBound) + // errors.push( + // `${rarity}-star relic (level ${level}) should have at least ${lowerBound} rolls. It currently has ${totalRolls} rolls.` + // ) + + // if (substats.some((substat) => !substat.key)) { + // const substat = substats.find((substat) => (substat.rolls?.length ?? 0) > 1) + // if (substat) + // // TODO: Translate + // errors.push( + // `Substat ${substat.key} has > 1 roll, but not all substats are unlocked.` + // ) + // } + + return { relic: validated, errors } +} + +export function validateRelic( + obj: unknown = {}, + allowZeroSub = false +): IRelic | undefined { + if (!obj || typeof obj !== 'object') return undefined + const { setKey, rarity, slotKey } = obj as IRelic + let { level, mainStatKey, substats, location, lock } = obj as IRelic + + if ( + !allRelicSetKeys.includes(setKey) || + !allRelicSlotKeys.includes(slotKey) || + !allRelicMainStatKeys.includes(mainStatKey) || + !allRelicRarityKeys.includes(rarity) || + typeof level !== 'number' || + level < 0 || + level > 15 + ) + return undefined // non-recoverable + level = Math.round(level) + if (level > relicMaxLevel[rarity]) return undefined + + substats = parseSubstats(substats, rarity, allowZeroSub) + // substat cannot have same key as mainstat + if (substats.find((sub) => sub.key === mainStatKey)) return undefined + lock = !!lock + const plausibleMainStats = relicSlotToMainStatKeys[slotKey] + if (!(plausibleMainStats as RelicMainStatKey[]).includes(mainStatKey)) + if (plausibleMainStats.length === 1) mainStatKey = plausibleMainStats[0] + else return undefined // ambiguous mainstat + if (!location || !allCharacterLocationKeys.includes(location)) location = '' + return { + setKey, + rarity, + level, + slotKey, + mainStatKey, + substats, + location, + lock, + } +} +function defSub(): ISubstat { + return { key: '', value: 0 } +} +function parseSubstats( + obj: unknown, + rarity: RelicRarityKey, + allowZeroSub = false +): ISubstat[] { + if (!Array.isArray(obj)) return new Array(4).map((_) => defSub()) + const substats = (obj as ISubstat[]) + .slice(0, 4) + .map(({ key = '', value = 0 }) => { + if ( + !allRelicSubStatKeys.includes(key as RelicSubStatKey) || + typeof value !== 'number' || + !isFinite(value) + ) + return defSub() + if (key) { + value = key.endsWith('_') + ? Math.round(value * 10) / 10 + : Math.round(value) + const { low, high } = getSubstatRange(rarity, key) + value = clamp(value, allowZeroSub ? 0 : low, high) + } else value = 0 + return { key, value } + }) + while (substats.length < 4) substats.push(defSub()) + + return substats +} diff --git a/libs/sr-db/src/Database/Database.test.ts b/libs/sr-db/src/Database/Database.test.ts new file mode 100644 index 0000000000..896aa17a83 --- /dev/null +++ b/libs/sr-db/src/Database/Database.test.ts @@ -0,0 +1,729 @@ +import { DBLocalStorage, SandboxStorage } from '@genshin-optimizer/database' +import type { LightConeKey } from '@genshin-optimizer/sr-consts' +import type { + ILightCone, + IRelic, + ISrObjectDescription, +} from '@genshin-optimizer/sr-srod' +import { randomizeRelic } from '@genshin-optimizer/sr-util' +import { objKeyMap, range } from '@genshin-optimizer/util' +import type { ICachedLightCone, ISroDatabase } from '../Interfaces' +import { SroSource } from '../Interfaces' +import { initialCharacter } from './DataManagers/CharacterData' +import { SroDatabase } from './Database' + +const dbStorage = new DBLocalStorage(localStorage, 'sro') +const dbIndex = 1 +let database = new SroDatabase(dbIndex, dbStorage) + +function newLightCone(key: LightConeKey): ICachedLightCone { + return { + key, + level: 1, + ascension: 0, + superimpose: 0, + location: '', + lock: false, + id: '', + } +} + +describe('Database', () => { + beforeEach(() => { + dbStorage.clear() + database = new SroDatabase(dbIndex, dbStorage) + }) + + test('Support roundtrip import-export', () => { + const march7th = initialCharacter('March7th'), + tingyun = initialCharacter('Tingyun') + const march7thLightCone = newLightCone('TrendOfTheUniversalMarket'), + tingyunLightCone = newLightCone('Chorus') + + const relic1 = randomizeRelic({ slotKey: 'body' }), + relic2 = randomizeRelic() + march7th.basic = 4 + relic1.location = 'March7th' + march7thLightCone.location = 'March7th' + + database.chars.set(march7th.key, march7th) + database.chars.set(tingyun.key, tingyun) + + database.lightCones.new(march7thLightCone) + const tingyunLightConeid = database.lightCones.new(tingyunLightCone) + + database.relics.new(relic1) + const relic2id = database.relics.new(relic2) + database.relics.set(relic2id, { location: 'Tingyun' }) + database.lightCones.set(tingyunLightConeid, { location: 'Tingyun' }) + + const newDB = new SroDatabase(dbIndex, new SandboxStorage(undefined, 'sro')) + const srod = database.exportSROD() + newDB.importSROD(srod, false, false) + expect( + database.storage.entries.filter( + ([k]) => + k.startsWith('lightCone_') || + k.startsWith('character_') || + k.startsWith('relic_') + ) + ).toEqual( + newDB.storage.entries.filter( + ([k]) => + k.startsWith('lightCone_') || + k.startsWith('character_') || + k.startsWith('relic_') + ) + ) + expect(database.chars.values).toEqual(newDB.chars.values) + expect(database.lightCones.values).toEqual(newDB.lightCones.values) + expect(database.relics.values).toEqual(newDB.relics.values) + // Can't check IcharacterCache because equipped can have differing id + }) + + test('Does not crash from invalid storage', () => { + function tryStorage( + setup: (storage: Storage) => void, + verify: (storage: Storage) => void = () => null + ) { + localStorage.clear() + setup(localStorage) + new SroDatabase(dbIndex, dbStorage) + verify(localStorage) + } + + tryStorage( + (storage) => { + storage['sro_char_x'] = '{ test: "test" }' + storage['sro_relic_x'] = '{}' + }, + (storage) => { + expect(storage.getItem('sro_char_x')).toBeNull() + } + ) + tryStorage( + (storage) => { + storage['sro_char_x'] = '{ test: "test" }' + storage['sro_relic_x'] = '{}' + expect(storage.getItem('sro_char_x')).not.toBeNull() + }, + (storage) => { + expect(storage.getItem('sro_char_x')).toBeNull() + expect(storage.getItem('sro_relic_x')).toBeNull() + } + ) + }) + + test('Equip swap', () => { + database.chars.set('March7th', initialCharacter('March7th')) + database.lightCones.new({ + ...newLightCone('TrendOfTheUniversalMarket'), + location: 'March7th', + }) + + const relic1 = randomizeRelic({ slotKey: 'body' }) + relic1.location = 'March7th' + const relic1id = database.relics.new(relic1) + expect(database.chars.get('March7th')!.equippedRelics.body).toEqual( + relic1id + ) + const relic2 = randomizeRelic({ slotKey: 'body' }) + relic2.location = 'March7th' + const relic2id = database.relics.new(relic2) + expect(database.chars.get('March7th')!.equippedRelics.body).toEqual( + relic2id + ) + expect(database.relics.get(relic1id)?.location).toEqual('') + + database.chars.set('Gepard', initialCharacter('Gepard')) + const chorusId = database.lightCones.new(newLightCone('WeAreWildfire')) + database.lightCones.set(chorusId, { location: 'Gepard' }) + expect(database.chars.get('Gepard')!.equippedLightCone).toEqual(chorusId) + database.relics.set(relic1id, { location: 'Gepard' }) + expect(database.chars.get('Gepard')!.equippedRelics.body).toEqual(relic1id) + + database.relics.set(relic2id, { location: 'Gepard' }) + expect(database.chars.get('March7th')!.equippedRelics.body).toEqual( + relic1id + ) + expect(database.chars.get('Gepard')!.equippedRelics.body).toEqual(relic2id) + expect(database.relics.get(relic1id)!.location).toEqual('March7th') + }) + + test('can remove equipped lightCone', () => { + database.chars.set('March7th', initialCharacter('March7th')) + const sword1 = database.lightCones.new({ + ...newLightCone('TrendOfTheUniversalMarket'), + location: 'March7th', + }) + database.lightCones.remove(sword1) + expect(database.lightCones.get(sword1)).toBeFalsy() + expect(database.chars.get('March7th')!.equippedLightCone).toEqual('') + }) + + test('Remove relic with equipment', () => { + database.chars.set('March7th', initialCharacter('March7th')) + const relic1id = database.relics.new({ + ...randomizeRelic({ slotKey: 'body' }), + location: 'March7th', + }) + expect(database.chars.get('March7th')!.equippedRelics.body).toEqual( + relic1id + ) + expect(database.relics.get(relic1id)?.location).toEqual('March7th') + database.relics.remove(relic1id) + expect(database.chars.get('March7th')!.equippedRelics.body).toEqual('') + expect(database.relics.get(relic1id)).toBeUndefined() + }) + + test('Test import with initials', () => { + // When adding relics with equipment, expect character/lightCones to be created + const relic1 = randomizeRelic({ slotKey: 'body', location: 'March7th' }), + relic2 = randomizeRelic({ location: 'Tingyun' }) + + const tingyunLightCone = newLightCone('Chorus') + tingyunLightCone.location = 'Tingyun' + + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + relics: [relic1, relic2], + lightCones: [tingyunLightCone], + } + const importResult = database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + false, + false + ) + expect(importResult.characters?.new?.length).toEqual(2) + expect(importResult.relics.invalid.length).toEqual(0) + expect(importResult.relics?.new?.length).toEqual(2) + expect(importResult.lightCones?.new?.length).toEqual(1) + }) + + test('Test import with no equip', () => { + // When adding relics with equipment, expect character/lightCones to be created + const relic1 = randomizeRelic({ slotKey: 'body', location: 'Tingyun' }) + + // Implicitly assign location + const id = database.relics.new(relic1) + + expect(database.chars.get('Tingyun')!.equippedRelics.body).toEqual(id) + + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + relics: [relic1], + } + + // Import the new relic, with no location. this should respect current equipment + database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + false, + false + ) + expect(database.chars.get('Tingyun')?.equippedRelics.body).toEqual(id) + }) + + test('Test partial merge', () => { + // Add Character and Relic + const march7th = initialCharacter('March7th') + const march7thLightCone = newLightCone('TrendOfTheUniversalMarket') + march7thLightCone.location = 'March7th' + + const relic1 = randomizeRelic({ + slotKey: 'body', + setKey: 'GuardOfWutheringSnow', + location: 'March7th', + }) + + database.chars.set(march7th.key, march7th) + const lightConeid = database.lightCones.new(march7thLightCone) + database.lightCones.set(lightConeid, march7thLightCone) + + const relic1id = database.relics.new(relic1) + expect(database.chars.get('March7th')?.equippedRelics.body).toEqual( + relic1id + ) + expect(database.chars.get('March7th')?.equippedLightCone).toEqual( + lightConeid + ) + const srod1: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + relics: [ + randomizeRelic({ slotKey: 'body', setKey: 'BandOfSizzlingThunder' }), + randomizeRelic({ + slotKey: 'body', + setKey: 'BandOfSizzlingThunder', + location: 'March7th', + }), + ], + lightCones: [{ ...newLightCone('WeAreWildfire'), location: 'March7th' }], + } + const importResult = database.importSROD( + srod1 as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.relics.new.length).toEqual(2) + expect(importResult.lightCones.new.length).toEqual(1) + expect(importResult.characters.new.length).toEqual(0) + + const relics = database.relics.values + expect(relics.length).toEqual(3) + expect( + database.relics.values.reduce( + (t, relic) => t + (relic.location === 'March7th' ? 1 : 0), + 0 + ) + ).toEqual(1) + const bodyId = database.chars.get('March7th')?.equippedRelics.body + expect(bodyId).toBeTruthy() + expect(database.relics.get(bodyId)?.setKey).toEqual('BandOfSizzlingThunder') + expect( + database.lightCones.get(database.chars.get('March7th')?.equippedLightCone) + ?.key + ).toEqual('WeAreWildfire') + }) + + test('should merge scanner with dups for lightCones', () => { + const a1 = newLightCone('Adversarial') + const a2old = newLightCone('Chorus') + const a2new = newLightCone('Chorus') + a2new.level = 20 + const a3 = newLightCone('MakeTheWorldClamor') // in db but not in import + const a4 = newLightCone('ASecretVow') // in import but not in db + + const dupId = database.lightCones.new(a1) + const upgradeId = database.lightCones.new(a2old) + database.lightCones.new(a3) + const srod1: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + lightCones: [a1, a2new, a4], + } + const importResult = database.importSROD( + srod1 as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.lightCones.upgraded.length).toEqual(1) + expect(importResult.lightCones.unchanged.length).toEqual(1) + expect(importResult.lightCones.notInImport).toEqual(1) + expect(importResult.lightCones.new.length).toEqual(1) + expect(database.lightCones.values.length).toEqual(4) + const dbA1 = database.lightCones.get(dupId) + expect(dbA1?.key).toEqual('Adversarial') + const dbA2 = database.lightCones.get(upgradeId) + expect(dbA2?.key).toEqual('Chorus') + expect(dbA2?.level).toEqual(20) + }) + + test('should merge scanner with dups for relics', () => { + const a1 = randomizeRelic({ + setKey: 'EagleOfTwilightLine', + slotKey: 'head', + }) // dup + const a2old: IRelic = { + // before + level: 0, + location: '', + lock: false, + mainStatKey: 'atk', + rarity: 3, + setKey: 'BandOfSizzlingThunder', + slotKey: 'hand', + substats: [{ key: 'atk_', value: 5 }], + } + const a2new: IRelic = { + // upgrade + level: 4, + location: '', + lock: false, + mainStatKey: 'atk', + rarity: 3, + setKey: 'BandOfSizzlingThunder', + slotKey: 'hand', + substats: [ + { key: 'atk_', value: 5 }, + { key: 'def_', value: 5 }, + ], + } + const a3 = randomizeRelic({ slotKey: 'sphere' }) // in db but not in import + const a4 = randomizeRelic({ slotKey: 'body' }) // in import but not in db + + const dupId = database.relics.new(a1) + const upgradeId = database.relics.new(a2old) + database.relics.new(a3) + const srod1: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + relics: [a1, a2new, a4], + } + const importResult = database.importSROD( + srod1 as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.relics.upgraded.length).toEqual(1) + expect(importResult.relics.unchanged.length).toEqual(1) + expect(importResult.relics.notInImport).toEqual(1) + expect(importResult.relics.new.length).toEqual(1) + expect(database.relics.values.length).toEqual(4) + const dbA1 = database.relics.get(dupId) + expect(dbA1?.slotKey).toEqual('head') + const dbA2 = database.relics.get(upgradeId) + expect(dbA2?.slotKey).toEqual('hand') + expect(dbA2?.level).toEqual(4) + }) + test('Import character without lightCone should not give default lightCone', () => { + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + characters: [ + { + key: 'March7th', + level: 40, + eidolon: 0, + ascension: 1, + basic: 1, + skill: 1, + ult: 1, + talent: 1, + bonusAbilities: objKeyMap(range(1, 3), () => false), + statBoosts: objKeyMap(range(1, 10), () => false), + }, + ], + } + const importResult = database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + false, + false + ) + expect(importResult.lightCones.new.length).toEqual(0) + expect(importResult.characters.new.length).toEqual(1) + expect(database.chars.get('March7th')?.equippedLightCone).toBeFalsy() + }) + describe('import again with overlapping ids', () => { + test('import again with overlapping relic ids', () => { + const old1 = randomizeRelic({ slotKey: 'head' }) + const old2 = randomizeRelic({ slotKey: 'hand' }) + const old3 = randomizeRelic({ slotKey: 'sphere' }) + const old4 = randomizeRelic({ slotKey: 'body' }) + + const oldId1 = database.relics.new(old1) + const oldId2 = database.relics.new(old2) + const oldId3 = database.relics.new(old3) + const oldId4 = database.relics.new(old4) + expect([oldId1, oldId2, oldId3, oldId4]).toEqual([ + 'relic_0', + 'relic_1', + 'relic_2', + 'relic_3', + ]) + + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: SroSource, + relics: [ + { ...old1, id: oldId1 } as IRelic, + { ...old2, id: oldId2 } as IRelic, + + //swap these two + { ...old4, id: oldId3 } as IRelic, + { ...old3, id: oldId4 } as IRelic, + ], + } + + const importResult = database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.relics.notInImport).toEqual(0) + expect(importResult.relics.unchanged.length).toEqual(4) + expect(database.relics.values.length).toEqual(4) + // Expect imports to overwrite the id of old + expect(database.relics.get(oldId1)?.slotKey).toEqual('head') + expect(database.relics.get(oldId2)?.slotKey).toEqual('hand') + expect(database.relics.get(oldId3)?.slotKey).toEqual('body') + expect(database.relics.get(oldId4)?.slotKey).toEqual('sphere') + }) + + test('import again with overlapping lightCone ids', () => { + const old1 = newLightCone('MakeTheWorldClamor') + const old2 = newLightCone('TrendOfTheUniversalMarket') + const old3 = newLightCone('Chorus') + const old4 = newLightCone('ASecretVow') + + const oldId1 = database.lightCones.new(old1) + const oldId2 = database.lightCones.new(old2) + const oldId3 = database.lightCones.new(old3) + const oldId4 = database.lightCones.new(old4) + expect([oldId1, oldId2, oldId3, oldId4]).toEqual([ + 'lightCone_0', + 'lightCone_1', + 'lightCone_2', + 'lightCone_3', + ]) + + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: SroSource, + lightCones: [ + { ...old1, id: oldId1 } as ILightCone, + { ...old2, id: oldId2 } as ILightCone, + + //swap these two + { ...old4, id: oldId3 } as ILightCone, + { ...old3, id: oldId4 } as ILightCone, + ], + } + + const importResult = database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.lightCones.notInImport).toEqual(0) + expect(importResult.lightCones.unchanged.length).toEqual(4) + expect(database.lightCones.values.length).toEqual(4) + // Expect imports to overwrite the id of old + expect(database.lightCones.get(oldId1)?.key).toEqual('MakeTheWorldClamor') + expect(database.lightCones.get(oldId2)?.key).toEqual( + 'TrendOfTheUniversalMarket' + ) + expect(database.lightCones.get(oldId3)?.key).toEqual('ASecretVow') + expect(database.lightCones.get(oldId4)?.key).toEqual('Chorus') + }) + }) + + describe('mutual exclusion import with ids', () => { + test('import with mutually-exclusive relic ids', () => { + const old1 = randomizeRelic({ slotKey: 'head' }) + const old2 = randomizeRelic({ slotKey: 'hand' }) + const new1 = randomizeRelic({ slotKey: 'sphere' }) + const new2 = randomizeRelic({ slotKey: 'body' }) + + const oldId1 = database.relics.new(old1) + const oldId2 = database.relics.new(old2) + expect([oldId1, oldId2]).toEqual(['relic_0', 'relic_1']) + + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: SroSource, + relics: [ + { ...new1, id: oldId1 } as IRelic, + { ...new2, id: oldId2 } as IRelic, + ], + } + + const importResult = database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.relics.notInImport).toEqual(2) + expect(database.relics.values.length).toEqual(4) + // Expect imports to overwrite the id of old + expect(database.relics.get(oldId1)?.slotKey).toEqual('sphere') + expect(database.relics.get(oldId2)?.slotKey).toEqual('body') + // Expect old relics to have new id + expect( + database.relics.values.find((a) => a.slotKey === 'head')?.id + ).not.toEqual(oldId1) + expect( + database.relics.values.find((a) => a.slotKey === 'hand')?.id + ).not.toEqual(oldId2) + }) + + test('import with mutually exclusive lightCone ids', () => { + const old1 = newLightCone('ASecretVow') + const old2 = newLightCone('Cornucopia') + const new1 = newLightCone('Amber') + const new2 = newLightCone('DataBank') + + const oldId1 = database.lightCones.new(old1) + const oldId2 = database.lightCones.new(old2) + expect([oldId1, oldId2]).toEqual(['lightCone_0', 'lightCone_1']) + + const srod: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: SroSource, + lightCones: [ + { ...new1, id: oldId1 } as ILightCone, + { ...new2, id: oldId2 } as ILightCone, + ], + } + + const importResult = database.importSROD( + srod as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.lightCones.notInImport).toEqual(2) + expect(database.lightCones.values.length).toEqual(4) + // Expect imports to overwrite the id of old + expect(database.lightCones.get(oldId1)?.key).toEqual('Amber') + expect(database.lightCones.get(oldId2)?.key).toEqual('DataBank') + // Expect old relics to have new id + expect( + database.lightCones.values.find((a) => a.key === 'ASecretVow')?.id + ).not.toEqual(oldId1) + expect( + database.lightCones.values.find((a) => a.key === 'Cornucopia')?.id + ).not.toEqual(oldId2) + }) + }) + + describe('Trailblazer Handling', () => { + test('Test Trailblazer share equipment', () => { + database.chars.set( + 'TrailblazerPhysical', + initialCharacter('TrailblazerPhysical') + ) + database.chars.set('TrailblazerFire', initialCharacter('TrailblazerFire')) + const relic1 = randomizeRelic({ + slotKey: 'body', + setKey: 'BandOfSizzlingThunder', + }) + const relic1Id = database.relics.new({ + ...relic1, + location: 'Trailblazer', + }) + + expect( + database.chars.get('TrailblazerPhysical')!.equippedRelics.body + ).toEqual(relic1Id) + expect( + database.chars.get('TrailblazerFire')!.equippedRelics.body + ).toEqual(relic1Id) + const lightCone1Id = database.chars.get( + 'TrailblazerPhysical' + )!.equippedLightCone + expect(database.chars.get('TrailblazerFire')!.equippedLightCone).toEqual( + lightCone1Id + ) + + const relic2 = randomizeRelic({ + slotKey: 'body', + setKey: 'BelobogOfTheArchitects', + }) + const relic2Id = database.relics.new({ + ...relic2, + location: 'Trailblazer', + }) + expect( + database.chars.get('TrailblazerPhysical')!.equippedRelics.body + ).toEqual(relic2Id) + expect( + database.chars.get('TrailblazerFire')!.equippedRelics.body + ).toEqual(relic2Id) + + const lightCone2Id = database.lightCones.new({ + ...newLightCone('Chorus'), + location: 'Trailblazer', + }) + expect( + database.chars.get('TrailblazerPhysical')!.equippedLightCone + ).toEqual(lightCone2Id) + expect(database.chars.get('TrailblazerFire')!.equippedLightCone).toEqual( + lightCone2Id + ) + + // deletion dont remove equipment until all traveler is gone + database.chars.remove('TrailblazerFire') + + expect( + database.chars.get('TrailblazerPhysical')!.equippedRelics.body + ).toEqual(relic2Id) + expect( + database.chars.get('TrailblazerPhysical')!.equippedLightCone + ).toEqual(lightCone2Id) + expect(database.relics.get(relic2Id)!.location).toEqual('Trailblazer') + expect(database.lightCones.get(lightCone2Id)!.location).toEqual( + 'Trailblazer' + ) + + // deletion of final traveler unequips + database.chars.remove('TrailblazerPhysical') + + expect(database.relics.get(relic2Id)!.location).toEqual('') + expect(database.lightCones.get(lightCone2Id)!.location).toEqual('') + }) + }) + + describe('DataManager.changeId', () => { + test('should changeId for relics', () => { + const relic = randomizeRelic({ location: 'March7th', slotKey: 'head' }) + const oldId = database.relics.new(relic) + const newId = 'newTestId' + expect(database.relics.changeId(oldId, newId)).toBeTruthy() + + expect(database.relics.get(oldId)).toBeUndefined() + + const cachrelic = database.relics.get(newId) + expect(cachrelic).toBeTruthy() + expect(cachrelic?.location).toEqual('March7th') + expect(database.chars.get('March7th')?.equippedRelics.head).toEqual(newId) + }) + test('should changeId for lightCones', () => { + const lightCone = newLightCone('TrendOfTheUniversalMarket') + lightCone.location = 'March7th' + const oldId = database.lightCones.new(lightCone) + const newId = 'newTestId' + database.lightCones.changeId(oldId, newId) + + expect(database.lightCones.get(oldId)).toBeUndefined() + + const cachWea = database.lightCones.get(newId) + expect(cachWea).toBeTruthy() + expect(cachWea?.location).toEqual('March7th') + expect(database.chars.get('March7th')?.equippedLightCone).toEqual(newId) + }) + }) + + test('Test mismatch lightCone path location; lc should stay equipped', () => { + // Add Character and Relic + const march7th = initialCharacter('March7th') + const march7thLightCone = newLightCone('TrendOfTheUniversalMarket') + march7thLightCone.location = 'March7th' + + database.chars.set(march7th.key, march7th) + const swordid = database.lightCones.new(march7thLightCone) + database.lightCones.set(swordid, march7thLightCone) + + expect(database.chars.get('March7th')?.equippedLightCone).toEqual(swordid) + const srod1: ISrObjectDescription = { + format: 'SROD', + version: 1, + source: 'Scanner', + lightCones: [ + // Invalid Abundance on Preservation char + { ...newLightCone('Chorus'), location: 'March7th' }, + ], + } + const importResult = database.importSROD( + srod1 as ISrObjectDescription & ISroDatabase, + true, + false + ) + expect(importResult.lightCones.new.length).toEqual(1) + expect(importResult.characters.new.length).toEqual(0) + expect( + database.lightCones.get(database.chars.get('March7th')?.equippedLightCone) + ?.key + ).toEqual('Chorus') + }) +}) diff --git a/libs/sr-db/src/Database/Database.ts b/libs/sr-db/src/Database/Database.ts new file mode 100644 index 0000000000..d03f6a17e9 --- /dev/null +++ b/libs/sr-db/src/Database/Database.ts @@ -0,0 +1,185 @@ +import type { DBStorage } from '@genshin-optimizer/database' +import { Database, SandboxStorage } from '@genshin-optimizer/database' +import type { GenderKey } from '@genshin-optimizer/sr-consts' +import type { ISrObjectDescription } from '@genshin-optimizer/sr-srod' +import { createContext } from 'react' +import type { ISroDatabase } from '../Interfaces' +import { SroSource } from '../Interfaces' +import { DBMetaEntry } from './DataEntries/DBMetaEntry' +import { BuildResultDataManager } from './DataManagers/BuildResultData' +import { BuildSettingDataManager } from './DataManagers/BuildSettingData' +import { CharMetaDataManager } from './DataManagers/CharMetaData' +import { CharacterDataManager } from './DataManagers/CharacterData' +import { LightConeDataManager } from './DataManagers/LightConeData' +import { RelicDataManager } from './DataManagers/RelicData' +import type { ImportResult } from './exim' +import { newImportResult } from './exim' +import { + currentDBVersion, + migrateSr as migrateSROD, + migrateStorage, +} from './migrate' +export class SroDatabase extends Database { + relics: RelicDataManager + chars: CharacterDataManager + lightCones: LightConeDataManager + buildSettings: BuildSettingDataManager + buildResult: BuildResultDataManager + charMeta: CharMetaDataManager + + dbMeta: DBMetaEntry + dbIndex: 1 | 2 | 3 | 4 + dbVer: number + + constructor(dbIndex: 1 | 2 | 3 | 4, storage: DBStorage) { + super(storage) + migrateStorage(storage) + // Transfer non DataManager/DataEntry data from storage + this.dbIndex = dbIndex + this.dbVer = storage.getDBVersion() + this.storage.setDBVersion(this.dbVer) + this.storage.setDBIndex(this.dbIndex) + + // Handle Datamanagers + this.chars = new CharacterDataManager(this) + + // Weapons needs to be instantiated after character to check for relations + this.lightCones = new LightConeDataManager(this) + + // Artifacts needs to be instantiated after character to check for relations + this.relics = new RelicDataManager(this) + + this.buildSettings = new BuildSettingDataManager(this) + + // This should be instantiated after artifacts, so that invalid artifacts that persists in build results can be pruned. + this.buildResult = new BuildResultDataManager(this) + + this.charMeta = new CharMetaDataManager(this) + + // Handle DataEntries + this.dbMeta = new DBMetaEntry(this) + + // invalidates character when things change. + this.chars.followAny(() => { + this.dbMeta.set({ lastEdit: Date.now() }) + }) + this.relics.followAny(() => { + this.dbMeta.set({ lastEdit: Date.now() }) + }) + this.lightCones.followAny(() => { + this.dbMeta.set({ lastEdit: Date.now() }) + }) + } + get dataManagers() { + // IMPORTANT: it must be chars, weapon, arts in order, to respect import order + return [ + this.chars, + this.lightCones, + this.relics, + this.buildSettings, + this.buildResult, + this.charMeta, + ] as const + } + get dataEntries() { + return [this.dbMeta] as const + } + + clear() { + this.dataManagers.map((dm) => dm.clear()) + this.dataEntries.map((de) => de.clear()) + } + get gender() { + const gender: GenderKey = this.dbMeta.get().gender ?? 'F' + return gender + } + exportSROD() { + const srod: Partial = { + format: 'SROD', + dbVersion: currentDBVersion, + source: SroSource, + version: 1, + } + this.dataManagers.map((dm) => dm.exportSROD(srod)) + this.dataEntries.map((de) => de.exportSROD(srod)) + return srod as ISroDatabase & ISrObjectDescription + } + importSROD( + srod: ISrObjectDescription & ISroDatabase, + keepNotInImport: boolean, + ignoreDups: boolean + ): ImportResult { + srod = migrateSROD(srod) + const source = srod.source ?? 'Unknown' + // Some Scanners might carry their own id field, which would conflict with GO dup resolution. + if (source !== SroSource) { + srod.relics?.forEach((a) => delete (a as unknown as { id?: string }).id) + srod.lightCones?.forEach( + (a) => delete (a as unknown as { id?: string }).id + ) + } + const result: ImportResult = newImportResult( + source, + keepNotInImport, + ignoreDups + ) + + // Follow updates from char/relic/lightCone to gather import results + const unfollows = [ + this.chars.followAny((key, reason, value) => { + const arr = result.characters[reason] + const ind = arr.findIndex((c) => c?.key === key) + if (ind < 0) arr.push(value) + else arr[ind] = value + }), + this.relics.followAny((_key, reason, value) => + result.relics[reason].push(value) + ), + this.lightCones.followAny((_key, reason, value) => + result.lightCones[reason].push(value) + ), + ] + + this.dataManagers.map((dm) => dm.importSROD(srod, result)) + this.dataEntries.map((de) => de.importSROD(srod, result)) + unfollows.forEach((f) => f()) + + return result + } + clearStorage() { + this.dataManagers.map((dm) => dm.clearStorage()) + this.dataEntries.map((de) => de.clearStorage()) + } + saveStorage() { + this.dataManagers.map((dm) => dm.saveStorage()) + this.dataEntries.map((de) => de.saveStorage()) + this.storage.setDBVersion(this.dbVer) + this.storage.setDBIndex(this.dbIndex) + } + swapStorage(other: SroDatabase) { + this.clearStorage() + other.clearStorage() + + const thisStorage = this.storage + this.storage = other.storage + other.storage = thisStorage + + this.saveStorage() + other.saveStorage() + } + toExtraLocalDB() { + const key = `extraDatabase_${this.storage.getDBIndex()}` + const other = new SandboxStorage(undefined, 'sro') + const oldstorage = this.storage + this.storage = other + this.saveStorage() + this.storage = oldstorage + localStorage.setItem(key, JSON.stringify(Object.fromEntries(other.entries))) + } +} +export type DatabaseContextObj = { + databases: SroDatabase[] + setDatabase: (index: number, db: SroDatabase) => void + database: SroDatabase +} +export const DatabaseContext = createContext({} as DatabaseContextObj) diff --git a/libs/sr-db/src/Database/exim.ts b/libs/sr-db/src/Database/exim.ts new file mode 100644 index 0000000000..58e66412a4 --- /dev/null +++ b/libs/sr-db/src/Database/exim.ts @@ -0,0 +1,53 @@ +import type { ILightCone, IRelic } from '@genshin-optimizer/sr-srod' +import type { ISroCharacter } from '../Interfaces' + +function newCounter(): ImportResultCounter { + return { + import: 0, + invalid: [], + new: [], + update: [], + unchanged: [], + upgraded: [], + remove: [], + notInImport: 0, + beforeMerge: 0, + } +} + +export function newImportResult( + source: string, + keepNotInImport: boolean, + ignoreDups: boolean +): ImportResult { + return { + type: 'SR', + source, + relics: newCounter(), + lightCones: newCounter(), + characters: newCounter(), + keepNotInImport, + ignoreDups, + } +} + +export type ImportResultCounter = { + import: number // total # in file + new: T[] + update: T[] // Use new object + unchanged: T[] // Use new object + upgraded: T[] + remove: T[] + invalid: T[] + notInImport: number + beforeMerge: number +} +export type ImportResult = { + type: 'SR' + source: string + relics: ImportResultCounter + lightCones: ImportResultCounter + characters: ImportResultCounter + keepNotInImport: boolean + ignoreDups: boolean +} diff --git a/libs/sr-db/src/Database/index.ts b/libs/sr-db/src/Database/index.ts new file mode 100644 index 0000000000..79a45dbc02 --- /dev/null +++ b/libs/sr-db/src/Database/index.ts @@ -0,0 +1 @@ +export * from './Database' diff --git a/libs/sr-db/src/Database/migrate.ts b/libs/sr-db/src/Database/migrate.ts new file mode 100644 index 0000000000..ba7d021984 --- /dev/null +++ b/libs/sr-db/src/Database/migrate.ts @@ -0,0 +1,60 @@ +// MIGRATION STEP +// 0. DO NOT change old `migrateVersion` calls +// 1. Add new `migrateVersion` call within `migrateSr` function +// 2. Add new `migrateVersion` call within `migrateStorage` function +// 3. Update `currentDBVersion` +// 4. Test on import, and also on version update + +import type { DBStorage } from '@genshin-optimizer/database' +import type { ISrObjectDescription } from '@genshin-optimizer/sr-srod' +import type { ISroDatabase } from '../Interfaces' + +export const currentDBVersion = 1 + +export function migrateSr( + sr: ISrObjectDescription & ISroDatabase +): ISrObjectDescription & ISroDatabase { + const version = sr.dbVersion ?? 0 + // function migrateVersion(version: number, cb: () => void) { + // const dbver = sr.dbVersion ?? 0 + // if (dbver < version) { + // cb() + // // Update version upon each successful migration, so we don't + // // need to migrate that part again if later parts fail. + // sr.dbVersion = version + // } + // } + + // migrateVersion(2, () => {}) + + sr.dbVersion = currentDBVersion + if (version > currentDBVersion) + throw new Error(`Database version ${version} is not supported`) + return sr +} + +/** + * Migrate parsed data in `storage` in-place to a parsed data of the latest supported DB version. + * + * **CAUTION** + * Throw an error if `storage` uses unsupported DB version. + */ +export function migrateStorage(storage: DBStorage) { + const version = storage.getDBVersion() + + // function migrateVersion(version: number, cb: () => void) { + // const dbver = storage.getDBVersion() + // if (dbver < version) { + // cb() + // // Update version upon each successful migration, so we don't + // // need to migrate that part again if later parts fail. + // storage.setDBVersion(version) + // } + // } + + // migrateVersion(2, () => {}) + + storage.setDBVersion(currentDBVersion) + if (version > currentDBVersion) + throw new Error(`Database version ${version} is not supported`) +} diff --git a/libs/sr-db/src/ISroCharacter.ts b/libs/sr-db/src/Interfaces/ISroCharacter.ts similarity index 96% rename from libs/sr-db/src/ISroCharacter.ts rename to libs/sr-db/src/Interfaces/ISroCharacter.ts index cbb14d1da9..a8271b0d97 100644 --- a/libs/sr-db/src/ISroCharacter.ts +++ b/libs/sr-db/src/Interfaces/ISroCharacter.ts @@ -26,5 +26,5 @@ export interface ISroCharacter extends ICharacter { export interface ICachedSroCharacter extends ISroCharacter { equippedRelics: Record - equippedLightCone: string + equippedLightCone?: string } diff --git a/libs/sr-db/src/ISroDatabase.ts b/libs/sr-db/src/Interfaces/ISroDatabase.ts similarity index 93% rename from libs/sr-db/src/ISroDatabase.ts rename to libs/sr-db/src/Interfaces/ISroDatabase.ts index 6addcb2e17..be71e9e0e6 100644 --- a/libs/sr-db/src/ISroDatabase.ts +++ b/libs/sr-db/src/Interfaces/ISroDatabase.ts @@ -4,7 +4,6 @@ export const SroSource = 'Star Rail Optimizer' as const export const SroFormat = 'SRO' as const export interface ISroDatabase extends ISrObjectDescription { - format: typeof SroFormat version: 1 dbVersion: number source: typeof SroSource diff --git a/libs/sr-db/src/Interfaces/ISroLightCone.ts b/libs/sr-db/src/Interfaces/ISroLightCone.ts new file mode 100644 index 0000000000..23bb96f66e --- /dev/null +++ b/libs/sr-db/src/Interfaces/ISroLightCone.ts @@ -0,0 +1,5 @@ +import type { ILightCone } from '@genshin-optimizer/sr-srod' + +export interface ICachedLightCone extends ILightCone { + id: string +} diff --git a/libs/sr-db/src/Interfaces/ISroRelic.ts b/libs/sr-db/src/Interfaces/ISroRelic.ts new file mode 100644 index 0000000000..791a6a77cf --- /dev/null +++ b/libs/sr-db/src/Interfaces/ISroRelic.ts @@ -0,0 +1,13 @@ +import type { IRelic, ISubstat } from '@genshin-optimizer/sr-srod' + +export interface ICachedRelic extends IRelic { + id: string + mainStatVal: number + substats: ICachedSubstat[] +} + +export interface ICachedSubstat extends ISubstat { + rolls: number[] + efficiency: number + accurateValue: number +} diff --git a/libs/sr-db/src/Interfaces/index.ts b/libs/sr-db/src/Interfaces/index.ts new file mode 100644 index 0000000000..800eef268d --- /dev/null +++ b/libs/sr-db/src/Interfaces/index.ts @@ -0,0 +1,4 @@ +export * from './ISroCharacter' +export * from './ISroDatabase' +export * from './ISroLightCone' +export * from './ISroRelic' diff --git a/libs/sr-db/src/index.ts b/libs/sr-db/src/index.ts index f51df54c23..85ceb4bf91 100644 --- a/libs/sr-db/src/index.ts +++ b/libs/sr-db/src/index.ts @@ -1,2 +1,2 @@ -export * from './ISroCharacter' -export * from './ISroDatabase' +export * from './Interfaces' +export * from './Database' diff --git a/libs/sr-db/tsconfig.json b/libs/sr-db/tsconfig.json index db7b566661..61630f6864 100644 --- a/libs/sr-db/tsconfig.json +++ b/libs/sr-db/tsconfig.json @@ -7,13 +7,19 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": false, + "noUnusedLocals": true, + "noUnusedParameters": true }, "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" } ] } diff --git a/libs/sr-db/tsconfig.spec.json b/libs/sr-db/tsconfig.spec.json new file mode 100644 index 0000000000..9f6b5825cc --- /dev/null +++ b/libs/sr-db/tsconfig.spec.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "node"] + }, + "include": [ + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/sr-db/vitest.config.ts b/libs/sr-db/vitest.config.ts new file mode 100644 index 0000000000..132b0c5118 --- /dev/null +++ b/libs/sr-db/vitest.config.ts @@ -0,0 +1,19 @@ +import { resolve } from 'path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + environment: 'jsdom', + }, + resolve: { + alias: [ + // e.g. Resolves '@genshin-optimizer/pando' -> 'libs/pando/src' + { find: /@genshin-optimizer\/(.*)/, replacement: resolve('libs/$1/src') }, + ], + }, +}) diff --git a/libs/sr-srod/src/IRelic.ts b/libs/sr-srod/src/IRelic.ts index 7b51f7c7b0..fc0a9f2e4c 100644 --- a/libs/sr-srod/src/IRelic.ts +++ b/libs/sr-srod/src/IRelic.ts @@ -1,7 +1,7 @@ import type { LocationKey, - RarityKey, RelicMainStatKey, + RelicRarityKey, RelicSetKey, RelicSlotKey, RelicSubStatKey, @@ -11,7 +11,7 @@ export interface IRelic { setKey: RelicSetKey slotKey: RelicSlotKey level: number - rarity: RarityKey + rarity: RelicRarityKey mainStatKey: RelicMainStatKey location: LocationKey lock: boolean diff --git a/libs/sr-util/.eslintrc.json b/libs/sr-util/.eslintrc.json new file mode 100644 index 0000000000..9d9c0db55b --- /dev/null +++ b/libs/sr-util/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/sr-util/README.md b/libs/sr-util/README.md new file mode 100644 index 0000000000..0630119d22 --- /dev/null +++ b/libs/sr-util/README.md @@ -0,0 +1,3 @@ +# sr-util + +This library was generated with [Nx](https://nx.dev). diff --git a/libs/sr-util/project.json b/libs/sr-util/project.json new file mode 100644 index 0000000000..20097fdf49 --- /dev/null +++ b/libs/sr-util/project.json @@ -0,0 +1,16 @@ +{ + "name": "sr-util", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/sr-util/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/sr-util/**/*.ts"] + } + } + }, + "tags": [] +} diff --git a/libs/sr-util/src/index.ts b/libs/sr-util/src/index.ts new file mode 100644 index 0000000000..973a8361fb --- /dev/null +++ b/libs/sr-util/src/index.ts @@ -0,0 +1,2 @@ +export * from './level' +export * from './relic' diff --git a/libs/sr-util/src/level.ts b/libs/sr-util/src/level.ts new file mode 100644 index 0000000000..9864ef4a67 --- /dev/null +++ b/libs/sr-util/src/level.ts @@ -0,0 +1,20 @@ +import type { AscensionKey } from '@genshin-optimizer/sr-consts' +export const ascensionMaxLevel = [20, 30, 40, 50, 60, 70, 80] as const + +export function validateLevelAsc( + level: number, + ascension: AscensionKey +): { level: number; ascension: AscensionKey } { + if (typeof level !== 'number' || level < 1 || level > 80) level = 1 + if (typeof ascension !== 'number' || ascension < 0 || ascension > 6) + ascension = 0 + + if ( + level > ascensionMaxLevel[ascension] || + level < (ascensionMaxLevel[ascension - 1] ?? 0) + ) + ascension = ascensionMaxLevel.findIndex( + (maxLvl) => level <= maxLvl + ) as AscensionKey + return { level, ascension } +} diff --git a/libs/sr-util/src/relic.ts b/libs/sr-util/src/relic.ts new file mode 100644 index 0000000000..8caab1eaec --- /dev/null +++ b/libs/sr-util/src/relic.ts @@ -0,0 +1,141 @@ +import type { RelicSlotKey } from '@genshin-optimizer/sr-consts' +import { + allRelicCavernSlotKeys, + allRelicPlanarSetKeys, + allRelicPlanarSlotKeys, + allRelicRarityKeys, + allRelicSetKeys, + allRelicSubStatKeys, + relicMaxLevel, + relicSlotToMainStatKeys, + relicSubstatRollData, + type RelicMainStatKey, + type RelicRarityKey, + type RelicSubStatKey, +} from '@genshin-optimizer/sr-consts' +import type { IRelic, ISubstat } from '@genshin-optimizer/sr-srod' +import { allStats } from '@genshin-optimizer/sr-stats' +import { + getRandomElementFromArray, + getRandomIntInclusive, + range, + toPercent, + unit, +} from '@genshin-optimizer/util' + +export function getRelicMainStatVal( + rarity: RelicRarityKey, + statKey: RelicMainStatKey, + level: number +) { + const { base, add } = allStats.relic.main[rarity][statKey] ?? {} + if (base === undefined || add === undefined) + throw new Error( + `Attempted to get relic main stat value that doesn't exist for a level ${level} ${rarity}-star, ${statKey} relic.` + ) + return base + add * level +} + +export function getRelicMainStatDisplayVal( + rarity: RelicRarityKey, + statKey: RelicMainStatKey, + level: number +) { + return roundStat( + toPercent(getRelicMainStatVal(rarity, statKey, level), statKey), + statKey + ) +} + +// TODO: Update this with proper corrected rolls +export function getSubstatValue( + rarity: RelicRarityKey, + statKey: RelicSubStatKey, + type: 'low' | 'med' | 'high' = 'high', + round = true +) { + const { base, step } = allStats.relic.sub[rarity][statKey] ?? {} + if (base === undefined || step === undefined) + throw new Error( + `Attempted to get relic sub stat value that doesn't exist for a ${rarity}-star relic with substat ${statKey}.` + ) + const steps = type === 'low' ? 0 : type === 'med' ? 1 : 2 + const value = base + steps * step + return round ? roundStat(value, statKey) : value +} + +// TODO: Update this with proper corrected rolls +export function getSubstatRange( + rarity: RelicRarityKey, + statKey: RelicSubStatKey, + round = true +) { + const { numUpgrades } = relicSubstatRollData[rarity] + const high = + getSubstatValue(rarity, statKey, 'high', false) * (numUpgrades + 1) + return { + low: getSubstatValue(rarity, statKey, 'low', round), + high: round ? roundStat(high, statKey) : high, + } +} + +export function randomizeRelic(base: Partial = {}): IRelic { + const setKey = base.setKey ?? getRandomElementFromArray(allRelicSetKeys) + + const rarity = base.rarity ?? getRandomElementFromArray(allRelicRarityKeys) + const slot: RelicSlotKey = + base.slotKey ?? + getRandomElementFromArray( + [...(allRelicPlanarSetKeys as readonly string[])].includes(setKey) + ? allRelicPlanarSlotKeys + : allRelicCavernSlotKeys + ) + const mainStatKey: RelicMainStatKey = + base.mainStatKey ?? getRandomElementFromArray(relicSlotToMainStatKeys[slot]) + const level = base.level ?? getRandomIntInclusive(0, relicMaxLevel[rarity]) + const substats: ISubstat[] = [0, 1, 2, 3].map(() => ({ key: '', value: 0 })) + + const { low, high } = relicSubstatRollData[rarity] + const totRolls = Math.floor(level / 3) + getRandomIntInclusive(low, high) + const numOfInitialSubstats = Math.min(totRolls, 4) + const numUpgradesOrUnlocks = totRolls - numOfInitialSubstats + + const RollStat = (substat: RelicSubStatKey): number => + allStats.relic.sub[rarity][substat].base + + getRandomElementFromArray(range(0, 2)) * + allStats.relic.sub[rarity][substat].step + + let remainingSubstats = allRelicSubStatKeys.filter( + (key) => mainStatKey !== key + ) + for (const substat of substats.slice(0, numOfInitialSubstats)) { + substat.key = getRandomElementFromArray(remainingSubstats) + substat.value = RollStat(substat.key as RelicSubStatKey) + remainingSubstats = remainingSubstats.filter((key) => key !== substat.key) + } + for (let i = 0; i < numUpgradesOrUnlocks; i++) { + const substat = getRandomElementFromArray(substats) + substat.value += RollStat(substat.key as any) + } + for (const substat of substats) + if (substat.key) { + substat.value = roundStat(substat.value, substat.key) + } + + return { + setKey, + rarity, + slotKey: slot, + mainStatKey, + level, + substats, + location: base.location ?? '', + lock: false, + } +} + +function roundStat(value: number, statKey: RelicMainStatKey | RelicSubStatKey) { + return unit(statKey) === '%' + ? Math.round(value * 10000) / 10000 + : Math.round(value * 100) / 100 +} diff --git a/libs/sr-util/tsconfig.json b/libs/sr-util/tsconfig.json new file mode 100644 index 0000000000..db7b566661 --- /dev/null +++ b/libs/sr-util/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/sr-util/tsconfig.lib.json b/libs/sr-util/tsconfig.lib.json new file mode 100644 index 0000000000..33eca2c2cd --- /dev/null +++ b/libs/sr-util/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/ui-common/src/components/GeneralAutocomplete.tsx b/libs/ui-common/src/components/GeneralAutocomplete.tsx index 72cb507ef3..b466f5d723 100644 --- a/libs/ui-common/src/components/GeneralAutocomplete.tsx +++ b/libs/ui-common/src/components/GeneralAutocomplete.tsx @@ -178,7 +178,7 @@ export function GeneralAutocompleteMulti({ multiple disableCloseOnSelect value={value} - onChange={(event, newValue, reason) => { + onChange={(_event, newValue, reason) => { if (reason === 'clear') return onChange([]) return newValue !== null && onChange(newValue.map((v) => v.key)) }} diff --git a/libs/ui-common/tsconfig.json b/libs/ui-common/tsconfig.json index 95d63299d7..7ff89730ac 100644 --- a/libs/ui-common/tsconfig.json +++ b/libs/ui-common/tsconfig.json @@ -3,7 +3,9 @@ "jsx": "react-jsx", "esModuleInterop": false, "jsxImportSource": "@emotion/react", - "exactOptionalPropertyTypes": false + "exactOptionalPropertyTypes": false, + "noUnusedLocals": true, + "noUnusedParameters": true }, "files": [], "include": [], diff --git a/tsconfig.base.json b/tsconfig.base.json index aed68436fb..67735688e6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -69,6 +69,7 @@ "@genshin-optimizer/sr-formula": ["libs/sr-formula/src/index.ts"], "@genshin-optimizer/sr-srod": ["libs/sr-srod/src/index.ts"], "@genshin-optimizer/sr-stats": ["libs/sr-stats/src/index.ts"], + "@genshin-optimizer/sr-util": ["libs/sr-util/src/index.ts"], "@genshin-optimizer/svgicons": ["libs/svgicons/src/index.ts"], "@genshin-optimizer/ui-common": ["libs/ui-common/src/index.ts"], "@genshin-optimizer/util": ["libs/util/src/index.ts"] From 203af6e56d3c0481b164a829b3b2fae7b7cd8a95 Mon Sep 17 00:00:00 2001 From: Van Nguyen <36019388+nguyentvan7@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:42:56 -0700 Subject: [PATCH 11/17] Update naming for beta release; add SRO beta release (#1397) --- .../{beta-release.yml => beta-go-release.yml} | 2 +- .github/workflows/beta-sro-release.yml | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) rename .github/workflows/{beta-release.yml => beta-go-release.yml} (95%) create mode 100644 .github/workflows/beta-sro-release.yml diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-go-release.yml similarity index 95% rename from .github/workflows/beta-release.yml rename to .github/workflows/beta-go-release.yml index 9dde70c1ec..fc1b3ec254 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-go-release.yml @@ -1,4 +1,4 @@ -name: New Beta Release +name: New Beta GO Release on: push: diff --git a/.github/workflows/beta-sro-release.yml b/.github/workflows/beta-sro-release.yml new file mode 100644 index 0000000000..eadec771e7 --- /dev/null +++ b/.github/workflows/beta-sro-release.yml @@ -0,0 +1,23 @@ +name: New Beta SRO Release + +on: + push: + branches: + - master + +# Only allow the most recent run of beta release to complete, if multiple are queued. +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + call-deploy-frontend: + uses: ./.github/workflows/deploy-frontend.yml + with: + frontend_name: 'sro-frontend' + repo_full_name: ${{ github.repository }} + ref: ${{ github.ref }} + deployment_name: 'beta' + pr_repo: ${{ vars.PR_REPO }} + show_dev_components: true + secrets: inherit From e7a9fba631155501b6d5f85609d3f21c70f74035 Mon Sep 17 00:00:00 2001 From: Jie Hao Liao Date: Thu, 28 Dec 2023 21:17:21 -0800 Subject: [PATCH 12/17] Character talent tab translations (#1359) * Add all translations for character talent page * Use interpolation for levels in talent and constellation levels * use datamine translations * fix regex --------- Co-authored-by: frzyc --- .../CharacterDisplay/Tabs/TabTalent.tsx | 29 ++++++++++--------- libs/dm/src/mapping/index.ts | 1 + libs/gi-dm-localization/project.json | 3 +- .../executors/gen-locale/lib/Data/sheet.ts | 20 +++++++++++++ .../src/executors/gen-locale/lib/parseUtil.ts | 26 +++++++++++++++++ 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabTalent.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabTalent.tsx index defdcf6f3d..dc2f18eb6f 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabTalent.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabTalent.tsx @@ -10,6 +10,7 @@ import { useTheme, } from '@mui/material' import { useCallback, useContext, useMemo } from 'react' +import { useTranslation } from 'react-i18next' import CardDark from '../../../Components/Card/CardDark' import CardLight from '../../../Components/Card/CardLight' import ConditionalWrapper from '../../../Components/ConditionalWrapper' @@ -33,22 +34,23 @@ const talentSpacing = { } export default function CharacterTalentPane() { + const { t } = useTranslation('sheet_gen') const { character, characterSheet } = useContext(CharacterContext) const { data } = useContext(DataContext) const characterDispatch = useCharacterReducer(character.key) const skillBurstList = [ - ['auto', 'Normal/Charged Attack'], - ['skill', 'Elemental Skill'], - ['burst', 'Elemental Burst'], + ['auto', t('talents.auto')], + ['skill', t('talents.skill')], + ['burst', t('talents.burst')], ] as [TalentSheetElementKey, string][] const passivesList: [ tKey: TalentSheetElementKey, tText: string, asc: number ][] = [ - ['passive1', 'Unlocked at Ascension 1', 1], - ['passive2', 'Unlocked at Ascension 4', 4], - ['passive3', 'Unlocked by Default', 0], + ['passive1', t('unlockPassive1'), 1], + ['passive2', t('unlockPassive2'), 4], + ['passive3', t('unlockPassive3'), 0], ] const ascension = data.get(input.asc).value const constellation = data.get(input.constellation).value @@ -60,7 +62,7 @@ export default function CharacterTalentPane() { range(1, maxConstellationCount).map((i) => ( characterDispatch({ constellation: i === constellation ? i - 1 : i, @@ -68,12 +70,12 @@ export default function CharacterTalentPane() { } /> )), - [constellation, characterDispatch] + [t, constellation, characterDispatch] ) const constellationHeader = ( @@ -88,7 +90,7 @@ export default function CharacterTalentPane() { }) } > - Constellation Lv. {i} + {t(`constellationLvl`, { level: i })} ))} @@ -126,7 +128,7 @@ export default function CharacterTalentPane() { )} @@ -210,6 +212,7 @@ function SkillDisplayCard({ subtitle, onClickTitle, }: SkillDisplayCardProps) { + const { t } = useTranslation('sheet_gen') const { character: { talent }, characterSheet, @@ -242,7 +245,7 @@ function SkillDisplayCard({ header = ( @@ -253,7 +256,7 @@ function SkillDisplayCard({ disabled={talent[talentKey] === i} onClick={() => setTalentLevel(talentKey, i)} > - Talent Lv. {i + levelBoost} + {t('talentLvl', { level: i + levelBoost })} ))} diff --git a/libs/dm/src/mapping/index.ts b/libs/dm/src/mapping/index.ts index c8c5823994..46f9b3f8ec 100644 --- a/libs/dm/src/mapping/index.ts +++ b/libs/dm/src/mapping/index.ts @@ -11,6 +11,7 @@ export const tagColor = { FF9999FF: 'pyro', FFACFFFF: 'electro', '99FF88FF': 'dendro', + '00FFFFFF': 'strong', } as const export type ColorTag = (typeof tagColor)[keyof typeof tagColor] diff --git a/libs/gi-dm-localization/project.json b/libs/gi-dm-localization/project.json index 06751511c0..3faeb0e7f8 100644 --- a/libs/gi-dm-localization/project.json +++ b/libs/gi-dm-localization/project.json @@ -8,7 +8,8 @@ "executor": "@genshin-optimizer/gi-dm-localization:gen-locale", "outputs": ["{projectRoot}/assets/locales"] }, - "lint": {} + "lint": {}, + "test": {} }, "tags": [] } diff --git a/libs/gi-dm-localization/src/executors/gen-locale/lib/Data/sheet.ts b/libs/gi-dm-localization/src/executors/gen-locale/lib/Data/sheet.ts index 3993e2fae6..86ddee7d88 100644 --- a/libs/gi-dm-localization/src/executors/gen-locale/lib/Data/sheet.ts +++ b/libs/gi-dm-localization/src/executors/gen-locale/lib/Data/sheet.ts @@ -105,5 +105,25 @@ const data = { res: {}, misc: {}, }, + // Constellation Lv. {{level}} + constellationLvl: [892900816, 'constellation'], + // Talent Lv. {{level}} + talentLvl: [1647967600, 'talent'], + talents: { + // Normal Attack + auto: 1171619685, // or 1653327868 + // Elemental Skill + skill: 3477257188, // or 4260972229 + // Elemental Burst + burst: 3250738285, // 2453877364 3626565793 3152729845 + // Altenate Sprint + altSprint: [3378550992, 'altSprint'], // mona's desc + }, + // Unlocks at Character Ascension Phase 1 + unlockPassive1: [941237898, 'passive1'], + // Unlocks at Character Ascension Phase 4 + unlockPassive2: [941237898, 'passive4'], + // Passive Talent + unlockPassive3: 2602723764, } as const export default data diff --git a/libs/gi-dm-localization/src/executors/gen-locale/lib/parseUtil.ts b/libs/gi-dm-localization/src/executors/gen-locale/lib/parseUtil.ts index 2eebd0cc20..5291624f6e 100644 --- a/libs/gi-dm-localization/src/executors/gen-locale/lib/parseUtil.ts +++ b/libs/gi-dm-localization/src/executors/gen-locale/lib/parseUtil.ts @@ -175,4 +175,30 @@ export const parsingFunctions: { plungeLow: (lang, string) => plungeUtil(lang, string, true), plungeHigh: (lang, string) => plungeUtil(lang, string, false), string: (lang, string) => string, + constellation: (lang, string) => constellation(string), + talent: (lang, string) => talent(string), + altSprint: (lang, string) => altSprint(string), + passive1: (lang, string) => passive1(string), + passive4: (lang, string) => passive4(string), +} + +export function constellation(string: string) { + return string.replace('{0}', '{{level}}') +} + +export function talent(string: string) { + return string.replace('+{0}', '{{level}}') +} + +export function altSprint(string: string) { + const re = new RegExp(/(.+)<\/strong>/) + const match = re.exec(string) + return match?.[1] ?? '' +} + +export function passive1(string: string) { + return string.replace('{0}', '1') +} +export function passive4(string: string) { + return string.replace('{0}', '4') } From 95a03bc57594ccdb01d3406988f5c9f16341cc46 Mon Sep 17 00:00:00 2001 From: frzyc Date: Fri, 29 Dec 2023 00:57:27 -0500 Subject: [PATCH 13/17] minor scanner changes (#1390) --- apps/frontend/src/app/PageArtifact/ArtifactEditor.tsx | 2 +- libs/gi-art-scanner/src/lib/processImg.ts | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/app/PageArtifact/ArtifactEditor.tsx b/apps/frontend/src/app/PageArtifact/ArtifactEditor.tsx index 006cd3d22e..6d69ed28cf 100644 --- a/apps/frontend/src/app/PageArtifact/ArtifactEditor.tsx +++ b/apps/frontend/src/app/PageArtifact/ArtifactEditor.tsx @@ -145,7 +145,7 @@ export default function ArtifactEditor({ disableSlot = false, }: ArtifactEditorProps) { const queueRef = useRef( - new ScanningQueue(textsFromImage, process.env.NODE_ENV === 'development') + new ScanningQueue(textsFromImage, shouldShowDevComponents) ) const queue = queueRef.current const { t } = useTranslation('artifact') diff --git a/libs/gi-art-scanner/src/lib/processImg.ts b/libs/gi-art-scanner/src/lib/processImg.ts index 9d1411356f..f4ec2f9c03 100644 --- a/libs/gi-art-scanner/src/lib/processImg.ts +++ b/libs/gi-art-scanner/src/lib/processImg.ts @@ -271,15 +271,7 @@ export async function processEntry( const [whiteTexts, substatTexts, artifactSetTexts, equippedTexts] = await Promise.all([ // slotkey, mainStatValue, level - textsFromImage(bwHeader, { - // only the left half is worth scanning - rectangle: { - top: 0, - left: 0, - width: Math.floor(bwHeader.width * 0.7), - height: bwHeader.height, - }, - }), + textsFromImage(bwHeader), // substats textsFromImage(substatsCardCropped), // artifact set, look for greenish texts From 8376e9e2ebd7379c4a9226149fc5427173535433 Mon Sep 17 00:00:00 2001 From: frzyc Date: Fri, 29 Dec 2023 00:58:31 -0500 Subject: [PATCH 14/17] more GO-next work (#1385) * update to latest * additional Character/weapon/artifact UI * format * update MUI cache provider * update * update WeaponCard --- .gitignore | 3 + .prettierignore | 3 + ...extjs-npm-5.15.0-74d10e1494-1bf462479b.zip | Bin 0 -> 218304 bytes .../Components/Artifact/ArtifactTooltip.tsx | 32 +- apps/gi-backend/src/app/graphql_gen.ts | 198 ++++++++++++- apps/gi-backend/src/app/schema_gen.graphql | 198 ++++++++++++- .../src/app/weapon/weapon.entity.ts | 18 +- .../src/app/weapon/weapon.resolver.ts | 12 +- apps/gi-frontend-next/next.config.js | 3 + .../character/components/CharacterList.tsx | 31 +- .../src/app/[locale]/character/page.tsx | 25 +- .../components/Header/MobileHeader.tsx | 2 +- .../[locale]/components/Header/TabsData.tsx | 22 +- .../app/[locale]/components/Header/index.tsx | 4 +- .../ThemeRegistry/EmotionCache.tsx | 99 ------- .../ThemeRegistry/ThemeRegistry.tsx | 11 +- .../layoutWrappers/ThemeRegistry/theme.ts | 1 + .../weapon/components/AddWeaponButton.tsx | 21 +- apps/gi-frontend-next/src/auth/jwt.ts | 2 +- .../src/components/FieldDisplay.tsx | 4 +- .../src/client/Artifact/ArtifactCardPico.tsx | 69 +++++ .../client/Artifact/ArtifactCardPicoBlank.tsx | 33 +++ .../src/client/Artifact/ArtifactTooltip.tsx | 96 ++++++ libs/gi-ui-next/src/client/Artifact/index.ts | 2 + libs/gi-ui-next/src/client/ArtifactCard.tsx | 5 +- .../ArtifactEditor/ArtifactSlotDropdown.tsx | 4 +- libs/gi-ui-next/src/client/CharacterCard.tsx | 278 ++++-------------- libs/gi-ui-next/src/client/CloseButton.tsx | 10 +- .../src/client/GenshinUserDataWrapper.tsx | 5 +- .../src/client/LocationAutocomplete.tsx | 4 +- libs/gi-ui-next/src/client/LocationName.tsx | 4 +- .../src/client/{ => Weapon}/WeaponCard.tsx | 132 +++++---- .../src/client/Weapon/WeaponCardPico.tsx | 125 ++++++++ .../src/client/Weapon/WeaponNameTooltip.tsx | 40 +++ libs/gi-ui-next/src/client/Weapon/index.ts | 2 + libs/gi-ui-next/src/client/index.ts | 3 +- libs/gi-ui/src/Artifact/IconStatDisplay.tsx | 36 +++ libs/gi-ui/src/Artifact/index.ts | 1 + libs/gi-ui/src/Translate.tsx | 1 + .../src/components/BootstrapTooltip.tsx | 1 + .../src/components/Card/CardThemed.tsx | 1 + libs/ui-common/src/components/ColorText.tsx | 1 + .../src/components/DropdownButton.tsx | 7 +- .../src/components/GeneralAutocomplete.tsx | 6 +- .../ui-common/src/components/ModalWrapper.tsx | 1 + libs/ui-common/src/components/SqBadge.tsx | 1 + libs/ui-common/src/components/TextButton.tsx | 1 + libs/ui-common/src/theme/index.ts | 1 + package.json | 1 + yarn.lock | 22 ++ 50 files changed, 1099 insertions(+), 483 deletions(-) create mode 100644 .yarn/cache/@mui-material-nextjs-npm-5.15.0-74d10e1494-1bf462479b.zip delete mode 100644 apps/gi-frontend-next/src/app/[locale]/layoutWrappers/ThemeRegistry/EmotionCache.tsx create mode 100644 libs/gi-ui-next/src/client/Artifact/ArtifactCardPico.tsx create mode 100644 libs/gi-ui-next/src/client/Artifact/ArtifactCardPicoBlank.tsx create mode 100644 libs/gi-ui-next/src/client/Artifact/ArtifactTooltip.tsx create mode 100644 libs/gi-ui-next/src/client/Artifact/index.ts rename libs/gi-ui-next/src/client/{ => Weapon}/WeaponCard.tsx (72%) create mode 100644 libs/gi-ui-next/src/client/Weapon/WeaponCardPico.tsx create mode 100644 libs/gi-ui-next/src/client/Weapon/WeaponNameTooltip.tsx create mode 100644 libs/gi-ui-next/src/client/Weapon/index.ts create mode 100644 libs/gi-ui/src/Artifact/IconStatDisplay.tsx diff --git a/.gitignore b/.gitignore index a5b75ccd53..f56e7fb896 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ Thumbs.db # Next.js .next + +# nx +.nx/cache diff --git a/.prettierignore b/.prettierignore index 5f08e61f1f..2d093240bc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,3 +14,6 @@ # generated from backend apps/gi-backend/src/app/graphql_gen.ts apps/gi-backend/src/app/schema_gen.graphql + +# nx +/.nx/cache diff --git a/.yarn/cache/@mui-material-nextjs-npm-5.15.0-74d10e1494-1bf462479b.zip b/.yarn/cache/@mui-material-nextjs-npm-5.15.0-74d10e1494-1bf462479b.zip new file mode 100644 index 0000000000000000000000000000000000000000..0c57b50d745d00a214dfdaa92cdeae229b03ccba GIT binary patch literal 218304 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�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 z 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 OZk_-E<#T}m{`x;Ae9!0r literal 0 HcmV?d00001 diff --git a/apps/frontend/src/app/Components/Artifact/ArtifactTooltip.tsx b/apps/frontend/src/app/Components/Artifact/ArtifactTooltip.tsx index b8d1beaaa2..170158cb3b 100644 --- a/apps/frontend/src/app/Components/Artifact/ArtifactTooltip.tsx +++ b/apps/frontend/src/app/Components/Artifact/ArtifactTooltip.tsx @@ -1,9 +1,8 @@ -import { - artDisplayValue, - getMainStatDisplayStr, -} from '@genshin-optimizer/gi-util' +import type { RollColorKey } from '@genshin-optimizer/gi-ui' +import { IconStatDisplay } from '@genshin-optimizer/gi-ui' +import { getMainStatDisplayStr } from '@genshin-optimizer/gi-util' import { iconInlineProps } from '@genshin-optimizer/svgicons' -import { clamp, unit } from '@genshin-optimizer/util' +import { clamp } from '@genshin-optimizer/util' import { Box, Skeleton, Typography } from '@mui/material' import { Suspense } from 'react' import { useTranslation } from 'react-i18next' @@ -11,7 +10,7 @@ import { getArtSheet } from '../../Data/Artifacts' import Artifact from '../../Data/Artifacts/Artifact' import KeyMap from '../../KeyMap' import StatIcon from '../../KeyMap/StatIcon' -import type { ICachedArtifact, ICachedSubstat } from '../../Types/artifact' +import type { ICachedArtifact } from '../../Types/artifact' import BootstrapTooltip from '../BootstrapTooltip' import LocationName from '../Character/LocationName' import SqBadge from '../SqBadge' @@ -70,18 +69,15 @@ function ArtifactData({ art }: { art: ICachedArtifact }) { {substats.map( - (stat: ICachedSubstat) => - !!stat.value && ( - - {' '} - {tk(stat.key)}{' '} - {`+${artDisplayValue(stat.value, unit(stat.key))}${unit( - stat.key - )}`} - + ({ value, key, rolls }) => + !!(value && key) && ( + ) )} diff --git a/apps/gi-backend/src/app/graphql_gen.ts b/apps/gi-backend/src/app/graphql_gen.ts index c6a859e61f..46b86ca1be 100644 --- a/apps/gi-backend/src/app/graphql_gen.ts +++ b/apps/gi-backend/src/app/graphql_gen.ts @@ -34,6 +34,7 @@ export enum ArtifactSetKey { MaidenBeloved = "MaidenBeloved", MarechausseeHunter = "MarechausseeHunter", MartialArtist = "MartialArtist", + NighttimeWhispersInTheEchoingWoods = "NighttimeWhispersInTheEchoingWoods", NoblesseOblige = "NoblesseOblige", NymphsDream = "NymphsDream", OceanHuedClam = "OceanHuedClam", @@ -46,6 +47,7 @@ export enum ArtifactSetKey { RetracingBolide = "RetracingBolide", Scholar = "Scholar", ShimenawasReminiscence = "ShimenawasReminiscence", + SongOfDaysPast = "SongOfDaysPast", TenacityOfTheMillelith = "TenacityOfTheMillelith", TheExile = "TheExile", ThunderingFury = "ThunderingFury", @@ -132,6 +134,7 @@ export enum LocationKey { Mika = "Mika", Mona = "Mona", Nahida = "Nahida", + Navia = "Navia", Neuvillette = "Neuvillette", Nilou = "Nilou", Ningguang = "Ningguang", @@ -179,6 +182,188 @@ export enum SubstatKey { critDMG_ = "critDMG_" } +export enum WeaponKey { + AmenomaKageuchi = "AmenomaKageuchi", + AquilaFavonia = "AquilaFavonia", + BlackcliffLongsword = "BlackcliffLongsword", + CinnabarSpindle = "CinnabarSpindle", + CoolSteel = "CoolSteel", + DarkIronSword = "DarkIronSword", + DullBlade = "DullBlade", + FavoniusSword = "FavoniusSword", + FesteringDesire = "FesteringDesire", + FilletBlade = "FilletBlade", + FinaleOfTheDeep = "FinaleOfTheDeep", + FleuveCendreFerryman = "FleuveCendreFerryman", + FreedomSworn = "FreedomSworn", + HaranGeppakuFutsu = "HaranGeppakuFutsu", + HarbingerOfDawn = "HarbingerOfDawn", + IronSting = "IronSting", + KagotsurubeIsshin = "KagotsurubeIsshin", + KeyOfKhajNisut = "KeyOfKhajNisut", + LightOfFoliarIncision = "LightOfFoliarIncision", + LionsRoar = "LionsRoar", + MistsplitterReforged = "MistsplitterReforged", + PrimordialJadeCutter = "PrimordialJadeCutter", + PrototypeRancour = "PrototypeRancour", + RoyalLongsword = "RoyalLongsword", + SacrificialSword = "SacrificialSword", + SapwoodBlade = "SapwoodBlade", + SilverSword = "SilverSword", + SkyriderSword = "SkyriderSword", + SkywardBlade = "SkywardBlade", + SplendorOfTranquilWaters = "SplendorOfTranquilWaters", + SummitShaper = "SummitShaper", + SwordOfDescension = "SwordOfDescension", + SwordOfNarzissenkreuz = "SwordOfNarzissenkreuz", + TheAlleyFlash = "TheAlleyFlash", + TheBlackSword = "TheBlackSword", + TheDockhandsAssistant = "TheDockhandsAssistant", + TheFlute = "TheFlute", + ToukabouShigure = "ToukabouShigure", + TravelersHandySword = "TravelersHandySword", + WolfFang = "WolfFang", + XiphosMoonlight = "XiphosMoonlight", + Akuoumaru = "Akuoumaru", + BeaconOfTheReedSea = "BeaconOfTheReedSea", + BlackcliffSlasher = "BlackcliffSlasher", + BloodtaintedGreatsword = "BloodtaintedGreatsword", + DebateClub = "DebateClub", + FavoniusGreatsword = "FavoniusGreatsword", + FerrousShadow = "FerrousShadow", + ForestRegalia = "ForestRegalia", + KatsuragikiriNagamasa = "KatsuragikiriNagamasa", + LithicBlade = "LithicBlade", + LuxuriousSeaLord = "LuxuriousSeaLord", + MailedFlower = "MailedFlower", + MakhairaAquamarine = "MakhairaAquamarine", + OldMercsPal = "OldMercsPal", + PortablePowerSaw = "PortablePowerSaw", + PrototypeArchaic = "PrototypeArchaic", + Rainslasher = "Rainslasher", + RedhornStonethresher = "RedhornStonethresher", + RoyalGreatsword = "RoyalGreatsword", + SacrificialGreatsword = "SacrificialGreatsword", + SerpentSpine = "SerpentSpine", + SkyriderGreatsword = "SkyriderGreatsword", + SkywardPride = "SkywardPride", + SnowTombedStarsilver = "SnowTombedStarsilver", + SongOfBrokenPines = "SongOfBrokenPines", + TalkingStick = "TalkingStick", + TheBell = "TheBell", + TheUnforged = "TheUnforged", + TidalShadow = "TidalShadow", + UltimateOverlordsMegaMagicSword = "UltimateOverlordsMegaMagicSword", + Verdict = "Verdict", + WasterGreatsword = "WasterGreatsword", + Whiteblind = "Whiteblind", + WhiteIronGreatsword = "WhiteIronGreatsword", + WolfsGravestone = "WolfsGravestone", + BalladOfTheFjords = "BalladOfTheFjords", + BeginnersProtector = "BeginnersProtector", + BlackcliffPole = "BlackcliffPole", + BlackTassel = "BlackTassel", + CalamityQueller = "CalamityQueller", + CrescentPike = "CrescentPike", + Deathmatch = "Deathmatch", + DragonsBane = "DragonsBane", + DragonspineSpear = "DragonspineSpear", + EngulfingLightning = "EngulfingLightning", + FavoniusLance = "FavoniusLance", + Halberd = "Halberd", + IronPoint = "IronPoint", + KitainCrossSpear = "KitainCrossSpear", + LithicSpear = "LithicSpear", + MissiveWindspear = "MissiveWindspear", + Moonpiercer = "Moonpiercer", + PrimordialJadeWingedSpear = "PrimordialJadeWingedSpear", + ProspectorsDrill = "ProspectorsDrill", + PrototypeStarglitter = "PrototypeStarglitter", + RightfulReward = "RightfulReward", + RoyalSpear = "RoyalSpear", + SkywardSpine = "SkywardSpine", + StaffOfHoma = "StaffOfHoma", + StaffOfTheScarletSands = "StaffOfTheScarletSands", + TheCatch = "TheCatch", + VortexVanquisher = "VortexVanquisher", + WavebreakersFin = "WavebreakersFin", + WhiteTassel = "WhiteTassel", + AlleyHunter = "AlleyHunter", + AmosBow = "AmosBow", + AquaSimulacra = "AquaSimulacra", + BlackcliffWarbow = "BlackcliffWarbow", + CompoundBow = "CompoundBow", + ElegyForTheEnd = "ElegyForTheEnd", + EndOfTheLine = "EndOfTheLine", + FadingTwilight = "FadingTwilight", + FavoniusWarbow = "FavoniusWarbow", + Hamayumi = "Hamayumi", + HuntersBow = "HuntersBow", + HuntersPath = "HuntersPath", + IbisPiercer = "IbisPiercer", + KingsSquire = "KingsSquire", + Messenger = "Messenger", + MitternachtsWaltz = "MitternachtsWaltz", + MouunsMoon = "MouunsMoon", + PolarStar = "PolarStar", + Predator = "Predator", + PrototypeCrescent = "PrototypeCrescent", + RangeGauge = "RangeGauge", + RavenBow = "RavenBow", + RecurveBow = "RecurveBow", + RoyalBow = "RoyalBow", + Rust = "Rust", + SacrificialBow = "SacrificialBow", + ScionOfTheBlazingSun = "ScionOfTheBlazingSun", + SeasonedHuntersBow = "SeasonedHuntersBow", + SharpshootersOath = "SharpshootersOath", + SkywardHarp = "SkywardHarp", + Slingshot = "Slingshot", + SongOfStillness = "SongOfStillness", + TheFirstGreatMagic = "TheFirstGreatMagic", + TheStringless = "TheStringless", + TheViridescentHunt = "TheViridescentHunt", + ThunderingPulse = "ThunderingPulse", + WindblumeOde = "WindblumeOde", + ApprenticesNotes = "ApprenticesNotes", + AThousandFloatingDreams = "AThousandFloatingDreams", + BalladOfTheBoundlessBlue = "BalladOfTheBoundlessBlue", + BlackcliffAgate = "BlackcliffAgate", + CashflowSupervision = "CashflowSupervision", + DodocoTales = "DodocoTales", + EmeraldOrb = "EmeraldOrb", + EverlastingMoonglow = "EverlastingMoonglow", + EyeOfPerception = "EyeOfPerception", + FavoniusCodex = "FavoniusCodex", + FlowingPurity = "FlowingPurity", + Frostbearer = "Frostbearer", + FruitOfFulfillment = "FruitOfFulfillment", + HakushinRing = "HakushinRing", + JadefallsSplendor = "JadefallsSplendor", + KagurasVerity = "KagurasVerity", + LostPrayerToTheSacredWinds = "LostPrayerToTheSacredWinds", + MagicGuide = "MagicGuide", + MappaMare = "MappaMare", + MemoryOfDust = "MemoryOfDust", + OathswornEye = "OathswornEye", + OtherworldlyStory = "OtherworldlyStory", + PocketGrimoire = "PocketGrimoire", + PrototypeAmber = "PrototypeAmber", + QuantumCatalyst = "QuantumCatalyst", + RoyalGrimoire = "RoyalGrimoire", + SacrificialFragments = "SacrificialFragments", + SacrificialJade = "SacrificialJade", + SkywardAtlas = "SkywardAtlas", + SolarPearl = "SolarPearl", + TheWidsith = "TheWidsith", + ThrillingTalesOfDragonSlayers = "ThrillingTalesOfDragonSlayers", + TomeOfTheEternalFlow = "TomeOfTheEternalFlow", + TulaytullahsRemembrance = "TulaytullahsRemembrance", + TwinNephrite = "TwinNephrite", + WanderingEvenstar = "WanderingEvenstar", + WineAndSong = "WineAndSong" +} + export enum CharacterKey { Albedo = "Albedo", Alhaitham = "Alhaitham", @@ -224,6 +409,7 @@ export enum CharacterKey { Mika = "Mika", Mona = "Mona", Nahida = "Nahida", + Navia = "Navia", Neuvillette = "Neuvillette", Nilou = "Nilou", Ningguang = "Ningguang", @@ -291,20 +477,20 @@ export interface UpdateArtifact { } export interface InputWeapon { - key: string; + key: WeaponKey; level: number; ascension: number; refinement: number; - location?: Nullable; + location?: Nullable; lock: boolean; } export interface UpdateWeapon { - key?: Nullable; + key?: Nullable; level?: Nullable; ascension?: Nullable; refinement?: Nullable; - location?: Nullable; + location?: Nullable; lock?: Nullable; id: string; } @@ -374,11 +560,11 @@ export interface Substat { export interface Weapon { id: string; genshinUserId: string; - key: string; + key: WeaponKey; level: number; ascension: number; refinement: number; - location?: Nullable; + location?: Nullable; lock: boolean; } diff --git a/apps/gi-backend/src/app/schema_gen.graphql b/apps/gi-backend/src/app/schema_gen.graphql index eec9c29ae1..6d64265b05 100644 --- a/apps/gi-backend/src/app/schema_gen.graphql +++ b/apps/gi-backend/src/app/schema_gen.graphql @@ -63,6 +63,7 @@ enum ArtifactSetKey { MaidenBeloved MarechausseeHunter MartialArtist + NighttimeWhispersInTheEchoingWoods NoblesseOblige NymphsDream OceanHuedClam @@ -75,6 +76,7 @@ enum ArtifactSetKey { RetracingBolide Scholar ShimenawasReminiscence + SongOfDaysPast TenacityOfTheMillelith TheExile ThunderingFury @@ -161,6 +163,7 @@ enum LocationKey { Mika Mona Nahida + Navia Neuvillette Nilou Ningguang @@ -216,14 +219,196 @@ enum SubstatKey { type Weapon { id: ID! genshinUserId: String! - key: String! + key: WeaponKey! level: Int! ascension: Int! refinement: Int! - location: String + location: LocationKey lock: Boolean! } +enum WeaponKey { + AmenomaKageuchi + AquilaFavonia + BlackcliffLongsword + CinnabarSpindle + CoolSteel + DarkIronSword + DullBlade + FavoniusSword + FesteringDesire + FilletBlade + FinaleOfTheDeep + FleuveCendreFerryman + FreedomSworn + HaranGeppakuFutsu + HarbingerOfDawn + IronSting + KagotsurubeIsshin + KeyOfKhajNisut + LightOfFoliarIncision + LionsRoar + MistsplitterReforged + PrimordialJadeCutter + PrototypeRancour + RoyalLongsword + SacrificialSword + SapwoodBlade + SilverSword + SkyriderSword + SkywardBlade + SplendorOfTranquilWaters + SummitShaper + SwordOfDescension + SwordOfNarzissenkreuz + TheAlleyFlash + TheBlackSword + TheDockhandsAssistant + TheFlute + ToukabouShigure + TravelersHandySword + WolfFang + XiphosMoonlight + Akuoumaru + BeaconOfTheReedSea + BlackcliffSlasher + BloodtaintedGreatsword + DebateClub + FavoniusGreatsword + FerrousShadow + ForestRegalia + KatsuragikiriNagamasa + LithicBlade + LuxuriousSeaLord + MailedFlower + MakhairaAquamarine + OldMercsPal + PortablePowerSaw + PrototypeArchaic + Rainslasher + RedhornStonethresher + RoyalGreatsword + SacrificialGreatsword + SerpentSpine + SkyriderGreatsword + SkywardPride + SnowTombedStarsilver + SongOfBrokenPines + TalkingStick + TheBell + TheUnforged + TidalShadow + UltimateOverlordsMegaMagicSword + Verdict + WasterGreatsword + Whiteblind + WhiteIronGreatsword + WolfsGravestone + BalladOfTheFjords + BeginnersProtector + BlackcliffPole + BlackTassel + CalamityQueller + CrescentPike + Deathmatch + DragonsBane + DragonspineSpear + EngulfingLightning + FavoniusLance + Halberd + IronPoint + KitainCrossSpear + LithicSpear + MissiveWindspear + Moonpiercer + PrimordialJadeWingedSpear + ProspectorsDrill + PrototypeStarglitter + RightfulReward + RoyalSpear + SkywardSpine + StaffOfHoma + StaffOfTheScarletSands + TheCatch + VortexVanquisher + WavebreakersFin + WhiteTassel + AlleyHunter + AmosBow + AquaSimulacra + BlackcliffWarbow + CompoundBow + ElegyForTheEnd + EndOfTheLine + FadingTwilight + FavoniusWarbow + Hamayumi + HuntersBow + HuntersPath + IbisPiercer + KingsSquire + Messenger + MitternachtsWaltz + MouunsMoon + PolarStar + Predator + PrototypeCrescent + RangeGauge + RavenBow + RecurveBow + RoyalBow + Rust + SacrificialBow + ScionOfTheBlazingSun + SeasonedHuntersBow + SharpshootersOath + SkywardHarp + Slingshot + SongOfStillness + TheFirstGreatMagic + TheStringless + TheViridescentHunt + ThunderingPulse + WindblumeOde + ApprenticesNotes + AThousandFloatingDreams + BalladOfTheBoundlessBlue + BlackcliffAgate + CashflowSupervision + DodocoTales + EmeraldOrb + EverlastingMoonglow + EyeOfPerception + FavoniusCodex + FlowingPurity + Frostbearer + FruitOfFulfillment + HakushinRing + JadefallsSplendor + KagurasVerity + LostPrayerToTheSacredWinds + MagicGuide + MappaMare + MemoryOfDust + OathswornEye + OtherworldlyStory + PocketGrimoire + PrototypeAmber + QuantumCatalyst + RoyalGrimoire + SacrificialFragments + SacrificialJade + SkywardAtlas + SolarPearl + TheWidsith + ThrillingTalesOfDragonSlayers + TomeOfTheEternalFlow + TulaytullahsRemembrance + TwinNephrite + WanderingEvenstar + WineAndSong +} + type Talent { auto: Int! skill: Int! @@ -285,6 +470,7 @@ enum CharacterKey { Mika Mona Nahida + Navia Neuvillette Nilou Ningguang @@ -379,20 +565,20 @@ input UpdateArtifact { } input InputWeapon { - key: String! + key: WeaponKey! level: Int! ascension: Int! refinement: Int! - location: String + location: LocationKey lock: Boolean! } input UpdateWeapon { - key: String + key: WeaponKey level: Int ascension: Int refinement: Int - location: String + location: LocationKey lock: Boolean id: ID! } diff --git a/apps/gi-backend/src/app/weapon/weapon.entity.ts b/apps/gi-backend/src/app/weapon/weapon.entity.ts index fff2929a45..d9cc369e9f 100644 --- a/apps/gi-backend/src/app/weapon/weapon.entity.ts +++ b/apps/gi-backend/src/app/weapon/weapon.entity.ts @@ -1,3 +1,6 @@ +import type { LocationCharacterKey } from '@genshin-optimizer/consts' +import { WeaponKey, allWeaponKeys } from '@genshin-optimizer/consts' +import { objKeyMap } from '@genshin-optimizer/util' import { Field, ID, @@ -6,7 +9,14 @@ import { ObjectType, OmitType, PartialType, + registerEnumType, } from '@nestjs/graphql' +import { LocationEnum } from '../common.entity' + +const WeaponKeyEnum = objKeyMap(allWeaponKeys, (k) => k) +registerEnumType(WeaponKeyEnum, { + name: 'WeaponKey', +}) @ObjectType() export class Weapon { @@ -16,8 +26,8 @@ export class Weapon { @Field(() => String) genshinUserId: string - @Field(() => String) - key: string + @Field(() => WeaponKeyEnum) + key: WeaponKey @Field(() => Int) level: number @@ -28,8 +38,8 @@ export class Weapon { @Field(() => Int) refinement: number - @Field(() => String, { nullable: true }) - location: string | null + @Field(() => LocationEnum, { nullable: true }) + location: LocationCharacterKey | null @Field(() => Boolean) lock: boolean diff --git a/apps/gi-backend/src/app/weapon/weapon.resolver.ts b/apps/gi-backend/src/app/weapon/weapon.resolver.ts index 4bb3c099e3..480743beed 100644 --- a/apps/gi-backend/src/app/weapon/weapon.resolver.ts +++ b/apps/gi-backend/src/app/weapon/weapon.resolver.ts @@ -29,7 +29,10 @@ export class WeaponResolver { inputWeapon: InputWeapon ): Promise { this.genshinUserService.validateGenshinUser(userId, genshinUserId) - return this.weaponService.create(inputWeapon, genshinUserId) + return this.weaponService.create( + inputWeapon, + genshinUserId + ) as Promise } @Mutation(() => Weapon) @@ -40,7 +43,10 @@ export class WeaponResolver { updateWeapon: UpdateWeapon ): Promise { this.genshinUserService.validateGenshinUser(userId, genshinUserId) - return this.weaponService.update(updateWeapon, genshinUserId) + return this.weaponService.update( + updateWeapon, + genshinUserId + ) as Promise } @Mutation(() => Weapon) @@ -51,6 +57,6 @@ export class WeaponResolver { weaponId: string ): Promise { this.genshinUserService.validateGenshinUser(userId, genshinUserId) - return this.weaponService.remove(weaponId, genshinUserId) + return this.weaponService.remove(weaponId, genshinUserId) as Promise } } diff --git a/apps/gi-frontend-next/next.config.js b/apps/gi-frontend-next/next.config.js index 5fefb89c89..91d00dea35 100644 --- a/apps/gi-frontend-next/next.config.js +++ b/apps/gi-frontend-next/next.config.js @@ -7,6 +7,9 @@ const { composePlugins, withNx } = require('@nx/next') * @type {import('@nx/next/plugins/with-nx').WithNxOptions} **/ const nextConfig = { + // to speed up slow dev + // https://github.com/vercel/next.js/issues/48748#issuecomment-1858705129 + swcMinify: true, nx: { // Set this to true if you would like to to use SVGR // See: https://github.com/gregberge/svgr diff --git a/apps/gi-frontend-next/src/app/[locale]/character/components/CharacterList.tsx b/apps/gi-frontend-next/src/app/[locale]/character/components/CharacterList.tsx index 933dedbebc..0a5d9d1f1a 100644 --- a/apps/gi-frontend-next/src/app/[locale]/character/components/CharacterList.tsx +++ b/apps/gi-frontend-next/src/app/[locale]/character/components/CharacterList.tsx @@ -1,29 +1,30 @@ +import type { Character } from '@genshin-optimizer/gi-frontend-gql' import { useGetAllUserCharacterQuery } from '@genshin-optimizer/gi-frontend-gql' -import { Grid } from '@mui/material' import { CharacterCard } from '@genshin-optimizer/gi-ui-next' -import type { ICharacter } from '@genshin-optimizer/gi-good' +import { Box, Grid, Skeleton } from '@mui/material' -const columns = { xs: 1, sm: 2, md: 3, lg: 3, xl: 4 } -export default function WeaponList({ +const columns = { xs: 1, sm: 2, md: 3, lg: 4, xl: 4 } +export default function CharacterList({ genshinUserId, }: { genshinUserId: string }) { - const { - data, - // loading, error - } = useGetAllUserCharacterQuery({ + const { data, loading, error } = useGetAllUserCharacterQuery({ variables: { genshinUserId, }, }) + if (error) console.error(error) + if (loading) return return ( - - {data?.getAllUserCharacter.map((character) => ( - - - - ))} - + + + {data?.getAllUserCharacter.map((character) => ( + + + + ))} + + ) } diff --git a/apps/gi-frontend-next/src/app/[locale]/character/page.tsx b/apps/gi-frontend-next/src/app/[locale]/character/page.tsx index c82462ad4f..0ec66ff1a4 100644 --- a/apps/gi-frontend-next/src/app/[locale]/character/page.tsx +++ b/apps/gi-frontend-next/src/app/[locale]/character/page.tsx @@ -1,10 +1,10 @@ 'use client' import { useGetUserQuery } from '@genshin-optimizer/gi-frontend-gql' import { CardThemed } from '@genshin-optimizer/ui-common' -import { CardContent, CardHeader, Divider } from '@mui/material' +import { CardContent, CardHeader, Divider, Stack } from '@mui/material' import { useSession } from 'next-auth/react' import AddCharacterButton from './components/AddCharacterButton' -import WeaponList from './components/CharacterList' +import CharacterList from './components/CharacterList' export default function CharacterPage() { const { data: session } = useSession() @@ -25,15 +25,16 @@ export default function CharacterPage() { const genshinUserId = genshinUser.id return ( - - } - /> - - - - - + + + } + /> + + + + + ) } diff --git a/apps/gi-frontend-next/src/app/[locale]/components/Header/MobileHeader.tsx b/apps/gi-frontend-next/src/app/[locale]/components/Header/MobileHeader.tsx index 26f1e7313a..817647ba8f 100644 --- a/apps/gi-frontend-next/src/app/[locale]/components/Header/MobileHeader.tsx +++ b/apps/gi-frontend-next/src/app/[locale]/components/Header/MobileHeader.tsx @@ -1,5 +1,5 @@ import { SillyContext } from '@genshin-optimizer/gi-ui-next' -import { Menu as MenuIcon } from '@mui/icons-material' +import MenuIcon from '@mui/icons-material/Menu' import { AppBar, Avatar, diff --git a/apps/gi-frontend-next/src/app/[locale]/components/Header/TabsData.tsx b/apps/gi-frontend-next/src/app/[locale]/components/Header/TabsData.tsx index e53a0608f1..dbd7b2d30d 100644 --- a/apps/gi-frontend-next/src/app/[locale]/components/Header/TabsData.tsx +++ b/apps/gi-frontend-next/src/app/[locale]/components/Header/TabsData.tsx @@ -1,13 +1,11 @@ import { FlowerIcon } from '@genshin-optimizer/gi-svgicons' import { GenshinUserContext, UserContext } from '@genshin-optimizer/gi-ui-next' import { AnvilIcon } from '@genshin-optimizer/svgicons' -import { - Article, - Construction, - People, - Scanner, - Settings, -} from '@mui/icons-material' +import ArticleIcon from '@mui/icons-material/Article' +import ConstructionIcon from '@mui/icons-material/Construction' +import PeopleIcon from '@mui/icons-material/People' +import ScannerIcon from '@mui/icons-material/Scanner' +import SettingsIcon from '@mui/icons-material/Settings' import { Chip } from '@mui/material' import type { ReactNode } from 'react' import { useContext } from 'react' @@ -34,32 +32,32 @@ const weapons: ITab = { } const characters: ITab = { i18Key: 'tabs.characters', - icon: , + icon: , to: (l) => `/${l}/character`, value: 'character', textSuffix: , } const tools: ITab = { i18Key: 'tabs.tools', - icon: , + icon: , to: (l) => `/${l}/tools`, value: 'tools', } const scanner: ITab = { i18Key: 'tabs.scanner', - icon: , + icon: , to: (l) => `/${l}/scanner`, value: 'scanner', } const doc: ITab = { i18Key: 'tabs.doc', - icon:
, + icon: , to: (l) => `/${l}/doc`, value: 'doc', } const setting: ITab = { i18Key: 'tabs.setting', - icon: , + icon: , to: (l) => `/${l}/login`, value: 'login', textSuffix: , diff --git a/apps/gi-frontend-next/src/app/[locale]/components/Header/index.tsx b/apps/gi-frontend-next/src/app/[locale]/components/Header/index.tsx index 10ccb1f571..70bc98ec9b 100644 --- a/apps/gi-frontend-next/src/app/[locale]/components/Header/index.tsx +++ b/apps/gi-frontend-next/src/app/[locale]/components/Header/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { KeyboardArrowUp } from '@mui/icons-material' +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp' import { Fab, useMediaQuery, useTheme } from '@mui/material' import { usePathname } from 'next/navigation' import DesktopHeader from './DesktopHeader' @@ -24,7 +24,7 @@ export default function Header({ locale }: { locale: string }) { )} - + diff --git a/apps/gi-frontend-next/src/app/[locale]/layoutWrappers/ThemeRegistry/EmotionCache.tsx b/apps/gi-frontend-next/src/app/[locale]/layoutWrappers/ThemeRegistry/EmotionCache.tsx deleted file mode 100644 index 971a66e475..0000000000 --- a/apps/gi-frontend-next/src/app/[locale]/layoutWrappers/ThemeRegistry/EmotionCache.tsx +++ /dev/null @@ -1,99 +0,0 @@ -'use client' -import * as React from 'react' -import createCache from '@emotion/cache' -import { useServerInsertedHTML } from 'next/navigation' -import { CacheProvider as DefaultCacheProvider } from '@emotion/react' -import type { - EmotionCache, - Options as OptionsOfCreateCache, -} from '@emotion/cache' - -export type NextAppDirEmotionCacheProviderProps = { - /** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */ - options: Omit - /** By default from 'import { CacheProvider } from "@emotion/react"' */ - CacheProvider?: (props: { - value: EmotionCache - children: React.ReactNode - }) => React.JSX.Element | null - children: React.ReactNode -} - -// Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx -export default function NextAppDirEmotionCacheProvider( - props: NextAppDirEmotionCacheProviderProps -) { - const { options, CacheProvider = DefaultCacheProvider, children } = props - - const [registry] = React.useState(() => { - const cache = createCache(options) - cache.compat = true - const prevInsert = cache.insert - let inserted: { name: string; isGlobal: boolean }[] = [] - cache.insert = (...args) => { - const [selector, serialized] = args - if (cache.inserted[serialized.name] === undefined) { - inserted.push({ - name: serialized.name, - isGlobal: !selector, - }) - } - return prevInsert(...args) - } - const flush = () => { - const prevInserted = inserted - inserted = [] - return prevInserted - } - return { cache, flush } - }) - - useServerInsertedHTML(() => { - const inserted = registry.flush() - if (inserted.length === 0) { - return null - } - let styles = '' - let dataEmotionAttribute = registry.cache.key - - const globals: { - name: string - style: string - }[] = [] - - inserted.forEach(({ name, isGlobal }) => { - const style = registry.cache.inserted[name] - - if (typeof style !== 'boolean') { - if (isGlobal) { - globals.push({ name, style }) - } else { - styles += style - dataEmotionAttribute += ` ${name}` - } - } - }) - - return ( - - {globals.map(({ name, style }) => ( -