This commit is contained in:
zkldi
2021-09-30 20:35:07 +01:00
parent 8b3820f08c
commit 2a6aa6b879
19 changed files with 538 additions and 138 deletions
@@ -9,6 +9,7 @@ import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import Muted from "components/util/Muted";
import SelectButton from "components/util/SelectButton";
import { UserContext } from "context/UserContext";
import React, { useContext, useMemo, useState } from "react";
@@ -31,6 +32,7 @@ import { GamePT, SetState } from "types/react";
import { PBDataset } from "types/tables";
import { APIFetchV1, UnsuccessfulAPIFetchResponse } from "util/api";
import { CreateUserMap } from "util/data";
import { SelectRightChart } from "util/misc";
// This component forms a wrapper around the Real GPT Chart Page
// which handles the case where activeChart == null.
@@ -60,9 +62,9 @@ export default function GPTChartPage({
`${formatSongTitle} (${formatDiff})`
);
if (!chart) {
setActiveChart(SelectRightChart(gptConfig, difficulty, allCharts));
setActiveChart(SelectRightChart(gptConfig, difficulty, allCharts));
if (!chart) {
return <Loading />;
}
@@ -158,7 +160,12 @@ function InternalGPTChartPage({
<Icon type="trophy" />
Best 100
</SelectButton>
<SelectButton value={mode} setValue={setMode} id="adjacent">
<SelectButton
value={mode}
setValue={setMode}
id="adjacent"
disabled={!data.adjacent}
>
<Icon type="user" />
Your Position
</SelectButton>
@@ -241,7 +248,7 @@ function TopShowcase({
const bestUser = userMap.get(bestPlay.userID)!;
// User hasn't played, or isn't logged in or something.
if (!data.adjacent || user?.id === bestPlay.userID) {
if (user?.id === bestPlay.userID) {
return (
<Col xs={12}>
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
@@ -249,6 +256,21 @@ function TopShowcase({
);
}
if (!data.adjacent) {
return (
<>
<Col xs={12} lg={6}>
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
</Col>
<Col xs={12} lg={6}>
<Card header="Your Score" className="text-center">
<Muted>You've not played this chart.</Muted>
</Card>
</Col>
</>
);
}
const thisUsersPlay = data.adjacent.pb;
return (
@@ -304,22 +326,3 @@ function PlayCard({
</Card>
);
}
function SelectRightChart(gptConfig: GamePTConfig, chartID: string, charts: ChartDocument[]) {
if (gptConfig.difficulties.includes(chartID as any)) {
for (const chart of charts) {
if (chart.difficulty === chartID && chart.isPrimary) {
return chart;
}
}
} else {
// else, its a chart ID.
for (const chart of charts) {
if (chartID === chart.chartID) {
return chart;
}
}
}
return null;
}
@@ -1,7 +1,28 @@
import ClassBadge from "components/game/ClassBadge";
import useSetSubheader from "components/layout/header/useSetSubheader";
import React from "react";
import { FormatGame, GetGameConfig } from "tachi-common";
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 DebugContent from "components/util/DebugContent";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import Muted from "components/util/Muted";
import useApiQuery from "components/util/query/useApiQuery";
import SelectButton from "components/util/SelectButton";
import useScoreRatingAlg, { useProfileRatingAlg } from "components/util/useScoreRatingAlg";
import { UGPTSettingsContext } from "context/UGPTSettingsContext";
import React, { useContext, useState } from "react";
import { Col, Row, Form } from "react-bootstrap";
import { FormatGame, GetGameConfig, GetGamePTConfig } from "tachi-common";
import { ScoreLeaderboardReturns, UserLeaderboardReturns } from "types/api-returns";
import { GamePT } from "types/react";
import { PBDataset, UGSDataset } from "types/tables";
import { CreateChartMap, CreateSongMap, CreateUserMap } from "util/data";
import { UppercaseFirst } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
useSetSubheader(
@@ -10,5 +31,216 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
`${FormatGame(game, playtype)} Leaderboards`
);
return <div>leaderboard</div>;
const { settings } = useContext(UGPTSettingsContext);
const [mode, setMode] = useState<"profile" | "score">("profile");
return (
<Row>
<Col xs={12} className="d-flex justify-content-center">
<div className="btn-group">
<SelectButton id="profile" value={mode} setValue={setMode}>
<Icon type="user" />
User Leaderboards
</SelectButton>
<SelectButton id="score" value={mode} setValue={setMode}>
<Icon type="sort-numeric-up-alt" />
Score Leaderboards
</SelectButton>
</div>
</Col>
<Col xs={12}>
<Divider />
{mode === "profile" ? (
<ProfileLeaderboard game={game} playtype={playtype} />
) : (
<ScoreLeaderboard game={game} playtype={playtype} />
)}
</Col>
</Row>
);
}
function ProfileLeaderboard({ game, playtype }: GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const defaultAlg = useProfileRatingAlg(game, playtype);
const [alg, setAlg] = useState(defaultAlg);
const SelectComponent =
gptConfig.profileRatingAlgs.length > 1 ? (
<Form.Control as="select" value={alg} onChange={e => setAlg(e.target.value as any)}>
{gptConfig.profileRatingAlgs.map(e => (
<option key={e} value={e}>
{UppercaseFirst(e)}
</option>
))}
</Form.Control>
) : null;
const { data, isLoading, error } = useApiQuery<UserLeaderboardReturns>(
`/games/${game}/${playtype}/leaderboard?alg=${alg}`
);
if (error) {
return (
<>
{SelectComponent}
<ApiError error={error} />
</>
);
}
if (!data || isLoading) {
return (
<>
{SelectComponent}
<Loading />
</>
);
}
const userMap = CreateUserMap(data.users);
const userDataset: UGSDataset = [];
for (const [index, gs] of data.gameStats.entries()) {
userDataset.push({
...gs,
__related: {
user: userMap.get(gs.userID)!,
index,
},
});
}
return (
<>
{SelectComponent}
<Divider />
<TachiTable
dataset={userDataset}
entryName="Rankers"
headers={[
["Ranking", "Rank", NumericSOV(x => x.__related.index)],
["User", "User", StrSOV(x => x.__related.user.username)],
...gptConfig.profileRatingAlgs.map(
e =>
[
UppercaseFirst(e),
UppercaseFirst(e),
NumericSOV(x => x.ratings[e] ?? -Infinity),
] as Header<UGSDataset[0]>
),
["Classes", "Classes"],
]}
rowFunction={r => (
<tr>
<IndexCell index={r.__related.index} />
<UserCell game={game} playtype={playtype} user={r.__related.user} />
{gptConfig.profileRatingAlgs.map(e => (
<td key={e}>
{r.ratings[e]
? gptConfig.profileRatingAlgFormatters[e]
? gptConfig.profileRatingAlgFormatters[e]!(r.ratings[e]!)
: r.ratings[e]!.toFixed(2)
: "No Data."}
</td>
))}
<td>
{Object.keys(r.classes).length === 0 ? (
<Muted>None</Muted>
) : (
Object.entries(r.classes).map(([k, v]) => (
<ClassBadge
key={k}
classSet={k as keyof typeof r.classes}
game={game}
playtype={playtype}
classValue={v}
/>
))
)}
</td>
</tr>
)}
/>
</>
);
}
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 ? (
<Form.Control as="select" value={alg} onChange={e => setAlg(e.target.value as any)}>
{gptConfig.scoreRatingAlgs.map(e => (
<option key={e} value={e}>
{UppercaseFirst(e)}
</option>
))}
</Form.Control>
) : null;
const { data, isLoading, error } = useApiQuery<ScoreLeaderboardReturns>(
`/games/${game}/${playtype}/score-leaderboard?alg=${alg}`
);
if (error) {
return (
<>
{SelectComponent}
<ApiError error={error} />
</>
);
}
if (!data || isLoading) {
return (
<>
{SelectComponent}
<Loading />
</>
);
}
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}
<Divider />
<PBTable
dataset={pbDataset}
game={game}
playtype={playtype}
showUser
showChart
indexCol
alg={alg}
/>
</>
);
}
@@ -1,29 +0,0 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import LinkButton from "components/util/LinkButton";
import React, { useEffect } from "react";
import { Button, Row } from "react-bootstrap";
import { ChartDocument, FormatDifficulty, GetGameConfig, GetGamePTConfig } from "tachi-common";
import { SongsReturn } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { ChangeOpacity } from "util/color-opacity";
import { NumericSOV } from "util/sorts";
export default function GPTSongPage({
game,
playtype,
song,
charts,
setActiveChart,
}: { setActiveChart: SetState<ChartDocument | null> } & SongsReturn & GamePT) {
const formatSongTitle = `${song.artist} - ${song.title}`;
useSetSubheader(
["Games", GetGameConfig(game).name, playtype, "Songs", formatSongTitle],
[game, playtype],
formatSongTitle
);
setActiveChart(null);
return <div className="w-100 text-center">You gotta select a chart to see leaderboards!</div>;
}
@@ -155,7 +155,7 @@ function LeaderboardsPageContent({
header={"Leaderboard"}
footer={
<Link
to={`/dashboard/games/${game}/${playtype}/leaderboard`}
to={`/dashboard/games/${game}/${playtype}/leaderboards`}
className="float-right"
>
View Global Leaderboards
+39 -13
View File
@@ -3,7 +3,6 @@ import GPTChartPage from "app/pages/dashboard/games/_game/_playtype/GPTChartPage
import GPTDevInfo from "app/pages/dashboard/games/_game/_playtype/GPTDevInfo";
import GPTLeaderboardsPage from "app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage";
import GPTMainPage from "app/pages/dashboard/games/_game/_playtype/GPTMainPage";
import GPTSongPage from "app/pages/dashboard/games/_game/_playtype/GPTSongPage";
import GPTSongsPage from "app/pages/dashboard/games/_game/_playtype/GPTSongsPage";
import { ErrorPage } from "app/pages/ErrorPage";
import ChartInfoFormat from "components/game/charts/ChartInfoFormat";
@@ -15,6 +14,8 @@ import Divider from "components/util/Divider";
import LinkButton from "components/util/LinkButton";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { UGPTSettingsContext, UGPTSettingsContextProvider } from "context/UGPTSettingsContext";
import { UserContext } from "context/UserContext";
import { UserSettingsContext } from "context/UserSettingsContext";
import React, { useContext, useEffect, useState } from "react";
import { Col, Row } from "react-bootstrap";
@@ -25,9 +26,11 @@ import {
Game,
GetGameConfig,
GetGamePTConfig,
UGPTSettings,
} from "tachi-common";
import { SongsReturn } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { APIFetchV1 } from "util/api";
import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts";
import { ChangeOpacity } from "util/color-opacity";
import { NumericSOV } from "util/sorts";
@@ -57,7 +60,9 @@ export default function GameRoutes() {
</Route>
<Route path="/dashboard/games/:game/:playtype">
<GamePlaytypeRoutes game={game} />
<UGPTSettingsContextProvider>
<GamePlaytypeRoutes game={game} />
</UGPTSettingsContextProvider>
</Route>
<Route path="*">
@@ -79,6 +84,30 @@ function GamePlaytypeRoutes({ game }: { game: Game }) {
);
}
const { user } = useContext(UserContext);
const { setSettings } = useContext(UGPTSettingsContext);
useEffect(() => {
(async () => {
if (user) {
const settingsRes = await APIFetchV1<UGPTSettings>(
`/users/${user.id}/games/${game}/${playtype}/settings`
);
if (!settingsRes.success) {
setSettings(null);
return;
}
setSettings(settingsRes.body);
}
})();
return () => {
setSettings(null);
};
}, [user, game, playtype]);
return (
<>
<div className="card">
@@ -122,12 +151,13 @@ function SongChartRoutes({ game, playtype }: GamePT) {
const { settings } = useContext(UserSettingsContext);
const gptConfig = GetGamePTConfig(game, playtype);
const [activeChart, setActiveChart] = useState<ChartDocument | null>(null);
useEffect(() => {
setActiveChart(null);
}, [game, playtype]);
if (error) {
return <ErrorPage statusCode={error.statusCode} customMessage={error.description} />;
}
@@ -148,12 +178,12 @@ function SongChartRoutes({ game, playtype }: GamePT) {
<Divider />
<Switch>
<Route exact path="/dashboard/games/:game/:playtype/songs/:songID">
<GPTSongPage
game={game}
playtype={playtype}
song={data.song}
charts={data.charts}
setActiveChart={setActiveChart}
{/* Select the hardest chart for this. */}
<Redirect
to={`/dashboard/games/${game}/${playtype}/songs/${data.song.id}/${
data.charts.slice(0).sort(NumericSOV(x => x.levelNum, true))[0]
.difficulty
}`}
/>
</Route>
@@ -193,10 +223,6 @@ function SongInfoHeader({
setActiveChart,
}: { activeChart: ChartDocument | null; setActiveChart: SetState<ChartDocument | null> } & GamePT &
SongsReturn) {
// @TODO: Custom game song formatting functions, since for some games
// (like popn) players use the genres to discuss them.
const formatSongTitle = `${song.artist} - ${song.title}`;
const [imgShow, setImgShow] = useState(true);
const gptConfig = GetGamePTConfig(game, playtype);
+2 -2
View File
@@ -165,8 +165,8 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument
);
if (!settingsRes.success) {
console.error(settingsRes);
throw settingsRes;
setSettings(null);
return;
}
setSettings(settingsRes.body);
@@ -1,13 +1,13 @@
import React, { useContext, useState } from "react";
import { Link } from "react-router-dom";
import Dropdown from "react-bootstrap/Dropdown";
import Divider from "components/util/Divider";
import { PublicUserDocument } from "tachi-common";
import { Button } from "react-bootstrap";
import { APIFetchV1, ToAPIURL } from "util/api";
import toast from "react-hot-toast";
import { UserContext } from "context/UserContext";
import DropdownToggleOverride from "components/util/DropdownToggleOverride";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { Button } from "react-bootstrap";
import Dropdown from "react-bootstrap/Dropdown";
import toast from "react-hot-toast";
import { Link } from "react-router-dom";
import { PublicUserDocument } from "tachi-common";
import { APIFetchV1, ToAPIURL } from "util/api";
export function UserProfileDropdown({ user }: { user: PublicUserDocument }) {
const { setUser } = useContext(UserContext);
@@ -1,6 +1,6 @@
import SmallText from "components/util/SmallText";
import { useZTable, ZTableSortFn } from "components/util/table/useZTable";
import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { Button } from "react-bootstrap";
import { integer } from "tachi-common";
import NoDataWrapper from "./NoDataWrapper";
@@ -8,6 +8,9 @@ import PageSelector from "./PageSelector";
import SortableTH from "./SortableTH";
import FilterDirectivesIndicator from "./FilterDirectivesIndicator";
import { ComposeSearchFunction, ValueGetterOrHybrid } from "util/ztable/search";
import { UserSettingsContext } from "context/UserSettingsContext";
import Icon from "components/util/Icon";
import { CopyToClipboard } from "util/misc";
export interface ZTableTHProps {
changeSort: (str: string) => void;
@@ -123,6 +126,8 @@ export default function TachiTable<D>({
reverseSort,
});
const { settings } = useContext(UserSettingsContext);
return (
<div className="justify-content-center w-100">
<div className="row">
@@ -156,8 +161,16 @@ export default function TachiTable<D>({
</div>
<div className="col-12 px-0">
<div className="row">
<div className="col-lg-6 align-self-center">{displayStr}</div>
<div className="col-lg-6 text-right">
<div className="col-lg-4 align-self-center">{displayStr}</div>
{settings?.preferences.developerMode && (
<div className="d-none d-lg-flex col-lg-4 justify-content-center">
<Button onClick={() => CopyToClipboard(window)} variant="outline-info">
<Icon type="table" />
Export Data
</Button>
</div>
)}
<div className="col-lg-4 ml-auto text-right">
<div className="btn-group">
<Button
variant="secondary"
@@ -6,6 +6,7 @@ import { FormatBMSTables, IsNullish } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import { ValueGetterOrHybrid } from "util/ztable/search";
import BMSDifficultyCell from "../cells/BMSDifficultyCell";
import DeltaCell from "../cells/DeltaCell";
@@ -21,6 +22,7 @@ import TachiTable, { Header } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import IndicatorHeader from "../headers/IndicatorHeader";
import PBLeadingRows from "./PBLeadingRows";
function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string) {
for (const table of chart.data.tableFolders) {
@@ -37,14 +39,18 @@ export default function BMSPBTable({
indexCol = true,
showPlaycount = false,
playtype,
showUser = false,
showChart = true,
}: {
dataset: PBDataset<"bms:7K" | "bms:14K">;
indexCol?: boolean;
showPlaycount?: boolean;
playtype: "7K" | "14K";
showUser?: boolean;
showChart?: boolean;
}) {
const headers: Header<PBDataset<"bms:7K" | "bms:14K">[0]>[] = [
[
...GetPBLeadingHeaders(showUser, showChart, [
"Chart",
"Chart",
NumericSOV(
@@ -53,9 +59,7 @@ export default function BMSPBTable({
x.__related.chart.tierlistInfo["sgl-HC"]?.value ??
x.__related.chart.levelNum
),
],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Deltas", "Deltas", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
@@ -102,7 +106,7 @@ export default function BMSPBTable({
dataset={dataset}
headers={headers}
entryName="PBs"
searchFunctions={CreateDefaultPBSearchParams("bms", playtype)}
searchFunctions={{ ...CreateDefaultPBSearchParams("bms", playtype), ...tableSearchFns }}
defaultSortMode={indexCol ? "#" : undefined}
rowFunction={pb => (
<Row
@@ -110,6 +114,8 @@ export default function BMSPBTable({
key={`${pb.chartID}:${pb.userID}`}
showPlaycount={showPlaycount}
indexCol={indexCol}
showUser={showUser}
showChart={showChart}
/>
)}
/>
@@ -120,10 +126,14 @@ function Row({
pb,
indexCol,
showPlaycount,
showUser,
showChart,
}: {
pb: PBDataset<"bms:7K" | "bms:14K">[0];
indexCol: boolean;
showPlaycount: boolean;
showUser: boolean;
showChart: boolean;
}) {
const scoreState = usePBState(pb);
@@ -140,9 +150,13 @@ function Row({
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<BMSDifficultyCell chart={pb.__related.chart} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="bms" />
<PBLeadingRows
pb={pb}
scoreState={scoreState}
showChart={showChart}
showUser={showUser}
overrideDiffCell={<BMSDifficultyCell chart={pb.__related.chart} />}
/>
<ScoreCell score={pb} />
<DeltaCell
game="bms"
@@ -5,6 +5,7 @@ import { Playtype } from "types/tachi";
import { NumericSOV, StrSOV } from "util/sorts";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DifficultyCell from "../cells/DifficultyCell";
import IndexCell from "../cells/IndexCell";
import IndicatorsCell from "../cells/IndicatorsCell";
@@ -19,6 +20,7 @@ import TachiTable, { Header } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import IndicatorHeader from "../headers/IndicatorHeader";
import PBLeadingRows from "./PBLeadingRows";
export default function GenericPBTable({
dataset,
@@ -27,6 +29,8 @@ export default function GenericPBTable({
game,
playtype,
showScore,
showUser = false,
showChart = true,
}: {
dataset: PBDataset;
indexCol?: boolean;
@@ -34,13 +38,17 @@ export default function GenericPBTable({
game: Game;
playtype: Playtype;
showScore?: boolean;
showUser?: boolean;
showChart?: boolean;
}) {
const gptConfig = GetGamePTConfig(game, playtype);
const headers: Header<PBDataset[0]>[] = [
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
...GetPBLeadingHeaders(showUser, showChart, [
"Chart",
"Chart",
NumericSOV(x => x.__related.chart.levelNum),
]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
[
@@ -74,6 +82,8 @@ export default function GenericPBTable({
indexCol={indexCol}
showScore={showScore}
showPlaycount={showPlaycount}
showUser={showUser}
showChart={showChart}
/>
)}
/>
@@ -85,11 +95,15 @@ function Row({
indexCol,
showScore,
showPlaycount,
showUser,
showChart,
}: {
pb: PBDataset[0];
indexCol: boolean;
showPlaycount: boolean;
showScore?: boolean;
showUser: boolean;
showChart: boolean;
}) {
const scoreState = usePBState(pb);
@@ -106,9 +120,12 @@ function Row({
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game={pb.game} chart={pb.__related.chart} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game={pb.game} />
<PBLeadingRows
pb={pb}
scoreState={scoreState}
showChart={showChart}
showUser={showUser}
/>
<ScoreCell score={pb} showScore={showScore} />
<LampCell score={pb} />
<RatingCell score={pb} />
@@ -5,7 +5,7 @@ import React, { useContext, useState } from "react";
import { PublicUserDocument, ScoreCalculatedDataLookup } from "tachi-common";
import { PBDataset } from "types/tables";
import { IsNullish } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { NumericSOV } from "util/sorts";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DeltaCell from "../cells/DeltaCell";
@@ -23,6 +23,7 @@ import SelectableRating from "../components/SelectableRating";
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import IIDXPBDropdown from "../dropdowns/IIDXPBDropdown";
import PBLeadingRows from "./PBLeadingRows";
export default function IIDXPBTable({
dataset,
@@ -140,7 +141,7 @@ function Row({
return (
<DropdownRow
className={pb.userID === user?.id ? "highlighted-row" : ""}
// className={pb.userID === user?.id ? "highlighted-row" : ""}
dropdown={
<IIDXPBDropdown
chart={pb.__related.chart}
@@ -152,27 +153,7 @@ function Row({
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
{showUser && showChart && (
<>
<UserCell game="iidx" playtype={pb.playtype} user={pb.__related.user!} />
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="iidx" />
</>
)}
{showUser && !showChart && (
<>
<IndicatorsCell highlight={scoreState.highlight} />
<UserCell game="iidx" playtype={pb.playtype} user={pb.__related.user!} />
</>
)}
{!showUser && showChart && (
<>
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="iidx" />
</>
)}
<PBLeadingRows {...{ showUser, showChart, pb, scoreState }} />
<ScoreCell score={pb} />
<DeltaCell
game="iidx"
@@ -3,6 +3,7 @@ import { PublicUserDocument } from "tachi-common";
import { PBDataset } from "types/tables";
import { NumericSOV, StrSOV } from "util/sorts";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DifficultyCell from "../cells/DifficultyCell";
import IndexCell from "../cells/IndexCell";
import IndicatorsCell from "../cells/IndicatorsCell";
@@ -18,22 +19,29 @@ import TachiTable, { Header } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import IndicatorHeader from "../headers/IndicatorHeader";
import PBLeadingRows from "./PBLeadingRows";
export default function MusecaPBTable({
dataset,
indexCol = true,
showPlaycount = false,
showUser = false,
showChart = true,
playtype,
}: {
dataset: PBDataset<"museca:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
showUser?: boolean;
showChart?: boolean;
playtype: "Single";
}) {
const headers: Header<PBDataset<"museca:Single">[0]>[] = [
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
...GetPBLeadingHeaders(showUser, showChart, [
"Chart",
"Chart",
NumericSOV(x => x.__related.chart.levelNum),
]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
@@ -63,6 +71,8 @@ export default function MusecaPBTable({
key={`${pb.chartID}:${pb.userID}`}
indexCol={indexCol}
showPlaycount={showPlaycount}
showChart={showChart}
showUser={showUser}
/>
)}
/>
@@ -73,10 +83,14 @@ function Row({
pb,
indexCol,
showPlaycount,
showChart,
showUser,
}: {
pb: PBDataset<"museca:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
showChart: boolean;
showUser: boolean;
}) {
const scoreState = usePBState(pb);
@@ -93,9 +107,7 @@ function Row({
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="museca" chart={pb.__related.chart} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="museca" />
<PBLeadingRows {...{ showUser, showChart, pb, scoreState }} />
<MillionsScoreCell score={pb} />
<MusecaJudgementCell score={pb} />
<LampCell score={pb} />
@@ -0,0 +1,51 @@
import React from "react";
import { SetState } from "types/react";
import { PBDataset } from "types/tables";
import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import TitleCell from "../cells/TitleCell";
import UserCell from "../cells/UserCell";
export default function PBLeadingRows({
showUser,
showChart,
pb,
scoreState,
overrideDiffCell,
}: {
showUser: boolean;
showChart: boolean;
pb: PBDataset[0];
scoreState: { highlight: boolean; setHighlight: SetState<boolean> };
overrideDiffCell?: JSX.Element;
}) {
const game = pb.game;
const diffCell = overrideDiffCell || <DifficultyCell chart={pb.__related.chart} game={game} />;
return (
<>
{showUser && showChart && (
<>
<UserCell game={game} playtype={pb.playtype} user={pb.__related.user!} />
{diffCell}
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game={game} />
</>
)}
{showUser && !showChart && (
<>
<IndicatorsCell highlight={scoreState.highlight} />
<UserCell game={game} playtype={pb.playtype} user={pb.__related.user!} />
</>
)}
{!showUser && showChart && (
<>
{diffCell}
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game={game} />
</>
)}
</>
);
}
@@ -4,6 +4,7 @@ import { PBDataset } from "types/tables";
import { IsNullish } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DifficultyCell from "../cells/DifficultyCell";
import IndexCell from "../cells/IndexCell";
import IndicatorsCell from "../cells/IndicatorsCell";
@@ -18,22 +19,29 @@ import TachiTable, { Header } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import IndicatorHeader from "../headers/IndicatorHeader";
import PBLeadingRows from "./PBLeadingRows";
export default function SDVXPBTable({
dataset,
indexCol = true,
showPlaycount = false,
playtype,
showUser = false,
showChart = true,
}: {
dataset: PBDataset<"sdvx:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
playtype: "7K" | "14K";
showUser?: boolean;
showChart?: boolean;
}) {
const headers: Header<PBDataset<"sdvx:Single">[0]>[] = [
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
...GetPBLeadingHeaders(showUser, showChart, [
"Chart",
"Chart",
NumericSOV(x => x.__related.chart.levelNum),
]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
@@ -63,6 +71,8 @@ export default function SDVXPBTable({
key={`${pb.chartID}:${pb.userID}`}
indexCol={indexCol}
showPlaycount={showPlaycount}
showChart={showChart}
showUser={showUser}
/>
)}
/>
@@ -73,10 +83,14 @@ function Row({
pb,
indexCol,
showPlaycount,
showChart,
showUser,
}: {
pb: PBDataset<"sdvx:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
showChart: boolean;
showUser: boolean;
}) {
const scoreState = usePBState(pb);
@@ -93,9 +107,7 @@ function Row({
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="sdvx" chart={pb.__related.chart} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="sdvx" />
<PBLeadingRows {...{ showUser, showChart, pb, scoreState }} />
<MillionsScoreCell score={pb} />
<SDVXJudgementCell score={pb} />
<LampCell score={pb} />
+10 -2
View File
@@ -1,6 +1,14 @@
import React from "react";
import { UserSettingsContext } from "context/UserSettingsContext";
import React, { useContext } from "react";
import { UnsuccessfulAPIFetchResponse } from "util/api";
export default function ApiError({ error }: { error: UnsuccessfulAPIFetchResponse }) {
return <div>An error has occured: {error.description}</div>;
const { settings } = useContext(UserSettingsContext);
return (
<div>
An error has occured
{settings?.preferences.developerMode ? ` (${error.description})` : ""}
</div>
);
}
+12
View File
@@ -153,3 +153,15 @@ export interface UGPTChartLeaderboardAdjacent<I extends IDStrings = IDStrings> {
adjacentAbove: PBScoreDocument<I>[];
adjacentBelow: PBScoreDocument<I>[];
}
export interface ScoreLeaderboardReturns<I extends IDStrings = IDStrings> {
users: PublicUserDocument[];
songs: SongDocument<IDStringToGame[I]>[];
charts: ChartDocument<I>[];
pbs: PBScoreDocument<I>[];
}
export interface UserLeaderboardReturns<I extends IDStrings = IDStrings> {
users: PublicUserDocument[];
gameStats: UserGameStats<I>[];
}
+8
View File
@@ -7,6 +7,7 @@ import {
integer,
ScoreDocument,
PublicUserDocument,
UserGameStats,
} from "tachi-common";
export type PBDataset<I extends IDStrings = IDStrings> = (PBScoreDocument<I> & {
@@ -41,3 +42,10 @@ export type ChartLeaderboardDataset<I extends IDStrings = IDStrings> = (PBScoreD
user: PublicUserDocument;
};
})[];
export type UGSDataset<I extends IDStrings = IDStrings> = (UserGameStats<I> & {
__related: {
user: PublicUserDocument;
index: integer;
};
})[];
+38 -1
View File
@@ -1,5 +1,14 @@
import { Playtype } from "types/tachi";
import { GetGamePTConfig, Game, ScoreCalculatedDataLookup, IDStrings, integer } from "tachi-common";
import {
GetGamePTConfig,
Game,
ScoreCalculatedDataLookup,
IDStrings,
integer,
ChartDocument,
GamePTConfig,
} from "tachi-common";
import toast from "react-hot-toast";
export function RFA<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
@@ -127,3 +136,31 @@ export function ComposeInverseExpFn(pow: number) {
export function ComposeLogFn(base: number) {
return (x: number) => Math.log(x) / Math.log(base);
}
export function SelectRightChart(
gptConfig: GamePTConfig,
chartID: string,
charts: ChartDocument[]
) {
if (gptConfig.difficulties.includes(chartID as any)) {
for (const chart of charts) {
if (chart.difficulty === chartID && chart.isPrimary) {
return chart;
}
}
} else {
// else, its a chart ID.
for (const chart of charts) {
if (chartID === chart.chartID) {
return chart;
}
}
}
return null;
}
export function CopyToClipboard(data: unknown) {
toast.success("Copied data to clipboard.");
navigator.clipboard.writeText(JSON.stringify(data, null, "\t"));
}
+7 -4
View File
@@ -7,11 +7,12 @@ export interface SearchDirective {
value: string;
}
type DirectiveModes = "!" | ">" | ">=" | "<" | "<=" | "~";
type DirectiveModes = "$" | ">" | ">=" | "<" | "<=" | "~" | "!";
export interface Directive {
key: string;
// :! - EXACT
// :$ - EXACT
// :! - NOT
// :> - GREATER THAN
// :>= - GREATER THAN OR EQ
// :< - LESS THAN
@@ -29,7 +30,7 @@ export interface Directive {
// VALUE = /("(?:\\"|[^"])+"|[^ ]+)/ - Parses the value.
export function ParseDirectives(search: string): Directive[] {
const parsedSearch = search.matchAll(/([^ ]+):(>=|<=|!|>|<|~)?("(?:\\"|[^"])+"|[^ ]+)/gu);
const parsedSearch = search.matchAll(/([^ ]+):(>=|<=|!|\$|>|<|~)?("(?:\\"|[^"])+"|[^ ]+)/gu);
const searchDirectives = [];
for (const ps of parsedSearch) {
@@ -62,7 +63,9 @@ type MatchFn = (searchValue: string, dataValue: string | number) => boolean;
const Matchers: Record<DirectiveModes, MatchFn> = {
// Exact: Make sure this string is exactly the data value.
"!": (sv, dv) => sv === dv.toString(),
$: (sv, dv) => sv === dv.toString(),
// Not: this value is not equal to the provided value. Case insensitive.
"!": (sv, dv) => sv.toLowerCase() !== dv.toString().toLowerCase(),
// LT, LTE, GT, GTE: If the data value is a number, compare numerically. If the data value is a string
// compare alphabetically.
// equality for alphabetical is implemented as .startsWith case insensitively.