+ |
{renderDropdown ? (
{dropdown}
diff --git a/client/src/components/tables/dropdowns/PBDropdown.tsx b/client/src/components/tables/dropdowns/PBDropdown.tsx
index e699edbb9..c42886abe 100644
--- a/client/src/components/tables/dropdowns/PBDropdown.tsx
+++ b/client/src/components/tables/dropdowns/PBDropdown.tsx
@@ -15,6 +15,7 @@ import DocComponentCreator, {
} from "./components/DocumentComponent";
import DropdownStructure from "./components/DropdownStructure";
import PlayHistory from "./components/PlayHistory";
+import RivalCompare from "./components/RivalCompare";
import { GPTDropdownSettings } from "./GPTDropdownSettings";
export interface ScoreDropdownProps {
@@ -35,13 +36,11 @@ export default function PBDropdown({
userID: integer;
chart: ChartDocument;
scoreState: ScoreState;
- defaultView?: "pb" | "scorePB" | "lampPB" | "history" | "debug";
+ defaultView?: "pb" | "scorePB" | "lampPB" | "history" | "debug" | "rivals";
} & GamePT) {
const DocComponent: DocumentComponentType = (props) =>
DocComponentCreator({ ...props, ...GPTDropdownSettings(game, playtype) });
- // const { settings } = useContext(UserSettingsContext);
-
const [view, setView] = useState(defaultView);
const { data, error } = useApiQuery>(
@@ -109,6 +108,8 @@ export default function PBDropdown({
);
} else if (view === "debug") {
body = ;
+ } else if (view === "rivals") {
+ body = ;
} else {
body = (
({
Play History{histData && ` (${histData.length})`}
+
+
+ Rivals
+
diff --git a/client/src/components/tables/dropdowns/ScoreDropdown.tsx b/client/src/components/tables/dropdowns/ScoreDropdown.tsx
index e0a72756e..cf2b89695 100644
--- a/client/src/components/tables/dropdowns/ScoreDropdown.tsx
+++ b/client/src/components/tables/dropdowns/ScoreDropdown.tsx
@@ -21,6 +21,7 @@ import DropdownStructure from "./components/DropdownStructure";
import ManageScore from "./components/ManageScore";
import PBCompare from "./components/PBCompare";
import PlayHistory from "./components/PlayHistory";
+import RivalCompare from "./components/RivalCompare";
import { GPTDropdownSettings } from "./GPTDropdownSettings";
export interface ScoreState {
@@ -45,7 +46,7 @@ export default function ScoreDropdown({
user: PublicUserDocument;
chart: ChartDocument;
scoreState: ScoreState;
- defaultView?: "vsPB" | "moreInfo" | "history" | "debug" | "manage";
+ defaultView?: "vsPB" | "moreInfo" | "history" | "debug" | "manage" | "rivals";
thisScore: ScoreDocument;
} & GamePT) {
const DocComponent: DocumentComponentType = (props) =>
@@ -106,6 +107,8 @@ export default function ScoreDropdown({
body = ;
} else if (view === "manage") {
body = ;
+ } else if (view === "rivals") {
+ body = ;
}
return (
@@ -130,6 +133,12 @@ export default function ScoreDropdown({
Manage Score
)}
+ {currentUser && (
+
+
+ Rivals
+
+ )}
diff --git a/client/src/components/tables/dropdowns/components/DocumentComponent.tsx b/client/src/components/tables/dropdowns/components/DocumentComponent.tsx
index f3b04ce2b..57a0df5e2 100644
--- a/client/src/components/tables/dropdowns/components/DocumentComponent.tsx
+++ b/client/src/components/tables/dropdowns/components/DocumentComponent.tsx
@@ -152,3 +152,43 @@ export default function DocumentComponent({
>
);
}
+
+export function GraphAndJudgementDataComponent({
+ score,
+ GraphComponent = null,
+ chart,
+}: {
+ score: ScoreDocument | PBScoreDocument;
+ renderScoreInfo?: boolean;
+ showSingleScoreNote?: boolean;
+ forceScoreData?: boolean;
+ chart: ChartDocument;
+ GraphComponent?:
+ | (({
+ score,
+ chart,
+ }: {
+ score: ScoreDocument | PBScoreDocument;
+ chart: ChartDocument;
+ }) => JSX.Element)
+ | null;
+}) {
+ return (
+
+
+
+ {GraphComponent ? (
+
+ ) : (
+
+ No graphs available :(
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/tables/dropdowns/components/PlayHistory.tsx b/client/src/components/tables/dropdowns/components/PlayHistory.tsx
index 9810fe493..c2103530c 100644
--- a/client/src/components/tables/dropdowns/components/PlayHistory.tsx
+++ b/client/src/components/tables/dropdowns/components/PlayHistory.tsx
@@ -25,7 +25,7 @@ export default function PlayHistory({
}
return (
-
+
);
diff --git a/client/src/components/tables/dropdowns/components/RivalCompare.tsx b/client/src/components/tables/dropdowns/components/RivalCompare.tsx
new file mode 100644
index 000000000..34abdad67
--- /dev/null
+++ b/client/src/components/tables/dropdowns/components/RivalCompare.tsx
@@ -0,0 +1,64 @@
+import { NumericSOV } from "util/sorts";
+import RivalChartTable from "components/tables/rivals/RivalChartTable";
+import ApiError from "components/util/ApiError";
+import Loading from "components/util/Loading";
+import useApiQuery from "components/util/query/useApiQuery";
+import useUGPTBase from "components/util/useUGPTBase";
+import { UserContext } from "context/UserContext";
+import React, { useContext } from "react";
+import { Link } from "react-router-dom";
+import { ChartDocument, Game } from "tachi-common";
+import { ChartRivalsReturn } from "types/api-returns";
+import { RivalChartDataset } from "types/tables";
+
+export default function RivalCompare({ chart, game }: { chart: ChartDocument; game: Game }) {
+ const { user: currentUser } = useContext(UserContext);
+
+ const playtype = chart.playtype;
+
+ if (!currentUser) {
+ return You're not signed in. How did you even get to this page? ;
+ }
+
+ const base = useUGPTBase({ reqUser: currentUser, game, playtype });
+
+ const { data, error } = useApiQuery (
+ `/users/${currentUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}/rivals`
+ );
+
+ if (!data) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ if (data.rivals.length === 0) {
+ return (
+
+ You have no rivals set!
+
+ Why not set some?
+
+ );
+ }
+
+ const rivalDataset: RivalChartDataset = [...data.rivals, currentUser]
+ .map((u) => ({
+ ...u,
+ __related: {
+ pb: data.pbs.find((p) => p.userID === u.id) ?? null,
+ },
+ }))
+ .sort(NumericSOV((x) => x.__related.pb?.scoreData.percent ?? -Infinity, true))
+ .map((e, index) => ({
+ ...e,
+ __related: {
+ ...e.__related,
+ index,
+ },
+ }));
+
+ return ;
+}
diff --git a/client/src/components/tables/headers/GameHeaders.tsx b/client/src/components/tables/headers/GameHeaders.tsx
index 67edcd08f..76a3ab57a 100644
--- a/client/src/components/tables/headers/GameHeaders.tsx
+++ b/client/src/components/tables/headers/GameHeaders.tsx
@@ -10,12 +10,14 @@ import {
ScoreDocument,
} from "tachi-common";
import { SetState } from "types/react";
-import { FolderDataset, PBDataset, ScoreDataset } from "types/tables";
+import { 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(
+export function GetGPTCoreHeaders<
+ Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset
+>(
game: Game,
playtype: Playtype,
rating: ScoreCalculatedDataLookup[IDStrings],
diff --git a/client/src/components/tables/history-scores/HistoryScoreTable.tsx b/client/src/components/tables/history-scores/HistoryScoreTable.tsx
index a3aafc158..1de8582f8 100644
--- a/client/src/components/tables/history-scores/HistoryScoreTable.tsx
+++ b/client/src/components/tables/history-scores/HistoryScoreTable.tsx
@@ -15,6 +15,11 @@ import TimestampCell from "../cells/TimestampCell";
import TachiTable from "../components/TachiTable";
import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
+import DropdownRow from "../components/DropdownRow";
+import { GraphAndJudgementDataComponent } from "../dropdowns/components/DocumentComponent";
+import { GPTDropdownSettings } from "../dropdowns/GPTDropdownSettings";
+import { EmptyHeader } from "../headers/IndicatorHeader";
+import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
export default function HistoryScoreTable({
dataset,
@@ -44,6 +49,7 @@ export default function HistoryScoreTable({
headers={[
...headers,
["Timestamp", "Timestamp", NumericSOV((x) => x.timeAchieved ?? 0)],
+ EmptyHeader,
]}
defaultSortMode="Timestamp"
defaultReverseSort
@@ -66,9 +72,19 @@ function Row({
chart: ChartDocument;
}) {
return (
-
+
+ }
+ >
-
+
+
);
}
diff --git a/client/src/components/tables/rivals/RivalChartTable.tsx b/client/src/components/tables/rivals/RivalChartTable.tsx
new file mode 100644
index 000000000..102f98686
--- /dev/null
+++ b/client/src/components/tables/rivals/RivalChartTable.tsx
@@ -0,0 +1,105 @@
+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 { RivalChartDataset } from "types/tables";
+import IndexCell from "../cells/IndexCell";
+import RankingCell from "../cells/RankingCell";
+import TimestampCell from "../cells/TimestampCell";
+import UserCell from "../cells/UserCell";
+import DropdownRow from "../components/DropdownRow";
+import TachiTable, { Header } from "../components/TachiTable";
+import { GraphAndJudgementDataComponent } from "../dropdowns/components/DocumentComponent";
+import { GPTDropdownSettings } from "../dropdowns/GPTDropdownSettings";
+import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
+import { GetGPTCoreHeaders } from "../headers/GameHeaders";
+
+export default function RivalChartTable({
+ dataset,
+ game,
+ chart,
+}: {
+ dataset: RivalChartDataset;
+ game: Game;
+ chart: ChartDocument;
+}) {
+ const playtype = chart.playtype;
+
+ const defaultRating = useScoreRatingAlg(game, playtype);
+
+ const [rating, setRating] = useState(defaultRating);
+
+ const headers: Header[] = [
+ ["#", "#", NumericSOV((x) => x.__related.index)],
+ ["User", "User", StrSOV((x) => x.username)],
+ ...GetGPTCoreHeaders(
+ game,
+ playtype,
+ rating,
+ setRating,
+ (x) => x.__related.pb
+ ),
+ [
+ "Site Ranking",
+ "Site Rank",
+ NumericSOV((x) => x.__related.pb?.rankingData.rank ?? -Infinity),
+ ],
+ ["Last Raised", "Last Raised", NumericSOV((x) => x.__related.pb?.timeAchieved ?? 0)],
+ ];
+
+ return (
+ (
+
+ )}
+ />
+ );
+}
+
+function Row({
+ data,
+ rating,
+ game,
+ chart,
+}: {
+ data: RivalChartDataset[0];
+ game: Game;
+ rating: ScoreCalculatedDataLookup[I];
+ chart: ChartDocument;
+}) {
+ const pb = data.__related.pb;
+
+ if (!pb) {
+ return (
+
+ | N/A |
+
+ Not Played. |
+
+ );
+ }
+
+ return (
+
+ }
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts
index a34b8e8d6..67baaa312 100644
--- a/client/src/types/api-returns.ts
+++ b/client/src/types/api-returns.ts
@@ -186,3 +186,8 @@ export interface ChallengeSubsReturn {
songs: Array;
charts: Array;
}
+
+export interface ChartRivalsReturn {
+ rivals: Array;
+ pbs: Array;
+}
diff --git a/client/src/types/tables.ts b/client/src/types/tables.ts
index 211ba4d7c..5eb251517 100644
--- a/client/src/types/tables.ts
+++ b/client/src/types/tables.ts
@@ -49,3 +49,10 @@ export type UGSDataset = (UserGameStats & {
index: integer;
};
})[];
+
+export type RivalChartDataset = (PublicUserDocument & {
+ __related: {
+ pb: PBScoreDocument | null;
+ index: number;
+ };
+})[];
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 71166b327..4e89c7ccf 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
@@ -1,5 +1,6 @@
import { Router } from "express";
import db from "external/mongo/db";
+import { GetRivalUsers } from "lib/rivals/rivals";
import { SearchSpecificGameSongsAndCharts } from "lib/search/search";
import { AggressiveRateLimitMiddleware } from "server/middleware/rate-limiter";
import { GetGamePTConfig } from "tachi-common";
@@ -7,7 +8,7 @@ import { GetRelevantSongsAndCharts } from "utils/db";
import { IsValidScoreAlg } from "utils/misc";
import { GetAdjacentAbove, GetAdjacentBelow } from "utils/queries/pbs";
import { GetUGPT } from "utils/req-tachi-data";
-import { FilterChartsAndSongs, GetScoreIDsFromComposed } from "utils/scores";
+import { FilterChartsAndSongs, GetPBOnChart, GetScoreIDsFromComposed } from "utils/scores";
import { GetUsersWithIDs } from "utils/user";
const router: Router = Router({ mergeParams: true });
@@ -203,6 +204,33 @@ router.get("/:chartID", async (req, res) => {
});
});
+/**
+ * Returns a user's PB on the given chart, and all of their rivals performances aswell.
+ *
+ * @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/:chartID/rivals
+ */
+router.get("/:chartID/rivals", async (req, res) => {
+ const { user, game, playtype } = GetUGPT(req);
+
+ const rivals = await GetRivalUsers(user.id, game, playtype);
+
+ const pbs = await db["personal-bests"].find({
+ userID: { $in: rivals.map((e) => e.id) },
+ chartID: req.params.chartID,
+ });
+
+ const usersPB = await GetPBOnChart(user.id, req.params.chartID);
+
+ return res.status(200).json({
+ success: true,
+ description: `Retrieved PBs and Rival PBs.`,
+ body: {
+ pbs: [...pbs, usersPB],
+ rivals,
+ },
+ });
+});
+
/**
* Return this users PB on this chart, and 5 nearby players on the
* leaderboard.
From 5379dd491e55e80d09aaf6db21443728c6fbd34d Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Fri, 14 Oct 2022 22:54:34 +0100
Subject: [PATCH 05/10] feat: rivals on gptchartpage
---
.../games/_game/_playtype/GPTChartPage.tsx | 48 +++++++++++++++----
.../_game/_playtype/rivals/RivalsMainPage.tsx | 2 +-
2 files changed, 39 insertions(+), 11 deletions(-)
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 ec74f30c6..495220a8d 100644
--- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx
+++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx
@@ -29,7 +29,11 @@ import {
PublicUserDocument,
SongDocument,
} from "tachi-common";
-import { ChartPBLeaderboardReturn, UGPTChartLeaderboardAdjacent } from "types/api-returns";
+import {
+ ChartPBLeaderboardReturn,
+ ChartRivalsReturn,
+ UGPTChartLeaderboardAdjacent,
+} from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { PBDataset } from "types/tables";
@@ -73,6 +77,7 @@ export default function GPTChartPage({
interface ChartPBData {
leaderboard: ChartPBLeaderboardReturn;
adjacent?: UGPTChartLeaderboardAdjacent;
+ rivals?: ChartRivalsReturn;
playcount: integer;
}
@@ -87,7 +92,7 @@ function InternalGPTChartPage({
} & GamePT) {
const { user } = useContext(UserContext);
- const [mode, setMode] = useState<"leaderboard" | "adjacent">("leaderboard");
+ const [mode, setMode] = useState<"leaderboard" | "adjacent" | "rivals">("leaderboard");
const { data, error } = useQuery(
["PBInfo", `${chart.chartID}`],
@@ -113,15 +118,24 @@ function InternalGPTChartPage({
`/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}/leaderboard-adjacent`
);
- if (!nRes.success) {
- return { leaderboard: lRes.body, playcount: pRes.body.count };
- }
+ const rRes = await APIFetchV1(
+ `/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}/rivals`
+ );
- return {
+ const returnValue: ChartPBData = {
leaderboard: lRes.body,
- adjacent: nRes.body,
playcount: pRes.body.count,
};
+
+ if (nRes.success) {
+ returnValue.adjacent = nRes.body;
+ }
+
+ if (rRes.success) {
+ returnValue.rivals = rRes.body;
+ }
+
+ return returnValue;
}
return { leaderboard: lRes.body, playcount: pRes.body.count };
@@ -148,6 +162,12 @@ function InternalGPTChartPage({
}
}
+ if (data.rivals) {
+ for (const user of data.rivals.rivals) {
+ userMap.set(user.id, user);
+ }
+ }
+
if (user) {
// Add current user, since there's no guarantee they are returned from either API.
userMap.set(user.id, user);
@@ -173,6 +193,12 @@ function InternalGPTChartPage({
Your Position
+ {user && (
+
+
+ VS. Rivals
+
+ )}
@@ -198,22 +224,24 @@ function ChartLeaderboardTable({
data: ChartPBData;
user: PublicUserDocument | null;
userMap: Map;
- mode: "leaderboard" | "adjacent";
+ mode: "leaderboard" | "adjacent" | "rivals";
chart: ChartDocument;
song: SongDocument;
} & GamePT) {
const dataset: PBDataset = useMemo(() => {
const ds: PBDataset = [];
- let pbs = [];
+ let pbs: Array = [];
if (mode === "leaderboard") {
pbs = data.leaderboard.pbs;
- } else {
+ } else if (mode === "adjacent") {
pbs = [
...data.adjacent!.adjacentAbove,
data.adjacent!.pb,
...data.adjacent!.adjacentBelow,
];
+ } else if (mode === "rivals") {
+ pbs = data.rivals!.pbs;
}
for (const pb of pbs) {
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 c14be4a02..1cbcef9a8 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
@@ -50,7 +50,7 @@ export default function RivalsMainPage({
- Change Rivals
+ Manage Rivals
From 2c3af78b7cfa43d3bbeb9e82ffae47865b8d9520 Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Fri, 14 Oct 2022 23:53:35 +0100
Subject: [PATCH 06/10] feat: rival ranking on server
---
common/src/types.ts | 6 ++++
server/src/lib/rivals/rivals.ts | 30 +++++++++++++++++++
.../score-import/framework/common/types.ts | 1 +
.../framework/pb/create-pb-doc.ts | 21 +++++++++++--
.../framework/pb/process-pbs.test.ts | 10 +++++--
.../score-import/framework/pb/process-pbs.ts | 6 ++--
.../score-importing/score-import-main.ts | 23 ++++++++++----
.../src/lib/score-mutation/delete-scores.ts | 20 ++++++++++---
server/src/lib/score-mutation/update-score.ts | 5 ++--
.../router/ir/usc/_playtype/router.test.ts | 1 +
.../router/ir/usc/_playtype/usc.test.ts | 1 +
server/src/test-utils/test-data.ts | 1 +
.../src/utils/calculations/recalc-scores.ts | 27 +++++++++++++----
13 files changed, 129 insertions(+), 23 deletions(-)
diff --git a/common/src/types.ts b/common/src/types.ts
index f2638b558..549c2f740 100644
--- a/common/src/types.ts
+++ b/common/src/types.ts
@@ -1256,6 +1256,12 @@ export interface PBScoreDocument extends MongoD
rankingData: {
rank: integer;
outOf: integer;
+
+ // out of their rivals, what is their position on this chart?
+ // note that we don't need to store rivalOutOf, as it's pretty much a constant
+ // that can just be read from the UGPT settings.
+ // null if the user has no rivals.
+ rivalRank: integer | null;
};
userID: integer;
chartID: string;
diff --git a/server/src/lib/rivals/rivals.ts b/server/src/lib/rivals/rivals.ts
index cece54034..0fe1a3df2 100644
--- a/server/src/lib/rivals/rivals.ts
+++ b/server/src/lib/rivals/rivals.ts
@@ -48,6 +48,36 @@ export async function GetRivalUsers(userID: integer, game: Game, playtype: Playt
return rivals;
}
+/**
+ * Retrieve *all* rival IDs for people on this game. Used to recalculate rival movements on charts,
+ * since that is stored and cached.
+ */
+export async function GetEveryonesRivalIDs(
+ game: Game,
+ playtype: Playtype
+): Promise>> {
+ const allGameSettings = await db["game-settings"].find(
+ {
+ game,
+ playtype,
+ },
+ {
+ projection: {
+ userID: 1,
+ rivals: 1,
+ },
+ }
+ );
+
+ const lookupTable: Record> = {};
+
+ for (const d of allGameSettings) {
+ lookupTable[d.userID] = d.rivals;
+ }
+
+ return lookupTable;
+}
+
/**
* Sets an array of userIDs to be this user's rivals. Performs validation on all of the
* rivals being players of the game, and not being duplicates. The maximum amount of rivals
diff --git a/server/src/lib/score-import/framework/common/types.ts b/server/src/lib/score-import/framework/common/types.ts
index cd722594d..d21248bd8 100644
--- a/server/src/lib/score-import/framework/common/types.ts
+++ b/server/src/lib/score-import/framework/common/types.ts
@@ -12,3 +12,4 @@ export type DryScore = Pick<
};
export type ScorePlaytypeMap = Partial>>;
+export type ChartIDPlaytypeMap = Partial>>;
diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts
index 8ca2643cd..eb25ad973 100644
--- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts
+++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts
@@ -6,9 +6,10 @@ import {
USCMergeFn,
} from "./game-specific-merge";
import db from "external/mongo/db";
+import { GetEveryonesRivalIDs, GetRivalIDs, GetRivalUsers } from "lib/rivals/rivals";
import type { KtLogger } from "lib/logger/logger";
import type { BulkWriteUpdateOneOperation } from "mongodb";
-import type { Game, integer, PBScoreDocument, ScoreDocument } from "tachi-common";
+import type { Game, integer, PBScoreDocument, Playtype, ScoreDocument } from "tachi-common";
export type PBScoreDocumentNoRank = Omit;
@@ -64,9 +65,9 @@ export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLo
}
/**
- * Updates user's rankings on a given chart.
+ * Updates rankings on a given chart.
*/
-export async function UpdateChartRanking(chartID: string) {
+export async function UpdateChartRanking(game: Game, playtype: Playtype, chartID: string) {
const scores = await db["personal-bests"].find(
{ chartID },
{
@@ -77,13 +78,26 @@ export async function UpdateChartRanking(chartID: string) {
}
);
+ const allRivals = await GetEveryonesRivalIDs(game, playtype);
+
const bwrite: Array> = [];
let rank = 0;
+ // what users have we saw so far? used for rivalRanking calculations
+ const seenUserIDs: Array = [];
+
for (const score of scores) {
rank++;
+ const thisUsersRivals = allRivals[score.userID];
+
+ let rivalRank: integer | null = null;
+
+ if (thisUsersRivals) {
+ rivalRank = thisUsersRivals.filter((e) => seenUserIDs.includes(e)).length + 1;
+ }
+
bwrite.push({
updateOne: {
filter: { chartID: score.chartID, userID: score.userID },
@@ -92,6 +106,7 @@ export async function UpdateChartRanking(chartID: string) {
rankingData: {
rank,
outOf: scores.length,
+ rivalRank,
},
},
},
diff --git a/server/src/lib/score-import/framework/pb/process-pbs.test.ts b/server/src/lib/score-import/framework/pb/process-pbs.test.ts
index 2167591f7..78362e253 100644
--- a/server/src/lib/score-import/framework/pb/process-pbs.test.ts
+++ b/server/src/lib/score-import/framework/pb/process-pbs.test.ts
@@ -16,7 +16,7 @@ t.test("#ProcessPBs", (t) => {
await db["personal-bests"].remove({});
// scores on 511 SPA are pre-loaded into the database
- await ProcessPBs(1, new Set([Testing511SPA.chartID]), logger);
+ await ProcessPBs("iidx", "SP", 1, new Set([Testing511SPA.chartID]), logger);
const pbs = await db["personal-bests"].find({});
@@ -48,7 +48,13 @@ t.test("#ProcessPBs", (t) => {
}),
]);
- await ProcessPBs(1, new Set([Testing511SPA.chartID, "test1", "test2", "test3"]), logger);
+ await ProcessPBs(
+ "iidx",
+ "SP",
+ 1,
+ new Set([Testing511SPA.chartID, "test1", "test2", "test3"]),
+ logger
+ );
const pbs = await db["personal-bests"].find({});
diff --git a/server/src/lib/score-import/framework/pb/process-pbs.ts b/server/src/lib/score-import/framework/pb/process-pbs.ts
index 2a51addb7..22634f341 100644
--- a/server/src/lib/score-import/framework/pb/process-pbs.ts
+++ b/server/src/lib/score-import/framework/pb/process-pbs.ts
@@ -2,12 +2,14 @@ import { CreatePBDoc, UpdateChartRanking } from "./create-pb-doc";
import db from "external/mongo/db";
import type { PBScoreDocumentNoRank } from "./create-pb-doc";
import type { KtLogger } from "lib/logger/logger";
-import type { integer } from "tachi-common";
+import type { Game, integer, Playtype } from "tachi-common";
/**
* Process, recalculate and update a users PBs for this set of chartIDs.
*/
export async function ProcessPBs(
+ game: Game,
+ playtype: Playtype,
userID: integer,
chartIDs: Set,
logger: KtLogger
@@ -58,7 +60,7 @@ export async function ProcessPBs(
// now that everything has been updated or inserted, we can refresh
// the chart rankings.
- await Promise.all(pbDocs.map((e) => UpdateChartRanking(e.chartID)));
+ await Promise.all(pbDocs.map((e) => UpdateChartRanking(game, playtype, e.chartID)));
// and we're done!
}
diff --git a/server/src/lib/score-import/framework/score-importing/score-import-main.ts b/server/src/lib/score-import/framework/score-importing/score-import-main.ts
index 819e3a835..4ce9d0711 100644
--- a/server/src/lib/score-import/framework/score-importing/score-import-main.ts
+++ b/server/src/lib/score-import/framework/score-importing/score-import-main.ts
@@ -14,7 +14,7 @@ import { GetGameConfig } from "tachi-common";
import { GetMillisecondsSince } from "utils/misc";
import { GetUserWithID } from "utils/user";
import type { ConverterFunction, ImportInputParser } from "../../import-types/common/types";
-import type { ScorePlaytypeMap } from "../common/types";
+import type { ChartIDPlaytypeMap, ScorePlaytypeMap } from "../common/types";
import type { ClassHandler } from "../user-game-stats/types";
import type { KtLogger } from "lib/logger/logger";
import type { ScoreImportJob } from "lib/score-import/worker/types";
@@ -292,21 +292,33 @@ export async function HandlePostImportSteps(
void SetJobProgress(job, "Processing scores and updating PBs.");
+ const playtypes = Object.keys(scorePlaytypeMap) as Array;
+
// --- 5. PersonalBests ---
// We want to keep an updated reference of a users best score on a given chart.
// This function also handles conjoining different scores together (such as unioning best lamp and
// best score).
const pbTimeStart = process.hrtime.bigint();
- await ProcessPBs(user.id, chartIDs, logger);
+ // processing PBs is a playtype-specific action. As such, we need to split chartIDs
+ // accordingly
+ const chartIDsSeparatedByPlaytype: ChartIDPlaytypeMap = {};
+
+ for (const [playtype, scores] of Object.entries(scorePlaytypeMap)) {
+ chartIDsSeparatedByPlaytype[playtype as Playtype] = new Set(scores.map((e) => e.chartID));
+ }
+
+ await Promise.all(
+ Object.entries(chartIDsSeparatedByPlaytype).map(([playtype, chartIDs]) =>
+ ProcessPBs(game, playtype as Playtype, user.id, chartIDs, logger)
+ )
+ );
const pbTime = GetMillisecondsSince(pbTimeStart);
const pbTimeRel = pbTime / chartIDs.size;
logger.debug(`PB Processing took ${pbTime} milliseconds (${pbTimeRel}ms/doc)`);
- const playtypes = Object.keys(scorePlaytypeMap) as Array;
-
void SetJobProgress(job, "Updating profile statistics.");
// --- 6. Game Stats ---
@@ -413,7 +425,8 @@ function ParseImportInfo(importInfo: Array) {
chartIDs.add(info.content.score.chartID);
if (scorePlaytypeMap[info.content.score.playtype]) {
- scorePlaytypeMap[info.content.score.playtype]!.push(info.content.score);
+ // @ts-expect-error obviously delusional typescript moment
+ scorePlaytypeMap[info.content.score.playtype].push(info.content.score);
} else {
scorePlaytypeMap[info.content.score.playtype] = [info.content.score];
}
diff --git a/server/src/lib/score-mutation/delete-scores.ts b/server/src/lib/score-mutation/delete-scores.ts
index cf19eed33..038c36f0a 100644
--- a/server/src/lib/score-mutation/delete-scores.ts
+++ b/server/src/lib/score-mutation/delete-scores.ts
@@ -84,14 +84,20 @@ export async function DeleteScore(
});
if (userHasOtherScores && attemptPBReprocess) {
- await ProcessPBs(score.userID, new Set([score.chartID]), logger);
+ await ProcessPBs(
+ score.game,
+ score.playtype,
+ score.userID,
+ new Set([score.chartID]),
+ logger
+ );
} else {
await db["personal-bests"].remove({
userID: score.userID,
chartID: score.chartID,
});
- await UpdateChartRanking(score.chartID);
+ await UpdateChartRanking(score.game, score.playtype, score.chartID);
}
await UpdateUsersGamePlaytypeStats(score.game, score.playtype, score.userID, null, logger);
@@ -186,14 +192,20 @@ export async function DeleteMultipleScores(scores: Array, blackli
});
if (userHasOtherScores) {
- await ProcessPBs(score.userID, new Set([score.chartID]), logger);
+ await ProcessPBs(
+ score.game,
+ score.playtype,
+ score.userID,
+ new Set([score.chartID]),
+ logger
+ );
} else {
await db["personal-bests"].remove({
userID: score.userID,
chartID: score.chartID,
});
- await UpdateChartRanking(score.chartID);
+ await UpdateChartRanking(score.game, score.playtype, score.chartID);
}
if (blacklist) {
diff --git a/server/src/lib/score-mutation/update-score.ts b/server/src/lib/score-mutation/update-score.ts
index 1ac2c65e9..9ba64fb75 100644
--- a/server/src/lib/score-mutation/update-score.ts
+++ b/server/src/lib/score-mutation/update-score.ts
@@ -64,6 +64,7 @@ export default async function UpdateScore(
logger.verbose("Received Update Score request.");
+ // eslint-disable-next-line require-atomic-updates
newScore.calculatedData = await CreateCalculatedData(
newScore,
chart,
@@ -148,13 +149,13 @@ export default async function UpdateScore(
chartID: newScore.chartID,
});
- await UpdateChartRanking(newScore.chartID);
+ await UpdateChartRanking(newScore.game, newScore.playtype, newScore.chartID);
if (updateOldChart) {
await UpdateAllPBs([userID], {
chartID: oldScore.chartID,
});
- await UpdateChartRanking(oldScore.chartID);
+ await UpdateChartRanking(oldScore.game, oldScore.playtype, oldScore.chartID);
}
const imports = await db.imports.find({
diff --git a/server/src/server/router/ir/usc/_playtype/router.test.ts b/server/src/server/router/ir/usc/_playtype/router.test.ts
index 955e22ba4..ae6e529a8 100644
--- a/server/src/server/router/ir/usc/_playtype/router.test.ts
+++ b/server/src/server/router/ir/usc/_playtype/router.test.ts
@@ -132,6 +132,7 @@ const USC_SCORE_PB: PBScoreDocument = {
rankingData: {
rank: 1,
outOf: 2,
+ rivalRank: null,
},
songID: 1,
userID: 1,
diff --git a/server/src/server/router/ir/usc/_playtype/usc.test.ts b/server/src/server/router/ir/usc/_playtype/usc.test.ts
index 6a716cdd6..970b9764b 100644
--- a/server/src/server/router/ir/usc/_playtype/usc.test.ts
+++ b/server/src/server/router/ir/usc/_playtype/usc.test.ts
@@ -26,6 +26,7 @@ const mockScorePB: PBScoreDocument<"usc:Controller" | "usc:Keyboard"> = {
rankingData: {
outOf: 2,
rank: 1,
+ rivalRank: null,
},
scoreData: {
esd: null,
diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts
index 96e8b50b9..f23171277 100644
--- a/server/src/test-utils/test-data.ts
+++ b/server/src/test-utils/test-data.ts
@@ -95,6 +95,7 @@ export const TestingIIDXSPScorePB: PBScoreDocument<"iidx:SP"> = {
rankingData: {
rank: 1,
outOf: 2,
+ rivalRank: null,
},
songID: 1,
game: "iidx",
diff --git a/server/src/utils/calculations/recalc-scores.ts b/server/src/utils/calculations/recalc-scores.ts
index 3a5dac342..c284d0caf 100644
--- a/server/src/utils/calculations/recalc-scores.ts
+++ b/server/src/utils/calculations/recalc-scores.ts
@@ -7,6 +7,8 @@ import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals";
import { UpdateUsersMilestones } from "lib/score-import/framework/milestones/milestones";
import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs";
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs";
+import { TachiConfig } from "lib/setup/config";
+import { GetGameConfig } from "tachi-common";
import { EfficientDBIterate } from "utils/efficient-db-iterate";
import { FormatUserDoc } from "utils/user";
import type { Game, integer, Playtype, PublicUserDocument } from "tachi-common";
@@ -95,12 +97,27 @@ export async function UpdateAllPBs(userIDs?: Array, filter = {}) {
for (const user of allUsers) {
logger.verbose(`Finding ${FormatUserDoc(user)}'s scores.`);
- const scores = await db.scores.find(deepmerge({ userID: user.id }, filter), {
- projection: { chartID: 1 },
- });
+ for (const game of TachiConfig.GAMES) {
+ const gameConfig = GetGameConfig(game);
- logger.verbose(`PBing ${FormatUserDoc(user)}'s scores.`);
- await ProcessPBs(user.id, new Set(scores.map((e) => e.chartID)), logger);
+ for (const playtype of gameConfig.validPlaytypes) {
+ const scores = await db.scores.find(
+ deepmerge({ userID: user.id, game, playtype }, filter),
+ {
+ projection: { chartID: 1 },
+ }
+ );
+
+ logger.verbose(`PBing ${FormatUserDoc(user)}'s scores.`);
+ await ProcessPBs(
+ game,
+ playtype,
+ user.id,
+ new Set(scores.map((e) => e.chartID)),
+ logger
+ );
+ }
+ }
}
logger.verbose(`Done!`);
From 0b54d6d862fd06b68391d5125a460a8dca3d4818 Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Sat, 15 Oct 2022 02:49:41 +0100
Subject: [PATCH 07/10] feat: rival ranking in UI
---
.../games/_game/_playtype/GPTChartPage.tsx | 1 +
client/src/app/routes/GameRoutes.tsx | 5 +-
.../components/tables/cells/RankingCell.tsx | 70 +++++++++++++++++--
.../tables/components/SelectableRanking.tsx | 64 +++++++++++++++++
.../components/tables/folders/FolderTable.tsx | 33 ++++++---
.../tables/headers/RankingHeader.tsx | 37 ++++++++++
client/src/components/tables/pbs/PBTable.tsx | 19 ++++-
.../tables/rivals/RivalChartTable.tsx | 31 +++++---
client/src/context/UGPTContext.tsx | 11 ---
client/src/util/tables/create-search.ts | 8 ++-
client/src/util/ztable/search.ts | 12 +++-
.../framework/pb/create-pb-doc.ts | 3 +-
12 files changed, 252 insertions(+), 42 deletions(-)
create mode 100644 client/src/components/tables/components/SelectableRanking.tsx
create mode 100644 client/src/components/tables/headers/RankingHeader.tsx
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 495220a8d..3477cb8be 100644
--- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx
+++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx
@@ -261,6 +261,7 @@ function ChartLeaderboardTable({
return (
();
@@ -66,7 +67,9 @@ export default function GameRoutes() {
-
+
+
+
diff --git a/client/src/components/tables/cells/RankingCell.tsx b/client/src/components/tables/cells/RankingCell.tsx
index 48a033dc5..7d1d863b5 100644
--- a/client/src/components/tables/cells/RankingCell.tsx
+++ b/client/src/components/tables/cells/RankingCell.tsx
@@ -1,15 +1,73 @@
+import useLUGPTSettings from "components/util/useLUGPTSettings";
import React from "react";
+import { integer } from "rg-stats/js/util/types";
import { PBScoreDocument } from "tachi-common";
+export type RankingViewMode = "global" | "rival" | "both-if-self" | "global-no-switch";
+
export default function RankingCell({
rankingData,
+ userID,
+ rankingViewMode,
}: {
rankingData: PBScoreDocument["rankingData"];
+ userID: integer;
+ rankingViewMode: RankingViewMode;
}) {
- return (
-
- #{rankingData.rank}
- /{rankingData.outOf}
- |
- );
+ const { settings } = useLUGPTSettings();
+
+ switch (rankingViewMode) {
+ case "global":
+ case "global-no-switch":
+ return (
+
+ #{rankingData.rank}
+ /{rankingData.outOf}
+ |
+ );
+ case "rival":
+ if (!settings) {
+ return (
+
+ No Settings, yet tried to view rival stats? not possible. how'd you get
+ here. report this.
+ |
+ );
+ }
+
+ if (settings?.userID !== userID) {
+ return (
+
+ N/A
+ |
+ );
+ }
+
+ return (
+
+ #{rankingData.rivalRank}
+ /{settings.rivals.length + 1}
+ |
+ );
+
+ case "both-if-self":
+ if (settings?.userID === userID && rankingData.rivalRank !== null) {
+ return (
+
+ Global #{rankingData.rank}
+ /{rankingData.outOf}
+
+ Rival #{rankingData.rivalRank}
+ /{settings.rivals.length + 1}
+ |
+ );
+ }
+
+ return (
+
+ #{rankingData.rank}
+ /{rankingData.outOf}
+ |
+ );
+ }
}
diff --git a/client/src/components/tables/components/SelectableRanking.tsx b/client/src/components/tables/components/SelectableRanking.tsx
new file mode 100644
index 000000000..84ddef0da
--- /dev/null
+++ b/client/src/components/tables/components/SelectableRanking.tsx
@@ -0,0 +1,64 @@
+import useLUGPTSettings from "components/util/useLUGPTSettings";
+import React from "react";
+import { SetState } from "types/react";
+import { RankingViewMode } from "../cells/RankingCell";
+import SortableTH from "./SortableTH";
+import { ZTableTHProps } from "./TachiTable";
+
+export default function SelectableRanking({
+ rankingViewMode,
+ setRankingViewMode,
+ changeSort,
+ currentSortMode,
+ reverseSort,
+}: {
+ rankingViewMode: RankingViewMode;
+ setRankingViewMode: SetState;
+} & ZTableTHProps) {
+ const { settings } = useLUGPTSettings();
+
+ if (rankingViewMode === "both-if-self" || rankingViewMode === "global-no-switch" || !settings) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ changeSort("Site Ranking")}>
+
+
+
+ |
+ );
+}
diff --git a/client/src/components/tables/folders/FolderTable.tsx b/client/src/components/tables/folders/FolderTable.tsx
index 88ad80192..616d5c15e 100644
--- a/client/src/components/tables/folders/FolderTable.tsx
+++ b/client/src/components/tables/folders/FolderTable.tsx
@@ -6,9 +6,10 @@ import { Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
import { FolderDataset } from "types/tables";
import { Playtype } from "types/tachi";
import Muted from "components/util/Muted";
+import useLUGPTSettings from "components/util/useLUGPTSettings";
import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
-import RankingCell from "../cells/RankingCell";
+import RankingCell, { RankingViewMode } from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
import TitleCell from "../cells/TitleCell";
import DropdownRow from "../components/DropdownRow";
@@ -19,6 +20,7 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import ChartHeader from "../headers/ChartHeader";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
import { EmptyHeader, FolderIndicatorHeader } from "../headers/IndicatorHeader";
+import { CreateRankingHeader } from "../headers/RankingHeader";
export default function FolderTable({
dataset,
@@ -31,7 +33,10 @@ export default function FolderTable({
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
+ const { settings } = useLUGPTSettings();
+
const [rating, setRating] = useState(defaultRating);
+ const [rankingViewMode, setRankingViewMode] = useState("global");
const headers: Header[] = [
ChartHeader(game, (k) => k),
@@ -45,11 +50,11 @@ export default function FolderTable({
setRating,
(x) => x.__related.pb
),
- [
- "Site Ranking",
- "Site Rank",
- NumericSOV((x) => x.__related.pb?.rankingData.rank ?? -Infinity),
- ],
+ CreateRankingHeader(
+ rankingViewMode,
+ setRankingViewMode,
+ (k) => k.__related.pb?.rankingData
+ ),
["Last Raised", "Last Raised", NumericSOV((x) => x.__related.pb?.timeAchieved ?? 0)],
];
@@ -60,7 +65,13 @@ export default function FolderTable({
entryName="Charts"
searchFunctions={CreateDefaultFolderSearchParams(game, playtype)}
rowFunction={(data) => (
-
+
)}
/>
);
@@ -70,10 +81,12 @@ function Row({
data,
rating,
game,
+ rankingViewMode,
}: {
data: FolderDataset[0];
game: Game;
rating: ScoreCalculatedDataLookup[I];
+ rankingViewMode: RankingViewMode;
}) {
const score = data.__related.pb;
@@ -110,7 +123,11 @@ function Row({
PB
|
-
+
);
diff --git a/client/src/components/tables/headers/RankingHeader.tsx b/client/src/components/tables/headers/RankingHeader.tsx
new file mode 100644
index 000000000..9f3b28fa8
--- /dev/null
+++ b/client/src/components/tables/headers/RankingHeader.tsx
@@ -0,0 +1,37 @@
+import { NumericSOV } from "util/sorts";
+import React from "react";
+import { PBScoreDocument } from "tachi-common";
+import { SetState } from "types/react";
+import { RankingViewMode } from "../cells/RankingCell";
+import SelectableRanking from "../components/SelectableRanking";
+import { Header, ZTableTHProps } from "../components/TachiTable";
+
+export function CreateRankingHeader(
+ rankingViewMode: RankingViewMode,
+ setRankingViewMode: SetState,
+ kMapToRankingData: (k: T) => PBScoreDocument["rankingData"] | undefined
+): Header {
+ return [
+ "Site Ranking",
+ "Site Ranking",
+
+ NumericSOV((x) => {
+ const rankingData = kMapToRankingData(x);
+
+ if (!rankingData) {
+ return -Infinity;
+ }
+
+ return rankingViewMode === "rival"
+ ? rankingData.rivalRank ?? -Infinity
+ : rankingData.rank;
+ }),
+ (thProps: ZTableTHProps) => (
+
+ ),
+ ];
+}
diff --git a/client/src/components/tables/pbs/PBTable.tsx b/client/src/components/tables/pbs/PBTable.tsx
index 33091825d..6c859b3d7 100644
--- a/client/src/components/tables/pbs/PBTable.tsx
+++ b/client/src/components/tables/pbs/PBTable.tsx
@@ -8,7 +8,7 @@ import { PBDataset } from "types/tables";
import { Playtype } from "types/tachi";
import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
import IndexCell from "../cells/IndexCell";
-import RankingCell from "../cells/RankingCell";
+import RankingCell, { RankingViewMode } from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
import DropdownRow from "../components/DropdownRow";
import TachiTable, { Header } from "../components/TachiTable";
@@ -18,6 +18,7 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import ChartHeader from "../headers/ChartHeader";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
import { EmptyHeader } from "../headers/IndicatorHeader";
+import { CreateRankingHeader } from "../headers/RankingHeader";
import PBLeadingRows from "./PBLeadingRows";
export default function PBTable({
@@ -29,6 +30,7 @@ export default function PBTable({
showChart = true,
playtype,
alg,
+ defaultRankingViewMode,
}: {
dataset: PBDataset;
indexCol?: boolean;
@@ -38,10 +40,14 @@ export default function PBTable({
playtype: Playtype;
game: Game;
alg?: ScoreCalculatedDataLookup[I];
+ defaultRankingViewMode?: RankingViewMode;
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
const [rating, setRating] = useState(alg ?? defaultRating);
+ const [rankingViewMode, setRankingViewMode] = useState(
+ defaultRankingViewMode ?? "global"
+ );
const headers: Header[0]>[] = [
...GetPBLeadingHeaders(
@@ -51,7 +57,7 @@ export default function PBTable({
),
EmptyHeader,
...GetGPTCoreHeaders(game, playtype, rating, setRating, (x) => x),
- ["Site Ranking", "Site Rank", NumericSOV((x) => x.rankingData.rank)],
+ CreateRankingHeader(rankingViewMode, setRankingViewMode, (k) => k.rankingData),
["Last Raised", "Last Raised", NumericSOV((x) => x.timeAchieved ?? 0)],
EmptyHeader,
];
@@ -86,6 +92,7 @@ export default function PBTable({
rating={rating}
game={game}
playtype={playtype}
+ rankingViewMode={rankingViewMode}
/>
)}
/>
@@ -100,6 +107,7 @@ function Row({
showUser,
game,
rating,
+ rankingViewMode,
}: {
pb: PBDataset[0];
indexCol: boolean;
@@ -110,6 +118,7 @@ function Row({
playtype: Playtype;
// ts bug?
rating: any; // ScoreCalculatedDataLookup[I];
+ rankingViewMode: RankingViewMode;
}) {
const scoreState = usePBState(pb);
@@ -128,7 +137,11 @@ function Row({
{indexCol && }
-
+
{showPlaycount && {pb.__playcount ?? 0} | }
diff --git a/client/src/components/tables/rivals/RivalChartTable.tsx b/client/src/components/tables/rivals/RivalChartTable.tsx
index 102f98686..03a5495eb 100644
--- a/client/src/components/tables/rivals/RivalChartTable.tsx
+++ b/client/src/components/tables/rivals/RivalChartTable.tsx
@@ -4,7 +4,7 @@ import React, { useState } from "react";
import { ChartDocument, Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
import { RivalChartDataset } from "types/tables";
import IndexCell from "../cells/IndexCell";
-import RankingCell from "../cells/RankingCell";
+import RankingCell, { RankingViewMode } from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
@@ -13,6 +13,7 @@ import { GraphAndJudgementDataComponent } from "../dropdowns/components/Document
import { GPTDropdownSettings } from "../dropdowns/GPTDropdownSettings";
import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
+import { CreateRankingHeader } from "../headers/RankingHeader";
export default function RivalChartTable({
dataset,
@@ -28,6 +29,7 @@ export default function RivalChartTable({
const defaultRating = useScoreRatingAlg(game, playtype);
const [rating, setRating] = useState(defaultRating);
+ const [rankingViewMode, setRankingViewMode] = useState("global");
const headers: Header[] = [
["#", "#", NumericSOV((x) => x.__related.index)],
@@ -39,11 +41,11 @@ export default function RivalChartTable({
setRating,
(x) => x.__related.pb
),
- [
- "Site Ranking",
- "Site Rank",
- NumericSOV((x) => x.__related.pb?.rankingData.rank ?? -Infinity),
- ],
+ CreateRankingHeader(
+ rankingViewMode,
+ setRankingViewMode,
+ (k) => k.__related.pb?.rankingData
+ ),
["Last Raised", "Last Raised", NumericSOV((x) => x.__related.pb?.timeAchieved ?? 0)],
];
@@ -55,7 +57,14 @@ export default function RivalChartTable({
noTopDisplayStr
defaultSortMode="#"
rowFunction={(data) => (
-
+
)}
/>
);
@@ -66,11 +75,13 @@ function Row({
rating,
game,
chart,
+ rankingViewMode,
}: {
data: RivalChartDataset[0];
game: Game;
rating: ScoreCalculatedDataLookup[I];
chart: ChartDocument;
+ rankingViewMode: RankingViewMode;
}) {
const pb = data.__related.pb;
@@ -98,7 +109,11 @@ function Row({
-
+
);
diff --git a/client/src/context/UGPTContext.tsx b/client/src/context/UGPTContext.tsx
index 897c839c7..8251b3045 100644
--- a/client/src/context/UGPTContext.tsx
+++ b/client/src/context/UGPTContext.tsx
@@ -4,7 +4,6 @@ import fetchUGPTData, { UGPTData } from "components/util/query/fetchUGPTData";
import React, { createContext, useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { JustChildren, SetState } from "types/react";
-import { ErrorPage } from "app/pages/ErrorPage";
import { UserContext } from "./UserContext";
/**
@@ -33,7 +32,6 @@ export const UGPTContext = createContext<{
setLoggedInData: () => void 0,
viewingData: null,
-
setViewingData: () => void 0,
});
@@ -106,15 +104,6 @@ export function UGPTContextProvider({ children }: JustChildren) {
return ;
}
- if (!viewingData) {
- return (
-
- );
- }
-
return (
(
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: {
@@ -98,9 +99,10 @@ export function CreateDefaultFolderSearchParams
title: (x) => x.__related.song.title,
difficulty: (x) => x.difficulty,
level: (x) => x.levelNum,
- score: (x) => x.__related.pb?.scoreData.score ?? -Infinity,
- percent: (x) => x.__related.pb?.scoreData.percent ?? -Infinity,
- ranking: (x) => x.__related.pb?.rankingData.rank ?? -Infinity,
+ 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: {
diff --git a/client/src/util/ztable/search.ts b/client/src/util/ztable/search.ts
index 3ce3caf65..71bf0824b 100644
--- a/client/src/util/ztable/search.ts
+++ b/client/src/util/ztable/search.ts
@@ -204,6 +204,16 @@ export function GetValueGetter(v: ValueGetterOrHybrid) {
return v.valueGetter;
}
+function GetValueInsensitive(record: Record, key: string): T | undefined {
+ for (const [recKey, value] of Object.entries(record)) {
+ if (recKey.toLowerCase() === key.toLowerCase()) {
+ return value;
+ }
+ }
+
+ return undefined;
+}
+
export function ComposeSearchFunction(
valueGetters: Record>
): ZTableSearchFn {
@@ -237,7 +247,7 @@ export function ComposeSearchFunction(
}
for (const directive of directives) {
- const vgOrHybrid = valueGetters[directive.key];
+ const vgOrHybrid = GetValueInsensitive(valueGetters, directive.key);
if (!vgOrHybrid) {
continue;
diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts
index eb25ad973..5fd06d12c 100644
--- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts
+++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts
@@ -89,12 +89,13 @@ export async function UpdateChartRanking(game: Game, playtype: Playtype, chartID
for (const score of scores) {
rank++;
+ seenUserIDs.push(score.userID);
const thisUsersRivals = allRivals[score.userID];
let rivalRank: integer | null = null;
- if (thisUsersRivals) {
+ if (thisUsersRivals && thisUsersRivals.length > 0) {
rivalRank = thisUsersRivals.filter((e) => seenUserIDs.includes(e)).length + 1;
}
From 5941902257b66e2e71d5bc8f2011f93e4897ce19 Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Sun, 16 Oct 2022 15:26:00 +0100
Subject: [PATCH 08/10] feat: set default rival or global rankings
---
.../_game/_playtype/GPTLeaderboardsPage.tsx | 1 +
.../games/_game/_playtype/ScoresPage.tsx | 6 +++-
.../_game/_playtype/UGPTSettingsPage.tsx | 16 +++++++++++
.../components/tables/folders/FolderTable.tsx | 10 ++++---
client/src/components/tables/pbs/PBTable.tsx | 2 +-
.../components/util/usePreferredRanking.tsx | 10 +++++++
common/src/types.ts | 1 +
.../lib/game-settings/create-game-settings.ts | 1 +
server/src/lib/migration/migrations.ts | 3 +-
.../add-preferredRanking-to-ugpt.ts | 28 +++++++++++++++++++
.../games/_game/_playtype/settings/router.ts | 5 ++++
11 files changed, 76 insertions(+), 7 deletions(-)
create mode 100644 client/src/components/util/usePreferredRanking.tsx
create mode 100644 server/src/lib/migration/migrations/add-preferredRanking-to-ugpt.ts
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 6e5786917..4e3b3c4be 100644
--- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx
+++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx
@@ -236,6 +236,7 @@ function ScoreLeaderboard({ game, playtype }: GamePT) {
showChart
indexCol
alg={alg}
+ defaultRankingViewMode="both-if-self"
/>
>
);
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 8fa9a5934..e66731f44 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
@@ -27,6 +27,7 @@ import {
UnsuccessfulAPIResponse,
} from "tachi-common";
import { GamePT, SetState } from "types/react";
+import usePreferredRanking from "components/util/usePreferredRanking";
export default function ScoresPage({
reqUser,
@@ -177,6 +178,8 @@ function PBsOverview({
const { data, error } = useFetchPBs(url, reqUser);
+ const preferredRanking = usePreferredRanking();
+
return (
@@ -195,7 +198,8 @@ function PBsOverview({
showPlaycount={showPlaycount}
indexCol={indexCol}
alg={alg}
- playtype={playtype as "SP" | "DP"}
+ playtype={playtype}
+ defaultRankingViewMode={preferredRanking}
/>
) : (
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 cc917275b..3c29a2b5e 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
@@ -104,6 +104,7 @@ function PreferencesForm({ reqUser, game, playtype }: Props) {
gameSpecific: settings!.preferences.gameSpecific as any,
defaultTable: settings!.preferences.defaultTable,
scoreBucket: settings!.preferences.scoreBucket ?? gptConfig.scoreBucket,
+ preferredRanking: settings!.preferences.preferredRanking ?? "global",
},
onSubmit: async (values) => {
const rj = await APIFetchV1
(
@@ -221,6 +222,21 @@ function PreferencesForm({ reqUser, game, playtype }: Props) {
breakdown. You can still view all the same stats!
+
+ Preferred Ranking
+
+
+
+
+
+ What should {TachiConfig.name} default to when showing your score rankings?
+
+
Preferred Table
({
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
- const { settings } = useLUGPTSettings();
+ const preferredRanking = usePreferredRanking();
const [rating, setRating] = useState(defaultRating);
- const [rankingViewMode, setRankingViewMode] = useState("global");
+ const [rankingViewMode, setRankingViewMode] = useState(
+ preferredRanking ?? "global"
+ );
const headers: Header[] = [
ChartHeader(game, (k) => k),
diff --git a/client/src/components/tables/pbs/PBTable.tsx b/client/src/components/tables/pbs/PBTable.tsx
index 6c859b3d7..aadc52f87 100644
--- a/client/src/components/tables/pbs/PBTable.tsx
+++ b/client/src/components/tables/pbs/PBTable.tsx
@@ -40,7 +40,7 @@ export default function PBTable({
playtype: Playtype;
game: Game;
alg?: ScoreCalculatedDataLookup[I];
- defaultRankingViewMode?: RankingViewMode;
+ defaultRankingViewMode?: RankingViewMode | null;
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
diff --git a/client/src/components/util/usePreferredRanking.tsx b/client/src/components/util/usePreferredRanking.tsx
new file mode 100644
index 000000000..54ec630a9
--- /dev/null
+++ b/client/src/components/util/usePreferredRanking.tsx
@@ -0,0 +1,10 @@
+import { UGPTSettings } from "tachi-common";
+import useLUGPTSettings from "./useLUGPTSettings";
+
+export default function usePreferredRanking():
+ | UGPTSettings["preferences"]["preferredRanking"]
+ | null {
+ const { settings } = useLUGPTSettings();
+
+ return settings?.preferences.preferredRanking ?? null;
+}
diff --git a/common/src/types.ts b/common/src/types.ts
index 549c2f740..6845c0211 100644
--- a/common/src/types.ts
+++ b/common/src/types.ts
@@ -1471,6 +1471,7 @@ export interface UGPTSettings extends MongoDBDo
stats: Array;
scoreBucket: "grade" | "lamp" | null;
defaultTable: string | null;
+ preferredRanking: "global" | "rival" | null;
gameSpecific: UGPTSpecificPreferences[I];
};
rivals: Array;
diff --git a/server/src/lib/game-settings/create-game-settings.ts b/server/src/lib/game-settings/create-game-settings.ts
index 35f030966..d9e81c14b 100644
--- a/server/src/lib/game-settings/create-game-settings.ts
+++ b/server/src/lib/game-settings/create-game-settings.ts
@@ -43,6 +43,7 @@ export async function CreateGameSettings(userID: integer, game: Game, playtype:
preferredScoreAlg: null,
scoreBucket: null,
defaultTable: null,
+ preferredRanking: null,
stats: [],
gameSpecific,
},
diff --git a/server/src/lib/migration/migrations.ts b/server/src/lib/migration/migrations.ts
index 6e3fbb1c5..16eeaa821 100644
--- a/server/src/lib/migration/migrations.ts
+++ b/server/src/lib/migration/migrations.ts
@@ -1,3 +1,4 @@
+import UGPTAddPreferredRanking from "./migrations/add-preferredRanking-to-ugpt";
import UGPTRivalsMigration from "./migrations/add-rivals-to-ugpt";
import RecalcBrokenIIDXNotecounts from "./migrations/recalc-broken-iidx-notecounts";
import RemoveIIDXBeginners from "./migrations/remove-iidx-beginners";
@@ -27,7 +28,7 @@ export const FAKE_MIGRATION: Migration = {
const REGISTERED_MIGRATIONS: Array =
Environment.nodeEnv === "test"
? [FAKE_MIGRATION]
- : [UGPTRivalsMigration, RemoveMultifolderStats];
+ : [UGPTRivalsMigration, RemoveMultifolderStats, UGPTAddPreferredRanking];
// only apply type-specific migrations if we're not in testing
if (Environment.nodeEnv !== "test") {
diff --git a/server/src/lib/migration/migrations/add-preferredRanking-to-ugpt.ts b/server/src/lib/migration/migrations/add-preferredRanking-to-ugpt.ts
new file mode 100644
index 000000000..8ec001cc5
--- /dev/null
+++ b/server/src/lib/migration/migrations/add-preferredRanking-to-ugpt.ts
@@ -0,0 +1,28 @@
+import db from "external/mongo/db";
+import type { Migration } from "utils/types";
+
+const migration: Migration = {
+ id: "add-preferredRanking-to-ugpt",
+ up: async () => {
+ await db["game-settings"].update(
+ {},
+ {
+ $set: {
+ "preferences.preferredRanking": null,
+ },
+ },
+ { multi: true }
+ );
+ },
+ down: async () => {
+ await db["game-settings"].update(
+ {},
+ {
+ $unset: { "preferences.preferredRanking": 1 },
+ },
+ { multi: true }
+ );
+ },
+};
+
+export default migration;
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 06b18214e..bf1be647e 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
@@ -53,6 +53,7 @@ router.patch(
preferredSessionAlg: p.optional(p.nullable(p.isIn(gptConfig.sessionRatingAlgs))),
preferredProfileAlg: p.optional(p.nullable(p.isIn(gptConfig.profileRatingAlgs))),
defaultTable: "*?string",
+ preferredRanking: optNull(p.isIn("global", "rival")),
// This is handled with game-specific schema validation below.
gameSpecific: optNull(gameSpecificSchema),
@@ -104,6 +105,10 @@ router.patch(
updateQuery[`preferences.defaultTable`] = body.defaultTable;
}
+ if (body.preferredRanking !== undefined) {
+ updateQuery[`preferences.preferredRanking`] = body.preferredRanking;
+ }
+
if (body.gameSpecific) {
for (const [key, value] of Object.entries(body.gameSpecific)) {
// @ts-expect-error This is a very hacky way of applying changes.
From 3e8dd5a4a6228f554e7d76906a586545c247f51c Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Sun, 16 Oct 2022 18:03:11 +0100
Subject: [PATCH 09/10] feat: rivals scorederboard
---
.../_game/_playtype/GPTLeaderboardsPage.tsx | 92 ++---------
.../_playtype/rivals/RivalsComparePage.tsx | 80 ++++++++++
.../_game/_playtype/rivals/RivalsMainPage.tsx | 10 +-
.../_playtype/rivals/RivalsManagePage.tsx | 11 +-
.../src/components/game/ScoreLeaderboard.tsx | 146 ++++++++++++++++++
.../src/components/util/SelectLinkButton.tsx | 8 +-
client/src/util/routing.ts | 8 +-
.../games/_game/_playtype/rivals/router.ts | 74 ++++++++-
8 files changed, 335 insertions(+), 94 deletions(-)
create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx
create mode 100644 client/src/components/game/ScoreLeaderboard.tsx
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 4e3b3c4be..8f859eea4 100644
--- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx
+++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx
@@ -1,12 +1,12 @@
-import { CreateChartMap, CreateSongMap, CreateUserMap } from "util/data";
+import { CreateUserMap } from "util/data";
import { UppercaseFirst } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import ClassBadge from "components/game/ClassBadge";
+import ScoreLeaderboard from "components/game/ScoreLeaderboard";
import useSetSubheader from "components/layout/header/useSetSubheader";
import IndexCell from "components/tables/cells/IndexCell";
import UserCell from "components/tables/cells/UserCell";
import TachiTable, { Header } from "components/tables/components/TachiTable";
-import PBTable from "components/tables/pbs/PBTable";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
@@ -14,13 +14,13 @@ 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 useScoreRatingAlg, { useProfileRatingAlg } from "components/util/useScoreRatingAlg";
+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 { ScoreLeaderboardReturns, UserLeaderboardReturns } from "types/api-returns";
+import { UserLeaderboardReturns } from "types/api-returns";
import { GamePT } from "types/react";
-import { PBDataset, UGSDataset } from "types/tables";
+import { UGSDataset } from "types/tables";
export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
useSetSubheader(
@@ -50,7 +50,11 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
{mode === "profile" ? (
) : (
-
+
)}
@@ -165,79 +169,3 @@ function ProfileLeaderboard({ game, playtype }: GamePT) {
>
);
}
-
-function ScoreLeaderboard({ game, playtype }: GamePT) {
- const gptConfig = GetGamePTConfig(game, playtype);
-
- const defaultAlg = useScoreRatingAlg(game, playtype);
-
- const [alg, setAlg] = useState(defaultAlg);
-
- const SelectComponent =
- gptConfig.scoreRatingAlgs.length > 1 ? (
- setAlg(e.target.value as any)}>
- {gptConfig.scoreRatingAlgs.map((e) => (
-
- ))}
-
- ) : null;
-
- const { data, error } = useApiQuery(
- `/games/${game}/${playtype}/score-leaderboard?alg=${alg}`
- );
-
- if (error) {
- return (
- <>
- {SelectComponent}
-
- >
- );
- }
-
- if (!data) {
- return (
- <>
- {SelectComponent}
-
- >
- );
- }
-
- const songMap = CreateSongMap(data.songs);
- const chartMap = CreateChartMap(data.charts);
- const userMap = CreateUserMap(data.users);
-
- const pbDataset: PBDataset = [];
-
- for (const [index, pb] of data.pbs.entries()) {
- pbDataset.push({
- ...pb,
- __related: {
- chart: chartMap.get(pb.chartID)!,
- song: songMap.get(pb.songID)!,
- index,
- user: userMap.get(pb.userID)!,
- },
- });
- }
-
- return (
- <>
- {SelectComponent}
-
-
- >
- );
-}
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
new file mode 100644
index 000000000..8a5bd1f99
--- /dev/null
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx
@@ -0,0 +1,80 @@
+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 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";
+
+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`;
+
+ return (
+
+
+
+
+
+ Compare Top 100s
+
+
+
+ Best PBs
+
+
+
+ Folder Comparisons
+
+
+
+
+
+
+
+ top100
+
+
+
+
+
+
+
+ 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 1cbcef9a8..816d452cc 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
@@ -7,6 +7,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 RivalsComparePage from "./RivalsComparePage";
import RivalsManagePage from "./RivalsManagePage";
export default function RivalsMainPage({
@@ -36,7 +37,7 @@ export default function RivalsMainPage({
Goals & Milestones
-
+
Compare
@@ -82,11 +83,8 @@ export default function RivalsMainPage({
goals milestones
-
- compare
+
+
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 047554054..2d857089c 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
@@ -12,8 +12,9 @@ import UserIcon from "components/util/UserIcon";
import { UserContext } from "context/UserContext";
import React, { useContext, useState } from "react";
import { Button, Col } from "react-bootstrap";
-import { Game, Playtype, PublicUserDocument } from "tachi-common";
+import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common";
import useLUGPTSettings from "components/util/useLUGPTSettings";
+import useSetSubheader from "components/layout/header/useSetSubheader";
export default function RivalsManagePage({
reqUser,
@@ -24,6 +25,14 @@ export default function RivalsManagePage({
game: Game;
playtype: Playtype;
}) {
+ const gameConfig = GetGameConfig(game);
+
+ useSetSubheader(
+ ["Users", reqUser.username, "Games", gameConfig.name, playtype, "Rivals", "Manage"],
+ [reqUser, game, playtype],
+ `Managing ${reqUser.username}'s ${FormatGame(game, playtype)} Rivals`
+ );
+
const { data, error } = useApiQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/rivals`
);
diff --git a/client/src/components/game/ScoreLeaderboard.tsx b/client/src/components/game/ScoreLeaderboard.tsx
new file mode 100644
index 000000000..44b43bdd9
--- /dev/null
+++ b/client/src/components/game/ScoreLeaderboard.tsx
@@ -0,0 +1,146 @@
+import { DEFAULT_BAR_PROPS } from "util/charts";
+import { CreateChartMap, CreateUserMap } from "util/data";
+import { UppercaseFirst } from "util/misc";
+import { NumericSOV, StrSOV } from "util/sorts";
+import { ResponsiveBar } from "@nivo/bar";
+import PBTable from "components/tables/pbs/PBTable";
+import ApiError from "components/util/ApiError";
+import Loading from "components/util/Loading";
+import useApiQuery from "components/util/query/useApiQuery";
+import useScoreRatingAlg from "components/util/useScoreRatingAlg";
+import React, { useState } from "react";
+import { Form } from "react-bootstrap";
+import { COLOUR_SET, CreateSongMap, GetGamePTConfig, integer } from "tachi-common";
+import { ScoreLeaderboardReturns } from "types/api-returns";
+import { GamePT } from "types/react";
+import { PBDataset } from "types/tables";
+import Divider from "components/util/Divider";
+
+const USER_COLOURS = [
+ COLOUR_SET.blue,
+ COLOUR_SET.red,
+ COLOUR_SET.green,
+ COLOUR_SET.pink,
+ COLOUR_SET.purple,
+ COLOUR_SET.orange,
+];
+
+export default function ScoreLeaderboard({ game, playtype, url }: GamePT & { url: string }) {
+ const gptConfig = GetGamePTConfig(game, playtype);
+
+ const defaultAlg = useScoreRatingAlg(game, playtype);
+
+ const [alg, setAlg] = useState(defaultAlg);
+
+ const SelectComponent =
+ gptConfig.scoreRatingAlgs.length > 1 ? (
+ setAlg(e.target.value as any)}>
+ {gptConfig.scoreRatingAlgs.map((e) => (
+
+ ))}
+
+ ) : null;
+
+ const { data, error } = useApiQuery(`${url}?alg=${alg}`);
+
+ if (error) {
+ return (
+ <>
+ {SelectComponent}
+
+ >
+ );
+ }
+
+ if (!data) {
+ return (
+ <>
+ {SelectComponent}
+
+ >
+ );
+ }
+
+ const songMap = CreateSongMap(data.songs);
+ const chartMap = CreateChartMap(data.charts);
+ const userMap = CreateUserMap(data.users);
+
+ const pbDataset: PBDataset = [];
+
+ for (const [index, pb] of data.pbs.entries()) {
+ pbDataset.push({
+ ...pb,
+ __related: {
+ chart: chartMap.get(pb.chartID)!,
+ song: songMap.get(pb.songID)!,
+ index,
+ user: userMap.get(pb.userID)!,
+ },
+ });
+ }
+
+ return (
+ <>
+ {SelectComponent}
+
+
+
+
+ >
+ );
+}
+
+function DistributionChart({ dataset }: { dataset: PBDataset }) {
+ // username -> scores in the top N
+ const dist: Record = {};
+
+ for (const pb of dataset) {
+ const key = pb.__related.user?.username;
+
+ if (key === undefined) {
+ // shouldn't be possible, but lets not have the ui crash
+ // because of something so menial
+ continue;
+ }
+
+ if (key in dist) {
+ dist[key]++;
+ } else {
+ dist[key] = 1;
+ }
+ }
+
+ const aggregatedData = Object.entries(dist)
+ .map(([username, count]) => ({ username, count }))
+ .sort(NumericSOV((x) => x.count));
+
+ const usernames = [...new Set(aggregatedData.map((e) => e.username))];
+
+ return (
+
+
+ USER_COLOURS[(usernames.indexOf(x.data.username) ?? 0) % USER_COLOURS.length]
+ }
+ margin={{ left: 80, bottom: 40, top: 20, right: 20 }}
+ tooltip={() => <>>}
+ {...DEFAULT_BAR_PROPS}
+ />
+
+ );
+}
diff --git a/client/src/components/util/SelectLinkButton.tsx b/client/src/components/util/SelectLinkButton.tsx
index 69058c82a..7d229f55a 100644
--- a/client/src/components/util/SelectLinkButton.tsx
+++ b/client/src/components/util/SelectLinkButton.tsx
@@ -9,15 +9,21 @@ export default function SelectLinkButton({
onVariant = "primary",
offVariant = "outline-secondary",
to,
+ matchIfStartsWith = false,
}: {
onVariant?: ButtonVariant;
offVariant?: ButtonVariant;
to: string;
+ matchIfStartsWith?: boolean;
} & JustChildren) {
return (
{children}
diff --git a/client/src/util/routing.ts b/client/src/util/routing.ts
index c75f10794..a18afc729 100644
--- a/client/src/util/routing.ts
+++ b/client/src/util/routing.ts
@@ -1,6 +1,6 @@
import { EscapeStringRegexp } from "./misc";
-export function DoesMatchRoute(str: string, route: string) {
+export function DoesMatchRoute(str: string, route: string, ends = true) {
const comps = EscapeStringRegexp(route).split("/");
let regexStr = "";
@@ -12,7 +12,11 @@ export function DoesMatchRoute(str: string, route: string) {
}
}
- regexStr += "?$";
+ regexStr += "?";
+
+ if (ends) {
+ regexStr += "$";
+ }
const regex = new RegExp(regexStr, "u");
return !!str.match(regex);
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 19098b63d..7a1f6f5ef 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
@@ -1,12 +1,17 @@
import { RequireAuthedAsUser } from "../../../../middleware";
import { Router } from "express";
+import db from "external/mongo/db";
import { SetRivalsFailReasons } from "lib/constants/err-codes";
-import { GetChallengerUsers, GetRivalUsers, SetRivals } from "lib/rivals/rivals";
+import { GetChallengerUsers, GetRivalIDs, GetRivalUsers, SetRivals } from "lib/rivals/rivals";
import p from "prudence";
import { RequirePermissions } from "server/middleware/auth";
import prValidate from "server/middleware/prudence-validate";
-import { FormatGame } from "tachi-common";
+import { FormatGame, GetGamePTConfig } from "tachi-common";
+import { GetRelevantSongsAndCharts } from "utils/db";
+import { IsString } from "utils/misc";
import { GetUGPT } from "utils/req-tachi-data";
+import { CheckStrScoreAlg } from "utils/string-checks";
+import { GetUsersWithIDs } from "utils/user";
import type { integer } from "tachi-common";
const router: Router = Router({ mergeParams: true });
@@ -97,4 +102,69 @@ router.get("/challengers", async (req, res) => {
});
});
+/**
+ * Retrieve a "score 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.
+ *
+ * @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
+ */
+router.get("/score-leaderboard", async (req, res) => {
+ const { user, game, playtype } = GetUGPT(req);
+ const gptConfig = GetGamePTConfig(game, playtype);
+
+ let alg = gptConfig.defaultScoreRatingAlg;
+
+ if (IsString(req.query.alg)) {
+ const temp = CheckStrScoreAlg(game, playtype, req.query.alg);
+
+ if (temp === null) {
+ return res.status(400).json({
+ success: false,
+ description: `Invalid value of ${
+ req.query.alg
+ } for alg. Expected one of ${gptConfig.scoreRatingAlgs.join(", ")}`,
+ });
+ }
+
+ alg = temp;
+ }
+
+ const rivalIDs = await GetRivalIDs(user.id, game, playtype);
+ const userSet = [...rivalIDs, user.id];
+
+ const pbs = await db["personal-bests"].find(
+ {
+ game,
+ playtype,
+ userID: { $in: userSet },
+ },
+ {
+ sort: {
+ [`calculatedData.${alg}`]: -1,
+ },
+ limit: 100,
+ }
+ );
+
+ const users = await GetUsersWithIDs(pbs.map((e) => e.userID));
+
+ const { songs, charts } = await GetRelevantSongsAndCharts(pbs, game);
+
+ return res.status(200).send({
+ success: true,
+ description: `Successfully returned ${pbs.length} pbs.`,
+ body: {
+ pbs,
+ songs,
+ charts,
+ users,
+ },
+ });
+});
+
export default router;
From 1be6acb326e5840bbf6c6c5137941f898b614a64 Mon Sep 17 00:00:00 2001
From: zkldi <20380519+zkldi@users.noreply.github.com>
Date: Sun, 16 Oct 2022 19:19:58 +0100
Subject: [PATCH 10/10] fix: make server tests compile and work
---
server/src/lib/showcase/get-stats.test.ts | 1 +
.../_userID/games/_game/_playtype/settings/router.test.ts | 5 +++++
.../_userID/games/_game/_playtype/showcase/router.test.ts | 2 ++
server/src/test-utils/mock-db/game-settings.json | 1 +
server/src/test-utils/test-data.ts | 1 +
5 files changed, 10 insertions(+)
diff --git a/server/src/lib/showcase/get-stats.test.ts b/server/src/lib/showcase/get-stats.test.ts
index 69ea12bf2..34f05bc45 100644
--- a/server/src/lib/showcase/get-stats.test.ts
+++ b/server/src/lib/showcase/get-stats.test.ts
@@ -26,6 +26,7 @@ t.test("#EvalulateUsersStatsShowcase", (t) => {
preferredSessionAlg: null,
scoreBucket: null,
defaultTable: null,
+ preferredRanking: null,
stats: [
{
folderID: TestingIIDXFolderSP10.folderID,
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 40509c96f..d080d3fab 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
@@ -19,6 +19,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
preferredScoreAlg: null,
preferredSessionAlg: null,
preferredProfileAlg: null,
+ preferredRanking: null,
scoreBucket: null,
defaultTable: null,
stats: [],
@@ -68,6 +69,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
preferredSessionAlg: null,
preferredProfileAlg: null,
scoreBucket: null,
+ preferredRanking: null,
defaultTable: null,
stats: [],
gameSpecific: {
@@ -123,6 +125,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
preferredScoreAlg: "ktLampRating",
preferredSessionAlg: null,
preferredProfileAlg: null,
+ preferredRanking: null,
scoreBucket: null,
defaultTable: null,
stats: [],
@@ -183,6 +186,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
preferredScoreAlg: null,
preferredSessionAlg: null,
preferredProfileAlg: null,
+ preferredRanking: null,
scoreBucket: null,
defaultTable: null,
stats: [],
@@ -415,6 +419,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
preferredScoreAlg: null,
preferredSessionAlg: null,
preferredProfileAlg: null,
+ preferredRanking: null,
scoreBucket: null,
defaultTable: "mock_table",
stats: [],
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 d5ba4d7e0..aa36692e7 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
@@ -22,6 +22,7 @@ const SetFolders = async () => {
preferredProfileAlg: null,
preferredScoreAlg: null,
preferredSessionAlg: null,
+ preferredRanking: null,
scoreBucket: null,
defaultTable: null,
stats: [
@@ -87,6 +88,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/showcase", (t) => {
preferredProfileAlg: null,
preferredScoreAlg: null,
preferredSessionAlg: null,
+ preferredRanking: null,
scoreBucket: null,
defaultTable: null,
stats: [
diff --git a/server/src/test-utils/mock-db/game-settings.json b/server/src/test-utils/mock-db/game-settings.json
index 7af25afa6..9f02ccb76 100644
--- a/server/src/test-utils/mock-db/game-settings.json
+++ b/server/src/test-utils/mock-db/game-settings.json
@@ -9,6 +9,7 @@
"preferredProfileAlg": null,
"scoreBucket": null,
"defaultTable": null,
+ "preferredRanking": null,
"stats": [],
"gameSpecific": {
"display2DXTra": false,
diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts
index f23171277..711aa7797 100644
--- a/server/src/test-utils/test-data.ts
+++ b/server/src/test-utils/test-data.ts
@@ -729,6 +729,7 @@ export const FakeGameSettings: UGPTSettings = {
preferredScoreAlg: null,
preferredSessionAlg: null,
scoreBucket: null,
+ preferredRanking: null,
stats: [],
},
rivals: [],