Skip to content

Commit

Permalink
SRO preliminary buildTC and UI (#2536)
Browse files Browse the repository at this point in the history
* preliminary buildTC and UI

* refactor LightConeEditor

* update compact build display, add tc lightcone editor

* show current buildtc in teammate display, fix db bug

* buildTC main level/stat/rarity editor

* relic set/substat editors

* pipe buildtc stats to calculator provider

* refactor

* refactor

* fix build row flashing
  • Loading branch information
frzyc authored Nov 15, 2024
1 parent 2af2e37 commit bc6fe59
Show file tree
Hide file tree
Showing 37 changed files with 1,825 additions and 890 deletions.
1 change: 1 addition & 0 deletions libs/common/ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './useConstObj'
export * from './useInfScroll'
export * from './useIsMount'
export * from './useOnScreen'
export * from './useRefOverflow'
export * from './useRefSize'
export * from './useTitle'
export * from './useWindowScrollPos'
30 changes: 30 additions & 0 deletions libs/common/ui/src/hooks/useRefOverflow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'
import { useEffect, useRef, useState } from 'react'

export function useRefOverflow() {
const ref = useRef<HTMLElement>()

const [isOverflowX, setIsOverflowX] = useState(false)
const [isOverflowY, setIsOverflowY] = useState(false)

useEffect(() => {
const handleResize = () => {
const ele = ref.current
setIsOverflowX(ele ? isOverflowedX(ele) : false)
setIsOverflowY(ele ? isOverflowedY(ele) : false)
}
handleResize() // Check on mount and whenever the window resizes
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // Safe to keep empty as we only want mount/unmount behavior
return { isOverflowX, isOverflowY, ref }
}
function isOverflowedX(ref: HTMLElement) {
return ref.scrollWidth > ref.clientWidth
}

function isOverflowedY(ref: HTMLElement) {
return ref.scrollHeight > ref.clientHeight
}
15 changes: 8 additions & 7 deletions libs/common/ui/src/hooks/useRefSize.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use client'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'

/**
* NOTE: the values of `width` & `height` starts at 0, since ref takes a rendering cycle to attach.
* @returns
*/
export function useRefSize() {
const ref = useRef<HTMLElement>()
const [width, setWidth] = useState(0)
Expand All @@ -11,14 +15,11 @@ export function useRefSize() {
setWidth(ref.current?.clientWidth ?? 0)
setHeight(ref.current?.clientHeight ?? 0)
}
if (ref.current) window.addEventListener('resize', handleResize)
handleResize() // Check on mount and whenever the window resizes
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
useLayoutEffect(() => {
setWidth(ref.current?.clientWidth ?? 0)
setHeight(ref.current?.clientHeight ?? 0)
}, [])
}, []) // Safe to keep empty as we only want mount/unmount behavior
return { width, height, ref }
}
7 changes: 7 additions & 0 deletions libs/sr/consts/src/relic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ export type RelicMainStatKey = (typeof allRelicMainStatKeys)[number]
export const allRelicRarityKeys = [2, 3, 4, 5] as const
export type RelicRarityKey = (typeof allRelicRarityKeys)[number]

export function isRelicRarityKey(rarity: unknown): rarity is RelicRarityKey {
return (
typeof rarity === 'number' &&
allRelicRarityKeys.includes(rarity as RelicRarityKey)
)
}

export const allRelicSetCountKeys = [2, 4] as const
export type RelicSetCountKey = (typeof allRelicSetCountKeys)[number]

Expand Down
1 change: 1 addition & 0 deletions libs/sr/db-ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './useBuild'
export * from './useBuildTc'
export * from './useCharacter'
export * from './useLightCone'
export * from './useRelic'
Expand Down
6 changes: 6 additions & 0 deletions libs/sr/db-ui/src/hooks/useBuildTc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useDataManagerBase } from '@genshin-optimizer/common/database-ui'
import { useDatabaseContext } from '../context'
export function useBuildTc(buildTcId: string | undefined) {
const { database } = useDatabaseContext()
return useDataManagerBase(database.buildTcs, buildTcId ?? '')
}
2 changes: 1 addition & 1 deletion libs/sr/db/src/Database/DataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class DataManager<
// Delete it from storage
for (const key of this.database.storage.keys)
if (
key.startsWith(this.goKeySingle) &&
key.startsWith(`${this.goKeySingle}_`) &&
!this.set(this.toCacheKey(key), {})
) {
this.database.storage.remove(key)
Expand Down
5 changes: 0 additions & 5 deletions libs/sr/db/src/Database/DataManagers/BuildDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ export class BuildDataManager extends DataManager<
if (!build) return ''
return this.new(structuredClone(build))
}
override remove(key: string, notify?: boolean): Build | undefined {
const build = super.remove(key, notify)
// TODO: remove builds from teams
return build
}
getBuildIds(characterKey: CharacterKey) {
return this.keys.filter(
(key) => this.get(key)?.characterKey === characterKey
Expand Down
54 changes: 36 additions & 18 deletions libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { clamp, objKeyMap, objMap } from '@genshin-optimizer/common/util'
import type {
CharacterKey,
RelicMainStatKey,
RelicRarityKey,
RelicSetKey,
RelicSlotKey,
} from '@genshin-optimizer/sr/consts'
Expand All @@ -10,11 +11,16 @@ import {
allLightConeKeys,
allRelicSlotKeys,
allRelicSubStatKeys,
isRelicRarityKey,
relicMaxLevel,
relicSubstatTypeKeys,
} from '@genshin-optimizer/sr/consts'
import { validateLevelAsc } from '@genshin-optimizer/sr/util'
import type { ICachedLightCone, ICachedRelic } from '../../Interfaces'
import type {
BuildTcRelicSlot,
ICachedLightCone,
ICachedRelic,
} from '../../Interfaces'
import { type IBuildTc } from '../../Interfaces/IBuildTc'
import { DataManager } from '../DataManager'
import type { SroDatabase } from '../Database'
Expand All @@ -30,13 +36,14 @@ export class BuildTcDataManager extends DataManager<
}
override validate(obj: unknown): IBuildTc | undefined {
if (!obj || typeof obj !== 'object') return undefined
const { characterKey } = obj as IBuildTc
const { characterKey, teamId } = obj as IBuildTc
if (!allCharacterKeys.includes(characterKey)) return undefined

let { name, teamId, description } = obj as IBuildTc
let { name, description } = obj as IBuildTc
const { lightCone, relic, optimization } = obj as IBuildTc

if (teamId && !this.database.teams.get(teamId)) teamId = undefined
// Cannot validate teamId, since on db init database.teams do not exist yet.
// if (teamId && !this.database.teams.get(teamId)) teamId = undefined

if (typeof name !== 'string') name = 'Build(TC) Name'
if (typeof description !== 'string') description = 'Build(TC) Description'
Expand Down Expand Up @@ -66,12 +73,6 @@ export class BuildTcDataManager extends DataManager<
if (!buildTc) return ''
return this.new(structuredClone(buildTc))
}
override remove(key: string, notify?: boolean): IBuildTc | undefined {
const buildTc = super.remove(key, notify)
// TODO: remove tcbuild from teams?

return buildTc
}
export(buildTcId: string): object | undefined {
const buildTc = this.database.buildTcs.get(buildTcId)
if (!buildTc) return undefined
Expand All @@ -82,6 +83,11 @@ export class BuildTcDataManager extends DataManager<
if (!this.set(id, data)) return ''
return id
}
getBuildTcIds(characterKey: CharacterKey) {
return this.keys.filter(
(key) => this.get(key)?.characterKey === characterKey
)
}
}

