From cd865a66b2274875a9d33edad7fa9f8ded8d8e09 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Sun, 15 Jan 2023 03:01:08 +0000 Subject: [PATCH] checkpoint: CLIENT BUILDS now db migration and we just start piling on the bug fixes --- .../dashboard/users/UserSettingsPage.tsx | 9 +- .../_game/_playtype/LeaderboardsPage.tsx | 31 +-- .../_game/_playtype/UGPTSettingsPage.tsx | 70 ------ .../_playtype/folders/FolderComparePage.tsx | 219 +++++++++--------- .../_playtype/folders/FolderSelectPage.tsx | 35 ++- .../_playtype/folders/FolderTablePage.tsx | 44 ++-- client/src/app/routes/GameRoutes.tsx | 42 ++-- .../src/components/charts/IIDXLampChart.tsx | 20 +- .../game/folder/FolderInfoHeader.tsx | 27 ++- .../folder/FolderScoreDistributionChart.tsx | 143 ++---------- .../sessions/SessionFolderRaiseBreakdown.tsx | 37 +-- .../src/components/tables/cells/BPICell.tsx | 10 +- .../src/components/tables/cells/DeltaCell.tsx | 23 +- .../tables/cells/DifficultyCell.tsx | 9 +- .../tables/cells/ITGDifficultyCell.tsx | 19 +- .../components/tables/cells/PBCompareCell.tsx | 73 +----- .../tables/cells/RatingSystemPart.tsx | 1 - .../src/components/tables/cells/ScoreCell.tsx | 12 +- .../src/components/tables/cells/TitleCell.tsx | 2 +- .../tables/cells/USCDifficultyCell.tsx | 10 +- .../components/BMSScoreDropdownParts.tsx | 40 ++-- .../components/IIDXScoreDropdownParts.tsx | 50 ++-- .../components/ITGScoreDropdownParts.tsx | 4 +- .../dropdowns/components/RivalCompare.tsx | 9 +- .../tables/game-core-cells/BMSCoreCells.tsx | 27 ++- .../game-core-cells/GitadoraCoreCells.tsx | 6 +- .../tables/game-core-cells/IIDXCoreCells.tsx | 23 +- .../tables/game-core-cells/ITGCoreCells.tsx | 6 +- .../game-core-cells/MaimaiDXCoreCells.tsx | 6 +- .../tables/game-core-cells/PMSCoreCells.tsx | 23 +- .../game-core-cells/ShortScoreCoreCells.tsx | 76 ------ .../tables/rivals/ComparePBsTable.tsx | 6 +- .../components/targets/SetNewGoalModal.tsx | 81 +++---- .../src/components/user/UGPTStatCreator.tsx | 83 +++---- client/src/lib/game-implementations.ts | 10 +- client/src/lib/games/bms-pms.ts | 139 ++++++++++- client/src/lib/games/gitadora.ts | 2 +- client/src/lib/games/sdvx-usc.ts | 63 ++++- client/src/util/grade-deltas.ts | 87 ------- client/src/util/misc.ts | 2 +- client/src/util/str-to-num.ts | 2 +- 41 files changed, 725 insertions(+), 856 deletions(-) delete mode 100644 client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx delete mode 100644 client/src/util/grade-deltas.ts diff --git a/client/src/app/pages/dashboard/users/UserSettingsPage.tsx b/client/src/app/pages/dashboard/users/UserSettingsPage.tsx index 9c2bef351..88e2e4855 100644 --- a/client/src/app/pages/dashboard/users/UserSettingsPage.tsx +++ b/client/src/app/pages/dashboard/users/UserSettingsPage.tsx @@ -9,16 +9,15 @@ import Muted from "components/util/Muted"; import SelectButton from "components/util/SelectButton"; import { UserSettingsContext } from "context/UserSettingsContext"; import { useFormik } from "formik"; -import { TachiConfig } from "lib/config"; import React, { useContext, useState } from "react"; import { Alert, Button, Form } from "react-bootstrap"; -import { UserDocument, UserSettings } from "tachi-common"; +import { UserDocument, UserSettingsDocument } from "tachi-common"; interface Props { reqUser: UserDocument; } -export default function UserSettingsPage({ reqUser }: Props) { +export default function UserSettingsDocumentPage({ reqUser }: Props) { useSetSubheader( ["Users", reqUser.username, "Settings"], [reqUser], @@ -82,7 +81,7 @@ function AccountSettings({ reqUser }: { reqUser: UserDocument }) { confPass: "", }, onSubmit: async (values) => { - const r = await APIFetchV1( + const r = await APIFetchV1( `/users/${reqUser.id}/change-password`, { method: "POST", @@ -174,7 +173,7 @@ function PreferencesForm({ reqUser }: { reqUser: UserDocument }) { deletableScores: settings?.preferences.deletableScores ?? false, }, onSubmit: async (values) => { - const res = await APIFetchV1( + const res = await APIFetchV1( `/users/${reqUser.id}/settings`, { method: "PATCH", diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx index d2e8eb228..bae6a86de 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx @@ -1,6 +1,6 @@ import { APIFetchV1, UnsuccessfulAPIFetchResponse } from "util/api"; import { ChangeOpacity } from "util/color-opacity"; -import { UppercaseFirst } from "util/misc"; +import { FormatGPTProfileRating, IsNotNullish, UppercaseFirst } from "util/misc"; import ClassBadge from "components/game/ClassBadge"; import useSetSubheader from "components/layout/header/useSetSubheader"; import Card from "components/layout/page/Card"; @@ -21,8 +21,8 @@ import { UserDocument, ProfileRatingAlgorithms, UserGameStats, + Classes, } from "tachi-common"; -import { GameClassSets } from "tachi-common/game-classes"; import { GPTLeaderboard, UGPTLeaderboardAdjacent } from "types/api-returns"; import { GamePT, SetState, UGPT } from "types/react"; @@ -129,24 +129,25 @@ function LeaderboardsPageContent({ - {s.ratings[alg] - ? gptConfig.profileRatingAlgFormatters[alg] - ? gptConfig.profileRatingAlgFormatters[alg]!(s.ratings[alg]!) - : s.ratings[alg]!.toFixed(2) + {IsNotNullish(s.ratings[alg]) + ? FormatGPTProfileRating(game, playtype, alg, s.ratings[alg]!) : "No Data."} {/* temp */} {Object.entries(s.classes).length - ? Object.entries(s.classes).map(([k, v]) => ( - - )) + ? Object.entries(s.classes).map( + ([k, v]) => + v && ( + + ) + ) : "No Classes"} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage.tsx index 3165ac51b..af2b130db 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage.tsx @@ -318,15 +318,6 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { Expected Profile VF6{" "} {((formik.values.gameSpecific.vf6Target ?? 0) * 50).toFixed(2)} -
- @@ -502,67 +493,6 @@ function RenderCurrentStats({ ); } -// Straight up stolen from tachi-server. -enum SDVXVFClasses { - SIENNA_I, - SIENNA_II, - SIENNA_III, - SIENNA_IV, - COBALT_I, - COBALT_II, - COBALT_III, - COBALT_IV, - DANDELION_I, - DANDELION_II, - DANDELION_III, - DANDELION_IV, - CYAN_I, - CYAN_II, - CYAN_III, - CYAN_IV, - SCARLET_I, - SCARLET_II, - SCARLET_III, - SCARLET_IV, - CORAL_I, - CORAL_II, - CORAL_III, - CORAL_IV, - ARGENTO_I, - ARGENTO_II, - ARGENTO_III, - ARGENTO_IV, - ELDORA_I, - ELDORA_II, - ELDORA_III, - ELDORA_IV, - CRIMSON_I, - CRIMSON_II, - CRIMSON_III, - CRIMSON_IV, - IMPERIAL_I, - IMPERIAL_II, - IMPERIAL_III, - IMPERIAL_IV, -} - -function ToVolforceClass(vf: number) { - if (vf >= 24) { - return SDVXVFClasses.IMPERIAL_IV; - } else if (vf >= 20) { - // imperial i -> iv has gaps of 1 - return SDVXVFClasses.IMPERIAL_I + Math.floor(vf - 20); - } else if (vf >= 14) { - // cyan i -> crimson iv has gaps of 0.25 - return SDVXVFClasses.CYAN_I + Math.floor(4 * (vf - 14)); - } else if (vf >= 10) { - // cobalt i -> dandelion iv have gaps of 0.5 - return SDVXVFClasses.COBALT_I + Math.floor(2 * (vf - 10)); - } - - return Math.floor(vf / 2.5); -} - function ManageAccount({ reqUser, game, playtype }: UGPT) { const [password, setPassword] = useState(""); const [deleting, setDeleting] = useState(false); diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx index 1983b2f16..c7207233a 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx @@ -178,11 +178,11 @@ function FolderCompare({ - + /> */} d.compare.scoreData.percent) { - scoreWin++; - } else if (d.base.scoreData.percent < d.compare.scoreData.percent) { - scoreLoss++; - } else { - scoreDraw++; - } +// if (!d.base) { +// scoreLoss++; +// lampLoss++; +// continue; +// } - if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) { - lampWin++; - } else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) { - lampLoss++; - } else { - lampDraw++; - } - } +// if (d.base.scoreData.percent > d.compare.scoreData.percent) { +// scoreWin++; +// } else if (d.base.scoreData.percent < d.compare.scoreData.percent) { +// scoreLoss++; +// } else { +// scoreDraw++; +// } - return ( - - - - - - - {baseUsername} Win +// if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) { +// lampWin++; +// } else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) { +// lampLoss++; +// } else { +// lampDraw++; +// } +// } - {scoreWin} +// return ( +// +// +// +// +// +// +// {baseUsername} Win - - {((100 * scoreWin) / dataset.length).toFixed(2)}% - - - - Draw - {scoreDraw} - - {((100 * scoreDraw) / dataset.length).toFixed(2)}% - - - - {otherUsername} Win - {scoreLoss} - - {((100 * scoreLoss) / dataset.length).toFixed(2)}% - - - - - - - - {baseUsername} Win +// {scoreWin} - {lampWin} +// +// {((100 * scoreWin) / dataset.length).toFixed(2)}% +// +// +// +// Draw +// {scoreDraw} +// +// {((100 * scoreDraw) / dataset.length).toFixed(2)}% +// +// +// +// {otherUsername} Win +// {scoreLoss} +// +// {((100 * scoreLoss) / dataset.length).toFixed(2)}% +// +// +// +// +// +// +// +// {baseUsername} Win - - {((100 * lampWin) / dataset.length).toFixed(2)}% - - - - Draw - {lampDraw} - - {((100 * lampDraw) / dataset.length).toFixed(2)}% - - - - {otherUsername} Win - {lampLoss} - - {((100 * lampLoss) / dataset.length).toFixed(2)}% - - - - - - - - ); -} +// {lampWin} + +// +// {((100 * lampWin) / dataset.length).toFixed(2)}% +// +// +// +// Draw +// {lampDraw} +// +// {((100 * lampDraw) / dataset.length).toFixed(2)}% +// +// +// +// {otherUsername} Win +// {lampLoss} +// +// {((100 * lampLoss) / dataset.length).toFixed(2)}% +// +// +// +// +// +// +// +// ); +// } diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx index 350fdcd6b..4249236fa 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx @@ -7,15 +7,17 @@ import Divider from "components/util/Divider"; import Icon from "components/util/Icon"; import LinkButton from "components/util/LinkButton"; import Loading from "components/util/Loading"; -import useApiQuery from "components/util/query/useApiQuery"; import SelectButton from "components/util/SelectButton"; +import useApiQuery from "components/util/query/useApiQuery"; import { useBucket } from "components/util/useBucket"; import useUGPTBase from "components/util/useUGPTBase"; import { UserContext } from "context/UserContext"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import React, { useContext, useMemo, useState } from "react"; -import { FolderDocument, GetGamePTConfig, UserDocument } from "tachi-common"; +import { FolderDocument, GetGPTString, GetGamePTConfig, GetScoreMetricConf } from "tachi-common"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; import { FolderStatsInfo, UGPTFolderSearch } from "types/api-returns"; -import { GamePT, UGPT } from "types/react"; +import { UGPT } from "types/react"; export default function FoldersSearch({ reqUser, game, playtype }: UGPT) { const [search, setSearch] = useState(""); @@ -82,34 +84,27 @@ export function FolderInfoComponent({ folder, }: UGPT & { folder: FolderDocument; folderStats: FolderStatsInfo }) { const gptConfig = GetGamePTConfig(game, playtype); + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)]; const preferredDefaultEnum = useBucket(game, playtype); - const [elements, setElements] = useState<"grade" | "lamp">(preferredDefaultEnum); + const [metric, setMetric] = useState(preferredDefaultEnum); const base = useUGPTBase({ reqUser, game, playtype }); const dataset = useMemo(() => { - if (elements === "grade") { - return ( - - ); - } + const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric; return ( ); - }, [elements]); + }, [metric]); const { user } = useContext(UserContext); @@ -139,11 +134,11 @@ export function FolderInfoComponent({
- + Grades - + Lamps diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx index aa994a10b..8e694f3e8 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx @@ -24,12 +24,16 @@ import { FolderDocument, Game, GamePTConfig, + GetGPTString, GetGamePTConfig, + GetScoreMetricConf, Playtype, TableDocument, UserDocument, } from "tachi-common"; import { FolderStatsInfo, UGPTTableReturns } from "types/api-returns"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; interface Props { reqUser: UserDocument; @@ -248,17 +252,14 @@ function TableBarChart({ dataMap: Map; }) { const gptConfig = GetGamePTConfig(table.game, table.playtype); + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(table.game, table.playtype)]; + const bucket = useBucket(table.game, table.playtype); - const [mode, setMode] = useState<"grades" | "lamps">(bucket === "grade" ? "grades" : "lamps"); + const [enumMetric, setEnumMetric] = useState(bucket); - const colours = useMemo(() => { - if (mode === "grades") { - return gptConfig.gradeColours; - } - - return gptConfig.lampColours; - }, [mode]); + // @ts-expect-error hack! + const colours = useMemo(() => gptImpl.enumColours[enumMetric], [enumMetric]); const dataset = useMemo(() => { const arr = []; @@ -275,11 +276,12 @@ function TableBarChart({ }; // this is a series of stupid hacks and i dont really care. - for (const key in data.stats[mode]) { + // @ts-expect-error hack! + for (const key in data.stats[enumMetric]) { // @ts-expect-error stupid key stuff - realData[key] = (100 * data.stats[mode][key]) / data.stats.chartCount; + realData[key] = (100 * data.stats[enumMetric][key]) / data.stats.chartCount; // @ts-expect-error stupid key stuff - realData[`${key}-count`] = data.stats[mode][key]; + realData[`${key}-count`] = data.stats[enumMetric][key]; } arr.push({ @@ -292,18 +294,18 @@ function TableBarChart({ } return arr.reverse(); - }, [dataMap, mode]); + }, [dataMap, enumMetric]); return (
- + Grades - + Lamps @@ -322,11 +324,11 @@ function TableBarChart({ {/* This hack is necessary because otherwise nivo gets caught in a render loop and dies */} - {mode === "grades" ? ( + {enumMetric === "grades" ? ( @@ -334,7 +336,7 @@ function TableBarChart({ @@ -348,12 +350,12 @@ function TableBarChart({ function OverviewBarChart({ gptConfig, - mode, + mode: metric, dataset, colours, }: { gptConfig: GamePTConfig; - mode: "grades" | "lamps"; + mode: string; dataset: any; colours: any; }) { @@ -367,6 +369,8 @@ function OverviewBarChart({ [dataset] ); + const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric; + return ( )} - keys={Reverse(gptConfig[mode])} + keys={Reverse(conf.values)} colors={(k) => ChangeOpacity(colours[k.id], 0.5)} borderColor={(k) => colours[k.data.id]} borderWidth={1} diff --git a/client/src/app/routes/GameRoutes.tsx b/client/src/app/routes/GameRoutes.tsx index 007466f2c..099f2d932 100644 --- a/client/src/app/routes/GameRoutes.tsx +++ b/client/src/app/routes/GameRoutes.tsx @@ -2,7 +2,7 @@ import { ToCDNURL } from "util/api"; import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts"; import { ChangeOpacity } from "util/color-opacity"; import { CreateChartLink } from "util/data"; -import { NumericSOV } from "util/sorts"; +import { NumericSOV, StrSOV } from "util/sorts"; import PlaytypeSelect from "app/pages/dashboard/games/_game/PlaytypeSelect"; import GPTChartPage from "app/pages/dashboard/games/_game/_playtype/GPTChartPage"; import GPTDevInfo from "app/pages/dashboard/games/_game/_playtype/GPTDevInfo"; @@ -30,6 +30,7 @@ import { ChartDocument, FormatDifficulty, Game, + GetGPTString, GetGameConfig, GetGamePTConfig, SongDocument, @@ -43,6 +44,8 @@ import QuestlinePage from "components/game/targets/QuestlinePage"; import QuestsPage from "components/game/targets/QuestsPage"; import ChartRedirector from "app/pages/dashboard/games/_game/_playtype/ChartRedirector"; import QuestPage from "components/game/targets/QuestPage"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import { DifficultyConfig, FixedDifficulties } from "tachi-common/types/game-config-utils"; export default function GameRoutes() { const { game } = useParams<{ game: string }>(); @@ -265,18 +268,16 @@ function SongInfoHeader({ SongsReturn) { const gptConfig = GetGamePTConfig(game, playtype); - // accidentally O(n^2) but this is a short list so who cares - const sortedCharts = charts - .slice(0) - .sort( - NumericSOV((x) => - gptConfig.difficulties.indexOf( - game === "itg" - ? (x as ChartDocument<"itg:Stamina">).data.difficultyTag - : x.difficulty - ) - ) - ); + const sortedCharts = charts.slice(0).sort( + gptConfig.difficulties.type === "DYNAMIC" + ? StrSOV((x) => x.difficulty) + : NumericSOV((x) => + // can TS *really* not infer this? + (gptConfig.difficulties as FixedDifficulties).order.indexOf( + x.difficulty + ) + ) + ); const [ImageCell, setImageCell] = useState(null); @@ -293,7 +294,7 @@ function SongInfoHeader({ /> ); } else if (game === "itg") { - const banner = (song as SongDocument<"itg">).data.banner; + const banner = (song as SongDocument<"itg">).data.originalPack; if (banner) { setImageCell( @@ -385,12 +386,9 @@ function DifficultyButton({ setActiveChart, activeChart, }: Props & { chart: ChartDocument }) { - const gptConfig = GetGamePTConfig(game, playtype); + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)]; - const diffTag = - game === "itg" - ? (chart as ChartDocument<"itg:Stamina">).data.difficultyTag - : chart.difficulty; + const diffTag = chart.difficulty; return ( (preferredDefaultEnum); + const [enumMetric, setEnumMetric] = useState(preferredDefaultEnum); const [breakdownInfo, setBreakdownInfo] = useState<"graph" | "switchboard">("switchboard"); + const gptConfig = GetGamePTConfig(game, playtype); + + const enumConf = GetScoreEnumConfs(gptConfig); + return (
- - - Score Info - - - - Lamp Info - + {Object.keys(enumConf).map((k) => ( + + + {UppercaseFirst(k)} Info + + ))}
@@ -58,7 +61,7 @@ export default function FolderInfoHeader({ game={game} folderDataset={folderDataset} playtype={playtype} - view={view} + view={enumMetric} /> ) : ( )} diff --git a/client/src/components/game/folder/FolderScoreDistributionChart.tsx b/client/src/components/game/folder/FolderScoreDistributionChart.tsx index 71be02348..c83a2e525 100644 --- a/client/src/components/game/folder/FolderScoreDistributionChart.tsx +++ b/client/src/components/game/folder/FolderScoreDistributionChart.tsx @@ -1,150 +1,45 @@ import { CountElements, Reverse } from "util/misc"; import React, { useMemo } from "react"; -import { GetGamePTConfig } from "tachi-common"; +import { GetGPTString, GetGamePTConfig, GetScoreMetricConf } from "tachi-common"; import { GamePT } from "types/react"; import { FolderDataset } from "types/tables"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import FolderDistributionTable from "./FolderDistributionTable"; type Props = { folderDataset: FolderDataset; - view: "grade" | "lamp"; + view: string; } & GamePT; export default function FolderScoreDistributionChart({ game, playtype, folderDataset, - view, + view: metric, }: Props) { const gptConfig = GetGamePTConfig(game, playtype); + const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric; - const values = useMemo(() => { - if (view === "grade") { - return CountElements(folderDataset, (x) => x.__related.pb?.scoreData.grade ?? null); - } + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)]; - return CountElements(folderDataset, (x) => x.__related.pb?.scoreData.lamp ?? null); - }, [folderDataset, view]); + const values = useMemo( + // @ts-expect-error hack + () => CountElements(folderDataset, (x) => x.__related.pb?.scoreData[metric] ?? null), + [folderDataset, metric] + ); return (
- {view === "grade" ? ( - - ) : ( - - )} +
- {/*
- -
*/}
); } - -// function ScoreDistributionChart({ game, folderDataset, playtype, view }: Props) { -// if (view === "grade") { -// return ; -// } - -// return <>LAMP TODO; -// } - -// function ScoreBarChart({ game, playtype, folderDataset }: Omit) { -// const dataMap = CreateChartIDMap(folderDataset); -// const gptConfig = GetGamePTConfig(game, playtype); - -// const expScale = GetGradeChartExpScale(game); -// const expFn = ComposeExpFn(expScale); -// const invExpFn = ComposeInverseExpFn(expScale); - -// const dataset = []; - -// for (const data of folderDataset) { -// const value = data.__related.pb ? expFn(data.__related.pb.scoreData.percent) : 0; -// dataset.push({ -// chartID: data.chartID, -// expValue: expFn(value), -// value, -// grade: data.__related.pb?.scoreData.grade, -// }); -// } - -// dataset.sort(NumericSOV(x => x.expValue)); - -// return ( -//
-// ( -// { -// const data = dataMap.get(d.indexValue as string)!; - -// return ( -//
-// {data.__related.song.title} -//
-// {data.__related.pb?.scoreData.percent.toFixed(2)}% -//
-// ); -// }} -// /> -// )} -// key={"value"} -// colors={k => { -// if (!k.data.grade) { -// return "black"; -// } -// // @ts-expect-error temp -// return ChangeOpacity(gptConfig.gradeColours[k.data.grade], 0.5); -// }} -// // @ts-expect-error temp -// borderColor={k => gptConfig.gradeColours[k.data.grade]} -// borderWidth={1} -// padding={0.2} -// // @ts-expect-error temp -// data={dataset} -// minValue={0} -// maxValue={expFn(100)} -// margin={{ left: 50, top: 20, bottom: 20 }} -// valueFormat={e => `${invExpFn(e).toFixed(2)}%`} -// axisLeft={{ -// tickValues: gptConfig.gradeBoundaries.map(e => (e === 0 ? 0 : expFn(e))), -// format: x => { -// let nearest; - -// const lgv = invExpFn(x); - -// for (const [i, gradeBnd] of gptConfig.gradeBoundaries.entries()) { -// if (Math.abs(gradeBnd - lgv) < 0.00005) { -// nearest = i; -// break; -// } -// } - -// if (nearest === undefined) { -// return null; -// } - -// return gptConfig.grades[nearest]; -// }, -// }} -// axisBottom={null} -// {...DEFAULT_BAR_PROPS} -// labelSkipWidth={40} -// /> -//
-// ); -// } diff --git a/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx b/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx index 11daad1ce..ccc06fe41 100644 --- a/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx +++ b/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx @@ -24,11 +24,16 @@ import { Game, GamePTConfig, GetGamePTConfig, + GetGPTString, + GetScoreMetricConf, + GPTString, integer, SongDocument, TableDocument, } from "tachi-common"; import { SessionFolderRaises, SessionReturns } from "types/api-returns"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; export default function SessionFolderRaiseBreakdown({ sessionData, @@ -182,7 +187,7 @@ export default function SessionFolderRaiseBreakdown({ chartMap={chartMap} game={game} folderRaiseInfo={folderRaiseInfo} - gptConfig={gptConfig} + gptString={GetGPTString(game, playtype)} /> ))} @@ -205,41 +210,39 @@ export default function SessionFolderRaiseBreakdown({ ); } -const SortRaisesNicely = (gptConfig: GamePTConfig, preferredScoreBucket: "grade" | "lamp") => - NumericSOV((x) => { - const baseValue = - x.type === "grade" - ? gptConfig.grades.indexOf(x.value) - : gptConfig.lamps.indexOf(x.value); +const SortRaisesNicely = (gptConfig: GamePTConfig, preferredEnum: string) => { + const conf = GetScoreMetricConf(gptConfig, preferredEnum) as ConfEnumScoreMetric; + + return NumericSOV((x) => { + const baseValue = conf.values.indexOf(x.value); // if this is what the user prefers to see, push it to the top - if (x.type === preferredScoreBucket) { + if (x.type === preferredEnum) { return baseValue + 1_000_000; } return baseValue; }, true); +}; function FolderRaiseRender({ folderRaiseInfo, game, songMap, chartMap, - gptConfig, + gptString, }: { folderRaiseInfo: SessionFolderRaises; chartMap: Map; songMap: Map; game: Game; - gptConfig: GamePTConfig; + gptString: GPTString; }) { - let colour; - - if (folderRaiseInfo.type === "grade") { - colour = gptConfig.gradeColours[folderRaiseInfo.value]; - } else { - colour = gptConfig.lampColours[folderRaiseInfo.value]; - } + const colour = + // @ts-expect-error lazy + GPT_CLIENT_IMPLEMENTATIONS[gptString].enumColours[folderRaiseInfo.type][ + folderRaiseInfo.value + ]; const newTotal = folderRaiseInfo.previousCount + folderRaiseInfo.raisedCharts.length; diff --git a/client/src/components/tables/cells/BPICell.tsx b/client/src/components/tables/cells/BPICell.tsx index dd5811898..0c1dedcf3 100644 --- a/client/src/components/tables/cells/BPICell.tsx +++ b/client/src/components/tables/cells/BPICell.tsx @@ -154,15 +154,7 @@ function FormatAverage(score: integer, playtype: "SP" | "DP", notecount: integer score={score} /> ), - delta: ( - - ), + delta: , }; } diff --git a/client/src/components/tables/cells/DeltaCell.tsx b/client/src/components/tables/cells/DeltaCell.tsx index 3d27755ce..818384b12 100644 --- a/client/src/components/tables/cells/DeltaCell.tsx +++ b/client/src/components/tables/cells/DeltaCell.tsx @@ -1,29 +1,24 @@ -import { GenericFormatGradeDelta } from "util/grade-deltas"; import React from "react"; -import { Game, Grades, GPTString, Playtypes } from "tachi-common"; +import { GetGradeDeltas, GradeBoundary } from "tachi-common"; export default function DeltaCell({ - game, - playtype, - score, - percent, + value, grade, + gradeBoundaries, }: { - game: Game; - playtype: Playtypes[Game]; - score: number; - percent: number; - grade: Grades[GPTString]; + value: number; + grade: string; + gradeBoundaries: Array>; }) { - if (score === 0) { + if (value === 0) { return N/A; } // eslint-disable-next-line prefer-const - let { lower, upper, closer } = GenericFormatGradeDelta(game, playtype, score, percent, grade); + let { lower, upper, closer } = GetGradeDeltas(gradeBoundaries, grade, value); // (max-)+20 is a stupid statistic. hard override it. - if ((game === "iidx" || game === "bms" || game === "pms") && lower.startsWith("(MAX-)+")) { + if (lower.startsWith("(MAX-)+")) { closer = "upper"; } diff --git a/client/src/components/tables/cells/DifficultyCell.tsx b/client/src/components/tables/cells/DifficultyCell.tsx index 9d2a2c9eb..5aa13af5b 100644 --- a/client/src/components/tables/cells/DifficultyCell.tsx +++ b/client/src/components/tables/cells/DifficultyCell.tsx @@ -8,8 +8,10 @@ import { FormatDifficulty, FormatDifficultyShort, Game, + GetGPTString, GetGamePTConfig, } from "tachi-common"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import BMSOrPMSDifficultyCell from "./BMSOrPMSDifficultyCell"; import RatingSystemPart from "./RatingSystemPart"; import USCDifficultyCell from "./USCDifficultyCell"; @@ -49,10 +51,13 @@ export default function DifficultyCell({ return } />; } + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, chart.playtype)]; + return ( )} - {!noTierlist && Object.keys(chart.tierlistInfo).length > 0 && ( + {!noTierlist && gptImpl.ratingSystems.length > 0 && ( )} {!chart.isPrimary && ( diff --git a/client/src/components/tables/cells/ITGDifficultyCell.tsx b/client/src/components/tables/cells/ITGDifficultyCell.tsx index a1611dbd7..430f1a23c 100644 --- a/client/src/components/tables/cells/ITGDifficultyCell.tsx +++ b/client/src/components/tables/cells/ITGDifficultyCell.tsx @@ -2,18 +2,25 @@ import { ChangeOpacity } from "util/color-opacity"; import QuickTooltip from "components/layout/misc/QuickTooltip"; import Muted from "components/util/Muted"; import React from "react"; -import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common"; +import { ChartDocument, COLOUR_SET } from "tachi-common"; import MiniTable from "../components/MiniTable"; import RatingSystemPart from "./RatingSystemPart"; -export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg:Stamina"> }) { - const gptConfig = GetGamePTConfig("itg", "Stamina"); +const COLOUR_LOOKUP = { + Beginner: COLOUR_SET.paleBlue, + Easy: COLOUR_SET.green, + Medium: COLOUR_SET.vibrantYellow, + Hard: COLOUR_SET.red, + Expert: COLOUR_SET.pink, + Edit: COLOUR_SET.gray, +}; +export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg:Stamina"> }) { return (
- {chart.data.difficultyTag}: {chart.level} [{chart.data.displayBPM.toFixed()} - ] + {chart.data.difficultyTag}: {chart.level} [ + {chart.data.primaryBPM?.toFixed() ?? "???"}]
{chart.data.charter} diff --git a/client/src/components/tables/cells/PBCompareCell.tsx b/client/src/components/tables/cells/PBCompareCell.tsx index 6f4eba576..55cc217f0 100644 --- a/client/src/components/tables/cells/PBCompareCell.tsx +++ b/client/src/components/tables/cells/PBCompareCell.tsx @@ -1,4 +1,3 @@ -import { FormatMillions } from "util/misc"; import React from "react"; import { COLOUR_SET, Game, PBScoreDocument, Playtype } from "tachi-common"; import { ConfScoreMetric } from "tachi-common/types/metrics"; @@ -34,41 +33,6 @@ export default function PBCompareCell({ status = cmp(base.scoreData[metric], compare.scoreData[metric]); } - let scoreRenderFn = (k: number) => k.toString(); - let shouldShowPercent = false; - let shouldPrioritisePercent = false; - - switch (game) { - case "chunithm": - case "gitadora": - case "usc": - case "sdvx": - case "popn": - case "wacca": - case "museca": - scoreRenderFn = FormatMillions; - shouldShowPercent = false; - break; - case "jubeat": - scoreRenderFn = FormatMillions; - shouldShowPercent = true; - shouldPrioritisePercent = true; - break; - case "bms": - case "iidx": - scoreRenderFn = (k: number) => k.toString(); - shouldShowPercent = true; - break; - } - - const fmtCmpPercent = fmtNum( - (base?.scoreData.percent ?? 0) - (compare?.scoreData.percent ?? 0), - 2, - "%" - ); - const cmpScore = (base?.scoreData.score ?? 0) - (compare?.scoreData.score ?? 0); - const cmpLamp = (base?.scoreData.lampIndex ?? 0) - (compare?.scoreData.lampIndex ?? 0); - return ( {metricConf.type === "ENUM" ? ( - <> + // @ts-expect-error this access is fine + <>{base.scoreData[metric]} ) : ( - <> - {shouldPrioritisePercent ? ( - <> - {fmtCmpPercent} - {shouldShowPercent && ( - <> -
- - ({(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)}) - - - )} - - ) : ( - <> - {(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)} - {shouldShowPercent && ( - <> -
- ({fmtCmpPercent}) - - )} - - )} - + metricConf.type === "DECIMAL" || + (metricConf.type === "INTEGER" && ( + + {/* @ts-expect-error this is fine */} + {(base.scoreData[metric] > 0 ? "+" : "") + + // @ts-expect-error this is fine + metricConf.formatter(base.scoreData[metric])} + + )) )} ); diff --git a/client/src/components/tables/cells/RatingSystemPart.tsx b/client/src/components/tables/cells/RatingSystemPart.tsx index f49c8d980..a32dfdb56 100644 --- a/client/src/components/tables/cells/RatingSystemPart.tsx +++ b/client/src/components/tables/cells/RatingSystemPart.tsx @@ -41,7 +41,6 @@ export default function RatingSystemPart({ chart, game }: { chart: ChartDocument {e} {strV} ({numV.toFixed(2)}) - {/* @ts-expect-error utterly silly types */} {e.idvDifference(chart) && ( <>
diff --git a/client/src/components/tables/cells/ScoreCell.tsx b/client/src/components/tables/cells/ScoreCell.tsx index e6a30cfd5..6e7220992 100644 --- a/client/src/components/tables/cells/ScoreCell.tsx +++ b/client/src/components/tables/cells/ScoreCell.tsx @@ -9,7 +9,7 @@ export default function ScoreCell({ percent, scoreRenderFn, }: { - score: integer; + score?: integer; percent: number; grade: string; colour: string; @@ -26,8 +26,14 @@ export default function ScoreCell({
{`${percent.toFixed(2)}%`} -
- [{scoreRenderFn ? scoreRenderFn(score) : score}] + {score !== undefined && ( + <> +
+ + [{scoreRenderFn ? scoreRenderFn(score) : score}] + + + )} ); } diff --git a/client/src/components/tables/cells/TitleCell.tsx b/client/src/components/tables/cells/TitleCell.tsx index f5faa26a4..4e1f3ea59 100644 --- a/client/src/components/tables/cells/TitleCell.tsx +++ b/client/src/components/tables/cells/TitleCell.tsx @@ -32,7 +32,7 @@ export default function TitleCell({ `/misc/popn/banners/${(chart as any).data.inGameID}.png` )})`; } else if (game === "itg") { - const banner = (song as SongDocument<"itg">).data.banner; + const banner = (song as SongDocument<"itg">).data.originalPack; if (banner) { backgroundImage = `url(${ToCDNURL( diff --git a/client/src/components/tables/cells/USCDifficultyCell.tsx b/client/src/components/tables/cells/USCDifficultyCell.tsx index 9c5c22a28..8139234b4 100644 --- a/client/src/components/tables/cells/USCDifficultyCell.tsx +++ b/client/src/components/tables/cells/USCDifficultyCell.tsx @@ -2,8 +2,10 @@ import { ChangeOpacity } from "util/color-opacity"; import { FormatTables } from "util/misc"; import QuickTooltip from "components/layout/misc/QuickTooltip"; import Icon from "components/util/Icon"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import { GPTClientImplementation } from "lib/types"; import React from "react"; -import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common"; +import { COLOUR_SET, ChartDocument } from "tachi-common"; import RatingSystemPart from "./RatingSystemPart"; export default function USCDifficultyCell({ @@ -15,10 +17,12 @@ export default function USCDifficultyCell({ ? `${chart.difficulty} ${chart.level}` : FormatTables(chart.data.tableFolders); - const gptConfig = GetGamePTConfig("usc", chart.playtype); + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[ + `usc:${chart.playtype}` + ] as GPTClientImplementation<"usc:Controller">; const bgColour = ChangeOpacity( - chart.data.isOfficial ? gptConfig.difficultyColours[chart.difficulty]! : COLOUR_SET.teal, + chart.data.isOfficial ? gptImpl.difficultyColours[chart.difficulty]! : COLOUR_SET.teal, 0.2 ); diff --git a/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx b/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx index fd3c359d4..a2d70a648 100644 --- a/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx +++ b/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from "react"; import { Nav } from "react-bootstrap"; import { ChartDocument, PBScoreDocument, ScoreDocument } from "tachi-common"; -type LampTypes = "NORMAL" | "EASY" | "HARD" | "EX_HARD"; +type LampTypes = "Normal" | "Easy" | "Hard" | "EXHard"; export function BMSGraphsComponent({ score, @@ -34,34 +34,34 @@ export function BMSGraphsComponent({
{gaugeStatus === "gsm" && lamp !== "DAN_GAUGE" ? ( - + ) : gaugeStatus === "single" ? ( ) : ( @@ -157,31 +162,34 @@ function LampToKey( if (IsScore(score) && score.scoreMeta.gauge) { switch (score.scoreMeta.gauge) { case "EASY": + return "Easy"; case "NORMAL": + return "Normal"; case "HARD": - return score.scoreMeta.gauge; + return "Hard"; case "ASSISTED EASY": - return "EASY"; + return "Easy"; case "EX-HARD": - return "EX_HARD"; + return "EXHard"; } } if (lamp === "CLEAR") { - return "NORMAL"; + return "Normal"; } else if (lamp === "EASY CLEAR") { - return "EASY"; + return "Easy"; } else if (lamp === "HARD CLEAR") { - return "HARD"; + return "Hard"; } else if (lamp === "EX HARD CLEAR") { - return "EX_HARD"; + return "EXHard"; } else if (lamp === "FULL COMBO") { // @hack - attempt to guess what gauge they used? + // this could be hard or easy, we actually legitimately do not know in this scenario if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) { - return "EX_HARD"; + return "EXHard"; } - return "NORMAL"; + return "Normal"; } - return "NORMAL"; + return "Normal"; } diff --git a/client/src/components/tables/dropdowns/components/ITGScoreDropdownParts.tsx b/client/src/components/tables/dropdowns/components/ITGScoreDropdownParts.tsx index 624b5059f..cfc788bc6 100644 --- a/client/src/components/tables/dropdowns/components/ITGScoreDropdownParts.tsx +++ b/client/src/components/tables/dropdowns/components/ITGScoreDropdownParts.tsx @@ -26,14 +26,14 @@ export function ITGGraphsComponent({
- {chart.data.breakdown.npsPerMeasure ? ( + {chart.data.npsPerMeasure ? ( ({ + data: chart.data.npsPerMeasure.map((e, i) => ({ x: i, y: e, })), diff --git a/client/src/components/tables/dropdowns/components/RivalCompare.tsx b/client/src/components/tables/dropdowns/components/RivalCompare.tsx index 8e0beee19..266b093d8 100644 --- a/client/src/components/tables/dropdowns/components/RivalCompare.tsx +++ b/client/src/components/tables/dropdowns/components/RivalCompare.tsx @@ -7,7 +7,7 @@ import useUGPTBase from "components/util/useUGPTBase"; import { UserContext } from "context/UserContext"; import React, { useContext } from "react"; import { Link } from "react-router-dom"; -import { ChartDocument, Game } from "tachi-common"; +import { ChartDocument, Game, GetGamePTConfig } from "tachi-common"; import { ChartRivalsReturn } from "types/api-returns"; import { RivalChartDataset } from "types/tables"; import { Col } from "react-bootstrap"; @@ -45,6 +45,8 @@ export default function RivalCompare({ chart, game }: { chart: ChartDocument; ga ); } + const gptConfig = GetGamePTConfig(game, playtype); + const rivalDataset: RivalChartDataset = [...data.rivals, currentUser] .map((u) => ({ ...u, @@ -52,7 +54,10 @@ export default function RivalCompare({ chart, game }: { chart: ChartDocument; ga pb: data.pbs.find((p) => p.userID === u.id) ?? null, }, })) - .sort(NumericSOV((x) => x.__related.pb?.scoreData.percent ?? -Infinity, true)) + .sort( + // @ts-expect-error this access is obviously legal + NumericSOV((x) => x.__related.pb?.scoreData[gptConfig.defaultMetric] ?? -Infinity, true) + ) .map((e, index) => ({ ...e, __related: { diff --git a/client/src/components/tables/game-core-cells/BMSCoreCells.tsx b/client/src/components/tables/game-core-cells/BMSCoreCells.tsx index afd623e60..49a27a9a6 100644 --- a/client/src/components/tables/game-core-cells/BMSCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/BMSCoreCells.tsx @@ -1,5 +1,13 @@ import React from "react"; -import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; +import { + GPTString, + PBScoreDocument, + ScoreRatingAlgorithms, + ScoreDocument, + IIDXLIKE_GBOUNDARIES, + GetGPTString, +} from "tachi-common"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell"; import DeltaCell from "../cells/DeltaCell"; import RatingCell from "../cells/RatingCell"; @@ -14,12 +22,19 @@ export default function BMSCoreCells({ }) { return ( <> - - + diff --git a/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx b/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx index c61d3fddc..ccf75d535 100644 --- a/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx @@ -17,7 +17,11 @@ export default function GitadoraCoreCells({ }) { return ( <> - + diff --git a/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx b/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx index 0ab2f5572..8af31a3b3 100644 --- a/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx @@ -1,5 +1,13 @@ import React from "react"; -import { ChartDocument, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; +import { + ChartDocument, + PBScoreDocument, + ScoreRatingAlgorithms, + ScoreDocument, + IIDXLIKE_GBOUNDARIES, + GetGPTString, +} from "tachi-common"; +import { GPT_CLIENT_IMPLEMENTATIONS, GetEnumColour } from "lib/game-implementations"; import BPICell from "../cells/BPICell"; import DeltaCell from "../cells/DeltaCell"; import IIDXLampCell from "../cells/IIDXLampCell"; @@ -17,12 +25,15 @@ export default function IIDXCoreCells({ }) { return ( <> - - + diff --git a/client/src/components/tables/game-core-cells/ITGCoreCells.tsx b/client/src/components/tables/game-core-cells/ITGCoreCells.tsx index 5a47b7dac..d4cf9f81b 100644 --- a/client/src/components/tables/game-core-cells/ITGCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/ITGCoreCells.tsx @@ -15,7 +15,11 @@ export default function ITGCoreCells({ }) { return ( <> - + diff --git a/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx b/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx index 477c0cc7e..8cd87ba04 100644 --- a/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx @@ -15,7 +15,11 @@ export default function MaimaiDXCoreCells({ }) { return ( <> - + diff --git a/client/src/components/tables/game-core-cells/PMSCoreCells.tsx b/client/src/components/tables/game-core-cells/PMSCoreCells.tsx index 93dbcebb8..ef92d5513 100644 --- a/client/src/components/tables/game-core-cells/PMSCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/PMSCoreCells.tsx @@ -1,5 +1,13 @@ import React from "react"; -import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; +import { + GPTString, + PBScoreDocument, + ScoreRatingAlgorithms, + ScoreDocument, + IIDXLIKE_GBOUNDARIES, + GetGPTString, +} from "tachi-common"; +import { GPT_CLIENT_IMPLEMENTATIONS, GetEnumColour } from "lib/game-implementations"; import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell"; import DeltaCell from "../cells/DeltaCell"; import RatingCell from "../cells/RatingCell"; @@ -16,12 +24,15 @@ export default function PMSCoreCells({ }) { return ( <> - - + diff --git a/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx b/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx deleted file mode 100644 index a61d5bcde..000000000 --- a/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import IIDXLampCell from "components/tables/cells/IIDXLampCell"; -import JubeatScoreCell from "components/tables/cells/JubeatScoreCell"; -import LampCell from "components/tables/cells/LampCell"; -import MillionsScoreCell from "components/tables/cells/MillionsScoreCell"; -import PopnLampCell from "components/tables/cells/PopnLampCell"; -import ScoreCell from "components/tables/cells/ScoreCell"; -import SDVXLampCell from "components/tables/cells/SDVXLampCell"; -import React from "react"; -import { ChartDocument, Game, PBScoreDocument, ScoreDocument } from "tachi-common"; -import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell"; -import BMSCoreCells from "./BMSCoreCells"; - -export default function ShortScoreCoreCells({ - game, - score, - chart, -}: { - score: ScoreDocument | PBScoreDocument; - chart: ChartDocument; - game: Game; -}) { - const sc = score as any; // lazy hack - - if (game === "iidx") { - return ( - <> - - } /> - - ); - } else if (game === "bms" || game === "pms") { - return ( - <> - - - - ); - } else if ( - game === "sdvx" || - game === "usc" || - game === "museca" || - game === "wacca" || - game === "chunithm" || - game === "gitadora" - ) { - return ( - <> - - - - ); - } else if (game === "jubeat") { - return ( - <> - - - - ); - } else if (game === "popn") { - return ( - <> - - - - ); - } else if (game === "itg") { - return ( - <> - - - - ); - } - - return <>No cells defined for {game}. Report this!; -} diff --git a/client/src/components/tables/rivals/ComparePBsTable.tsx b/client/src/components/tables/rivals/ComparePBsTable.tsx index 2b0a59e5f..23376bb6f 100644 --- a/client/src/components/tables/rivals/ComparePBsTable.tsx +++ b/client/src/components/tables/rivals/ComparePBsTable.tsx @@ -8,8 +8,8 @@ import PBCompareCell from "../cells/PBCompareCell"; import TitleCell from "../cells/TitleCell"; import SelectableCompareType from "../components/SelectableCompareType"; import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable"; -import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells"; import ChartHeader from "../headers/ChartHeader"; +import ScoreCoreCells from "../game-core-cells/ScoreCoreCells"; export default function ComparePBsTable({ dataset, @@ -103,7 +103,7 @@ function Row({ data, game, metric }: { data: ComparePBsDataset[0]; game: Game; m {data.base ? ( - + ) : ( Not Played )} @@ -116,7 +116,7 @@ function Row({ data, game, metric }: { data: ComparePBsDataset[0]; game: Game; m metric={metric} /> {data.compare ? ( - + ) : ( Not Played )} diff --git a/client/src/components/targets/SetNewGoalModal.tsx b/client/src/components/targets/SetNewGoalModal.tsx index 15c1117e0..5dec17771 100644 --- a/client/src/components/targets/SetNewGoalModal.tsx +++ b/client/src/components/targets/SetNewGoalModal.tsx @@ -10,12 +10,14 @@ import { FormatChart, Game, GetGamePTConfig, + GetScoreMetricConf, GoalDocument, Playtype, SongDocument, } from "tachi-common"; import { GamePT, SetState, UGPT } from "types/react"; import CheckEdit from "components/util/CheckEdit"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; export default function SetNewGoalModal({ show, @@ -32,14 +34,15 @@ export default function SetNewGoalModal({ preData: { chart: ChartDocument; song: SongDocument } | FolderDocument; } & UGPT) { const gptConfig = GetGamePTConfig(game, playtype); + const conf = GetScoreMetricConf( + gptConfig, + gptConfig.preferredDefaultEnum + ) as ConfEnumScoreMetric; const [criteria, setCriteria] = useState({ mode: "single", - key: gptConfig.preferredDefaultEnum === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex", - value: - gptConfig.preferredDefaultEnum === "grade" - ? gptConfig.grades.indexOf(gptConfig.clearGrade) - : gptConfig.lamps.indexOf(gptConfig.clearLamp), + key: gptConfig.preferredDefaultEnum, + value: conf.values.indexOf(conf.minimumRelevantValue), }); const charts = useMemo( @@ -283,20 +286,18 @@ function CriteriaValuePicker({ onChange: (value: GoalDocument["criteria"]["value"]) => void; } & GamePT) { const gptConfig = GetGamePTConfig(game, playtype); + const conf = GetScoreMetricConf(gptConfig, criteria.key); - switch (criteria.key) { - case "scoreData.score": - return ( - onChange(Number(e.target.value))} - type="number" - min={0} - value={criteria.value} - /> - ); - case "scoreData.percent": + if (!conf) { + return <>ENOCONF {criteria.key}; + } + + switch (conf.type) { + case "NULLABLE_GRAPH": + case "GRAPH": + return <>Cannot set goals for graph metrics.; + case "DECIMAL": + case "INTEGER": return ( ); - case "scoreData.gradeIndex": + case "ENUM": return ( - ); - - case "scoreData.lampIndex": - return ( - ); } @@ -342,12 +329,16 @@ function CriteriaValuePicker({ function getBaseKeyValue(game: Game, playtype: Playtype, key: GoalDocument["criteria"]["key"]) { const gptConfig = GetGamePTConfig(game, playtype); + const conf = GetScoreMetricConf(gptConfig, key); - switch (key) { - case "scoreData.gradeIndex": - return gptConfig.grades.indexOf(gptConfig.clearGrade); - case "scoreData.lampIndex": - return gptConfig.lamps.indexOf(gptConfig.clearLamp); + if (!conf) { + // SHOULD NEVER HAPPEN! + return 0; + } + + switch (conf.type) { + case "ENUM": + return conf.values.indexOf(conf.minimumRelevantValue); default: return 0; } diff --git a/client/src/components/user/UGPTStatCreator.tsx b/client/src/components/user/UGPTStatCreator.tsx index 4e9dd7bf0..a73e54ca4 100644 --- a/client/src/components/user/UGPTStatCreator.tsx +++ b/client/src/components/user/UGPTStatCreator.tsx @@ -1,5 +1,6 @@ import { APIFetchV1 } from "util/api"; import { CreateSongMap } from "util/data"; +import { UppercaseFirst } from "util/misc"; import { useFormik } from "formik"; import React, { ChangeEventHandler, useContext, useEffect, useState } from "react"; import { Button, Form, Modal } from "react-bootstrap"; @@ -11,6 +12,8 @@ import { UserDocument, ShowcaseStatDetails, Playtype, + GetScoreMetricConf, + GetScoreMetrics, } from "tachi-common"; import { GamePT, SetState } from "types/react"; import { SongChartsSearch } from "types/api-returns"; @@ -48,13 +51,13 @@ export default function UGPTStatCreator({ if (values.mode === "chart") { stat = { mode: "chart", - metric: values.property as ShowcaseStatDetails["metric"], + metric: values.metric as ShowcaseStatDetails["metric"], chartID: values.chartID ?? "", }; } else if (values.mode === "folder") { stat = { mode: "folder", - metric: values.property as "lamp" | "score" | "percent" | "grade", + metric: values.metric as "lamp" | "score" | "percent" | "grade", folderID: values.folderID ?? "", gte, }; @@ -68,7 +71,7 @@ export default function UGPTStatCreator({ }); useEffect(() => { - if (formik.values.mode === "folder" && formik.values.property === "playcount") { + if (formik.values.mode === "folder" && formik.values.metric === "playcount") { formik.setValues({ ...formik.values, metric: "lamp" }); } }, [formik.values.mode]); @@ -143,6 +146,8 @@ export default function UGPTStatCreator({ })(); }, [chartSearch, requesterHasPlayed]); + const gptConfig = GetGamePTConfig(game, playtype); + return ( setShow(false)}> @@ -167,14 +172,15 @@ export default function UGPTStatCreator({ Property - - - - + {GetScoreMetrics(gptConfig, ["DECIMAL", "INTEGER", "ENUM"]).map((e) => ( + + ))} {formik.values.mode === "chart" && ( )} @@ -222,7 +228,7 @@ export default function UGPTStatCreator({ {...{ game, playtype, - property: formik.values.property, + metric: formik.values.metric, }} /> @@ -268,52 +274,39 @@ export default function UGPTStatCreator({ } function FolderGTESelect({ - property, + metric, game, playtype, value, onChange, }: { - property: string; + metric: string; value: number; onChange: ChangeEventHandler; } & GamePT) { const gptConfig = GetGamePTConfig(game, playtype); const props = { value, onChange }; - if (property === "grade") { - return ( - - ); - } else if (property === "lamp" || property === "playcount") { - return ( - - ); - } else if (property === "percent") { - return ( - - ); - } else if (property === "score") { - return ; + + const conf = GetScoreMetricConf(gptConfig, metric); + + if (!conf) { + return <>error: no conf? what?; } - throw new Error(`Invalid property ${property}.`); + if (conf.type === "ENUM") { + return ( + + ); + } else if (conf.type === "GRAPH" || conf.type === "NULLABLE_GRAPH") { + return <>can't set stats for graphs. how'd you get here?; + } + + return ; } diff --git a/client/src/lib/game-implementations.ts b/client/src/lib/game-implementations.ts index 1f42d9d59..80f53e9a7 100644 --- a/client/src/lib/game-implementations.ts +++ b/client/src/lib/game-implementations.ts @@ -4,7 +4,7 @@ import { GITADORA_DORA_IMPL, GITADORA_GITA_IMPL } from "./games/gitadora"; import { SDVX_IMPL, USC_IMPL } from "./games/sdvx-usc"; import { GPTClientImplementation } from "./types"; import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./games/iidx"; -import { BMS_PMS_IMPL } from "./games/bms-pms"; +import { BMS_14K_IMPL, BMS_7K_IMPL, PMS_IMPL } from "./games/bms-pms"; import { bg, bgc } from "./games/_util"; type GPTClientImplementations = { @@ -397,10 +397,10 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = { }, "gitadora:Dora": GITADORA_DORA_IMPL, "gitadora:Gita": GITADORA_GITA_IMPL, - "bms:14K": BMS_PMS_IMPL, - "bms:7K": BMS_PMS_IMPL, - "pms:Controller": BMS_PMS_IMPL, - "pms:Keyboard": BMS_PMS_IMPL, + "bms:14K": BMS_14K_IMPL, + "bms:7K": BMS_7K_IMPL, + "pms:Controller": PMS_IMPL, + "pms:Keyboard": PMS_IMPL, "sdvx:Single": SDVX_IMPL, "usc:Controller": USC_IMPL, "usc:Keyboard": USC_IMPL, diff --git a/client/src/lib/games/bms-pms.ts b/client/src/lib/games/bms-pms.ts index dd4603502..930f742a0 100644 --- a/client/src/lib/games/bms-pms.ts +++ b/client/src/lib/games/bms-pms.ts @@ -3,9 +3,9 @@ import { NumericSOV } from "util/sorts"; import { GPTClientImplementation } from "lib/types"; import { COLOUR_SET, GPTStrings } from "tachi-common"; import { FormatSieglindeBMS } from "tachi-common/config/game-support/bms"; -import { CreateRatingSys } from "./_util"; +import { CreateRatingSys, bg } from "./_util"; -const BMS_PMS_IMPL: GPTClientImplementation = { +const BASE_IMPL: GPTClientImplementation = { difficultyColours: { CHART: COLOUR_SET.gray, // lol }, @@ -55,4 +55,137 @@ const BMS_PMS_IMPL: GPTClientImplementation = { ], }; -export const; +export const BMS_7K_IMPL: GPTClientImplementation<"bms:7K"> = { + ...BASE_IMPL, + classColours: { + genocideDan: { + NORMAL_1: bg("lightblue"), + NORMAL_2: bg("lightblue"), + NORMAL_3: bg("lightblue"), + NORMAL_4: bg("lightblue"), + NORMAL_5: bg("lightblue"), + NORMAL_6: bg("lightblue"), + NORMAL_7: bg("lightblue"), + NORMAL_8: bg("lightblue"), + NORMAL_9: bg("lightred"), + NORMAL_10: bg("lightred"), + INSANE_1: "info", + INSANE_2: "info", + INSANE_3: "info", + INSANE_4: "info", + INSANE_5: "info", + INSANE_6: "info", + INSANE_7: "info", + INSANE_8: "info", + INSANE_9: bg("red"), + INSANE_10: bg("red"), + INSANE_KAIDEN: bg("teal"), + OVERJOY: bg("purple"), + }, + lnDan: { + DAN_1: bg("lightblue"), + DAN_2: bg("lightblue"), + DAN_3: bg("lightblue"), + DAN_4: bg("lightblue"), + DAN_5: bg("lightblue"), + DAN_6: bg("lightblue"), + DAN_7: bg("lightblue"), + DAN_8: bg("lightblue"), + DAN_9: bg("lightred"), + DAN_10: bg("lightred"), + KAIDEN: bg("teal"), + OVERJOY: bg("purple"), + UDON: bg("gold"), + }, + scratchDan: { + KYU_7: bg("green"), + KYU_6: bg("green"), + KYU_5: bg("green"), + KYU_4: bg("green"), + KYU_3: bg("green"), + KYU_2: bg("green"), + KYU_1: bg("green"), + DAN_1: "info", + DAN_2: "info", + DAN_3: "info", + DAN_4: "info", + DAN_5: "info", + DAN_6: "info", + DAN_7: "info", + DAN_8: "info", + DAN_9: "danger", + DAN_10: "danger", + KAIDEN: "warning", + }, + stslDan: { + SL0: null, + SL1: null, + SL2: null, + SL3: null, + SL4: null, + SL5: null, + SL6: null, + SL7: null, + SL8: null, + SL9: null, + SL10: null, + SL11: null, + SL12: null, + ST0: null, + ST1: null, + ST2: null, + ST3: null, + ST4: null, + ST5: null, + ST6: null, + ST7: null, + ST8: null, + ST9: null, + ST10: null, + ST11: null, + }, + }, +}; + +export const BMS_14K_IMPL: GPTClientImplementation<"bms:14K"> = { + ...BASE_IMPL, + classColours: { + genocideDan: BMS_7K_IMPL.classColours.genocideDan, + stslDan: { + SL0: null, + SL1: null, + SL2: null, + SL3: null, + SL4: null, + SL5: null, + SL6: null, + SL7: null, + SL8: null, + SL9: null, + SL10: null, + SL11: null, + SL12: null, + }, + }, +}; + +export const PMS_IMPL: GPTClientImplementation = { + ...BASE_IMPL, + classColours: { + dan: { + INSANE_1: "info", + INSANE_2: "info", + INSANE_3: "info", + INSANE_4: "info", + INSANE_5: "info", + INSANE_6: "info", + INSANE_7: "info", + INSANE_8: "info", + INSANE_9: bg("red"), + INSANE_10: bg("red"), + INSANE_KAIDEN: bg("teal"), + OVERJOY: bg("purple"), + UNDEFINED: "warning", + }, + }, +}; diff --git a/client/src/lib/games/gitadora.ts b/client/src/lib/games/gitadora.ts index 098d24eb7..0fcdaf4c4 100644 --- a/client/src/lib/games/gitadora.ts +++ b/client/src/lib/games/gitadora.ts @@ -1,6 +1,6 @@ import { NumericSOV } from "util/sorts"; import { GPTClientImplementation } from "lib/types"; -import { COLOUR_SET, GPTString, GPTStrings } from "tachi-common"; +import { COLOUR_SET, GPTStrings } from "tachi-common"; import { bg, bgc } from "./_util"; const GITADORA_ENUM_COLOURS: GPTClientImplementation< diff --git a/client/src/lib/games/sdvx-usc.ts b/client/src/lib/games/sdvx-usc.ts index c620cba7a..97d0c79b5 100644 --- a/client/src/lib/games/sdvx-usc.ts +++ b/client/src/lib/games/sdvx-usc.ts @@ -1,7 +1,7 @@ import { NumericSOV } from "util/sorts"; import { GPTClientImplementation } from "lib/types"; import { COLOUR_SET, GPTStrings } from "tachi-common"; -import { CreateRatingSys } from "./_util"; +import { CreateRatingSys, bg, bgc } from "./_util"; const SDVXLIKE_ENUM_COLOURS: GPTClientImplementation["enumColours"] = { grade: { @@ -39,6 +39,64 @@ export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = { XCD: COLOUR_SET.blue, MXM: COLOUR_SET.white, }, + classColours: { + dan: { + DAN_1: bg(COLOUR_SET.red), + DAN_2: bg(COLOUR_SET.paleBlue), + DAN_3: bgc("gold", "black"), + DAN_4: bg("gray"), + DAN_5: bgc(COLOUR_SET.teal, "black"), + DAN_6: bg("blue"), + DAN_7: bgc(COLOUR_SET.vibrantPink, "black"), + DAN_8: bgc("pink", "black"), + DAN_9: bgc("white", "black"), + DAN_10: "warning", + DAN_11: "danger", + INF: bgc("purple", "gold"), + }, + vfClass: { + SIENNA_I: bg(COLOUR_SET.red), + SIENNA_II: bg(COLOUR_SET.red), + SIENNA_III: bg(COLOUR_SET.red), + SIENNA_IV: bg(COLOUR_SET.red), + COBALT_I: bg(COLOUR_SET.paleBlue), + COBALT_II: bg(COLOUR_SET.paleBlue), + COBALT_III: bg(COLOUR_SET.paleBlue), + COBALT_IV: bg(COLOUR_SET.paleBlue), + DANDELION_I: bgc(COLOUR_SET.gold, "black"), + DANDELION_II: bgc(COLOUR_SET.gold, "black"), + DANDELION_III: bgc(COLOUR_SET.gold, "black"), + DANDELION_IV: bgc(COLOUR_SET.gold, "black"), + CYAN_I: bgc(COLOUR_SET.teal, "black"), + CYAN_II: bgc(COLOUR_SET.teal, "black"), + CYAN_III: bgc(COLOUR_SET.teal, "black"), + CYAN_IV: bgc(COLOUR_SET.teal, "black"), + SCARLET_I: bgc(COLOUR_SET.vibrantPink, "black"), + SCARLET_II: bgc(COLOUR_SET.vibrantPink, "black"), + SCARLET_III: bgc(COLOUR_SET.vibrantPink, "black"), + SCARLET_IV: bgc(COLOUR_SET.vibrantPink, "black"), + CORAL_I: bgc(COLOUR_SET.pink, "black"), + CORAL_II: bgc(COLOUR_SET.pink, "black"), + CORAL_III: bgc(COLOUR_SET.pink, "black"), + CORAL_IV: bgc(COLOUR_SET.pink, "black"), + ARGENTO_I: bgc(COLOUR_SET.white, "black"), + ARGENTO_II: bgc(COLOUR_SET.white, "black"), + ARGENTO_III: bgc(COLOUR_SET.white, "black"), + ARGENTO_IV: bgc(COLOUR_SET.white, "black"), + ELDORA_I: "warning", + ELDORA_II: "warning", + ELDORA_III: "warning", + ELDORA_IV: "warning", + CRIMSON_I: bg(COLOUR_SET.vibrantRed), + CRIMSON_II: bg(COLOUR_SET.vibrantRed), + CRIMSON_III: bg(COLOUR_SET.vibrantRed), + CRIMSON_IV: bg(COLOUR_SET.vibrantRed), + IMPERIAL_I: bg(COLOUR_SET.vibrantPurple), + IMPERIAL_II: bg(COLOUR_SET.vibrantPurple), + IMPERIAL_III: bg(COLOUR_SET.vibrantPurple), + IMPERIAL_IV: bg(COLOUR_SET.vibrantPurple), + }, + }, ratingSystems: [ CreateRatingSys( "Tierlist", @@ -68,4 +126,7 @@ export const USC_IMPL: GPTClientImplementation = { ["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)], ["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)], ], + classColours: { + vfClass: SDVX_IMPL.classColours.vfClass, + }, }; diff --git a/client/src/util/grade-deltas.ts b/client/src/util/grade-deltas.ts deleted file mode 100644 index b3fe5a0bc..000000000 --- a/client/src/util/grade-deltas.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Game, GetGamePTConfig, Grades, GPTString, integer, Playtypes } from "tachi-common"; - -export function AbsoluteGradeDelta( - game: Game, - playtype: Playtypes[Game], - score: number, - percent: number, - gradeOrIndex: Grades[GPT] | integer -) { - const gptConfig = GetGamePTConfig(game, playtype); - - if (!gptConfig.gradeBoundaries) { - return 0; - } - - const maxScore = Math.round(score * (100 / percent)); - - const gradeIndex = - typeof gradeOrIndex === "number" ? gradeOrIndex : gptConfig.grades.indexOf(gradeOrIndex); - - const gradeScore = Math.ceil((gptConfig.gradeBoundaries[gradeIndex] * maxScore) / 100); - - return score - gradeScore; -} - -export function RelativeGradeDelta( - game: Game, - playtype: Playtypes[Game], - score: number, - percent: number, - grade: Grades[GPT], - relativeIndex: integer -) { - const gptConfig = GetGamePTConfig(game, playtype); - - const nextGradeIndex = gptConfig.grades.indexOf(grade) + relativeIndex; - - if (nextGradeIndex < 0 || nextGradeIndex >= gptConfig.grades.length) { - return null; - } - - return { - grade: gptConfig.grades[nextGradeIndex], - delta: AbsoluteGradeDelta(game, playtype, score, percent, nextGradeIndex), - }; -} - -function WrapGrade(grade: string) { - if (grade.endsWith("-") || grade.endsWith("+")) { - return `(${grade})`; - } - - return grade; -} - -export function GenericFormatGradeDelta( - game: Game, - playtype: Playtypes[Game], - score: number, - percent: number, - grade: Grades[GPT], - formatNumFn: (n: number) => number = (s) => s -): { - lower: string; - upper?: string; - closer: "lower" | "upper"; -} { - const upper = RelativeGradeDelta(game, playtype, score, percent, grade, 1); - const lower = AbsoluteGradeDelta(game, playtype, score, percent, grade); - - const formatLower = `${WrapGrade(grade)}+${formatNumFn(lower)}`; - - if (!upper) { - return { - lower: formatLower, - closer: "lower", - }; - } - - const formatUpper = `${WrapGrade(upper.grade)}${formatNumFn(upper.delta)}`; - - return { - lower: formatLower, - upper: formatUpper, - closer: upper.delta + lower < 0 ? "lower" : "upper", - }; -} diff --git a/client/src/util/misc.ts b/client/src/util/misc.ts index 74a4d91d3..67e28ef16 100644 --- a/client/src/util/misc.ts +++ b/client/src/util/misc.ts @@ -128,7 +128,7 @@ export function EscapeStringRegexp(string: string) { return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d"); } -export function Reverse(arr: T[]): T[] { +export function Reverse(arr: readonly T[]): T[] { return arr.slice(0).reverse(); } diff --git a/client/src/util/str-to-num.ts b/client/src/util/str-to-num.ts index d45e769ee..9bd7202ee 100644 --- a/client/src/util/str-to-num.ts +++ b/client/src/util/str-to-num.ts @@ -9,7 +9,7 @@ export function HumanFriendlyStrToEnumIndex( const conf = GetScoreMetricConf(gptConfig, enumMetric); - if (conf.type !== "ENUM") { + if (conf?.type !== "ENUM") { return () => 0; // wut }