From 5cbb2d5f8ab2c90e744f5e8a653eba4e2beeb70f Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Sun, 15 Jan 2023 00:57:06 +0000 Subject: [PATCH] checkpoint: insanity ascends (35) 127 errors to go --- client/package.json | 3 +- .../src/app/pages/dashboard/DashboardPage.tsx | 4 +- .../games/_game/_playtype/GPTChartPage.tsx | 50 +- .../games/_game/_playtype/GPTDevInfo.tsx | 11 +- .../_game/_playtype/GPTLeaderboardsPage.tsx | 37 +- .../games/_game/_playtype/GPTSongsPage.tsx | 15 +- .../_game/_playtype/LeaderboardsPage.tsx | 10 +- .../games/_game/_playtype/OverviewPage.tsx | 4 +- .../games/_game/_playtype/ScoresPage.tsx | 20 +- .../games/_game/_playtype/SessionsPage.tsx | 2 +- .../_game/_playtype/UGPTSettingsPage.tsx | 25 +- .../_playtype/folders/FolderComparePage.tsx | 15 - .../_playtype/folders/FolderSelectPage.tsx | 4 +- .../_playtype/folders/SpecificFolderPage.tsx | 523 ++---------------- client/src/components/activity/Activity.tsx | 32 +- client/src/components/game/ClassBadge.tsx | 48 +- .../src/components/game/ScoreLeaderboard.tsx | 4 +- .../game/charts/ChartInfoFormat.tsx | 69 ++- .../game/folder/FolderInfoHeader.tsx | 6 +- .../components/game/folder/FolderMinimap.tsx | 120 ++-- .../components/game/songs/SongInfoFormat.tsx | 2 +- client/src/components/gpt-utils/GPTUtils.tsx | 6 +- .../gpt-utils/tools/BMSSieglindeInfoTool.tsx | 10 +- .../sessions/SessionFolderRaiseBreakdown.tsx | 3 +- .../components/sessions/SessionOverview.tsx | 8 +- .../sessions/SessionRaiseBreakdown.tsx | 248 ++++----- .../tables/cells/BMSOrPMSDifficultyCell.tsx | 14 +- .../tables/cells/BMSOrPMSLampCell.tsx | 11 +- .../src/components/tables/cells/BPICell.tsx | 29 +- .../src/components/tables/cells/DeltaCell.tsx | 4 +- .../tables/cells/DifficultyCell.tsx | 4 +- .../components/tables/cells/IIDXLampCell.tsx | 30 +- .../tables/cells/ITGDifficultyCell.tsx | 4 +- .../tables/cells/JubeatScoreCell.tsx | 20 +- .../src/components/tables/cells/LampCell.tsx | 9 +- .../tables/cells/MillionsScoreCell.tsx | 20 +- .../components/tables/cells/PBCompareCell.tsx | 47 +- .../components/tables/cells/PopnLampCell.tsx | 24 +- .../tables/cells/RatingSystemPart.tsx | 81 +++ .../components/tables/cells/SDVXLampCell.tsx | 7 +- .../src/components/tables/cells/ScoreCell.tsx | 34 +- .../tables/cells/ScoreCellLarge.tsx | 12 - .../tables/cells/TierlistInfoPart.tsx | 65 --- .../tables/cells/USCDifficultyCell.tsx | 4 +- .../src/components/tables/cells/VF6Cell.tsx | 25 +- .../components/SelectableCompareType.tsx | 12 +- .../tables/components/SelectableRating.tsx | 23 +- .../tables/dropdowns/PBDropdown.tsx | 61 +- .../tables/dropdowns/ScoreDropdown.tsx | 12 +- .../components/BMSScoreDropdownParts.tsx | 4 +- .../components/IIDXScoreDropdownParts.tsx | 10 +- .../dropdowns/components/JudgementTable.tsx | 9 +- .../tables/dropdowns/components/PBCompare.tsx | 10 +- .../components/tables/folders/FolderTable.tsx | 12 +- .../tables/game-core-cells/BMSCoreCells.tsx | 4 +- .../game-core-cells/CHUNITHMCoreCells.tsx | 9 +- .../game-core-cells/GitadoraCoreCells.tsx | 3 +- .../tables/game-core-cells/IIDXCoreCells.tsx | 9 +- .../tables/game-core-cells/ITGCoreCells.tsx | 3 +- .../game-core-cells/JubeatCoreCells.tsx | 4 +- .../game-core-cells/MaimaiDXCoreCells.tsx | 3 +- .../game-core-cells/MusecaCoreCells.tsx | 13 +- .../tables/game-core-cells/PMSCoreCells.tsx | 4 +- .../tables/game-core-cells/PopnCoreCells.tsx | 7 +- .../tables/game-core-cells/SDVXCoreCells.tsx | 7 +- .../tables/game-core-cells/ScoreCoreCells.tsx | 38 +- .../tables/game-core-cells/WACCACoreCells.tsx | 9 +- .../components/tables/headers/ChartHeader.tsx | 60 +- .../components/tables/headers/GameHeaders.tsx | 160 ++---- .../history-scores/HistoryScoreTable.tsx | 6 +- client/src/components/tables/pbs/PBTable.tsx | 16 +- .../tables/rivals/ComparePBsTable.tsx | 78 ++- .../tables/rivals/RivalChartTable.tsx | 18 +- .../components/tables/scores/ScoreTable.tsx | 18 +- .../seeds/SeedsBMSCourseLookupTable.tsx | 4 +- .../tables/sessions/GenericSessionTable.tsx | 10 +- .../targets/AddNewGoalForQuestModal.tsx | 21 +- .../components/targets/AddNewQuestModal.tsx | 4 +- .../components/targets/SetNewGoalModal.tsx | 4 +- .../src/components/user/UGPTRankingData.tsx | 6 +- .../src/components/user/UGPTStatShowcase.tsx | 59 +- .../src/components/user/UGPTStatsOverview.tsx | 29 +- client/src/components/util/useBucket.tsx | 6 +- .../src/components/util/useLUGPTSettings.tsx | 8 +- .../components/util/usePreferredRanking.tsx | 4 +- .../src/components/util/useScoreRatingAlg.tsx | 32 +- client/src/context/UserSettingsContext.tsx | 10 +- client/src/lib/game-implementations.ts | 414 ++++++++++++++ client/src/lib/games/_util.ts | 27 + client/src/lib/games/bms-pms.ts | 58 ++ client/src/lib/games/gitadora.ts | 86 +++ client/src/lib/games/iidx.ts | 123 ++++ client/src/lib/games/sdvx-usc.ts | 71 +++ client/src/lib/types.ts | 79 +++ client/src/types/api-returns.ts | 32 +- client/src/types/seeds.ts | 4 +- client/src/types/tables.ts | 2 +- client/src/util/data.ts | 14 +- client/src/util/misc.ts | 97 +--- client/src/util/str-to-num.ts | 48 +- client/src/util/tables/create-search.ts | 76 ++- client/src/util/tierlist.ts | 73 --- client/tsconfig.json | 2 +- common/src/config/game-support/_common.ts | 7 +- common/src/config/game-support/bms.ts | 31 +- common/src/config/game-support/chunithm.ts | 3 +- common/src/config/game-support/gitadora.ts | 3 +- common/src/config/game-support/iidx.ts | 10 +- common/src/config/game-support/itg.ts | 17 +- common/src/config/game-support/jubeat.ts | 5 +- common/src/config/game-support/maimai-dx.ts | 3 +- common/src/config/game-support/museca.ts | 3 +- common/src/config/game-support/pms.ts | 29 +- common/src/config/game-support/popn.ts | 5 +- common/src/config/game-support/sdvx.ts | 7 +- common/src/config/game-support/usc.ts | 5 +- common/src/config/game-support/wacca.ts | 3 +- common/src/lib/schemas.ts | 2 +- common/src/types/documents.ts | 2 +- common/src/types/game-config.ts | 2 +- common/src/types/metrics.ts | 6 +- common/src/utils/util.ts | 54 +- .../scripts/test/match-type.test.ts | 2 +- .../src/game-implementations/games/_common.ts | 18 +- .../lib/game-settings/create-game-settings.ts | 2 +- server/src/lib/showcase/get-stats.test.ts | 2 +- .../_game/_playtype/settings/router.test.ts | 10 +- .../games/_game/_playtype/settings/router.ts | 6 +- .../_game/_playtype/showcase/router.test.ts | 4 +- server/src/test-utils/test-data.ts | 2 +- staging | 1 + 131 files changed, 2106 insertions(+), 1886 deletions(-) create mode 100644 client/src/components/tables/cells/RatingSystemPart.tsx delete mode 100644 client/src/components/tables/cells/ScoreCellLarge.tsx delete mode 100644 client/src/components/tables/cells/TierlistInfoPart.tsx create mode 100644 client/src/lib/game-implementations.ts create mode 100644 client/src/lib/games/_util.ts create mode 100644 client/src/lib/games/bms-pms.ts create mode 100644 client/src/lib/games/gitadora.ts create mode 100644 client/src/lib/games/iidx.ts create mode 100644 client/src/lib/games/sdvx-usc.ts create mode 100644 client/src/lib/types.ts delete mode 100644 client/src/util/tierlist.ts create mode 160000 staging diff --git a/client/package.json b/client/package.json index 4caebd8f6..bb6d8080e 100644 --- a/client/package.json +++ b/client/package.json @@ -7,7 +7,8 @@ "start": "vite", "build": "vite build", "lint": "eslint src --fix", - "preview": "vite build && vite preview" + "preview": "vite build && vite preview", + "typecheck": "tsc -b" }, "browserslist": { "production": [ diff --git a/client/src/app/pages/dashboard/DashboardPage.tsx b/client/src/app/pages/dashboard/DashboardPage.tsx index 4ccb3b85f..2144519fa 100644 --- a/client/src/app/pages/dashboard/DashboardPage.tsx +++ b/client/src/app/pages/dashboard/DashboardPage.tsx @@ -199,8 +199,8 @@ function UserGameStatsInfo({ user }: { user: UserDocument }) { const gameConfig = GetGameConfig(a.game); return ( - gameConfig.validPlaytypes.indexOf(a.playtype) - - gameConfig.validPlaytypes.indexOf(b.playtype) + gameConfig.playtypes.indexOf(a.playtype) - + gameConfig.playtypes.indexOf(b.playtype) ); } diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx index cc062327f..6805b69d5 100644 --- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx @@ -4,15 +4,20 @@ import { SelectRightChart } from "util/misc"; import { MillisToSince } from "util/time"; import useSetSubheader from "components/layout/header/useSetSubheader"; import Card from "components/layout/page/Card"; -import LampCell from "components/tables/cells/LampCell"; -import ScoreCell from "components/tables/cells/ScoreCell"; import MiniTable from "components/tables/components/MiniTable"; +import TargetInfo from "components/tables/dropdowns/components/TargetInfo"; +import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells"; import PBTable from "components/tables/pbs/PBTable"; import ProfilePicture from "components/user/ProfilePicture"; import ApiError from "components/util/ApiError"; +import Divider from "components/util/Divider"; import Icon from "components/util/Icon"; import Loading from "components/util/Loading"; import Muted from "components/util/Muted"; +import SelectLinkButton from "components/util/SelectLinkButton"; +import useApiQuery from "components/util/query/useApiQuery"; +import useLUGPTSettings from "components/util/useLUGPTSettings"; +import { TargetsContext } from "context/TargetsContext"; import { UserContext } from "context/UserContext"; import React, { useContext, useMemo, useState } from "react"; import { Col, Row } from "react-bootstrap"; @@ -23,10 +28,10 @@ import { FormatDifficulty, GetGameConfig, GetGamePTConfig, - integer, PBScoreDocument, - UserDocument, SongDocument, + UserDocument, + integer, } from "tachi-common"; import { ChartPBLeaderboardReturn, @@ -36,12 +41,6 @@ import { } from "types/api-returns"; import { GamePT, SetState } from "types/react"; import { PBDataset } from "types/tables"; -import useLUGPTSettings from "components/util/useLUGPTSettings"; -import TargetInfo from "components/tables/dropdowns/components/TargetInfo"; -import useApiQuery from "components/util/query/useApiQuery"; -import Divider from "components/util/Divider"; -import { TargetsContext } from "context/TargetsContext"; -import SelectLinkButton from "components/util/SelectLinkButton"; // This component forms a wrapper around the Real GPT Chart Page // which handles the case where activeChart == null. @@ -206,7 +205,7 @@ function InternalGPTChartPage({ )} - + @@ -375,10 +374,12 @@ function TopShowcase({ data, user, userMap, + chart, }: { data: ChartPBData; user: UserDocument | null; userMap: Map; + chart: ChartDocument; }) { // We have a couple of conditions. // User is #1: col-12 #1, @@ -392,7 +393,7 @@ function TopShowcase({ if (user?.id === bestPlay.userID) { return ( - + ); } @@ -401,7 +402,7 @@ function TopShowcase({ return ( <> - + @@ -417,16 +418,26 @@ function TopShowcase({ return ( <> - + - + ); } -function PlayCard({ pb, user, name }: { pb: PBScoreDocument; user: UserDocument; name: string }) { +function PlayCard({ + pb, + user, + name, + chart, +}: { + pb: PBScoreDocument; + user: UserDocument; + name: string; + chart: ChartDocument; +}) { return ( @@ -447,12 +458,7 @@ function PlayCard({ pb, user, name }: { pb: PBScoreDocument; user: UserDocument; - - - - - - +
diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx index 90867f90a..aca70e2fa 100644 --- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx @@ -4,8 +4,7 @@ import Card from "components/layout/page/Card"; import MiniTable from "components/tables/components/MiniTable"; import DebugContent from "components/util/DebugContent"; import React from "react"; -import { FormatGame, GetGameConfig, GetGamePTConfig, IDStrings, gameClasses } from "tachi-common"; -import { GameClassSets } from "tachi-common/game-classes"; +import { Classes, FormatGame, GetGameConfig, GetGamePTConfig, GPTString } from "tachi-common"; import { GamePT } from "types/react"; export default function GPTDevInfo({ game, playtype }: GamePT) { @@ -28,16 +27,16 @@ export default function GPTDevInfo({ game, playtype }: GamePT) {
- {Object.entries(gptConfig.classHumanisedFormat).map(([classSet, value]) => ( + {Object.entries(gptConfig.classes).map(([classSet, conf]) => (
- {value.map((e, i) => ( + {conf.values.map((e) => ( {e.id} diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx index 0328efefd..210ad0684 100644 --- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx @@ -17,7 +17,7 @@ import SelectButton from "components/util/SelectButton"; import { useProfileRatingAlg } from "components/util/useScoreRatingAlg"; import React, { useState } from "react"; import { Col, Form, Row } from "react-bootstrap"; -import { FormatGame, GetGameConfig, GetGamePTConfig } from "tachi-common"; +import { AnyProfileRatingAlg, FormatGame, GetGameConfig, GetGamePTConfig } from "tachi-common"; import { UserLeaderboardReturns } from "types/api-returns"; import { GamePT } from "types/react"; import { UGSDataset } from "types/tables"; @@ -69,9 +69,9 @@ function ProfileLeaderboard({ game, playtype }: GamePT) { const [alg, setAlg] = useState(defaultAlg); const SelectComponent = - gptConfig.profileRatingAlgs.length > 1 ? ( + Object.keys(gptConfig.profileRatingAlgs).length > 1 ? ( setAlg(e.target.value as any)}> - {gptConfig.profileRatingAlgs.map((e) => ( + {Object.keys(gptConfig.profileRatingAlgs).map((e) => ( @@ -125,7 +125,7 @@ function ProfileLeaderboard({ game, playtype }: GamePT) { headers={[ ["Ranking", "Rank", NumericSOV((x) => x.__related.index)], ["User", "User", StrSOV((x) => x.__related.user.username)], - ...gptConfig.profileRatingAlgs.map( + ...(Object.keys(gptConfig.profileRatingAlgs) as Array).map( (e) => [ UppercaseFirst(e), @@ -139,11 +139,13 @@ function ProfileLeaderboard({ game, playtype }: GamePT) { - {gptConfig.profileRatingAlgs.map((e) => ( + {( + Object.keys(gptConfig.profileRatingAlgs) as Array + ).map((e) => ( {r.ratings[e] - ? gptConfig.profileRatingAlgFormatters[e] - ? gptConfig.profileRatingAlgFormatters[e]!(r.ratings[e]!) + ? gptConfig.profileRatingAlgs[e].formatter + ? gptConfig.profileRatingAlgs[e].formatter!(r.ratings[e]!) : r.ratings[e]!.toFixed(2) : "No Data."} @@ -152,15 +154,18 @@ function ProfileLeaderboard({ game, playtype }: GamePT) { {Object.keys(r.classes).length === 0 ? ( None ) : ( - Object.entries(r.classes).map(([k, v]) => ( - - )) + Object.entries(r.classes).map( + ([k, v]) => + v && ( + + ) + ) )} diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx index 66c6550e9..4e6aa61ca 100644 --- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx @@ -20,6 +20,7 @@ import { SongDocument, } from "tachi-common"; import { GamePT } from "types/react"; +import { CascadingRatingValue } from "components/tables/headers/ChartHeader"; export default function GPTSongsPage({ game, playtype }: GamePT) { useSetSubheader( @@ -90,19 +91,7 @@ function SearchSongsTable({ game, playtype, search }: { search: string } & GameP entryName="Charts" dataset={dataset} headers={[ - [ - "Chart", - "Chart", - NumericSOV((chart) => { - for (const tierlist of gptConfig.tierlists) { - if (chart.tierlistInfo[tierlist]) { - return chart.tierlistInfo[tierlist]!.value; - } - } - - return chart.levelNum; - }), - ], + ["Chart", "Chart", (a, b) => CascadingRatingValue(game, a, b)], ["Song Title", "Song", StrSOV((x) => x.__related.song.title)], ["Playcount", "Playcount", NumericSOV((x) => x.__playcount)], ]} 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 4cba7d98a..d2e8eb228 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 @@ -16,10 +16,10 @@ import { FormatGame, GetGameConfig, GetGamePTConfig, - IDStrings, + GPTString, integer, UserDocument, - ProfileRatingLookup, + ProfileRatingAlgorithms, UserGameStats, } from "tachi-common"; import { GameClassSets } from "tachi-common/game-classes"; @@ -84,8 +84,8 @@ function LeaderboardsPageContent({ }: { reqUser: UserDocument; data: LeaderboardsData; - alg: ProfileRatingLookup[IDStrings]; - setAlg: SetState; + alg: ProfileRatingAlgorithms[GPTString]; + setAlg: SetState; } & GamePT) { const { stats, leaderboard } = data; @@ -142,7 +142,7 @@ function LeaderboardsPageContent({ diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx index d52c6f9bf..de4988691 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx @@ -167,7 +167,7 @@ function UserHistory({ setRating(e.target.value as keyof UserGameStats["ratings"]) } > - {gptConfig.profileRatingAlgs.map((e) => ( + {Object.keys(gptConfig.profileRatingAlgs).map((e) => ( @@ -219,7 +219,7 @@ function UserHistory({ setRating(e.target.value as keyof UserGameStats["ratings"]) } > - {gptConfig.profileRatingAlgs.map((e) => ( + {Object.keys(gptConfig.profileRatingAlgs).map((e) => ( diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx index ce0c66ec5..755cb4257 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx @@ -18,10 +18,10 @@ import { GamePTConfig, GetGameConfig, GetGamePTConfig, - IDStrings, + GPTString, PBScoreDocument, UserDocument, - ScoreCalculatedDataLookup, + ScoreRatingAlgorithms, ScoreDocument, SongDocument, UnsuccessfulAPIResponse, @@ -77,7 +77,7 @@ export default function ScoresPage({ <> - {gptConfig.scoreRatingAlgs.length > 1 && ( + {Object.keys(gptConfig.scoreRatingAlgs).length > 1 && ( )} ; + alg: ScoreRatingAlgorithms[GPTString]; + setAlg: SetState; }) { return (
@@ -132,7 +132,7 @@ function AlgSelector({ value={alg} onChange={(e) => setAlg(e.target.value as any)} > - {gptConfig.scoreRatingAlgs.map((e) => ( + {Object.keys(gptConfig.scoreRatingAlgs).map((e) => ( ))} @@ -172,7 +172,7 @@ function PBsOverview({ url: string; indexCol?: boolean; showPlaycount?: boolean; - alg?: ScoreCalculatedDataLookup[IDStrings]; + alg?: ScoreRatingAlgorithms[GPTString]; } & GamePT) { const [search, setSearch] = useState(""); @@ -212,9 +212,9 @@ function PBsOverview({ function FormatData< D extends PBScoreDocument | ScoreDocument, - I extends IDStrings = IDStrings, + GPT extends GPTString = GPTString, G extends Game = Game ->(d: D[], songs: SongDocument[], charts: ChartDocument[], reqUser: UserDocument) { +>(d: D[], songs: SongDocument[], charts: ChartDocument[], reqUser: UserDocument) { const songMap = new Map(); const chartMap = new Map(); @@ -266,7 +266,7 @@ function PBsSearch({ }: { reqUser: UserDocument; search: string; - alg?: ScoreCalculatedDataLookup[IDStrings]; + alg?: ScoreRatingAlgorithms[GPTString]; } & GamePT) { const { data, error } = useFetchPBs( `/users/${reqUser.id}/games/${game}/${playtype}/pbs?search=${search}`, diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx index 4b1894d69..8c67eeb7c 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx @@ -135,7 +135,7 @@ function SearchSessionsTable({ __related: { index: i, }, - })); + })) as SessionDataset; } ); 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 b8d8e013d..3165ac51b 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 @@ -27,7 +27,7 @@ import { GetGamePTConfig, ShowcaseStatDetails, TableDocument, - UGPTSettings, + UGPTSettingsDocument, UserDocument, } from "tachi-common"; import { SetState, UGPT } from "types/react"; @@ -109,7 +109,8 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { // eslint-disable-next-line @typescript-eslint/no-explicit-any gameSpecific: settings!.preferences.gameSpecific as any, defaultTable: settings!.preferences.defaultTable, - scoreBucket: settings!.preferences.scoreBucket ?? gptConfig.scoreBucket, + preferredDefaultEnum: + settings!.preferences.preferredDefaultEnum ?? gptConfig.preferredDefaultEnum, preferredRanking: settings!.preferences.preferredRanking ?? "global", }, onSubmit: async (values) => { @@ -129,7 +130,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { if (rj.success) { setLoggedInData({ ...loggedInData, - settings: deepmerge(settings as UGPTSettings, { preferences: values }), + settings: deepmerge(settings as UGPTSettingsDocument, { preferences: values }), }); } }, @@ -153,7 +154,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { return (
- {gptConfig.scoreRatingAlgs.length > 1 && ( + {Object.keys(gptConfig.scoreRatingAlgs).length > 1 && ( Preferred Score Algorithm - {gptConfig.scoreRatingAlgs.map((e) => ( + {Object.keys(gptConfig.scoreRatingAlgs).map((e) => ( ))} @@ -172,7 +173,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { )} - {gptConfig.sessionRatingAlgs.length > 1 && ( + {Object.keys(gptConfig.sessionRatingAlgs).length > 1 && ( Preferred Session Algorithm - {gptConfig.sessionRatingAlgs.map((e) => ( + {Object.keys(gptConfig.sessionRatingAlgs).map((e) => ( ))} @@ -191,7 +192,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { )} - {gptConfig.profileRatingAlgs.length > 1 && ( + {Object.keys(gptConfig.profileRatingAlgs).length > 1 && ( Preferred Profile Algorithm - {gptConfig.profileRatingAlgs.map((e) => ( + {Object.keys(gptConfig.profileRatingAlgs).map((e) => ( ))} @@ -214,8 +215,8 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) { Preferred Folder Info @@ -408,7 +409,7 @@ function ShowcaseForm({ reqUser, game, playtype }: UGPT) { const [show, setShow] = useState(false); const SaveChanges = async () => { - const r = await APIFetchV1( + const r = await APIFetchV1( `/users/${reqUser.id}/games/${game}/${playtype}/showcase`, { method: "PUT", 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 0b4d7fd74..1983b2f16 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 @@ -125,8 +125,6 @@ function FolderCompare({ withUser: UserDocument; folder: FolderDocument; } & GamePT) { - const gptConfig = GetGamePTConfig(game, playtype); - const { data: baseData, error: baseError } = useApiQuery( `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folder.folderID}` ); @@ -136,11 +134,6 @@ function FolderCompare({ ); const [shouldIncludeNotPlayed, setShouldIncludeNotPlayed] = useState(false); - const [shouldESD, setShouldESD] = useState(gptConfig.supportsESD); - - useEffect(() => { - setShouldESD(gptConfig.supportsESD); - }, [gptConfig]); const dataset = useMemo(() => { // i *LOVE* the rules of hooks! they're so convenient! @@ -197,13 +190,6 @@ function FolderCompare({ onChange={() => setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)} label="Include charts without plays?" /> - {gptConfig.supportsESD && ( - setShouldESD(!shouldESD)} - label="Sort score differences intelligently? (i.e. Treat the difference between 70% and 71% as smaller than the difference between 99% and 100%)" - /> - )} 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 6edea59df..350fdcd6b 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 @@ -83,9 +83,9 @@ export function FolderInfoComponent({ }: UGPT & { folder: FolderDocument; folderStats: FolderStatsInfo }) { const gptConfig = GetGamePTConfig(game, playtype); - const scoreBucket = useBucket(game, playtype); + const preferredDefaultEnum = useBucket(game, playtype); - const [elements, setElements] = useState<"grade" | "lamp">(scoreBucket); + const [elements, setElements] = useState<"grade" | "lamp">(preferredDefaultEnum); const base = useUGPTBase({ reqUser, game, playtype }); diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx index 3fcbfc4ea..f15f3213a 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -1,47 +1,35 @@ import { ONE_DAY } from "util/constants/time"; import { CreateChartIDMap, CreateChartLink, CreateSongMap } from "util/data"; -import { DistinctArr, NO_OP } from "util/misc"; import { NumericSOV, StrSOV } from "util/sorts"; -import { GetScaleAchievedFn } from "util/tierlist"; import { FormatDate, FormatTime } from "util/time"; -import { ChangeOpacity } from "util/color-opacity"; import FolderInfoHeader from "components/game/folder/FolderInfoHeader"; -import QuickTooltip from "components/layout/misc/QuickTooltip"; import Card from "components/layout/page/Card"; import FolderTable from "components/tables/folders/FolderTable"; import ApiError from "components/util/ApiError"; import Divider from "components/util/Divider"; import Icon from "components/util/Icon"; import Loading from "components/util/Loading"; -import Muted from "components/util/Muted"; -import useApiQuery from "components/util/query/useApiQuery"; -import ReferToUser from "components/util/ReferToUser"; import SelectLinkButton from "components/util/SelectLinkButton"; +import useApiQuery from "components/util/query/useApiQuery"; import useUGPTBase from "components/util/useUGPTBase"; -import { useFormik } from "formik"; import React, { useEffect, useMemo, useState } from "react"; -import { Col, Form, Row } from "react-bootstrap"; +import { Form } from "react-bootstrap"; import { Link, Route, Switch, useParams } from "react-router-dom"; import { ChartDocument, - ChartTierlistInfo, FormatDifficulty, - FormatDifficultyShort, Game, GetGamePTConfig, - GPTTierlists, - IDStrings, - integer, - UserDocument, + GetScoreEnumConfs, + Playtype, ScoreDocument, SongDocument, - Playtype, - COLOUR_SET, + UserDocument, + integer, } from "tachi-common"; import { UGPTFolderReturns } from "types/api-returns"; import { FolderDataset } from "types/tables"; -import MiniTable from "components/tables/components/MiniTable"; -import DifficultyCell from "components/tables/cells/DifficultyCell"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; import FolderComparePage from "./FolderComparePage"; import FolderQuestsPage from "./FolderQuestsPage"; @@ -58,8 +46,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}` ); - const gptConfig = GetGamePTConfig(game, playtype); - const folderDataset = useMemo(() => { if (!data) { return null; @@ -124,15 +110,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { Normal View - {gptConfig.tierlists.length !== 0 && - // temp: tierlist view sucks for BMS and PMS - game !== "bms" && - game !== "pms" && ( - - - Tierlist View - - )} Timeline View @@ -155,15 +132,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { - - - (gptConfig.scoreBucket); - const [value, setValue] = useState( - type === "grade" - ? gptConfig.grades.indexOf(gptConfig.clearGrade) - : gptConfig.lamps.indexOf(gptConfig.clearLamp) - ); + const enumConfs = GetScoreEnumConfs(gptConfig); + + const [selectedEnum, setSelectedEnum] = useState(gptConfig.preferredDefaultEnum); + const [enumConf, setEnumConf] = useState>(enumConfs[selectedEnum]!); + + const [value, setValue] = useState(enumConf.minimumRelevantValue); useEffect(() => { - setValue( - type === "grade" - ? gptConfig.grades.indexOf(gptConfig.clearGrade) - : gptConfig.lamps.indexOf(gptConfig.clearLamp) - ); - }, [type]); + setValue(enumConfs[selectedEnum]!.minimumRelevantValue); + setEnumConf(enumConfs[selectedEnum]!); + }, [selectedEnum]); return ( <> @@ -225,36 +190,33 @@ function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID:
setType(e.target.value as "grade" | "lamp")} + value={selectedEnum} + onChange={(e) => setSelectedEnum(e.target.value)} > - - + {Object.keys(enumConfs).map((e) => ( + + ))}
setValue(Number(e.target.value))} + onChange={(e) => setValue(e.target.value)} > - {type === "grade" - ? gptConfig.grades.map((e, i) => ( - - )) - : gptConfig.lamps.map((e, i) => ( - - ))} + {enumConf.values + .slice(enumConf.values.indexOf(enumConf.minimumRelevantValue)) + .map((e) => ( + + ))}
- + ); } @@ -264,19 +226,19 @@ function TimelineMain({ game, playtype, folderID, - type, + enumMetric: enumMetric, value, }: Props & { folderID: string; - type: "grade" | "lamp"; - value: integer; + enumMetric: string; + value: string; }) { const { data, error } = useApiQuery<{ scores: ScoreDocument[]; songs: SongDocument[]; charts: ChartDocument[]; }>( - `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${type}` + `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${enumMetric}` ); if (error) { @@ -411,420 +373,3 @@ function TimelineElement({
); } - -type InfoProps = Props & { - folderDataset: FolderDataset; - data: UGPTFolderReturns; -}; - -function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps) { - const gptConfig = GetGamePTConfig(game, playtype); - - const formik = useFormik({ - initialValues: { - __hideAchieved: false, - ...Object.fromEntries(gptConfig.tierlists.map((e) => [e, true])), - } as { __hideAchieved: boolean } & Partial>, - onSubmit: NO_OP, - }); - - const tierlistInfo = useMemo( - () => FolderDatasetToTierlistInfo(folderDataset, game, playtype, formik.values), - [formik.values] - ); - - const dataMap = CreateChartIDMap(folderDataset); - - return ( - - - - Here you can select what tierlist blocks to show! - - {gptConfig.tierlists.length > 1 && - gptConfig.tierlists.map((e) => ( - <> - - {gptConfig.tierlistDescriptions[e]} - - ))} - - - Hide achieved elements of the tierlist. - - - - - - - - - - ); -} - -const NO_TIERLIST_DATA_VALUE = { - text: "No Tierlist Info", - value: 0, -}; - -function TierlistInfoLadder({ - tierlistInfo, - dataMap, - game, - playtype, - reqUser, -}: { - tierlistInfo: NullableTierlistInfo[]; - dataMap: Map; - game: Game; - playtype: Playtype; - reqUser: UserDocument; -}) { - const buckets: TierlistInfo[][] = useMemo(() => { - const buckets: TierlistInfo[][] = []; - let currentBucket: TierlistInfo[] = []; - const noDataBucket: TierlistInfo[] = []; - - let lastValue; - for (const tl of tierlistInfo) { - if (tl.data === null) { - noDataBucket.push({ - achieved: tl.achieved, - chartID: tl.chartID, - data: NO_TIERLIST_DATA_VALUE, - key: tl.key, - }); - continue; - } - - // no longer nullable - const tlx = tl as TierlistInfo; - - if (lastValue && tlx.data.value !== lastValue) { - buckets.push(currentBucket); - currentBucket = [tlx]; - } else { - currentBucket.push(tlx); - } - lastValue = tlx.data.value; - } - - if (currentBucket.length) { - buckets.push(currentBucket); - } - - buckets.push(noDataBucket); - - return buckets; - }, [tierlistInfo]); - - const gptConfig = GetGamePTConfig(game, playtype); - - for (const bucket of buckets) { - bucket.sort(NumericSOV((x) => gptConfig.tierlists.indexOf(x.key), true)); - } - - if (tierlistInfo.length === 0) { - return Got no tierlist data to show you!; - } - - return ( - - {buckets - .filter((e) => e.length > 0) - .map((bucket, i) => ( - - - {bucket[0].data!.value} ( - {DistinctArr( - bucket.map((e) => - gptConfig.tierlists.length === 1 - ? e.data.text - : `${e.key}: ${e.data.text}` - ) - ).join(", ")} - ) - - - - - ))} - - ); -} - -function TierlistBucket({ - bucket, - dataMap, - game, - reqUser, -}: { - dataMap: Map; - game: Game; - playtype: Playtype; - reqUser: UserDocument; - bucket: TierlistInfo[]; -}) { - // xs view is tabular - if (window.screen.width <= 576) { - return ( - - {bucket.map((tierlistInfo, i) => ( - - ))} - - ); - } - - return ( - <> - {bucket.map((tierlistInfo, i) => ( - - ))} - - ); -} - -function TierlistInfoBucketValues({ - tierlistInfo, - game, - dataMap, - bucket, - i, - reqUser, -}: { - tierlistInfo: TierlistInfo; - bucket: TierlistInfo[]; - dataMap: Map; - game: Game; - i: integer; - reqUser: UserDocument; -}) { - const data = dataMap.get(tierlistInfo.chartID)!; - - const lastKey = bucket[i - 1]; - - let statusClass; - - switch (tierlistInfo.achieved) { - case AchievedStatuses.ACHIEVED: - statusClass = "achieved"; - break; - case AchievedStatuses.FAILED: - statusClass = "unachieved"; - break; - case AchievedStatuses.NOT_PLAYED: - case AchievedStatuses.SCORE_BASED: - statusClass = ""; - } - - // xs view - if (window.screen.width <= 576) { - return ( - - - - - {data.__related.song.title} - {" "} -
-
- {tierlistInfo.key} ({tierlistInfo.data.text}) - {tierlistInfo.data.individualDifference && ( - - - - )} -
- - - - ); - } - - return ( - <> - {lastKey && lastKey.key !== tierlistInfo.key && } - - - {data.__related.song.title} - {" "} - {FormatDifficultyShort(data, game)} - - {tierlistInfo.key} ({tierlistInfo.data.text}) - {tierlistInfo.data.individualDifference && ( - <> -
- -
- - - - - -
- - )} - - - {" "} - {tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED - ? "Not Played" - : tierlistInfo.achieved === AchievedStatuses.SCORE_BASED - ? data.__related.pb!.scoreData.grade - : data.__related.pb!.scoreData.lamp} - - - - ); -} - -function TierlistInfoCell({ tierlistInfo }: { tierlistInfo: TierlistInfo }) { - let colour; - let text; - - if ( - tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED || - tierlistInfo.achieved === AchievedStatuses.FAILED - ) { - colour = COLOUR_SET.red; - text = "✗"; - } else { - colour = COLOUR_SET.green; - text = "✓"; - } - - return ( - - {text} - - ); -} - -interface NullableTierlistInfo { - chartID: string; - key: GPTTierlists[IDStrings]; - data: ChartTierlistInfo | null; - achieved: AchievedStatuses; -} - -interface TierlistInfo { - chartID: string; - key: GPTTierlists[IDStrings]; - data: ChartTierlistInfo; - achieved: AchievedStatuses; -} - -enum AchievedStatuses { - NOT_PLAYED, - FAILED, - ACHIEVED, - SCORE_BASED, -} - -function FolderDatasetToTierlistInfo( - folderDataset: FolderDataset, - game: Game, - playtype: Playtype, - options: Partial> & { __hideAchieved: boolean } -) { - const tierlistInfo: NullableTierlistInfo[] = []; - - const tierlistKeys: GPTTierlists[IDStrings][] = []; - - for (const k in options) { - if (k === "__hideAchieved") { - continue; - } - - const key = k as GPTTierlists[IDStrings]; - if (options[key]) { - tierlistKeys.push(key); - } - } - - for (const data of folderDataset) { - for (const key of tierlistKeys) { - let achieved: AchievedStatuses; - - if (!data.__related.pb) { - achieved = AchievedStatuses.NOT_PLAYED; - } else { - const fn = GetScaleAchievedFn(game, playtype, key); - - if (fn) { - achieved = fn(data.__related.pb) - ? AchievedStatuses.ACHIEVED - : AchievedStatuses.FAILED; - } else { - achieved = AchievedStatuses.SCORE_BASED; - } - } - - if (options.__hideAchieved && achieved === AchievedStatuses.ACHIEVED) { - continue; - } - - tierlistInfo.push({ - chartID: data.chartID, - key, - data: data.tierlistInfo[key] ?? null, - achieved, - }); - } - } - - return tierlistInfo.sort(NumericSOV((x) => (x.data ? x.data.value : -Infinity), true)); -} diff --git a/client/src/components/activity/Activity.tsx b/client/src/components/activity/Activity.tsx index 54a84fb38..2fb2aa5db 100644 --- a/client/src/components/activity/Activity.tsx +++ b/client/src/components/activity/Activity.tsx @@ -18,7 +18,13 @@ import { UserContext } from "context/UserContext"; import React, { useContext, useEffect, useState } from "react"; import { Button, Col, Row } from "react-bootstrap"; import { Link } from "react-router-dom"; -import { FormatChart, FormatGame, GetGamePTConfig, UserDocument } from "tachi-common"; +import { + FormatChart, + FormatGame, + GetGamePTConfig, + GetScoreEnumConfs, + UserDocument, +} from "tachi-common"; import { ActivityReturn, RecordActivityReturn, SessionReturns } from "types/api-returns"; import { UGPT } from "types/react"; import { ScoreDataset } from "types/tables"; @@ -556,15 +562,21 @@ function SessionShower({ sessionID }: { sessionID: string }) { return false; } - if (e.isNewScore) { - return ( - score.scoreData.lampIndex > gptConfig.lamps.indexOf(gptConfig.clearLamp) || - score.scoreData.gradeIndex > gptConfig.grades.indexOf(gptConfig.clearGrade) - ); - } else if (e.gradeDelta > 0) { - return score.scoreData.gradeIndex > gptConfig.grades.indexOf(gptConfig.clearGrade); - } else if (e.lampDelta > 0) { - return score.scoreData.lampIndex > gptConfig.lamps.indexOf(gptConfig.clearLamp); + const enumMetrics = GetScoreEnumConfs(gptConfig); + + // for all enum metrics, check if this score beats the minimum relevant enum + // and is a raise. + for (const [metric, conf] of Object.entries(enumMetrics)) { + if (!e.isNewScore && e.deltas[metric] <= 0) { + continue; + } + + if ( + // @ts-expect-error its gonna exist buddy + score.scoreData.enumIndexes[metric] > conf.values.indexOf(conf.minimumRelevantValue) + ) { + return true; + } } return false; diff --git a/client/src/components/game/ClassBadge.tsx b/client/src/components/game/ClassBadge.tsx index 9fa46551f..6ad6ad497 100644 --- a/client/src/components/game/ClassBadge.tsx +++ b/client/src/components/game/ClassBadge.tsx @@ -1,59 +1,73 @@ import { UppercaseFirst } from "util/misc"; import QuickTooltip from "components/layout/misc/QuickTooltip"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import React from "react"; import { Badge } from "react-bootstrap"; -import { GetGamePTConfig, IDStrings, integer } from "tachi-common"; -import { GameClassSets } from "tachi-common/game-classes"; +import { Classes, GPTString, GetGPTString, GetGamePTConfig } from "tachi-common"; import { GamePT } from "types/react"; -export default function ClassBadge({ +export default function ClassBadge({ game, playtype, classSet, classValue, showSetOnHover = true, }: { - classSet: GameClassSets[I]; - classValue: integer; + classSet: Classes[GPT]; + classValue: string; showSetOnHover?: boolean; } & GamePT) { - const gptConfig = GetGamePTConfig(game, playtype); + const classStyle = + // @ts-expect-error hepl i'm trapped in a type factory + GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)].classColours[classSet][classValue]; - const data = gptConfig.classHumanisedFormat[classSet][classValue]; + const data = GetGamePTConfig(game, playtype).classes[classSet].values.find( + (e) => e.id === classValue + ); if (!data) { - throw new Error(`Unknown class value ${classSet}:${classValue}`); + return ( + <> + {classSet} {classValue} (messed up!) + + ); } let badgeComponent; - if (data.variant) { + + if (classStyle === null) { badgeComponent = ( - + {data.display} ); - } else if (data.css) { + } else if (typeof classStyle === "string") { badgeComponent = ( - + {data.display} ); } else { badgeComponent = ( - + {data.display} ); } - if (data.mouseover && showSetOnHover) { + if (data.hoverText && showSetOnHover) { return ( - + {badgeComponent} ); - } else if (data.mouseover) { - return {badgeComponent}; + } else if (data.hoverText) { + return {badgeComponent}; } else if (showSetOnHover) { {badgeComponent}; } diff --git a/client/src/components/game/ScoreLeaderboard.tsx b/client/src/components/game/ScoreLeaderboard.tsx index e31b2f7c5..f34145035 100644 --- a/client/src/components/game/ScoreLeaderboard.tsx +++ b/client/src/components/game/ScoreLeaderboard.tsx @@ -38,9 +38,9 @@ export default function ScoreLeaderboard({ const [alg, setAlg] = useState(defaultAlg); const SelectComponent = - gptConfig.scoreRatingAlgs.length > 1 ? ( + Object.keys(gptConfig.scoreRatingAlgs).length > 1 ? ( setAlg(e.target.value as any)}> - {gptConfig.scoreRatingAlgs.map((e) => ( + {Object.keys(gptConfig.scoreRatingAlgs).map((e) => ( diff --git a/client/src/components/game/charts/ChartInfoFormat.tsx b/client/src/components/game/charts/ChartInfoFormat.tsx index 5434fb913..c0a56822a 100644 --- a/client/src/components/game/charts/ChartInfoFormat.tsx +++ b/client/src/components/game/charts/ChartInfoFormat.tsx @@ -1,4 +1,5 @@ import QuickTooltip from "components/layout/misc/QuickTooltip"; +import RatingSystemPart from "components/tables/cells/RatingSystemPart"; import MiniTable from "components/tables/components/MiniTable"; import ApiError from "components/util/ApiError"; import ExternalLink from "components/util/ExternalLink"; @@ -6,12 +7,13 @@ import Icon from "components/util/Icon"; import Loading from "components/util/Loading"; import Muted from "components/util/Muted"; import useApiQuery from "components/util/query/useApiQuery"; -import { UserContext } from "context/UserContext"; import { AllLUGPTStatsContext } from "context/AllLUGPTStatsContext"; +import { UserContext } from "context/UserContext"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import React, { useContext } from "react"; import { Col, Row } from "react-bootstrap"; import { Link } from "react-router-dom"; -import { ChartDocument, FolderDocument, Game, GetGamePTConfig } from "tachi-common"; +import { ChartDocument, FolderDocument, Game, GetGPTString } from "tachi-common"; import { GamePT } from "types/react"; export default function ChartInfoFormat({ @@ -19,9 +21,9 @@ export default function ChartInfoFormat({ game, playtype, }: { chart: ChartDocument } & GamePT) { - const gptConfig = GetGamePTConfig(game, playtype); + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)]; - const withTierlists = gptConfig.tierlists.filter((e) => chart.tierlistInfo[e]); + const ratingSystems = gptImpl.ratingSystems; const { data, error } = useApiQuery( `/games/${game}/${playtype}/charts/${chart.chartID}/folders` @@ -71,27 +73,44 @@ export default function ChartInfoFormat({ - {withTierlists.length !== 0 ? ( - - {withTierlists.map((e) => ( - - {e} - - {chart.tierlistInfo[e]!.text}{" "} - ({chart.tierlistInfo[e]!.value.toFixed(2)}) - {chart.tierlistInfo[e]!.individualDifference && ( - <> -
- - - - - - - )} - - - ))} + + {ratingSystems.length !== 0 ? ( + + {ratingSystems.map((e) => { + // @ts-expect-error bad types + const strV = e.toString(chart); + // @ts-expect-error bad types + const numV = e.toNumber(chart); + + if ( + strV === null || + strV === undefined || + numV === null || + numV === undefined + ) { + return null; + } + + return ( + + {e} + + {strV} ({numV.toFixed(2)}) + {/* @ts-expect-error utterly silly types */} + {e.idvDifference(chart) && ( + <> +
+ + + + + + + )} + + + ); + })}
) : ( No tierlist info. diff --git a/client/src/components/game/folder/FolderInfoHeader.tsx b/client/src/components/game/folder/FolderInfoHeader.tsx index 9426b5f09..92a1d9c35 100644 --- a/client/src/components/game/folder/FolderInfoHeader.tsx +++ b/client/src/components/game/folder/FolderInfoHeader.tsx @@ -21,8 +21,8 @@ export default function FolderInfoHeader({ folderDataset: FolderDataset; folderTitle: string; } & GamePT) { - const scoreBucket = useBucket(game, playtype); - const [view, setView] = useState<"grade" | "lamp">(scoreBucket); + const preferredDefaultEnum = useBucket(game, playtype); + const [view, setView] = useState<"grade" | "lamp">(preferredDefaultEnum); const [breakdownInfo, setBreakdownInfo] = useState<"graph" | "switchboard">("switchboard"); return ( @@ -66,7 +66,7 @@ export default function FolderInfoHeader({ game={game} playtype={playtype} folderDataset={folderDataset} - view={view} + enumMetric={view} /> )} diff --git a/client/src/components/game/folder/FolderMinimap.tsx b/client/src/components/game/folder/FolderMinimap.tsx index bcd65a7ba..11a7d337c 100644 --- a/client/src/components/game/folder/FolderMinimap.tsx +++ b/client/src/components/game/folder/FolderMinimap.tsx @@ -1,34 +1,35 @@ import { ChangeOpacity } from "util/color-opacity"; +import { ONE_WEEK } from "util/constants/time"; import { CreateChartLink } from "util/data"; import { NumericSOV } from "util/sorts"; -import { ONE_WEEK } from "util/constants/time"; import QuickTooltip from "components/layout/misc/QuickTooltip"; +import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells"; import ApiError from "components/util/ApiError"; import Divider from "components/util/Divider"; import Loading from "components/util/Loading"; import useApiQuery from "components/util/query/useApiQuery"; -import React, { useEffect, useMemo, useState } from "react"; -import { Form } from "react-bootstrap"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import React, { useMemo } from "react"; import { Link } from "react-router-dom"; import { - ChartDocument, FormatChart, Game, GamePTConfig, + GetGPTString, GetGamePTConfig, + GetScoreMetricConf, IIDX_LAMPS, - UserDocument, SessionDocument, SessionScoreInfo, + UserDocument, } from "tachi-common"; import { GamePT } from "types/react"; import { FolderDataset } from "types/tables"; -import { SessionReturns } from "types/api-returns"; type Props = { folderDataset: FolderDataset; reqUser: UserDocument; - view: "grade" | "lamp"; + enumMetric: string; } & GamePT; export default function FolderMinimap(props: Props) { @@ -61,7 +62,7 @@ export default function FolderMinimap(props: Props) {
- Sadly, Switchboard view doesn't work very well on mobile at the moment. + Sadly, Switchboard view doesn't work on mobile at the moment.
); @@ -71,40 +72,32 @@ function FolderMinimapMain({ game, playtype, folderDataset, - view, + enumMetric, recentSession, }: Props & { recentSession: { session: SessionDocument; scoreInfo: Array } | null; }) { const gptConfig = GetGamePTConfig(game, playtype); - const [sortAlg, setSortAlg] = useState(view === "grade" ? "score" : "lamp"); - - useEffect(() => { - if (view === "grade") { - setSortAlg("score"); - } else { - setSortAlg("lamp"); - } - }, [view]); - const getterFn = useMemo<(f: FolderDataset[0]) => number | undefined>(() => { - if (sortAlg === "score") { - return (c) => c.__related.pb?.scoreData.percent; - } else if (sortAlg === "lamp") { - return (c) => c.__related.pb?.scoreData.lampIndex; - } else if (sortAlg.startsWith("tierlist:")) { - const v = sortAlg.split("tierlist:")[1]; + const conf = GetScoreMetricConf(gptConfig, enumMetric); - return (c) => c.tierlistInfo[v as keyof ChartDocument["tierlistInfo"]]?.value; + if (!conf) { + return () => 0; // wut } - return () => 0; - }, [sortAlg]); + if (conf.type === "ENUM") { + // @ts-expect-error insane dynamic access + return (c) => c.__related.pb?.scoreData.enumIndexes[enumMetric]; + } + + // @ts-expect-error insane dynamic access + return (c) => c.__related.pb?.scoreData[enumMetric]; + }, [enumMetric]); const sortedDataset = useMemo( () => folderDataset.slice(0).sort(NumericSOV((a) => getterFn(a) ?? -Infinity, true)), - [getterFn, folderDataset, sortAlg] + [getterFn, folderDataset, enumMetric] ); // For the switchboard chart, display a raise icon on stuff the user has recently played. @@ -114,7 +107,19 @@ function FolderMinimapMain({ } return recentSession.scoreInfo - .filter((e) => e.isNewScore || e.gradeDelta > 0 || e.lampDelta > 0) + .filter((e) => { + if (e.isNewScore) { + return true; + } + + for (const v of Object.values(e.deltas)) { + if (v >= 0) { + return true; + } + } + + return false; + }) .map((e) => e.scoreID); }, [recentSession]); @@ -127,37 +132,20 @@ function FolderMinimapMain({ key={d.chartID} data={d} gptConfig={gptConfig} - view={view} + enumMetric={enumMetric} game={game} wasRecent={ (d.__related.pb && - recentlyTouched.some( - (scoreID) => - d.__related.pb!.composedFrom.scorePB === scoreID || - d.__related.pb!.composedFrom.lampPB === scoreID + recentlyTouched.some((scoreID) => + d.__related.pb!.composedFrom.find( + (e) => e.scoreID === scoreID + ) )) ?? false } /> ))}
- - - Sorting switchboard on: - setSortAlg(e.target.value)} - > - {view === "grade" && } - {view === "lamp" && } - {gptConfig.tierlists.map((e) => ( - - ))} - - ); @@ -166,16 +154,18 @@ function FolderMinimapMain({ function MinimapElement({ data, gptConfig, - view, + enumMetric, game, wasRecent, }: { data: FolderDataset[0]; - view: "grade" | "lamp"; + enumMetric: string; gptConfig: GamePTConfig; game: Game; wasRecent: boolean; }) { + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, data.playtype)]; + let icon = "level-up-alt"; // if this user recently cleared mare nectaris @@ -183,7 +173,8 @@ function MinimapElement({ if ( data.chartID === "924bf011fdd8334b609b02e382123f9f5440d16d" && data.__related.pb && - data.__related.pb?.scoreData.lampIndex >= IIDX_LAMPS.EASY_CLEAR + // @ts-expect-error the above chartID guarantees mare nectaris + data.__related.pb?.scoreData.enumIndexes.lamp >= IIDX_LAMPS.EASY_CLEAR ) { icon = "hand-middle-finger"; } @@ -194,8 +185,8 @@ function MinimapElement({ } // @ts-expect-error unhinged dynamic access (i dont care) - return gptConfig[`${view}Colours`][data.__related.pb.scoreData[view]]; - }, [data.__related.pb, gptConfig, view]); + return gptImpl.enumColours[enumMetric][data.__related.pb.scoreData[enumMetric]]; + }, [data.__related.pb, gptConfig, enumMetric]); return ( )} - {data.__related.pb?.scoreData.lamp ?? "Not Played"} - {data.__related.pb && ( - <> -
- {data.__related.pb.scoreData.grade} -
- - {data.__related.pb.scoreData.score} ( - {data.__related.pb.scoreData.percent.toFixed(2)}%) - - + {data.__related.pb ? ( + + ) : ( + Not Played )} } diff --git a/client/src/components/game/songs/SongInfoFormat.tsx b/client/src/components/game/songs/SongInfoFormat.tsx index ffd83a1c0..aaae99d80 100644 --- a/client/src/components/game/songs/SongInfoFormat.tsx +++ b/client/src/components/game/songs/SongInfoFormat.tsx @@ -12,7 +12,7 @@ export default function SongChartInfoFormat({ chart: ChartDocument | null; game: Game; }) { - // lazy casting hack to avoid IDStrings + // lazy casting hack to avoid GPTString const props = { song: tempSong, chart: tempChart, game } as any; const { song, chart } = props; diff --git a/client/src/components/gpt-utils/GPTUtils.tsx b/client/src/components/gpt-utils/GPTUtils.tsx index 4e1f93f50..5362a9352 100644 --- a/client/src/components/gpt-utils/GPTUtils.tsx +++ b/client/src/components/gpt-utils/GPTUtils.tsx @@ -1,4 +1,4 @@ -import { Game, IDStrings, Playtype } from "tachi-common"; +import { Game, GPTString, Playtype } from "tachi-common"; import { GPTUtility } from "types/ugpt"; import { BMSCustomTablesTool } from "./tools/BMSCustomTablesTool"; import { IIDXEamusementExportTool } from "./tools/IIDXEamusementExportTool"; @@ -7,7 +7,7 @@ import { JubilityBreakdownInsight } from "./insights/JubilityBreakdownInsight"; import { BMSSieglindeInfoTool } from "./tools/BMSSieglindeInfoTool"; // What utils does each game support? -const GPT_UTILS: Record> = { +const GPT_UTILS: Record> = { "bms:7K": [BMSCustomTablesTool, BMSSieglindeInfoTool], "bms:14K": [BMSCustomTablesTool, BMSSieglindeInfoTool], "chunithm:Single": [], @@ -29,7 +29,7 @@ const GPT_UTILS: Record> = { }; export function GetGPTUtils(game: Game, playtype: Playtype) { - const idString = `${game}:${playtype}` as IDStrings; + const idString = `${game}:${playtype}` as GPTString; return GPT_UTILS[idString]; } diff --git a/client/src/components/gpt-utils/tools/BMSSieglindeInfoTool.tsx b/client/src/components/gpt-utils/tools/BMSSieglindeInfoTool.tsx index e6b67181c..ff39df5be 100644 --- a/client/src/components/gpt-utils/tools/BMSSieglindeInfoTool.tsx +++ b/client/src/components/gpt-utils/tools/BMSSieglindeInfoTool.tsx @@ -12,9 +12,9 @@ import { TachiConfig } from "lib/config"; import React from "react"; import { Col, Row } from "react-bootstrap"; import { BMS_TABLES, ChartDocument, SongDocument } from "tachi-common"; -import { FormatSieglindeBMS } from "tachi-common/config/internal-utils"; import { UGPT } from "types/react"; import { GPTUtility } from "types/ugpt"; +import { FormatSieglindeBMS } from "tachi-common/config/game-support/bms"; type DatasetElement = ChartDocument<"bms:7K" | "bms:14K"> & { __related: { song: SongDocument } }; @@ -76,20 +76,20 @@ function Component({ game, playtype, reqUser }: UGPT) { [ "EASY CLEAR Sieglinde", "EC sgl.", - NumericSOV((x) => x.tierlistInfo["sgl-EC"]?.value ?? -Infinity), + NumericSOV((x) => x.data.sglEC ?? -Infinity), ], [ "HARD CLEAR Sieglinde", "EC sgl.", - NumericSOV((x) => x.tierlistInfo["sgl-HC"]?.value ?? -Infinity), + NumericSOV((x) => x.data.sglHC ?? -Infinity), ], ]} rowFunction={(d) => ( - {FormatSieglindeBMS(d.tierlistInfo["sgl-EC"]?.value ?? 0)} - {FormatSieglindeBMS(d.tierlistInfo["sgl-HC"]?.value ?? 0)} + {FormatSieglindeBMS(d.data.sglEC ?? 0)} + {FormatSieglindeBMS(d.data.sglHC ?? 0)} )} defaultSortMode="EASY CLEAR Sieglinde" diff --git a/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx b/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx index 1e94b46e4..11daad1ce 100644 --- a/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx +++ b/client/src/components/sessions/SessionFolderRaiseBreakdown.tsx @@ -115,7 +115,8 @@ export default function SessionFolderRaiseBreakdown({ const chartMap = CreateChartMap(sessionData.charts); const songMap = CreateSongMap(sessionData.songs); - const preferredScoreBucket = settings?.preferences.scoreBucket ?? gptConfig.scoreBucket; + const preferredScoreBucket = + settings?.preferences.preferredDefaultEnum ?? gptConfig.preferredDefaultEnum; if (selectedTable === "LOADING") { return ; diff --git a/client/src/components/sessions/SessionOverview.tsx b/client/src/components/sessions/SessionOverview.tsx index 8566b8530..808290351 100644 --- a/client/src/components/sessions/SessionOverview.tsx +++ b/client/src/components/sessions/SessionOverview.tsx @@ -5,7 +5,7 @@ import ScoreTable from "components/tables/scores/ScoreTable"; import Divider from "components/util/Divider"; import React, { useState } from "react"; import { Col, Row } from "react-bootstrap"; -import { GetGamePTConfig, ScoreDocument, SessionDocument } from "tachi-common"; +import { AnySessionRatingAlg, GetGamePTConfig, ScoreDocument, SessionDocument } from "tachi-common"; import { SessionReturns } from "types/api-returns"; import { ScoreDataset } from "types/tables"; import { SetState } from "types/react"; @@ -73,15 +73,15 @@ function RatingsOverview({ session }: { session: SessionDocument }) {
- {gptConfig.sessionRatingAlgs.map((e) => ( + {Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
{UppercaseFirst(e)}
{FormatSessionRating( session.game, session.playtype, - e, - session.calculatedData[e] + e as AnySessionRatingAlg, + session.calculatedData[e as AnySessionRatingAlg] )}
diff --git a/client/src/components/sessions/SessionRaiseBreakdown.tsx b/client/src/components/sessions/SessionRaiseBreakdown.tsx index 5c48fa3ef..d4f0160f6 100644 --- a/client/src/components/sessions/SessionRaiseBreakdown.tsx +++ b/client/src/components/sessions/SessionRaiseBreakdown.tsx @@ -1,35 +1,38 @@ import { APIFetchV1 } from "util/api"; import { ChangeOpacity } from "util/color-opacity"; import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data"; -import { PartialArrayRecordAssign } from "util/misc"; +import { PartialArrayRecordAssign, UppercaseFirst } from "util/misc"; import DifficultyCell from "components/tables/cells/DifficultyCell"; -import LampCell from "components/tables/cells/LampCell"; -import ScoreCell from "components/tables/cells/ScoreCell"; import TitleCell from "components/tables/cells/TitleCell"; import MiniTable from "components/tables/components/MiniTable"; import { CommentModal, ModifyScore } from "components/tables/dropdowns/components/ScoreEditButtons"; +import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells"; import Divider from "components/util/Divider"; import Icon from "components/util/Icon"; import Loading from "components/util/Loading"; import SelectButton from "components/util/SelectButton"; import { UserContext } from "context/UserContext"; import deepmerge from "deepmerge"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import { GPTClientImplementation } from "lib/types"; import React, { useContext, useEffect, useMemo, useState } from "react"; import { useQuery } from "react-query"; import { ChartDocument, + GPTString, Game, GamePTConfig, + GetGPTString, GetGamePTConfig, - Grades, - IDStrings, - integer, - Lamps, + GetScoreMetricConf, + GetScoreMetrics, ScoreDocument, SessionScoreInfo, SongDocument, TableDocument, + integer, } from "tachi-common"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; import { SessionReturns } from "types/api-returns"; type SetScores = (scores: ScoreDocument[]) => void; @@ -57,7 +60,10 @@ export default function SessionRaiseBreakdown({ return res.body; }); - const [view, setView] = useState<"lamps" | "both" | "grades">("both"); + + // null -> view all in small format + // string -> view this specific metric. + const [view, setView] = useState(null); if (error) { return <>{(error as Error).message}; @@ -75,17 +81,17 @@ export default function SessionRaiseBreakdown({
- + Lamps Only - + - Both + All - + Grades Only @@ -115,122 +121,103 @@ function SessionScoreStatBreakdown({ }: { sessionData: SessionReturns; setScores?: SetScores; - view: "lamps" | "both" | "grades"; + view: string | null; }) { const songMap = CreateSongMap(sessionData.songs); const chartMap = CreateChartMap(sessionData.charts); const scoreMap = CreateScoreIDMap(sessionData.scores); + const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype); - const [newLamps, newGrades] = useMemo(() => { - const newLamps: Partial< - Record - > = {}; - const newGrades: Partial< - Record + const enumMetrics = GetScoreMetrics(gptConfig, "ENUM"); + + const newEnums = useMemo(() => { + const newEnums: Record< + string, + Record > = {}; - for (const scoreInfo of sessionData.scoreInfo) { - const score = scoreMap.get(scoreInfo.scoreID); + for (const metric of enumMetrics) { + newEnums[metric] = {}; - if (!score) { - console.error( - `Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?` - ); - continue; - } + for (const scoreInfo of sessionData.scoreInfo) { + const score = scoreMap.get(scoreInfo.scoreID); - if (scoreInfo.isNewScore) { - PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo }); - PartialArrayRecordAssign(newGrades, score.scoreData.grade, { score, scoreInfo }); - } else { - if (scoreInfo.lampDelta > 0) { - PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo }); + if (!score) { + console.error( + `Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?` + ); + continue; } - if (scoreInfo.gradeDelta > 0) { - PartialArrayRecordAssign(newGrades, score.scoreData.grade, { - score, - scoreInfo, - }); + if (scoreInfo.isNewScore || scoreInfo.deltas[metric] > 0) { + PartialArrayRecordAssign( + newEnums[metric], + + // @ts-expect-error yeah this is fine pls + score.scoreData[metric] as string, + { + score, + scoreInfo, + } + ); } } } - return [newLamps, newGrades]; + return newEnums; }, [view]); - const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype); + const gptImpl = + GPT_CLIENT_IMPLEMENTATIONS[ + GetGPTString(sessionData.session.game, sessionData.session.playtype) + ]; return ( <> - {view === "both" ? ( - <> -
- - - -
-
- - - -
- - ) : view === "grades" ? ( -
- - - + {view === null ? ( +
+ {enumMetrics.map((metric) => ( +
+ + + +
+ ))}
) : (
- +
@@ -240,7 +227,7 @@ function SessionScoreStatBreakdown({ } function ElementStatTable({ - type, + metric: metric, counts, gptConfig, songMap, @@ -249,27 +236,26 @@ function ElementStatTable({ fullSize = false, scores, setScores, + gptImpl, }: { - type: "lamp" | "grade"; + metric: string; setScores?: SetScores; scores: ScoreDocument[]; counts: Record; gptConfig: GamePTConfig; + gptImpl: GPTClientImplementation; songMap: Map>; - chartMap: Map>; + chartMap: Map>; game: Game; fullSize?: boolean; }) { const tableContents = useMemo(() => { - // relements.. haha - const relevantElements = - type === "lamp" - ? gptConfig.lamps.slice(gptConfig.lamps.indexOf(gptConfig.clearLamp) - 1).reverse() - : gptConfig.grades - .slice(gptConfig.grades.indexOf(gptConfig.clearGrade) - 1) - .reverse(); + const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric; - const colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours; + // relements.. haha + const relevantElements = conf.values.slice(conf.values.indexOf(conf.minimumRelevantValue)); + + const colours = gptImpl.enumColours[metric]; const tableContents = []; for (const element of relevantElements) { @@ -283,7 +269,6 @@ function ElementStatTable({ ; chartMap: Map; gptConfig: GamePTConfig; - type: "lamp" | "grade"; + metric: string; scores: Array; setScores?: SetScores; }) { @@ -425,25 +410,24 @@ function BreakdownChartContents({ let preScoreCell = No Play; if (!scoreInfo.isNewScore) { - const oldGradeIndex = score.scoreData.gradeIndex - scoreInfo.gradeDelta; - const oldLampIndex = score.scoreData.lampIndex - scoreInfo.lampDelta; + const newScoreData = score.scoreData; + + for (const [k, d] of Object.entries(scoreInfo.deltas)) { + // @ts-expect-error ugh + if (typeof score.scoreData[k] === "string") { + // @ts-expect-error ugh + newScoreData.enumIndexes[k] = score.scoreData.enumIndexes[k] - d; + } else { + // @ts-expect-error ugh + newScoreData[k] = score.scoreData[k] - d; + } + } const mockScore = deepmerge(score, { - scoreData: { - score: score.scoreData.score - scoreInfo.scoreDelta, - percent: score.scoreData.percent - scoreInfo.percentDelta, - grade: gptConfig.grades[oldGradeIndex], - gradeIndex: oldGradeIndex, - lamp: gptConfig.lamps[oldLampIndex], - lampIndex: oldLampIndex, - }, + scoreData: newScoreData, }) as ScoreDocument; - if (type === "grade") { - preScoreCell = ; - } else { - preScoreCell = ; - } + preScoreCell = ; } if (score) { @@ -453,11 +437,7 @@ function BreakdownChartContents({ {preScoreCell} ⟶ - {type === "grade" ? ( - - ) : ( - - )} + ); } diff --git a/client/src/components/tables/cells/BMSOrPMSDifficultyCell.tsx b/client/src/components/tables/cells/BMSOrPMSDifficultyCell.tsx index 4ccff02b3..26cd7e930 100644 --- a/client/src/components/tables/cells/BMSOrPMSDifficultyCell.tsx +++ b/client/src/components/tables/cells/BMSOrPMSDifficultyCell.tsx @@ -5,8 +5,8 @@ import Divider from "components/util/Divider"; import Muted from "components/util/Muted"; import useLUGPTSettings from "components/util/useLUGPTSettings"; import React from "react"; -import { BMS_TABLES, ChartDocument, COLOUR_SET, UGPTSettings } from "tachi-common"; -import TierlistInfoPart from "./TierlistInfoPart"; +import { BMS_TABLES, ChartDocument, COLOUR_SET, UGPTSettingsDocument } from "tachi-common"; +import RatingSystemPart from "./RatingSystemPart"; export default function BMSOrPMSDifficultyCell({ chart, @@ -15,7 +15,9 @@ export default function BMSOrPMSDifficultyCell({ chart: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">; game: "bms" | "pms"; }) { - const { settings } = useLUGPTSettings() as { settings: UGPTSettings<"bms:7K" | "bms:14K"> }; + const { settings } = useLUGPTSettings() as { + settings: UGPTSettingsDocument<"bms:7K" | "bms:14K">; + }; const hasLevel = chart.data.tableFolders.length > 0; @@ -31,10 +33,10 @@ export default function BMSOrPMSDifficultyCell({ if (settings?.preferences.gameSpecific.displayTables) { filteredTables = tables.filter( - (e) => !settings.preferences.gameSpecific.displayTables.includes(e.table) + (e) => !settings.preferences.gameSpecific.displayTables!.includes(e.table) ); tables = tables.filter((e) => - settings.preferences.gameSpecific.displayTables.includes(e.table) + settings.preferences.gameSpecific.displayTables!.includes(e.table) ); } @@ -81,7 +83,7 @@ export default function BMSOrPMSDifficultyCell({ {levelText} )} - + ); } diff --git a/client/src/components/tables/cells/BMSOrPMSLampCell.tsx b/client/src/components/tables/cells/BMSOrPMSLampCell.tsx index e779a7af4..48104d617 100644 --- a/client/src/components/tables/cells/BMSOrPMSLampCell.tsx +++ b/client/src/components/tables/cells/BMSOrPMSLampCell.tsx @@ -1,7 +1,8 @@ import { ChangeOpacity } from "util/color-opacity"; import { IsNotNullish } from "util/misc"; +import { GetEnumColour } from "lib/game-implementations"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { PBScoreDocument, ScoreDocument } from "tachi-common"; export default function BMSOrPMSLampCell({ score, @@ -10,19 +11,17 @@ export default function BMSOrPMSLampCell({ | ScoreDocument<"bms:7K" | "bms:14K" | "pms:Keyboard" | "pms:Controller"> | PBScoreDocument<"bms:7K" | "bms:14K" | "pms:Keyboard" | "pms:Controller">; }) { - const gptConfig = GetGamePTConfig(score.game, score.playtype); - return ( {score.scoreData.lamp} - {IsNotNullish(score.scoreData.hitMeta.bp) && ( + {IsNotNullish(score.scoreData.optional.bp) && ( <>
- [BP: {score.scoreData.hitMeta.bp}] + [BP: {score.scoreData.optional.bp}] )} diff --git a/client/src/components/tables/cells/BPICell.tsx b/client/src/components/tables/cells/BPICell.tsx index 25232b896..dd5811898 100644 --- a/client/src/components/tables/cells/BPICell.tsx +++ b/client/src/components/tables/cells/BPICell.tsx @@ -1,4 +1,4 @@ -import { GetGradeFromPercent, IsNullish } from "util/misc"; +import { IsNullish } from "util/misc"; import QuickTooltip from "components/layout/misc/QuickTooltip"; import Divider from "components/util/Divider"; import Muted from "components/util/Muted"; @@ -6,7 +6,15 @@ import useLUGPTSettings from "components/util/useLUGPTSettings"; import { UserContext } from "context/UserContext"; import React, { useContext } from "react"; import { PoyashiBPI } from "rg-stats"; -import { ChartDocument, integer, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { + ChartDocument, + GetGrade, + IIDXLIKE_GBOUNDARIES, + integer, + PBScoreDocument, + ScoreDocument, +} from "tachi-common"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import MiniTable from "../components/MiniTable"; import DeltaCell from "./DeltaCell"; import ScoreCell from "./ScoreCell"; @@ -135,22 +143,15 @@ export default function BPICell({ function FormatAverage(score: integer, playtype: "SP" | "DP", notecount: integer) { const percent = (100 * score) / (notecount * 2); - const grade = GetGradeFromPercent("iidx", playtype, percent); + const grade = GetGrade(IIDXLIKE_GBOUNDARIES, percent); return { score: ( ), delta: ( diff --git a/client/src/components/tables/cells/DeltaCell.tsx b/client/src/components/tables/cells/DeltaCell.tsx index 8806c048c..3d27755ce 100644 --- a/client/src/components/tables/cells/DeltaCell.tsx +++ b/client/src/components/tables/cells/DeltaCell.tsx @@ -1,6 +1,6 @@ import { GenericFormatGradeDelta } from "util/grade-deltas"; import React from "react"; -import { Game, Grades, IDStrings, Playtypes } from "tachi-common"; +import { Game, Grades, GPTString, Playtypes } from "tachi-common"; export default function DeltaCell({ game, @@ -13,7 +13,7 @@ export default function DeltaCell({ playtype: Playtypes[Game]; score: number; percent: number; - grade: Grades[IDStrings]; + grade: Grades[GPTString]; }) { if (score === 0) { return N/A; diff --git a/client/src/components/tables/cells/DifficultyCell.tsx b/client/src/components/tables/cells/DifficultyCell.tsx index 0e2b99f3f..9d2a2c9eb 100644 --- a/client/src/components/tables/cells/DifficultyCell.tsx +++ b/client/src/components/tables/cells/DifficultyCell.tsx @@ -11,7 +11,7 @@ import { GetGamePTConfig, } from "tachi-common"; import BMSOrPMSDifficultyCell from "./BMSOrPMSDifficultyCell"; -import TierlistInfoPart from "./TierlistInfoPart"; +import RatingSystemPart from "./RatingSystemPart"; import USCDifficultyCell from "./USCDifficultyCell"; import ITGDifficultyCell from "./ITGDifficultyCell"; @@ -73,7 +73,7 @@ export default function DifficultyCell({ )} {!noTierlist && Object.keys(chart.tierlistInfo).length > 0 && ( - + )} {!chart.isPrimary && ( diff --git a/client/src/components/tables/cells/IIDXLampCell.tsx b/client/src/components/tables/cells/IIDXLampCell.tsx index 5051869e7..e9af56cbe 100644 --- a/client/src/components/tables/cells/IIDXLampCell.tsx +++ b/client/src/components/tables/cells/IIDXLampCell.tsx @@ -1,8 +1,9 @@ import { IsScore } from "util/asserts"; import { ChangeOpacity } from "util/color-opacity"; import { IsNotNullish } from "util/misc"; -import { ChartDocument, GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import React from "react"; +import { ChartDocument, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common"; export default function IIDXLampCell({ sc, @@ -11,33 +12,36 @@ export default function IIDXLampCell({ sc: ScoreDocument<"iidx:SP" | "iidx:DP"> | PBScoreDocument<"iidx:SP" | "iidx:DP">; chart: ChartDocument<"iidx:SP" | "iidx:DP">; }) { - const gptConfig = GetGamePTConfig("iidx", sc.playtype); - let gaugeText = null; - if (sc.scoreData.hitMeta.gsm) { + if ( + sc.scoreData.optional.gsmEXHard && + sc.scoreData.optional.gsmHard && + sc.scoreData.optional.gsmNormal && + sc.scoreData.optional.gsmEasy + ) { gaugeText = `EC: ${ - sc.scoreData.hitMeta.gsm.EASY[sc.scoreData.hitMeta.gsm.EASY.length - 1] - }%, NC: ${sc.scoreData.hitMeta.gsm.NORMAL[sc.scoreData.hitMeta.gsm.NORMAL.length - 1]}%`; + sc.scoreData.optional.gsmEasy[sc.scoreData.optional.gsmEasy.length - 1] + }%, NC: ${sc.scoreData.optional.gsmNormal[sc.scoreData.optional.gsmNormal.length - 1]}%`; } else if (IsScore(sc)) { if (sc.scoreMeta.gauge === "EASY") { - gaugeText = `EC: ${sc.scoreData.hitMeta.gauge}%`; + gaugeText = `EC: ${sc.scoreData.optional.gauge}%`; } else if (sc.scoreMeta.gauge === "NORMAL") { - gaugeText = `NC: ${sc.scoreData.hitMeta.gauge}%`; + gaugeText = `NC: ${sc.scoreData.optional.gauge}%`; } } let bpText; - if (IsNotNullish(sc.scoreData.hitMeta.bp)) { - bpText = `[BP: ${sc.scoreData.hitMeta.bp}]`; + if (IsNotNullish(sc.scoreData.optional.bp)) { + bpText = `[BP: ${sc.scoreData.optional.bp}]`; } let cbrkCount; let cbrkText; - if (sc.scoreData.hitMeta.comboBreak) { - cbrkCount = sc.scoreData.hitMeta.comboBreak; + if (sc.scoreData.optional.comboBreak) { + cbrkCount = sc.scoreData.optional.comboBreak; } else if ( IsNotNullish(sc.scoreData.judgements.pgreat) && IsNotNullish(sc.scoreData.judgements.great) && @@ -57,7 +61,7 @@ export default function IIDXLampCell({ return ( {sc.scoreData.lamp} diff --git a/client/src/components/tables/cells/ITGDifficultyCell.tsx b/client/src/components/tables/cells/ITGDifficultyCell.tsx index 141dea326..a1611dbd7 100644 --- a/client/src/components/tables/cells/ITGDifficultyCell.tsx +++ b/client/src/components/tables/cells/ITGDifficultyCell.tsx @@ -4,7 +4,7 @@ import Muted from "components/util/Muted"; import React from "react"; import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common"; import MiniTable from "../components/MiniTable"; -import TierlistInfoPart from "./TierlistInfoPart"; +import RatingSystemPart from "./RatingSystemPart"; export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg:Stamina"> }) { const gptConfig = GetGamePTConfig("itg", "Stamina"); @@ -45,7 +45,7 @@ export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg
{chart.data.charter} - +
diff --git a/client/src/components/tables/cells/JubeatScoreCell.tsx b/client/src/components/tables/cells/JubeatScoreCell.tsx index 0584527eb..597dd63c8 100644 --- a/client/src/components/tables/cells/JubeatScoreCell.tsx +++ b/client/src/components/tables/cells/JubeatScoreCell.tsx @@ -1,20 +1,28 @@ import { ChangeOpacity } from "util/color-opacity"; import { FormatMillions } from "util/misc"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; - -export default function JubeatScoreCell({ sc }: { sc: ScoreDocument | PBScoreDocument }) { - const gptConfig = GetGamePTConfig(sc.game, sc.playtype); +import { PBScoreDocument, ScoreDocument } from "tachi-common"; +export default function JubeatScoreCell({ + sc, +}: { + sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">; +}) { return ( {sc.scoreData.grade}
- {sc.scoreData.percent.toFixed(2)}% + {sc.scoreData.musicRate.toFixed(2)}%
{FormatMillions(sc.scoreData.score)} diff --git a/client/src/components/tables/cells/LampCell.tsx b/client/src/components/tables/cells/LampCell.tsx index 169c2f6eb..6e7328412 100644 --- a/client/src/components/tables/cells/LampCell.tsx +++ b/client/src/components/tables/cells/LampCell.tsx @@ -1,17 +1,14 @@ import { ChangeOpacity } from "util/color-opacity"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; - -export default function LampCell({ score }: { score: ScoreDocument | PBScoreDocument }) { - const gptConfig = GetGamePTConfig(score.game, score.playtype); +export default function LampCell({ lamp, colour }: { lamp: string; colour: string }) { return ( - {score.scoreData.lamp} + {lamp} ); } diff --git a/client/src/components/tables/cells/MillionsScoreCell.tsx b/client/src/components/tables/cells/MillionsScoreCell.tsx index 5406e31ef..c0e94f1c8 100644 --- a/client/src/components/tables/cells/MillionsScoreCell.tsx +++ b/client/src/components/tables/cells/MillionsScoreCell.tsx @@ -1,20 +1,26 @@ import { ChangeOpacity } from "util/color-opacity"; import { FormatMillions } from "util/misc"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; - -export default function MillionsScoreCell({ score }: { score: PBScoreDocument | ScoreDocument }) { - const gptConfig = GetGamePTConfig(score.game, score.playtype); +import { GetGamePTConfig, PBScoreDocument, ScoreDocument, integer } from "tachi-common"; +export default function MillionsScoreCell({ + score, + colour, + grade, +}: { + score: integer; + grade: string; + colour: string; +}) { return ( - {score.scoreData.grade} + {grade}
- {FormatMillions(score.scoreData.score)} + {FormatMillions(score)} ); } diff --git a/client/src/components/tables/cells/PBCompareCell.tsx b/client/src/components/tables/cells/PBCompareCell.tsx index b4a764f95..6f4eba576 100644 --- a/client/src/components/tables/cells/PBCompareCell.tsx +++ b/client/src/components/tables/cells/PBCompareCell.tsx @@ -1,22 +1,21 @@ import { FormatMillions } from "util/misc"; import React from "react"; import { COLOUR_SET, Game, PBScoreDocument, Playtype } from "tachi-common"; -import { ESDCompare } from "tachi-common/lib/esd"; -import Muted from "components/util/Muted"; +import { ConfScoreMetric } from "tachi-common/types/metrics"; export default function PBCompareCell({ base, compare, game, - compareType, - shouldESD, + metricConf, + metric, }: { base: PBScoreDocument | null; compare: PBScoreDocument | null; game: Game; playtype: Playtype; - compareType: "score" | "lamp"; - shouldESD: boolean; + metricConf: ConfScoreMetric; + metric: string; }) { let status: "win" | "draw" | "lose"; @@ -27,10 +26,12 @@ export default function PBCompareCell({ status = "lose"; } else if (!compare) { status = "win"; - } else if (compareType === "score") { - status = cmp(base.scoreData.score, compare.scoreData.score); + } else if (metricConf.type === "ENUM") { + // @ts-expect-error yeah this index will work sorry + status = cmp(base.scoreData.enumIndexes[metric], compare.scoreData.enumIndexes[metric]); } else { - status = cmp(base.scoreData.lampIndex, compare.scoreData.lampIndex); + // @ts-expect-error yeah this index will work sorry + status = cmp(base.scoreData[metric], compare.scoreData[metric]); } let scoreRenderFn = (k: number) => k.toString(); @@ -68,14 +69,6 @@ export default function PBCompareCell({ const cmpScore = (base?.scoreData.score ?? 0) - (compare?.scoreData.score ?? 0); const cmpLamp = (base?.scoreData.lampIndex ?? 0) - (compare?.scoreData.lampIndex ?? 0); - let esdDiff; - - if (shouldESD) { - if (base?.scoreData.esd && compare?.scoreData.esd) { - esdDiff = ESDCompare(compare.scoreData.esd, base.scoreData.esd); - } - } - return ( - {compareType === "score" ? ( + {metricConf.type === "ENUM" ? ( + <> + ) : ( <> {shouldPrioritisePercent ? ( <> @@ -114,22 +109,6 @@ export default function PBCompareCell({ )} )} - {esdDiff !== undefined && ( - <> -
- {esdDiff.toFixed(2) === "0.00" ? ( - Negligable Difference - ) : ( - - Relative: {(esdDiff > 0 ? "+" : "") + esdDiff.toFixed(2)} - - )} - - )} - - ) : ( - <> - {(cmpLamp > 0 ? "+" : "") + cmpLamp} )} diff --git a/client/src/components/tables/cells/PopnLampCell.tsx b/client/src/components/tables/cells/PopnLampCell.tsx index 6751ae2a8..6d9406b63 100644 --- a/client/src/components/tables/cells/PopnLampCell.tsx +++ b/client/src/components/tables/cells/PopnLampCell.tsx @@ -1,31 +1,31 @@ import { ToCDNURL } from "util/api"; import { ChangeOpacity } from "util/color-opacity"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { PBScoreDocument, ScoreDocument } from "tachi-common"; export default function PopnLampCell({ score, }: { score: ScoreDocument<"popn:9B"> | PBScoreDocument<"popn:9B">; }) { - const gptConfig = GetGamePTConfig(score.game, score.playtype); - return ( {score.scoreData.lamp}
- {score.scoreData.hitMeta.specificClearType && ( - - )} + ); } diff --git a/client/src/components/tables/cells/RatingSystemPart.tsx b/client/src/components/tables/cells/RatingSystemPart.tsx new file mode 100644 index 000000000..f49c8d980 --- /dev/null +++ b/client/src/components/tables/cells/RatingSystemPart.tsx @@ -0,0 +1,81 @@ +import QuickTooltip from "components/layout/misc/QuickTooltip"; +import Icon from "components/util/Icon"; +import Muted from "components/util/Muted"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import React from "react"; +import { ChartDocument, Game, GetGPTString } from "tachi-common"; +import { GPTRatingSystem } from "lib/types"; +import MiniTable from "../components/MiniTable"; + +export default function RatingSystemPart({ chart, game }: { chart: ChartDocument; game: Game }) { + const ratingSystems: Array> = + GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, chart.playtype)].ratingSystems; + + // don't render anything if this game has no other rating systems defined + // for this chart + if (ratingSystems.filter((e) => typeof e.toNumber(chart) === "number").length === 0) { + return null; + } + + return ( + <> +
+ + {ratingSystems.map((e) => { + const strV = e.toString(chart); + const numV = e.toNumber(chart); + + if ( + strV === null || + strV === undefined || + numV === null || + numV === undefined + ) { + return null; + } + + return ( + + {e} + + {strV} ({numV.toFixed(2)}) + {/* @ts-expect-error utterly silly types */} + {e.idvDifference(chart) && ( + <> +
+ + + + + + + )} + + + ); + })} + + } + > +
+ + {ratingSystems.map((r) => r.toString(chart as any)).join(" / ")} + +
+
+
+
+ + {ratingSystems.map((r) => ( + + {r.toString(chart as any)} +
+
+ ))} +
+
+ + ); +} diff --git a/client/src/components/tables/cells/SDVXLampCell.tsx b/client/src/components/tables/cells/SDVXLampCell.tsx index eb9fbd70b..4162bb228 100644 --- a/client/src/components/tables/cells/SDVXLampCell.tsx +++ b/client/src/components/tables/cells/SDVXLampCell.tsx @@ -1,6 +1,7 @@ import { ChangeOpacity } from "util/color-opacity"; +import { GetEnumColour } from "lib/game-implementations"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { PBScoreDocument, ScoreDocument } from "tachi-common"; // Lamp cell, but if the lamp is FAILED, display it slightly differently. export default function SDVXLampCell({ @@ -10,12 +11,10 @@ export default function SDVXLampCell({ | ScoreDocument<"sdvx:Single" | "usc:Controller" | "usc:Keyboard"> | PBScoreDocument<"sdvx:Single" | "usc:Controller" | "usc:Keyboard">; }) { - const gptConfig = GetGamePTConfig(score.game, score.playtype); - return ( {score.scoreData.lamp === "FAILED" ? "PLAYED" : score.scoreData.lamp} diff --git a/client/src/components/tables/cells/ScoreCell.tsx b/client/src/components/tables/cells/ScoreCell.tsx index c90eb86ee..e6a30cfd5 100644 --- a/client/src/components/tables/cells/ScoreCell.tsx +++ b/client/src/components/tables/cells/ScoreCell.tsx @@ -1,39 +1,33 @@ import { ChangeOpacity } from "util/color-opacity"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { integer } from "tachi-common"; export default function ScoreCell({ score, - showScore = true, + colour, + grade, + percent, scoreRenderFn, }: { - score: PBScoreDocument | ScoreDocument; + score: integer; + percent: number; + grade: string; + colour: string; showScore?: boolean; scoreRenderFn?: (s: number) => string; }) { - const gptConfig = GetGamePTConfig(score.game, score.playtype); - return ( - {score.scoreData.grade} + {grade}
- {`${score.scoreData.percent.toFixed(2)}%`} - {showScore && ( - <> -
- - [ - {scoreRenderFn - ? scoreRenderFn(score.scoreData.score) - : score.scoreData.score} - ] - - - )} + {`${percent.toFixed(2)}%`} + +
+ [{scoreRenderFn ? scoreRenderFn(score) : score}] ); } diff --git a/client/src/components/tables/cells/ScoreCellLarge.tsx b/client/src/components/tables/cells/ScoreCellLarge.tsx deleted file mode 100644 index f76ad4c83..000000000 --- a/client/src/components/tables/cells/ScoreCellLarge.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { FormatMillions } from "util/misc"; -import React from "react"; -import { Game, PBScoreDocument, ScoreDocument, Playtype } from "tachi-common"; -import ScoreCell from "./ScoreCell"; - -export default function ScoreCellLarge(props: { - game: Game; - playtype: Playtype; - score: ScoreDocument | PBScoreDocument; -}) { - return ; -} diff --git a/client/src/components/tables/cells/TierlistInfoPart.tsx b/client/src/components/tables/cells/TierlistInfoPart.tsx deleted file mode 100644 index f522fe377..000000000 --- a/client/src/components/tables/cells/TierlistInfoPart.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import QuickTooltip from "components/layout/misc/QuickTooltip"; -import Icon from "components/util/Icon"; -import Muted from "components/util/Muted"; -import React from "react"; -import { ChartDocument, Game, GetGamePTConfig } from "tachi-common"; -import MiniTable from "../components/MiniTable"; - -// I hate the fact object.keys doesnt infer types. -type K = keyof ChartDocument["tierlistInfo"]; - -export default function TierlistInfoPart({ chart, game }: { chart: ChartDocument; game: Game }) { - const keys = GetGamePTConfig(game, chart.playtype).tierlists.filter( - (e) => chart.tierlistInfo[e] - ); - - return ( - <> -
- - - - Tierlist - Value - - - - {keys.map((k) => ( - - {k} - - {chart.tierlistInfo[k]!.text} ( - {chart.tierlistInfo[k]!.value.toFixed(2)}) - {chart.tierlistInfo[k]!.individualDifference && ( - <> -
- - - )} - - - ))} - - - } - > -
- {keys.map((k) => chart.tierlistInfo[k]!.text).join(" / ")} -
-
-
-
- - {keys.map((k) => ( - - {chart.tierlistInfo[k]!.text} -
-
- ))} -
-
- - ); -} diff --git a/client/src/components/tables/cells/USCDifficultyCell.tsx b/client/src/components/tables/cells/USCDifficultyCell.tsx index a564bc0d3..9c5c22a28 100644 --- a/client/src/components/tables/cells/USCDifficultyCell.tsx +++ b/client/src/components/tables/cells/USCDifficultyCell.tsx @@ -4,7 +4,7 @@ import QuickTooltip from "components/layout/misc/QuickTooltip"; import Icon from "components/util/Icon"; import React from "react"; import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common"; -import TierlistInfoPart from "./TierlistInfoPart"; +import RatingSystemPart from "./RatingSystemPart"; export default function USCDifficultyCell({ chart, @@ -29,7 +29,7 @@ export default function USCDifficultyCell({ }} > {levelText} - + {!chart.isPrimary && (
diff --git a/client/src/components/tables/cells/VF6Cell.tsx b/client/src/components/tables/cells/VF6Cell.tsx index 4a3bd4ac9..11304d980 100644 --- a/client/src/components/tables/cells/VF6Cell.tsx +++ b/client/src/components/tables/cells/VF6Cell.tsx @@ -3,9 +3,15 @@ import useLUGPTSettings from "components/util/useLUGPTSettings"; import { UserContext } from "context/UserContext"; import React, { useContext } from "react"; import { Volforce } from "rg-stats"; -import { ChartDocument, GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { + ChartDocument, + GetGPTString, + GetSpecificGPTConfig, + PBScoreDocument, + ScoreDocument, +} from "tachi-common"; -type VF6IDStrings = "sdvx:Single" | "usc:Keyboard" | "usc:Controller"; +type VF6GPTString = "sdvx:Single" | "usc:Keyboard" | "usc:Controller"; const SHORT_LAMPS = { CLEAR: "CLR", @@ -17,11 +23,11 @@ export default function VF6Cell({ score, chart, }: { - score: ScoreDocument | PBScoreDocument; - chart: ChartDocument; + score: ScoreDocument | PBScoreDocument; + chart: ChartDocument; }) { const { user } = useContext(UserContext); - const { settings } = useLUGPTSettings(); + const { settings } = useLUGPTSettings(); if (IsNullish(score.calculatedData.VF6)) { return N/A; @@ -29,13 +35,18 @@ export default function VF6Cell({ const vf6Target = settings?.preferences.gameSpecific.vf6Target; - const gptConfig = GetGamePTConfig(score.game, score.playtype); + const gptConfig = GetSpecificGPTConfig( + GetGPTString(score.game, score.playtype) as VF6GPTString + ); const targets: Record = {}; if (vf6Target && score.userID === user?.id) { for (const lamp of ["CLEAR", "EXCESSIVE CLEAR", "ULTIMATE CHAIN"] as const) { - if (score.scoreData.lampIndex <= gptConfig.lamps.indexOf(lamp)) { + if ( + score.scoreData.enumIndexes.lamp <= + gptConfig.providedMetrics.lamp.values.indexOf(lamp) + ) { const expectedScore = InverseVF6(vf6Target, lamp, chart.levelNum); if (expectedScore === null) { diff --git a/client/src/components/tables/components/SelectableCompareType.tsx b/client/src/components/tables/components/SelectableCompareType.tsx index c2bcee02b..6db0a05ce 100644 --- a/client/src/components/tables/components/SelectableCompareType.tsx +++ b/client/src/components/tables/components/SelectableCompareType.tsx @@ -3,20 +3,20 @@ import { SetState } from "types/react"; import { ZTableTHProps } from "./TachiTable"; export default function SelectableCompareType({ - compareType, - setCompareType, + metric, + setMetric, changeSort, currentSortMode, reverseSort, }: { - compareType: "score" | "lamp"; - setCompareType: SetState<"score" | "lamp">; + metric: string; + setMetric: SetState; } & ZTableTHProps) { return ( setRating(v.target.value as AllRatings)} + onChange={(v) => setRating(v.target.value as AllRatings)} value={rating} style={{ backgroundColor: "#131313", @@ -59,7 +48,7 @@ export default function SelectableRating({ textAlign: "center", }} > - {gptConfig[key].map((s) => ( + {Object.keys(gptConfig[key]).map((s) => ( diff --git a/client/src/components/tables/dropdowns/PBDropdown.tsx b/client/src/components/tables/dropdowns/PBDropdown.tsx index 10c00d197..8dc12e0de 100644 --- a/client/src/components/tables/dropdowns/PBDropdown.tsx +++ b/client/src/components/tables/dropdowns/PBDropdown.tsx @@ -9,7 +9,7 @@ import { UserContext } from "context/UserContext"; import React, { useContext, useMemo, useReducer, useState } from "react"; import { ChartDocument, - IDStrings, + GPTString, integer, PBScoreDocument, ScoreDocument, @@ -27,14 +27,14 @@ import RivalCompare from "./components/RivalCompare"; import TargetInfo from "./components/TargetInfo"; import { GPTDropdownSettings } from "./GPTDropdownSettings"; -export interface ScoreDropdownProps { - score: ScoreDocument | PBScoreDocument; +export interface ScoreDropdownProps { + score: ScoreDocument | PBScoreDocument; scoreState: ScoreState; - pbData: UGPTChartPBComposition; - chart: ChartDocument; + pbData: UGPTChartPBComposition; + chart: ChartDocument; } -export default function PBDropdown({ +export default function PBDropdown({ game, playtype, chart, @@ -47,7 +47,7 @@ export default function PBDropdown({ chart: ChartDocument; song: SongDocument; scoreState: ScoreState; - defaultView?: "pb" | "scorePB" | "lampPB" | "history" | "debug" | "rivals" | "targets"; + defaultView?: "pb" | `otherPB::${string}` | "history" | "debug" | "rivals" | "targets"; } & GamePT) { const { user: currentUser } = useContext(UserContext); @@ -56,11 +56,11 @@ export default function PBDropdown({ const [view, setView] = useState(defaultView); - const { data, error } = useApiQuery>( + const { data, error } = useApiQuery( `/users/${userID}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` ); - const { error: histError, data: histData } = useApiQuery[]>( + const { error: histError, data: histData } = useApiQuery( `/users/${userID}/games/${game}/${playtype}/scores/${chart.chartID}` ); @@ -76,32 +76,28 @@ export default function PBDropdown({ currentUser === null ); - const currentScoreDoc: ScoreDocument | PBScoreDocument | null = useMemo(() => { + const currentScoreDoc: ScoreDocument | PBScoreDocument | null = useMemo(() => { if (!data) { // dont worry about this null, it never gets below the rquery checks return null; } if (view === "pb") { - if (data.pb.composedFrom.lampPB === data.pb.composedFrom.scorePB) { + if (data.pb.composedFrom.length === 1) { // scores have more information than PBs. // In this case, the PB is only composed of one score, // so we should default to this instead. - return data.scores.filter((e) => e.scoreID === data.pb.composedFrom.lampPB)[0]; + return data.scores.find((e) => e.scoreID === data.pb.composedFrom[0]!.scoreID)!; } + return data.pb; + } else if (view.startsWith("otherPB::")) { + const scoreID = view.split("otherPB::")[1]; + + return data.scores.filter((e) => e.scoreID === scoreID)[0]; } - const idMap = { - scorePB: data.pb.composedFrom.scorePB, - lampPB: data.pb.composedFrom.lampPB, - ...Object.fromEntries( - (data.pb.composedFrom.other ?? []).map((e) => [e.name, e.scoreID]) - ), - }; - - // @ts-expect-error awful - return data.scores.filter((e) => e.scoreID === idMap[view])[0]; + return null; }, [view, data]); if (error) { @@ -116,8 +112,7 @@ export default function PBDropdown({ ); } - // @TODO this doesn't support alternate PBs. - const isComposedFromSingleScore = data.pb.composedFrom.lampPB === data.pb.composedFrom.scorePB; + const isComposedFromSingleScore = data.pb.composedFrom.length === 1; let body; @@ -174,14 +169,16 @@ export default function PBDropdown({ {!isComposedFromSingleScore && ( <> - - - Best Score - - - - Best Lamp - + {data.pb.composedFrom.map((e) => ( + + + {e.name} + + ))} )} diff --git a/client/src/components/tables/dropdowns/ScoreDropdown.tsx b/client/src/components/tables/dropdowns/ScoreDropdown.tsx index eef64f923..a5a527fb4 100644 --- a/client/src/components/tables/dropdowns/ScoreDropdown.tsx +++ b/client/src/components/tables/dropdowns/ScoreDropdown.tsx @@ -9,7 +9,7 @@ import { UserSettingsContext } from "context/UserSettingsContext"; import React, { useContext, useReducer, useState } from "react"; import { ChartDocument, - IDStrings, + GPTString, PBScoreDocument, ScoreDocument, SongDocument, @@ -32,12 +32,12 @@ export interface ScoreState { setHighlight: SetState; } -export interface ScoreDropdownProps { - score: ScoreDocument | PBScoreDocument; +export interface ScoreDropdownProps { + score: ScoreDocument | PBScoreDocument; scoreState: ScoreState; } -export default function ScoreDropdown({ +export default function ScoreDropdown({ game, playtype, user, @@ -65,11 +65,11 @@ export default function ScoreDropdown({ const { user: currentUser } = useContext(UserContext); const { settings } = useLUGPTSettings(); - const { data, error } = useApiQuery>( + const { data, error } = useApiQuery( `/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` ); - const { error: histError, data: histData } = useApiQuery[]>( + const { error: histError, data: histData } = useApiQuery( `/users/${user.id}/games/${game}/${playtype}/scores/${chart.chartID}` ); diff --git a/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx b/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx index f07994d81..fd3c359d4 100644 --- a/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx +++ b/client/src/components/tables/dropdowns/components/BMSScoreDropdownParts.tsx @@ -13,7 +13,7 @@ export function BMSGraphsComponent({ score: ScoreDocument<"bms:7K" | "bms:14K"> | PBScoreDocument<"bms:7K" | "bms:14K">; chart: ChartDocument<"bms:7K" | "bms:14K">; }) { - const gaugeHistory = score.scoreData.hitMeta.gaugeHistory; + const gaugeHistory = score.scoreData.optional.gaugeHistory; const [lamp, setLamp] = useState(LampToKey(score)); @@ -126,7 +126,7 @@ function LampToKey( return "EX_HARD"; } else if (lamp === "FULL COMBO") { // @hack - attempt to guess what gauge they used? - if ((score.scoreData.hitMeta.gaugeHistory?.[0] ?? 0) > 22) { + if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) { return "EX_HARD"; } return "NORMAL"; diff --git a/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx b/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx index e578847c8..6b3843ed8 100644 --- a/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx +++ b/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx @@ -43,9 +43,9 @@ export function IIDXGraphsComponent({ let gaugeStatus: "none" | "single" | "gsm" = "none"; - if (score.scoreData.hitMeta.gsm) { + if (score.scoreData.optional.gsm) { gaugeStatus = "gsm"; - } else if (score.scoreData.hitMeta.gaugeHistory) { + } else if (score.scoreData.optional.gaugeHistory) { gaugeStatus = "single"; } @@ -113,9 +113,9 @@ export function IIDXGraphsComponent({
{gaugeStatus === "gsm" && lamp !== "DAN_GAUGE" ? ( - + ) : gaugeStatus === "single" ? ( - + ) : (
22) { + if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) { return "EX_HARD"; } return "NORMAL"; diff --git a/client/src/components/tables/dropdowns/components/JudgementTable.tsx b/client/src/components/tables/dropdowns/components/JudgementTable.tsx index 8c0de8113..adc4dd8e7 100644 --- a/client/src/components/tables/dropdowns/components/JudgementTable.tsx +++ b/client/src/components/tables/dropdowns/components/JudgementTable.tsx @@ -9,18 +9,21 @@ export default function JudgementTable({ score }: { score: ScoreDocument | PBSco return ( - {gptConfig.judgements.map((j) => ( + {gptConfig.orderedJudgements.map((j) => ( {j.toUpperCase()} + {/* @ts-expect-error fine access */} {score.scoreData.judgements[j] ?? "No Data."} ))} - Slow: {score.scoreData.hitMeta.slow ?? "Unknown"} + {/* @ts-expect-error it might exist */} + Slow: {score.scoreData.optional.slow ?? "Unknown"} - Fast: {score.scoreData.hitMeta.fast ?? "Unknown"} + {/* @ts-expect-error it might exist */} + Fast: {score.scoreData.optional.fast ?? "Unknown"} diff --git a/client/src/components/tables/dropdowns/components/PBCompare.tsx b/client/src/components/tables/dropdowns/components/PBCompare.tsx index ebfeece49..d4d18c824 100644 --- a/client/src/components/tables/dropdowns/components/PBCompare.tsx +++ b/client/src/components/tables/dropdowns/components/PBCompare.tsx @@ -1,21 +1,21 @@ import React from "react"; -import { ScoreDocument, PBScoreDocument, IDStrings, ChartDocument } from "tachi-common"; +import { ScoreDocument, PBScoreDocument, GPTString, ChartDocument } from "tachi-common"; import { UGPTChartPBComposition } from "types/api-returns"; import { ScoreState } from "../ScoreDropdown"; -export default function PBCompare({ +export default function PBCompare({ data, scoreState, DocComponent, }: { - data: UGPTChartPBComposition; + data: UGPTChartPBComposition; scoreState: ScoreState; DocComponent: (props: { - score: ScoreDocument | PBScoreDocument; + score: ScoreDocument | PBScoreDocument; scoreState: ScoreState; pbData: UGPTChartPBComposition; forceScoreData: boolean; - chart: ChartDocument; + chart: ChartDocument; }) => JSX.Element; }) { return ( diff --git a/client/src/components/tables/folders/FolderTable.tsx b/client/src/components/tables/folders/FolderTable.tsx index 31d0c6eb2..58472f65f 100644 --- a/client/src/components/tables/folders/FolderTable.tsx +++ b/client/src/components/tables/folders/FolderTable.tsx @@ -4,7 +4,7 @@ import Muted from "components/util/Muted"; import usePreferredRanking from "components/util/usePreferredRanking"; import useScoreRatingAlg from "components/util/useScoreRatingAlg"; import React, { useState } from "react"; -import { Game, IDStrings, ScoreCalculatedDataLookup, Playtype } from "tachi-common"; +import { Game, GPTString, ScoreRatingAlgorithms, Playtype } from "tachi-common"; import { FolderDataset } from "types/tables"; import DifficultyCell from "../cells/DifficultyCell"; import IndicatorsCell from "../cells/IndicatorsCell"; @@ -21,12 +21,12 @@ import { GetGPTCoreHeaders } from "../headers/GameHeaders"; import { EmptyHeader, FolderIndicatorHeader } from "../headers/IndicatorHeader"; import { CreateRankingHeader } from "../headers/RankingHeader"; -export default function FolderTable({ +export default function FolderTable({ dataset, game, playtype, }: { - dataset: FolderDataset; + dataset: FolderDataset; game: Game; playtype: Playtype; }) { @@ -78,15 +78,15 @@ export default function FolderTable({ ); } -function Row({ +function Row({ data, rating, game, rankingViewMode, }: { - data: FolderDataset[0]; + data: FolderDataset[0]; game: Game; - rating: ScoreCalculatedDataLookup[I]; + rating: ScoreRatingAlgorithms[GPTString]; rankingViewMode: RankingViewMode; }) { const score = data.__related.pb; diff --git a/client/src/components/tables/game-core-cells/BMSCoreCells.tsx b/client/src/components/tables/game-core-cells/BMSCoreCells.tsx index 32781d732..afd623e60 100644 --- a/client/src/components/tables/game-core-cells/BMSCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/BMSCoreCells.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { IDStrings, PBScoreDocument, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common"; +import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell"; import DeltaCell from "../cells/DeltaCell"; import RatingCell from "../cells/RatingCell"; @@ -10,7 +10,7 @@ export default function BMSCoreCells({ rating, }: { sc: PBScoreDocument<"bms:7K" | "bms:14K"> | ScoreDocument<"bms:7K" | "bms:14K">; - rating: ScoreCalculatedDataLookup[IDStrings]; + rating: ScoreRatingAlgorithms[GPTString]; }) { return ( <> diff --git a/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx b/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx index e1eef2267..a61e94351 100644 --- a/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import CHUNITHMJudgementCell from "../cells/CHUNITHMJudgementCell"; import LampCell from "../cells/LampCell"; import MillionsScoreCell from "../cells/MillionsScoreCell"; @@ -14,9 +15,13 @@ export default function CHUNITHMCoreCells({ }) { return ( <> - + - + ); diff --git a/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx b/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx index ffe6faf1f..c61d3fddc 100644 --- a/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/GitadoraCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import GitadoraJudgementCell from "../cells/GitadoraJudgementCell"; import LampCell from "../cells/LampCell"; import RatingCell from "../cells/RatingCell"; @@ -18,7 +19,7 @@ export default function GitadoraCoreCells({ <> - + ); diff --git a/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx b/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx index 61b127bba..0ab2f5572 100644 --- a/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/IIDXCoreCells.tsx @@ -1,10 +1,5 @@ import React from "react"; -import { - ChartDocument, - PBScoreDocument, - ScoreCalculatedDataLookup, - ScoreDocument, -} from "tachi-common"; +import { ChartDocument, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; import BPICell from "../cells/BPICell"; import DeltaCell from "../cells/DeltaCell"; import IIDXLampCell from "../cells/IIDXLampCell"; @@ -17,7 +12,7 @@ export default function IIDXCoreCells({ chart, }: { sc: PBScoreDocument<"iidx:SP" | "iidx:DP"> | ScoreDocument<"iidx:SP" | "iidx:DP">; - rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]; + rating: ScoreRatingAlgorithms["iidx:SP" | "iidx:DP"]; chart: ChartDocument<"iidx:SP" | "iidx:DP">; }) { return ( diff --git a/client/src/components/tables/game-core-cells/ITGCoreCells.tsx b/client/src/components/tables/game-core-cells/ITGCoreCells.tsx index 11481fa7f..5a47b7dac 100644 --- a/client/src/components/tables/game-core-cells/ITGCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/ITGCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import ITGJudgementCell from "../cells/ITGJudgementCell"; import LampCell from "../cells/LampCell"; import RatingCell from "../cells/RatingCell"; @@ -16,7 +17,7 @@ export default function ITGCoreCells({ <> - + ); diff --git a/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx b/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx index 2d63cddd9..52d3199ef 100644 --- a/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import JubeatJudgementCell from "../cells/JubeatJudgementCell"; import JubeatScoreCell from "../cells/JubeatScoreCell"; import JubilityCell from "../cells/JubilityCell"; @@ -7,7 +8,6 @@ import LampCell from "../cells/LampCell"; export default function JubeatCoreCells({ sc, - rating, }: { sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">; rating: keyof ScoreDocument["calculatedData"]; @@ -16,7 +16,7 @@ export default function JubeatCoreCells({ <> - + ); diff --git a/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx b/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx index e4d4b58b4..477c0cc7e 100644 --- a/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/MaimaiDXCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import LampCell from "../cells/LampCell"; import RatingCell from "../cells/RatingCell"; import ScoreCell from "../cells/ScoreCell"; @@ -16,7 +17,7 @@ export default function MaimaiDXCoreCells({ <> - + ); diff --git a/client/src/components/tables/game-core-cells/MusecaCoreCells.tsx b/client/src/components/tables/game-core-cells/MusecaCoreCells.tsx index ccf868fa8..46a83e248 100644 --- a/client/src/components/tables/game-core-cells/MusecaCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/MusecaCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { IDStrings, PBScoreDocument, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common"; +import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import LampCell from "../cells/LampCell"; import MillionsScoreCell from "../cells/MillionsScoreCell"; import MusecaJudgementCell from "../cells/MusecaJudgementCell"; @@ -10,13 +11,17 @@ export default function MusecaCoreCells({ rating, }: { sc: ScoreDocument<"museca:Single"> | PBScoreDocument<"museca:Single">; - rating: ScoreCalculatedDataLookup[IDStrings]; + rating: ScoreRatingAlgorithms[GPTString]; }) { return ( <> - + - + ); diff --git a/client/src/components/tables/game-core-cells/PMSCoreCells.tsx b/client/src/components/tables/game-core-cells/PMSCoreCells.tsx index d5e999033..93dbcebb8 100644 --- a/client/src/components/tables/game-core-cells/PMSCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/PMSCoreCells.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { IDStrings, PBScoreDocument, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common"; +import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common"; import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell"; import DeltaCell from "../cells/DeltaCell"; import RatingCell from "../cells/RatingCell"; @@ -12,7 +12,7 @@ export default function PMSCoreCells({ sc: | PBScoreDocument<"pms:Controller" | "pms:Keyboard"> | ScoreDocument<"pms:Controller" | "pms:Keyboard">; - rating: ScoreCalculatedDataLookup[IDStrings]; + rating: ScoreRatingAlgorithms[GPTString]; }) { return ( <> diff --git a/client/src/components/tables/game-core-cells/PopnCoreCells.tsx b/client/src/components/tables/game-core-cells/PopnCoreCells.tsx index 72320119a..6010a3b6d 100644 --- a/client/src/components/tables/game-core-cells/PopnCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/PopnCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import MillionsScoreCell from "../cells/MillionsScoreCell"; import PopnJudgementCell from "../cells/PopnJudgementCell"; import PopnLampCell from "../cells/PopnLampCell"; @@ -14,7 +15,11 @@ export default function PopnCoreCells({ }) { return ( <> - + diff --git a/client/src/components/tables/game-core-cells/SDVXCoreCells.tsx b/client/src/components/tables/game-core-cells/SDVXCoreCells.tsx index fd06703c3..91d09aded 100644 --- a/client/src/components/tables/game-core-cells/SDVXCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/SDVXCoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { ChartDocument, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import MillionsScoreCell from "../cells/MillionsScoreCell"; import SDVXJudgementCell from "../cells/SDVXJudgementCell"; import SDVXLampCell from "../cells/SDVXLampCell"; @@ -14,7 +15,11 @@ export default function SDVXScoreCoreCells({ }) { return ( <> - + diff --git a/client/src/components/tables/game-core-cells/ScoreCoreCells.tsx b/client/src/components/tables/game-core-cells/ScoreCoreCells.tsx index b96b3062f..f6ecc4ae3 100644 --- a/client/src/components/tables/game-core-cells/ScoreCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/ScoreCoreCells.tsx @@ -2,11 +2,13 @@ import React from "react"; import { ChartDocument, Game, - IDStrings, + GPTString, PBScoreDocument, - ScoreCalculatedDataLookup, + ScoreRatingAlgorithms, ScoreDocument, + AnyScoreRatingAlg, } from "tachi-common"; +import useScoreRatingAlg from "components/util/useScoreRatingAlg"; import BMSCoreCells from "./BMSCoreCells"; import CHUNITHMCoreCells from "./CHUNITHMCoreCells"; import IIDXCoreCells from "./IIDXCoreCells"; @@ -28,22 +30,28 @@ export default function ScoreCoreCells({ }: { score: ScoreDocument | PBScoreDocument; chart: ChartDocument; - rating: ScoreCalculatedDataLookup[IDStrings]; + rating?: AnyScoreRatingAlg; game: Game; -}): React.ReactChild { +}): JSX.Element { + const [defaultRating] = useScoreRatingAlg(game, chart.playtype); + + // fallback to this users preferred rating if none provided. + // @ts-expect-error whateverr + const rt: AnyScoreRatingAlg = rating ?? defaultRating; + const sc = score as any; // lazy hack switch (game) { case "iidx": return ( } sc={sc} /> ); case "bms": - return ; + return ; case "sdvx": case "usc": return ( @@ -55,22 +63,22 @@ export default function ScoreCoreCells({ /> ); case "museca": - return ; + return ; case "wacca": - return ; + return ; case "popn": - return ; + return ; case "jubeat": - return ; + return ; case "chunithm": - return ; + return ; case "gitadora": - return ; + return ; case "pms": - return ; + return ; case "itg": - return ; + return ; case "maimaidx": - return ; + return ; } } diff --git a/client/src/components/tables/game-core-cells/WACCACoreCells.tsx b/client/src/components/tables/game-core-cells/WACCACoreCells.tsx index 8910be953..9f47fc0b5 100644 --- a/client/src/components/tables/game-core-cells/WACCACoreCells.tsx +++ b/client/src/components/tables/game-core-cells/WACCACoreCells.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; +import { GetEnumColour } from "lib/game-implementations"; import LampCell from "../cells/LampCell"; import MillionsScoreCell from "../cells/MillionsScoreCell"; import RatingCell from "../cells/RatingCell"; @@ -14,9 +15,13 @@ export default function WACCACoreCells({ }) { return ( <> - + - + ); diff --git a/client/src/components/tables/headers/ChartHeader.tsx b/client/src/components/tables/headers/ChartHeader.tsx index 810e5ef29..272d9feae 100644 --- a/client/src/components/tables/headers/ChartHeader.tsx +++ b/client/src/components/tables/headers/ChartHeader.tsx @@ -1,47 +1,37 @@ -import { IsNotNullish } from "util/misc"; -import { NumericSOV } from "util/sorts"; -import { ChartDocument, Game } from "tachi-common"; +import { IsNullish } from "util/misc"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; +import { GPTRatingSystem } from "lib/types"; +import { ChartDocument, Game, GetGPTString } from "tachi-common"; import { Header } from "../components/TachiTable"; -function CascadingTierlistValue( - chart: ChartDocument, - ...tiers: (keyof ChartDocument["tierlistInfo"])[] -) { - for (const tier of tiers) { - if (IsNotNullish(chart.tierlistInfo[tier])) { - return chart.tierlistInfo[tier]!.value; +export function CascadingRatingValue(game: Game, chartA: ChartDocument, chartB: ChartDocument) { + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, chartA.playtype)]; + + if (chartA.levelNum !== chartB.levelNum) { + return chartA.levelNum - chartB.levelNum; + } + + for (const rating of gptImpl.ratingSystems as GPTRatingSystem[]) { + const a = rating.toNumber(chartA); + const b = rating.toNumber(chartB); + if (a !== b) { + if (IsNullish(a)) { + return -Infinity; + } else if (IsNullish(b)) { + return Infinity; + } + + return a! - b!; } } - return chart.levelNum; + // these things are equal on all counts + return 0; } export default function ChartHeader( game: Game, chartGetter: (k: D) => ChartDocument ): Header { - let sortFn: (d: D) => number = (k) => chartGetter(k).levelNum; - - if (game === "iidx") { - sortFn = (k) => CascadingTierlistValue(chartGetter(k), "kt-HC", "kt-NC"); - } else if (game === "bms" || game === "pms") { - sortFn = (k) => CascadingTierlistValue(chartGetter(k), "sgl-EC", "sgl-HC"); - } else if (game === "itg") { - return [ - "Chart", - "Chart", - (a, b) => { - const ac = chartGetter(a) as ChartDocument<"itg:Stamina">; - const bc = chartGetter(b) as ChartDocument<"itg:Stamina">; - - if (ac.levelNum === bc.levelNum) { - return ac.data.displayBPM - bc.data.displayBPM; - } - - return ac.levelNum - bc.levelNum; - }, - ]; - } - - return ["Chart", "Chart", NumericSOV(sortFn)]; + return ["Chart", "Chart", (a, b) => CascadingRatingValue(game, chartGetter(a), chartGetter(b))]; } diff --git a/client/src/components/tables/headers/GameHeaders.tsx b/client/src/components/tables/headers/GameHeaders.tsx index 35ee2ee5d..1e5f5b979 100644 --- a/client/src/components/tables/headers/GameHeaders.tsx +++ b/client/src/components/tables/headers/GameHeaders.tsx @@ -4,11 +4,13 @@ import React from "react"; import { Game, GetGamePTConfig, - IDStrings, + GPTString, PBScoreDocument, - ScoreCalculatedDataLookup, + ScoreRatingAlgorithms, ScoreDocument, Playtype, + AnyScoreRatingAlg, + GetGPTString, } from "tachi-common"; import { SetState } from "types/react"; import { @@ -18,6 +20,7 @@ import { RivalChartDataset, ScoreDataset, } from "types/tables"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import SelectableRating from "../components/SelectableRating"; import { Header, ZTableTHProps } from "../components/TachiTable"; @@ -26,28 +29,16 @@ export function GetGPTCoreHeaders< >( game: Game, playtype: Playtype, - rating: ScoreCalculatedDataLookup[IDStrings], - setRating: SetState, + rating: ScoreRatingAlgorithms[GPTString], + setRating: SetState, kMapToScoreOrPB: (k: Dataset[0]) => PBScoreDocument | ScoreDocument | null ): Header[] { - const ScoreHeader: Header = [ - "Score", - "Score", - NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity), - ]; - - const LampHeader: Header = [ - "Lamp", - "Lamp", - NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.lampIndex ?? -Infinity), - ]; + const gptConfig = GetGamePTConfig(game, playtype); let RatingHeader: Header; - const gptConfig = GetGamePTConfig(game, playtype); - - if (gptConfig.scoreRatingAlgs.length === 1) { - const alg = gptConfig.scoreRatingAlgs[0]; + if (Object.keys(gptConfig.scoreRatingAlgs).length === 1) { + const alg = Object.keys(gptConfig.scoreRatingAlgs)[0] as AnyScoreRatingAlg; RatingHeader = [ UppercaseFirst(alg), @@ -72,108 +63,41 @@ export function GetGPTCoreHeaders< ]; } - switch (game) { - case "sdvx": - case "museca": - case "usc": - return [ - ScoreHeader, - [ - "Near - Miss", - "Nr. Ms.", - NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity), - ], - LampHeader, - RatingHeader, - ]; - case "iidx": - case "bms": - case "pms": - return [ - ScoreHeader, - [ - "Deltas", - "Deltas", - NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity), - ], - LampHeader, - RatingHeader, - ]; - case "gitadora": - case "jubeat": - case "wacca": - case "chunithm": - case "maimaidx": - case "itg": - return [ - ScoreHeader, - [ - "Judgements", - "Hits", - NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity), - ], - LampHeader, - RatingHeader, - ]; - case "popn": - return [ - ScoreHeader, - [ - "Judgements", - "Hits", - NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity), - ], - [ - "Lamp", - "Lamp", - (a, b) => { - const aSc = kMapToScoreOrPB(a) as - | ScoreDocument<"popn:9B"> - | PBScoreDocument<"popn:9B">; - const bSc = kMapToScoreOrPB(b) as - | ScoreDocument<"popn:9B"> - | PBScoreDocument<"popn:9B">; + const implHeaders = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)].scoreHeaders; - if (!aSc) { - return -Infinity; - } + const outHeaders: Array> = []; - if (aSc && !bSc) { - return Infinity; - } + for (const header of implHeaders) { + if (header[2]) { + // replace the sort function with one that correctly resolves + // pbs + outHeaders.push([ + header[0], + header[1], + (a, b) => { + const pbA = kMapToScoreOrPB(a); + const pbB = kMapToScoreOrPB(b); - if (aSc.scoreData.lampIndex === bSc.scoreData.lampIndex) { - if ( - !aSc.scoreData.hitMeta.specificClearType || - !bSc.scoreData.hitMeta.specificClearType - ) { - return -Infinity; - } + if (!pbA) { + return -Infinity; + } + if (!pbB) { + return Infinity; + } - return ( - popnClearTypeToInt[aSc.scoreData.hitMeta.specificClearType] - - popnClearTypeToInt[bSc.scoreData.hitMeta.specificClearType] - ); - } - - return aSc.scoreData.lampIndex - bSc.scoreData.lampIndex; - }, - ], - RatingHeader, - ]; + // typescript is NOT happy with what i've done here + // LOL, who cares. + // safety is for chumps + return (header[2] as any)!(pbA, pbB); + }, + ]); + } else { + outHeaders.push(header as Header); + } } -} -const popnClearTypeToInt = { - failedCircle: 0, - failedDiamond: 1, - failedStar: 2, - easyClear: 3, - clearCircle: 4, - clearDiamond: 5, - clearStar: 6, - fullComboCircle: 7, - fullComboDiamond: 8, - fullComboStar: 9, - perfect: 10, -} as const; + // all games have a rating header after their impl header. + outHeaders.push(RatingHeader); + + return outHeaders; +} diff --git a/client/src/components/tables/history-scores/HistoryScoreTable.tsx b/client/src/components/tables/history-scores/HistoryScoreTable.tsx index b05626e57..a610b9a83 100644 --- a/client/src/components/tables/history-scores/HistoryScoreTable.tsx +++ b/client/src/components/tables/history-scores/HistoryScoreTable.tsx @@ -4,9 +4,9 @@ import React, { useState } from "react"; import { ChartDocument, Game, - IDStrings, + GPTString, integer, - ScoreCalculatedDataLookup, + ScoreRatingAlgorithms, ScoreDocument, Playtype, } from "tachi-common"; @@ -67,7 +67,7 @@ function Row({ game, }: { sc: ScoreDocument; - rating: ScoreCalculatedDataLookup[IDStrings]; + rating: ScoreRatingAlgorithms[GPTString]; game: Game; chart: ChartDocument; }) { diff --git a/client/src/components/tables/pbs/PBTable.tsx b/client/src/components/tables/pbs/PBTable.tsx index 87d00e933..d81eede68 100644 --- a/client/src/components/tables/pbs/PBTable.tsx +++ b/client/src/components/tables/pbs/PBTable.tsx @@ -3,7 +3,7 @@ import { CreateDefaultPBSearchParams } from "util/tables/create-search"; import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders"; import useScoreRatingAlg from "components/util/useScoreRatingAlg"; import React, { useState } from "react"; -import { Game, IDStrings, ScoreCalculatedDataLookup, Playtype } from "tachi-common"; +import { Game, GPTString, ScoreRatingAlgorithms, Playtype, AnyScoreRatingAlg } from "tachi-common"; import { PBDataset } from "types/tables"; import DropdownIndicatorCell from "../cells/DropdownIndicatorCell"; import IndexCell from "../cells/IndexCell"; @@ -20,7 +20,7 @@ import { EmptyHeader } from "../headers/IndicatorHeader"; import { CreateRankingHeader } from "../headers/RankingHeader"; import PBLeadingRows from "./PBLeadingRows"; -export default function PBTable({ +export default function PBTable({ dataset, game, indexCol = true, @@ -31,14 +31,14 @@ export default function PBTable({ alg, defaultRankingViewMode, }: { - dataset: PBDataset; + dataset: PBDataset; indexCol?: boolean; showPlaycount?: boolean; showUser?: boolean; showChart?: boolean; playtype: Playtype; game: Game; - alg?: ScoreCalculatedDataLookup[I]; + alg?: AnyScoreRatingAlg; defaultRankingViewMode?: RankingViewMode | null; }) { const defaultRating = useScoreRatingAlg(game, playtype); @@ -48,7 +48,7 @@ export default function PBTable({ defaultRankingViewMode ?? "global" ); - const headers: Header[0]>[] = [ + const headers: Header[] = [ ...GetPBLeadingHeaders( showUser, showChart, @@ -98,7 +98,7 @@ export default function PBTable({ ); } -function Row({ +function Row({ pb, indexCol, showPlaycount, @@ -108,7 +108,7 @@ function Row({ rating, rankingViewMode, }: { - pb: PBDataset[0]; + pb: PBDataset[0]; indexCol: boolean; showPlaycount: boolean; showChart: boolean; @@ -116,7 +116,7 @@ function Row({ game: Game; playtype: Playtype; // ts bug? - rating: any; // ScoreCalculatedDataLookup[I]; + rating: any; // ScoreRatingAlgorithms[I]; rankingViewMode: RankingViewMode; }) { const scoreState = usePBState(pb); diff --git a/client/src/components/tables/rivals/ComparePBsTable.tsx b/client/src/components/tables/rivals/ComparePBsTable.tsx index 80d3c0857..2b0a59e5f 100644 --- a/client/src/components/tables/rivals/ComparePBsTable.tsx +++ b/client/src/components/tables/rivals/ComparePBsTable.tsx @@ -1,8 +1,7 @@ import { NumericSOV, StrSOV } from "util/sorts"; import { CreatePBCompareSearchParams } from "util/tables/create-search"; -import React, { useState } from "react"; -import { Game, IDStrings, Playtype } from "tachi-common"; -import { ESDCompare } from "tachi-common/lib/esd"; +import React, { useEffect, useState } from "react"; +import { Game, GetGamePTConfig, GetScoreMetricConf, Playtype } from "tachi-common"; import { ComparePBsDataset } from "types/tables"; import DifficultyCell from "../cells/DifficultyCell"; import PBCompareCell from "../cells/PBCompareCell"; @@ -12,22 +11,26 @@ import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable"; import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells"; import ChartHeader from "../headers/ChartHeader"; -export default function ComparePBsTable({ +export default function ComparePBsTable({ dataset, game, playtype, baseUser, compareUser, - shouldESD, }: { - dataset: ComparePBsDataset; + dataset: ComparePBsDataset; game: Game; playtype: Playtype; baseUser: string; compareUser: string; - shouldESD: boolean; }) { - const [compareType, setCompareType] = useState<"score" | "lamp">("score"); + const gptConfig = GetGamePTConfig(game, playtype); + + const [metric, setMetric] = useState(gptConfig.defaultMetric); + + useEffect(() => { + setMetric(gptConfig.defaultMetric); + }, [gptConfig]); const headers: Header[] = [ ChartHeader(game, (d) => d.chart), @@ -45,26 +48,33 @@ export default function ComparePBsTable({ return -Infinity; } - if (compareType === "score") { - if (shouldESD && x.base.scoreData.esd && x.compare.scoreData.esd) { - // esd is inverted, whoops - const e = ESDCompare(x.compare.scoreData.esd, x.base.scoreData.esd); + const conf = GetScoreMetricConf(gptConfig, metric); - if (e !== 0) { - return e; - } - } - - return x.base.scoreData.percent - x.compare.scoreData.percent; + if (!conf) { + return 0; // wut } - return x.base.scoreData.lampIndex - x.compare.scoreData.lampIndex; + if (conf.type === "ENUM") { + return ( + // @ts-expect-error this will work + x.base.scoreData.enumIndexes[metric] - + // @ts-expect-error this will work + x.compare.scoreData.enumIndexes[metric] + ); + } + + return ( + // @ts-expect-error this will work + x.base.scoreData[metric] - + // @ts-expect-error this will work + x.compare.scoreData[metric] + ); }), (thProps: ZTableTHProps) => ( ), @@ -74,32 +84,20 @@ export default function ComparePBsTable({ return ( ( - - )} + rowFunction={(data) => } /> ); } -function Row({ - data, - game, - compareType, - shouldESD, -}: { - data: ComparePBsDataset[0]; - game: Game; - compareType: "score" | "lamp"; - shouldESD: boolean; -}) { +function Row({ data, game, metric }: { data: ComparePBsDataset[0]; game: Game; metric: string }) { + const metricConf = GetScoreMetricConf(GetGamePTConfig(game, data.chart.playtype), metric)!; + return ( @@ -110,12 +108,12 @@ function Row({ Not Played )} {data.compare ? ( diff --git a/client/src/components/tables/rivals/RivalChartTable.tsx b/client/src/components/tables/rivals/RivalChartTable.tsx index 03a5495eb..eb980d687 100644 --- a/client/src/components/tables/rivals/RivalChartTable.tsx +++ b/client/src/components/tables/rivals/RivalChartTable.tsx @@ -1,7 +1,13 @@ import { NumericSOV, StrSOV } from "util/sorts"; import useScoreRatingAlg from "components/util/useScoreRatingAlg"; import React, { useState } from "react"; -import { ChartDocument, Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common"; +import { + AnyScoreRatingAlg, + ChartDocument, + Game, + GPTString, + ScoreRatingAlgorithms, +} from "tachi-common"; import { RivalChartDataset } from "types/tables"; import IndexCell from "../cells/IndexCell"; import RankingCell, { RankingViewMode } from "../cells/RankingCell"; @@ -15,12 +21,12 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells"; import { GetGPTCoreHeaders } from "../headers/GameHeaders"; import { CreateRankingHeader } from "../headers/RankingHeader"; -export default function RivalChartTable({ +export default function RivalChartTable({ dataset, game, chart, }: { - dataset: RivalChartDataset; + dataset: RivalChartDataset; game: Game; chart: ChartDocument; }) { @@ -70,16 +76,16 @@ export default function RivalChartTable({ ); } -function Row({ +function Row({ data, rating, game, chart, rankingViewMode, }: { - data: RivalChartDataset[0]; + data: RivalChartDataset[0]; game: Game; - rating: ScoreCalculatedDataLookup[I]; + rating: AnyScoreRatingAlg; chart: ChartDocument; rankingViewMode: RankingViewMode; }) { diff --git a/client/src/components/tables/scores/ScoreTable.tsx b/client/src/components/tables/scores/ScoreTable.tsx index 7fc1085cd..d88653a91 100644 --- a/client/src/components/tables/scores/ScoreTable.tsx +++ b/client/src/components/tables/scores/ScoreTable.tsx @@ -2,9 +2,10 @@ import { NumericSOV, StrSOV } from "util/sorts"; import { CreateDefaultScoreSearchParams } from "util/tables/create-search"; import useScoreRatingAlg from "components/util/useScoreRatingAlg"; import React, { useState } from "react"; -import { Game, IDStrings, integer, ScoreCalculatedDataLookup, Playtype } from "tachi-common"; +import { AnyScoreRatingAlg, Game, Playtype, integer } from "tachi-common"; import { ScoreDataset } from "types/tables"; import DifficultyCell from "../cells/DifficultyCell"; +import DropdownIndicatorCell from "../cells/DropdownIndicatorCell"; import IndicatorsCell from "../cells/IndicatorsCell"; import TimestampCell from "../cells/TimestampCell"; import TitleCell from "../cells/TitleCell"; @@ -17,9 +18,8 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells"; import ChartHeader from "../headers/ChartHeader"; import { GetGPTCoreHeaders } from "../headers/GameHeaders"; import IndicatorHeader, { EmptyHeader } from "../headers/IndicatorHeader"; -import DropdownIndicatorCell from "../cells/DropdownIndicatorCell"; -export default function ScoreTable({ +export default function ScoreTable({ dataset, pageLen, playtype, @@ -28,18 +28,18 @@ export default function ScoreTable({ alg, noTopDisplayStr, }: { - dataset: ScoreDataset; + dataset: ScoreDataset; pageLen?: integer; playtype: Playtype; userCol?: boolean; game: Game; - alg?: ScoreCalculatedDataLookup[I]; + alg?: AnyScoreRatingAlg; noTopDisplayStr?: boolean; }) { const defaultRating = useScoreRatingAlg(game, playtype); const [rating, setRating] = useState(alg ?? defaultRating); - const headers: Header[0]>[] = [ + const headers: Header[] = [ ChartHeader(game, (k) => k.__related.chart), IndicatorHeader, ["Song", "Song", StrSOV((x) => x.__related.song.title)], @@ -74,15 +74,15 @@ export default function ScoreTable({ ); } -function Row({ +function Row({ sc, rating, playtype, userCol, game, }: { - sc: ScoreDataset[0]; - rating: ScoreCalculatedDataLookup[I]; + sc: ScoreDataset[0]; + rating: AnyScoreRatingAlg; game: Game; playtype: Playtype; userCol: boolean; diff --git a/client/src/components/tables/seeds/SeedsBMSCourseLookupTable.tsx b/client/src/components/tables/seeds/SeedsBMSCourseLookupTable.tsx index 7165c6262..06337e189 100644 --- a/client/src/components/tables/seeds/SeedsBMSCourseLookupTable.tsx +++ b/client/src/components/tables/seeds/SeedsBMSCourseLookupTable.tsx @@ -1,7 +1,7 @@ import { StrSOV } from "util/sorts"; import { SearchFunctions } from "util/ztable/search"; import React from "react"; -import { FormatChart } from "tachi-common"; +import { FormatChart, GetBMSCourseIndex } from "tachi-common"; import { BMSCourseWithRelated, CellsRenderFN } from "types/seeds"; import { Header } from "../components/TachiTable"; @@ -16,7 +16,7 @@ export const SeedsBMSCourseLookupHeaders: Header[] = [ const b2 = `${b.playtype} ${b.set}`; if (a2 === b2) { - return a.value - b.value; + return GetBMSCourseIndex(a) - GetBMSCourseIndex(b); } return a2.localeCompare(b2); diff --git a/client/src/components/tables/sessions/GenericSessionTable.tsx b/client/src/components/tables/sessions/GenericSessionTable.tsx index 5e9bae2f1..a61179507 100644 --- a/client/src/components/tables/sessions/GenericSessionTable.tsx +++ b/client/src/components/tables/sessions/GenericSessionTable.tsx @@ -8,10 +8,10 @@ import { Link } from "react-router-dom"; import { Game, GetGamePTConfig, - IDStrings, + GPTString, integer, UserDocument, - SessionCalculatedDataLookup, + SessionRatingAlgorithms, SessionDocument, Playtype, SessionScoreInfo, @@ -41,7 +41,7 @@ export default function GenericSessionTable({ const defaultRating = useSessionRatingAlg(game, playtype); - const [alg, setAlg] = useState(defaultRating); + const [alg, setAlg] = useState(defaultRating); const headers: Header[] = [ ["Name", "Name", StrSOV((x) => x.name)], @@ -51,7 +51,7 @@ export default function GenericSessionTable({ ["Timestamp", "Timestamp", NumericSOV((x) => x.timeStarted)], ]; - if (gptConfig.sessionRatingAlgs.length > 1) { + if (Object.keys(gptConfig.sessionRatingAlgs).length > 1) { headers[2] = [ "Rating", "Rating", @@ -108,7 +108,7 @@ function Row({ { data: SessionDataset[0]; // reqUser: PublicUserDocument; - ratingAlg: SessionCalculatedDataLookup[IDStrings]; + ratingAlg: SessionRatingAlgorithms[GPTString]; reqUser: UserDocument; indexCol?: boolean; }) { diff --git a/client/src/components/targets/AddNewGoalForQuestModal.tsx b/client/src/components/targets/AddNewGoalForQuestModal.tsx index 5ff7d03eb..65bcb6b8b 100644 --- a/client/src/components/targets/AddNewGoalForQuestModal.tsx +++ b/client/src/components/targets/AddNewGoalForQuestModal.tsx @@ -6,11 +6,18 @@ import Divider from "components/util/Divider"; import Select from "components/util/Select"; import React, { useEffect, useState } from "react"; import { Button, Col, Form, InputGroup, Modal, Row } from "react-bootstrap"; -import { FolderDocument, FormatChart, GetGamePTConfig, GoalDocument } from "tachi-common"; +import { + FolderDocument, + FormatChart, + GetGamePTConfig, + GetScoreMetricConf, + GoalDocument, +} from "tachi-common"; import { SongChartsSearch } from "types/api-returns"; import { GamePT, SetState } from "types/react"; import { RawQuestGoal } from "types/tachi"; import { OptionsOrGroups, GroupBase } from "react-select"; +import { ConfEnumScoreMetric } from "tachi-common/types/metrics"; import { RenderGoalCriteriaPicker } from "./SetNewGoalModal"; export default function AddNewGoalForQuestModal({ @@ -30,14 +37,16 @@ export default function AddNewGoalForQuestModal({ } & GamePT) { const gptConfig = GetGamePTConfig(game, playtype); + const enumConf = GetScoreMetricConf( + gptConfig, + gptConfig.preferredDefaultEnum + ) as ConfEnumScoreMetric; + const [criteria, setCriteria] = useState( initialState?.goal.criteria ?? { mode: "single", - key: gptConfig.scoreBucket === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex", - value: - gptConfig.scoreBucket === "grade" - ? gptConfig.grades.indexOf(gptConfig.clearGrade) - : gptConfig.lamps.indexOf(gptConfig.clearLamp), + key: gptConfig.preferredDefaultEnum, + value: enumConf.values.indexOf(enumConf.minimumRelevantValue), } ); diff --git a/client/src/components/targets/AddNewQuestModal.tsx b/client/src/components/targets/AddNewQuestModal.tsx index 656902d13..38597f0d5 100644 --- a/client/src/components/targets/AddNewQuestModal.tsx +++ b/client/src/components/targets/AddNewQuestModal.tsx @@ -3,7 +3,7 @@ import Select from "components/util/Select"; import { TachiConfig } from "lib/config"; import React, { useState } from "react"; import { Button, Col, Modal, Row } from "react-bootstrap"; -import { FormatGame, Game, GetGameConfig, IDStrings, Playtype } from "tachi-common"; +import { FormatGame, Game, GetGameConfig, GPTString, Playtype } from "tachi-common"; import { SetState } from "types/react"; import { RawQuestDocument } from "types/tachi"; @@ -16,7 +16,7 @@ export default function AddNewQuestModal({ setShow: SetState; onCreate: (rawQuest: RawQuestDocument) => void; }) { - const [gpt, setGPT] = useState(null); + const [gpt, setGPT] = useState(null); return ( setShow(false)}> diff --git a/client/src/components/targets/SetNewGoalModal.tsx b/client/src/components/targets/SetNewGoalModal.tsx index 076f57e36..15c1117e0 100644 --- a/client/src/components/targets/SetNewGoalModal.tsx +++ b/client/src/components/targets/SetNewGoalModal.tsx @@ -35,9 +35,9 @@ export default function SetNewGoalModal({ const [criteria, setCriteria] = useState({ mode: "single", - key: gptConfig.scoreBucket === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex", + key: gptConfig.preferredDefaultEnum === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex", value: - gptConfig.scoreBucket === "grade" + gptConfig.preferredDefaultEnum === "grade" ? gptConfig.grades.indexOf(gptConfig.clearGrade) : gptConfig.lamps.indexOf(gptConfig.clearLamp), }); diff --git a/client/src/components/user/UGPTRankingData.tsx b/client/src/components/user/UGPTRankingData.tsx index 38d2f9067..b1f6a4b53 100644 --- a/client/src/components/user/UGPTRankingData.tsx +++ b/client/src/components/user/UGPTRankingData.tsx @@ -2,7 +2,7 @@ import { UppercaseFirst } from "util/misc"; import { useProfileRatingAlg } from "components/util/useScoreRatingAlg"; import React from "react"; import { Link } from "react-router-dom"; -import { IDStrings, integer, ProfileRatingLookup } from "tachi-common"; +import { GPTString, integer, ProfileRatingAlgorithms } from "tachi-common"; import { GamePT } from "types/react"; export default function RankingData({ @@ -11,7 +11,7 @@ export default function RankingData({ userID, playtype, }: { - rankingData: Record; + rankingData: Record; userID: integer; } & GamePT) { const alg = useProfileRatingAlg(game, playtype); @@ -19,7 +19,7 @@ export default function RankingData({ const extendData = []; for (const k in rankingData) { - const key = k as ProfileRatingLookup[IDStrings]; + const key = k as ProfileRatingAlgorithms[GPTString]; if (key !== alg) { extendData.push( diff --git a/client/src/components/user/UGPTStatShowcase.tsx b/client/src/components/user/UGPTStatShowcase.tsx index 68aee2c29..045459767 100644 --- a/client/src/components/user/UGPTStatShowcase.tsx +++ b/client/src/components/user/UGPTStatShowcase.tsx @@ -24,6 +24,7 @@ import { ShowcaseStatDetails, SongDocument, Playtype, + GetScoreMetricConf, } from "tachi-common"; import { UGPTPreferenceStatsReturn } from "types/api-returns"; import { GamePT, UGPT } from "types/react"; @@ -259,14 +260,14 @@ function StatDelta({ v1, v2, mode, - property, + metric: property, game, playtype, }: { v1: number; v2?: number; mode: "folder" | "chart"; - property: UGPTPreferenceStatsReturn["stat"]["metric"]; + metric: string; game: Game; playtype: Playtype; }) { @@ -276,11 +277,8 @@ function StatDelta({ v2 = 0; } - let d: string | number = v2 - v1; const formattedV2 = FormatValue(game, playtype, mode, property, v2); - if (property === "percent") { - d = `${d.toFixed(2)}%`; - } + const d = FormatValue(game, playtype, mode, property, v2 - v1); let colour; if (v2 === v1) { @@ -310,7 +308,7 @@ export function FormatValue( game: Game, playtype: Playtype, mode: "folder" | "chart", - prop: "grade" | "lamp" | "score" | "percent" | "playcount", + metric: string, value: number ) { if (mode === "folder") { @@ -318,42 +316,21 @@ export function FormatValue( } const gptConfig = GetGamePTConfig(game, playtype); + const conf = GetScoreMetricConf(gptConfig, metric); - if (prop === "percent") { - return `${value.toFixed(2)}%`; - } else if (prop === "grade") { - return gptConfig.grades[value]; - } else if (prop === "lamp") { - return gptConfig.lamps[value]; + if (!conf) { + return "UNKNOWN METRIC"; + } + + if (conf.type === "ENUM") { + return conf.values[value]; + } else if (conf.type === "DECIMAL" || conf.type === "INTEGER") { + return conf.formatter(value); } return value; } -export function FormatPropertyGTE( - game: Game, - playtype: Playtype, - prop: "grade" | "lamp" | "score" | "percent" | "playcount", - gte: number -) { - const gptConfig = GetGamePTConfig(game, playtype); - - if (gte === null) { - return "NO DATA"; - } - - if (prop === "grade") { - return gptConfig.grades[gte]; - } else if (prop === "lamp") { - return gptConfig.lamps[gte]; - } else if (prop === "score" || prop === "playcount") { - return gte; - } - // else if (prop === "percent") { - return gte.toFixed(2); - // } -} - export function GetStatName( stat: ShowcaseStatDetails, game: Game, @@ -398,14 +375,14 @@ export function StatDisplay({

{UppercaseFirst(stat.metric)}:{" "} - {FormatPropertyGTE(game, playtype, stat.metric, result.value)} + {FormatValue(game, playtype, stat.mode, stat.metric, result.value)}

{user && user.id !== reqUser.id && ( @@ -432,7 +409,7 @@ export function StatDisplay({
{UppercaseFirst(stat.metric)} >={" "} - {FormatPropertyGTE(game, playtype, stat.metric, stat.gte)} + {FormatValue(game, playtype, stat.mode, stat.metric, stat.gte)}

{result.value} @@ -447,7 +424,7 @@ export function StatDisplay({ v1={statData.result.value} v2={compareData?.result.value} mode={stat.mode} - property={stat.metric} + metric={stat.metric} game={game} playtype={playtype} /> diff --git a/client/src/components/user/UGPTStatsOverview.tsx b/client/src/components/user/UGPTStatsOverview.tsx index 36b84aa3f..55cc0ee56 100644 --- a/client/src/components/user/UGPTStatsOverview.tsx +++ b/client/src/components/user/UGPTStatsOverview.tsx @@ -1,4 +1,4 @@ -import { FormatGPTProfileRating, FormatGPTRating, UppercaseFirst } from "util/misc"; +import { FormatGPTProfileRating, UppercaseFirst } from "util/misc"; import ClassBadge from "components/game/ClassBadge"; import QuickTooltip from "components/layout/misc/QuickTooltip"; import MiniTable from "components/tables/components/MiniTable"; @@ -6,22 +6,21 @@ import Divider from "components/util/Divider"; import React from "react"; import { GetGamePTConfig, - IDStrings, - ScoreCalculatedDataLookup, - ProfileRatingLookup, + GPTString, + ProfileRatingAlgorithms, UserGameStats, + Classes, } from "tachi-common"; -import { GameClassSets } from "tachi-common/game-classes"; export default function UGPTRatingsTable({ ugs }: { ugs: UserGameStats }) { const gptConfig = GetGamePTConfig(ugs.game, ugs.playtype); - const ratings = Object.entries(ugs.ratings) as [ProfileRatingLookup[IDStrings], number][]; + const ratings = Object.entries(ugs.ratings) as [ProfileRatingAlgorithms[GPTString], number][]; return ( <> - {(Object.keys(gptConfig.classHumanisedFormat) as GameClassSets[IDStrings][]) + {(Object.keys(gptConfig.classes) as Classes[GPTString][]) .filter((k) => ugs.classes[k] !== undefined) .map((k) => ( @@ -44,15 +43,13 @@ export default function UGPTRatingsTable({ ugs }: { ugs: UserGameStats }) { - {gptConfig.profileRatingAlgDescriptions[k]} - {k in gptConfig.scoreRatingAlgDescriptions && - gptConfig.idString !== "itg:Stamina" && ( - <> - ({UppercaseFirst(k)}:{" "} - {/* @ts-expect-error I know better. */} - {gptConfig.scoreRatingAlgDescriptions[k]}) - - )} + {gptConfig.profileRatingAlgs[k].description} + {k in gptConfig.scoreRatingAlgs && ( + <> + ({UppercaseFirst(k)}:{" "} + {gptConfig.scoreRatingAlgs[k].description}) + + )}

} wide diff --git a/client/src/components/util/useBucket.tsx b/client/src/components/util/useBucket.tsx index 50576dbdc..c7cfc8895 100644 --- a/client/src/components/util/useBucket.tsx +++ b/client/src/components/util/useBucket.tsx @@ -4,11 +4,11 @@ import useLUGPTSettings from "./useLUGPTSettings"; export function useBucket(game: Game, playtype: Playtype) { const { settings } = useLUGPTSettings(); - if (!settings?.preferences.scoreBucket) { + if (!settings?.preferences.preferredDefaultEnum) { const gptConfig = GetGamePTConfig(game, playtype); - return gptConfig.scoreBucket; + return gptConfig.preferredDefaultEnum; } - return settings.preferences.scoreBucket; + return settings.preferences.preferredDefaultEnum; } diff --git a/client/src/components/util/useLUGPTSettings.tsx b/client/src/components/util/useLUGPTSettings.tsx index ece42fe78..2326f6498 100644 --- a/client/src/components/util/useLUGPTSettings.tsx +++ b/client/src/components/util/useLUGPTSettings.tsx @@ -1,13 +1,13 @@ import { UGPTContext } from "context/UGPTContext"; import { useContext } from "react"; -import { IDStrings, UGPTSettings } from "tachi-common"; +import { GPTString, UGPTSettingsDocument } from "tachi-common"; -export default function useLUGPTSettings() { +export default function useLUGPTSettings() { const { loggedInData, setLoggedInData } = useContext(UGPTContext); - const settings = (loggedInData?.settings ?? null) as UGPTSettings | null; + const settings = (loggedInData?.settings ?? null) as UGPTSettingsDocument | null; - const setSettings = (newSettings: UGPTSettings) => { + const setSettings = (newSettings: UGPTSettingsDocument) => { if (!loggedInData) { throw new Error(`Tried to set settings while nobody was logged in?`); } diff --git a/client/src/components/util/usePreferredRanking.tsx b/client/src/components/util/usePreferredRanking.tsx index 54ec630a9..0953fe340 100644 --- a/client/src/components/util/usePreferredRanking.tsx +++ b/client/src/components/util/usePreferredRanking.tsx @@ -1,8 +1,8 @@ -import { UGPTSettings } from "tachi-common"; +import { UGPTSettingsDocument } from "tachi-common"; import useLUGPTSettings from "./useLUGPTSettings"; export default function usePreferredRanking(): - | UGPTSettings["preferences"]["preferredRanking"] + | UGPTSettingsDocument["preferences"]["preferredRanking"] | null { const { settings } = useLUGPTSettings(); diff --git a/client/src/components/util/useScoreRatingAlg.tsx b/client/src/components/util/useScoreRatingAlg.tsx index 2a91a2ea8..1da928449 100644 --- a/client/src/components/util/useScoreRatingAlg.tsx +++ b/client/src/components/util/useScoreRatingAlg.tsx @@ -1,55 +1,55 @@ import { Game, GetGamePTConfig, - IDStrings, - ScoreCalculatedDataLookup, - SessionCalculatedDataLookup, - ProfileRatingLookup, + GPTString, Playtype, + ProfileRatingAlgorithms, + ScoreRatingAlgorithms, + SessionRatingAlgorithms, } from "tachi-common"; import useLUGPTSettings from "./useLUGPTSettings"; -export default function useScoreRatingAlg( +export default function useScoreRatingAlg( game: Game, playtype: Playtype -): ScoreCalculatedDataLookup[I] { +): ScoreRatingAlgorithms[GPT] { const { settings } = useLUGPTSettings(); if (!settings?.preferences.preferredScoreAlg) { const gptConfig = GetGamePTConfig(game, playtype); - return gptConfig.defaultScoreRatingAlg as ScoreCalculatedDataLookup[I]; + return gptConfig.defaultScoreRatingAlg as ScoreRatingAlgorithms[GPT]; } - return settings.preferences.preferredScoreAlg as ScoreCalculatedDataLookup[I]; + return settings.preferences.preferredScoreAlg as ScoreRatingAlgorithms[GPT]; } -export function useSessionRatingAlg( +export function useSessionRatingAlg( game: Game, playtype: Playtype -): SessionCalculatedDataLookup[I] { +): SessionRatingAlgorithms[GPT] { const { settings } = useLUGPTSettings(); if (!settings?.preferences.preferredSessionAlg) { const gptConfig = GetGamePTConfig(game, playtype); - return gptConfig.defaultSessionRatingAlg as SessionCalculatedDataLookup[I]; + return gptConfig.defaultSessionRatingAlg as SessionRatingAlgorithms[GPT]; } - return settings.preferences.preferredSessionAlg as SessionCalculatedDataLookup[I]; + return settings.preferences.preferredSessionAlg as SessionRatingAlgorithms[GPT]; } -export function useProfileRatingAlg( +export function useProfileRatingAlg( game: Game, playtype: Playtype -): ProfileRatingLookup[I] { +): ProfileRatingAlgorithms[GPT] { const { settings } = useLUGPTSettings(); if (!settings?.preferences.preferredProfileAlg) { const gptConfig = GetGamePTConfig(game, playtype); - return gptConfig.defaultProfileRatingAlg as ProfileRatingLookup[I]; + return gptConfig.defaultProfileRatingAlg as ProfileRatingAlgorithms[GPT]; } - return settings.preferences.preferredProfileAlg as ProfileRatingLookup[I]; + return settings.preferences.preferredProfileAlg as ProfileRatingAlgorithms[GPT]; } diff --git a/client/src/context/UserSettingsContext.tsx b/client/src/context/UserSettingsContext.tsx index 968c3eaa6..23014062d 100644 --- a/client/src/context/UserSettingsContext.tsx +++ b/client/src/context/UserSettingsContext.tsx @@ -1,6 +1,6 @@ import { APIFetchV1 } from "util/api"; import React, { createContext, useContext, useEffect, useState } from "react"; -import { UserSettings } from "tachi-common"; +import { UserSettingsDocument } from "tachi-common"; import { JustChildren, SetState } from "types/react"; import { UserContext } from "./UserContext"; @@ -8,14 +8,14 @@ import { UserContext } from "./UserContext"; * Contains the current user's settings. */ export const UserSettingsContext = createContext<{ - settings: UserSettings | null; - setSettings: SetState; + settings: UserSettingsDocument | null; + setSettings: SetState; }>({ settings: null, setSettings: () => void 0 }); UserSettingsContext.displayName = "UserSettingsContext"; export function UserSettingsContextProvider({ children }: JustChildren) { - const [settings, setSettings] = useState(null); + const [settings, setSettings] = useState(null); const { user } = useContext(UserContext); @@ -25,7 +25,7 @@ export function UserSettingsContextProvider({ children }: JustChildren) { } (async () => { - const res = await APIFetchV1(`/users/${user.id}/settings`); + const res = await APIFetchV1(`/users/${user.id}/settings`); if (res.success) { setSettings(res.body); diff --git a/client/src/lib/game-implementations.ts b/client/src/lib/game-implementations.ts new file mode 100644 index 000000000..1f42d9d59 --- /dev/null +++ b/client/src/lib/game-implementations.ts @@ -0,0 +1,414 @@ +import { NumericSOV } from "util/sorts"; +import { COLOUR_SET, GPTString, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common"; +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 { bg, bgc } from "./games/_util"; + +type GPTClientImplementations = { + [GPT in GPTString]: GPTClientImplementation; +}; + +export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = { + "iidx:SP": IIDX_SP_IMPL, + "iidx:DP": IIDX_DP_IMPL, + "chunithm:Single": { + classColours: { + colour: { + BLUE: "info", + GREEN: bg("green"), + ORANGE: bg("orange"), + RED: "danger", + PURPLE: bg("purple"), + COPPER: bg("bronze"), + SILVER: "secondary", + GOLD: "warning", + PLATINUM: bgc("silver", "black"), + RAINBOW: "success", + }, + }, + enumColours: { + grade: { + D: COLOUR_SET.red, + C: COLOUR_SET.purple, + B: COLOUR_SET.paleBlue, + BB: COLOUR_SET.blue, + BBB: COLOUR_SET.vibrantBlue, + A: COLOUR_SET.paleGreen, + AA: COLOUR_SET.green, + AAA: COLOUR_SET.vibrantGreen, + S: COLOUR_SET.vibrantOrange, + SS: COLOUR_SET.vibrantYellow, + SSS: COLOUR_SET.teal, + }, + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.paleGreen, + "FULL COMBO": COLOUR_SET.paleBlue, + "ALL JUSTICE": COLOUR_SET.gold, + "ALL JUSTICE CRITICAL": COLOUR_SET.white, + }, + }, + difficultyColours: { + BASIC: COLOUR_SET.blue, + ADVANCED: COLOUR_SET.orange, + EXPERT: COLOUR_SET.red, + MASTER: COLOUR_SET.purple, + }, + ratingSystems: [], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x.scoreData.score)], + ["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], + ], + }, + "jubeat:Single": { + enumColours: { + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.blue, + "FULL COMBO": COLOUR_SET.teal, + EXCELLENT: COLOUR_SET.white, + }, + grade: { + E: COLOUR_SET.gray, + D: COLOUR_SET.maroon, + C: COLOUR_SET.red, + B: COLOUR_SET.blue, + A: COLOUR_SET.green, + S: COLOUR_SET.gold, + SS: COLOUR_SET.orange, + SSS: COLOUR_SET.teal, + EXC: COLOUR_SET.white, + }, + }, + classColours: { + colour: { + BLACK: bg("secondary"), + YELLOW_GREEN: bgc("yellowgreen", "black"), + GREEN: bg("green"), + LIGHT_BLUE: bgc("cyan", "black"), + BLUE: bg("info"), + VIOLET: bg("violet"), + PURPLE: bg("purple"), + PINK: bgc("pink", "black"), + ORANGE: bg("orange"), + GOLD: bg("warning"), + }, + }, + difficultyColours: { + BSC: COLOUR_SET.green, + ADV: COLOUR_SET.gold, + EXT: COLOUR_SET.red, + "HARD BSC": COLOUR_SET.darkGreen, + "HARD ADV": COLOUR_SET.orange, + "HARD EXT": COLOUR_SET.vibrantRed, + }, + ratingSystems: [], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x?.scoreData.score ?? -Infinity)], + ["Judgements", "Hits", NumericSOV((x) => x?.scoreData.score ?? -Infinity)], + ["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp ?? -Infinity)], + ], + }, + "maimaidx:Single": { + enumColours: { + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.green, + "FULL COMBO": COLOUR_SET.blue, + "FULL COMBO+": COLOUR_SET.paleBlue, + "ALL PERFECT": COLOUR_SET.gold, + "ALL PERFECT+": COLOUR_SET.teal, + }, + grade: { + D: COLOUR_SET.gray, + C: COLOUR_SET.red, + B: COLOUR_SET.maroon, + BB: COLOUR_SET.purple, + BBB: COLOUR_SET.paleGreen, + A: COLOUR_SET.green, + AA: COLOUR_SET.paleBlue, + AAA: COLOUR_SET.blue, + S: COLOUR_SET.gold, + "S+": COLOUR_SET.vibrantYellow, + SS: COLOUR_SET.paleOrange, + "SS+": COLOUR_SET.orange, + SSS: COLOUR_SET.teal, + "SSS+": COLOUR_SET.white, + }, + }, + classColours: { + dan: { + DAN_1: bg("warning"), + DAN_2: bg("warning"), + DAN_3: bg("warning"), + DAN_4: bg("warning"), + DAN_5: bg("warning"), + DAN_6: bg("warning"), + DAN_7: bg("warning"), + DAN_8: bg("warning"), + DAN_9: bg("warning"), + DAN_10: bg("warning"), + + SHINDAN_1: bg("purple"), + SHINDAN_2: bg("purple"), + SHINDAN_3: bg("purple"), + SHINDAN_4: bg("purple"), + SHINDAN_5: bg("purple"), + SHINDAN_6: bg("purple"), + SHINDAN_7: bg("purple"), + SHINDAN_8: bg("purple"), + SHINDAN_9: bg("purple"), + SHINDAN_10: bg("purple"), + + SHINKAIDEN: bg("purple"), + }, + colour: { + WHITE: bgc("white", "black"), + BLUE: bgc("cyan", "black"), + GREEN: bg("green"), + YELLOW: bgc("yellow", "black"), + RED: bg("red"), + PURPLE: bg("purple"), + BRONZE: bg("brown"), + SILVER: bg("gray"), + GOLD: bg("warning"), + PLATINUM: bgc("lightgoldenrodyellow", "black"), + + RAINBOW: { + background: + "linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)", + }, + }, + }, + difficultyColours: { + Basic: COLOUR_SET.green, + Advanced: COLOUR_SET.orange, + Expert: COLOUR_SET.red, + Master: COLOUR_SET.purple, + "Re:Master": COLOUR_SET.white, + "DX Basic": COLOUR_SET.green, + "DX Advanced": COLOUR_SET.orange, + "DX Expert": COLOUR_SET.red, + "DX Master": COLOUR_SET.purple, + "DX Re:Master": COLOUR_SET.white, + }, + ratingSystems: [], + scoreHeaders: [ + ["Percent", "%", NumericSOV((x) => x?.scoreData.percent)], + ["Judgements", "Hits", NumericSOV((x) => x?.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)], + ], + }, + "museca:Single": { + enumColours: { + grade: { + 没: COLOUR_SET.gray, + 拙: COLOUR_SET.maroon, + 凡: COLOUR_SET.red, + 佳: COLOUR_SET.paleGreen, + 良: COLOUR_SET.paleBlue, + 優: COLOUR_SET.green, + 秀: COLOUR_SET.blue, + 傑: COLOUR_SET.teal, + 傑G: COLOUR_SET.gold, + }, + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.green, + "CONNECT ALL": COLOUR_SET.teal, + "PERFECT CONNECT ALL": COLOUR_SET.gold, + }, + }, + classColours: {}, + difficultyColours: { + Green: COLOUR_SET.green, + Yellow: COLOUR_SET.vibrantYellow, + Red: COLOUR_SET.red, + }, + ratingSystems: [], + + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x?.scoreData.score)], + ["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)], + ["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)], + ], + }, + "popn:9B": { + enumColours: { + grade: { + E: COLOUR_SET.gray, + D: COLOUR_SET.maroon, + C: COLOUR_SET.red, + B: COLOUR_SET.blue, + A: COLOUR_SET.green, + AA: COLOUR_SET.orange, + AAA: COLOUR_SET.gold, + S: COLOUR_SET.teal, + }, + lamp: { + FAILED: COLOUR_SET.red, + "EASY CLEAR": COLOUR_SET.green, + CLEAR: COLOUR_SET.blue, + "FULL COMBO": COLOUR_SET.teal, + PERFECT: COLOUR_SET.gold, + }, + clearMedal: { + failedCircle: COLOUR_SET.red, + failedDiamond: COLOUR_SET.red, + failedStar: COLOUR_SET.red, + easyClear: COLOUR_SET.green, + clearCircle: COLOUR_SET.blue, + clearDiamond: COLOUR_SET.blue, + clearStar: COLOUR_SET.blue, + fullComboCircle: COLOUR_SET.teal, + fullComboDiamond: COLOUR_SET.teal, + fullComboStar: COLOUR_SET.teal, + perfect: COLOUR_SET.gold, + }, + }, + classColours: { + class: { + KITTY: bg("brown"), + STUDENT: bg("green"), + DELINQUENT: bgc("lime", "black"), + DETECTIVE: bg("purple"), + IDOL: "danger", + GENERAL: bg("darkgoldenrod"), + HERMIT: "success", + GOD: "warning", + }, + }, + difficultyColours: { + Easy: COLOUR_SET.blue, + Normal: COLOUR_SET.green, + Hyper: COLOUR_SET.orange, + EX: COLOUR_SET.red, + }, + ratingSystems: [], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x.scoreData.score)], + ["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.clearMedal)], + ], + }, + "wacca:Single": { + enumColours: { + grade: { + D: COLOUR_SET.gray, + C: COLOUR_SET.maroon, + B: COLOUR_SET.red, + A: COLOUR_SET.paleGreen, + AA: COLOUR_SET.green, + AAA: COLOUR_SET.vibrantGreen, + S: COLOUR_SET.gold, + "S+": COLOUR_SET.vibrantYellow, + SS: COLOUR_SET.paleOrange, + "SS+": COLOUR_SET.orange, + SSS: COLOUR_SET.pink, + "SSS+": COLOUR_SET.vibrantPink, + MASTER: COLOUR_SET.white, + }, + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.blue, + MISSLESS: COLOUR_SET.orange, + "FULL COMBO": COLOUR_SET.pink, + "ALL MARVELOUS": COLOUR_SET.gold, + }, + }, + classColours: { + colour: { + ASH: "secondary", + NAVY: bg("darkblue"), + YELLOW: bg("orange"), + RED: "danger", + PURPLE: bg("purple"), + BLUE: "info", + SILVER: bgc("silver", "black"), + GOLD: "warning", + RAINBOW: "success", + }, + stageUp: { + I: null, + II: null, + III: null, + IV: null, + V: null, + VI: null, + VII: null, + VIII: null, + IX: null, + X: null, + XI: null, + XII: null, + XIII: null, + XIV: null, + }, + }, + difficultyColours: { + NORMAL: COLOUR_SET.blue, + HARD: COLOUR_SET.gold, + EXPERT: COLOUR_SET.pink, + INFERNO: COLOUR_SET.purple, + }, + ratingSystems: [], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x.scoreData.score)], + ["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], + ], + }, + "itg:Stamina": { + enumColours: { + grade: { + F: COLOUR_SET.gray, + D: COLOUR_SET.red, + C: COLOUR_SET.maroon, + B: COLOUR_SET.purple, + A: COLOUR_SET.green, + S: COLOUR_SET.orange, + "★": COLOUR_SET.pink, + "★★": COLOUR_SET.vibrantPink, + "★★★": COLOUR_SET.teal, + "★★★★": COLOUR_SET.white, + }, + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.blue, + "FULL COMBO": COLOUR_SET.pink, + "FULL EXCELLENT COMBO": COLOUR_SET.gold, + QUAD: COLOUR_SET.white, + }, + }, + difficultyColours: { + // this game has dynamic difficulties. Formatting is handled elsewhere. + }, + classColours: {}, + ratingSystems: [], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x.scoreData.finalPercent)], + ["Judgements", "Hits", NumericSOV((x) => x.scoreData.scorePercent)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], + ], + }, + "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, + "sdvx:Single": SDVX_IMPL, + "usc:Controller": USC_IMPL, + "usc:Keyboard": USC_IMPL, +}; + +export function GetEnumColour(score: ScoreDocument | PBScoreDocument, enumName: string) { + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(score.game, score.playtype)]; + + // @ts-expect-error lol + return gptImpl.enumColours[enumName][score.scoreData[enumName]]; +} diff --git a/client/src/lib/games/_util.ts b/client/src/lib/games/_util.ts new file mode 100644 index 000000000..930a14158 --- /dev/null +++ b/client/src/lib/games/_util.ts @@ -0,0 +1,27 @@ +import { GPTRatingSystem } from "lib/types"; +import { CSSProperties } from "react"; +import { GPTString } from "tachi-common"; + +export function CreateRatingSys( + name: string, + description: string, + toNumber: GPTRatingSystem["toNumber"], + toString: GPTRatingSystem["toString"], + idvDifference: GPTRatingSystem["idvDifference"] = () => false +): GPTRatingSystem { + return { + description, + name, + toNumber, + toString, + idvDifference, + }; +} + +export function bg(bgColour: string): CSSProperties { + return { backgroundColor: bgColour }; +} + +export function bgc(bgColour: string, colour: string): CSSProperties { + return { backgroundColor: bgColour, color: colour }; +} diff --git a/client/src/lib/games/bms-pms.ts b/client/src/lib/games/bms-pms.ts new file mode 100644 index 000000000..dd4603502 --- /dev/null +++ b/client/src/lib/games/bms-pms.ts @@ -0,0 +1,58 @@ +import { IsNullish } from "util/misc"; +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"; + +const BMS_PMS_IMPL: GPTClientImplementation = { + difficultyColours: { + CHART: COLOUR_SET.gray, // lol + }, + enumColours: { + grade: { + F: COLOUR_SET.gray, + E: COLOUR_SET.red, + D: COLOUR_SET.maroon, + C: COLOUR_SET.purple, + B: COLOUR_SET.paleBlue, + A: COLOUR_SET.green, + AA: COLOUR_SET.blue, + AAA: COLOUR_SET.gold, + "MAX-": COLOUR_SET.teal, + MAX: COLOUR_SET.white, + }, + lamp: { + "NO PLAY": COLOUR_SET.gray, + FAILED: COLOUR_SET.red, + "ASSIST CLEAR": COLOUR_SET.purple, + "EASY CLEAR": COLOUR_SET.green, + CLEAR: COLOUR_SET.blue, + "HARD CLEAR": COLOUR_SET.orange, + "EX HARD CLEAR": COLOUR_SET.gold, + "FULL COMBO": COLOUR_SET.teal, + }, + }, + classColours: {}, + ratingSystems: [ + CreateRatingSys( + "Sieglinde EC", + "TODO lol", + (c) => c.data.sglEC, + (c) => (IsNullish(c.data.sglEC) ? null : FormatSieglindeBMS(c.data.sglEC!)) + ), + CreateRatingSys( + "Sieglinde HC", + "TODO lol", + (c) => c.data.sglHC, + (c) => (IsNullish(c.data.sglHC) ? null : FormatSieglindeBMS(c.data.sglHC!)) + ), + ], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x.scoreData.percent)], + ["Deltas", "Deltas", NumericSOV((x) => x.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], + ], +}; + +export const; diff --git a/client/src/lib/games/gitadora.ts b/client/src/lib/games/gitadora.ts new file mode 100644 index 000000000..098d24eb7 --- /dev/null +++ b/client/src/lib/games/gitadora.ts @@ -0,0 +1,86 @@ +import { NumericSOV } from "util/sorts"; +import { GPTClientImplementation } from "lib/types"; +import { COLOUR_SET, GPTString, GPTStrings } from "tachi-common"; +import { bg, bgc } from "./_util"; + +const GITADORA_ENUM_COLOURS: GPTClientImplementation< + "gitadora:Dora" | "gitadora:Gita" +>["enumColours"] = { + grade: { + C: COLOUR_SET.purple, + B: COLOUR_SET.blue, + A: COLOUR_SET.green, + S: COLOUR_SET.orange, + SS: COLOUR_SET.gold, + MAX: COLOUR_SET.white, + }, + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.blue, + "FULL COMBO": COLOUR_SET.teal, + EXCELLENT: COLOUR_SET.gold, + }, +}; + +const GITADORA_HEADERS: GPTClientImplementation<"gitadora:Dora" | "gitadora:Gita">["scoreHeaders"] = + [ + ["Percent", "Percent", NumericSOV((x) => x.scoreData.percent)], + ["Judgements", "Hits", NumericSOV((x) => x.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], + ]; + +const GITADORA_COLOURS: GPTClientImplementation["classColours"] = { + colour: { + WHITE: bgc("white", "black"), + ORANGE: bg("orange"), + ORANGE_GRD: bg("orange"), + YELLOW: "warning", + YELLOW_GRD: "warning", + GREEN: bg("green"), + GREEN_GRD: bg("green"), + BLUE: "info", + BLUE_GRD: "info", + PURPLE: bg("purple"), + PURPLE_GRD: bg("purple"), + RED: "danger", + RED_GRD: "danger", + BRONZE: bg("bronze"), + SILVER: bgc("silver", "black"), + GOLD: bg("gold"), + + RAINBOW: { + background: + "linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)", + }, + }, +}; + +export const GITADORA_GITA_IMPL: GPTClientImplementation<"gitadora:Gita"> = { + enumColours: GITADORA_ENUM_COLOURS, + difficultyColours: { + BASIC: COLOUR_SET.blue, + ADVANCED: COLOUR_SET.orange, + EXTREME: COLOUR_SET.red, + MASTER: COLOUR_SET.purple, + "BASS BASIC": COLOUR_SET.vibrantBlue, + "BASS ADVANCED": COLOUR_SET.vibrantOrange, + "BASS EXTREME": COLOUR_SET.vibrantRed, + "BASS MASTER": COLOUR_SET.vibrantPurple, + }, + ratingSystems: [], + scoreHeaders: GITADORA_HEADERS, + classColours: GITADORA_COLOURS, +}; + +export const GITADORA_DORA_IMPL: GPTClientImplementation<"gitadora:Dora"> = { + enumColours: GITADORA_ENUM_COLOURS, + difficultyColours: { + BASIC: COLOUR_SET.blue, + ADVANCED: COLOUR_SET.orange, + EXTREME: COLOUR_SET.red, + MASTER: COLOUR_SET.purple, + }, + ratingSystems: [], + scoreHeaders: GITADORA_HEADERS, + classColours: GITADORA_COLOURS, +}; diff --git a/client/src/lib/games/iidx.ts b/client/src/lib/games/iidx.ts new file mode 100644 index 000000000..c752a970f --- /dev/null +++ b/client/src/lib/games/iidx.ts @@ -0,0 +1,123 @@ +import { NumericSOV } from "util/sorts"; +import { GPTClientImplementation } from "lib/types"; +import { COLOUR_SET, GPTStrings } from "tachi-common"; +import { CreateRatingSys, bg, bgc } from "./_util"; + +const IIDX_ENUM_COLOURS: GPTClientImplementation["enumColours"] = { + grade: { + F: COLOUR_SET.gray, + E: COLOUR_SET.red, + D: COLOUR_SET.maroon, + C: COLOUR_SET.purple, + B: COLOUR_SET.paleBlue, + A: COLOUR_SET.green, + AA: COLOUR_SET.blue, + AAA: COLOUR_SET.gold, + "MAX-": COLOUR_SET.teal, + MAX: COLOUR_SET.white, + }, + lamp: { + "NO PLAY": COLOUR_SET.gray, + FAILED: COLOUR_SET.red, + "ASSIST CLEAR": COLOUR_SET.purple, + "EASY CLEAR": COLOUR_SET.green, + CLEAR: COLOUR_SET.blue, + "HARD CLEAR": COLOUR_SET.orange, + "EX HARD CLEAR": COLOUR_SET.gold, + "FULL COMBO": COLOUR_SET.teal, + }, +}; + +const IIDX_DIFF_COLOURS: GPTClientImplementation["difficultyColours"] = { + NORMAL: COLOUR_SET.blue, + HYPER: COLOUR_SET.orange, + ANOTHER: COLOUR_SET.red, + LEGGENDARIA: COLOUR_SET.purple, + "All Scratch NORMAL": COLOUR_SET.blue, + "All Scratch HYPER": COLOUR_SET.orange, + "All Scratch ANOTHER": COLOUR_SET.red, + "All Scratch LEGGENDARIA": COLOUR_SET.purple, + "Kichiku NORMAL": COLOUR_SET.blue, + "Kichiku HYPER": COLOUR_SET.orange, + "Kichiku ANOTHER": COLOUR_SET.red, + "Kichiku LEGGENDARIA": COLOUR_SET.purple, + "Kiraku NORMAL": COLOUR_SET.blue, + "Kiraku HYPER": COLOUR_SET.orange, + "Kiraku ANOTHER": COLOUR_SET.red, + "Kiraku LEGGENDARIA": COLOUR_SET.purple, +}; + +const IIDX_HEADERS: GPTClientImplementation<"iidx:SP" | "iidx:DP">["scoreHeaders"] = [ + ["Score", "Score", NumericSOV((x) => x.scoreData.percent)], + ["Deltas", "Deltas", NumericSOV((x) => x.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)], +]; + +const IIDX_COLOURS: GPTClientImplementation<"iidx:SP" | "iidx:DP">["classColours"] = { + dan: { + 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", + CHUUDEN: bgc("silver", "black"), + KAIDEN: "warning", + }, +}; + +export const IIDX_SP_IMPL: GPTClientImplementation<"iidx:SP"> = { + difficultyColours: IIDX_DIFF_COLOURS, + enumColours: IIDX_ENUM_COLOURS, + ratingSystems: [ + CreateRatingSys( + "NC Tier", + "Tierlist Ratings for Normal Clears.", + (c) => c.data.ncTier?.value, + (c) => c.data.ncTier?.text, + (c) => c.data.ncTier?.individualDifference + ), + CreateRatingSys( + "HC Tier", + "Tierlist Ratings for Hard Clears.", + (c) => c.data.hcTier?.value, + (c) => c.data.hcTier?.text, + (c) => c.data.hcTier?.individualDifference + ), + CreateRatingSys( + "EXHC Tier", + "Tierlist Ratings for EX-HARD Clears.", + (c) => c.data.exhcTier?.value, + (c) => c.data.exhcTier?.text, + (c) => c.data.exhcTier?.individualDifference + ), + ], + scoreHeaders: IIDX_HEADERS, + classColours: IIDX_COLOURS, +}; + +export const IIDX_DP_IMPL: GPTClientImplementation<"iidx:DP"> = { + difficultyColours: IIDX_DIFF_COLOURS, + enumColours: IIDX_ENUM_COLOURS, + ratingSystems: [ + CreateRatingSys( + "DP Tier", + "The unofficial DP tiers, taken from https://zasa.sakura.ne.jp/dp/run.php.", + (c) => c.data.dpTier?.value, + (c) => c.data.dpTier?.text + ), + ], + scoreHeaders: IIDX_HEADERS, + classColours: IIDX_COLOURS, +}; diff --git a/client/src/lib/games/sdvx-usc.ts b/client/src/lib/games/sdvx-usc.ts new file mode 100644 index 000000000..c620cba7a --- /dev/null +++ b/client/src/lib/games/sdvx-usc.ts @@ -0,0 +1,71 @@ +import { NumericSOV } from "util/sorts"; +import { GPTClientImplementation } from "lib/types"; +import { COLOUR_SET, GPTStrings } from "tachi-common"; +import { CreateRatingSys } from "./_util"; + +const SDVXLIKE_ENUM_COLOURS: GPTClientImplementation["enumColours"] = { + grade: { + D: COLOUR_SET.gray, + C: COLOUR_SET.red, + B: COLOUR_SET.maroon, + A: COLOUR_SET.paleBlue, + "A+": COLOUR_SET.blue, + AA: COLOUR_SET.paleGreen, + "AA+": COLOUR_SET.green, + AAA: COLOUR_SET.gold, + "AAA+": COLOUR_SET.vibrantOrange, + S: COLOUR_SET.teal, + PUC: COLOUR_SET.pink, + }, + lamp: { + FAILED: COLOUR_SET.red, + CLEAR: COLOUR_SET.green, + "EXCESSIVE CLEAR": COLOUR_SET.purple, + "ULTIMATE CHAIN": COLOUR_SET.teal, + "PERFECT ULTIMATE CHAIN": COLOUR_SET.gold, + }, +}; + +export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = { + enumColours: SDVXLIKE_ENUM_COLOURS, + difficultyColours: { + NOV: COLOUR_SET.purple, + ADV: COLOUR_SET.vibrantYellow, + EXH: COLOUR_SET.red, + INF: COLOUR_SET.vibrantPink, + GRV: COLOUR_SET.orange, + HVN: COLOUR_SET.teal, + VVD: COLOUR_SET.pink, + XCD: COLOUR_SET.blue, + MXM: COLOUR_SET.white, + }, + ratingSystems: [ + CreateRatingSys( + "Tierlist", + "The unofficial SDVX clearing tierlist", + (c) => c.data.clearTier?.value, + (c) => c.data.clearTier?.text, + (c) => c.data.clearTier?.individualDifference + ), + ], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x?.scoreData.score)], + ["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)], + ["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)], + ], +}; +export const USC_IMPL: GPTClientImplementation = { + enumColours: SDVXLIKE_ENUM_COLOURS, + difficultyColours: { + NOV: COLOUR_SET.purple, + ADV: COLOUR_SET.vibrantYellow, + EXH: COLOUR_SET.red, + INF: COLOUR_SET.vibrantPink, + }, + ratingSystems: [], + scoreHeaders: [ + ["Score", "Score", NumericSOV((x) => x?.scoreData.score)], + ["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)], + ["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)], + ], +}; diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts new file mode 100644 index 000000000..7441ca45e --- /dev/null +++ b/client/src/lib/types.ts @@ -0,0 +1,79 @@ +import { Header } from "components/tables/components/TachiTable"; +import { CSSProperties } from "react"; +import { + ChartDocument, + ClassConfigs, + Classes, + ConfScoreMetrics, + Difficulties, + ExtractedClasses, + GPTString, + PBScoreDocument, + ScoreDocument, +} from "tachi-common"; +import { ExtractEnumMetricNames, GetEnumValue } from "tachi-common/types/metrics"; + +export type GPTEnumColours = { + // @ts-expect-error this is fine please do not worry + [M in ExtractEnumMetricNames]: Record, string>; +}; + +export type GPTDifficultyColours = Record; + +/** + * Every GPT has to have some sort of "rating system" for charts defined. + * The UI uses this to handle things like sorting on the difficulty cell. + */ +export type GPTRatingSystem = { + name: string; + description: string; + toNumber: (c: ChartDocument) => number | null | undefined; + toString: (c: ChartDocument) => string | null | undefined; + + /** + * Does this rating system say this chart has strong individual differences + * between players? + */ + idvDifference: (c: ChartDocument) => boolean | null | undefined; +}; + +export type GPTClassColours = { + [C in keyof ClassConfigs[GPT]]: { + // @ts-expect-error it's kinda cool how TS lets me just uhh + // ignore, errors + // in the abhorrent stringly based typesystem i have created. + // Whatever! This works. + + // string => bootstrap variant + // css properties => css + // null -> no styling + [V in ClassConfigs[GPT][C]["values"][number]["id"]]: + | "warning" + | "danger" + | "secondary" + | "primary" + | "info" + | "success" + | null + | CSSProperties; + }; +}; + +export interface GPTClientImplementation { + enumColours: GPTEnumColours; + difficultyColours: GPTDifficultyColours; + classColours: GPTClassColours; + + /** + * Other than chart.level and chart.levelNum, what other rating systems exist + * for this game? + * + * You can use this to add things like tierlists (chart.data.ncTier, hcTier, etc.) + */ + ratingSystems: Array>; + + /** + * What headers should be used when rendering scores in a table for this game? + */ + scoreHeaders: Array | PBScoreDocument>>; +} diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts index 47ba28c38..462fe9c0c 100644 --- a/client/src/types/api-returns.ts +++ b/client/src/types/api-returns.ts @@ -7,13 +7,11 @@ import { GamePTConfig, GoalDocument, GoalSubscriptionDocument, - Grades, GPTString, GPTStringToGame, ImportDocument, ImportTrackerFailed, integer, - Lamps, PBScoreDocument, UserDocument, ScoreDocument, @@ -22,13 +20,13 @@ import { ShowcaseStatFolder, SongDocument, TableDocument, - ProfileRatingLookup, UserGameStats, UserGameStatsSnapshotDocument, QuestDocument, QuestSubscriptionDocument, QuestlineDocument, SessionScoreInfo, + ProfileRatingAlgorithms, } from "tachi-common"; export interface UGPTStatsReturn { @@ -37,7 +35,7 @@ export interface UGPTStatsReturn { mostRecentScore: ScoreDocument; totalScores: number; rankingData: Record< - ProfileRatingLookup[GPT], + ProfileRatingAlgorithms[GPT], { ranking: integer; outOf: integer; @@ -93,8 +91,8 @@ export interface UGPTChartPBComposition { pb: PBScoreDocument; } -export type UGSWithRankingData = UserGameStats & { - __rankingData: Record; +export type UGSWithRankingData = UserGameStats & { + __rankingData: Record; }; export interface SongChartsSearch { @@ -102,9 +100,8 @@ export interface SongChartsSearch { charts: ChartDocument[]; } -export interface FolderStatsInfo { - grades: Record; - lamps: Record; +export interface FolderStatsInfo { + stats: Record>; folderID: string; chartCount: integer; } @@ -169,9 +166,9 @@ export interface ScoreLeaderboardReturns { pbs: PBScoreDocument[]; } -export interface UserLeaderboardReturns { +export interface UserLeaderboardReturns { users: UserDocument[]; - gameStats: UserGameStats[]; + gameStats: UserGameStats[]; } export interface UserRecentSummary { @@ -292,13 +289,6 @@ export type SessionFolderRaises = { previousCount: integer; // how many AAAs/HARD CLEARs/whatevers was on this // folder before this session? totalCharts: integer; -} & ( - | { - type: "grade"; - value: Grades[GPTString]; - } - | { - type: "lamp"; - value: Lamps[GPTString]; - } -); + type: string; + value: string; +}; diff --git a/client/src/types/seeds.ts b/client/src/types/seeds.ts index d9c27bbc1..3fa1be709 100644 --- a/client/src/types/seeds.ts +++ b/client/src/types/seeds.ts @@ -4,7 +4,7 @@ import { ChartDocument, FolderDocument, Game, - GameToGPTString, + GPTStrings, GoalDocument, GoalDocumentFolder, GoalDocumentMulti, @@ -70,7 +70,7 @@ type SongSeedsWithRelated = { }; type ChartSeedsWithRelated = { - [G in Game as `charts-${G}.json`]: Array>; + [G in Game as `charts-${G}.json`]: Array>; }; /** diff --git a/client/src/types/tables.ts b/client/src/types/tables.ts index 007cb66a7..3b93c6987 100644 --- a/client/src/types/tables.ts +++ b/client/src/types/tables.ts @@ -48,7 +48,7 @@ export type ChartLeaderboardDataset = (PBScor }; })[]; -export type UGSDataset = (UserGameStats & { +export type UGSDataset = (UserGameStats & { __related: { user: UserDocument; index: integer; diff --git a/client/src/util/data.ts b/client/src/util/data.ts index 1aea25fba..e04fd6d94 100644 --- a/client/src/util/data.ts +++ b/client/src/util/data.ts @@ -13,7 +13,19 @@ import { import { GoalsOnChartReturn, GoalsOnFolderReturn } from "types/api-returns"; export function GetPBs(scoreInfo: SessionScoreInfo[]) { - return scoreInfo.filter((e) => e.isNewScore === true || e.lampDelta > 0 || e.scoreDelta > 0); + return scoreInfo.filter((e) => { + if (e.isNewScore) { + return true; + } + + for (const v of Object.values(e.deltas)) { + if (v >= 0) { + return true; + } + } + + return false; + }); } export function CreateSongMap(songs: SongDocument[]) { diff --git a/client/src/util/misc.ts b/client/src/util/misc.ts index 5406c80e4..74a4d91d3 100644 --- a/client/src/util/misc.ts +++ b/client/src/util/misc.ts @@ -3,20 +3,16 @@ import toast from "react-hot-toast"; import { useHistory } from "react-router-dom"; import { APIPermissions, + AnyProfileRatingAlg, + AnySessionRatingAlg, ChartDocument, Game, GamePTConfig, GetGamePTConfig, - Grades, - GPTString, Playtype, - ProfileRatingLookup, QuestDocument, QuestSubscriptionDocument, - ScoreCalculatedDataLookup, ScoreDocument, - SessionCalculatedDataLookup, - SessionDocument, integer, } from "tachi-common"; @@ -56,29 +52,10 @@ export function FormatTables(tables: { table: string; level: string }[]) { return tables.map((e) => `${e.table}${e.level}`).join(", "); } -export function FormatGPTRating( - game: Game, - playtype: Playtype, - key: ScoreCalculatedDataLookup[GPTString], - value: number | null -) { - if (value === null) { - return "No Data."; - } - - const gptConfig = GetGamePTConfig(game, playtype); - - if (gptConfig.scoreRatingAlgFormatters[key]) { - return gptConfig.scoreRatingAlgFormatters[key]!(value); - } - - return value.toFixed(2); -} - export function FormatGPTProfileRating( game: Game, playtype: Playtype, - key: ProfileRatingLookup[GPTString], + key: AnyProfileRatingAlg, value: number | null ) { if (value === null) { @@ -87,8 +64,8 @@ export function FormatGPTProfileRating( const gptConfig = GetGamePTConfig(game, playtype); - if (gptConfig.profileRatingAlgFormatters[key]) { - return gptConfig.profileRatingAlgFormatters[key]!(value); + if (gptConfig.profileRatingAlgs[key].formatter) { + return gptConfig.profileRatingAlgs[key].formatter!(value); } return value.toFixed(2); @@ -97,7 +74,7 @@ export function FormatGPTProfileRating( export function FormatGPTSessionRating( game: Game, playtype: Playtype, - key: SessionCalculatedDataLookup[GPTString], + key: AnySessionRatingAlg, value: number | null | undefined ) { if (value === null || value === undefined) { @@ -106,8 +83,8 @@ export function FormatGPTSessionRating( const gptConfig = GetGamePTConfig(game, playtype); - if (gptConfig.sessionRatingAlgFormatters[key]) { - return gptConfig.sessionRatingAlgFormatters[key]!(value); + if (gptConfig.sessionRatingAlgs[key].formatter) { + return gptConfig.sessionRatingAlgs[key].formatter!(value); } return value.toFixed(2); @@ -191,27 +168,14 @@ export function SelectRightChart( chartID: string, charts: ChartDocument[] ) { - if (gptConfig.difficulties.includes(chartID as any)) { - for (const chart of charts) { - if (chart.difficulty === chartID && chart.isPrimary) { - return chart; - } + // try matching difficulty names, if it's not any of those, it's probably a chartID. + for (const chart of charts) { + if (chart.difficulty === chartID && chart.isPrimary) { + return chart; } - } else if (gptConfig.gptString === "itg:Stamina") { - for (const chart of charts) { - if ( - (chart as ChartDocument<"itg:Stamina">).data.hashGSV3 === chartID && - chart.isPrimary - ) { - return chart; - } - } - } else { - // else, its a chart ID. - for (const chart of charts) { - if (chartID === chart.chartID) { - return chart; - } + + if (chartID === chart.chartID) { + return chart; } } @@ -287,7 +251,7 @@ export function FormatScoreRating( return "No Data."; } - const formatter = GetGamePTConfig(game, playtype).scoreRatingAlgFormatters[rating]; + const formatter = GetGamePTConfig(game, playtype).scoreRatingAlgs[rating].formatter; if (!formatter) { return value.toFixed(2); @@ -299,14 +263,14 @@ export function FormatScoreRating( export function FormatSessionRating( game: Game, playtype: Playtype, - rating: keyof SessionDocument["calculatedData"], + rating: AnySessionRatingAlg, value: number | null | undefined ) { if (value === null || value === undefined) { return "No Data."; } - const formatter = GetGamePTConfig(game, playtype).sessionRatingAlgFormatters[rating]; + const formatter = GetGamePTConfig(game, playtype).sessionRatingAlgs[rating].formatter; if (!formatter) { return value.toFixed(2); @@ -315,31 +279,6 @@ export function FormatSessionRating( return formatter(value); } -export function GetGradeFromPercent( - game: Game, - playtype: Playtype, - percent: number -): Grades[GPT] { - const gptConfig = GetGamePTConfig(game, playtype); - const boundaries = gptConfig.gradeBoundaries; - const grades = gptConfig.grades; - - if (!boundaries) { - throw new Error( - `Invalid call to GetGradeFromPercent! GPT ${game}:${playtype} does not use grade boundaries.` - ); - } - - // (hey, this for loop is backwards!) - for (let i = boundaries.length; i >= 0; i--) { - if (percent >= boundaries[i]) { - return grades[i] as Grades[GPT]; - } - } - - throw new Error(`Could not resolve grade for percent ${percent} on game ${game}`); -} - export function ConditionalLeadingSpace(maybeStr: string | null) { if (maybeStr === null) { return ""; diff --git a/client/src/util/str-to-num.ts b/client/src/util/str-to-num.ts index 7e8bd36de..d45e769ee 100644 --- a/client/src/util/str-to-num.ts +++ b/client/src/util/str-to-num.ts @@ -1,46 +1,32 @@ -import { Game, GetGamePTConfig, Playtypes } from "tachi-common"; +import { Game, GetGamePTConfig, GetScoreMetricConf, Playtypes } from "tachi-common"; -export function HumanFriendlyStrToGradeIndex(game: Game, playtype: Playtypes[Game]) { +export function HumanFriendlyStrToEnumIndex( + game: Game, + playtype: Playtypes[Game], + enumMetric: string +) { const gptConfig = GetGamePTConfig(game, playtype); - const lowerGrades = gptConfig.grades.map((e) => e.toLowerCase()); + const conf = GetScoreMetricConf(gptConfig, enumMetric); + + if (conf.type !== "ENUM") { + return () => 0; // wut + } + + const lowerValues = conf.values.map((e) => e.toLowerCase()); return (str: string) => { const lowerStr = str.toLowerCase(); let partialMatch: number | null = null; - for (let i = 0; i < gptConfig.grades.length; i++) { - const grade = lowerGrades[i]; + for (let i = 0; i < conf.values.length; i++) { + const value = lowerValues[i]; - if (grade === lowerStr) { + if (value === lowerStr) { return i; } - if (grade.startsWith(lowerStr) && partialMatch === null) { - partialMatch = i; - } - } - - return partialMatch; - }; -} - -export function HumanFriendlyStrToLampIndex(game: Game, playtype: Playtypes[Game]) { - const gptConfig = GetGamePTConfig(game, playtype); - const lowerLamps = gptConfig.lamps.map((e) => e.toLowerCase()); - - return (str: string) => { - const lowerStr = str.toLowerCase(); - let partialMatch: number | null = null; - - for (let i = 0; i < gptConfig.lamps.length; i++) { - const lamp = lowerLamps[i]; - - if (lamp === lowerStr) { - return i; - } - - if (lamp.startsWith(lowerStr) && partialMatch === null) { + if (value.startsWith(lowerStr) && partialMatch === null) { partialMatch = i; } } diff --git a/client/src/util/tables/create-search.ts b/client/src/util/tables/create-search.ts index 3f04e2e57..4a7dd3fd3 100644 --- a/client/src/util/tables/create-search.ts +++ b/client/src/util/tables/create-search.ts @@ -1,4 +1,4 @@ -import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num"; +import { HumanFriendlyStrToEnumIndex } from "util/str-to-num"; import { ValueGetterOrHybrid } from "util/ztable/search"; import { BMS_TABLES, @@ -7,7 +7,9 @@ import { GamePTConfig, GetGamePTConfig, GPTString, + PBScoreDocument, Playtype, + ScoreDocument, } from "tachi-common"; import { ComparePBsDataset, FolderDataset, PBDataset, ScoreDataset } from "types/tables"; @@ -32,17 +34,8 @@ export function CreateDefaultScoreSearchParams x.__related.song.title, difficulty: (x) => x.__related.chart.difficulty, level: (x) => x.__related.chart.levelNum, - score: (x) => x.scoreData.score, - percent: (x) => x.scoreData.percent, highlight: (x) => !!x.highlight, - lamp: { - valueGetter: (x) => [x.scoreData.lamp, x.scoreData.lamp.index], - strToNum: HumanFriendlyStrToLampIndex(game, playtype), - }, - grade: { - valueGetter: (x) => [x.scoreData.grade.string, x.scoreData.grade.string], - strToNum: HumanFriendlyStrToGradeIndex(game, playtype), - }, + ...GetMetricSearchParams(game, playtype), ...CreateCalcDataSearchFns(gptConfig), }; @@ -53,6 +46,33 @@ export function CreateDefaultScoreSearchParams> = {}; + + const gptConfig = GetGamePTConfig(game, playtype); + + for (const [metric, conf] of Object.entries({ + ...gptConfig.providedMetrics, + ...gptConfig.derivedMetrics, + })) { + switch (conf.type) { + case "ENUM": + searchFns[metric] = { + // @ts-expect-error lol this is fine pls + valueGetter: (x) => x.scoreData[metric], + strToNum: HumanFriendlyStrToEnumIndex(game, playtype, metric), + }; + break; + case "INTEGER": + case "DECIMAL": + // @ts-expect-error lol this is fine pls + searchFns[metric] = (x) => x.scoreData[metric]; + } + } + + return searchFns; +} + export function CreateDefaultPBSearchParams( game: Game, playtype: Playtype @@ -64,20 +84,11 @@ export function CreateDefaultPBSearchParams( title: (x) => x.__related.song.title, difficulty: (x) => x.__related.chart.difficulty, level: (x) => x.__related.chart.levelNum, - score: (x) => x.scoreData.score, - percent: (x) => x.scoreData.percent, ranking: (x) => x.rankingData.rank, rivalRanking: (x) => x.rankingData.rivalRank, highlight: (x) => !!x.highlight, username: (x) => x.__related.user?.username ?? null, - lamp: { - valueGetter: (x) => [x.scoreData.lamp, x.scoreData.lamp.index], - strToNum: HumanFriendlyStrToLampIndex(game, playtype), - }, - grade: { - valueGetter: (x) => [x.scoreData.grade.string, x.scoreData.grade.string], - strToNum: HumanFriendlyStrToGradeIndex(game, playtype), - }, + ...GetMetricSearchParams(game, playtype), ...CreateCalcDataSearchFns(gptConfig), }; @@ -117,26 +128,11 @@ export function CreateDefaultFolderSearchParams x.__related.song.title, difficulty: (x) => x.difficulty, level: (x) => x.levelNum, - score: (x) => x.__related.pb?.scoreData.score ?? null, - percent: (x) => x.__related.pb?.scoreData.percent ?? null, ranking: (x) => x.__related.pb?.rankingData.rank ?? null, rivalRanking: (x) => x.__related.pb?.rankingData.rivalRank ?? null, highlight: (x) => !!x.__related.pb?.highlight, played: (x) => !!x.__related.pb, - lamp: { - valueGetter: (x) => - x.__related.pb - ? [x.__related.pb.scoreData.lamp, x.__related.pb.scoreData.lamp.index] - : null, - strToNum: HumanFriendlyStrToLampIndex(game, playtype), - }, - grade: { - valueGetter: (x) => - x.__related.pb - ? [x.__related.pb.scoreData.grade.string, x.__related.pb.scoreData.grade.string] - : null, - strToNum: HumanFriendlyStrToGradeIndex(game, playtype), - }, + ...GetMetricSearchParams(game, playtype), ...CreateFolderCalcDataSearchFns(gptConfig), }; @@ -149,8 +145,9 @@ export function CreateDefaultFolderSearchParams [ + Object.keys(gptConfig.scoreRatingAlgs).map((e) => [ e.toLowerCase(), + // @ts-expect-error this is fine please leave me alone (x: FolderDataset[0]) => x.__related.pb?.calculatedData[e] ?? null, ]) ); @@ -158,7 +155,8 @@ function CreateFolderCalcDataSearchFns(gptConfig: GamePTConfig) { function CreateCalcDataSearchFns(gptConfig: GamePTConfig) { return Object.fromEntries( - gptConfig.scoreRatingAlgs.map( + Object.keys(gptConfig.scoreRatingAlgs).map( + // @ts-expect-error this is fine please leave me alone (e) => [e.toLowerCase(), (x: PBDataset[0]) => x.calculatedData[e]] ?? null ) ); diff --git a/client/src/util/tierlist.ts b/client/src/util/tierlist.ts deleted file mode 100644 index d20cfb065..000000000 --- a/client/src/util/tierlist.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - Game, - GPTTierlists, - GPTString, - IIDX_LAMPS, - ScoreDocument, - SDVX_LAMPS, - Playtype, -} from "tachi-common"; - -type ScaleAchievedFns = { - [I in GPTString]: { - [K in GPTTierlists[GPT]]: ((k: ScoreDocument) => boolean) | null; - }; -}; - -const ScaleNameAchievedFns: ScaleAchievedFns = { - "bms:14K": { - "sgl-EC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.EASY_CLEAR, - "sgl-HC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.HARD_CLEAR, - }, - "bms:7K": { - "sgl-EC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.EASY_CLEAR, - "sgl-HC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.HARD_CLEAR, - }, - "pms:Controller": { - "sgl-EC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.EASY_CLEAR, - "sgl-HC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.HARD_CLEAR, - }, - "pms:Keyboard": { - "sgl-EC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.EASY_CLEAR, - "sgl-HC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.HARD_CLEAR, - }, - "chunithm:Single": {}, - "gitadora:Dora": {}, - "gitadora:Gita": {}, - "iidx:DP": { - "dp-tier": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.CLEAR, - }, - "iidx:SP": { - "kt-NC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.CLEAR, - "kt-HC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.HARD_CLEAR, - "kt-EXHC": (k) => k.scoreData.lamp.index >= IIDX_LAMPS.EX_HARD_CLEAR, - }, - "maimaidx:Single": {}, - "museca:Single": { - "tachi-score": null, - }, - "sdvx:Single": { - clear: (k) => k.scoreData.lamp.index >= SDVX_LAMPS.CLEAR, - }, - "usc:Controller": {}, - "usc:Keyboard": {}, - "wacca:Single": {}, - "popn:9B": {}, - "jubeat:Single": {}, - "itg:Stamina": {}, -}; - -export function GetScaleAchievedFn( - game: Game, - playtype: Playtype, - tierlist: GPTTierlists[GPTString] -) { - const scl = ScaleNameAchievedFns[`${game}:${playtype}` as GPTString]; - - if (!scl) { - throw new Error(`Invalid game + pt combination ${game}:${playtype}. Can't find scale.`); - } - - // @ts-expect-error hack haha - return scl[tierlist]; -} diff --git a/client/tsconfig.json b/client/tsconfig.json index caa96a4b7..b85735fc0 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -49,7 +49,7 @@ // kept around to keep eslint happy, otherwise it won't try and lint the file // note that this technically results in an error because this file isn't in // `rootDir`. However, I don't care. - "vite.config.js" + // "vite.config.js" ], "exclude": [ "node_modules" diff --git a/common/src/config/game-support/_common.ts b/common/src/config/game-support/_common.ts index 717a29a88..377b7587c 100644 --- a/common/src/config/game-support/_common.ts +++ b/common/src/config/game-support/_common.ts @@ -2,6 +2,7 @@ * Common stuff for that all games might want to inherit. */ +import { FmtNum } from "../../utils/util"; import { p } from "prudence"; import type { ConfScoreMetric } from "../../types/metrics"; @@ -20,7 +21,7 @@ import type { ConfScoreMetric } from "../../types/metrics"; * as that necessitates that you either can't miss, or can't hit. */ export const FAST_SLOW_MAXCOMBO = { - fast: { type: "INTEGER", validate: p.isPositiveInteger }, - slow: { type: "INTEGER", validate: p.isPositiveInteger }, - maxCombo: { type: "INTEGER", validate: p.isPositiveInteger }, + fast: { type: "INTEGER", validate: p.isPositiveInteger, formatter: FmtNum }, + slow: { type: "INTEGER", validate: p.isPositiveInteger, formatter: FmtNum }, + maxCombo: { type: "INTEGER", validate: p.isPositiveInteger, formatter: FmtNum }, } as const satisfies Record; diff --git a/common/src/config/game-support/bms.ts b/common/src/config/game-support/bms.ts index e5f877c7f..787db8447 100644 --- a/common/src/config/game-support/bms.ts +++ b/common/src/config/game-support/bms.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtPercent, FmtScoreNoCommas } from "../../utils/util"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -16,7 +17,7 @@ export const BMS_CONF = { }), } as const satisfies INTERNAL_GAME_CONFIG; -function FormatSieglindeBMS(sgl: number): string { +export function FormatSieglindeBMS(sgl: number): string { if (sgl < 13) { return `☆${sgl.toFixed(2)}`; } @@ -135,7 +136,7 @@ const RANDOM_SCHEMA = z.enum(["MIRROR", "NONRAN", "R-RANDOM", "RANDOM", "S-RANDO export const BMS_7K_CONF = { providedMetrics: { - score: { type: "INTEGER", chartDependentMax: true }, + score: { type: "INTEGER", chartDependentMax: true, formatter: FmtScoreNoCommas }, lamp: { type: "ENUM", @@ -163,7 +164,7 @@ export const BMS_7K_CONF = { // if #RANDOM is to ever be supported, the user's percent would become // a *mandatory* metric, as a chart's notecount can be completely unknown. // However, supporting #RANDOM is an awful pain, so I don't really care. - percent: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + percent: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, }, defaultMetric: "percent", @@ -171,19 +172,19 @@ export const BMS_7K_CONF = { optionalMetrics: { ...FAST_SLOW_MAXCOMBO, - bp: { type: "INTEGER", validate: p.isPositive }, - gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + bp: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + gauge: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, gaugeHistory: { type: "GRAPH", validate: p.isBetween(0, 100) }, - epg: { type: "INTEGER", validate: p.isPositive }, - egr: { type: "INTEGER", validate: p.isPositive }, - egd: { type: "INTEGER", validate: p.isPositive }, - ebd: { type: "INTEGER", validate: p.isPositive }, - epr: { type: "INTEGER", validate: p.isPositive }, - lpg: { type: "INTEGER", validate: p.isPositive }, - lgr: { type: "INTEGER", validate: p.isPositive }, - lgd: { type: "INTEGER", validate: p.isPositive }, - lbd: { type: "INTEGER", validate: p.isPositive }, - lpr: { type: "INTEGER", validate: p.isPositive }, + epg: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + egr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + egd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + ebd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + epr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lpg: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lgr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lgd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lbd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lpr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, }, scoreRatingAlgs: { diff --git a/common/src/config/game-support/chunithm.ts b/common/src/config/game-support/chunithm.ts index ea62b5c04..6911d1744 100644 --- a/common/src/config/game-support/chunithm.ts +++ b/common/src/config/game-support/chunithm.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum } from "../../utils/util"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -29,7 +30,7 @@ export const CHUNITHMColours = [ export const CHUNITHM_SINGLE_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 1_010_000) }, + score: { type: "INTEGER", validate: p.isBetween(0, 1_010_000), formatter: FmtNum }, lamp: { type: "ENUM", values: ["FAILED", "CLEAR", "FULL COMBO", "ALL JUSTICE", "ALL JUSTICE CRITICAL"], diff --git a/common/src/config/game-support/gitadora.ts b/common/src/config/game-support/gitadora.ts index 43622093f..b27dd5c31 100644 --- a/common/src/config/game-support/gitadora.ts +++ b/common/src/config/game-support/gitadora.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtPercent } from "../../utils/util"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -36,7 +37,7 @@ const GitadoraColours = [ export const GITADORA_GITA_CONF = { providedMetrics: { - percent: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + percent: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, lamp: { type: "ENUM", values: ["FAILED", "CLEAR", "FULL COMBO", "EXCELLENT"], diff --git a/common/src/config/game-support/iidx.ts b/common/src/config/game-support/iidx.ts index 65bfcb3d9..95e14886e 100644 --- a/common/src/config/game-support/iidx.ts +++ b/common/src/config/game-support/iidx.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum, FmtPercent, FmtScoreNoCommas } from "../../utils/util"; import { ClassValue, zodNonNegativeInt, zodTierlistData } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -50,7 +51,7 @@ const RANDOM_SCHEMA = z.enum(["NONRAN", "MIRROR", "R-RANDOM", "RANDOM", "S-RANDO export const IIDX_SP_CONF = { providedMetrics: { - score: { type: "INTEGER", chartDependentMax: true }, + score: { type: "INTEGER", chartDependentMax: true, formatter: FmtScoreNoCommas }, lamp: { type: "ENUM", values: [ @@ -71,6 +72,7 @@ export const IIDX_SP_CONF = { percent: { type: "DECIMAL", validate: p.isBetween(0, 100), + formatter: FmtPercent, }, grade: { type: "ENUM", @@ -85,9 +87,9 @@ export const IIDX_SP_CONF = { optionalMetrics: { ...FAST_SLOW_MAXCOMBO, - bp: { type: "INTEGER", validate: p.isPositive }, - gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) }, - comboBreak: { type: "INTEGER", validate: p.isPositive }, + bp: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + gauge: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, + comboBreak: { type: "INTEGER", validate: p.isPositive, formatter: FmtNum }, // The players history for the gauge type they were playing on. // this may fall into "NULL" if the user fails. diff --git a/common/src/config/game-support/itg.ts b/common/src/config/game-support/itg.ts index ba54f7cb5..4b236e573 100644 --- a/common/src/config/game-support/itg.ts +++ b/common/src/config/game-support/itg.ts @@ -1,3 +1,4 @@ +import { FmtPercent } from "../../utils/util"; import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -15,13 +16,13 @@ export const ITG_CONF = { export const ITG_STAMINA_CONF = { providedMetrics: { - scorePercent: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + scorePercent: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, // How far through the chart did they get? // 100 means they cleared. // 50 means they got halfway through. // 0 means they died instantly etc. - survivedPercent: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + survivedPercent: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, lamp: { type: "ENUM", @@ -50,7 +51,17 @@ export const ITG_STAMINA_CONF = { // // NOTE that in our ITG implementation we don't allow for negative percents // as it breaks this silly metric. Also negative percents are stupid. - finalPercent: { type: "DECIMAL", validate: p.isBetween(0, 200) }, + finalPercent: { + type: "DECIMAL", + validate: p.isBetween(0, 200), + formatter: (v) => { + if (v < 100) { + return `Died ${v.toFixed(2)}% in`; + } + + return `Cleared with ${(v - 100).toFixed(2)}%`; + }, + }, }, optionalMetrics: { diff --git a/common/src/config/game-support/jubeat.ts b/common/src/config/game-support/jubeat.ts index 823a41e45..5e45d5353 100644 --- a/common/src/config/game-support/jubeat.ts +++ b/common/src/config/game-support/jubeat.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum, FmtPercent } from "../../utils/util"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -28,8 +29,8 @@ const JubeatColours = [ export const JUBEAT_SINGLE_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000) }, - musicRate: { type: "DECIMAL", chartDependentMax: true }, + score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000), formatter: FmtNum }, + musicRate: { type: "DECIMAL", chartDependentMax: true, formatter: FmtPercent }, lamp: { type: "ENUM", values: ["FAILED", "CLEAR", "FULL COMBO", "EXCELLENT"], diff --git a/common/src/config/game-support/maimai-dx.ts b/common/src/config/game-support/maimai-dx.ts index 3dfd31505..4d16bec6b 100644 --- a/common/src/config/game-support/maimai-dx.ts +++ b/common/src/config/game-support/maimai-dx.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtPercent } from "../../utils/util"; import { ClassValue, NoDecimalPlace } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -57,7 +58,7 @@ const MaimaiDXColours = [ export const MAIMAI_DX_SINGLE_CONF = { providedMetrics: { - percent: { type: "DECIMAL", validate: p.isBetween(0, 101) }, + percent: { type: "DECIMAL", validate: p.isBetween(0, 101), formatter: FmtPercent }, lamp: { type: "ENUM", values: ["FAILED", "CLEAR", "FULL COMBO", "FULL COMBO+", "ALL PERFECT", "ALL PERFECT+"], diff --git a/common/src/config/game-support/museca.ts b/common/src/config/game-support/museca.ts index 33478cf0e..492b1b427 100644 --- a/common/src/config/game-support/museca.ts +++ b/common/src/config/game-support/museca.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum } from "../../utils/util"; import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -17,7 +18,7 @@ export const MUSECA_CONF = { export const MUSECA_SINGLE_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000) }, + score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000), formatter: FmtNum }, lamp: { type: "ENUM", values: ["FAILED", "CLEAR", "CONNECT ALL", "PERFECT CONNECT ALL"], diff --git a/common/src/config/game-support/pms.ts b/common/src/config/game-support/pms.ts index c39760119..bb890b034 100644 --- a/common/src/config/game-support/pms.ts +++ b/common/src/config/game-support/pms.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtScoreNoCommas, FmtPercent } from "../../utils/util"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -42,7 +43,7 @@ function FormatSieglindePMS(sgl: number): string { export const PMS_CONTROLLER_CONF = { providedMetrics: { - score: { type: "INTEGER", chartDependentMax: true }, + score: { type: "INTEGER", chartDependentMax: true, formatter: FmtScoreNoCommas }, lamp: { type: "ENUM", @@ -70,7 +71,7 @@ export const PMS_CONTROLLER_CONF = { // if #RANDOM is to ever be supported, the user's percent would become // a *mandatory* metric, as a chart's notecount can be completely unknown. // However, supporting #RANDOM is an awful pain, so I don't really care. - percent: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + percent: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, }, defaultMetric: "percent", @@ -78,19 +79,19 @@ export const PMS_CONTROLLER_CONF = { optionalMetrics: { ...FAST_SLOW_MAXCOMBO, - bp: { type: "INTEGER", validate: p.isPositive }, - gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + bp: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + gauge: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, gaugeHistory: { type: "GRAPH", validate: p.isBetween(0, 100) }, - epg: { type: "INTEGER", validate: p.isPositive }, - egr: { type: "INTEGER", validate: p.isPositive }, - egd: { type: "INTEGER", validate: p.isPositive }, - ebd: { type: "INTEGER", validate: p.isPositive }, - epr: { type: "INTEGER", validate: p.isPositive }, - lpg: { type: "INTEGER", validate: p.isPositive }, - lgr: { type: "INTEGER", validate: p.isPositive }, - lgd: { type: "INTEGER", validate: p.isPositive }, - lbd: { type: "INTEGER", validate: p.isPositive }, - lpr: { type: "INTEGER", validate: p.isPositive }, + epg: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + egr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + egd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + ebd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + epr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lpg: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lgr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lgd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lbd: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, + lpr: { type: "INTEGER", validate: p.isPositive, formatter: FmtScoreNoCommas }, }, scoreRatingAlgs: { diff --git a/common/src/config/game-support/popn.ts b/common/src/config/game-support/popn.ts index 9c96830cc..9e8f55f45 100644 --- a/common/src/config/game-support/popn.ts +++ b/common/src/config/game-support/popn.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum, FmtPercent } from "../../utils/util"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -28,7 +29,7 @@ const PopnClasses = [ export const POPN_9B_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 100_000) }, + score: { type: "INTEGER", validate: p.isBetween(0, 100_000), formatter: FmtNum }, clearMedal: { type: "ENUM", values: [ @@ -70,7 +71,7 @@ export const POPN_9B_CONF = { optionalMetrics: { ...FAST_SLOW_MAXCOMBO, - gauge: { type: "INTEGER", validate: p.isBetween(0, 100) }, + gauge: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, }, scoreRatingAlgs: { diff --git a/common/src/config/game-support/sdvx.ts b/common/src/config/game-support/sdvx.ts index f3e508536..1aa3b24e8 100644 --- a/common/src/config/game-support/sdvx.ts +++ b/common/src/config/game-support/sdvx.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum, FmtPercent } from "../../utils/util"; import { ClassValue, ToDecimalPlaces, zodNonNegativeInt, zodTierlistData } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -73,7 +74,7 @@ export const SDVXVFClasses = [ export const SDVX_SINGLE_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 10_000_000) }, + score: { type: "INTEGER", validate: p.isBetween(0, 10_000_000), formatter: FmtNum }, lamp: { type: "ENUM", values: [ @@ -100,8 +101,8 @@ export const SDVX_SINGLE_CONF = { optionalMetrics: { ...FAST_SLOW_MAXCOMBO, - exScore: { type: "INTEGER", chartDependentMax: true }, - gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + exScore: { type: "INTEGER", chartDependentMax: true, formatter: FmtNum }, + gauge: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, }, scoreRatingAlgs: { diff --git a/common/src/config/game-support/usc.ts b/common/src/config/game-support/usc.ts index ed750b2de..ecffea392 100644 --- a/common/src/config/game-support/usc.ts +++ b/common/src/config/game-support/usc.ts @@ -1,5 +1,6 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { SDVXVFClasses } from "./sdvx"; +import { FmtNum, FmtPercent } from "../../utils/util"; import { ToDecimalPlaces } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -14,7 +15,7 @@ export const USC_CONF = { export const USC_CONTROLLER_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 10_000_000) }, + score: { type: "INTEGER", validate: p.isBetween(0, 10_000_000), formatter: FmtNum }, lamp: { type: "ENUM", values: [ @@ -41,7 +42,7 @@ export const USC_CONTROLLER_CONF = { optionalMetrics: { ...FAST_SLOW_MAXCOMBO, - gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) }, + gauge: { type: "DECIMAL", validate: p.isBetween(0, 100), formatter: FmtPercent }, }, scoreRatingAlgs: { diff --git a/common/src/config/game-support/wacca.ts b/common/src/config/game-support/wacca.ts index dc6ae2518..88037e8b3 100644 --- a/common/src/config/game-support/wacca.ts +++ b/common/src/config/game-support/wacca.ts @@ -1,4 +1,5 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; +import { FmtNum } from "../../utils/util"; import { ClassValue } from "../config-utils"; import { p } from "prudence"; import { z } from "zod"; @@ -47,7 +48,7 @@ export const WaccaColours = [ export const WACCA_SINGLE_CONF = { providedMetrics: { - score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000) }, + score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000), formatter: FmtNum }, lamp: { type: "ENUM", values: ["FAILED", "CLEAR", "MISSLESS", "FULL COMBO", "ALL MARVELOUS"], diff --git a/common/src/lib/schemas.ts b/common/src/lib/schemas.ts index 85a633161..0dfb8bc77 100644 --- a/common/src/lib/schemas.ts +++ b/common/src/lib/schemas.ts @@ -581,7 +581,7 @@ const PRE_SCHEMAS = { ], (self) => Array.isArray(self) && self.length <= 6 ), - scoreBucket: p.isIn(null, "grade", "lamp"), + preferredDefaultEnum: p.isIn(null, "grade", "lamp"), gameSpecific: PrudenceZodShim(gptConfig.preferences), }, diff --git a/common/src/types/documents.ts b/common/src/types/documents.ts index a3ff266c1..25c50289f 100644 --- a/common/src/types/documents.ts +++ b/common/src/types/documents.ts @@ -638,7 +638,7 @@ export interface UGPTSettingsDocument { preferredSessionAlg: SessionRatingAlgorithms[GPT] | null; preferredProfileAlg: ProfileRatingAlgorithms[GPT] | null; stats: Array; - scoreBucket: "grade" | "lamp" | null; + preferredDefaultEnum: string | null; defaultTable: string | null; preferredRanking: "global" | "rival" | null; gameSpecific: Preferences[GPT]; diff --git a/common/src/types/game-config.ts b/common/src/types/game-config.ts index e5f2ff956..532e79f28 100644 --- a/common/src/types/game-config.ts +++ b/common/src/types/game-config.ts @@ -158,7 +158,7 @@ export type AnyClasses = { }; export type Classes = { - [G in GPTString]: keyof typeof GAME_PT_CONFIGS[G]["classes"]; + [G in GPTString]: string & keyof typeof GAME_PT_CONFIGS[G]["classes"]; }; export type ClassConfigs = { diff --git a/common/src/types/metrics.ts b/common/src/types/metrics.ts index ecc59ae6e..26cd5a05b 100644 --- a/common/src/types/metrics.ts +++ b/common/src/types/metrics.ts @@ -22,6 +22,7 @@ import type { interface ConfDecimalScoreMetricNormal { type: "DECIMAL"; + formatter: (v: number) => string; validate: (v: number) => string | true; @@ -31,6 +32,7 @@ interface ConfDecimalScoreMetricNormal { interface ConfIntegerScoreMetricNormal { type: "INTEGER"; + formatter: (v: number) => string; validate: (v: number) => string | true; @@ -40,6 +42,7 @@ interface ConfIntegerScoreMetricNormal { interface ConfDecimalScoreMetricChartDependent { type: "DECIMAL"; + formatter: (v: number) => string; /** * Is the maximum/minimum value of this metric chart dependent? @@ -51,6 +54,7 @@ interface ConfDecimalScoreMetricChartDependent { interface ConfIntegerScoreMetricChartDependent { type: "INTEGER"; + formatter: (v: number) => string; /** * Is the maximum/minimum value of this metric chart dependent? @@ -143,7 +147,7 @@ export type ExtractMetricValue = M extends ConfDecima * would return a type of "grade" | "lamp" */ export type ExtractEnumMetricNames> = { - [K in keyof R]: R[K] extends ConfEnumScoreMetric ? K : never; + [K in keyof R]: R[K] extends ConfEnumScoreMetric ? K & string : never; }[keyof R]; /** diff --git a/common/src/utils/util.ts b/common/src/utils/util.ts index ebc8b05b6..df9641980 100644 --- a/common/src/utils/util.ts +++ b/common/src/utils/util.ts @@ -6,6 +6,7 @@ import { } from "../config/config"; import type { GradeBoundary } from "../constants/grade-boundaries"; import type { + BMSCourseDocument, ChartDocument, GPTString, GPTStrings, @@ -228,6 +229,17 @@ export function FmtNum(num: number) { return num.toLocaleString(); } +export function FmtPercent(v: number, dp = 2) { + return `${v.toFixed(dp)}%`; +} + +/** + * Turns a number of 12834 into "12834" instead of "12,834". + */ +export function FmtScoreNoCommas(v: number) { + return v.toString(); +} + function WrapGrade(grade: string) { if (grade.endsWith("-") || grade.endsWith("+")) { return `(${grade})`; @@ -236,18 +248,6 @@ function WrapGrade(grade: string) { return grade; } -type FormatGradeDeltaReturns = - | { - lower: string; - upper: string; - closer: "upper"; - } - | { - lower: string; - upper?: string; - closer: "lower"; - }; - function RelativeGradeDelta( gradeBoundaries: Array>, scoreGrade: G, @@ -378,3 +378,33 @@ export function FormatPrError(err: PrudenceError, foreword = "Error"): string { return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`; } + +export function GetBMSCourseIndex(course: BMSCourseDocument) { + const gptConf = GetGamePTConfig("bms", course.playtype); + + const cls = gptConf.classes[course.set]; + + if (!cls) { + throw new Error( + `Invalid BMSCourse set of ${course.set}. No classes are defined for this set.` + ); + } + + return cls.values.findIndex((e) => e.id === course.value); +} + +/** + * Util for getting a games' grade for a given score. + */ +export function GetGrade(grades: Array>, score: number): G { + // sort grades going downwards in their boundaries. + const descendingGrades = grades.slice(0).sort((a, b) => b.lowerBound - a.lowerBound); + + for (const { name, lowerBound } of descendingGrades) { + if (score >= lowerBound) { + return name; + } + } + + throw new Error(`Could not resolve grade for score ${score}.`); +} diff --git a/database-seeds/scripts/test/match-type.test.ts b/database-seeds/scripts/test/match-type.test.ts index ab8c58253..aa0fee46c 100644 --- a/database-seeds/scripts/test/match-type.test.ts +++ b/database-seeds/scripts/test/match-type.test.ts @@ -16,7 +16,7 @@ import { ReadCollection } from "../util"; const uniquenessChecks: Array<{ game: Game; playtype: Playtype; matchType: MatchTypes }> = []; for (const game of allSupportedGames) { - for (const playtype of GetGameConfig(game).validPlaytypes) { + for (const playtype of GetGameConfig(game).playtypes) { const gptConfig = GetGamePTConfig(game, playtype); for (const matchType of gptConfig.supportedMatchTypes) { diff --git a/server/src/game-implementations/games/_common.ts b/server/src/game-implementations/games/_common.ts index 3c732f628..8591c69de 100644 --- a/server/src/game-implementations/games/_common.ts +++ b/server/src/game-implementations/games/_common.ts @@ -1,11 +1,11 @@ import { CreatePBMergeFor } from "game-implementations/utils/pb-merge"; import { ProfileSumBestN } from "game-implementations/utils/profile-calc"; import { SessionAvgBest10For } from "game-implementations/utils/session-calc"; -import { InternalFailure } from "lib/score-import/framework/common/converter-failures"; import { Volforce } from "rg-stats"; import { FmtNum, FmtNumCompact, + GetGrade, GetGradeDeltas, IIDXLIKE_GBOUNDARIES, SDVXLIKE_GBOUNDARIES, @@ -26,22 +26,6 @@ import type { } from "game-implementations/types"; import type { GPTStrings, GradeBoundary, integer, SpecificUserGameStats } from "tachi-common"; -/** - * Util for getting a games' grade for a given score. - */ -export function GetGrade(grades: Array>, score: number): G { - // sort grades going downwards in their boundaries. - const descendingGrades = grades.slice(0).sort((a, b) => b.lowerBound - a.lowerBound); - - for (const { name, lowerBound } of descendingGrades) { - if (score >= lowerBound) { - return name; - } - } - - throw new InternalFailure(`Could not resolve grade for score ${score}.`); -} - export const EX_SCORE_CHECK: ChartSpecificMetricValidator = (exScore, chart) => { if (exScore < 0) { return `EX Score cannot be negative. Got ${exScore}.`; diff --git a/server/src/lib/game-settings/create-game-settings.ts b/server/src/lib/game-settings/create-game-settings.ts index d9e81c14b..d869f5327 100644 --- a/server/src/lib/game-settings/create-game-settings.ts +++ b/server/src/lib/game-settings/create-game-settings.ts @@ -41,7 +41,7 @@ export async function CreateGameSettings(userID: integer, game: Game, playtype: preferredProfileAlg: null, preferredSessionAlg: null, preferredScoreAlg: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, preferredRanking: null, stats: [], diff --git a/server/src/lib/showcase/get-stats.test.ts b/server/src/lib/showcase/get-stats.test.ts index 484e04ba0..8bb96d285 100644 --- a/server/src/lib/showcase/get-stats.test.ts +++ b/server/src/lib/showcase/get-stats.test.ts @@ -24,7 +24,7 @@ t.test("#EvalulateUsersStatsShowcase", (t) => { preferredProfileAlg: null, preferredScoreAlg: null, preferredSessionAlg: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, preferredRanking: null, stats: [ diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts index 30bae52ef..ab7bf667b 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts @@ -21,7 +21,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { preferredSessionAlg: null, preferredProfileAlg: null, preferredRanking: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, stats: [], gameSpecific: { @@ -69,7 +69,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { preferredScoreAlg: "ktLampRating", preferredSessionAlg: null, preferredProfileAlg: null, - scoreBucket: null, + preferredDefaultEnum: null, preferredRanking: null, defaultTable: null, stats: [], @@ -127,7 +127,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { preferredSessionAlg: null, preferredProfileAlg: null, preferredRanking: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, stats: [], gameSpecific: { @@ -188,7 +188,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { preferredSessionAlg: null, preferredProfileAlg: null, preferredRanking: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, stats: [], gameSpecific: { @@ -380,7 +380,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { preferredSessionAlg: null, preferredProfileAlg: null, preferredRanking: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: "mock_table", stats: [], gameSpecific: { diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts index e98510e0c..55d4f2b02 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts @@ -46,7 +46,7 @@ router.patch( preferredRanking: optNull(p.isIn("global", "rival")), gameSpecific: optNull(gameSpecificSchema), - scoreBucket: optNull(p.isIn("grade", "lamp")), + preferredDefaultEnum: optNull(p.isIn("grade", "lamp")), }); if (err) { @@ -86,8 +86,8 @@ router.patch( } } - if (body.scoreBucket !== undefined) { - updateQuery[`preferences.scoreBucket`] = body.scoreBucket; + if (body.preferredDefaultEnum !== undefined) { + updateQuery[`preferences.preferredDefaultEnum`] = body.preferredDefaultEnum; } if (body.defaultTable !== undefined) { diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts index 4e33dde7d..680c69156 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts @@ -24,7 +24,7 @@ const SetFolders = async () => { preferredScoreAlg: null, preferredSessionAlg: null, preferredRanking: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, stats: [ { @@ -90,7 +90,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/showcase", (t) => { preferredScoreAlg: null, preferredSessionAlg: null, preferredRanking: null, - scoreBucket: null, + preferredDefaultEnum: null, defaultTable: null, stats: [ { diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index 64f669a5a..c05cfc0e4 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -839,7 +839,7 @@ export const FakeGameSettings: UGPTSettingsDocument = { preferredProfileAlg: null, preferredScoreAlg: null, preferredSessionAlg: null, - scoreBucket: null, + preferredDefaultEnum: null, preferredRanking: null, stats: [], }, diff --git a/staging b/staging new file mode 160000 index 000000000..3a5a1af20 --- /dev/null +++ b/staging @@ -0,0 +1 @@ +Subproject commit 3a5a1af20ab74d6d66f81be13fe112f2e7a0906a