From f20b0c792af5dd71981c4b4a85bca26bbb626fda Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Thu, 27 Oct 2022 19:27:49 +0100 Subject: [PATCH 1/8] fix: make compare top 100s default --- .../_playtype/rivals/RivalsComparePage.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx index 75cd0e1b1..d9245297a 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx @@ -39,14 +39,16 @@ export default function RivalsComparePage({
- - - Compare Top 100s - - + Best PBs + + + + Compare Top 100s + + Folder Comparisons @@ -58,14 +60,7 @@ export default function RivalsComparePage({ - top100 - - - + + Date: Thu, 27 Oct 2022 19:28:47 +0100 Subject: [PATCH 2/8] refactor: rename score-leaderboard to PB leaderboard --- .../dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx | 4 ++-- .../games/_game/_playtype/rivals/RivalsComparePage.tsx | 4 ++-- docs/docs/api/routes/gpt.md | 6 +++--- .../server/router/api/v1/games/_game/_playtype/router.ts | 4 ++-- .../v1/users/_userID/games/_game/_playtype/rivals/router.ts | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) 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 8f859eea4..0328efefd 100644 --- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx @@ -41,7 +41,7 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) { - Score Leaderboards + PB Leaderboards
@@ -53,7 +53,7 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) { )} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx index d9245297a..a8a70cba0 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx @@ -65,8 +65,8 @@ export default function RivalsComparePage({ diff --git a/docs/docs/api/routes/gpt.md b/docs/docs/api/routes/gpt.md index 9064deeb8..52db8ee9b 100644 --- a/docs/docs/api/routes/gpt.md +++ b/docs/docs/api/routes/gpt.md @@ -507,9 +507,9 @@ GET /api/v1/games/bms/7K/tableID/insane ***** -## Retrieve the score leaderboard for this Game. +## Retrieve the PB leaderboard for this Game. -`GET /api/v1/games/:game/:playtype/score-leaderboard` +`GET /api/v1/games/:game/:playtype/pb-leaderboard` ### Parameters @@ -522,7 +522,7 @@ GET /api/v1/games/bms/7K/tableID/insane | Property | Type | Description | | :: | :: | :: | -| `pbs` | Array<PBDocument> | The array of pbs part of the score leaderboard. | +| `pbs` | Array<PBDocument> | The array of pbs part of the PB leaderboard. | | `songs` | Array<[SongDocument](../../schemas/song.md)> | The array of songs part of the PBs. | | `charts` | Array<[ChartDocument](../../schemas/chart.md)> | The array of charts part of the PBs. | | `users` | Array<[UserDocument](../../schemas/user.md)> | The array of users part of the PBs. | diff --git a/server/src/server/router/api/v1/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/router.ts index 6587784e4..b9ab72651 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/router.ts @@ -153,9 +153,9 @@ router.get("/leaderboard", async (req, res) => { * @param alg - An alternative algorithm to use instead of the gpts default. * @param limit - How many scores to return. * - * @name GET /api/v1/games/:game/:playtype/score-leaderboard + * @name GET /api/v1/games/:game/:playtype/pb-leaderboard */ -router.get("/score-leaderboard", async (req, res) => { +router.get("/pb-leaderboard", async (req, res) => { const { game, playtype } = GetGPT(req); const gptConfig = GetGamePTConfig(game, playtype); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts index 7a1f6f5ef..95ae0519e 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts @@ -103,7 +103,7 @@ router.get("/challengers", async (req, res) => { }); /** - * Retrieve a "score leaderboard" for this user's set of rivals. + * Retrieve a "PB leaderboard" for this user's set of rivals. * * This is - effectively - the best 100 scores from this set of users on the given * rating algorithm. @@ -111,9 +111,9 @@ router.get("/challengers", async (req, res) => { * @param alg - The score rating algorithm to sort on. Defaults to whatever the GPTConfig * default is. * - * @name GET /api/v1/users/:userID/games/:game/:playtype/rivals/score-leaderboard + * @name GET /api/v1/users/:userID/games/:game/:playtype/rivals/pb-leaderboard */ -router.get("/score-leaderboard", async (req, res) => { +router.get("/pb-leaderboard", async (req, res) => { const { user, game, playtype } = GetUGPT(req); const gptConfig = GetGamePTConfig(game, playtype); From 73f929b3e4c059c0e8ad32f801f7d8697efc0237 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Sun, 30 Oct 2022 19:24:17 +0000 Subject: [PATCH 3/8] feat: initial pb-union code --- .../rivals/RivalCompareTop100Page.tsx | 30 +++++ .../_playtype/rivals/RivalsComparePage.tsx | 5 +- .../games/_game/_playtype/pbs/router.ts | 124 +++++++++++++++++- server/src/utils/queries/pbs.ts | 38 +++++- 4 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx new file mode 100644 index 000000000..72e05ca08 --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx @@ -0,0 +1,30 @@ +import DebugContent from "components/util/DebugContent"; +import useApiQuery from "components/util/query/useApiQuery"; +import useLUGPTSettings from "components/util/useLUGPTSettings"; +import React from "react"; +import { PublicUserDocument } from "tachi-common"; +import { GamePT } from "types/react"; + +export default function RivalCompareTop100Page({ + reqUser, + game, + playtype, +}: GamePT & { + reqUser: PublicUserDocument; +}) { + const { settings } = useLUGPTSettings(); + + if (!settings) { + return
You have no settings. How did you get here?
; + } + + const { data, error } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil` + ); + + return ( +
+ +
+ ); +} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx index a8a70cba0..a013d6934 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx @@ -9,6 +9,7 @@ import React from "react"; import { Col, Row } from "react-bootstrap"; import { Route, Switch } from "react-router-dom"; import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common"; +import RivalCompareTop100Page from "./RivalCompareTop100Page"; export default function RivalsComparePage({ reqUser, @@ -73,7 +74,9 @@ export default function RivalsComparePage({ + > + + { * * @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/best */ -router.get("/best", async (req, res) => { +router.get("/best", prValidate({ alg: "*string" }), async (req, res) => { const { user, game, playtype } = GetUGPT(req); const gptConfig = GetGamePTConfig(game, playtype); @@ -141,6 +142,121 @@ router.get("/best", async (req, res) => { }); }); +/** + * Return this user's best 100 personal-bests, unioned with the best 100 of another + * player. + * + * This is used for comparing best-100s with a player, so for example, if player B + * has charts in their top 100 that player A doesn't, it will still fetch the PB + * of player A for comparison reasons. + * + * @param alg - Specifies an override for the default algorithm + * to sort on. + * @param withUser - The userID to union personal bests with. + */ +router.get("/best-union", prValidate({ alg: "*string", withUser: "string" }), async (req, res) => { + const { user, game, playtype } = GetUGPT(req); + + const gptConfig = GetGamePTConfig(game, playtype); + + const query = req.query as { + alg?: string; + withUser: string; + }; + + const otherUser = await ResolveUser(query.withUser); + + if (!otherUser) { + return res.status(400).json({ + success: false, + description: `The user '${query.withUser}' does not exist.`, + }); + } + + const hasPlayed = await db["game-stats"].findOne({ + game, + playtype, + userID: otherUser.id, + }); + + if (!hasPlayed) { + return res.status(400).json({ + success: false, + description: `The user '${otherUser.username}' has not played ${FormatGame( + game, + playtype + )}.`, + }); + } + + if (req.query.alg !== undefined && !IsValidScoreAlg(gptConfig, req.query.alg)) { + return res.status(400).json({ + success: false, + description: `Invalid score algorithm. Expected any of ${gptConfig.scoreRatingAlgs.join( + ", " + )}`, + }); + } + + const alg = req.query.alg ?? gptConfig.defaultScoreRatingAlg; + + const options = { + projection: { + chartID: 1, + }, + limit: 100, + sort: { + [`calculatedData.${alg}`]: -1, + }, + }; + + // lets fetch the set of chartIDs in each users best 100s + const baseUserBestChartIDs = await db["personal-bests"].find( + { + userID: user.id, + game, + playtype, + isPrimary: true, + }, + options + ); + + const withUserBestChartIDs = await db["personal-bests"].find( + { + userID: otherUser.id, + game, + playtype, + isPrimary: true, + }, + options + ); + + const chartIDs = [ + ...baseUserBestChartIDs.map((e) => e.chartID), + ...withUserBestChartIDs.map((e) => e.chartID), + ]; + + // then fetch both of their scores on each. + const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg); + const withUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg); + + const { songs, charts } = await GetRelevantSongsAndCharts( + [...baseUserPBs, ...withUserPBs], + game + ); + + return res.status(200).json({ + success: true, + description: `Retrieved the union best 100 of ${user.username} and ${otherUser.username}.`, + body: { + baseUserPBs, + withUserPBs, + songs, + charts, + }, + }); +}); + /** * Returns a user's PB on the given chart. If the user has not played this chart, 404 is * returned. diff --git a/server/src/utils/queries/pbs.ts b/server/src/utils/queries/pbs.ts index fdc1a0bb6..4e4f0ddcb 100644 --- a/server/src/utils/queries/pbs.ts +++ b/server/src/utils/queries/pbs.ts @@ -1,5 +1,5 @@ import db from "external/mongo/db"; -import type { PBScoreDocument } from "tachi-common"; +import type { IDStrings, integer, PBScoreDocument, ScoreCalculatedDataLookup } from "tachi-common"; export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) { const adjAbove = (await db["personal-bests"].find( @@ -30,3 +30,39 @@ export async function GetAdjacentBelow(userPB: PBScoreDocument, size = 5) { return adjAbove; } + +export type PBsWithPersonalRanking = Array; + +/** + * Given a user, an algorithm, and an array of chartIDs, return the user's PBs + * on those charts with where they rank in their personal PBs. + */ +export async function GetPBsWithUserRankings( + userID: integer, + chartIDs: Array, + alg: ScoreCalculatedDataLookup[IDStrings] +) { + // i was going to do this with a fancy aggregate pipeline + // but it's slower. + // ah well + const pbs = (await db["personal-bests"].find({ + userID, + chartID: { $in: chartIDs }, + isPrimary: true, + })) as Array; + + await Promise.all( + pbs.map(async (pb) => { + const personalRanking = await db["personal-bests"].count({ + userID, + [`calculatedData.${alg}`]: { $gt: pb.calculatedData[alg] }, + }); + + // eslint-disable-next-line require-atomic-updates + pb.__personalRanking = personalRanking + 1; + }) + ); + + // __personalRanking guaranteed to be assigned + return pbs as PBsWithPersonalRanking; +} From faf4ff16ed6d4b083f0ff3e8792c0613ba0a0818 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Tue, 1 Nov 2022 00:59:19 +0000 Subject: [PATCH 4/8] feat: initial comparePBsTable draft --- .../rivals/RivalCompareTop100Page.tsx | 144 +++++++++++++++++- client/src/components/layout/page/Card.tsx | 18 ++- .../tables/cells/JubeatScoreCell.tsx | 22 +++ .../game-core-cells/CHUNITHMCoreCells.tsx | 17 +-- .../game-core-cells/JubeatCoreCells.tsx | 19 +-- .../game-core-cells/ShortScoreCoreCells.tsx | 84 ++++++++++ .../components/tables/headers/GameHeaders.tsx | 10 +- .../tables/rivals/ComparePBsTable.tsx | 87 +++++++++++ client/src/components/user/ProfilePicture.tsx | 23 ++- client/src/components/user/UGPTHeader.tsx | 2 +- client/src/types/api-returns.ts | 7 + client/src/types/tables.ts | 7 + .../games/_game/_playtype/pbs/router.ts | 2 +- 13 files changed, 394 insertions(+), 48 deletions(-) create mode 100644 client/src/components/tables/cells/JubeatScoreCell.tsx create mode 100644 client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx create mode 100644 client/src/components/tables/rivals/ComparePBsTable.tsx diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx index 72e05ca08..03cad597e 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx @@ -1,9 +1,20 @@ -import DebugContent from "components/util/DebugContent"; +import { CreateSongMap } from "util/data"; +import Card from "components/layout/page/Card"; +import ComparePBsTable from "components/tables/rivals/ComparePBsTable"; +import ProfilePicture from "components/user/ProfilePicture"; +import UGPTRatingsTable from "components/user/UGPTStatsOverview"; +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 useLUGPTSettings from "components/util/useLUGPTSettings"; -import React from "react"; +import React, { useState } from "react"; +import { Col, Form, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; import { PublicUserDocument } from "tachi-common"; +import { BestPBsUnion, UGPTStatsReturn } from "types/api-returns"; import { GamePT } from "types/react"; +import { ComparePBsDataset } from "types/tables"; export default function RivalCompareTop100Page({ reqUser, @@ -18,13 +29,136 @@ export default function RivalCompareTop100Page({ return
You have no settings. How did you get here?
; } - const { data, error } = useApiQuery( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil` + const [selectedUser, setSelectedUser] = useState(null); + + const { data, error } = useApiQuery>( + `/users/${reqUser.id}/games/${game}/${playtype}/rivals`, + undefined, + [settings.rivals.join(",")] ); + if (error) { + return ; + } + + if (!data) { + return ; + } + return (
- + + setSelectedUser(e.target.value || null)} + > + + {data.map((e) => ( + + ))} + +
Select a rival to compare your best 100 PBs with.
+
+ {selectedUser !== null && ( + <> + + + + )}
); } + +function RivalBest100Compare({ + reqUser, + game, + playtype, + withUser, +}: { + reqUser: PublicUserDocument; + withUser: string; +} & GamePT) { + const { data, error } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=${withUser}` + ); + + if (error) { + return ; + } + + if (!data) { + return ; + } + + const basePBLookup = new Map(data.baseUserPBs.map((e) => [e.chartID, e])); + const comparePBLookup = new Map(data.withUserPBs.map((e) => [e.chartID, e])); + + const songMap = CreateSongMap(data.songs); + + const dataset: ComparePBsDataset = data.charts.map((chart) => ({ + chart, + base: basePBLookup.get(chart.chartID) ?? null, + compare: comparePBLookup.get(chart.chartID) ?? null, + song: songMap.get(chart.songID)!, + })); + + return ( + + + + + + + + + ); +} + +function UserCard({ username, game, playtype }: { username: string } & GamePT) { + const { data, error } = useApiQuery( + `/users/${username}/games/${game}/${playtype}` + ); + + if (error) { + return ; + } + + return ( + + + + + + + +

+ + {username} + +

+ + + {data ? : } + +
+
+ + ); +} diff --git a/client/src/components/layout/page/Card.tsx b/client/src/components/layout/page/Card.tsx index 7d288b726..b7ce6228d 100644 --- a/client/src/components/layout/page/Card.tsx +++ b/client/src/components/layout/page/Card.tsx @@ -8,20 +8,22 @@ export default function Card({ className, style, }: { - header: JSX.Element | string; + header?: JSX.Element | string; footer?: JSX.Element | string; className?: string; style?: CSSProperties; } & JustChildren) { return (
-
- {typeof header === "string" ? ( -

{header}

- ) : ( - header - )} -
+ {header && ( +
+ {typeof header === "string" ? ( +

{header}

+ ) : ( + header + )} +
+ )}
{children}
{footer &&
{footer}
}
diff --git a/client/src/components/tables/cells/JubeatScoreCell.tsx b/client/src/components/tables/cells/JubeatScoreCell.tsx new file mode 100644 index 000000000..0584527eb --- /dev/null +++ b/client/src/components/tables/cells/JubeatScoreCell.tsx @@ -0,0 +1,22 @@ +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 JubeatScoreCell({ sc }: { sc: ScoreDocument | PBScoreDocument }) { + const gptConfig = GetGamePTConfig(sc.game, sc.playtype); + + return ( + + {sc.scoreData.grade} +
+ {sc.scoreData.percent.toFixed(2)}% +
+ {FormatMillions(sc.scoreData.score)} + + ); +} diff --git a/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx b/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx index 6ffa03391..e1eef2267 100644 --- a/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx @@ -1,9 +1,8 @@ -import { ChangeOpacity } from "util/color-opacity"; -import { FormatMillions } from "util/misc"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { PBScoreDocument, ScoreDocument } from "tachi-common"; import CHUNITHMJudgementCell from "../cells/CHUNITHMJudgementCell"; import LampCell from "../cells/LampCell"; +import MillionsScoreCell from "../cells/MillionsScoreCell"; import RatingCell from "../cells/RatingCell"; export default function CHUNITHMCoreCells({ @@ -13,19 +12,9 @@ export default function CHUNITHMCoreCells({ sc: ScoreDocument<"chunithm:Single"> | PBScoreDocument<"chunithm:Single">; rating: keyof ScoreDocument["calculatedData"]; }) { - const gptConfig = GetGamePTConfig(sc.game, sc.playtype); - return ( <> - - {sc.scoreData.grade} -
- {FormatMillions(sc.scoreData.score)} - + diff --git a/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx b/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx index 409e7f7d5..40009dc05 100644 --- a/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx +++ b/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx @@ -1,8 +1,7 @@ -import { ChangeOpacity } from "util/color-opacity"; -import { FormatMillions } from "util/misc"; import React from "react"; -import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { PBScoreDocument, ScoreDocument } from "tachi-common"; import JubeatJudgementCell from "../cells/JubeatJudgementCell"; +import JubeatScoreCell from "../cells/JubeatScoreCell"; import LampCell from "../cells/LampCell"; import RatingCell from "../cells/RatingCell"; @@ -13,21 +12,9 @@ export default function JubeatCoreCells({ sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">; rating: keyof ScoreDocument["calculatedData"]; }) { - const gptConfig = GetGamePTConfig(sc.game, sc.playtype); - return ( <> - - {sc.scoreData.grade} -
- {sc.scoreData.percent.toFixed(2)}% -
- {FormatMillions(sc.scoreData.score)} - + diff --git a/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx b/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx new file mode 100644 index 000000000..d24fc6485 --- /dev/null +++ b/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx @@ -0,0 +1,84 @@ +import IIDXLampCell from "components/tables/cells/IIDXLampCell"; +import JubeatScoreCell from "components/tables/cells/JubeatScoreCell"; +import LampCell from "components/tables/cells/LampCell"; +import MillionsScoreCell from "components/tables/cells/MillionsScoreCell"; +import PopnLampCell from "components/tables/cells/PopnLampCell"; +import ScoreCell from "components/tables/cells/ScoreCell"; +import SDVXLampCell from "components/tables/cells/SDVXLampCell"; +import React from "react"; +import { ChartDocument, Game, PBScoreDocument, ScoreDocument } from "tachi-common"; +import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell"; +import BMSCoreCells from "./BMSCoreCells"; + +export default function ShortScoreCoreCells({ + game, + score, + chart, +}: { + score: ScoreDocument | PBScoreDocument; + chart: ChartDocument; + game: Game; +}) { + const sc = score as any; // lazy hack + + if (game === "iidx") { + return ( + <> + + } /> + + ); + } else if (game === "bms" || game === "pms") { + return ( + <> + + + + ); + } else if ( + game === "sdvx" || + game === "usc" || + game === "museca" || + game === "wacca" || + game === "chunithm" || + game === "ddr" || + game === "gitadora" + ) { + return ( + <> + + + + ); + } else if (game === "maimai") { + return ( + <> + + + + ); + } else if (game === "jubeat") { + return ( + <> + + + + ); + } else if (game === "popn") { + return ( + <> + + + + ); + } else if (game === "itg") { + return ( + <> + + + + ); + } + + return <>No cells defined for {game}. Report this!; +} diff --git a/client/src/components/tables/headers/GameHeaders.tsx b/client/src/components/tables/headers/GameHeaders.tsx index 76a3ab57a..08e11cf2a 100644 --- a/client/src/components/tables/headers/GameHeaders.tsx +++ b/client/src/components/tables/headers/GameHeaders.tsx @@ -10,13 +10,19 @@ import { ScoreDocument, } from "tachi-common"; import { SetState } from "types/react"; -import { FolderDataset, PBDataset, RivalChartDataset, ScoreDataset } from "types/tables"; +import { + ComparePBsDataset, + FolderDataset, + PBDataset, + RivalChartDataset, + ScoreDataset, +} from "types/tables"; import { Playtype } from "types/tachi"; import SelectableRating from "../components/SelectableRating"; import { Header, ZTableTHProps } from "../components/TachiTable"; export function GetGPTCoreHeaders< - Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset + Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset | ComparePBsDataset >( game: Game, playtype: Playtype, diff --git a/client/src/components/tables/rivals/ComparePBsTable.tsx b/client/src/components/tables/rivals/ComparePBsTable.tsx new file mode 100644 index 000000000..f7a52a016 --- /dev/null +++ b/client/src/components/tables/rivals/ComparePBsTable.tsx @@ -0,0 +1,87 @@ +import { NumericSOV, StrSOV } from "util/sorts"; +import React from "react"; +import { Game, IDStrings, Playtype } from "tachi-common"; +import { ESDCompare } from "tachi-common/lib/esd"; +import { ComparePBsDataset } from "types/tables"; +import DifficultyCell from "../cells/DifficultyCell"; +import TitleCell from "../cells/TitleCell"; +import TachiTable, { Header } from "../components/TachiTable"; +import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells"; +import ChartHeader from "../headers/ChartHeader"; + +export default function ComparePBsTable({ + dataset, + game, + baseUser, + compareUser, +}: { + dataset: ComparePBsDataset; + game: Game; + playtype: Playtype; + baseUser: string; + compareUser: string; +}) { + const headers: Header[] = [ + // ["#", "#"], + ChartHeader(game, (d) => d.chart), + ["Song", "Song", StrSOV((x) => x.song.title)], + ["", "", () => 1, () => {baseUser}], + [ + "Vs.", + "Vs.", + NumericSOV((x) => { + if (!x.base) { + return -Infinity; + } + + if (!x.compare) { + return Infinity; + } + + if (x.base.scoreData.esd && x.compare.scoreData.esd) { + return ESDCompare(x.base.scoreData.esd, x.compare.scoreData.esd); + } + + return x.base.scoreData.percent - x.compare.scoreData.percent; + }), + ], + ["", "", () => 1, () => {compareUser}], + ]; + + return ( + } + /> + ); +} + +function Row({ + data, + game, +}: { + data: ComparePBsDataset[0]; + game: Game; +}) { + return ( + + + + {data.base ? ( + + ) : ( + Not Played + )} + some comparison thing here + {data.compare ? ( + + ) : ( + Not Played + )} + + ); +} diff --git a/client/src/components/user/ProfilePicture.tsx b/client/src/components/user/ProfilePicture.tsx index b9d5372eb..be8722506 100644 --- a/client/src/components/user/ProfilePicture.tsx +++ b/client/src/components/user/ProfilePicture.tsx @@ -2,7 +2,28 @@ import { ToAPIURL } from "util/api"; import React from "react"; import { PublicUserDocument } from "tachi-common"; -export default function ProfilePicture({ user, src }: { user: PublicUserDocument; src?: string }) { +export default function ProfilePicture({ + user, + src, +}: { + user: PublicUserDocument | string; + src?: string; +}) { + if (typeof user === "string") { + return ( + {`${user}'s + ); + } + return ( = (PublicUserDocu index: number; }; })[]; + +export type ComparePBsDataset = Array<{ + base: (PBScoreDocument & { __personalRanking: integer }) | null; + compare: (PBScoreDocument & { __personalRanking: integer }) | null; + chart: ChartDocument; + song: SongDocument; +}>; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts index b159e21f0..05f244f56 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts @@ -238,7 +238,7 @@ router.get("/best-union", prValidate({ alg: "*string", withUser: "string" }), as // then fetch both of their scores on each. const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg); - const withUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg); + const withUserPBs = await GetPBsWithUserRankings(otherUser.id, chartIDs, alg); const { songs, charts } = await GetRelevantSongsAndCharts( [...baseUserPBs, ...withUserPBs], From 8f903103ad26c5702d5556e75afea39017a85f4c Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Tue, 1 Nov 2022 03:10:03 +0000 Subject: [PATCH 5/8] feat: allow more than 100 best pbs in rival compare --- ...00Page.tsx => RivalCompareBestPBsPage.tsx} | 10 +- .../_playtype/rivals/RivalsComparePage.tsx | 12 +- .../games/_game/_playtype/pbs/router.ts | 207 ++++++++++-------- 3 files changed, 127 insertions(+), 102 deletions(-) rename client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/{RivalCompareTop100Page.tsx => RivalCompareBestPBsPage.tsx} (94%) diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx similarity index 94% rename from client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx rename to client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx index 03cad597e..ef8c39d7c 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx @@ -16,7 +16,7 @@ import { BestPBsUnion, UGPTStatsReturn } from "types/api-returns"; import { GamePT } from "types/react"; import { ComparePBsDataset } from "types/tables"; -export default function RivalCompareTop100Page({ +export default function RivalCompareBestPBsPage({ reqUser, game, playtype, @@ -60,12 +60,12 @@ export default function RivalCompareTop100Page({ ))} -
Select a rival to compare your best 100 PBs with.
+
Select a rival to compare against.
{selectedUser !== null && ( <> - ( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=${withUser}` + `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=${withUser}&limit=250` ); if (error) { diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx index a013d6934..c98256a23 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx @@ -9,7 +9,7 @@ import React from "react"; import { Col, Row } from "react-bootstrap"; import { Route, Switch } from "react-router-dom"; import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common"; -import RivalCompareTop100Page from "./RivalCompareTop100Page"; +import RivalCompareBestPBsPage from "./RivalCompareBestPBsPage"; export default function RivalsComparePage({ reqUser, @@ -42,12 +42,12 @@ export default function RivalsComparePage({
- Best PBs + Everyone's Best PBs - Compare Top 100s + Compare PBs @@ -75,7 +75,11 @@ export default function RivalsComparePage({ exact path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare" > - + { }); /** - * Return this user's best 100 personal-bests, unioned with the best 100 of another + * Return this user's best N personal-bests, unioned with the best 100 of another * player. * - * This is used for comparing best-100s with a player, so for example, if player B - * has charts in their top 100 that player A doesn't, it will still fetch the PB + * This is used for comparing best-Ns with a player, so for example, if player B + * has charts in their top N that player A doesn't, it will still fetch the PB * of player A for comparison reasons. * * @param alg - Specifies an override for the default algorithm * to sort on. * @param withUser - The userID to union personal bests with. + * @param limit - How many records to retrieve. Defaults to 100, Caps at 500. + * + * @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/best-union */ -router.get("/best-union", prValidate({ alg: "*string", withUser: "string" }), async (req, res) => { - const { user, game, playtype } = GetUGPT(req); +router.get( + "/best-union", + prValidate({ alg: "*string", withUser: "string", limit: "*string" }), + async (req, res) => { + const { user, game, playtype } = GetUGPT(req); - const gptConfig = GetGamePTConfig(game, playtype); + const gptConfig = GetGamePTConfig(game, playtype); - const query = req.query as { - alg?: string; - withUser: string; - }; + const query = req.query as { + alg?: string; + withUser: string; + }; - const otherUser = await ResolveUser(query.withUser); + const otherUser = await ResolveUser(query.withUser); - if (!otherUser) { - return res.status(400).json({ - success: false, - description: `The user '${query.withUser}' does not exist.`, - }); - } + if (!otherUser) { + return res.status(400).json({ + success: false, + description: `The user '${query.withUser}' does not exist.`, + }); + } - const hasPlayed = await db["game-stats"].findOne({ - game, - playtype, - userID: otherUser.id, - }); - - if (!hasPlayed) { - return res.status(400).json({ - success: false, - description: `The user '${otherUser.username}' has not played ${FormatGame( - game, - playtype - )}.`, - }); - } - - if (req.query.alg !== undefined && !IsValidScoreAlg(gptConfig, req.query.alg)) { - return res.status(400).json({ - success: false, - description: `Invalid score algorithm. Expected any of ${gptConfig.scoreRatingAlgs.join( - ", " - )}`, - }); - } - - const alg = req.query.alg ?? gptConfig.defaultScoreRatingAlg; - - const options = { - projection: { - chartID: 1, - }, - limit: 100, - sort: { - [`calculatedData.${alg}`]: -1, - }, - }; - - // lets fetch the set of chartIDs in each users best 100s - const baseUserBestChartIDs = await db["personal-bests"].find( - { - userID: user.id, + const hasPlayed = await db["game-stats"].findOne({ game, playtype, - isPrimary: true, - }, - options - ); - - const withUserBestChartIDs = await db["personal-bests"].find( - { userID: otherUser.id, - game, - playtype, - isPrimary: true, - }, - options - ); + }); - const chartIDs = [ - ...baseUserBestChartIDs.map((e) => e.chartID), - ...withUserBestChartIDs.map((e) => e.chartID), - ]; + if (!hasPlayed) { + return res.status(400).json({ + success: false, + description: `The user '${otherUser.username}' has not played ${FormatGame( + game, + playtype + )}.`, + }); + } - // then fetch both of their scores on each. - const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg); - const withUserPBs = await GetPBsWithUserRankings(otherUser.id, chartIDs, alg); + if (req.query.alg !== undefined && !IsValidScoreAlg(gptConfig, req.query.alg)) { + return res.status(400).json({ + success: false, + description: `Invalid score algorithm. Expected any of ${gptConfig.scoreRatingAlgs.join( + ", " + )}`, + }); + } - const { songs, charts } = await GetRelevantSongsAndCharts( - [...baseUserPBs, ...withUserPBs], - game - ); + const alg = req.query.alg ?? gptConfig.defaultScoreRatingAlg; + let limit = 100; - return res.status(200).json({ - success: true, - description: `Retrieved the union best 100 of ${user.username} and ${otherUser.username}.`, - body: { - baseUserPBs, - withUserPBs, - songs, - charts, - }, - }); -}); + if (req.query.limit !== undefined) { + const providedLimit = Number(req.query.limit); + + if (!Number.isInteger(providedLimit) || providedLimit > 500 || providedLimit < 1) { + return res.status(400).json({ + success: false, + description: `Invalid limit. Expected an integer between 1 and 500.`, + }); + } + + limit = providedLimit; + } + + const options = { + projection: { + chartID: 1, + }, + limit, + sort: { + [`calculatedData.${alg}`]: -1, + }, + }; + + // lets fetch the set of chartIDs in each users bests + const baseUserBestChartIDs = await db["personal-bests"].find( + { + userID: user.id, + game, + playtype, + isPrimary: true, + }, + options + ); + + const withUserBestChartIDs = await db["personal-bests"].find( + { + userID: otherUser.id, + game, + playtype, + isPrimary: true, + }, + options + ); + + const chartIDs = [ + ...baseUserBestChartIDs.map((e) => e.chartID), + ...withUserBestChartIDs.map((e) => e.chartID), + ]; + + // then fetch both of their scores on each. + const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg); + const withUserPBs = await GetPBsWithUserRankings(otherUser.id, chartIDs, alg); + + const { songs, charts } = await GetRelevantSongsAndCharts( + [...baseUserPBs, ...withUserPBs], + game + ); + + return res.status(200).json({ + success: true, + description: `Retrieved the union best 100 of ${user.username} and ${otherUser.username}.`, + body: { + baseUserPBs, + withUserPBs, + songs, + charts, + }, + }); + } +); /** * Returns a user's PB on the given chart. If the user has not played this chart, 404 is From 1d2f94a8aa44e67ac92a8133b6be6739b09a0986 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Tue, 1 Nov 2022 22:17:38 +0000 Subject: [PATCH 6/8] feat: rival folder compare --- .../_playtype/folders/FolderComparePage.tsx | 244 ++++++++++++++++++ .../_playtype/folders/FoldersMainPage.tsx | 5 +- .../_playtype/folders/SpecificFolderPage.tsx | 86 +++--- .../rivals/RivalCompareBestPBsPage.tsx | 164 ------------ .../_playtype/rivals/RivalsComparePage.tsx | 95 ------- .../_game/_playtype/rivals/RivalsMainPage.tsx | 36 +-- .../_playtype/rivals/RivalsManagePage.tsx | 1 + .../components/tables/cells/PBCompareCell.tsx | 136 ++++++++++ .../components/SelectableCompareType.tsx | 47 ++++ .../tables/rivals/ComparePBsTable.tsx | 61 ++++- client/src/components/util/UserIcon.tsx | 4 +- .../components/util/modal/UserSelectModal.tsx | 7 +- client/src/types/api-returns.ts | 13 +- client/src/types/tables.ts | 4 +- .../games/_game/_playtype/pbs/router.ts | 142 +--------- server/src/utils/queries/pbs.ts | 38 +-- 16 files changed, 564 insertions(+), 519 deletions(-) create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx delete mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx delete mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx create mode 100644 client/src/components/tables/cells/PBCompareCell.tsx create mode 100644 client/src/components/tables/components/SelectableCompareType.tsx 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 new file mode 100644 index 000000000..d8fb498ea --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx @@ -0,0 +1,244 @@ +import { CreateSongMap } from "util/data"; +import Card from "components/layout/page/Card"; +import ComparePBsTable from "components/tables/rivals/ComparePBsTable"; +import ProfilePicture from "components/user/ProfilePicture"; +import UGPTRatingsTable from "components/user/UGPTStatsOverview"; +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 useLUGPTSettings from "components/util/useLUGPTSettings"; +import React, { useContext, useEffect, useMemo, useState } from "react"; +import { Button, Col, Form, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { FolderDocument, GetGamePTConfig, PublicUserDocument } from "tachi-common"; +import { UGPTFolderReturns, UGPTStatsReturn } from "types/api-returns"; +import { GamePT } from "types/react"; +import { ComparePBsDataset } from "types/tables"; +import UserSelectModal from "components/util/modal/UserSelectModal"; +import UserIcon from "components/util/UserIcon"; +import { UserContext } from "context/UserContext"; + +export default function RivalCompareFolderPage({ + reqUser, + game, + playtype, + folder, +}: GamePT & { + reqUser: PublicUserDocument; + folder: FolderDocument; +}) { + const { settings } = useLUGPTSettings(); + const { user } = useContext(UserContext); + + const [selectedUser, setSelectedUser] = useState(null); + const [show, setShow] = useState(false); + + // honestly don't care if this errors or not + const { data, error } = useApiQuery>( + `/users/${settings?.userID}/games/${game}/${playtype}/rivals`, + undefined + // [settings?.rivals.join(",")] + ); + + if (!data) { + return ; + } + + let suggestUsers = data; + + // if the user viewing this page is logged in and *is not* + // the person this page is for + // show them in the suggestions. + if (user && user.id !== reqUser.id && data) { + suggestUsers = [...suggestUsers, user]; + } + + return ( +
+ + {suggestUsers && ( + <> + + {suggestUsers.map((e) => ( + + + + ))} + + + + + )} + + { + setSelectedUser(user); + setShow(false); + }} + show={show} + setShow={setShow} + url={`/games/${game}/${playtype}/players`} + excludeSet={[reqUser.id]} + excludeMsg="Can't pick the same user!" + /> + + + {selectedUser !== null && folder !== null && ( + <> + + + + )} +
+ ); +} + +function FolderCompare({ + reqUser, + game, + playtype, + withUser, + folder, +}: { + reqUser: PublicUserDocument; + withUser: PublicUserDocument; + folder: FolderDocument; +} & GamePT) { + const gptConfig = GetGamePTConfig(game, playtype); + + const { data: baseData, error: baseError } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folder.folderID}` + ); + + const { data: compareData, error: compareError } = useApiQuery( + `/users/${withUser.id}/games/${game}/${playtype}/folders/${folder.folderID}` + ); + + 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! + if (!baseData || !compareData) { + return []; + } + + const basePBLookup = new Map(baseData.pbs.map((e) => [e.chartID, e])); + const comparePBLookup = new Map(compareData.pbs.map((e) => [e.chartID, e])); + + const songMap = CreateSongMap(baseData.songs); + + let ds: ComparePBsDataset = baseData.charts.map((chart) => ({ + chart, + base: basePBLookup.get(chart.chartID) ?? null, + compare: comparePBLookup.get(chart.chartID) ?? null, + song: songMap.get(chart.songID)!, + })); + + if (!shouldIncludeNotPlayed) { + ds = ds.filter((e) => e.base && e.compare); + } + + return ds; + }, [shouldIncludeNotPlayed, baseData, compareData]); + + if (baseError) { + return ; + } + if (compareError) { + return ; + } + + if (!compareData || !baseData) { + return ; + } + + return ( + + + + + + setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)} + label="Include charts you haven't both played?" + /> + {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%)" + /> + )} + + + + + ); +} + +function UserCard({ username, game, playtype }: { username: string } & GamePT) { + const { data, error } = useApiQuery( + `/users/${username}/games/${game}/${playtype}` + ); + + if (error) { + return ; + } + + return ( + + + + + + + +

