From 059a2359428a732186cb62da7a1c10814708d8b8 Mon Sep 17 00:00:00 2001 From: frzyc Date: Tue, 14 Jan 2025 23:07:54 -0500 Subject: [PATCH] update disc editor --- .../Database/DataManagers/DiscDataManager.ts | 163 +--------- libs/zzz/db/src/Interfaces/IDisc.ts | 2 +- libs/zzz/page-discs/src/index.tsx | 34 ++- .../zzz/ui/src/Disc/DiscEditor/DiscEditor.tsx | 285 ++++++------------ .../ui/src/Disc/DiscEditor/SubstatInput.tsx | 8 +- libs/zzz/ui/src/Disc/DiscEditor/reducer.ts | 50 --- 6 files changed, 137 insertions(+), 405 deletions(-) delete mode 100644 libs/zzz/ui/src/Disc/DiscEditor/reducer.ts diff --git a/libs/zzz/db/src/Database/DataManagers/DiscDataManager.ts b/libs/zzz/db/src/Database/DataManagers/DiscDataManager.ts index 70ce42bd27..2a95d812fb 100644 --- a/libs/zzz/db/src/Database/DataManagers/DiscDataManager.ts +++ b/libs/zzz/db/src/Database/DataManagers/DiscDataManager.ts @@ -11,11 +11,9 @@ import { allDiscSubStatKeys, discMaxLevel, discSlotToMainStatKeys, - getDiscMainStatVal, } from '@genshin-optimizer/zzz/consts' import type { ICachedDisc, - ICachedSubstat, IZenlessObjectDescription, IZZZDatabase, } from '../../Interfaces' @@ -38,7 +36,7 @@ export class DiscDataManager extends DataManager< } override toCache(storageObj: IDisc, id: string): ICachedDisc | undefined { // Generate cache fields - const newDisc = cachedDisc(storageObj, id).disc + const newDisc = { ...storageObj, id } as ICachedDisc // Check relations and update equipment /* TODO: @@ -217,7 +215,14 @@ export class DiscDataManager extends DataManager< editorDisc: IDisc, idList = this.keys ): { duplicated: ICachedDisc[]; upgraded: ICachedDisc[] } { - const { setKey, rarity, level, slotKey, mainStatKey, substats } = editorDisc + const { + setKey, + rarity, + level = 0, + slotKey, + mainStatKey, + substats = [], + } = editorDisc const discs = idList .map((id) => this.get(id)) @@ -286,153 +291,6 @@ export class DiscDataManager extends DataManager< } } -export function cachedDisc( - flex: IDisc, - id: string -): { disc: ICachedDisc; errors: string[] } { - const { location, lock, trash, setKey, slotKey, rarity, mainStatKey } = flex - const level = Math.round( - Math.min(Math.max(0, flex.level), discMaxLevel[rarity]) - ) - const mainStatVal = getDiscMainStatVal(rarity, mainStatKey, level) - - const errors: string[] = [] - const substats: ICachedSubstat[] = flex.substats.map((substat) => ({ - ...substat, - rolls: 0, - accurateValue: substat.value, - })) - - const validated: ICachedDisc = { - id, - setKey, - location, - slotKey, - lock, - trash, - mainStatKey, - rarity, - level, - substats, - mainStatVal, - } - - // TODO: Validate rolls - // const allPossibleRolls: { index: number; substatRolls: number[][] }[] = [] - // let totalUnambiguousRolls = 0 - - // function efficiency(value: number, key: DiscSubStatKey): 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 { disc: validated, errors } - - // const { low, high } = discSubstatRollData[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 DiscSubStatKey - // 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 disc (level ${level}) should have no more than ${upperBound} rolls. It currently has ${totalRolls} rolls.` - // ) - // else if (totalRolls < lowerBound) - // errors.push( - // `${rarity}-star disc (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 { disc: validated, errors } -} - export function validateDisc( obj: unknown = {}, allowZeroSub = false, @@ -449,8 +307,9 @@ export function validateDisc( mainStatKey = discSlotToMainStatKeys[slotKey][0] if (!allDiscRarityKeys.includes(rarity)) rarity = 'S' + if (typeof level !== 'number') level = 0 level = Math.round(level) - if (level > discMaxLevel[rarity]) return undefined + if (level > discMaxLevel[rarity]) level = 0 substats = parseSubstats(substats, rarity, allowZeroSub, sortSubs) // substat cannot have same key as mainstat diff --git a/libs/zzz/db/src/Interfaces/IDisc.ts b/libs/zzz/db/src/Interfaces/IDisc.ts index b88be661cf..538fe28f73 100644 --- a/libs/zzz/db/src/Interfaces/IDisc.ts +++ b/libs/zzz/db/src/Interfaces/IDisc.ts @@ -8,7 +8,7 @@ import type { export interface ISubstat { key: DiscSubStatKey - value: number + value: number // TODO: should this be the # of rolls? } export interface IDisc { setKey: DiscSetKey diff --git a/libs/zzz/page-discs/src/index.tsx b/libs/zzz/page-discs/src/index.tsx index ecd6efed54..556bf61d28 100644 --- a/libs/zzz/page-discs/src/index.tsx +++ b/libs/zzz/page-discs/src/index.tsx @@ -1,22 +1,34 @@ +import { useBoolState } from '@genshin-optimizer/common/react-util' +import type { ICachedDisc } from '@genshin-optimizer/zzz/db' +import { useDatabaseContext } from '@genshin-optimizer/zzz/db-ui' import { DiscEditor, DiscInventory } from '@genshin-optimizer/zzz/ui' import { Box } from '@mui/material' -import { useState } from 'react' +import { useCallback, useState } from 'react' export default function PageDiscs() { - const [discIdToEdit, setDiscIdToEdit] = useState('') + const [disc, setDisc] = useState>({}) + const [show, onOpen, onClose] = useBoolState() + const { database } = useDatabaseContext() + const onAddNew = useCallback(() => { + setDisc({}) + onOpen() + }, [onOpen]) + const onEdit = useCallback( + (id: string) => { + const disc = database.discs.get(id) + if (disc) { + setDisc(disc) + onOpen() + } + }, + [database.discs, onOpen] + ) return ( - setDiscIdToEdit('')} - allowEmpty - /> - setDiscIdToEdit('new')} - onEdit={setDiscIdToEdit} - /> + + ) } diff --git a/libs/zzz/ui/src/Disc/DiscEditor/DiscEditor.tsx b/libs/zzz/ui/src/Disc/DiscEditor/DiscEditor.tsx index 06487d4139..b979e41244 100644 --- a/libs/zzz/ui/src/Disc/DiscEditor/DiscEditor.tsx +++ b/libs/zzz/ui/src/Disc/DiscEditor/DiscEditor.tsx @@ -1,28 +1,26 @@ -import { useForceUpdate } from '@genshin-optimizer/common/react-util' import { CardThemed, DropdownButton, ModalWrapper, } from '@genshin-optimizer/common/ui' import { - clamp, - deepClone, + range, statKeyToFixed, toPercent, } from '@genshin-optimizer/common/util' -import type { - DiscRarityKey, - DiscSetKey, - DiscSlotKey, -} from '@genshin-optimizer/zzz/consts' +import type { DiscSetKey, DiscSlotKey } from '@genshin-optimizer/zzz/consts' import { allDiscSlotKeys, discMaxLevel, - discSlotToMainStatKeys, getDiscMainStatVal, } from '@genshin-optimizer/zzz/consts' -import type { ICachedDisc, IDisc, ISubstat } from '@genshin-optimizer/zzz/db' -import { cachedDisc } from '@genshin-optimizer/zzz/db' +import type { IDisc } from '@genshin-optimizer/zzz/db' +import { + validateDisc, + type ICachedDisc, + type ICachedSubstat, + type ISubstat, +} from '@genshin-optimizer/zzz/db' import { useDatabaseContext } from '@genshin-optimizer/zzz/db-ui' import AddIcon from '@mui/icons-material/Add' import ChevronRightIcon from '@mui/icons-material/ChevronRight' @@ -33,7 +31,6 @@ import LockOpenIcon from '@mui/icons-material/LockOpen' import ReplayIcon from '@mui/icons-material/Replay' import UpdateIcon from '@mui/icons-material/Update' import { - Alert, Box, Button, ButtonGroup, @@ -49,83 +46,57 @@ import { useTheme, } from '@mui/material' import type { MouseEvent } from 'react' -import { - Suspense, - useCallback, - useEffect, - useMemo, - useReducer, - useState, -} from 'react' +import { Suspense, useCallback, useEffect, useMemo, useReducer } from 'react' import { useTranslation } from 'react-i18next' import { DiscCard } from '../DiscCard' import { DiscMainStatDropdown } from '../DiscMainStatDropdown' import { DiscRarityDropdown } from '../DiscRarityDropdown' import { DiscSetAutocomplete } from '../DiscSetAutocomplete' -import { discReducer } from './reducer' import SubstatInput from './SubstatInput' - -// TODO: temporary until disc sheet is implemented -interface IDiscSheet { - rarity: readonly DiscRarityKey[] - // setEffects: Partial> - setEffects: any +interface DiscReducerState { + disc: Partial + validatedDisc?: IDisc } -const tempDiscSheet: IDiscSheet = { - rarity: ['S', 'A', 'B'], - setEffects: {}, +function reducer(state: DiscReducerState, action: Partial) { + const disc = { ...state.disc, ...action } + const validatedDisc = validateDisc(disc) + + return { + // Combine because validatedDisc:IDisc is missing the `id` field in ICachedDisc + disc: { ...disc, ...(validatedDisc || {}) } as Partial, + validatedDisc, + } } +function useDiscValidation(discFromProp: Partial) { + const [{ disc, validatedDisc }, setDisc] = useReducer(reducer, { + disc: discFromProp, + validatedDisc: undefined, + }) + useEffect(() => setDisc(discFromProp), [discFromProp]) -// TODO: disc sheets, errors, autocomplete, display text, i18n, ... -export type DiscEditorProps = { - discIdToEdit?: string - cancelEdit: () => void - allowEmpty?: boolean - disableSet?: boolean - fixedSlotKey?: DiscSlotKey + return { disc, validatedDisc, setDisc } } + export function DiscEditor({ - discIdToEdit = 'new', - cancelEdit, + disc: discFromProp, + show, + onClose, fixedSlotKey, allowEmpty = false, disableSet = false, -}: DiscEditorProps) { +}: { + disc: Partial + show: boolean + onClose: () => void + allowEmpty?: boolean + disableSet?: boolean + fixedSlotKey?: DiscSlotKey +}) { const { t } = useTranslation('disc') const { t: tk } = useTranslation(['discs_gen', 'statKey_gen']) const { database } = useDatabaseContext() - const [dirtyDatabase, setDirtyDatabase] = useForceUpdate() - useEffect( - () => database.discs.followAny(setDirtyDatabase), - [database, setDirtyDatabase] - ) - - const [showEditor, setShowEditor] = useState(false) - - useEffect(() => { - if (discIdToEdit === 'new') { - setShowEditor(true) - dispatchDisc({ type: 'reset' }) - } - const dbDisc = - discIdToEdit && dirtyDatabase && database.discs.get(discIdToEdit) - if (dbDisc) { - setShowEditor(true) - dispatchDisc({ - type: 'overwrite', - disc: deepClone(dbDisc), - }) - } - }, [discIdToEdit, database, dirtyDatabase]) - - const [disc, dispatchDisc] = useReducer(discReducer, undefined) - const { disc: cDisc, errors } = useMemo(() => { - if (!disc) return { disc: undefined, errors: [] } - const validated = cachedDisc(disc, discIdToEdit) - return validated - }, [disc, discIdToEdit]) - + const { disc, validatedDisc, setDisc } = useDiscValidation(discFromProp) const { prev, prevEditType, @@ -133,113 +104,71 @@ export function DiscEditor({ prev: ICachedDisc | undefined prevEditType: 'edit' | 'duplicate' | 'upgrade' | '' } = useMemo(() => { - const dbDisc = - dirtyDatabase && discIdToEdit && database.discs.get(discIdToEdit) + if (!disc) return { prev: undefined, prevEditType: '' } + const dbDisc = disc?.id && database.discs.get(disc?.id) if (dbDisc) return { prev: dbDisc, prevEditType: 'edit' } if (disc === undefined) return { prev: undefined, prevEditType: '' } - const { duplicated, upgraded } = - dirtyDatabase && database.discs.findDups(disc) + const { duplicated, upgraded } = database.discs.findDups( + disc as ICachedDisc + ) return { prev: duplicated[0] ?? upgraded[0], prevEditType: duplicated.length !== 0 ? 'duplicate' : 'upgrade', } - }, [disc, discIdToEdit, database, dirtyDatabase]) + }, [disc, database]) const disableEditSlot = - (!['new', ''].includes(discIdToEdit) && !!disc?.location) || // Disable slot for equipped disc - !!fixedSlotKey || // Disable slot if its fixed - // Disable editing slot of existing discs - // TODO: disable slot only for discs that are in a build? - (!!discIdToEdit && discIdToEdit !== 'new') + (!disc.id && !!disc?.location) || // Disable slot for equipped disc + !!fixedSlotKey // Disable slot if its fixed const { rarity = 'S', level = 0 } = disc ?? {} const slotKey = useMemo(() => { return disc?.slotKey ?? fixedSlotKey ?? '1' }, [fixedSlotKey, disc]) - const sheet: IDiscSheet | undefined = disc ? tempDiscSheet : undefined - - const update = useCallback( - (newValue: Partial) => { - // const newSheet = newValue.setKey ? getArtSheet(newValue.setKey) : sheet! - const newSheet = newValue.setKey ? tempDiscSheet : sheet! - - function pick( - value: T | undefined, - available: readonly T[], - prefer?: T - ): T { - return value && available.includes(value) - ? value - : prefer ?? available[0] - } - - if (newValue.setKey) - newValue.rarity = pick(disc?.rarity, newSheet.rarity, 'S') - - if (newValue.rarity) newValue.level = disc?.level ?? 0 - if (newValue.level) - newValue.level = clamp( - newValue.level, - 0, - discMaxLevel[newValue.rarity ?? disc!.rarity] - ) - if (newValue.slotKey) - newValue.mainStatKey = pick( - disc?.mainStatKey, - discSlotToMainStatKeys[newValue.slotKey] - ) - - if (newValue.mainStatKey) - newValue.substats = (disc?.substats ?? []).filter( - ({ key }) => key !== newValue.mainStatKey - ) - - dispatchDisc({ type: 'update', disc: newValue }) - }, - [disc, sheet, dispatchDisc] - ) const reset = useCallback(() => { - cancelEdit?.() - dispatchDisc({ type: 'reset' }) - }, [cancelEdit, dispatchDisc]) + setDisc({}) + if (!allowEmpty) onClose() + }, [allowEmpty, onClose, setDisc]) const setSubstat = useCallback( - (index: number, substat?: ISubstat) => - dispatchDisc({ type: 'substat', index, substat }), - [] + (index: number, substat?: ISubstat) => { + const substats = [...(disc.substats || [])] + if (substat) substats[index] = substat as ICachedSubstat + else substats.filter((_, i) => i !== index) + setDisc({ substats }) + }, + [disc, setDisc] ) - const isValid = !errors.length - const onClose = useCallback( + const onCloseModal = useCallback( (e: MouseEvent) => { if ( - !discIdToEdit && - disc && + (disc.id || Object.keys(disc).length > 0) && !window.confirm(t('editor.clearPrompt') as string) ) { e?.preventDefault() return } - setShowEditor(false) + onClose() reset() }, - [t, discIdToEdit, disc, setShowEditor, reset] + [t, disc, onClose, reset] ) const theme = useTheme() const grmd = useMediaQuery(theme.breakpoints.up('md')) - const removeId = (discIdToEdit !== 'new' && discIdToEdit) || prev?.id + const removeId = disc?.id || prev?.id const canClearDisc = (): boolean => window.confirm(t('editor.clearPrompt') as string) return ( - + + } @@ -257,16 +186,15 @@ export function DiscEditor({ size="small" discSetKey={disc?.setKey ?? ''} setDiscSetKey={(key) => - update({ setKey: key as DiscSetKey }) + setDisc({ setKey: key as DiscSetKey }) } sx={{ flexGrow: 1 }} label={disc?.setKey ? '' : t('editor.unknownSetName')} /> update({ rarity })} - filter={(r) => !!sheet?.rarity?.includes?.(r)} - disabled={!sheet} + onRarityChange={(rarity) => setDisc({ rarity })} + disabled={!disc.mainStatKey} /> @@ -279,35 +207,35 @@ export function DiscEditor({ margin="dense" size="small" value={level} - disabled={!sheet} + disabled={!disc.rarity} onChange={(e) => { const value = parseInt(e.target.value) || 0 - update({ level: value }) + setDisc({ level: value }) }} /> {rarity - ? [...Array(rarity + 1).keys()] + ? range(0, discMaxLevel[rarity] / 3) .map((i) => 3 * i) .map((i) => ( )) : null} @@ -322,9 +250,9 @@ export function DiscEditor({ // // ) : undefined // } - title={disc ? tk(disc.slotKey) : t('slot')} + title={disc?.slotKey ? tk(disc.slotKey) : t('slot')} value={slotKey} - disabled={disableEditSlot || !sheet} + disabled={disableEditSlot} color={disc ? 'success' : 'primary'} > {allDiscSlotKeys.map((sk) => ( @@ -332,7 +260,7 @@ export function DiscEditor({ key={sk} selected={slotKey === sk} disabled={slotKey === sk} - onClick={() => update({ slotKey: sk })} + onClick={() => setDisc({ slotKey: sk })} > {/* @@ -355,16 +283,15 @@ export function DiscEditor({ update({ mainStatKey })} + setStatKey={(mainStatKey) => setDisc({ mainStatKey })} defText={t('mainStat')} dropdownButtonProps={{ - disabled: !sheet, color: disc ? 'success' : 'primary', }} /> - {disc + {disc?.mainStatKey ? toPercent( getDiscMainStatVal(rarity, disc.mainStatKey, level), disc.mainStatKey @@ -373,7 +300,7 @@ export function DiscEditor({