checkpoint: insanity ascends (35)

127 errors to go
This commit is contained in:
zkldi
2023-01-19 16:39:42 +00:00
parent 2b0e65f0c8
commit 5cbb2d5f8a
131 changed files with 2106 additions and 1886 deletions
@@ -199,8 +199,8 @@ function UserGameStatsInfo({ user }: { user: UserDocument }) {
const gameConfig = GetGameConfig(a.game);
return (
gameConfig.validPlaytypes.indexOf(a.playtype) -
gameConfig.validPlaytypes.indexOf(b.playtype)
gameConfig.playtypes.indexOf(a.playtype) -
gameConfig.playtypes.indexOf(b.playtype)
);
}
@@ -4,15 +4,20 @@ import { SelectRightChart } from "util/misc";
import { MillisToSince } from "util/time";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import LampCell from "components/tables/cells/LampCell";
import ScoreCell from "components/tables/cells/ScoreCell";
import MiniTable from "components/tables/components/MiniTable";
import TargetInfo from "components/tables/dropdowns/components/TargetInfo";
import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells";
import PBTable from "components/tables/pbs/PBTable";
import ProfilePicture from "components/user/ProfilePicture";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import Muted from "components/util/Muted";
import SelectLinkButton from "components/util/SelectLinkButton";
import useApiQuery from "components/util/query/useApiQuery";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import { TargetsContext } from "context/TargetsContext";
import { UserContext } from "context/UserContext";
import React, { useContext, useMemo, useState } from "react";
import { Col, Row } from "react-bootstrap";
@@ -23,10 +28,10 @@ import {
FormatDifficulty,
GetGameConfig,
GetGamePTConfig,
integer,
PBScoreDocument,
UserDocument,
SongDocument,
UserDocument,
integer,
} from "tachi-common";
import {
ChartPBLeaderboardReturn,
@@ -36,12 +41,6 @@ import {
} from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { PBDataset } from "types/tables";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import TargetInfo from "components/tables/dropdowns/components/TargetInfo";
import useApiQuery from "components/util/query/useApiQuery";
import Divider from "components/util/Divider";
import { TargetsContext } from "context/TargetsContext";
import SelectLinkButton from "components/util/SelectLinkButton";
// This component forms a wrapper around the Real GPT Chart Page
// which handles the case where activeChart == null.
@@ -206,7 +205,7 @@ function InternalGPTChartPage({
)}
</div>
</Col>
<TopShowcase data={data} user={user} userMap={userMap} />
<TopShowcase data={data} user={user} userMap={userMap} chart={chart} />
<Col xs={12} className="mt-4">
<Switch>
<Route exact path="/games/:game/:playtype/songs/:songID/:difficulty/targets">
@@ -375,10 +374,12 @@ function TopShowcase({
data,
user,
userMap,
chart,
}: {
data: ChartPBData;
user: UserDocument | null;
userMap: Map<integer, UserDocument>;
chart: ChartDocument;
}) {
// We have a couple of conditions.
// User is #1: col-12 #1,
@@ -392,7 +393,7 @@ function TopShowcase({
if (user?.id === bestPlay.userID) {
return (
<Col xs={12}>
<PlayCard name="Best PB" pb={bestPlay} user={bestUser} />
<PlayCard name="Best PB" pb={bestPlay} user={bestUser} chart={chart} />
</Col>
);
}
@@ -401,7 +402,7 @@ function TopShowcase({
return (
<>
<Col xs={12} lg={6}>
<PlayCard name="Best PB" pb={bestPlay} user={bestUser} />
<PlayCard name="Best PB" pb={bestPlay} user={bestUser} chart={chart} />
</Col>
<Col xs={12} lg={6}>
<Card header="Your PB" className="text-center">
@@ -417,16 +418,26 @@ function TopShowcase({
return (
<>
<Col xs={12} lg={6}>
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} chart={chart} />
</Col>
<Col xs={12} lg={6}>
<PlayCard name="Your PB" pb={thisUsersPlay} user={user!} />
<PlayCard name="Your PB" pb={thisUsersPlay} user={user!} chart={chart} />
</Col>
</>
);
}
function PlayCard({ pb, user, name }: { pb: PBScoreDocument; user: UserDocument; name: string }) {
function PlayCard({
pb,
user,
name,
chart,
}: {
pb: PBScoreDocument;
user: UserDocument;
name: string;
chart: ChartDocument;
}) {
return (
<Card header={name}>
<Row className="align-items-center">
@@ -447,12 +458,7 @@ function PlayCard({ pb, user, name }: { pb: PBScoreDocument; user: UserDocument;
</Col>
<Col lg={6}>
<MiniTable headers={["PB Info"]}>
<tr>
<ScoreCell score={pb} />
</tr>
<tr>
<LampCell score={pb} />
</tr>
<ScoreCoreCells game={pb.game} chart={chart} score={pb} />
</MiniTable>
<div className="text-center">
<Muted>
@@ -4,8 +4,7 @@ import Card from "components/layout/page/Card";
import MiniTable from "components/tables/components/MiniTable";
import DebugContent from "components/util/DebugContent";
import React from "react";
import { FormatGame, GetGameConfig, GetGamePTConfig, IDStrings, gameClasses } from "tachi-common";
import { GameClassSets } from "tachi-common/game-classes";
import { Classes, FormatGame, GetGameConfig, GetGamePTConfig, GPTString } from "tachi-common";
import { GamePT } from "types/react";
export default function GPTDevInfo({ game, playtype }: GamePT) {
@@ -28,16 +27,16 @@ export default function GPTDevInfo({ game, playtype }: GamePT) {
</Card>
<Card className="mt-4" header="Class Badges">
<div className="d-flex w-100 justify-content-center" style={{ gap: "30px" }}>
{Object.entries(gptConfig.classHumanisedFormat).map(([classSet, value]) => (
{Object.entries(gptConfig.classes).map(([classSet, conf]) => (
<div>
<MiniTable headers={[classSet]} colSpan={2}>
{value.map((e, i) => (
{conf.values.map((e) => (
<tr key={e.id}>
<td>{e.id}</td>
<td>
<ClassBadge
classSet={classSet as GameClassSets[IDStrings]}
classValue={i}
classSet={classSet as Classes[GPTString]}
classValue={e.id}
game={game}
playtype={playtype}
/>
@@ -17,7 +17,7 @@ import SelectButton from "components/util/SelectButton";
import { useProfileRatingAlg } from "components/util/useScoreRatingAlg";
import React, { useState } from "react";
import { Col, Form, Row } from "react-bootstrap";
import { FormatGame, GetGameConfig, GetGamePTConfig } from "tachi-common";
import { AnyProfileRatingAlg, FormatGame, GetGameConfig, GetGamePTConfig } from "tachi-common";
import { UserLeaderboardReturns } from "types/api-returns";
import { GamePT } from "types/react";
import { UGSDataset } from "types/tables";
@@ -69,9 +69,9 @@ function ProfileLeaderboard({ game, playtype }: GamePT) {
const [alg, setAlg] = useState(defaultAlg);
const SelectComponent =
gptConfig.profileRatingAlgs.length > 1 ? (
Object.keys(gptConfig.profileRatingAlgs).length > 1 ? (
<Form.Control as="select" value={alg} onChange={(e) => setAlg(e.target.value as any)}>
{gptConfig.profileRatingAlgs.map((e) => (
{Object.keys(gptConfig.profileRatingAlgs).map((e) => (
<option key={e} value={e}>
{UppercaseFirst(e)}
</option>
@@ -125,7 +125,7 @@ function ProfileLeaderboard({ game, playtype }: GamePT) {
headers={[
["Ranking", "Rank", NumericSOV((x) => x.__related.index)],
["User", "User", StrSOV((x) => x.__related.user.username)],
...gptConfig.profileRatingAlgs.map(
...(Object.keys(gptConfig.profileRatingAlgs) as Array<AnyProfileRatingAlg>).map(
(e) =>
[
UppercaseFirst(e),
@@ -139,11 +139,13 @@ function ProfileLeaderboard({ game, playtype }: GamePT) {
<tr>
<IndexCell index={r.__related.index} />
<UserCell game={game} playtype={playtype} user={r.__related.user} />
{gptConfig.profileRatingAlgs.map((e) => (
{(
Object.keys(gptConfig.profileRatingAlgs) as Array<AnyProfileRatingAlg>
).map((e) => (
<td key={e}>
{r.ratings[e]
? gptConfig.profileRatingAlgFormatters[e]
? gptConfig.profileRatingAlgFormatters[e]!(r.ratings[e]!)
? gptConfig.profileRatingAlgs[e].formatter
? gptConfig.profileRatingAlgs[e].formatter!(r.ratings[e]!)
: r.ratings[e]!.toFixed(2)
: "No Data."}
</td>
@@ -152,15 +154,18 @@ function ProfileLeaderboard({ game, playtype }: GamePT) {
{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}
/>
))
Object.entries(r.classes).map(
([k, v]) =>
v && (
<ClassBadge
key={k}
classSet={k as keyof typeof r.classes}
game={game}
playtype={playtype}
classValue={v}
/>
)
)
)}
</td>
</tr>
@@ -20,6 +20,7 @@ import {
SongDocument,
} from "tachi-common";
import { GamePT } from "types/react";
import { CascadingRatingValue } from "components/tables/headers/ChartHeader";
export default function GPTSongsPage({ game, playtype }: GamePT) {
useSetSubheader(
@@ -90,19 +91,7 @@ function SearchSongsTable({ game, playtype, search }: { search: string } & GameP
entryName="Charts"
dataset={dataset}
headers={[
[
"Chart",
"Chart",
NumericSOV((chart) => {
for (const tierlist of gptConfig.tierlists) {
if (chart.tierlistInfo[tierlist]) {
return chart.tierlistInfo[tierlist]!.value;
}
}
return chart.levelNum;
}),
],
["Chart", "Chart", (a, b) => CascadingRatingValue(game, a, b)],
["Song Title", "Song", StrSOV((x) => x.__related.song.title)],
["Playcount", "Playcount", NumericSOV((x) => x.__playcount)],
]}
@@ -16,10 +16,10 @@ import {
FormatGame,
GetGameConfig,
GetGamePTConfig,
IDStrings,
GPTString,
integer,
UserDocument,
ProfileRatingLookup,
ProfileRatingAlgorithms,
UserGameStats,
} from "tachi-common";
import { GameClassSets } from "tachi-common/game-classes";
@@ -84,8 +84,8 @@ function LeaderboardsPageContent({
}: {
reqUser: UserDocument;
data: LeaderboardsData;
alg: ProfileRatingLookup[IDStrings];
setAlg: SetState<ProfileRatingLookup[IDStrings]>;
alg: ProfileRatingAlgorithms[GPTString];
setAlg: SetState<ProfileRatingAlgorithms[GPTString]>;
} & GamePT) {
const { stats, leaderboard } = data;
@@ -142,7 +142,7 @@ function LeaderboardsPageContent({
<ClassBadge
key={`${k}:${v}`}
classValue={v}
classSet={k as GameClassSets[IDStrings]}
classSet={k as GameClassSets[GPTString]}
game={game}
playtype={playtype}
/>
@@ -167,7 +167,7 @@ function UserHistory({
setRating(e.target.value as keyof UserGameStats["ratings"])
}
>
{gptConfig.profileRatingAlgs.map((e) => (
{Object.keys(gptConfig.profileRatingAlgs).map((e) => (
<option key={e} value={e}>
{UppercaseFirst(e)}
</option>
@@ -219,7 +219,7 @@ function UserHistory({
setRating(e.target.value as keyof UserGameStats["ratings"])
}
>
{gptConfig.profileRatingAlgs.map((e) => (
{Object.keys(gptConfig.profileRatingAlgs).map((e) => (
<option key={e} value={e}>
{UppercaseFirst(e)}
</option>
@@ -18,10 +18,10 @@ import {
GamePTConfig,
GetGameConfig,
GetGamePTConfig,
IDStrings,
GPTString,
PBScoreDocument,
UserDocument,
ScoreCalculatedDataLookup,
ScoreRatingAlgorithms,
ScoreDocument,
SongDocument,
UnsuccessfulAPIResponse,
@@ -77,7 +77,7 @@ export default function ScoresPage({
<Switch>
<Route exact path="/u/:userID/games/:game/:playtype/scores">
<>
{gptConfig.scoreRatingAlgs.length > 1 && (
{Object.keys(gptConfig.scoreRatingAlgs).length > 1 && (
<AlgSelector {...{ alg, setAlg, gptConfig }} />
)}
<PBsOverview
@@ -120,8 +120,8 @@ function AlgSelector({
setAlg,
}: {
gptConfig: GamePTConfig;
alg: ScoreCalculatedDataLookup[IDStrings];
setAlg: SetState<ScoreCalculatedDataLookup[IDStrings]>;
alg: ScoreRatingAlgorithms[GPTString];
setAlg: SetState<ScoreRatingAlgorithms[GPTString]>;
}) {
return (
<div className="row justify-content-center mb-4">
@@ -132,7 +132,7 @@ function AlgSelector({
value={alg}
onChange={(e) => setAlg(e.target.value as any)}
>
{gptConfig.scoreRatingAlgs.map((e) => (
{Object.keys(gptConfig.scoreRatingAlgs).map((e) => (
<option key={e}>{e}</option>
))}
</select>
@@ -172,7 +172,7 @@ function PBsOverview({
url: string;
indexCol?: boolean;
showPlaycount?: boolean;
alg?: ScoreCalculatedDataLookup[IDStrings];
alg?: ScoreRatingAlgorithms[GPTString];
} & GamePT) {
const [search, setSearch] = useState("");
@@ -212,9 +212,9 @@ function PBsOverview({
function FormatData<
D extends PBScoreDocument | ScoreDocument,
I extends IDStrings = IDStrings,
GPT extends GPTString = GPTString,
G extends Game = Game
>(d: D[], songs: SongDocument<G>[], charts: ChartDocument<I>[], reqUser: UserDocument) {
>(d: D[], songs: SongDocument<G>[], charts: ChartDocument<GPT>[], reqUser: UserDocument) {
const songMap = new Map();
const chartMap = new Map();
@@ -266,7 +266,7 @@ function PBsSearch({
}: {
reqUser: UserDocument;
search: string;
alg?: ScoreCalculatedDataLookup[IDStrings];
alg?: ScoreRatingAlgorithms[GPTString];
} & GamePT) {
const { data, error } = useFetchPBs(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs?search=${search}`,
@@ -135,7 +135,7 @@ function SearchSessionsTable({
__related: {
index: i,
},
}));
})) as SessionDataset;
}
);
@@ -27,7 +27,7 @@ import {
GetGamePTConfig,
ShowcaseStatDetails,
TableDocument,
UGPTSettings,
UGPTSettingsDocument,
UserDocument,
} from "tachi-common";
import { SetState, UGPT } from "types/react";
@@ -109,7 +109,8 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
gameSpecific: settings!.preferences.gameSpecific as any,
defaultTable: settings!.preferences.defaultTable,
scoreBucket: settings!.preferences.scoreBucket ?? gptConfig.scoreBucket,
preferredDefaultEnum:
settings!.preferences.preferredDefaultEnum ?? gptConfig.preferredDefaultEnum,
preferredRanking: settings!.preferences.preferredRanking ?? "global",
},
onSubmit: async (values) => {
@@ -129,7 +130,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
if (rj.success) {
setLoggedInData({
...loggedInData,
settings: deepmerge(settings as UGPTSettings, { preferences: values }),
settings: deepmerge(settings as UGPTSettingsDocument, { preferences: values }),
});
}
},
@@ -153,7 +154,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
return (
<Form onSubmit={formik.handleSubmit}>
{gptConfig.scoreRatingAlgs.length > 1 && (
{Object.keys(gptConfig.scoreRatingAlgs).length > 1 && (
<Form.Group>
<Form.Label>Preferred Score Algorithm</Form.Label>
<Form.Control
@@ -162,7 +163,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
value={formik.values.preferredScoreAlg}
onChange={formik.handleChange}
>
{gptConfig.scoreRatingAlgs.map((e) => (
{Object.keys(gptConfig.scoreRatingAlgs).map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
@@ -172,7 +173,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
</Form.Text>
</Form.Group>
)}
{gptConfig.sessionRatingAlgs.length > 1 && (
{Object.keys(gptConfig.sessionRatingAlgs).length > 1 && (
<Form.Group>
<Form.Label>Preferred Session Algorithm</Form.Label>
<Form.Control
@@ -181,7 +182,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
value={formik.values.preferredSessionAlg}
onChange={formik.handleChange}
>
{gptConfig.sessionRatingAlgs.map((e) => (
{Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
@@ -191,7 +192,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
</Form.Text>
</Form.Group>
)}
{gptConfig.profileRatingAlgs.length > 1 && (
{Object.keys(gptConfig.profileRatingAlgs).length > 1 && (
<Form.Group>
<Form.Label>Preferred Profile Algorithm</Form.Label>
<Form.Control
@@ -200,7 +201,7 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
value={formik.values.preferredProfileAlg}
onChange={formik.handleChange}
>
{gptConfig.profileRatingAlgs.map((e) => (
{Object.keys(gptConfig.profileRatingAlgs).map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
@@ -214,8 +215,8 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
<Form.Label>Preferred Folder Info</Form.Label>
<Form.Control
as="select"
id="scoreBucket"
value={formik.values.scoreBucket}
id="preferredDefaultEnum"
value={formik.values.preferredDefaultEnum}
onChange={formik.handleChange}
>
<option value="grade">Grades</option>
@@ -408,7 +409,7 @@ function ShowcaseForm({ reqUser, game, playtype }: UGPT) {
const [show, setShow] = useState(false);
const SaveChanges = async () => {
const r = await APIFetchV1<UGPTSettings>(
const r = await APIFetchV1<UGPTSettingsDocument>(
`/users/${reqUser.id}/games/${game}/${playtype}/showcase`,
{
method: "PUT",
@@ -125,8 +125,6 @@ function FolderCompare({
withUser: UserDocument;
folder: FolderDocument;
} & GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const { data: baseData, error: baseError } = useApiQuery<UGPTFolderReturns>(
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folder.folderID}`
);
@@ -136,11 +134,6 @@ function FolderCompare({
);
const [shouldIncludeNotPlayed, setShouldIncludeNotPlayed] = useState(false);
const [shouldESD, setShouldESD] = useState(gptConfig.supportsESD);
useEffect(() => {
setShouldESD(gptConfig.supportsESD);
}, [gptConfig]);
const dataset = useMemo(() => {
// i *LOVE* the rules of hooks! they're so convenient!
@@ -197,13 +190,6 @@ function FolderCompare({
onChange={() => setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)}
label="Include charts without plays?"
/>
{gptConfig.supportsESD && (
<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
@@ -211,7 +197,6 @@ function FolderCompare({
compareUser={withUser.username}
dataset={dataset}
game={game}
shouldESD={shouldESD}
playtype={playtype}
/>
</Row>
@@ -83,9 +83,9 @@ export function FolderInfoComponent({
}: UGPT & { folder: FolderDocument; folderStats: FolderStatsInfo }) {
const gptConfig = GetGamePTConfig(game, playtype);
const scoreBucket = useBucket(game, playtype);
const preferredDefaultEnum = useBucket(game, playtype);
const [elements, setElements] = useState<"grade" | "lamp">(scoreBucket);
const [elements, setElements] = useState<"grade" | "lamp">(preferredDefaultEnum);
const base = useUGPTBase({ reqUser, game, playtype });
@@ -1,47 +1,35 @@
import { ONE_DAY } from "util/constants/time";
import { CreateChartIDMap, CreateChartLink, CreateSongMap } from "util/data";
import { DistinctArr, NO_OP } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { GetScaleAchievedFn } from "util/tierlist";
import { FormatDate, FormatTime } from "util/time";
import { ChangeOpacity } from "util/color-opacity";
import FolderInfoHeader from "components/game/folder/FolderInfoHeader";
import QuickTooltip from "components/layout/misc/QuickTooltip";
import Card from "components/layout/page/Card";
import FolderTable from "components/tables/folders/FolderTable";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import Muted from "components/util/Muted";
import useApiQuery from "components/util/query/useApiQuery";
import ReferToUser from "components/util/ReferToUser";
import SelectLinkButton from "components/util/SelectLinkButton";
import useApiQuery from "components/util/query/useApiQuery";
import useUGPTBase from "components/util/useUGPTBase";
import { useFormik } from "formik";
import React, { useEffect, useMemo, useState } from "react";
import { Col, Form, Row } from "react-bootstrap";
import { Form } from "react-bootstrap";
import { Link, Route, Switch, useParams } from "react-router-dom";
import {
ChartDocument,
ChartTierlistInfo,
FormatDifficulty,
FormatDifficultyShort,
Game,
GetGamePTConfig,
GPTTierlists,
IDStrings,
integer,
UserDocument,
GetScoreEnumConfs,
Playtype,
ScoreDocument,
SongDocument,
Playtype,
COLOUR_SET,
UserDocument,
integer,
} from "tachi-common";
import { UGPTFolderReturns } from "types/api-returns";
import { FolderDataset } from "types/tables";
import MiniTable from "components/tables/components/MiniTable";
import DifficultyCell from "components/tables/cells/DifficultyCell";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import FolderComparePage from "./FolderComparePage";
import FolderQuestsPage from "./FolderQuestsPage";
@@ -58,8 +46,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}`
);
const gptConfig = GetGamePTConfig(game, playtype);
const folderDataset = useMemo(() => {
if (!data) {
return null;
@@ -124,15 +110,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
<Icon type="table" />
Normal View
</SelectLinkButton>
{gptConfig.tierlists.length !== 0 &&
// temp: tierlist view sucks for BMS and PMS
game !== "bms" &&
game !== "pms" && (
<SelectLinkButton to={`${base}/tierlist`}>
<Icon type="sort-alpha-up" />
Tierlist View
</SelectLinkButton>
)}
<SelectLinkButton to={`${base}/timeline`}>
<Icon type="stream" />
Timeline View
@@ -155,15 +132,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
<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}
@@ -196,20 +164,17 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID: string }) {
const gptConfig = GetGamePTConfig(game, playtype);
const [type, setType] = useState<"lamp" | "grade">(gptConfig.scoreBucket);
const [value, setValue] = useState<integer>(
type === "grade"
? gptConfig.grades.indexOf(gptConfig.clearGrade)
: gptConfig.lamps.indexOf(gptConfig.clearLamp)
);
const enumConfs = GetScoreEnumConfs(gptConfig);
const [selectedEnum, setSelectedEnum] = useState<string>(gptConfig.preferredDefaultEnum);
const [enumConf, setEnumConf] = useState<ConfEnumScoreMetric<string>>(enumConfs[selectedEnum]!);
const [value, setValue] = useState<string>(enumConf.minimumRelevantValue);
useEffect(() => {
setValue(
type === "grade"
? gptConfig.grades.indexOf(gptConfig.clearGrade)
: gptConfig.lamps.indexOf(gptConfig.clearLamp)
);
}, [type]);
setValue(enumConfs[selectedEnum]!.minimumRelevantValue);
setEnumConf(enumConfs[selectedEnum]!);
}, [selectedEnum]);
return (
<>
@@ -225,36 +190,33 @@ function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID:
<div className="col-12 col-lg-6">
<Form.Control
as="select"
value={type}
onChange={(e) => setType(e.target.value as "grade" | "lamp")}
value={selectedEnum}
onChange={(e) => setSelectedEnum(e.target.value)}
>
<option value="grade">Grades</option>
<option value="lamp">Lamps</option>
{Object.keys(enumConfs).map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
</div>
<div className="col-12 col-lg-6">
<Form.Control
as="select"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
onChange={(e) => setValue(e.target.value)}
>
{type === "grade"
? gptConfig.grades.map((e, i) => (
<option key={e} value={i}>
{e}
</option>
))
: gptConfig.lamps.map((e, i) => (
<option key={e} value={i}>
{e}
</option>
))}
{enumConf.values
.slice(enumConf.values.indexOf(enumConf.minimumRelevantValue))
.map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
</div>
</div>
</Card>
<Divider />
<TimelineMain {...{ reqUser, game, playtype, folderID, type, value }} />
<TimelineMain
{...{ reqUser, game, playtype, folderID, enumMetric: selectedEnum, value }}
/>
</>
);
}
@@ -264,19 +226,19 @@ function TimelineMain({
game,
playtype,
folderID,
type,
enumMetric: enumMetric,
value,
}: Props & {
folderID: string;
type: "grade" | "lamp";
value: integer;
enumMetric: string;
value: string;
}) {
const { data, error } = useApiQuery<{
scores: ScoreDocument[];
songs: SongDocument[];
charts: ChartDocument[];
}>(
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${type}`
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${enumMetric}`
);
if (error) {
@@ -411,420 +373,3 @@ function TimelineElement({
</div>
);
}
type InfoProps = Props & {
folderDataset: FolderDataset;
data: UGPTFolderReturns;
};
function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps) {
const gptConfig = GetGamePTConfig(game, playtype);
const formik = useFormik({
initialValues: {
__hideAchieved: false,
...Object.fromEntries(gptConfig.tierlists.map((e) => [e, true])),
} as { __hideAchieved: boolean } & Partial<Record<GPTTierlists[IDStrings], true>>,
onSubmit: NO_OP,
});
const tierlistInfo = useMemo(
() => FolderDatasetToTierlistInfo(folderDataset, game, playtype, formik.values),
[formik.values]
);
const dataMap = CreateChartIDMap(folderDataset);
return (
<Row>
<Col xs={12}>
<Card header="Tierlist View Configuration">
<span>Here you can select what tierlist blocks to show!</span>
<Divider />
{gptConfig.tierlists.length > 1 &&
gptConfig.tierlists.map((e) => (
<>
<Form.Check
key={e}
type="checkbox"
id={e}
checked={formik.values[e]}
onChange={formik.handleChange}
label={e}
/>
<Form.Text>{gptConfig.tierlistDescriptions[e]}</Form.Text>
</>
))}
<Form.Check
type="checkbox"
id="__hideAchieved"
checked={formik.values.__hideAchieved}
onChange={formik.handleChange}
label="Hide Achieved"
/>
<Form.Text>Hide achieved elements of the tierlist.</Form.Text>
</Card>
</Col>
<Col xs={12}>
<Divider />
</Col>
<Col xs={12}>
<TierlistInfoLadder
tierlistInfo={tierlistInfo}
dataMap={dataMap}
game={game}
playtype={playtype}
reqUser={reqUser}
/>
</Col>
</Row>
);
}
const NO_TIERLIST_DATA_VALUE = {
text: "No Tierlist Info",
value: 0,
};
function TierlistInfoLadder({
tierlistInfo,
dataMap,
game,
playtype,
reqUser,
}: {
tierlistInfo: NullableTierlistInfo[];
dataMap: Map<string, FolderDataset[0]>;
game: Game;
playtype: Playtype;
reqUser: UserDocument;
}) {
const buckets: TierlistInfo[][] = useMemo(() => {
const buckets: TierlistInfo[][] = [];
let currentBucket: TierlistInfo[] = [];
const noDataBucket: TierlistInfo[] = [];
let lastValue;
for (const tl of tierlistInfo) {
if (tl.data === null) {
noDataBucket.push({
achieved: tl.achieved,
chartID: tl.chartID,
data: NO_TIERLIST_DATA_VALUE,
key: tl.key,
});
continue;
}
// no longer nullable
const tlx = tl as TierlistInfo;
if (lastValue && tlx.data.value !== lastValue) {
buckets.push(currentBucket);
currentBucket = [tlx];
} else {
currentBucket.push(tlx);
}
lastValue = tlx.data.value;
}
if (currentBucket.length) {
buckets.push(currentBucket);
}
buckets.push(noDataBucket);
return buckets;
}, [tierlistInfo]);
const gptConfig = GetGamePTConfig(game, playtype);
for (const bucket of buckets) {
bucket.sort(NumericSOV((x) => gptConfig.tierlists.indexOf(x.key), true));
}
if (tierlistInfo.length === 0) {
return <Row className="justify-content-center">Got no tierlist data to show you!</Row>;
}
return (
<Row className="text-center">
{buckets
.filter((e) => e.length > 0)
.map((bucket, i) => (
<React.Fragment key={i}>
<Col className="ladder-header" xs={12}>
{bucket[0].data!.value} (
{DistinctArr(
bucket.map((e) =>
gptConfig.tierlists.length === 1
? e.data.text
: `${e.key}: ${e.data.text}`
)
).join(", ")}
)
</Col>
<TierlistBucket {...{ bucket, dataMap, game, playtype, reqUser }} />
</React.Fragment>
))}
</Row>
);
}
function TierlistBucket({
bucket,
dataMap,
game,
reqUser,
}: {
dataMap: Map<string, FolderDataset[0]>;
game: Game;
playtype: Playtype;
reqUser: UserDocument;
bucket: TierlistInfo[];
}) {
// xs view is tabular
if (window.screen.width <= 576) {
return (
<MiniTable>
{bucket.map((tierlistInfo, i) => (
<TierlistInfoBucketValues
tierlistInfo={tierlistInfo}
key={`${tierlistInfo.chartID}-${tierlistInfo.key}`}
game={game}
dataMap={dataMap}
bucket={bucket}
i={i}
reqUser={reqUser}
/>
))}
</MiniTable>
);
}
return (
<>
{bucket.map((tierlistInfo, i) => (
<TierlistInfoBucketValues
tierlistInfo={tierlistInfo}
key={`${tierlistInfo.chartID}-${tierlistInfo.key}`}
game={game}
dataMap={dataMap}
bucket={bucket}
i={i}
reqUser={reqUser}
/>
))}
</>
);
}
function TierlistInfoBucketValues({
tierlistInfo,
game,
dataMap,
bucket,
i,
reqUser,
}: {
tierlistInfo: TierlistInfo;
bucket: TierlistInfo[];
dataMap: Map<string, FolderDataset[0]>;
game: Game;
i: integer;
reqUser: UserDocument;
}) {
const data = dataMap.get(tierlistInfo.chartID)!;
const lastKey = bucket[i - 1];
let statusClass;
switch (tierlistInfo.achieved) {
case AchievedStatuses.ACHIEVED:
statusClass = "achieved";
break;
case AchievedStatuses.FAILED:
statusClass = "unachieved";
break;
case AchievedStatuses.NOT_PLAYED:
case AchievedStatuses.SCORE_BASED:
statusClass = "";
}
// xs view
if (window.screen.width <= 576) {
return (
<tr>
<DifficultyCell game={game} chart={data} alwaysShort noTierlist />
<td className="text-left">
<Link className="gentle-link" to={CreateChartLink(data, game)}>
{data.__related.song.title}
</Link>{" "}
<br />
<div>
{tierlistInfo.key} ({tierlistInfo.data.text})
{tierlistInfo.data.individualDifference && (
<span className="ml-1">
<Icon type="balance-scale-left" />
</span>
)}
</div>
</td>
<TierlistInfoCell tierlistInfo={tierlistInfo} />
</tr>
);
}
return (
<>
{lastKey && lastKey.key !== tierlistInfo.key && <Col xl={12} className="my-2" />}
<Col
className={`ladder-element ${
i % 12 < 6 ? "ladder-element-dark" : ""
} ladder-element-${statusClass} d-none d-sm-block`}
xs={12}
sm={6}
md={4}
lg={3}
xl={2}
>
<Link className="gentle-link" to={CreateChartLink(data, game)}>
{data.__related.song.title}
</Link>{" "}
{FormatDifficultyShort(data, game)}
<Divider className="my-2" />
{tierlistInfo.key} ({tierlistInfo.data.text})
{tierlistInfo.data.individualDifference && (
<>
<br />
<div className="mt-1">
<QuickTooltip tooltipContent="Individual Difference - The difficulty of this varies massively between people!">
<span>
<Icon type="balance-scale-left" />
</span>
</QuickTooltip>
</div>
</>
)}
<Divider className="my-2" />
<Muted>
<ReferToUser reqUser={reqUser} />{" "}
{tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED
? "Not Played"
: tierlistInfo.achieved === AchievedStatuses.SCORE_BASED
? data.__related.pb!.scoreData.grade
: data.__related.pb!.scoreData.lamp}
</Muted>
</Col>
</>
);
}
function TierlistInfoCell({ tierlistInfo }: { tierlistInfo: TierlistInfo }) {
let colour;
let text;
if (
tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED ||
tierlistInfo.achieved === AchievedStatuses.FAILED
) {
colour = COLOUR_SET.red;
text = "✗";
} else {
colour = COLOUR_SET.green;
text = "✓";
}
return (
<td
style={{
backgroundColor: ChangeOpacity(colour, 0.2),
width: "60px",
minWidth: "60px",
maxWidth: "60px",
}}
>
{text}
</td>
);
}
interface NullableTierlistInfo {
chartID: string;
key: GPTTierlists[IDStrings];
data: ChartTierlistInfo | null;
achieved: AchievedStatuses;
}
interface TierlistInfo {
chartID: string;
key: GPTTierlists[IDStrings];
data: ChartTierlistInfo;
achieved: AchievedStatuses;
}
enum AchievedStatuses {
NOT_PLAYED,
FAILED,
ACHIEVED,
SCORE_BASED,
}
function FolderDatasetToTierlistInfo(
folderDataset: FolderDataset,
game: Game,
playtype: Playtype,
options: Partial<Record<GPTTierlists[IDStrings], boolean>> & { __hideAchieved: boolean }
) {
const tierlistInfo: NullableTierlistInfo[] = [];
const tierlistKeys: GPTTierlists[IDStrings][] = [];
for (const k in options) {
if (k === "__hideAchieved") {
continue;
}
const key = k as GPTTierlists[IDStrings];
if (options[key]) {
tierlistKeys.push(key);
}
}
for (const data of folderDataset) {
for (const key of tierlistKeys) {
let achieved: AchievedStatuses;
if (!data.__related.pb) {
achieved = AchievedStatuses.NOT_PLAYED;
} else {
const fn = GetScaleAchievedFn(game, playtype, key);
if (fn) {
achieved = fn(data.__related.pb)
? AchievedStatuses.ACHIEVED
: AchievedStatuses.FAILED;
} else {
achieved = AchievedStatuses.SCORE_BASED;
}
}
if (options.__hideAchieved && achieved === AchievedStatuses.ACHIEVED) {
continue;
}
tierlistInfo.push({
chartID: data.chartID,
key,
data: data.tierlistInfo[key] ?? null,
achieved,
});
}
}
return tierlistInfo.sort(NumericSOV((x) => (x.data ? x.data.value : -Infinity), true));
}