mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 16:58:25 +03:00
Merge pull request #738 from TNG-dev/zkldi/rival-pb-compare
This commit is contained in:
@@ -41,7 +41,7 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
|
||||
</SelectButton>
|
||||
<SelectButton id="score" value={mode} setValue={setMode}>
|
||||
<Icon type="sort-numeric-up-alt" />
|
||||
Score Leaderboards
|
||||
PB Leaderboards
|
||||
</SelectButton>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -53,7 +53,7 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
|
||||
<ScoreLeaderboard
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
url={`/games/${game}/${playtype}/score-leaderboard`}
|
||||
url={`/games/${game}/${playtype}/pb-leaderboard`}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
import { CreateSongMap } from "util/data";
|
||||
import Card from "components/layout/page/Card";
|
||||
import ComparePBsTable from "components/tables/rivals/ComparePBsTable";
|
||||
import ProfilePicture from "components/user/ProfilePicture";
|
||||
import UGPTRatingsTable from "components/user/UGPTStatsOverview";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import Divider from "components/util/Divider";
|
||||
import Loading from "components/util/Loading";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import useLUGPTSettings from "components/util/useLUGPTSettings";
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Col, Form, Row } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { COLOUR_SET, FolderDocument, GetGamePTConfig, PublicUserDocument } from "tachi-common";
|
||||
import { UGPTFolderReturns, UGPTStatsReturn } from "types/api-returns";
|
||||
import { GamePT } from "types/react";
|
||||
import { ComparePBsDataset } from "types/tables";
|
||||
import UserSelectModal from "components/util/modal/UserSelectModal";
|
||||
import UserIcon from "components/util/UserIcon";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
|
||||
export default function RivalCompareFolderPage({
|
||||
reqUser,
|
||||
game,
|
||||
playtype,
|
||||
folder,
|
||||
}: GamePT & {
|
||||
reqUser: PublicUserDocument;
|
||||
folder: FolderDocument;
|
||||
}) {
|
||||
const { settings } = useLUGPTSettings();
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<PublicUserDocument | null>(null);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
// honestly don't care if this errors or not
|
||||
const { data, error } = useApiQuery<Array<PublicUserDocument>>(
|
||||
`/users/${settings?.userID}/games/${game}/${playtype}/rivals`,
|
||||
undefined
|
||||
// [settings?.rivals.join(",")]
|
||||
);
|
||||
|
||||
if (!data && !error) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
let suggestUsers = data ?? [];
|
||||
|
||||
// if the user viewing this page is logged in and *is not*
|
||||
// the person this page is for
|
||||
// show them in the suggestions.
|
||||
if (user && user.id !== reqUser.id && data) {
|
||||
suggestUsers = [...suggestUsers, user];
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card header="Pick a user to compare against...">
|
||||
{suggestUsers.length > 0 && (
|
||||
<>
|
||||
<Col xs={12} className="d-flex justify-content-center flex-wrap">
|
||||
{suggestUsers.map((e) => (
|
||||
<UserIcon key={e.id} user={e} game={game} playtype={playtype}>
|
||||
<Button
|
||||
variant={
|
||||
selectedUser?.id === e.id ? "secondary" : "primary"
|
||||
}
|
||||
onClick={() => setSelectedUser(e)}
|
||||
>
|
||||
Compare Against
|
||||
</Button>
|
||||
</UserIcon>
|
||||
))}
|
||||
</Col>
|
||||
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<UserSelectModal
|
||||
callback={(user) => {
|
||||
setSelectedUser(user);
|
||||
setShow(false);
|
||||
}}
|
||||
show={show}
|
||||
setShow={setShow}
|
||||
url={`/games/${game}/${playtype}/players`}
|
||||
excludeSet={[reqUser.id]}
|
||||
excludeMsg="Can't pick the same user!"
|
||||
/>
|
||||
<Button
|
||||
className={suggestUsers.length === 0 ? "d-flex mx-auto" : ""}
|
||||
variant={selectedUser ? "secondary" : "primary"}
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
Pick{suggestUsers.length > 0 ? " Other" : ""} User
|
||||
</Button>
|
||||
</Card>
|
||||
{selectedUser !== null && folder !== null && (
|
||||
<>
|
||||
<Divider />
|
||||
<FolderCompare
|
||||
reqUser={reqUser}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
withUser={selectedUser}
|
||||
folder={folder}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FolderCompare({
|
||||
reqUser,
|
||||
game,
|
||||
playtype,
|
||||
withUser,
|
||||
folder,
|
||||
}: {
|
||||
reqUser: PublicUserDocument;
|
||||
withUser: PublicUserDocument;
|
||||
folder: FolderDocument;
|
||||
} & GamePT) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const { data: baseData, error: baseError } = useApiQuery<UGPTFolderReturns>(
|
||||
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folder.folderID}`
|
||||
);
|
||||
|
||||
const { data: compareData, error: compareError } = useApiQuery<UGPTFolderReturns>(
|
||||
`/users/${withUser.id}/games/${game}/${playtype}/folders/${folder.folderID}`
|
||||
);
|
||||
|
||||
const [shouldIncludeNotPlayed, setShouldIncludeNotPlayed] = useState(false);
|
||||
const [shouldESD, setShouldESD] = useState(gptConfig.supportsESD);
|
||||
|
||||
useEffect(() => {
|
||||
setShouldESD(gptConfig.supportsESD);
|
||||
}, [gptConfig]);
|
||||
|
||||
const dataset = useMemo(() => {
|
||||
// i *LOVE* the rules of hooks! they're so convenient!
|
||||
if (!baseData || !compareData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const basePBLookup = new Map(baseData.pbs.map((e) => [e.chartID, e]));
|
||||
const comparePBLookup = new Map(compareData.pbs.map((e) => [e.chartID, e]));
|
||||
|
||||
const songMap = CreateSongMap(baseData.songs);
|
||||
|
||||
let ds: ComparePBsDataset = baseData.charts.map((chart) => ({
|
||||
chart,
|
||||
base: basePBLookup.get(chart.chartID) ?? null,
|
||||
compare: comparePBLookup.get(chart.chartID) ?? null,
|
||||
song: songMap.get(chart.songID)!,
|
||||
}));
|
||||
|
||||
if (!shouldIncludeNotPlayed) {
|
||||
ds = ds.filter((e) => e.base && e.compare);
|
||||
}
|
||||
|
||||
return ds;
|
||||
}, [shouldIncludeNotPlayed, baseData, compareData]);
|
||||
|
||||
if (baseError) {
|
||||
return <ApiError error={baseError} />;
|
||||
}
|
||||
if (compareError) {
|
||||
return <ApiError error={compareError} />;
|
||||
}
|
||||
|
||||
if (!compareData || !baseData) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<UserCard username={reqUser.username} game={game} playtype={playtype} />
|
||||
<UserCard username={withUser.username} game={game} playtype={playtype} />
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
</Col>
|
||||
<CompareCard
|
||||
dataset={dataset}
|
||||
baseUsername={reqUser.username}
|
||||
otherUsername={withUser.username}
|
||||
/>
|
||||
<Col xs={12}>
|
||||
<Divider />
|
||||
<Form.Check
|
||||
checked={shouldIncludeNotPlayed}
|
||||
onChange={() => setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)}
|
||||
label="Include charts without plays?"
|
||||
/>
|
||||
{gptConfig.supportsESD && (
|
||||
<Form.Check
|
||||
checked={shouldESD}
|
||||
onChange={() => setShouldESD(!shouldESD)}
|
||||
label="Sort score differences intelligently? (i.e. Treat the difference between 70% and 71% as smaller than the difference between 99% and 100%)"
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
</Col>
|
||||
<ComparePBsTable
|
||||
baseUser={reqUser.username}
|
||||
compareUser={withUser.username}
|
||||
dataset={dataset}
|
||||
game={game}
|
||||
shouldESD={shouldESD}
|
||||
playtype={playtype}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function UserCard({ username, game, playtype }: { username: string } & GamePT) {
|
||||
const { data, error } = useApiQuery<UGPTStatsReturn>(
|
||||
`/users/${username}/games/${game}/${playtype}`
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <ApiError error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Col xs={12} lg={6}>
|
||||
<Card>
|
||||
<Row className="align-items-center">
|
||||
<Col lg={3}>
|
||||
<ProfilePicture user={username} />
|
||||
</Col>
|
||||
<Col lg={3}>
|
||||
<h4>
|
||||
<Link
|
||||
className="gentle-link"
|
||||
to={`/dashboard/users/${username}/games/${game}/${playtype}`}
|
||||
>
|
||||
{username}
|
||||
</Link>
|
||||
</h4>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
{data ? <UGPTRatingsTable ugs={data.gameStats} /> : <Loading />}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
function CompareCard({
|
||||
dataset,
|
||||
baseUsername,
|
||||
otherUsername,
|
||||
}: {
|
||||
dataset: ComparePBsDataset;
|
||||
|
||||
baseUsername: string;
|
||||
otherUsername: string;
|
||||
}) {
|
||||
// this code sucks
|
||||
let scoreWin = 0;
|
||||
let scoreDraw = 0;
|
||||
let scoreLoss = 0;
|
||||
let lampWin = 0;
|
||||
let lampDraw = 0;
|
||||
let lampLoss = 0;
|
||||
|
||||
for (const d of dataset) {
|
||||
if (!d.compare && !d.base) {
|
||||
scoreDraw++;
|
||||
lampDraw++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!d.compare) {
|
||||
scoreWin++;
|
||||
lampWin++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!d.base) {
|
||||
scoreLoss++;
|
||||
lampLoss++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d.base.scoreData.percent > d.compare.scoreData.percent) {
|
||||
scoreWin++;
|
||||
} else if (d.base.scoreData.percent < d.compare.scoreData.percent) {
|
||||
scoreLoss++;
|
||||
} else {
|
||||
scoreDraw++;
|
||||
}
|
||||
|
||||
if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) {
|
||||
lampWin++;
|
||||
} else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) {
|
||||
lampLoss++;
|
||||
} else {
|
||||
lampDraw++;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Col xs={12}>
|
||||
<Card>
|
||||
<Row>
|
||||
<Col xs={12} lg={6}>
|
||||
<MiniTable headers={["Score Win/Loss"]} colSpan={23}>
|
||||
<tr>
|
||||
<td>{baseUsername} Win</td>
|
||||
|
||||
<td style={{ color: COLOUR_SET.green }}>{scoreWin}</td>
|
||||
|
||||
<td style={{ color: COLOUR_SET.green }}>
|
||||
{((100 * scoreWin) / dataset.length).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Draw</td>
|
||||
<td style={{ color: COLOUR_SET.gold }}>{scoreDraw}</td>
|
||||
<td style={{ color: COLOUR_SET.gold }}>
|
||||
{((100 * scoreDraw) / dataset.length).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{otherUsername} Win</td>
|
||||
<td style={{ color: COLOUR_SET.red }}>{scoreLoss}</td>
|
||||
<td style={{ color: COLOUR_SET.red }}>
|
||||
{((100 * scoreLoss) / dataset.length).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
</MiniTable>
|
||||
</Col>
|
||||
<Col xs={12} lg={6}>
|
||||
<MiniTable headers={["Lamp Win/Loss"]} colSpan={23}>
|
||||
<tr>
|
||||
<td>{baseUsername} Win</td>
|
||||
|
||||
<td style={{ color: COLOUR_SET.green }}>{lampWin}</td>
|
||||
|
||||
<td style={{ color: COLOUR_SET.green }}>
|
||||
{((100 * lampWin) / dataset.length).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Draw</td>
|
||||
<td style={{ color: COLOUR_SET.gold }}>{lampDraw}</td>
|
||||
<td style={{ color: COLOUR_SET.gold }}>
|
||||
{((100 * lampDraw) / dataset.length).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{otherUsername} Loss</td>
|
||||
<td style={{ color: COLOUR_SET.red }}>{lampLoss}</td>
|
||||
<td style={{ color: COLOUR_SET.red }}>
|
||||
{((100 * lampLoss) / dataset.length).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
</MiniTable>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
+1
-4
@@ -71,10 +71,7 @@ export default function FoldersMainPage({ reqUser, game, playtype }: Props) {
|
||||
>
|
||||
<RecentFoldersPage {...{ reqUser, game, playtype }} />
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/folders/:folderID"
|
||||
>
|
||||
<Route path="/dashboard/users/:userID/games/:game/:playtype/folders/:folderID">
|
||||
<SpecificFolderPage {...{ reqUser, game, playtype }} />
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
+55
-31
@@ -14,11 +14,13 @@ import Icon from "components/util/Icon";
|
||||
import Loading from "components/util/Loading";
|
||||
import Muted from "components/util/Muted";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import ReferToUser from "components/util/ReferToUser";
|
||||
import SelectLinkButton from "components/util/SelectLinkButton";
|
||||
import useUGPTBase from "components/util/useUGPTBase";
|
||||
import { useFormik } from "formik";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Col, Form, Row } from "react-bootstrap";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Link, Route, Switch, useParams } from "react-router-dom";
|
||||
import {
|
||||
ChartDocument,
|
||||
ChartTierlistInfo,
|
||||
@@ -36,7 +38,7 @@ import {
|
||||
import { UGPTFolderReturns } from "types/api-returns";
|
||||
import { FolderDataset } from "types/tables";
|
||||
import { Playtype } from "types/tachi";
|
||||
import ReferToUser from "components/util/ReferToUser";
|
||||
import FolderComparePage from "./FolderComparePage";
|
||||
|
||||
interface Props {
|
||||
reqUser: PublicUserDocument;
|
||||
@@ -51,8 +53,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
|
||||
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}`
|
||||
);
|
||||
|
||||
const [mode, setMode] = useState<"normal" | "ladder" | "timeline">("normal");
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const folderDataset = useMemo(() => {
|
||||
@@ -97,6 +97,8 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
|
||||
);
|
||||
}, [folderDataset]);
|
||||
|
||||
const base = `${useUGPTBase({ reqUser, game, playtype })}/folders/${folderID}`;
|
||||
|
||||
if (error) {
|
||||
return <ApiError error={error} />;
|
||||
}
|
||||
@@ -113,47 +115,63 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
|
||||
</div>
|
||||
<div className="col-12 d-flex">
|
||||
<div className="btn-group mx-auto">
|
||||
<SelectButton value={mode} setValue={setMode} id="normal">
|
||||
<SelectLinkButton to={base}>
|
||||
<Icon type="table" />
|
||||
Normal View
|
||||
</SelectButton>
|
||||
</SelectLinkButton>
|
||||
{gptConfig.tierlists.length !== 0 &&
|
||||
// temp: tierlist view sucks for BMS and PMS
|
||||
game !== "bms" &&
|
||||
game !== "pms" && (
|
||||
<SelectButton value={mode} setValue={setMode} id="ladder">
|
||||
<SelectLinkButton to={`${base}/tierlist`}>
|
||||
<Icon type="sort-alpha-up" />
|
||||
Tierlist View
|
||||
</SelectButton>
|
||||
</SelectLinkButton>
|
||||
)}
|
||||
<SelectButton value={mode} setValue={setMode} id="timeline">
|
||||
<SelectLinkButton to={`${base}/timeline`}>
|
||||
<Icon type="stream" />
|
||||
Timeline View
|
||||
</SelectButton>
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${base}/compare`}>
|
||||
<Icon type="users" />
|
||||
Compare Against User
|
||||
</SelectLinkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<Divider />
|
||||
</div>
|
||||
<div className="col-12">
|
||||
{mode === "normal" ? (
|
||||
<FolderTable dataset={folderDataset} game={game} playtype={playtype} />
|
||||
) : mode === "ladder" ? (
|
||||
<TierlistBreakdown
|
||||
folderDataset={folderDataset}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
data={data}
|
||||
/>
|
||||
) : (
|
||||
<TimelineView
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
folderID={folderID}
|
||||
/>
|
||||
)}
|
||||
<Switch>
|
||||
<Route exact path={base}>
|
||||
<FolderTable dataset={folderDataset} game={game} playtype={playtype} />
|
||||
</Route>
|
||||
<Route exact path={`${base}/tierlist`}>
|
||||
<TierlistBreakdown
|
||||
folderDataset={folderDataset}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
data={data}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact path={`${base}/timeline`}>
|
||||
<TimelineView
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
folderID={folderID}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact path={`${base}/compare`}>
|
||||
<FolderComparePage
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
folder={data.folder}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -163,11 +181,17 @@ function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID:
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const [type, setType] = useState<"lamp" | "grade">(gptConfig.scoreBucket);
|
||||
const [value, setValue] = useState<integer>(
|
||||
type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1
|
||||
type === "grade"
|
||||
? gptConfig.grades.indexOf(gptConfig.clearGrade)
|
||||
: gptConfig.lamps.indexOf(gptConfig.clearLamp)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1);
|
||||
setValue(
|
||||
type === "grade"
|
||||
? gptConfig.grades.indexOf(gptConfig.clearGrade)
|
||||
: gptConfig.lamps.indexOf(gptConfig.clearLamp)
|
||||
);
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
import ScoreLeaderboard from "components/game/ScoreLeaderboard";
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import Divider from "components/util/Divider";
|
||||
import Icon from "components/util/Icon";
|
||||
import SelectLinkButton from "components/util/SelectLinkButton";
|
||||
import useLUGPTSettings from "components/util/useLUGPTSettings";
|
||||
import useUGPTBase from "components/util/useUGPTBase";
|
||||
import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common";
|
||||
|
||||
export default function RivalsComparePage({
|
||||
reqUser,
|
||||
game,
|
||||
playtype,
|
||||
}: {
|
||||
reqUser: PublicUserDocument;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
useSetSubheader(
|
||||
["Users", reqUser.username, "Games", gameConfig.name, playtype, "Rivals", "Compare"],
|
||||
[reqUser, game, playtype],
|
||||
`Comparing ${reqUser.username}'s ${FormatGame(game, playtype)} Rivals`
|
||||
);
|
||||
|
||||
const base = `${useUGPTBase({ reqUser, game, playtype })}/rivals/compare`;
|
||||
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (!settings) {
|
||||
return <div>You have no settings. How did you get here?</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={12} className="text-center">
|
||||
<div className="btn-group">
|
||||
<SelectLinkButton to={`${base}/top-100`}>
|
||||
<Icon type="list" />
|
||||
Compare Top 100s
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={base}>
|
||||
<Icon type="sort-amount-up" />
|
||||
Best PBs
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${base}/folders`}>
|
||||
<Icon type="folder-open" />
|
||||
Folder Comparisons
|
||||
</SelectLinkButton>
|
||||
</div>
|
||||
<Divider />
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare/top-100"
|
||||
>
|
||||
top100
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare"
|
||||
>
|
||||
<ScoreLeaderboard
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
refreshDeps={[`rivals-score-leaderboard-${settings.rivals.join(",")}`]}
|
||||
url={`/users/${reqUser.id}/games/${game}/${playtype}/rivals/score-leaderboard`}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare/folders"
|
||||
>
|
||||
folders
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
+13
-23
@@ -1,3 +1,4 @@
|
||||
import ScoreLeaderboard from "components/game/ScoreLeaderboard";
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import Divider from "components/util/Divider";
|
||||
import Icon from "components/util/Icon";
|
||||
@@ -8,7 +9,6 @@ import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { Redirect, Route, Switch } from "react-router-dom";
|
||||
import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common";
|
||||
import RivalsComparePage from "./RivalsComparePage";
|
||||
import RivalsManagePage from "./RivalsManagePage";
|
||||
|
||||
export default function RivalsMainPage({
|
||||
@@ -40,21 +40,17 @@ export default function RivalsMainPage({
|
||||
<Row>
|
||||
<Col xs={12} className="text-center">
|
||||
<div className="btn-group">
|
||||
<SelectLinkButton to={`${base}/rivals/targets`}>
|
||||
<Icon type="bullseye" />
|
||||
Goals & Quests
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton matchIfStartsWith to={`${base}/rivals/compare`}>
|
||||
<Icon type="balance-scale-left" />
|
||||
Compare
|
||||
<SelectLinkButton to={`${base}/rivals/pb-leaderboard`}>
|
||||
<Icon type="sort-amount-up" />
|
||||
Rival's Bests
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${base}/rivals`}>
|
||||
<Icon type="list" />
|
||||
Activity
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${base}/rivals/folders`}>
|
||||
<SelectLinkButton to={`${base}/rivals/tracking`}>
|
||||
<Icon type="thumbtack" />
|
||||
Manage Folders
|
||||
Manage Tracked Charts
|
||||
</SelectLinkButton>
|
||||
<SelectLinkButton to={`${base}/rivals/manage`}>
|
||||
<Icon type="users" />
|
||||
@@ -79,20 +75,14 @@ export default function RivalsMainPage({
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/folders"
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/pb-leaderboard"
|
||||
>
|
||||
NOT IMPLEMENTED YET
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/targets"
|
||||
>
|
||||
NOT IMPLEMENTED YET
|
||||
</Route>
|
||||
|
||||
<Route path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare">
|
||||
<RivalsComparePage reqUser={reqUser} game={game} playtype={playtype} />
|
||||
<ScoreLeaderboard
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
refreshDeps={[`rivals-pb-leaderboard-${settings.rivals.join(",")}`]}
|
||||
url={`/users/${reqUser.id}/games/${game}/${playtype}/rivals/pb-leaderboard`}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
|
||||
@@ -202,6 +202,7 @@ function RivalsOverviewPage({
|
||||
setShow={setShow}
|
||||
url={`/games/${game}/${playtype}/players`}
|
||||
excludeSet={rivals.map((e) => e.id)}
|
||||
excludeMsg="Already Rivaled!"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -8,20 +8,22 @@ export default function Card({
|
||||
className,
|
||||
style,
|
||||
}: {
|
||||
header: JSX.Element | string;
|
||||
header?: JSX.Element | string;
|
||||
footer?: JSX.Element | string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
} & JustChildren) {
|
||||
return (
|
||||
<div className={`card card-custom ${className ? className : ""}`} style={style}>
|
||||
<div className="card-header">
|
||||
{typeof header === "string" ? (
|
||||
<h3 className="text-center mb-0">{header}</h3>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
</div>
|
||||
{header && (
|
||||
<div className="card-header">
|
||||
{typeof header === "string" ? (
|
||||
<h3 className="text-center mb-0">{header}</h3>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-body">{children}</div>
|
||||
{footer && <div className="card-footer">{footer}</div>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export default function JubeatScoreCell({ sc }: { sc: ScoreDocument | PBScoreDocument }) {
|
||||
const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.grade}</strong>
|
||||
<br />
|
||||
<b>{sc.scoreData.percent.toFixed(2)}%</b>
|
||||
<br />
|
||||
{FormatMillions(sc.scoreData.score)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { FormatMillions } from "util/misc";
|
||||
import React from "react";
|
||||
import { COLOUR_SET, Game, PBScoreDocument, Playtype } from "tachi-common";
|
||||
import { ESDCompare } from "tachi-common/lib/esd";
|
||||
import Muted from "components/util/Muted";
|
||||
|
||||
export default function PBCompareCell({
|
||||
base,
|
||||
compare,
|
||||
game,
|
||||
playtype,
|
||||
compareType,
|
||||
shouldESD,
|
||||
}: {
|
||||
base: PBScoreDocument | null;
|
||||
compare: PBScoreDocument | null;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
compareType: "score" | "lamp";
|
||||
shouldESD: boolean;
|
||||
}) {
|
||||
let status: "win" | "draw" | "lose";
|
||||
|
||||
// wut?
|
||||
if (!base && !compare) {
|
||||
status = "draw";
|
||||
} else if (!base) {
|
||||
status = "lose";
|
||||
} else if (!compare) {
|
||||
status = "win";
|
||||
} else if (compareType === "score") {
|
||||
status = cmp(base.scoreData.score, compare.scoreData.score);
|
||||
} else {
|
||||
status = cmp(base.scoreData.lampIndex, compare.scoreData.lampIndex);
|
||||
}
|
||||
|
||||
let scoreRenderFn = (k: number) => k.toString();
|
||||
let shouldShowPercent = false;
|
||||
|
||||
switch (game) {
|
||||
case "chunithm":
|
||||
case "ddr":
|
||||
case "gitadora":
|
||||
case "maimai":
|
||||
case "usc":
|
||||
case "sdvx":
|
||||
case "popn":
|
||||
case "wacca":
|
||||
case "museca":
|
||||
scoreRenderFn = FormatMillions;
|
||||
shouldShowPercent = false;
|
||||
break;
|
||||
case "jubeat":
|
||||
scoreRenderFn = FormatMillions;
|
||||
shouldShowPercent = true;
|
||||
break;
|
||||
case "bms":
|
||||
case "iidx":
|
||||
scoreRenderFn = (k: number) => k.toString();
|
||||
shouldShowPercent = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const fmtCmpPercent = fmtNum(
|
||||
(base?.scoreData.percent ?? 0) - (compare?.scoreData.percent ?? 0),
|
||||
2,
|
||||
"%"
|
||||
);
|
||||
const cmpScore = (base?.scoreData.score ?? 0) - (compare?.scoreData.score ?? 0);
|
||||
const cmpLamp = (base?.scoreData.lampIndex ?? 0) - (compare?.scoreData.lampIndex ?? 0);
|
||||
|
||||
let esdDiff;
|
||||
|
||||
if (shouldESD) {
|
||||
if (base?.scoreData.esd && compare?.scoreData.esd) {
|
||||
esdDiff = ESDCompare(compare.scoreData.esd, base.scoreData.esd);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
color:
|
||||
status === "win"
|
||||
? COLOUR_SET.green
|
||||
: status === "draw"
|
||||
? COLOUR_SET.gold
|
||||
: COLOUR_SET.red,
|
||||
borderLeft: "1px black double",
|
||||
borderRight: "1px black double",
|
||||
}}
|
||||
>
|
||||
{compareType === "score" ? (
|
||||
<>
|
||||
<strong>{(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)}</strong>
|
||||
{shouldShowPercent && (
|
||||
<>
|
||||
<br />
|
||||
<small>({fmtCmpPercent})</small>
|
||||
</>
|
||||
)}
|
||||
{esdDiff !== undefined && (
|
||||
<>
|
||||
<br />
|
||||
{esdDiff.toFixed(2) === "0.00" ? (
|
||||
<Muted>Negligable Difference</Muted>
|
||||
) : (
|
||||
<Muted>
|
||||
Relative: {(esdDiff > 0 ? "+" : "") + esdDiff.toFixed(2)}
|
||||
</Muted>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong>{(cmpLamp > 0 ? "+" : "") + cmpLamp}</strong>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
function cmp(a: number, b: number) {
|
||||
if (a === b) {
|
||||
return "draw";
|
||||
} else if (a > b) {
|
||||
return "win";
|
||||
}
|
||||
|
||||
return "lose";
|
||||
}
|
||||
|
||||
function fmtNum(num: number, fixed?: number, suffix?: string) {
|
||||
let str = fixed ? num.toFixed(fixed) : num.toString();
|
||||
|
||||
if (num >= 0) {
|
||||
str = `+${str}`;
|
||||
}
|
||||
|
||||
if (suffix) {
|
||||
str += suffix;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { SetState } from "types/react";
|
||||
import { ZTableTHProps } from "./TachiTable";
|
||||
|
||||
export default function SelectableCompareType({
|
||||
compareType,
|
||||
setCompareType,
|
||||
changeSort,
|
||||
currentSortMode,
|
||||
reverseSort,
|
||||
}: {
|
||||
compareType: "score" | "lamp";
|
||||
setCompareType: SetState<"score" | "lamp">;
|
||||
} & ZTableTHProps) {
|
||||
return (
|
||||
<th>
|
||||
<select
|
||||
onChange={(v) => setCompareType(v.target.value as "score" | "lamp")}
|
||||
value={compareType}
|
||||
style={{
|
||||
backgroundColor: "#131313",
|
||||
border: "none",
|
||||
color: "#ffffff",
|
||||
fontSize: "inherit",
|
||||
font: "inherit",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<option value="score">Vs. (Score)</option>
|
||||
<option value="lamp">Vs. (Lamp)</option>
|
||||
</select>
|
||||
<br />
|
||||
<span onClick={() => changeSort("Vs.")}>
|
||||
<i
|
||||
className={`flaticon2-arrow-up icon-sm sort-icon ${
|
||||
currentSortMode === "Vs." && reverseSort ? "active" : ""
|
||||
}`}
|
||||
></i>
|
||||
<i
|
||||
className={`flaticon2-arrow-down icon-sm sort-icon ${
|
||||
currentSortMode === "Vs." && !reverseSort ? "active" : ""
|
||||
}`}
|
||||
></i>
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import CHUNITHMJudgementCell from "../cells/CHUNITHMJudgementCell";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import MillionsScoreCell from "../cells/MillionsScoreCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
|
||||
export default function CHUNITHMCoreCells({
|
||||
@@ -13,19 +12,9 @@ export default function CHUNITHMCoreCells({
|
||||
sc: ScoreDocument<"chunithm:Single"> | PBScoreDocument<"chunithm:Single">;
|
||||
rating: keyof ScoreDocument["calculatedData"];
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
|
||||
|
||||
return (
|
||||
<>
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.grade}</strong>
|
||||
<br />
|
||||
{FormatMillions(sc.scoreData.score)}
|
||||
</td>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<CHUNITHMJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import JubeatJudgementCell from "../cells/JubeatJudgementCell";
|
||||
import JubeatScoreCell from "../cells/JubeatScoreCell";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
|
||||
@@ -13,21 +12,9 @@ export default function JubeatCoreCells({
|
||||
sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">;
|
||||
rating: keyof ScoreDocument["calculatedData"];
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
|
||||
|
||||
return (
|
||||
<>
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.grade}</strong>
|
||||
<br />
|
||||
<b>{sc.scoreData.percent.toFixed(2)}%</b>
|
||||
<br />
|
||||
{FormatMillions(sc.scoreData.score)}
|
||||
</td>
|
||||
<JubeatScoreCell sc={sc} />
|
||||
<JubeatJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import IIDXLampCell from "components/tables/cells/IIDXLampCell";
|
||||
import JubeatScoreCell from "components/tables/cells/JubeatScoreCell";
|
||||
import LampCell from "components/tables/cells/LampCell";
|
||||
import MillionsScoreCell from "components/tables/cells/MillionsScoreCell";
|
||||
import PopnLampCell from "components/tables/cells/PopnLampCell";
|
||||
import ScoreCell from "components/tables/cells/ScoreCell";
|
||||
import SDVXLampCell from "components/tables/cells/SDVXLampCell";
|
||||
import React from "react";
|
||||
import { ChartDocument, Game, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
|
||||
import BMSCoreCells from "./BMSCoreCells";
|
||||
|
||||
export default function ShortScoreCoreCells({
|
||||
game,
|
||||
score,
|
||||
chart,
|
||||
}: {
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
chart: ChartDocument;
|
||||
game: Game;
|
||||
}) {
|
||||
const sc = score as any; // lazy hack
|
||||
|
||||
if (game === "iidx") {
|
||||
return (
|
||||
<>
|
||||
<ScoreCell score={sc} />
|
||||
<IIDXLampCell sc={sc} chart={chart as ChartDocument<"iidx:SP" | "iidx:DP">} />
|
||||
</>
|
||||
);
|
||||
} else if (game === "bms" || game === "pms") {
|
||||
return (
|
||||
<>
|
||||
<ScoreCell score={sc} />
|
||||
<BMSOrPMSLampCell score={sc} />
|
||||
</>
|
||||
);
|
||||
} else if (
|
||||
game === "sdvx" ||
|
||||
game === "usc" ||
|
||||
game === "museca" ||
|
||||
game === "wacca" ||
|
||||
game === "chunithm" ||
|
||||
game === "ddr" ||
|
||||
game === "gitadora"
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<SDVXLampCell score={sc} />
|
||||
</>
|
||||
);
|
||||
} else if (game === "maimai") {
|
||||
return (
|
||||
<>
|
||||
<ScoreCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
</>
|
||||
);
|
||||
} else if (game === "jubeat") {
|
||||
return (
|
||||
<>
|
||||
<JubeatScoreCell sc={sc} />
|
||||
<LampCell score={sc} />
|
||||
</>
|
||||
);
|
||||
} else if (game === "popn") {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<PopnLampCell score={sc} />
|
||||
</>
|
||||
);
|
||||
} else if (game === "itg") {
|
||||
return (
|
||||
<>
|
||||
<ScoreCell showScore={false} score={sc} />
|
||||
<LampCell score={sc} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <>No cells defined for {game}. Report this!</>;
|
||||
}
|
||||
@@ -10,13 +10,19 @@ import {
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
import { SetState } from "types/react";
|
||||
import { FolderDataset, PBDataset, RivalChartDataset, ScoreDataset } from "types/tables";
|
||||
import {
|
||||
ComparePBsDataset,
|
||||
FolderDataset,
|
||||
PBDataset,
|
||||
RivalChartDataset,
|
||||
ScoreDataset,
|
||||
} from "types/tables";
|
||||
import { Playtype } from "types/tachi";
|
||||
import SelectableRating from "../components/SelectableRating";
|
||||
import { Header, ZTableTHProps } from "../components/TachiTable";
|
||||
|
||||
export function GetGPTCoreHeaders<
|
||||
Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset
|
||||
Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset | ComparePBsDataset
|
||||
>(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import { CreatePBCompareSearchParams } from "util/tables/create-search";
|
||||
import React, { useState } from "react";
|
||||
import { Game, IDStrings, Playtype } from "tachi-common";
|
||||
import { ESDCompare } from "tachi-common/lib/esd";
|
||||
import { ComparePBsDataset } from "types/tables";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import PBCompareCell from "../cells/PBCompareCell";
|
||||
import TitleCell from "../cells/TitleCell";
|
||||
import SelectableCompareType from "../components/SelectableCompareType";
|
||||
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
|
||||
import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells";
|
||||
import ChartHeader from "../headers/ChartHeader";
|
||||
|
||||
export default function ComparePBsTable<I extends IDStrings = IDStrings>({
|
||||
dataset,
|
||||
game,
|
||||
playtype,
|
||||
baseUser,
|
||||
compareUser,
|
||||
shouldESD,
|
||||
}: {
|
||||
dataset: ComparePBsDataset<I>;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
baseUser: string;
|
||||
compareUser: string;
|
||||
shouldESD: boolean;
|
||||
}) {
|
||||
const [compareType, setCompareType] = useState<"score" | "lamp">("score");
|
||||
|
||||
const headers: Header<ComparePBsDataset[0]>[] = [
|
||||
ChartHeader(game, (d) => d.chart),
|
||||
["Song", "Song", StrSOV((x) => x.song.title)],
|
||||
["", "", () => 1, () => <td colSpan={2}>{baseUser}</td>],
|
||||
[
|
||||
"Vs.",
|
||||
"Vs.",
|
||||
NumericSOV((x) => {
|
||||
if (!x.base) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
if (!x.compare) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
if (compareType === "score") {
|
||||
if (shouldESD && x.base.scoreData.esd && x.compare.scoreData.esd) {
|
||||
// esd is inverted, whoops
|
||||
const e = ESDCompare(x.compare.scoreData.esd, x.base.scoreData.esd);
|
||||
|
||||
if (e !== 0) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
return x.base.scoreData.percent - x.compare.scoreData.percent;
|
||||
}
|
||||
|
||||
return x.base.scoreData.lampIndex - x.compare.scoreData.lampIndex;
|
||||
}),
|
||||
(thProps: ZTableTHProps) => (
|
||||
<SelectableCompareType
|
||||
key={compareType}
|
||||
compareType={compareType}
|
||||
setCompareType={setCompareType}
|
||||
{...thProps}
|
||||
/>
|
||||
),
|
||||
],
|
||||
["", "", () => 1, () => <td colSpan={2}>{compareUser}</td>],
|
||||
];
|
||||
|
||||
return (
|
||||
<TachiTable
|
||||
// very funny way of getting the table to re-render when shouldESD is changed
|
||||
key={`${shouldESD}`}
|
||||
dataset={dataset}
|
||||
headers={headers}
|
||||
entryName="Charts"
|
||||
defaultReverseSort
|
||||
defaultSortMode="Vs."
|
||||
searchFunctions={CreatePBCompareSearchParams(game, playtype)}
|
||||
rowFunction={(data) => (
|
||||
<Row data={data} game={game} compareType={compareType} shouldESD={shouldESD} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Row<I extends IDStrings = IDStrings>({
|
||||
data,
|
||||
game,
|
||||
compareType,
|
||||
shouldESD,
|
||||
}: {
|
||||
data: ComparePBsDataset<I>[0];
|
||||
game: Game;
|
||||
compareType: "score" | "lamp";
|
||||
shouldESD: boolean;
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
<DifficultyCell chart={data.chart} game={game} alwaysShort />
|
||||
<TitleCell game={game} song={data.song} chart={data.chart} />
|
||||
{data.base ? (
|
||||
<ShortScoreCoreCells score={data.base} game={game} chart={data.chart} />
|
||||
) : (
|
||||
<td colSpan={2}>Not Played</td>
|
||||
)}
|
||||
<PBCompareCell
|
||||
shouldESD={shouldESD}
|
||||
base={data.base}
|
||||
compare={data.compare}
|
||||
game={game}
|
||||
playtype={data.chart.playtype}
|
||||
compareType={compareType}
|
||||
/>
|
||||
{data.compare ? (
|
||||
<ShortScoreCoreCells score={data.compare} game={game} chart={data.chart} />
|
||||
) : (
|
||||
<td colSpan={2}>Not Played</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,28 @@ import { ToAPIURL } from "util/api";
|
||||
import React from "react";
|
||||
import { PublicUserDocument } from "tachi-common";
|
||||
|
||||
export default function ProfilePicture({ user, src }: { user: PublicUserDocument; src?: string }) {
|
||||
export default function ProfilePicture({
|
||||
user,
|
||||
src,
|
||||
}: {
|
||||
user: PublicUserDocument | string;
|
||||
src?: string;
|
||||
}) {
|
||||
if (typeof user === "string") {
|
||||
return (
|
||||
<img
|
||||
src={src ? src : ToAPIURL(`/users/${user}/pfp`)}
|
||||
alt={`${user}'s Profile Picture`}
|
||||
className="rounded"
|
||||
style={{
|
||||
width: "128px",
|
||||
height: "128px",
|
||||
boxShadow: "0px 0px 10px 0px #000000",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src ? src : ToAPIURL(`/users/${user.id}/pfp`)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import NavItem from "components/nav/NavItem";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import Divider from "components/util/Divider";
|
||||
import React from "react";
|
||||
import { Game, GetGamePTConfig, PublicUserDocument } from "tachi-common";
|
||||
import { Game, PublicUserDocument } from "tachi-common";
|
||||
import { UGPTStatsReturn } from "types/api-returns";
|
||||
import { Playtype } from "types/tachi";
|
||||
import ProfileBadges from "./ProfileBadges";
|
||||
@@ -100,16 +100,16 @@ export function UGPTBottomNav({
|
||||
<NavItem key="leaderboard" to={`${baseUrl}/leaderboard`}>
|
||||
Leaderboard
|
||||
</NavItem>,
|
||||
<NavItem key="rivals" to={`${baseUrl}/rivals`}>
|
||||
Rivals
|
||||
</NavItem>,
|
||||
<NavItem key="targets" to={`${baseUrl}/targets`}>
|
||||
Goals & Quests
|
||||
</NavItem>,
|
||||
];
|
||||
|
||||
if (isRequestedUser) {
|
||||
navItems.push(
|
||||
<NavItem key="rivals" to={`${baseUrl}/rivals`}>
|
||||
Rivals
|
||||
</NavItem>,
|
||||
<NavItem key="targets" to={`${baseUrl}/targets`}>
|
||||
Goals & Quests
|
||||
</NavItem>,
|
||||
<NavItem key="settings" to={`${baseUrl}/settings`}>
|
||||
Settings
|
||||
</NavItem>
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function UserIcon({
|
||||
children,
|
||||
game,
|
||||
playtype,
|
||||
}: { user: PublicUserDocument } & JustChildren & Partial<GamePT>) {
|
||||
}: { user: PublicUserDocument } & Partial<JustChildren> & Partial<GamePT>) {
|
||||
return (
|
||||
<div className="text-center p-8">
|
||||
<ProfilePicture user={user} />
|
||||
@@ -25,7 +25,7 @@ export default function UserIcon({
|
||||
{user.username}
|
||||
</Link>
|
||||
</h4>
|
||||
<div className="d-flex justify-content-center">{children}</div>
|
||||
{children && <div className="d-flex justify-content-center">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import ProfilePicture from "components/user/ProfilePicture";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Col, Modal, Row } from "react-bootstrap";
|
||||
import { integer, PublicUserDocument } from "tachi-common";
|
||||
import { SetState } from "types/react";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import DebounceSearch from "../DebounceSearch";
|
||||
import Divider from "../Divider";
|
||||
|
||||
@@ -14,12 +13,14 @@ export default function UserSelectModal({
|
||||
setShow,
|
||||
url = `/users`,
|
||||
excludeSet = [],
|
||||
excludeMsg = "N/A",
|
||||
}: {
|
||||
show: boolean;
|
||||
setShow: SetState<boolean>;
|
||||
callback: (user: PublicUserDocument) => void;
|
||||
url?: string;
|
||||
excludeSet?: Array<integer>;
|
||||
excludeMsg?: string;
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [users, setUsers] = useState<Array<PublicUserDocument> | null>(null);
|
||||
@@ -67,7 +68,7 @@ export default function UserSelectModal({
|
||||
<h4 className="mt-2">{user.username}</h4>
|
||||
{excludeSet.includes(user.id) ? (
|
||||
<Button variant="outline-secondary" disabled>
|
||||
Already Rivalled!
|
||||
{excludeMsg}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -121,6 +121,12 @@ export interface UGPTFolderReturns<I extends IDStrings = IDStrings> {
|
||||
pbs: PBScoreDocument<I>[];
|
||||
}
|
||||
|
||||
export interface GPTFolderReturns<I extends IDStrings = IDStrings> {
|
||||
folder: FolderDocument;
|
||||
songs: SongDocument<IDStringToGame[I]>[];
|
||||
charts: ChartDocument<I>[];
|
||||
}
|
||||
|
||||
export interface GPTStatsReturn {
|
||||
config: GamePTConfig;
|
||||
playerCount: integer;
|
||||
|
||||
@@ -56,3 +56,10 @@ export type RivalChartDataset<I extends IDStrings = IDStrings> = (PublicUserDocu
|
||||
index: number;
|
||||
};
|
||||
})[];
|
||||
|
||||
export type ComparePBsDataset<I extends IDStrings = IDStrings> = Array<{
|
||||
base: PBScoreDocument<I> | null;
|
||||
compare: PBScoreDocument<I> | null;
|
||||
chart: ChartDocument;
|
||||
song: SongDocument;
|
||||
}>;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
|
||||
import { ValueGetterOrHybrid } from "util/ztable/search";
|
||||
import {
|
||||
BMS_TABLES,
|
||||
ChartDocument,
|
||||
Game,
|
||||
GamePTConfig,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
BMS_TABLES,
|
||||
} from "tachi-common";
|
||||
import { FolderDataset, PBDataset, ScoreDataset } from "types/tables";
|
||||
import { ComparePBsDataset, FolderDataset, PBDataset, ScoreDataset } from "types/tables";
|
||||
import { Playtype } from "types/tachi";
|
||||
|
||||
function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string) {
|
||||
@@ -88,6 +88,24 @@ export function CreateDefaultPBSearchParams<I extends IDStrings = IDStrings>(
|
||||
return searchFunctions;
|
||||
}
|
||||
|
||||
export function CreatePBCompareSearchParams<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
) {
|
||||
const searchFunctions: Record<string, ValueGetterOrHybrid<ComparePBsDataset<I>[0]>> = {
|
||||
artist: (x) => x.song.artist,
|
||||
title: (x) => x.song.title,
|
||||
difficulty: (x) => x.chart.difficulty,
|
||||
level: (x) => x.chart.levelNum,
|
||||
};
|
||||
|
||||
if (game === "bms") {
|
||||
HandleBMSNonsense(searchFunctions, playtype, (k) => k);
|
||||
}
|
||||
|
||||
return searchFunctions;
|
||||
}
|
||||
|
||||
export function CreateDefaultFolderSearchParams<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
|
||||
@@ -507,9 +507,9 @@ GET /api/v1/games/bms/7K/tableID/insane
|
||||
|
||||
*****
|
||||
|
||||
## Retrieve the score leaderboard for this Game.
|
||||
## Retrieve the PB leaderboard for this Game.
|
||||
|
||||
`GET /api/v1/games/:game/:playtype/score-leaderboard`
|
||||
`GET /api/v1/games/:game/:playtype/pb-leaderboard`
|
||||
|
||||
### Parameters
|
||||
|
||||
@@ -522,7 +522,7 @@ GET /api/v1/games/bms/7K/tableID/insane
|
||||
|
||||
| Property | Type | Description |
|
||||
| :: | :: | :: |
|
||||
| `pbs` | Array<PBDocument> | The array of pbs part of the score leaderboard. |
|
||||
| `pbs` | Array<PBDocument> | The array of pbs part of the PB leaderboard. |
|
||||
| `songs` | Array<[SongDocument](../../schemas/song.md)> | The array of songs part of the PBs. |
|
||||
| `charts` | Array<[ChartDocument](../../schemas/chart.md)> | The array of charts part of the PBs. |
|
||||
| `users` | Array<[UserDocument](../../schemas/user.md)> | The array of users part of the PBs. |
|
||||
|
||||
@@ -153,9 +153,9 @@ router.get("/leaderboard", async (req, res) => {
|
||||
* @param alg - An alternative algorithm to use instead of the gpts default.
|
||||
* @param limit - How many scores to return.
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype/score-leaderboard
|
||||
* @name GET /api/v1/games/:game/:playtype/pb-leaderboard
|
||||
*/
|
||||
router.get("/score-leaderboard", async (req, res) => {
|
||||
router.get("/pb-leaderboard", async (req, res) => {
|
||||
const { game, playtype } = GetGPT(req);
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { GetRivalUsers } from "lib/rivals/rivals";
|
||||
import { SearchSpecificGameSongsAndCharts } from "lib/search/search";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { AggressiveRateLimitMiddleware } from "server/middleware/rate-limiter";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { GetRelevantSongsAndCharts } from "utils/db";
|
||||
@@ -97,7 +98,7 @@ router.get("/all", AggressiveRateLimitMiddleware, async (req, res) => {
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/best
|
||||
*/
|
||||
router.get("/best", async (req, res) => {
|
||||
router.get("/best", prValidate({ alg: "*string" }), async (req, res) => {
|
||||
const { user, game, playtype } = GetUGPT(req);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
+3
-3
@@ -103,7 +103,7 @@ router.get("/challengers", async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve a "score leaderboard" for this user's set of rivals.
|
||||
* Retrieve a "PB leaderboard" for this user's set of rivals.
|
||||
*
|
||||
* This is - effectively - the best 100 scores from this set of users on the given
|
||||
* rating algorithm.
|
||||
@@ -111,9 +111,9 @@ router.get("/challengers", async (req, res) => {
|
||||
* @param alg - The score rating algorithm to sort on. Defaults to whatever the GPTConfig
|
||||
* default is.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/rivals/score-leaderboard
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/rivals/pb-leaderboard
|
||||
*/
|
||||
router.get("/score-leaderboard", async (req, res) => {
|
||||
router.get("/pb-leaderboard", async (req, res) => {
|
||||
const { user, game, playtype } = GetUGPT(req);
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user