diff --git a/apps/frontend/src/app/Components/AddArtInfo.tsx b/apps/frontend/src/app/Components/AddArtInfo.tsx new file mode 100644 index 0000000000..7d154eccb0 --- /dev/null +++ b/apps/frontend/src/app/Components/AddArtInfo.tsx @@ -0,0 +1,14 @@ +import { Alert, Link } from '@mui/material' +import { Link as RouterLink } from 'react-router-dom' + +export default function AddArtInfo() { + return ( + + Looks like you haven't added any artifacts yet. If you want, there are + + automatic scanners + + that can speed up the import process! + + ) +} diff --git a/apps/frontend/src/app/Components/NoArtWarning.tsx b/apps/frontend/src/app/Components/NoArtWarning.tsx new file mode 100644 index 0000000000..b990749fe9 --- /dev/null +++ b/apps/frontend/src/app/Components/NoArtWarning.tsx @@ -0,0 +1,19 @@ +import { Alert, Link } from '@mui/material' +import { Trans, useTranslation } from 'react-i18next' +import { Link as RouterLink } from 'react-router-dom' + +export default function NoArtWarning() { + const { t } = useTranslation('page_character_optimize') + return ( + + + Oops! It looks like you haven't added any artifacts to GO yet! You + should go to the + + Artifacts + + page and add some! + + + ) +} diff --git a/apps/frontend/src/app/PageArtifact/index.tsx b/apps/frontend/src/app/PageArtifact/index.tsx index 40779cd15d..a945780a6a 100644 --- a/apps/frontend/src/app/PageArtifact/index.tsx +++ b/apps/frontend/src/app/PageArtifact/index.tsx @@ -8,12 +8,10 @@ import { clamp, filterFunction, sortFunction } from '@genshin-optimizer/util' import { Add } from '@mui/icons-material' import DifferenceIcon from '@mui/icons-material/Difference' import { - Alert, Box, Button, CardContent, Grid, - Link, Pagination, Skeleton, Typography, @@ -30,7 +28,7 @@ import React, { } from 'react' import ReactGA from 'react-ga4' import { Trans, useTranslation } from 'react-i18next' -import { Link as RouterLink } from 'react-router-dom' +import AddArtInfo from '../Components/AddArtInfo' import SubstatToggle from '../Components/Artifact/SubstatToggle' import BootstrapTooltip from '../Components/BootstrapTooltip' import CardDark from '../Components/Card/CardDark' @@ -197,15 +195,7 @@ export default function PageArtifact() { - {noArtifact && ( - - Looks like you haven't added any artifacts yet. If you want, there are - - automatic scanners - - that can speed up the import process! - - )} + {noArtifact && } `#${index + 1}`, []) return ( - {noArtifact && ( - - - Oops! It looks like you haven't added any artifacts to GO yet! You - should go to the - - Artifacts - - page and add some! - - - )} + {noArtifact && } {/* Build Generator Editor */} {dataContext && ( diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx new file mode 100644 index 0000000000..aa5e62767f --- /dev/null +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/UpgradeOptChartCard.tsx @@ -0,0 +1,279 @@ +import type { ArtifactSlotKey } from '@genshin-optimizer/consts' +import { useTimeout } from '@genshin-optimizer/react-util' +import { linspace } from '@genshin-optimizer/util' +import { Box, CardContent, Grid, Typography } from '@mui/material' +import { useCallback, useContext, useMemo } from 'react' +import type { TooltipProps } from 'recharts' +import { + Area, + ComposedChart, + Label, + Legend, + Line, + ReferenceDot, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' +import ArtifactCardPico from '../../../../Components/Artifact/ArtifactCardPico' +import CardLight from '../../../../Components/Card/CardLight' +import { DataContext } from '../../../../Context/DataContext' +import { DatabaseContext } from '../../../../Database/Database' +import { uiInput as input } from '../../../../Formula' +import ArtifactCard from '../../../../PageArtifact/ArtifactCard' +import { erf } from './mathUtil' +import type { UpOptArtifact } from './upOpt' +import { ResultType } from './upOpt' + +type Props = { + upgradeOpt: UpOptArtifact + showTrue?: boolean + objMin: number + objMax: number + thresholds: number[] + ix?: number + calcExactCallback: () => void +} +type ChartData = { + x: number + est: number + estCons: number +} + +const nbins = 50 + +export default function UpgradeOptChartCard(props: Props) { + return ( + + + + + + + + + ) +} + +function UpgradeOptChartCardGraph({ + upgradeOpt, + thresholds, + objMin, + objMax, + calcExactCallback, +}: Props) { + const { database } = useContext(DatabaseContext) + const bla = database.arts.get(upgradeOpt.id) + if (!bla) { + throw new Error(`artifact ${upgradeOpt.id} not found.`) + } + + const constrained = thresholds.length > 1 + + // Returns P(a < DMG < b) + const integral = (a: number, b: number) => + upgradeOpt.result!.distr.gmm.reduce((pv, { phi, mu, sig2 }) => { + const sig = Math.sqrt(sig2) + if (sig < 1e-3) return a <= mu && mu < b ? phi + pv : pv + const P = erf((mu - a) / sig) - erf((mu - b) / sig) + return pv + (phi * P) / 2 + }, 0) + const integralCons = (a: number, b: number) => + upgradeOpt.result!.distr.gmm.reduce((pv, { cp, phi, mu, sig2 }) => { + const sig = Math.sqrt(sig2) + if (sig < 1e-3) return a <= mu && mu < b ? cp * phi + pv : pv + const P = erf((mu - a) / sig) - erf((mu - b) / sig) + return pv + (cp * phi * P) / 2 + }, 0) + const thr0 = thresholds[0] + const perc = useCallback((x: number) => (100 * (x - thr0)) / thr0, [thr0]) + + const step = (objMax - objMin) / nbins + const dataHist: ChartData[] = linspace(objMin, objMax, nbins, false).flatMap( + (v) => { + return [ + { + x: perc(v), + est: integral(v, v + step), + estCons: integralCons(v, v + step), + }, + { + x: perc(v + step), + est: integral(v, v + step), + estCons: integralCons(v, v + step), + }, + ] + } + ) + dataHist.unshift({ x: perc(objMin), est: 0, estCons: 0 }) + dataHist.push({ x: perc(objMax), est: 0, estCons: 0 }) + + const ymax = dataHist.reduce((max, { est }) => Math.max(max, est!), 0) + const xpercent = (thr0 - objMin) / (objMax - objMin) + + // if trueP/E have been calculated, otherwise use upgradeOpt's estimate + const reportP = upgradeOpt.result!.p + const reportD = upgradeOpt.result!.upAvg + const chartData = dataHist + const isExact = upgradeOpt.result!.evalMode === ResultType.Exact + + const reportBin = linspace(objMin, objMax, nbins, false).reduce((a, b) => + b < thr0 + reportD ? b : a + ) + const reportY = integralCons(reportBin, reportBin + step) + const timeoutFunc = useTimeout() + if (!isExact) timeoutFunc(calcExactCallback, 1000) // lazy load the exact calculation + + const probUpgradeText = ( + + Prob. upgrade{isExact ? '' : ' (est.)'}:{' '} + {(100 * reportP).toFixed(1)}% + + ) + const avgIncText = ( + + Average increase{isExact ? '' : ' (est.)'}:{' '} + + {reportD <= 0 ? '' : '+'} + {((100 * reportD) / thr0).toFixed(1)}% + + + ) + const CustomTooltip = ({ active }: TooltipProps) => { + if (!active) return null + // I kinda want the [average increase] to only appear when hovering the white dot. + return ( +
+

+

{probUpgradeText}

+

{avgIncText}

+
+ ) + } + + return ( + + + + + {probUpgradeText} + {avgIncText} + Currently Equipped + + + + + + + + + + + `${v <= 0 ? '' : '+'}${v}%`} + > + + + + + + + + + + + + + + {constrained && ( + + )} + + + + } + /> + + } cursor={false} /> + + + + + ) +} + +function EquippedArtifact({ slotKey }: { slotKey: ArtifactSlotKey }) { + const { database } = useContext(DatabaseContext) + const { data } = useContext(DataContext) + const artifact = useMemo( + () => database.arts.get(data.get(input.art[slotKey].id).value), + [slotKey, data, database] + ) + return +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx new file mode 100644 index 0000000000..eed54670d9 --- /dev/null +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/Tabs/TabUpgradeOpt/index.tsx @@ -0,0 +1,586 @@ +import { CheckBox, CheckBoxOutlineBlank, Upgrade } from '@mui/icons-material' +import { + Box, + Button, + CardContent, + Grid, + Pagination, + Skeleton, + Typography, +} from '@mui/material' +import CardLight from '../../../../Components/Card/CardLight' +import CharacterCard from '../../../../Components/Character/CharacterCard' +import { + HitModeToggle, + ReactionToggle, +} from '../../../../Components/HitModeEditor' +import useDBMeta from '../../../../ReactHooks/useDBMeta' +import ArtifactSetConfig from '../TabOptimize/Components/ArtifactSetConfig' +import BonusStatsCard from '../TabOptimize/Components/BonusStatsCard' +import OptimizationTargetSelector from '../TabOptimize/Components/OptimizationTargetSelector' +import StatFilterCard from '../TabOptimize/Components/StatFilterCard' + +import type { ArtifactSlotKey, CharacterKey } from '@genshin-optimizer/consts' +import { + allArtifactSlotKeys, + charKeyToLocCharKey, +} from '@genshin-optimizer/consts' +import { useForceUpdate } from '@genshin-optimizer/react-util' +import { clamp } from '@genshin-optimizer/util' +import { + Suspense, + useCallback, + useContext, + useDeferredValue, + useEffect, + useMemo, + useState, +} from 'react' +import { Trans } from 'react-i18next' +import ArtifactLevelSlider from '../../../../Components/Artifact/ArtifactLevelSlider' +import { CharacterContext } from '../../../../Context/CharacterContext' +import type { dataContextObj } from '../../../../Context/DataContext' +import { DataContext } from '../../../../Context/DataContext' +import { DatabaseContext } from '../../../../Database/Database' +import { mergeData, uiDataForTeam } from '../../../../Formula/api' +import { optimize } from '../../../../Formula/optimization' +import type { NumNode } from '../../../../Formula/type' +import useCharSelectionCallback from '../../../../ReactHooks/useCharSelectionCallback' +import useTeamData, { getTeamData } from '../../../../ReactHooks/useTeamData' +import type { DynStat } from '../../../../Solver/common' +import type { ICachedArtifact } from '../../../../Types/artifact' +import { objPathValue, shouldShowDevComponents } from '../../../../Util/Util' +import MainStatSelectionCard from '../TabOptimize/Components/MainStatSelectionCard' +import { dynamicData } from '../TabOptimize/foreground' +import useBuildSetting from '../TabOptimize/useBuildSetting' +import UpgradeOptChartCard from './UpgradeOptChartCard' + +import { Stack } from '@mui/system' +import AddArtInfo from '../../../../Components/AddArtInfo' +import NoArtWarning from '../../../../Components/NoArtWarning' +import type { UpOptBuild } from './upOpt' +import { toArtifact, UpOptCalculator } from './upOpt' + +export default function TabUpopt() { + const { + character: { key: characterKey }, + } = useContext(CharacterContext) + const { database } = useContext(DatabaseContext) + const { gender } = useDBMeta() + + const onClickTeammate = useCharSelectionCallback() + + const noArtifact = useMemo(() => !database.arts.values.length, [database]) + + const { buildSetting, buildSettingDispatch } = useBuildSetting(characterKey) + const { optimizationTarget, levelLow, levelHigh } = buildSetting + const teamData = useTeamData(characterKey) + const { target: data } = teamData?.[characterKey as CharacterKey] ?? {} + + const [artsDirty] = useForceUpdate() + // const [{ equipmentPriority, threads = defThreads }, setDisplayOptimize] = useState(database.displayOptimize.get()) + const [, setDisplayOptimize] = useState(database.displayOptimize.get()) + useEffect( + () => database.displayOptimize.follow((_r, to) => setDisplayOptimize(to)), + [database, setDisplayOptimize] + ) + const deferredArtsDirty = useDeferredValue(artsDirty) + const deferredBuildSetting = useDeferredValue(buildSetting) + const filteredArts = useMemo(() => { + const { + mainStatKeys, + excludedLocations, + artExclusion, + levelLow, + levelHigh, + allowLocationsState, + useExcludedArts, + } = deferredArtsDirty && deferredBuildSetting + + return database.arts.values.filter((art) => { + if (!useExcludedArts && artExclusion.includes(art.id)) return false + if (art.level < levelLow) return false + if (art.level > levelHigh) return false + const mainStats = mainStatKeys[art.slotKey] + if (mainStats?.length && !mainStats.includes(art.mainStatKey)) + return false + + const locKey = charKeyToLocCharKey(characterKey) + const unequippedStateAndEquippedElsewhere = + allowLocationsState === 'unequippedOnly' && + art.location && + art.location !== locKey + const customListStateAndNotOnList = + allowLocationsState === 'customList' && + art.location && + art.location !== locKey && + excludedLocations.includes(art.location) + if (unequippedStateAndEquippedElsewhere || customListStateAndNotOnList) + return false + + return true + }) + }, [database, characterKey, deferredArtsDirty, deferredBuildSetting]) + const filteredArtIdMap = useMemo( + () => Object.fromEntries(filteredArts.map(({ id }) => [id, true])), + [filteredArts] + ) + + const [upOptCalc, setUpOptCalc] = useState( + undefined as UpOptCalculator | undefined + ) + + const [, setForceUpdate] = useForceUpdate() + + const [show20, setShow20] = useState(true) + const [check4th, setCheck4th] = useState(true) + const [useFilters, setUseMainStatFilter] = useState(false) + + // Paging logic + const [pageIdex, setpageIdex] = useState(0) + + const artifactsToDisplayPerPage = 5 + const { artifactsToShow, numPages, currentPageIndex, minObj0, maxObj0 } = + useMemo(() => { + if (!upOptCalc) + return { + artifactsToShow: [], + numPages: 0, + currentPageIndex: 0, + toShow: 0, + minObj0: 0, + maxObj0: 0, + } + const numPages = Math.ceil( + upOptCalc.artifacts.length / artifactsToDisplayPerPage + ) + const currentPageIndex = clamp(pageIdex, 0, numPages - 1) + const toShow = upOptCalc.artifacts.slice( + currentPageIndex * artifactsToDisplayPerPage, + (currentPageIndex + 1) * artifactsToDisplayPerPage + ) + const thr = upOptCalc.thresholds[0] + + return { + artifactsToShow: toShow, + numPages, + currentPageIndex, + minObj0: toShow.reduce( + (a, b) => Math.min(b.result!.distr.lower, a), + thr + ), + maxObj0: toShow.reduce( + (a, b) => Math.max(b.result!.distr.upper, a), + thr + ), + } + }, [pageIdex, upOptCalc]) + + const setPage = useCallback( + (e, value) => { + if (!upOptCalc) return + const end = value * artifactsToDisplayPerPage + upOptCalc.calcSlowToIndex(end) + setpageIdex(value - 1) + }, + [upOptCalc] + ) + + const generateBuilds = useCallback(async () => { + const { + statFilters, + optimizationTarget, + mainStatKeys, + levelLow, + levelHigh, + artSetExclusion, + } = buildSetting + + if (!shouldShowDevComponents) return + if (!characterKey || !optimizationTarget) return + const teamData = getTeamData(database, characterKey, 0, []) + if (!teamData) return + const workerData = uiDataForTeam(teamData.teamData, gender, characterKey)[ + characterKey + ]?.target.data![0] + if (!workerData) return + Object.assign(workerData, mergeData([workerData, dynamicData])) // Mark art fields as dynamic + const optimizationTargetNode = objPathValue( + workerData.display ?? {}, + optimizationTarget + ) as NumNode | undefined + if (!optimizationTargetNode) return + setUpOptCalc(undefined) + setpageIdex(0) + + const valueFilter: { value: NumNode; minimum: number }[] = Object.entries( + statFilters + ).flatMap(([pathStr, settings]) => + settings + .filter((setting) => !setting.disabled) + .map((setting) => { + const filterNode: NumNode = objPathValue( + workerData.display ?? {}, + JSON.parse(pathStr) + ) + const minimum = + filterNode.info?.unit === '%' ? setting.value / 100 : setting.value + return { value: filterNode, minimum } + }) + ) + + const equippedArts = + database.chars.get(characterKey)?.equippedArtifacts ?? + ({} as StrictDict) + const curEquip: UpOptBuild = Object.fromEntries( + allArtifactSlotKeys.map((slotKey) => { + const art = database.arts.get(equippedArts[slotKey] ?? '') + return [slotKey, art ? toArtifact(art) : undefined] + }) + ) as UpOptBuild + const curEquipSetKeys = Object.fromEntries( + allArtifactSlotKeys.map((slotKey) => { + const art = database.arts.get(equippedArts[slotKey] ?? '') + return [slotKey, art?.setKey ?? ''] + }) + ) + function respectSexExclusion(art: ICachedArtifact) { + const newSK = { ...curEquipSetKeys } + newSK[art.slotKey] = art.setKey + const skc: DynStat = {} + allArtifactSlotKeys.forEach( + (slotKey) => (skc[newSK[slotKey]] = (skc[newSK[slotKey]] ?? 0) + 1) + ) + const pass = Object.entries(skc).every(([setKey, num]) => { + if (!artSetExclusion[setKey]) return true + switch (num) { + case 0: + case 1: + return true + case 2: + case 3: + return !artSetExclusion[setKey].includes(2) + case 4: + case 5: + return !artSetExclusion[setKey].includes(4) + default: + throw Error('error in respectSetExclude: num > 5') + } + }) + if (!pass) return false + + if (!artSetExclusion['rainbow']) return true + const nRainbow = Object.values(skc).reduce((a, b) => a + (b % 2), 0) + switch (nRainbow) { + case 0: + case 1: + return true + case 2: + case 3: + return !artSetExclusion['rainbow'].includes(2) + case 4: + case 5: + return !artSetExclusion['rainbow'].includes(4) + default: + throw Error('error in respectSex: nRainbow > 5') + } + } + + const nodesPreOpt = [ + optimizationTargetNode, + ...valueFilter.map((x) => x.value), + ] + const nodes = optimize( + nodesPreOpt, + workerData, + ({ path: [p] }) => p !== 'dyn' + ) + const artifactsToConsider = database.arts.values + .filter((art) => art.rarity === 5) + .filter(respectSexExclusion) + .filter((art) => show20 || art.level !== 20) + .filter( + (art) => + !useFilters || + !mainStatKeys[art.slotKey]?.length || + mainStatKeys[art.slotKey]?.includes(art.mainStatKey) + ) + .filter( + (art) => + !useFilters || (levelLow <= art.level && art.level <= levelHigh) + ) + + const upoptCalc = new UpOptCalculator( + nodes, + [-Infinity, ...valueFilter.map((x) => x.minimum)], + curEquip, + artifactsToConsider + ) + upoptCalc.calc4th = check4th + upoptCalc.calcFastAll() + upoptCalc.calcSlowToIndex(5) + setUpOptCalc(upoptCalc) + }, [ + buildSetting, + characterKey, + database, + gender, + show20, + useFilters, + check4th, + ]) + + const dataContext: dataContextObj | undefined = useMemo(() => { + return data && teamData && { data, teamData } + }, [data, teamData]) + + const pagination = numPages > 1 && ( + + + + + + + + + + + + + ) + + return ( + + {noArtifact && } + {/* Build Generator Editor */} + {dataContext && ( + + + + {/* 1*/} + + {/* character card */} + + + + + + + {/* 2 */} + + + + + + Optimization Target: + { + + buildSettingDispatch({ + optimizationTarget: target, + }) + } + disabled={false} + /> + } + + + + + + + + {useFilters && ( + + + Artifact Level Filter + + + buildSettingDispatch({ levelLow }) + } + setHigh={(levelHigh) => + buildSettingDispatch({ levelHigh }) + } + setBoth={(levelLow, levelHigh) => + buildSettingDispatch({ levelLow, levelHigh }) + } + disabled={false} + /> + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {pagination} + {noArtifact && } + + } + > + {artifactsToShow.map((art, i) => ( + + { + const ix = + currentPageIndex * artifactsToDisplayPerPage + i + upOptCalc?.calcExact(ix) + setForceUpdate() + }} + /> + + ))} + + {pagination} + + + )} + + ) +} + +function ShowingArt({ numShowing, total }) { + return ( + + + Showing {{ count: numShowing }} out of {{ value: total }} Artifacts + + + ) +} diff --git a/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx b/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx index 5b24247271..24bc705b48 100644 --- a/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx +++ b/apps/frontend/src/app/PageCharacter/CharacterDisplay/index.tsx @@ -62,12 +62,14 @@ import CharSelectButton from './CharSelectButton' import FormulaModal from './FormulaModal' import StatModal from './StatModal' import TabBuild from './Tabs/TabOptimize' +import TabUpopt from './Tabs/TabUpgradeOpt' import TabOverview from './Tabs/TabOverview' import TabTalent from './Tabs/TabTalent' import TabTeambuffs from './Tabs/TabTeambuffs' import TabTheorycraft from './Tabs/TabTheorycraft' import TravelerElementSelect from './TravelerElementSelect' import TravelerGenderSelect from './TravelerGenderSelect' +import { shouldShowDevComponents } from '../../Util/Util' export default function CharacterDisplay() { const navigate = useNavigate() @@ -231,12 +233,18 @@ function CharacterPanel() { } /> } /> } /> + {shouldShowDevComponents && ( + } /> + )} ) } function TabNav({ tab }: { tab: string }) { const { t } = useTranslation('page_character') + const tabSx = shouldShowDevComponents + ? { minWidth: '16.6%' } + : { minWidth: '20%' } return ( } @@ -258,7 +266,7 @@ function TabNav({ tab }: { tab: string }) { to="" /> } @@ -266,7 +274,7 @@ function TabNav({ tab }: { tab: string }) { to="talent" /> } @@ -274,7 +282,7 @@ function TabNav({ tab }: { tab: string }) { to="teambuffs" /> } @@ -282,13 +290,23 @@ function TabNav({ tab }: { tab: string }) { to="optimize" /> } component={RouterLink} to="theorycraft" /> + {shouldShowDevComponents && ( + } + component={RouterLink} + to="upopt" + /> + )} ) } diff --git a/libs/util/src/lib/array.spec.ts b/libs/util/src/lib/array.spec.ts new file mode 100644 index 0000000000..bcde89b7b4 --- /dev/null +++ b/libs/util/src/lib/array.spec.ts @@ -0,0 +1,7 @@ +import { linspace } from './array' +describe('test @genshin_optimizer/util/array', function () { + it('test linspace', () => { + expect(linspace(1, 2, 3)).toEqual([1, 1.5, 2]) + expect(linspace(1, 5, 4, false)).toEqual([1, 2, 3, 4]) + }) +}) diff --git a/libs/util/src/lib/array.ts b/libs/util/src/lib/array.ts index d02d739a98..d762141fb1 100644 --- a/libs/util/src/lib/array.ts +++ b/libs/util/src/lib/array.ts @@ -46,3 +46,13 @@ export function arrayMove(arr: T[], oldIndex: number, newIndex: number) { export function transposeArray(arr: T[][]): T[][] { return arr[0].map((_, i) => arr.map((row) => row[i])) } + +export function linspace( + start: number, + stop: number, + num: number, + inclusiveEnd = true +) { + const step = (stop - start) / (inclusiveEnd ? num - 1 : num) + return range(0, num - 1).map((i) => start + step * i) +}