diff --git a/libs/common/ui/src/hooks/index.ts b/libs/common/ui/src/hooks/index.ts index 757164c542..71019af6c2 100644 --- a/libs/common/ui/src/hooks/index.ts +++ b/libs/common/ui/src/hooks/index.ts @@ -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' diff --git a/libs/common/ui/src/hooks/useRefOverflow.tsx b/libs/common/ui/src/hooks/useRefOverflow.tsx new file mode 100644 index 0000000000..ce512f3c16 --- /dev/null +++ b/libs/common/ui/src/hooks/useRefOverflow.tsx @@ -0,0 +1,30 @@ +'use client' +import { useEffect, useRef, useState } from 'react' + +export function useRefOverflow() { + const ref = useRef() + + 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 +} diff --git a/libs/common/ui/src/hooks/useRefSize.tsx b/libs/common/ui/src/hooks/useRefSize.tsx index 1360f1aef1..d59dc7f7df 100644 --- a/libs/common/ui/src/hooks/useRefSize.tsx +++ b/libs/common/ui/src/hooks/useRefSize.tsx @@ -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() const [width, setWidth] = useState(0) @@ -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 } } diff --git a/libs/sr/consts/src/relic.ts b/libs/sr/consts/src/relic.ts index 52a26a52ce..a8ba9512c7 100644 --- a/libs/sr/consts/src/relic.ts +++ b/libs/sr/consts/src/relic.ts @@ -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] diff --git a/libs/sr/db-ui/src/hooks/index.ts b/libs/sr/db-ui/src/hooks/index.ts index 0467a22d42..2a5ea92e27 100644 --- a/libs/sr/db-ui/src/hooks/index.ts +++ b/libs/sr/db-ui/src/hooks/index.ts @@ -1,4 +1,5 @@ export * from './useBuild' +export * from './useBuildTc' export * from './useCharacter' export * from './useLightCone' export * from './useRelic' diff --git a/libs/sr/db-ui/src/hooks/useBuildTc.ts b/libs/sr/db-ui/src/hooks/useBuildTc.ts new file mode 100644 index 0000000000..f75b155ed2 --- /dev/null +++ b/libs/sr/db-ui/src/hooks/useBuildTc.ts @@ -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 ?? '') +} diff --git a/libs/sr/db/src/Database/DataManager.ts b/libs/sr/db/src/Database/DataManager.ts index c621ff7eff..3b897b944d 100644 --- a/libs/sr/db/src/Database/DataManager.ts +++ b/libs/sr/db/src/Database/DataManager.ts @@ -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) diff --git a/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts b/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts index 243695fbd1..03c0815278 100644 --- a/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts +++ b/libs/sr/db/src/Database/DataManagers/BuildDataManager.ts @@ -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 diff --git a/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts b/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts index 4e01777ed0..444ac92f79 100644 --- a/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts +++ b/libs/sr/db/src/Database/DataManagers/BuildTcDataManager.ts @@ -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' @@ -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' @@ -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' @@ -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 @@ -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 { @@ -94,6 +100,7 @@ export function initCharTC(characterKey: CharacterKey): IBuildTc { substats: { type: 'max', stats: objKeyMap(allRelicSubStatKeys, () => 0), + rarity: 5, }, sets: {}, }, @@ -103,7 +110,7 @@ export function initCharTC(characterKey: CharacterKey): IBuildTc { }, } } -function initCharTCRelicSlots() { +function initCharTCRelicSlots(): Record { return objKeyMap(allRelicSlotKeys, (s) => ({ level: 20, statKey: (s === 'head' @@ -111,6 +118,7 @@ function initCharTCRelicSlots() { : s === 'hands' ? 'atk' : 'atk_') as RelicMainStatKey, + rarity: 5, })) } @@ -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) @@ -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 @@ -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 diff --git a/libs/sr/db/src/Interfaces/IBuildTc.ts b/libs/sr/db/src/Interfaces/IBuildTc.ts index 776b54a1f4..44ec89a6c4 100644 --- a/libs/sr/db/src/Interfaces/IBuildTc.ts +++ b/libs/sr/db/src/Interfaces/IBuildTc.ts @@ -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 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 substats: { type: RelicSubstatTypeKey stats: Record + rarity: RelicRarityKey } sets: Partial> } diff --git a/libs/sr/db/src/Interfaces/index.ts b/libs/sr/db/src/Interfaces/index.ts index 800eef268d..1f291a2f7e 100644 --- a/libs/sr/db/src/Interfaces/index.ts +++ b/libs/sr/db/src/Interfaces/index.ts @@ -1,3 +1,4 @@ +export * from './IBuildTc' export * from './ISroCharacter' export * from './ISroDatabase' export * from './ISroLightCone' diff --git a/libs/sr/formula/src/data/common/index.ts b/libs/sr/formula/src/data/common/index.ts index f1e15c21b1..f7ab9f32f1 100644 --- a/libs/sr/formula/src/data/common/index.ts +++ b/libs/sr/formula/src/data/common/index.ts @@ -13,7 +13,7 @@ const data: TagMapNodeEntries = [ reader.withTag({ sheet: 'agg', et: 'own' }).reread(reader.sheet('custom')), // convert sheet: to sheet:agg for accumulation - // sheet: is reread in src/util.ts:relicsData() + // sheet: is reread in src/util.ts:relicTagMapNodeEntries() reader.sheet('agg').reread(reader.sheet('char')), // add all light cones by default diff --git a/libs/sr/formula/src/formula.test.ts b/libs/sr/formula/src/formula.test.ts index 3d73e75bd3..059326a9ea 100644 --- a/libs/sr/formula/src/formula.test.ts +++ b/libs/sr/formula/src/formula.test.ts @@ -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 { @@ -37,7 +41,7 @@ describe('character test', () => { const data: TagMapNodeEntries = [ ...withMember( 'March7th', - ...charData({ + ...charTagMapNodeEntries({ level: lvl, ascension: ascension as AscensionKey, key: charKey, @@ -71,7 +75,7 @@ describe('lightCone test', () => { const data: TagMapNodeEntries = [ ...withMember( 'March7th', - ...charData({ + ...charTagMapNodeEntries({ level: 1, ascension: 0, key: 'March7th', @@ -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)) @@ -114,7 +111,7 @@ describe('char+lightCone test', () => { const data: TagMapNodeEntries = [ ...withMember( 'March7th', - ...charData({ + ...charTagMapNodeEntries({ level: 1, ascension: 0, key: charKey, @@ -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)) diff --git a/libs/sr/formula/src/util.ts b/libs/sr/formula/src/util.ts index 675c9ea810..cae36873d0 100644 --- a/libs/sr/formula/src/util.ts +++ b/libs/sr/formula/src/util.ts @@ -1,10 +1,17 @@ import { cmpEq, cmpNE } from '@genshin-optimizer/pando/engine' -import type { RelicSetKey, StatKey } from '@genshin-optimizer/sr/consts' +import type { + AscensionKey, + LightConeKey, + RelicMainStatKey, + RelicSetKey, + RelicSubStatKey, + SuperimposeKey, +} from '@genshin-optimizer/sr/consts' import { allBonusAbilityKeys, allStatBoostKeys, } from '@genshin-optimizer/sr/consts' -import type { ICharacter, ILightCone } from '@genshin-optimizer/sr/srod' +import type { ICharacter } from '@genshin-optimizer/sr/srod' import type { Member, Preset, TagMapNodeEntries } from './data/util' import { convert, @@ -28,7 +35,7 @@ export function withMember( return data.map(({ tag, value }) => ({ tag: { ...tag, src }, value })) } -export function charData(data: ICharacter): TagMapNodeEntries { +export function charTagMapNodeEntries(data: ICharacter): TagMapNodeEntries { const { lvl, basic, skill, ult, talent, ascension, eidolon } = own.char const { char, iso, [data.key]: sheet } = reader.withAll('sheet', []) @@ -58,42 +65,29 @@ export function charData(data: ICharacter): TagMapNodeEntries { ] } -export function lightConeData(data: ILightCone | undefined): TagMapNodeEntries { - if (!data) return [] - const { lvl, ascension, superimpose } = own.lightCone - +export function lightConeTagMapNodeEntries( + key: LightConeKey, + level: number, + ascension: AscensionKey, + superimpose: SuperimposeKey +): TagMapNodeEntries { return [ // Mark light cones as used - own.common.count.sheet(data.key).add(1), - - lvl.add(data.level), - ascension.add(data.ascension), - superimpose.add(data.superimpose), + own.common.count.sheet(key).add(1), + own.lightCone.lvl.add(level), + own.lightCone.ascension.add(ascension), + own.lightCone.superimpose.add(superimpose), ] } -export function relicsData( - data: { - set: RelicSetKey - stats: readonly { key: StatKey; value: number }[] - }[] +export function relicTagMapNodeEntries( + stats: Partial>, + sets: Partial> ): TagMapNodeEntries { const { common: { count }, premod, } = convert(ownTag, { sheet: 'relic', et: 'own' }) - const sets: Partial> = {}, - stats: Partial> = {} - for (const { set: setKey, stats: stat } of data) { - const set = sets[setKey] - if (set === undefined) sets[setKey] = 1 - else sets[setKey] = set + 1 - for (const { key, value } of stat) { - const stat = stats[key] - if (stat === undefined) stats[key] = value - else stats[key] = stat + value - } - } return [ // Opt-in for artifact buffs, instead of enabling it by default to reduce `read` traffic reader.sheet('agg').reread(reader.sheet('relic')), diff --git a/libs/sr/page-team/src/BuildsDisplay.tsx b/libs/sr/page-team/src/BuildsDisplay.tsx index c372972d2d..fa5c32f6af 100644 --- a/libs/sr/page-team/src/BuildsDisplay.tsx +++ b/libs/sr/page-team/src/BuildsDisplay.tsx @@ -1,51 +1,140 @@ -import { useForceUpdate } from '@genshin-optimizer/common/react-util' -import { CardThemed } from '@genshin-optimizer/common/ui' -import { allRelicSlotKeys } from '@genshin-optimizer/sr/consts' -import type { RelicIds, TeammateDatum } from '@genshin-optimizer/sr/db' +import { + useBoolState, + useForceUpdate, +} from '@genshin-optimizer/common/react-util' +import { + CardThemed, + ModalWrapper, + NumberInputLazy, + SqBadge, + useRefSize, +} from '@genshin-optimizer/common/ui' +import { getUnitStr } from '@genshin-optimizer/common/util' +import type { + RelicMainStatKey, + RelicRarityKey, + RelicSetKey, + RelicSlotKey, + RelicSubStatKey, + RelicSubstatTypeKey, +} from '@genshin-optimizer/sr/consts' +import { + allRelicSlotKeys, + isCavernRelicSetKey, + isPlanarRelicSetKey, + relicMaxLevel, +} from '@genshin-optimizer/sr/consts' +import type { + BuildTCLightCone, + BuildTcRelicSlot, + IBuildTc, +} from '@genshin-optimizer/sr/db' +import { + initCharTC, + type ICachedLightCone, + type RelicIds, + type TeammateDatum, +} from '@genshin-optimizer/sr/db' import { useBuild, + useBuildTc, useCharacterContext, useDatabaseContext, } from '@genshin-optimizer/sr/db-ui' +import { SlotIcon } from '@genshin-optimizer/sr/svgicons' import { + COMPACT_ELE_HEIGHT, + COMPACT_ELE_WIDTH, + COMPACT_ELE_WIDTH_NUMBER, LightConeCardCompact, + LightConeCardCompactEmpty, + LightConeCardCompactObj, + LightConeEditorCard, RelicCardCompact, + RelicMainsCardCompact, + RelicMainStatDropdown, + RelicRarityDropdown, + RelicSetAutocomplete, + RelicSetCardCompact, + RelicSetName, + RelicSubCard, + StatDisplay, } from '@genshin-optimizer/sr/ui' +import CloseIcon from '@mui/icons-material/Close' +import DeleteForeverIcon from '@mui/icons-material/DeleteForever' import { Box, + Button, + ButtonGroup, CardActionArea, CardContent, CardHeader, Divider, + IconButton, + InputAdornment, Stack, + Typography, + useTheme, } from '@mui/material' import type { ReactNode } from 'react' -import { useEffect, useMemo } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTeamContext } from './context' -export function BuildsDisplay() { +export function BuildsDisplay({ onClose }: { onClose?: () => void }) { const { database } = useDatabaseContext() const { teammateDatum: { characterKey }, } = useTeamContext() const [dbDirty, setDbDirty] = useForceUpdate() + const [dbTCDirty, setDbTCDirty] = useForceUpdate() const buildIds = useMemo( () => dbDirty && database.builds.getBuildIds(characterKey), [dbDirty, database, characterKey] ) - useEffect(() => { - database.builds.followAny(setDbDirty) - }, [database.builds, setDbDirty]) + const buildTcIds = useMemo( + () => dbTCDirty && database.buildTcs.getBuildTcIds(characterKey), + [dbTCDirty, database, characterKey] + ) + useEffect( + () => database.builds.followAny(setDbDirty), + [database.builds, setDbDirty] + ) + useEffect( + () => database.buildTcs.followAny(setDbTCDirty), + [database.buildTcs, setDbTCDirty] + ) return ( - + + + + ) : null + } + /> + Equipped build + Other builds + {/* TODO: new Build button */} {buildIds.map((buildId) => ( ))} + TC builds + + {buildTcIds.map((buildTcId) => ( + + ))} @@ -58,7 +147,7 @@ function useActiveBuildSwap( const { database } = useDatabaseContext() const { teamId, - teammateDatum: { characterKey, buildType, buildId }, + teammateDatum: { characterKey, buildType, buildId, buildTcId }, } = useTeamContext() return useMemo( @@ -66,8 +155,10 @@ function useActiveBuildSwap( active: buildType === 'equipped' ? buildType === newBuildType - : buildType === 'real' && buildId === newBuildId, - onClick: () => { + : buildType === 'real' + ? buildId === newBuildId + : buildType === 'tc' && buildTcId === newBuildId, + onActive: () => { database.teams.set(teamId, (team) => { const teammateDatum = team.teamMetadata.find( (teammateDatum) => teammateDatum?.characterKey === characterKey @@ -79,12 +170,16 @@ function useActiveBuildSwap( return } teammateDatum.buildType = newBuildType - if (newBuildId) teammateDatum.buildId = newBuildId + if (newBuildType === 'real' && newBuildId) + teammateDatum.buildId = newBuildId + else if (newBuildType === 'tc' && newBuildId) + teammateDatum.buildTcId = newBuildId }) }, }), [ buildId, + buildTcId, buildType, characterKey, database.teams, @@ -97,11 +192,11 @@ function useActiveBuildSwap( export function EquippedBuild() { const character = useCharacterContext()! const { equippedRelics, equippedLightCone } = character - const { active, onClick } = useActiveBuildSwap('equipped') + const { active, onActive } = useActiveBuildSwap('equipped') return ( @@ -112,12 +207,12 @@ export function EquippedBuild() { export function Build({ buildId }: { buildId: string }) { const build = useBuild(buildId)! const { relicIds, lightConeId, name, description } = build - const { active, onClick } = useActiveBuildSwap('real', buildId) + const { active, onActive } = useActiveBuildSwap('real', buildId) return ( } /> @@ -128,26 +223,38 @@ function BuildBase({ description, buildGrid, active, - onClick, + onActive: onEquip, }: { name: string description?: string buildGrid: ReactNode active?: boolean - onClick?: () => void + onActive?: () => void }) { + const onEdit = useCallback(() => {}, []) // TODO: implement return ( ({ outline: active ? `solid ${theme.palette.success.main}` : undefined, })} > - {/* Disable the onClick swap when its the active build */} - - - - {buildGrid} - + + + + + + + + + + {buildGrid} ) } @@ -159,9 +266,55 @@ export function EquipRow({ relicIds: RelicIds lightConeId?: string }) { + const { database } = useDatabaseContext() + const sets = useMemo(() => { + const sets: Partial> = {} + Object.values(relicIds).forEach((relicId) => { + const setKey = database.relics.get(relicId)?.setKey + if (!setKey) return + sets[setKey] = (sets[setKey] || 0) + 1 + }) + return Object.fromEntries( + Object.entries(sets) + .map(([setKey, count]): [RelicSetKey, number] => { + if (count >= 4) return [setKey as RelicSetKey, 4] + if (count >= 2) return [setKey as RelicSetKey, 2] + return [setKey as RelicSetKey, 0] + }) + .filter(([, count]) => count > 0) + ) as Partial> + }, [database.relics, relicIds]) + + // Calculate how many rows is needed for the layout + const [rows, setRows] = useState(2) + const { ref, width } = useRefSize() + const theme = useTheme() + useEffect(() => { + if (!ref.current || !width) return + const fontSize = parseFloat(window.getComputedStyle(ref.current).fontSize) + const spacing = parseFloat(theme.spacing(1)) + const eleWidthPx = fontSize * COMPACT_ELE_WIDTH_NUMBER + const numCols = + Math.floor((width - eleWidthPx) / (eleWidthPx + spacing)) + 1 + const numRows = Math.ceil(8 / numCols) // 6 relic + set + lc + setRows(numRows) + }, [ref, theme, width]) return ( - + + {allRelicSlotKeys.map((slot) => ( ) } +export function BuildTC({ buildTcId }: { buildTcId: string }) { + const build = useBuildTc(buildTcId) + const { name = 'ERROR', description = 'ERROR' } = build ?? {} + const { active, onActive: onClick } = useActiveBuildSwap('tc', buildTcId) + return ( + } + /> + ) +} + +export function EquipRowTC({ buildTcId }: { buildTcId: string }) { + const { database } = useDatabaseContext() + const build = useBuildTc(buildTcId)! + const { lightCone, relic } = build + + const [show, onShow, onHide] = useBoolState() + const [showMainEditor, onShowMainEditor, onHideMainEditor] = useBoolState() + const [showSubsEditor, onShowSubsEditor, onHideSubsEditor] = useBoolState() + const [showSetsEditor, onShowSetsEditor, onHideSetsEditor] = useBoolState() + const onUpdate = useCallback( + (data: Partial) => { + database.buildTcs.set(buildTcId, (buildTc) => ({ + lightCone: { ...buildTc.lightCone, ...data } as BuildTCLightCone, + })) + }, + [buildTcId, database.buildTcs] + ) + const setSlot = useCallback( + (slotKey: RelicSlotKey, data: Partial) => { + database.buildTcs.set(buildTcId, (buildTc) => ({ + relic: { + ...buildTc.relic, + slots: { + ...buildTc.relic.slots, + [slotKey]: { ...buildTc.relic.slots[slotKey], ...data }, + }, + }, + })) + }, + [buildTcId, database.buildTcs] + ) + const setSubstat = useCallback( + (statKey: RelicSubStatKey, value: number) => { + database.buildTcs.set(buildTcId, (buildTc) => ({ + relic: { + ...buildTc.relic, + substats: { + ...buildTc.relic.substats, + stats: { + ...buildTc.relic.substats.stats, + [statKey]: value, + }, + }, + }, + })) + }, + [buildTcId, database.buildTcs] + ) + const setType = useCallback( + (type: RelicSubstatTypeKey) => { + database.buildTcs.set(buildTcId, (buildTc) => ({ + relic: { + ...buildTc.relic, + substats: { + ...buildTc.relic.substats, + type, + }, + }, + })) + }, + [buildTcId, database.buildTcs] + ) + const setSets = useCallback( + (setKey: RelicSetKey, count: number) => { + database.buildTcs.set(buildTcId, (buildTc) => ({ + relic: { + ...buildTc.relic, + sets: { + ...buildTc.relic.sets, + [setKey]: count, + }, + }, + })) + }, + [buildTcId, database.buildTcs] + ) + const removeSet = useCallback( + (setKey: RelicSetKey) => { + database.buildTcs.set(buildTcId, (buildTc) => { + const sets = { ...buildTc.relic.sets } + delete sets[setKey] + return { + relic: { + ...buildTc.relic, + sets, + }, + } + }) + }, + [buildTcId, database.buildTcs] + ) + return ( + + + {lightCone ? ( + + ) : ( + + )} + + + + + + + + + + ) +} +function TCLightconeEditor({ + lightCone = {}, + onUpdate, + show, + onClose, +}: { + lightCone?: Partial + onUpdate: (lightCone: Partial) => void + show: boolean + onClose: () => void +}) { + return ( + + + + ) +} +function TCMainsEditor({ + slots, + show, + onClose, + setSlot, +}: { + slots: IBuildTc['relic']['slots'] + show: boolean + onClose: () => void + setSlot: (slotKey: RelicSlotKey, data: Partial) => void +}) { + return ( + + + + + + } + /> + + + + {Object.entries(slots).map( + ([slotKey, { level, statKey, rarity }]) => ( + + setSlot(slotKey as RelicSlotKey, { statKey }) + } + setRarity={(rarity) => + setSlot(slotKey as RelicSlotKey, { rarity }) + } + setLevel={(level) => + setSlot(slotKey as RelicSlotKey, { level }) + } + /> + ) + )} + + + + + ) +} +function SlotEditor({ + slotKey, + level, + rarity, + statKey, + setStatKey, + setRarity, + setLevel, +}: { + slotKey: RelicSlotKey + level: number + rarity: RelicRarityKey + statKey: RelicMainStatKey + setStatKey: (statKey: RelicMainStatKey) => void + setRarity: (rarity: RelicRarityKey) => void + setLevel: (level: number) => void +}) { + return ( + + + + + + + Level + ), + }} + size="small" + onChange={setLevel} + /> + + ) +} +function TCSubsEditor({ + show, + onClose, + stats, + setStat, +}: { + show: boolean + onClose: () => void + type: RelicSubstatTypeKey + setType: (type: RelicSubstatTypeKey) => void + stats: Record + setStat: (key: RelicSubStatKey, value: number) => void +}) { + return ( + + + + + + } + /> + + + + {/* TODO: sub type + rarity */} + + {Object.entries(stats).map(([statKey, value]) => ( + + setStat(statKey as RelicSubStatKey, value) + } + /> + ))} + + + + + + ) +} +function TCSubstatEditor({ + statKey, + value, + setValue, +}: { + statKey: RelicSubStatKey + value: number + setValue: (v: number) => void +}) { + return ( + + + + ), + }} + /> + + ) +} +function TCRelicSetEditor({ + show, + onClose, + sets, + setSets, + removeSet, +}: { + show: boolean + onClose: () => void + sets: Partial> + setSets: (setKey: RelicSetKey, count: 2 | 4) => void + removeSet: (setKey: RelicSetKey) => void +}) { + const [localSetKey, setLocalSetKey] = useState(null) + const setRelicSetKey = useCallback( + (setKey: RelicSetKey | '') => { + if (!setKey) return + if (isPlanarRelicSetKey(setKey)) { + setSets(setKey, 2) + } else if (isCavernRelicSetKey(setKey)) { + setLocalSetKey(setKey) + } + }, + [setSets] + ) + const setRelicSetCount = useCallback( + (count: 2 | 4) => { + if (localSetKey) { + setSets(localSetKey, count) + setLocalSetKey(null) + } + }, + [localSetKey, setSets] + ) + return ( + + + + + + } + /> + + + + {Object.entries(sets).map(([setKey, count]) => ( + + + {count} + removeSet(setKey as RelicSetKey)} + > + + + + ))} + + + + + + + + + + + + ) +} diff --git a/libs/sr/page-team/src/TeamCalcProvider.tsx b/libs/sr/page-team/src/TeamCalcProvider.tsx index cc7f643f54..c2c368a5b5 100644 --- a/libs/sr/page-team/src/TeamCalcProvider.tsx +++ b/libs/sr/page-team/src/TeamCalcProvider.tsx @@ -1,28 +1,44 @@ +import { notEmpty } from '@genshin-optimizer/common/util' import { constant } from '@genshin-optimizer/pando/engine' import { CalcContext } from '@genshin-optimizer/pando/ui-sheet' -import type { RelicSubStatKey } from '@genshin-optimizer/sr/consts' -import type { ICachedRelic, TeammateDatum } from '@genshin-optimizer/sr/db' +import type { + RelicMainStatKey, + RelicSetKey, + RelicSubStatKey, +} from '@genshin-optimizer/sr/consts' +import type { + IBuildTc, + ICachedRelic, + TeammateDatum, +} from '@genshin-optimizer/sr/db' import { useBuild, + useBuildTc, useCharacter, useLightCone, useRelics, useTeam, } from '@genshin-optimizer/sr/db-ui' -import type { Member, Preset } from '@genshin-optimizer/sr/formula' +import type { + Member, + Preset, + TagMapNodeEntries, +} from '@genshin-optimizer/sr/formula' import { - charData, + charTagMapNodeEntries, conditionalEntries, enemyDebuff, - lightConeData, + lightConeTagMapNodeEntries, members, ownBuff, - relicsData, + relicTagMapNodeEntries, srCalculatorWithEntries, teamData, withMember, withPreset, } from '@genshin-optimizer/sr/formula' +import type { ILightCone } from '@genshin-optimizer/sr/srod' +import { getRelicMainStatVal } from '@genshin-optimizer/sr/util' import type { ReactNode } from 'react' import { useMemo } from 'react' @@ -89,7 +105,7 @@ export function TeamCalcProvider({ function useCharacterAndEquipment(meta: TeammateDatum | undefined) { const character = useCharacter(meta?.characterKey) - // TODO: Handle tc build + const buildTc = useBuildTc(meta?.buildTcId) const build = useBuild(meta?.buildId) const lightCone = useLightCone( meta?.buildType === 'equipped' @@ -105,28 +121,61 @@ function useCharacterAndEquipment(meta: TeammateDatum | undefined) { ? build?.relicIds : undefined ) + const lcTagEntries = useMemo(() => { + const lc = + meta?.buildType === 'tc' ? (buildTc?.lightCone as ILightCone) : lightCone + if (!lc) return [] + return lightConeTagMapNodeEntries( + lc.key, + lc.level, + lc.ascension, + lc.superimpose + ) + }, [meta?.buildType, buildTc?.lightCone, lightCone]) + const relicTagEntries = useMemo(() => { + const tcrelic = buildTc?.relic + if (meta?.buildType === 'tc' && tcrelic) return relicTcData(tcrelic) + if (!relics) return [] + return relicsData(Object.values(relics).filter(notEmpty)) + }, [buildTc?.relic, meta?.buildType, relics]) return useMemo(() => { if (!character) return [] return withMember( character.key, - ...charData(character), - ...lightConeData(lightCone), - ...relicsData( - Object.values(relics) - .filter((relic): relic is ICachedRelic => !!relic) - .map((relic) => ({ - set: relic.setKey, - stats: [ - ...relic.substats - .filter(({ key }) => key !== '') - .map((substat) => ({ - key: substat.key as RelicSubStatKey, // Safe because of the above filter - value: substat.accurateValue, - })), - { key: relic.mainStatKey, value: relic.mainStatVal }, - ], - })) - ) + ...charTagMapNodeEntries(character), + ...lcTagEntries, + ...relicTagEntries ) - }, [character, lightCone, relics]) + }, [character, lcTagEntries, relicTagEntries]) +} +function relicsData(relics: ICachedRelic[]): TagMapNodeEntries { + const sets: Partial> = {}, + stats: Partial> = {} + relics.forEach((relic) => { + sets[relic.setKey] = (sets[relic.setKey] ?? 0) + 1 + stats[relic.mainStatKey] = + (stats[relic.mainStatKey] ?? 0) + relic.mainStatVal + relic.substats.forEach((substat) => { + if (!substat.key || !substat.accurateValue) return + stats[substat.key] = (stats[substat.key] ?? 0) + substat.accurateValue + }) + }) + return relicTagMapNodeEntries(stats, sets) +} + +function relicTcData(relic: IBuildTc['relic']): TagMapNodeEntries { + const { + slots, + substats: { stats: substats }, + sets, + } = relic + const stats = { ...substats } as Record< + RelicMainStatKey | RelicSubStatKey, + number + > + Object.values(slots).forEach(({ level, statKey, rarity }) => { + const val = getRelicMainStatVal(rarity, statKey, level) + stats[statKey] = (stats[statKey] ?? 0) + val + }) + return relicTagMapNodeEntries(stats, sets) } diff --git a/libs/sr/page-team/src/TeammateDisplay.tsx b/libs/sr/page-team/src/TeammateDisplay.tsx index 11588abaf1..1a9825772e 100644 --- a/libs/sr/page-team/src/TeammateDisplay.tsx +++ b/libs/sr/page-team/src/TeammateDisplay.tsx @@ -1,4 +1,7 @@ -import { useBoolState } from '@genshin-optimizer/common/react-util' +import { + useBoolState, + useForceUpdate, +} from '@genshin-optimizer/common/react-util' import { CardThemed, ModalWrapper } from '@genshin-optimizer/common/ui' import type { CharacterKey } from '@genshin-optimizer/sr/consts' import { @@ -24,8 +27,8 @@ import { Stack, Typography, } from '@mui/material' -import { useMemo, useState } from 'react' -import { BuildsDisplay, EquipRow } from './BuildsDisplay' +import { useEffect, useMemo, useState } from 'react' +import { BuildsDisplay, EquipRow, EquipRowTC } from './BuildsDisplay' import { ComboEditor } from './ComboEditor' import { useTeamContext } from './context' import Optimize from './Optimize' @@ -33,7 +36,7 @@ import CharacterTalentPane from './TalentContent' export default function TeammateDisplay() { const { - teammateDatum: { characterKey }, + teammateDatum: { characterKey, buildType }, } = useTeamContext() const character = useCharacterContext() const [editorKey, setCharacterKey] = useState( @@ -62,23 +65,30 @@ export default function TeammateDisplay() { - + {buildType !== 'tc' && } ) } function CurrentBuildDisplay() { const { teammateDatum } = useTeamContext() const { database } = useDatabaseContext() - const { buildName, relicIds, lightConeId } = useMemo( - () => ({ - buildName: database.teams.getActiveBuildName(teammateDatum), - ...database.teams.getTeamActiveBuild(teammateDatum), - }), - [database.teams, teammateDatum] + const { buildType, buildId, buildTcId } = teammateDatum + const [dbDirty, setDbDirty] = useForceUpdate() + const buildName = useMemo( + () => dbDirty && database.teams.getActiveBuildName(teammateDatum), + [database.teams, dbDirty, teammateDatum] ) + useEffect(() => { + let unFollow = () => {} + if (buildType === 'real') + unFollow = database.builds.follow(buildId, setDbDirty) + if (buildType === 'tc') + unFollow = database.buildTcs.follow(buildTcId, setDbDirty) + return () => unFollow() + }, [buildId, buildTcId, buildType, database, setDbDirty]) const [show, onShow, onHide] = useBoolState() return ( - + - + {buildType === 'tc' ? : } ) } +function BuildDisplay() { + const { teammateDatum } = useTeamContext() + const { database } = useDatabaseContext() + const { relicIds, lightConeId } = useMemo( + () => database.teams.getTeamActiveBuild(teammateDatum), + [database.teams, teammateDatum] + ) + return +} +function BuildTCDisplay() { + const { teammateDatum } = useTeamContext() + if (teammateDatum.buildType !== 'tc') return null + return +} function BuildsModal({ show, onClose, @@ -109,7 +133,7 @@ function BuildsModal({ onClose={onClose} containerProps={{ maxWidth: 'xl' }} > - + ) } diff --git a/libs/sr/ui/src/Character/CharacterCard.tsx b/libs/sr/ui/src/Character/CharacterCard.tsx index e6af3053a5..0efc61601f 100644 --- a/libs/sr/ui/src/Character/CharacterCard.tsx +++ b/libs/sr/ui/src/Character/CharacterCard.tsx @@ -13,7 +13,7 @@ import type { CharacterKey } from '@genshin-optimizer/sr/consts' import type { ICachedCharacter } from '@genshin-optimizer/sr/db' import type { Calculator } from '@genshin-optimizer/sr/formula' import { - charData, + charTagMapNodeEntries, own, srCalculatorWithEntries, } from '@genshin-optimizer/sr/formula' @@ -53,7 +53,8 @@ export function CharacterCard({ onClick?: () => void }) { const calc = - useSrCalcContext() ?? srCalculatorWithEntries(charData(character)) + useSrCalcContext() ?? + srCalculatorWithEntries(charTagMapNodeEntries(character)) return ( diff --git a/libs/sr/ui/src/LightCone/LightConeAutocomplete.tsx b/libs/sr/ui/src/LightCone/LightConeAutocomplete.tsx new file mode 100644 index 0000000000..5554fd7b6b --- /dev/null +++ b/libs/sr/ui/src/LightCone/LightConeAutocomplete.tsx @@ -0,0 +1,60 @@ +import type { GeneralAutocompleteOption } from '@genshin-optimizer/common/ui' +import { GeneralAutocomplete, ImgIcon } from '@genshin-optimizer/common/ui' +import { lightConeAsset } from '@genshin-optimizer/sr/assets' +import type { LightConeKey } from '@genshin-optimizer/sr/consts' +import { allLightConeKeys } from '@genshin-optimizer/sr/consts' +import { Skeleton } from '@mui/material' +import { Suspense, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +type LightConeAutocompleteProps = { + lcKey: LightConeKey | '' + setLCKey: (key: LightConeKey | '') => void + label?: string +} +export default function LightConeAutocomplete({ + lcKey, + setLCKey, + label = '', +}: LightConeAutocompleteProps) { + const { t } = useTranslation(['lightCone', 'lightConeNames_gen']) + label = label ? label : t('editor.lightConeName') + + const options = useMemo( + () => + allLightConeKeys.map( + (key): GeneralAutocompleteOption => ({ + key, + label: t(`lightConeNames_gen:${key}`), + }) + ), + [t] + ) + + const onChange = useCallback( + (k: LightConeKey | '' | null) => setLCKey(k ?? ''), + [setLCKey] + ) + const toImg = useCallback( + (key: LightConeKey | '') => + !key ? undefined : ( + + ), + [] + ) + return ( + }> + + + ) +} diff --git a/libs/sr/ui/src/LightCone/LightConeCard.tsx b/libs/sr/ui/src/LightCone/LightConeCard.tsx index 095059aa94..42fe4b2ccd 100644 --- a/libs/sr/ui/src/LightCone/LightConeCard.tsx +++ b/libs/sr/ui/src/LightCone/LightConeCard.tsx @@ -8,7 +8,7 @@ import { lightConeAsset } from '@genshin-optimizer/sr/assets' import type { LocationKey } from '@genshin-optimizer/sr/consts' import type { Calculator } from '@genshin-optimizer/sr/formula' import { - lightConeData, + lightConeTagMapNodeEntries, own, srCalculatorWithEntries, } from '@genshin-optimizer/sr/formula' @@ -56,8 +56,11 @@ export function LightConeCard({ const { key, level, ascension, superimpose, location = '', lock } = lightCone const calc = useMemo( - () => srCalculatorWithEntries(lightConeData(lightCone)), - [lightCone] + () => + srCalculatorWithEntries( + lightConeTagMapNodeEntries(key, level, ascension, superimpose) + ), + [ascension, key, level, superimpose] ) const lcStat = getLightConeStat(key) diff --git a/libs/sr/ui/src/LightCone/LightConeCardCompact.tsx b/libs/sr/ui/src/LightCone/LightConeCardCompact.tsx index 21ccda6377..dc312d1219 100644 --- a/libs/sr/ui/src/LightCone/LightConeCardCompact.tsx +++ b/libs/sr/ui/src/LightCone/LightConeCardCompact.tsx @@ -13,19 +13,23 @@ import type { ICachedLightCone } from '@genshin-optimizer/sr/db' import { useLightCone } from '@genshin-optimizer/sr/db-ui' import type { Calculator } from '@genshin-optimizer/sr/formula' import { - lightConeData, + lightConeTagMapNodeEntries, own, srCalculatorWithEntries, } from '@genshin-optimizer/sr/formula' import { getLightConeStat } from '@genshin-optimizer/sr/stats' import { LightConeIcon, StatIcon } from '@genshin-optimizer/sr/svgicons' import BusinessCenterIcon from '@mui/icons-material/BusinessCenter' -import { Box, CardActionArea, Chip, Typography } from '@mui/material' +import { + Box, + CardActionArea, + CardContent, + Chip, + Typography, +} from '@mui/material' import type { ReactNode } from 'react' import { useCallback, useMemo } from 'react' - -const ELE_HEIGHT = '7em' as const -const ELE_WIDTH = '12em' as const +import { COMPACT_ELE_HEIGHT, COMPACT_ELE_WIDTH } from '../compactConst' export function LightConeCardCompact({ lightConeId, @@ -59,106 +63,122 @@ export function LightConeCardCompactObj({ }) { const actionWrapperFunc = useCallback( (children: ReactNode) => ( - + {children} ), [onClick] ) - const { key, level, location } = lightCone + const { key, level, location, ascension, superimpose } = lightCone const calc = useMemo( - () => srCalculatorWithEntries(lightConeData(lightCone)), - [lightCone] + () => + srCalculatorWithEntries( + lightConeTagMapNodeEntries(key, level, ascension, superimpose) + ), + [ascension, key, level, superimpose] ) const { rarity } = getLightConeStat(key) return ( - - ({ - display: 'flex', - maxHeight: ELE_HEIGHT, - maxWidth: ELE_WIDTH, - borderLeft: '5px solid', - borderImage: `${theme.palette[`grad${rarity}`].gradient} 1`, - })} - > + + ({ display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - }} + height: '100%', + width: '100%', + borderLeft: '5px solid', + borderImage: `${theme.palette[`grad${rarity}`].gradient} 1`, + })} > - + + + {showLocation && ( + + {location ? ( + + ) : ( + + )} + + )} + {` +${level}`}} - sx={{ backgroundColor: 'rgba(0,0,0,0.4)' }} + sx={{ + position: 'absolute', + top: 3, + left: 3, + backgroundColor: 'rgba(0,0,0,0.3)', + backdropFilter: 'blur(2px)', + }} /> - {showLocation && ( - - {location ? ( - - ) : ( - - )} - - )} + + {/* substats */} + + {(['hp', 'atk', 'def'] as const).map((stat) => ( + + ))} - {/* substats */} - - {(['hp', 'atk', 'def'] as const).map((stat) => ( - - ))} - - - + + ) } function SubstatDisplay({ @@ -191,21 +211,40 @@ function SubstatDisplay({ } export function LightConeCardCompactEmpty({ bgt, + onClick, }: { bgt?: CardBackgroundColor + onClick?: () => void }) { + const actionWrapperFunc = useCallback( + (children: ReactNode) => ( + + {children} + + ), + [onClick] + ) return ( - + + + + + ) } diff --git a/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditor.tsx b/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditor.tsx index 2753a0d10d..315fe9331f 100644 --- a/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditor.tsx +++ b/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditor.tsx @@ -1,435 +1,130 @@ -import { useForceUpdate } from '@genshin-optimizer/common/react-util' -import type { GeneralAutocompleteOption } from '@genshin-optimizer/common/ui' -import { - CardThemed, - DropdownButton, - GeneralAutocomplete, - ModalWrapper, -} from '@genshin-optimizer/common/ui' -import { clamp, deepClone } from '@genshin-optimizer/common/util' -import type { - AscensionKey, - LightConeKey, - SuperimposeKey, -} from '@genshin-optimizer/sr/consts' -import { - allLightConeKeys, - allSuperimposeKeys, - lightConeMaxLevel, -} from '@genshin-optimizer/sr/consts' -import type { ICachedLightCone } from '@genshin-optimizer/sr/db' -import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' +import { ModalWrapper } from '@genshin-optimizer/common/ui' +import { validateLightCone } from '@genshin-optimizer/sr/db' +import { useDatabaseContext, useLightCone } from '@genshin-optimizer/sr/db-ui' import type { ILightCone } from '@genshin-optimizer/sr/srod' -import { ascensionMaxLevel, milestoneLevels } from '@genshin-optimizer/sr/util' -import { Add, Close, DeleteForever, Update } from '@mui/icons-material' -import { - Box, - Button, - CardContent, - CardHeader, - Grid, - IconButton, - MenuItem, - Skeleton, - Typography, -} from '@mui/material' +import { Add, DeleteForever, Update } from '@mui/icons-material' +import { Box, Button } from '@mui/material' import type { MouseEvent } from 'react' -import { - Suspense, - useCallback, - useEffect, - useMemo, - useReducer, - useState, -} from 'react' +import { Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { LocationAutocomplete } from '../../Character' -import { lightConeReducer } from './reducer' +import { LightConeEditorCard } from './LightConeEditorCard' -// TODO: temporary until light cone sheet is implemented -interface ILightConeSheet { - superimpose: readonly SuperimposeKey[] -} - -const tempLightConeSheet: ILightConeSheet = { - superimpose: [1, 2, 3, 4, 5], -} - -// TODO: light cone sheets, errors, autocomplete, display text, i18n, ... export type LightConeEditorProps = { - lightConeIdToEdit?: string + lightConeIdToEdit?: string | '' | 'new' cancelEdit: () => void } export function LightConeEditor({ - lightConeIdToEdit = 'new', + lightConeIdToEdit = '', cancelEdit, }: LightConeEditorProps) { const { t } = useTranslation(['lightCone', 'common']) const { database } = useDatabaseContext() - const [dirtyDatabase, setDirtyDatabase] = useForceUpdate() - - useEffect( - () => database.lightCones.followAny(setDirtyDatabase), - [database, setDirtyDatabase] - ) - - const [showEditor, setShowEditor] = useState(false) + const dbLightCone = useLightCone(lightConeIdToEdit) + const [lightConeState, setLightConeState] = useState< + Partial | undefined + >(undefined) useEffect(() => { - if (lightConeIdToEdit === 'new') { - setShowEditor(true) - dispatchLightCone({ type: 'reset' }) - } - const dbLightCone = - lightConeIdToEdit && - dirtyDatabase && - database.lightCones.get(lightConeIdToEdit) - if (dbLightCone) { - setShowEditor(true) - dispatchLightCone({ - type: 'overwrite', - lightCone: deepClone(dbLightCone), + if (dbLightCone) setLightConeState(structuredClone(dbLightCone)) + }, [dbLightCone]) + useEffect(() => { + if (lightConeIdToEdit === 'new') + setLightConeState({ + level: 1, + ascension: 0, + superimpose: 1, }) - } - }, [lightConeIdToEdit, database, dirtyDatabase]) - - const [lightCone, dispatchLightCone] = useReducer(lightConeReducer, undefined) - const sheet: ILightConeSheet | undefined = lightCone - ? tempLightConeSheet - : undefined + }, [lightConeIdToEdit]) - const { - prev, - prevEditType, - }: { - prev: ICachedLightCone | undefined - prevEditType: 'edit' | '' - } = useMemo(() => { - const dbLightCone = - dirtyDatabase && - lightConeIdToEdit && - database.lightCones.get(lightConeIdToEdit) - if (dbLightCone) return { prev: dbLightCone, prevEditType: 'edit' } - return { prev: undefined, prevEditType: '' } - }, [lightConeIdToEdit, database, dirtyDatabase]) + const update = useCallback((newValue: Partial) => { + setLightConeState((lc) => ({ ...lc, ...newValue })) + }, []) - const update = useCallback( - (newValue: Partial) => { - // const newSheet = newValue.key ? getLightConeSheet(newValue.key) : sheet! - const newSheet = newValue.key ? tempLightConeSheet : sheet! - - function pick( - value: T | undefined, - available: readonly T[], - prefer?: T - ): T { - return value && available.includes(value) - ? value - : prefer ?? available[0] - } - - if (newValue.key) { - newValue.superimpose = pick( - lightCone?.superimpose, - newSheet.superimpose, - Math.min(...newSheet.superimpose) as SuperimposeKey - ) - } - if (newValue.level) { - newValue.level = clamp(newValue.level, 0, lightConeMaxLevel) - } - - dispatchLightCone({ type: 'update', lightCone: newValue }) - }, - [lightCone, sheet, dispatchLightCone] - ) - - const reset = useCallback(() => { - cancelEdit?.() - dispatchLightCone({ type: 'reset' }) - }, [cancelEdit, dispatchLightCone]) + const clear = useCallback(() => { + cancelEdit() + setLightConeState(undefined) + }, [cancelEdit]) const onClose = useCallback( (e: MouseEvent) => { if ( - !lightConeIdToEdit && - lightCone && + lightConeState?.key && !window.confirm(t('editor.clearPrompt') as string) ) { e?.preventDefault() return } - setShowEditor(false) - reset() + clear() }, - [t, lightConeIdToEdit, lightCone, setShowEditor, reset] + [t, lightConeState, clear] ) - const removeId = - (lightConeIdToEdit !== 'new' && lightConeIdToEdit) || prev?.id - - return ( - - - - - - - } - /> - - - {/* name */} - - update({ key: lcKey as LightConeKey })} - label={lightCone?.key ? '' : t('editor.unknownLightCone')} - /> - - - {/* superimpose */} - - - update({ superimpose: sk }) - } - disabled={!lightCone} - /> - - - - {/* level */} - - { - update({ level: lv, ascension: as }) - }} - disabled={!lightCone} - /> - - - {/* ascension */} - - - }> - - {t('editor.ascension')} {lightCone?.ascension || 0} - - - - - - {/* character location */} - - update({ location: charKey })} - /> - - - - {prevEditType === 'edit' ? ( - - ) : ( - - )} - {prev && prevEditType !== 'edit' && ( - - )} - {!!removeId && ( - - )} - - - - - + const validatedLightcone = useMemo( + () => validateLightCone(lightConeState), + [lightConeState] ) -} - -type LightConeAutocompleteProps = { - lcKey: LightConeKey | '' - setLCKey: (key: LightConeKey | '') => void - label?: string -} - -export default function LightConeAutocomplete({ - lcKey, - setLCKey, - label = '', -}: LightConeAutocompleteProps) { - const { t } = useTranslation(['lightCone', 'lightConeNames_gen']) - label = label ? label : t('editor.lightConeName') - - const options = useMemo( - () => - allLightConeKeys.map( - (key): GeneralAutocompleteOption => ({ - key, - label: t(`lightConeNames_gen:${key}`), - }) - ), - [t] - ) - - const onChange = useCallback( - (k: LightConeKey | '' | null) => setLCKey(k ?? ''), - [setLCKey] - ) - return ( - }> - <> } // TODO - label={label} - /> - - ) -} - -type SuperimpositionDropdownProps = { - superimpose: SuperimposeKey | undefined - setSuperimposition: (superimpose: SuperimposeKey) => void - disabled?: boolean -} -function SuperimpositionDropdown({ - superimpose, - setSuperimposition, - disabled = false, -}: SuperimpositionDropdownProps) { - const { t } = useTranslation('lightCone_gen') - return ( - - {allSuperimposeKeys.map((sk) => ( - setSuperimposition(sk)} + const footer = useMemo( + () => ( + + + {validatedLightcone && dbLightCone && ( + + )} + {!!dbLightCone?.id && ( + + )} + + ), + [clear, database, dbLightCone, t, validatedLightcone] ) -} - -type LevelDropdownProps = { - level: number | undefined - ascension: AscensionKey | undefined - setLevelAscension: ( - level: number | undefined, - ascension: AscensionKey | undefined - ) => void - disabled?: boolean -} - -function LevelDropdown({ - level, - ascension, - setLevelAscension, - disabled = false, -}: LevelDropdownProps) { - const { t } = useTranslation(['common_gen', 'common']) return ( - - {milestoneLevels.map(([lv, as]) => ( - setLevelAscension(lv, as)} - > - {lv === ascensionMaxLevel[as] - ? `${t('lv')} ${lv}` - : `${t('lv')} ${lv}/${ascensionMaxLevel[as]}`} - - ))} - + + {lightConeState && ( + + + + )} + ) } diff --git a/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditorCard.tsx b/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditorCard.tsx new file mode 100644 index 0000000000..9150fa88ff --- /dev/null +++ b/libs/sr/ui/src/LightCone/LightConeEditor/LightConeEditorCard.tsx @@ -0,0 +1,199 @@ +import { CardThemed, DropdownButton } from '@genshin-optimizer/common/ui' +import type { + AscensionKey, + LightConeKey, + SuperimposeKey, +} from '@genshin-optimizer/sr/consts' +import { allSuperimposeKeys } from '@genshin-optimizer/sr/consts' +import type { ILightCone } from '@genshin-optimizer/sr/srod' +import { ascensionMaxLevel, milestoneLevels } from '@genshin-optimizer/sr/util' +import CloseIcon from '@mui/icons-material/Close' +import { + CardContent, + CardHeader, + Grid, + IconButton, + MenuItem, + Skeleton, + Typography, +} from '@mui/material' +import type { MouseEvent, ReactNode } from 'react' +import { Suspense } from 'react' +import { useTranslation } from 'react-i18next' +import { LocationAutocomplete } from '../../Character' +import LightConeAutocomplete from '../LightConeAutocomplete' + +export function LightConeEditorCard({ + onClose, + lightCone, + setLightCone, + footer, + hideLocation = false, +}: { + onClose: (e: MouseEvent) => void + lightCone: Partial + setLightCone: (lightConeData: Partial) => void + footer?: ReactNode + hideLocation?: boolean +}) { + const { t } = useTranslation(['lightCone', 'common']) + + return ( + + + + + } + /> + + + {/* name */} + + setLightCone({ key: lcKey as LightConeKey })} + label={lightCone?.key ? '' : t('editor.unknownLightCone')} + /> + + + {/* superimpose */} + + + setLightCone({ superimpose: sk }) + } + disabled={!lightCone?.key} + /> + + + + {/* level */} + + { + setLightCone({ level: lv, ascension: as }) + }} + disabled={!lightCone?.key} + /> + + + {/* ascension */} + + + }> + + {t('editor.ascension')} {lightCone?.ascension || 0} + + + + + + {/* character location */} + {!hideLocation && ( + + setLightCone({ location: charKey })} + /> + + )} + + {footer} + + + ) +} + +type SuperimpositionDropdownProps = { + superimpose: SuperimposeKey | undefined + setSuperimposition: (superimpose: SuperimposeKey) => void + disabled?: boolean +} + +function SuperimpositionDropdown({ + superimpose, + setSuperimposition, + disabled = false, +}: SuperimpositionDropdownProps) { + const { t } = useTranslation('lightCone_gen') + return ( + + {allSuperimposeKeys.map((sk) => ( + setSuperimposition(sk)} + > + {t('superimpose')} {sk} + + ))} + + ) +} + +type LevelDropdownProps = { + level: number | undefined + ascension: AscensionKey | undefined + setLevelAscension: ( + level: number | undefined, + ascension: AscensionKey | undefined + ) => void + disabled?: boolean +} + +function LevelDropdown({ + level, + ascension, + setLevelAscension, + disabled = false, +}: LevelDropdownProps) { + const { t } = useTranslation(['common_gen', 'common']) + + return ( + + {milestoneLevels.map(([lv, as]) => ( + setLevelAscension(lv, as)} + > + {lv === ascensionMaxLevel[as] + ? `${t('lv')} ${lv}` + : `${t('lv')} ${lv}/${ascensionMaxLevel[as]}`} + + ))} + + ) +} diff --git a/libs/sr/ui/src/LightCone/LightConeEditor/index.tsx b/libs/sr/ui/src/LightCone/LightConeEditor/index.tsx index 060019619e..8153d8c58c 100644 --- a/libs/sr/ui/src/LightCone/LightConeEditor/index.tsx +++ b/libs/sr/ui/src/LightCone/LightConeEditor/index.tsx @@ -1 +1,2 @@ export * from './LightConeEditor' +export * from './LightConeEditorCard' diff --git a/libs/sr/ui/src/LightCone/LightConeEditor/reducer.ts b/libs/sr/ui/src/LightCone/LightConeEditor/reducer.ts deleted file mode 100644 index f67787e525..0000000000 --- a/libs/sr/ui/src/LightCone/LightConeEditor/reducer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { validateLightCone } from '@genshin-optimizer/sr/db' -import type { ILightCone } from '@genshin-optimizer/sr/srod' - -type ResetMessage = { type: 'reset' } -type OverwriteMessage = { type: 'overwrite'; lightCone: ILightCone } -type UpdateMessage = { type: 'update'; lightCone: Partial } -type Message = ResetMessage | OverwriteMessage | UpdateMessage - -export function lightConeReducer( - state: ILightCone | undefined, - action: Message -): ILightCone | undefined { - const handle = () => { - switch (action.type) { - case 'reset': - return undefined - case 'overwrite': - return action.lightCone - case 'update': - return { ...state!, ...action.lightCone } - } - } - const lc = handle() - if (!lc) return lc - return validateLightCone(lc) -} diff --git a/libs/sr/ui/src/LightCone/index.tsx b/libs/sr/ui/src/LightCone/index.tsx index 12ba6868a8..9c1afbcf9f 100644 --- a/libs/sr/ui/src/LightCone/index.tsx +++ b/libs/sr/ui/src/LightCone/index.tsx @@ -1,3 +1,4 @@ +export * from './LightConeAutocomplete' export * from './LightConeCard' export * from './LightConeCardCompact' export * from './LightConeEditor' diff --git a/libs/sr/ui/src/Relic/RelicCardCompact.tsx b/libs/sr/ui/src/Relic/RelicCardCompact.tsx index 1dcee7bcd9..fc6fa709a1 100644 --- a/libs/sr/ui/src/Relic/RelicCardCompact.tsx +++ b/libs/sr/ui/src/Relic/RelicCardCompact.tsx @@ -1,8 +1,10 @@ +import { iconInlineProps } from '@genshin-optimizer/common/svgicons' import type { CardBackgroundColor } from '@genshin-optimizer/common/ui' import { CardThemed, ConditionalWrapper, NextImage, + SqBadge, } from '@genshin-optimizer/common/ui' import { getUnitStr, toPercent } from '@genshin-optimizer/common/util' import { @@ -10,8 +12,16 @@ import { characterKeyToGenderedKey, relicAsset, } from '@genshin-optimizer/sr/assets' -import type { RelicSlotKey } from '@genshin-optimizer/sr/consts' -import type { ICachedRelic, ICachedSubstat } from '@genshin-optimizer/sr/db' +import type { + RelicSetKey, + RelicSlotKey, + RelicSubStatKey, +} from '@genshin-optimizer/sr/consts' +import type { + IBuildTc, + ICachedRelic, + ICachedSubstat, +} from '@genshin-optimizer/sr/db' import { useRelic } from '@genshin-optimizer/sr/db-ui' import { SlotIcon, StatIcon } from '@genshin-optimizer/sr/svgicons' import { @@ -22,9 +32,8 @@ import BusinessCenterIcon from '@mui/icons-material/BusinessCenter' import { Box, CardActionArea, Chip, Typography } from '@mui/material' import type { ReactNode } from 'react' import { useCallback } from 'react' - -const ELE_HEIGHT = '7em' as const -const ELE_WIDTH = '12em' as const +import { COMPACT_ELE_HEIGHT, COMPACT_ELE_WIDTH } from '../compactConst' +import { RelicSetName } from './RelicTrans' export function RelicCardCompact({ relicId, @@ -63,7 +72,7 @@ export function RelicCardCompactObj({ }) { const actionWrapperFunc = useCallback( (children: ReactNode) => ( - + {children} ), @@ -74,126 +83,133 @@ export function RelicCardCompactObj({ relic return ( - - ({ - display: 'flex', - height: ELE_HEIGHT, - width: ELE_WIDTH, - borderLeft: '5px solid', - borderImage: `${theme.palette[`grad${rarity}`].gradient} 1`, - })} - > + + ({ display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - }} + height: '100%', + width: '100%', + borderLeft: '5px solid', + borderImage: `${theme.palette[`grad${rarity}`].gradient} 1`, + })} > - - {showLocation && ( - - {location ? ( - - ) : ( - - )} - - )} - - {` +${level}`}} - sx={{ - position: 'absolute', - top: 3, - left: 3, - backgroundColor: 'rgba(0,0,0,0.3)', - backdropFilter: 'blur(2px)', - }} - /> - {/* mainstats */} - - - - {getRelicMainStatDisplayVal(rarity, mainStatKey, level)} - {getUnitStr(mainStatKey)} - - - - {/* substats */} - - {substats.map((stat: ICachedSubstat, i: number) => ( - - ))} + + {showLocation && ( + + {location ? ( + + ) : ( + + )} + + )} + + {` +${level}`}} + sx={{ + position: 'absolute', + top: 3, + left: 3, + backgroundColor: 'rgba(0,0,0,0.3)', + backdropFilter: 'blur(2px)', + }} + /> + {/* mainstats */} + + + + {getRelicMainStatDisplayVal(rarity, mainStatKey, level)} + {getUnitStr(mainStatKey)} + + + + {/* substats */} + + {substats.map((stat: ICachedSubstat, i: number) => ( + + ))} + - - + + ) } function SubstatDisplay({ stat }: { stat: ICachedSubstat }) { @@ -232,8 +248,8 @@ export function RelicCardCompactEmpty({ bgt={bgt} sx={{ display: 'flex', - height: ELE_HEIGHT, - width: ELE_WIDTH, + height: COMPACT_ELE_HEIGHT, + width: COMPACT_ELE_WIDTH, alignItems: 'center', justifyContent: 'center', }} @@ -245,3 +261,163 @@ export function RelicCardCompactEmpty({ ) } + +export function RelicSubCard({ + relic, + keys, + bgt, + onClick, +}: { + relic: IBuildTc['relic'] + keys: RelicSubStatKey[] + bgt?: CardBackgroundColor + onClick?: () => void +}) { + const actionWrapperFunc = useCallback( + (children: ReactNode) => ( + + {children} + + ), + [onClick] + ) + return ( + + + + {keys.map((key) => ( + + + + {(relic.substats.stats[key] ?? 0).toFixed(statToFixed(key))} + {getUnitStr(key)} + + + ))} + + + + ) +} +export function RelicSetCardCompact({ + sets, + bgt, + onClick, +}: { + sets: Partial> + bgt?: CardBackgroundColor + onClick?: () => void +}) { + const actionWrapperFunc = useCallback( + (children: ReactNode) => ( + + {children} + + ), + [onClick] + ) + return ( + + + + {/* TODO: translate */} + {!Object.keys(sets).length && No Relic sets} + {Object.entries(sets).map(([key, count]) => ( + + {count} + + ))} + + + + ) +} + +export function RelicMainsCardCompact({ + slots, + bgt, + onClick, +}: { + slots: IBuildTc['relic']['slots'] + bgt?: CardBackgroundColor + onClick?: () => void +}) { + const actionWrapperFunc = useCallback( + (children: ReactNode) => ( + + {children} + + ), + [onClick] + ) + return ( + + + + {Object.entries(slots).map(([slotKey, { level, statKey }]) => ( + + {' '} + +{level} + + ))} + + + + ) +} diff --git a/libs/sr/ui/src/Relic/RelicEditor/RelicEditor.tsx b/libs/sr/ui/src/Relic/RelicEditor/RelicEditor.tsx index ed788eaaf6..7365231f56 100644 --- a/libs/sr/ui/src/Relic/RelicEditor/RelicEditor.tsx +++ b/libs/sr/ui/src/Relic/RelicEditor/RelicEditor.tsx @@ -18,7 +18,7 @@ import type { ICachedRelic } from '@genshin-optimizer/sr/db' import { cachedRelic } from '@genshin-optimizer/sr/db' import { useDatabaseContext } from '@genshin-optimizer/sr/db-ui' import type { IRelic, ISubstat } from '@genshin-optimizer/sr/srod' -import { SlotIcon, StatIcon } from '@genshin-optimizer/sr/svgicons' +import { SlotIcon } from '@genshin-optimizer/sr/svgicons' import { getRelicMainStatDisplayVal } from '@genshin-optimizer/sr/util' import AddIcon from '@mui/icons-material/Add' import ChevronRightIcon from '@mui/icons-material/ChevronRight' @@ -57,10 +57,10 @@ import { import { useTranslation } from 'react-i18next' import { LocationAutocomplete } from '../../Character' import { RelicCard } from '../RelicCard' +import { RelicMainStatDropdown } from '../RelicMainStatDropdown' +import { RelicRarityDropdown } from '../RelicRarityDropdown' +import { RelicSetAutocomplete } from '../RelicSetAutocomplete' import { relicReducer } from './reducer' -import RelicRarityDropdown from './RelicRarityDropdown' -import { RelicSetAutocomplete } from './RelicSetAutocomplete' -import { RelicStatWithUnit } from './RelicStatKeyDisplay' import SubstatInput from './SubstatInput' // TODO: temporary until relic sheet is implemented @@ -357,39 +357,16 @@ export function RelicEditor({ {/* main stat */} - - ) : undefined - } - title={ - - {relic ? ( - - ) : ( - t('mainStat') - )} - - } - disabled={!sheet} - color={relic ? 'success' : 'primary'} - > - {relicSlotToMainStatKeys[slotKey].map((mk) => ( - update({ mainStatKey: mk })} - > - {/* TODO: Replace with colored text with icon at some point */} - - - - - - ))} - + update({ mainStatKey })} + defText={t('mainStat')} + dropdownButtonProps={{ + disabled: !sheet, + color: relic ? 'success' : 'primary', + }} + /> {relic diff --git a/libs/sr/ui/src/Relic/RelicEditor/RelicStatKeyDisplay.tsx b/libs/sr/ui/src/Relic/RelicEditor/RelicStatKeyDisplay.tsx deleted file mode 100644 index 14b53efe14..0000000000 --- a/libs/sr/ui/src/Relic/RelicEditor/RelicStatKeyDisplay.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { iconInlineProps } from '@genshin-optimizer/common/svgicons' -import type { - RelicMainStatKey, - RelicSubStatKey, -} from '@genshin-optimizer/sr/consts' -import { StatIcon } from '@genshin-optimizer/sr/svgicons' -import { Box } from '@mui/material' -import { useTranslation } from 'react-i18next' -import { relicStatPercent } from '../util' - -// Special consideration for relic stats, by displaying % behind hp_, atk_ and def_. -export function RelicStatWithUnit({ - statKey, -}: { - statKey: RelicMainStatKey | RelicSubStatKey -}) { - const { t: tk } = useTranslation('statKey_gen') - return ( - - {tk(statKey)} - {relicStatPercent(statKey)} - - ) -} -export function RelicIconStatWithUnit({ - statKey, - disableIcon = false, -}: { - statKey: RelicMainStatKey | RelicSubStatKey - disableIcon?: boolean -}) { - return ( - - {!disableIcon && ( - - )} - - - ) -} - -// TODO: find alternative to KeyMap from WR to use for grabbing color programmatically -// export function RelicColoredIconStatWithUnit({ -// statKey, -// disableIcon = false, -// }: { -// statKey: MainStatKey | SubstatKey -// disableIcon?: boolean -// }) { -// return ( -// -// -// -// ) -// } diff --git a/libs/sr/ui/src/Relic/RelicEditor/SubstatInput.tsx b/libs/sr/ui/src/Relic/RelicEditor/SubstatInput.tsx index 11abcb0c9c..dd22d5f3a5 100644 --- a/libs/sr/ui/src/Relic/RelicEditor/SubstatInput.tsx +++ b/libs/sr/ui/src/Relic/RelicEditor/SubstatInput.tsx @@ -32,7 +32,7 @@ import { } from '@mui/material' import { useEffect, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { RelicStatWithUnit } from './RelicStatKeyDisplay' +import { StatDisplay } from '../../Character' // TODO: validation, roll table, roll values, efficiency, display text, icons, ... export default function SubstatInput({ @@ -94,7 +94,7 @@ export default function SubstatInput({ startIcon={key ? : undefined} title={ key ? ( - + ) : ( t('editor.substat.substatFormat', { value: index + 1 }) ) @@ -123,7 +123,7 @@ export default function SubstatInput({ - + ))} diff --git a/libs/sr/ui/src/Relic/RelicEditor/index.tsx b/libs/sr/ui/src/Relic/RelicEditor/index.tsx index 76eac1a27a..53fa20f5a7 100644 --- a/libs/sr/ui/src/Relic/RelicEditor/index.tsx +++ b/libs/sr/ui/src/Relic/RelicEditor/index.tsx @@ -1,2 +1 @@ export * from './RelicEditor' -export * from './RelicStatKeyDisplay' diff --git a/libs/sr/ui/src/Relic/RelicMainStatDropdown.tsx b/libs/sr/ui/src/Relic/RelicMainStatDropdown.tsx new file mode 100644 index 0000000000..c76a9ae95c --- /dev/null +++ b/libs/sr/ui/src/Relic/RelicMainStatDropdown.tsx @@ -0,0 +1,64 @@ +import type { DropdownButtonProps } from '@genshin-optimizer/common/ui' +import { CardThemed, DropdownButton } from '@genshin-optimizer/common/ui' +import type { + RelicMainStatKey, + RelicSlotKey, +} from '@genshin-optimizer/sr/consts' +import { relicSlotToMainStatKeys } from '@genshin-optimizer/sr/consts' +import { StatIcon } from '@genshin-optimizer/sr/svgicons' +import { Box, ListItemIcon, MenuItem } from '@mui/material' +import type { ReactNode } from 'react' +import { StatDisplay } from '../Character' + +export function RelicMainStatDropdown({ + statKey, + slotKey, + setStatKey, + defText, + dropdownButtonProps = {}, +}: { + statKey?: RelicMainStatKey + slotKey: RelicSlotKey + setStatKey: (statKey: RelicMainStatKey) => void + defText?: ReactNode + dropdownButtonProps?: Omit +}) { + if ((slotKey === 'head' || slotKey === 'hands') && statKey) + return ( + + + + ) + return ( + : undefined} + title={ + + {statKey ? ( + + ) : ( + defText + )} + + } + {...dropdownButtonProps} + > + {relicSlotToMainStatKeys[slotKey].map((mk) => ( + setStatKey(mk)} + > + + + + + + ))} + + ) +} diff --git a/libs/sr/ui/src/Relic/RelicEditor/RelicRarityDropdown.tsx b/libs/sr/ui/src/Relic/RelicRarityDropdown.tsx similarity index 53% rename from libs/sr/ui/src/Relic/RelicEditor/RelicRarityDropdown.tsx rename to libs/sr/ui/src/Relic/RelicRarityDropdown.tsx index f40bb99285..736e9a8786 100644 --- a/libs/sr/ui/src/Relic/RelicEditor/RelicRarityDropdown.tsx +++ b/libs/sr/ui/src/Relic/RelicRarityDropdown.tsx @@ -1,4 +1,5 @@ import { DropdownButton, StarsDisplay } from '@genshin-optimizer/common/ui' +import type { RarityKey } from '@genshin-optimizer/sr/consts' import { allRelicRarityKeys, type RelicRarityKey, @@ -10,13 +11,15 @@ import { useTranslation } from 'react-i18next' type props = ButtonProps & { rarity?: RelicRarityKey onRarityChange: (rarity: RelicRarityKey) => void - filter: (rarity: RelicRarityKey) => boolean + filter?: (rarity: RelicRarityKey) => boolean + showNumber?: boolean } -export default function RelicRarityDropdown({ +export function RelicRarityDropdown({ rarity, onRarityChange, filter, + showNumber = false, ...props }: props) { const { t } = useTranslation('relic') @@ -24,19 +27,38 @@ export default function RelicRarityDropdown({ : t('editor.rarity') + rarity ? ( + + ) : ( + t('editor.rarity') + ) } color={rarity ? 'success' : 'primary'} > {allRelicRarityKeys.map((rarity) => ( onRarityChange(rarity)} > - + ))} ) } +function StarNumDisplay({ + stars, + showNumber, +}: { + stars: RarityKey + showNumber?: boolean +}) { + if (showNumber) + return ( + + {stars} + + ) + return +} diff --git a/libs/sr/ui/src/Relic/RelicEditor/RelicSetAutocomplete.tsx b/libs/sr/ui/src/Relic/RelicSetAutocomplete.tsx similarity index 100% rename from libs/sr/ui/src/Relic/RelicEditor/RelicSetAutocomplete.tsx rename to libs/sr/ui/src/Relic/RelicSetAutocomplete.tsx diff --git a/libs/sr/ui/src/Relic/index.tsx b/libs/sr/ui/src/Relic/index.tsx index e011f6cad6..ef6093feda 100644 --- a/libs/sr/ui/src/Relic/index.tsx +++ b/libs/sr/ui/src/Relic/index.tsx @@ -3,5 +3,8 @@ export * from './RelicCard' export * from './RelicCardCompact' export * from './RelicEditor' export * from './RelicInventory' +export * from './RelicMainStatDropdown' +export * from './RelicRarityDropdown' +export * from './RelicSetAutocomplete' export * from './RelicTrans' export * from './util' diff --git a/libs/sr/ui/src/compactConst.ts b/libs/sr/ui/src/compactConst.ts new file mode 100644 index 0000000000..217364befa --- /dev/null +++ b/libs/sr/ui/src/compactConst.ts @@ -0,0 +1,4 @@ +export const COMPACT_ELE_HEIGHT_NUMBER = 8 as const +export const COMPACT_ELE_HEIGHT = `${COMPACT_ELE_HEIGHT_NUMBER}em` as const +export const COMPACT_ELE_WIDTH_NUMBER = 12 as const +export const COMPACT_ELE_WIDTH = `${COMPACT_ELE_WIDTH_NUMBER}em` as const diff --git a/libs/sr/ui/src/index.tsx b/libs/sr/ui/src/index.tsx index 3842adbb18..d28e487b7d 100644 --- a/libs/sr/ui/src/index.tsx +++ b/libs/sr/ui/src/index.tsx @@ -1,4 +1,5 @@ export * from './Character' +export * from './compactConst' export * from './Components' export * from './Hook' export * from './LightCone'