+ + {username} + +

+ + + {data ? : } + +
+
+ + ); +} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx index ab7b8e111..9fef9893b 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx @@ -71,10 +71,7 @@ export default function FoldersMainPage({ reqUser, game, playtype }: Props) { >
- + 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 c1dd0d1ad..b4f679b66 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 @@ -14,11 +14,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 SelectButton from "components/util/SelectButton"; +import ReferToUser from "components/util/ReferToUser"; +import SelectLinkButton from "components/util/SelectLinkButton"; +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 { Link, useParams } from "react-router-dom"; +import { Link, Route, Switch, useParams } from "react-router-dom"; import { ChartDocument, ChartTierlistInfo, @@ -36,7 +38,7 @@ import { import { UGPTFolderReturns } from "types/api-returns"; import { FolderDataset } from "types/tables"; import { Playtype } from "types/tachi"; -import ReferToUser from "components/util/ReferToUser"; +import FolderComparePage from "./FolderComparePage"; interface Props { reqUser: PublicUserDocument; @@ -51,8 +53,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}` ); - const [mode, setMode] = useState<"normal" | "ladder" | "timeline">("normal"); - const gptConfig = GetGamePTConfig(game, playtype); const folderDataset = useMemo(() => { @@ -97,6 +97,8 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { ); }, [folderDataset]); + const base = `${useUGPTBase({ reqUser, game, playtype })}/folders/${folderID}`; + if (error) { return ; } @@ -113,47 +115,63 @@ 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 - + + + + Compare Against User +
- {mode === "normal" ? ( - - ) : mode === "ladder" ? ( - - ) : ( - - )} + + + + + + + + + + + + + +
); @@ -163,11 +181,17 @@ function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID: const gptConfig = GetGamePTConfig(game, playtype); const [type, setType] = useState<"lamp" | "grade">(gptConfig.scoreBucket); const [value, setValue] = useState( - type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1 + type === "grade" + ? gptConfig.grades.indexOf(gptConfig.clearGrade) + : gptConfig.lamps.indexOf(gptConfig.clearLamp) ); useEffect(() => { - setValue(type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1); + setValue( + type === "grade" + ? gptConfig.grades.indexOf(gptConfig.clearGrade) + : gptConfig.lamps.indexOf(gptConfig.clearLamp) + ); }, [type]); return ( diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx deleted file mode 100644 index ef8c39d7c..000000000 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareBestPBsPage.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { CreateSongMap } from "util/data"; -import Card from "components/layout/page/Card"; -import ComparePBsTable from "components/tables/rivals/ComparePBsTable"; -import ProfilePicture from "components/user/ProfilePicture"; -import UGPTRatingsTable from "components/user/UGPTStatsOverview"; -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 useLUGPTSettings from "components/util/useLUGPTSettings"; -import React, { useState } from "react"; -import { Col, Form, Row } from "react-bootstrap"; -import { Link } from "react-router-dom"; -import { PublicUserDocument } from "tachi-common"; -import { BestPBsUnion, UGPTStatsReturn } from "types/api-returns"; -import { GamePT } from "types/react"; -import { ComparePBsDataset } from "types/tables"; - -export default function RivalCompareBestPBsPage({ - reqUser, - game, - playtype, -}: GamePT & { - reqUser: PublicUserDocument; -}) { - const { settings } = useLUGPTSettings(); - - if (!settings) { - return
You have no settings. How did you get here?
; - } - - const [selectedUser, setSelectedUser] = useState(null); - - const { data, error } = useApiQuery>( - `/users/${reqUser.id}/games/${game}/${playtype}/rivals`, - undefined, - [settings.rivals.join(",")] - ); - - if (error) { - return ; - } - - if (!data) { - return ; - } - - return ( -
- - setSelectedUser(e.target.value || null)} - > - - {data.map((e) => ( - - ))} - -
Select a rival to compare against.
-
- {selectedUser !== null && ( - <> - - - - )} -
- ); -} - -function RivalBestPBsCompare({ - reqUser, - game, - playtype, - withUser, -}: { - reqUser: PublicUserDocument; - withUser: string; -} & GamePT) { - const { data, error } = useApiQuery( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=${withUser}&limit=250` - ); - - if (error) { - return ; - } - - if (!data) { - return ; - } - - const basePBLookup = new Map(data.baseUserPBs.map((e) => [e.chartID, e])); - const comparePBLookup = new Map(data.withUserPBs.map((e) => [e.chartID, e])); - - const songMap = CreateSongMap(data.songs); - - const dataset: ComparePBsDataset = data.charts.map((chart) => ({ - chart, - base: basePBLookup.get(chart.chartID) ?? null, - compare: comparePBLookup.get(chart.chartID) ?? null, - song: songMap.get(chart.songID)!, - })); - - return ( - - - - - - - - - ); -} - -function UserCard({ username, game, playtype }: { username: string } & GamePT) { - const { data, error } = useApiQuery( - `/users/${username}/games/${game}/${playtype}` - ); - - if (error) { - return ; - } - - return ( - - - - - - - -

- - {username} - -

- - - {data ? : } - -
-
- - ); -} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx deleted file mode 100644 index c98256a23..000000000 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import ScoreLeaderboard from "components/game/ScoreLeaderboard"; -import useSetSubheader from "components/layout/header/useSetSubheader"; -import Divider from "components/util/Divider"; -import Icon from "components/util/Icon"; -import SelectLinkButton from "components/util/SelectLinkButton"; -import useLUGPTSettings from "components/util/useLUGPTSettings"; -import useUGPTBase from "components/util/useUGPTBase"; -import React from "react"; -import { Col, Row } from "react-bootstrap"; -import { Route, Switch } from "react-router-dom"; -import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common"; -import RivalCompareBestPBsPage from "./RivalCompareBestPBsPage"; - -export default function RivalsComparePage({ - reqUser, - game, - playtype, -}: { - reqUser: PublicUserDocument; - game: Game; - playtype: Playtype; -}) { - const gameConfig = GetGameConfig(game); - - useSetSubheader( - ["Users", reqUser.username, "Games", gameConfig.name, playtype, "Rivals", "Compare"], - [reqUser, game, playtype], - `Comparing ${reqUser.username}'s ${FormatGame(game, playtype)} Rivals` - ); - - const base = `${useUGPTBase({ reqUser, game, playtype })}/rivals/compare`; - - const { settings } = useLUGPTSettings(); - - if (!settings) { - return
You have no settings. How did you get here?
; - } - - return ( - - -
- - - Everyone's Best PBs - - - - - Compare PBs - - - - - Folder Comparisons - -
- - - - - - - - - - - - - - folders - - - -
- ); -} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx index 6417ed2bc..e604add93 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx @@ -1,3 +1,4 @@ +import ScoreLeaderboard from "components/game/ScoreLeaderboard"; import useSetSubheader from "components/layout/header/useSetSubheader"; import Divider from "components/util/Divider"; import Icon from "components/util/Icon"; @@ -8,7 +9,6 @@ import React from "react"; import { Col, Row } from "react-bootstrap"; import { Redirect, Route, Switch } from "react-router-dom"; import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common"; -import RivalsComparePage from "./RivalsComparePage"; import RivalsManagePage from "./RivalsManagePage"; export default function RivalsMainPage({ @@ -40,21 +40,17 @@ export default function RivalsMainPage({
- - - Goals & Quests - - - - Compare + + + Rival's Bests Activity - + - Manage Folders + Manage Tracked Charts @@ -79,20 +75,14 @@ export default function RivalsMainPage({ - NOT IMPLEMENTED YET - - - - NOT IMPLEMENTED YET - - - - + diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx index b90772aac..e1cb3dc98 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx @@ -202,6 +202,7 @@ function RivalsOverviewPage({ setShow={setShow} url={`/games/${game}/${playtype}/players`} excludeSet={rivals.map((e) => e.id)} + excludeMsg="Already Rivaled!" /> )} diff --git a/client/src/components/tables/cells/PBCompareCell.tsx b/client/src/components/tables/cells/PBCompareCell.tsx new file mode 100644 index 000000000..f39e47a66 --- /dev/null +++ b/client/src/components/tables/cells/PBCompareCell.tsx @@ -0,0 +1,136 @@ +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"; + +export default function PBCompareCell({ + base, + compare, + game, + playtype, + compareType, + shouldESD, +}: { + base: PBScoreDocument | null; + compare: PBScoreDocument | null; + game: Game; + playtype: Playtype; + compareType: "score" | "lamp"; + shouldESD: boolean; +}) { + let status: "win" | "draw" | "lose"; + + // wut? + if (!base && !compare) { + status = "draw"; + } else if (!base) { + status = "lose"; + } else if (!compare) { + status = "win"; + } else if (compareType === "score") { + status = cmp(base.scoreData.score, compare.scoreData.score); + } else { + status = cmp(base.scoreData.lampIndex, compare.scoreData.lampIndex); + } + + let scoreRenderFn = (k: number) => k.toString(); + + switch (game) { + case "chunithm": + case "ddr": + case "gitadora": + case "jubeat": + case "maimai": + case "usc": + case "sdvx": + case "popn": + case "wacca": + case "museca": + scoreRenderFn = FormatMillions; + break; + case "bms": + case "iidx": + scoreRenderFn = (k: number) => k.toString(); + break; + } + + const fmtCmpPercent = fmtNum( + (base?.scoreData.percent ?? 0) - (compare?.scoreData.percent ?? 0), + 2, + "%" + ); + const cmpScore = (base?.scoreData.score ?? 0) - (compare?.scoreData.score ?? 0); + const cmpLamp = (base?.scoreData.lampIndex ?? 0) - (compare?.scoreData.lampIndex ?? 0); + + let esdDiff; + + if (shouldESD) { + if (base?.scoreData.esd && compare?.scoreData.esd) { + esdDiff = ESDCompare(compare.scoreData.esd, base.scoreData.esd); + } + } + + return ( + + {compareType === "score" ? ( + <> + {(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)} +
+ ({fmtCmpPercent}) + {esdDiff !== undefined && ( + <> +
+ {esdDiff.toFixed(2) === "0.00" ? ( + Negligable Difference + ) : ( + + Relative: {(esdDiff > 0 ? "+" : "") + esdDiff.toFixed(2)} + + )} + + )} + + ) : ( + <> + {(cmpLamp > 0 ? "+" : "") + cmpLamp} + + )} + + ); +} + +function cmp(a: number, b: number) { + if (a === b) { + return "draw"; + } else if (a > b) { + return "win"; + } + + return "lose"; +} + +function fmtNum(num: number, fixed?: number, suffix?: string) { + let str = fixed ? num.toFixed(fixed) : num.toString(); + + if (num >= 0) { + str = `+${str}`; + } + + if (suffix) { + str += suffix; + } + + return str; +} diff --git a/client/src/components/tables/components/SelectableCompareType.tsx b/client/src/components/tables/components/SelectableCompareType.tsx new file mode 100644 index 000000000..c2bcee02b --- /dev/null +++ b/client/src/components/tables/components/SelectableCompareType.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { SetState } from "types/react"; +import { ZTableTHProps } from "./TachiTable"; + +export default function SelectableCompareType({ + compareType, + setCompareType, + changeSort, + currentSortMode, + reverseSort, +}: { + compareType: "score" | "lamp"; + setCompareType: SetState<"score" | "lamp">; +} & ZTableTHProps) { + return ( + + +
+ changeSort("Vs.")}> + + + + + ); +} diff --git a/client/src/components/tables/rivals/ComparePBsTable.tsx b/client/src/components/tables/rivals/ComparePBsTable.tsx index f7a52a016..427d66eaf 100644 --- a/client/src/components/tables/rivals/ComparePBsTable.tsx +++ b/client/src/components/tables/rivals/ComparePBsTable.tsx @@ -1,28 +1,33 @@ import { NumericSOV, StrSOV } from "util/sorts"; -import React from "react"; +import React, { useState } from "react"; import { Game, IDStrings, Playtype } from "tachi-common"; import { ESDCompare } from "tachi-common/lib/esd"; import { ComparePBsDataset } from "types/tables"; import DifficultyCell from "../cells/DifficultyCell"; import TitleCell from "../cells/TitleCell"; -import TachiTable, { Header } from "../components/TachiTable"; +import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable"; import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells"; import ChartHeader from "../headers/ChartHeader"; +import PBCompareCell from "../cells/PBCompareCell"; +import SelectableCompareType from "../components/SelectableCompareType"; export default function ComparePBsTable({ dataset, game, baseUser, compareUser, + shouldESD, }: { dataset: ComparePBsDataset; game: Game; playtype: Playtype; baseUser: string; compareUser: string; + shouldESD: boolean; }) { + const [compareType, setCompareType] = useState<"score" | "lamp">("score"); + const headers: Header[] = [ - // ["#", "#"], ChartHeader(game, (d) => d.chart), ["Song", "Song", StrSOV((x) => x.song.title)], ["", "", () => 1, () => {baseUser}], @@ -35,27 +40,48 @@ export default function ComparePBsTable({ } if (!x.compare) { - return Infinity; + return -Infinity; } - if (x.base.scoreData.esd && x.compare.scoreData.esd) { - return ESDCompare(x.base.scoreData.esd, x.compare.scoreData.esd); + 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); + + if (e !== 0) { + return e; + } + } + + return x.base.scoreData.percent - x.compare.scoreData.percent; } - return x.base.scoreData.percent - x.compare.scoreData.percent; + return x.base.scoreData.lampIndex - x.compare.scoreData.lampIndex; }), + (thProps: ZTableTHProps) => ( + + ), ], ["", "", () => 1, () => {compareUser}], ]; return ( } + entryName="Charts" + defaultReverseSort + defaultSortMode="Vs." + rowFunction={(data) => ( + + )} /> ); } @@ -63,9 +89,13 @@ export default function ComparePBsTable({ function Row({ data, game, + compareType, + shouldESD, }: { data: ComparePBsDataset[0]; game: Game; + compareType: "score" | "lamp"; + shouldESD: boolean; }) { return ( @@ -76,7 +106,14 @@ function Row({ ) : ( Not Played )} - some comparison thing here + {data.compare ? ( ) : ( diff --git a/client/src/components/util/UserIcon.tsx b/client/src/components/util/UserIcon.tsx index 7962515db..3d094b340 100644 --- a/client/src/components/util/UserIcon.tsx +++ b/client/src/components/util/UserIcon.tsx @@ -9,7 +9,7 @@ export default function UserIcon({ children, game, playtype, -}: { user: PublicUserDocument } & JustChildren & Partial) { +}: { user: PublicUserDocument } & Partial & Partial) { return (
@@ -25,7 +25,7 @@ export default function UserIcon({ {user.username} -
{children}
+ {children &&
{children}
}
); } diff --git a/client/src/components/util/modal/UserSelectModal.tsx b/client/src/components/util/modal/UserSelectModal.tsx index c1783fddb..a5e2cb2b4 100644 --- a/client/src/components/util/modal/UserSelectModal.tsx +++ b/client/src/components/util/modal/UserSelectModal.tsx @@ -1,10 +1,9 @@ import { APIFetchV1 } from "util/api"; import ProfilePicture from "components/user/ProfilePicture"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Col, Modal, Row } from "react-bootstrap"; import { integer, PublicUserDocument } from "tachi-common"; import { SetState } from "types/react"; -import { UserContext } from "context/UserContext"; import DebounceSearch from "../DebounceSearch"; import Divider from "../Divider"; @@ -14,12 +13,14 @@ export default function UserSelectModal({ setShow, url = `/users`, excludeSet = [], + excludeMsg = "N/A", }: { show: boolean; setShow: SetState; callback: (user: PublicUserDocument) => void; url?: string; excludeSet?: Array; + excludeMsg?: string; }) { const [search, setSearch] = useState(""); const [users, setUsers] = useState | null>(null); @@ -67,7 +68,7 @@ export default function UserSelectModal({

{user.username}

{excludeSet.includes(user.id) ? ( ) : ( {selectedUser !== null && folder !== null && ( @@ -180,12 +182,20 @@ function FolderCompare({ + + + + setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)} - label="Include charts you haven't both played?" + label="Include charts without plays?" /> {gptConfig.supportsESD && ( ); } + +function CompareCard({ + dataset, + baseUsername, + otherUsername, +}: { + dataset: ComparePBsDataset; + + baseUsername: string; + otherUsername: string; +}) { + // this code sucks + let scoreWin = 0; + let scoreDraw = 0; + let scoreLoss = 0; + let lampWin = 0; + let lampDraw = 0; + let lampLoss = 0; + + for (const d of dataset) { + if (!d.compare && !d.base) { + scoreDraw++; + lampDraw++; + continue; + } + + if (!d.compare) { + scoreWin++; + lampWin++; + continue; + } + + if (!d.base) { + scoreLoss++; + lampLoss++; + continue; + } + + if (d.base.scoreData.percent > d.compare.scoreData.percent) { + scoreWin++; + } else if (d.base.scoreData.percent < d.compare.scoreData.percent) { + scoreLoss++; + } else { + scoreDraw++; + } + + if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) { + lampWin++; + } else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) { + lampLoss++; + } else { + lampDraw++; + } + } + + return ( + + + + + + + {baseUsername} Win + + {scoreWin} + + + {((100 * scoreWin) / dataset.length).toFixed(2)}% + + + + Draw + {scoreDraw} + + {((100 * scoreDraw) / dataset.length).toFixed(2)}% + + + + {otherUsername} Win + {scoreLoss} + + {((100 * scoreLoss) / dataset.length).toFixed(2)}% + + + + + + + + {baseUsername} Win + + {lampWin} + + + {((100 * lampWin) / dataset.length).toFixed(2)}% + + + + Draw + {lampDraw} + + {((100 * lampDraw) / dataset.length).toFixed(2)}% + + + + {otherUsername} Loss + {lampLoss} + + {((100 * lampLoss) / dataset.length).toFixed(2)}% + + + + + + + + ); +} diff --git a/client/src/components/tables/cells/PBCompareCell.tsx b/client/src/components/tables/cells/PBCompareCell.tsx index f39e47a66..9a65d8233 100644 --- a/client/src/components/tables/cells/PBCompareCell.tsx +++ b/client/src/components/tables/cells/PBCompareCell.tsx @@ -35,12 +35,12 @@ export default function PBCompareCell({ } let scoreRenderFn = (k: number) => k.toString(); + let shouldShowPercent = false; switch (game) { case "chunithm": case "ddr": case "gitadora": - case "jubeat": case "maimai": case "usc": case "sdvx": @@ -48,10 +48,16 @@ export default function PBCompareCell({ case "wacca": case "museca": scoreRenderFn = FormatMillions; + shouldShowPercent = false; + break; + case "jubeat": + scoreRenderFn = FormatMillions; + shouldShowPercent = true; break; case "bms": case "iidx": scoreRenderFn = (k: number) => k.toString(); + shouldShowPercent = true; break; } @@ -87,8 +93,12 @@ export default function PBCompareCell({ {compareType === "score" ? ( <> {(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)} -
- ({fmtCmpPercent}) + {shouldShowPercent && ( + <> +
+ ({fmtCmpPercent}) + + )} {esdDiff !== undefined && ( <>
diff --git a/client/src/components/tables/rivals/ComparePBsTable.tsx b/client/src/components/tables/rivals/ComparePBsTable.tsx index 427d66eaf..80d3c0857 100644 --- a/client/src/components/tables/rivals/ComparePBsTable.tsx +++ b/client/src/components/tables/rivals/ComparePBsTable.tsx @@ -1,19 +1,21 @@ 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 { ComparePBsDataset } from "types/tables"; import DifficultyCell from "../cells/DifficultyCell"; +import PBCompareCell from "../cells/PBCompareCell"; import TitleCell from "../cells/TitleCell"; +import SelectableCompareType from "../components/SelectableCompareType"; import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable"; import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells"; import ChartHeader from "../headers/ChartHeader"; -import PBCompareCell from "../cells/PBCompareCell"; -import SelectableCompareType from "../components/SelectableCompareType"; export default function ComparePBsTable({ dataset, game, + playtype, baseUser, compareUser, shouldESD, @@ -79,6 +81,7 @@ export default function ComparePBsTable({ entryName="Charts" defaultReverseSort defaultSortMode="Vs." + searchFunctions={CreatePBCompareSearchParams(game, playtype)} rowFunction={(data) => ( )} diff --git a/client/src/util/tables/create-search.ts b/client/src/util/tables/create-search.ts index cb742c7d7..3f611b100 100644 --- a/client/src/util/tables/create-search.ts +++ b/client/src/util/tables/create-search.ts @@ -1,14 +1,14 @@ import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num"; import { ValueGetterOrHybrid } from "util/ztable/search"; import { + BMS_TABLES, ChartDocument, Game, GamePTConfig, GetGamePTConfig, IDStrings, - BMS_TABLES, } from "tachi-common"; -import { FolderDataset, PBDataset, ScoreDataset } from "types/tables"; +import { ComparePBsDataset, FolderDataset, PBDataset, ScoreDataset } from "types/tables"; import { Playtype } from "types/tachi"; function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string) { @@ -88,6 +88,24 @@ export function CreateDefaultPBSearchParams( return searchFunctions; } +export function CreatePBCompareSearchParams( + game: Game, + playtype: Playtype +) { + const searchFunctions: Record[0]>> = { + artist: (x) => x.song.artist, + title: (x) => x.song.title, + difficulty: (x) => x.chart.difficulty, + level: (x) => x.chart.levelNum, + }; + + if (game === "bms") { + HandleBMSNonsense(searchFunctions, playtype, (k) => k); + } + + return searchFunctions; +} + export function CreateDefaultFolderSearchParams( game: Game, playtype: Playtype From 0841f8bb903d6218babddba22b45b686c69d7b75 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Tue, 1 Nov 2022 23:30:36 +0000 Subject: [PATCH 8/8] fix: dont show rivals and goals for other users --- client/src/components/user/UGPTHeader.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/components/user/UGPTHeader.tsx b/client/src/components/user/UGPTHeader.tsx index 32679da82..757030a90 100644 --- a/client/src/components/user/UGPTHeader.tsx +++ b/client/src/components/user/UGPTHeader.tsx @@ -100,16 +100,16 @@ export function UGPTBottomNav({ Leaderboard , - - Rivals - , - - Goals & Quests - , ]; if (isRequestedUser) { navItems.push( + + Rivals + , + + Goals & Quests + , Settings