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}
+ />
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ :
+ }
+ color={show20 ? 'success' : 'secondary'}
+ onClick={() => setShow20(!show20)}
+ >
+ show lvl 20
+
+
+
+
+ ) : (
+
+ )
+ }
+ color={check4th ? 'success' : 'secondary'}
+ onClick={() => setCheck4th(!check4th)}
+ >
+ compute 4th sub
+
+
+
+
+ ) : (
+
+ )
+ }
+ color={useFilters ? 'success' : 'secondary'}
+ onClick={() => setUseMainStatFilter(!useFilters)}
+ >
+ enable filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ Calc Upgrade Priority
+
+
+
+
+
+
+
+
+
+
+
+ {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)
+}