export function initCharTC(characterKey: CharacterKey): IBuildTc {
Expand All @@ -94,6 +100,7 @@ export function initCharTC(characterKey: CharacterKey): IBuildTc {
substats: {
type: 'max',
stats: objKeyMap(allRelicSubStatKeys, () => 0),
rarity: 5,
},
sets: {},
},
Expand All @@ -103,14 +110,15 @@ export function initCharTC(characterKey: CharacterKey): IBuildTc {
},
}
}
function initCharTCRelicSlots() {
function initCharTCRelicSlots(): Record<RelicSlotKey, BuildTcRelicSlot> {
return objKeyMap(allRelicSlotKeys, (s) => ({
level: 20,
statKey: (s === 'head'
? 'hp'
: s === 'hands'
? 'atk'
: 'atk_') as RelicMainStatKey,
rarity: 5,
}))
}

Expand All @@ -137,7 +145,7 @@ function validateCharTCRelic(relic: unknown): IBuildTc['relic'] | undefined {
if (typeof relic !== 'object') return undefined
let {
slots,
substats: { type, stats },
substats: { type, stats, rarity },
sets,
} = relic as IBuildTc['relic']
const _slots = validateCharTCRelicSlots(slots)
Expand All @@ -148,11 +156,16 @@ function validateCharTCRelic(relic: unknown): IBuildTc['relic'] | undefined {
stats = objKeyMap(allRelicSubStatKeys, (k) =>
typeof stats[k] === 'number' ? stats[k] : 0
)
rarity = validateRelicRarity(rarity)

if (typeof sets !== 'object') sets = {}
// TODO: validate sets

return { slots, substats: { type, stats }, sets }
return { slots, substats: { type, stats, rarity }, sets }
}

function validateRelicRarity(rarity: unknown): RelicRarityKey {
return isRelicRarityKey(rarity) ? rarity : 5
}
function validateCharTCRelicSlots(
slots: unknown
Expand All @@ -167,12 +180,17 @@ function validateCharTCRelicSlots(
)
)
return initCharTCRelicSlots()
return objMap(slots as IBuildTc['relic']['slots'], ({ level, ...rest }) => {
return {
level: clamp(level, 0, relicMaxLevel[5]),
...rest,
return objMap(
slots as IBuildTc['relic']['slots'],
({ level, rarity, ...rest }) => {
rarity = validateRelicRarity(rarity)
return {
level: clamp(level, 0, relicMaxLevel[rarity]),
rarity,
...rest,
}
}
})
)
}
function validateCharTcOptimization(
optimization: unknown
Expand Down
15 changes: 6 additions & 9 deletions libs/sr/db/src/Interfaces/IBuildTc.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,32 @@
import type {
AscensionKey,
CharacterKey,
LightConeKey,
RelicMainStatKey,
RelicRarityKey,
RelicSetKey,
RelicSlotKey,
RelicSubStatKey,
RelicSubstatTypeKey,
SuperimposeKey,
} from '@genshin-optimizer/sr/consts'
import type { ILightCone } from '@genshin-optimizer/sr/srod'

export type BuildTcRelicSlot = {
level: number
statKey: RelicMainStatKey
rarity: RelicRarityKey
}
export type BuildTCLightCone = Omit<ILightCone, 'location' | 'lock'>
export interface IBuildTc {
name: string
description: string
characterKey: CharacterKey
teamId?: string
lightCone?: {
key: LightConeKey
level: number
ascension: AscensionKey
superimpose: SuperimposeKey
}
lightCone?: BuildTCLightCone
relic: {
slots: Record<RelicSlotKey, BuildTcRelicSlot>
substats: {
type: RelicSubstatTypeKey
stats: Record<RelicSubStatKey, number>
rarity: RelicRarityKey
}
sets: Partial<Record<RelicSetKey, 2 | 4>>
}
Expand Down
1 change: 1 addition & 0 deletions libs/sr/db/src/Interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './IBuildTc'
export * from './ISroCharacter'
export * from './ISroDatabase'
export * from './ISroLightCone'
Expand Down
2 changes: 1 addition & 1 deletion libs/sr/formula/src/data/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const data: TagMapNodeEntries = [
reader.withTag({ sheet: 'agg', et: 'own' }).reread(reader.sheet('custom')),

// convert sheet:<char/lightCone> to sheet:agg for accumulation
// sheet:<relic> is reread in src/util.ts:relicsData()
// sheet:<relic> is reread in src/util.ts:relicTagMapNodeEntries()
reader.sheet('agg').reread(reader.sheet('char')),

// add all light cones by default
Expand Down
30 changes: 10 additions & 20 deletions libs/sr/formula/src/formula.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
type LightConeKey,
} from '@genshin-optimizer/sr/consts'
import { fail } from 'assert'
import { charData, lightConeData, withMember } from '.'
import {
charTagMapNodeEntries,
lightConeTagMapNodeEntries,
withMember,
} from '.'
import { Calculator } from './calculator'
import { data, keys, values } from './data'
import {
Expand All @@ -37,7 +41,7 @@ describe('character test', () => {
const data: TagMapNodeEntries = [
...withMember(
'March7th',
...charData({
...charTagMapNodeEntries({
level: lvl,
ascension: ascension as AscensionKey,
key: charKey,
Expand Down Expand Up @@ -71,7 +75,7 @@ describe('lightCone test', () => {
const data: TagMapNodeEntries = [
...withMember(
'March7th',
...charData({
...charTagMapNodeEntries({
level: 1,
ascension: 0,
key: 'March7th',
Expand All @@ -83,14 +87,7 @@ describe('lightCone test', () => {
bonusAbilities: {},
statBoosts: {},
}),
...lightConeData({
key: lcKey,
level: lvl,
ascension: ascension as AscensionKey,
superimpose: 1,
lock: false,
location: 'March7th',
})
...lightConeTagMapNodeEntries(lcKey, lvl, ascension as AscensionKey, 1)
),
]
const calc = new Calculator(keys, values, compileTagMapValues(keys, data))
Expand All @@ -114,7 +111,7 @@ describe('char+lightCone test', () => {
const data: TagMapNodeEntries = [
...withMember(
'March7th',
...charData({
...charTagMapNodeEntries({
level: 1,
ascension: 0,
key: charKey,
Expand All @@ -126,14 +123,7 @@ describe('char+lightCone test', () => {
bonusAbilities: {},
statBoosts: {},
}),
...lightConeData({
key: lcKey,
level: 1,
ascension: 0,
superimpose: 1,
lock: false,
location: 'March7th',
})
...lightConeTagMapNodeEntries(lcKey, 1, 0, 1)
),
]
const calc = new Calculator(keys, values, compileTagMapValues(keys, data))
Expand Down
Loading

0 comments on commit bc6fe59

Please sign in to comment.