mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-27 09:28:12 +03:00
checkpoint: insanity ascends (35)
127 errors to go
This commit is contained in:
+2
-1
@@ -7,7 +7,8 @@
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint src --fix",
|
||||
"preview": "vite build && vite preview"
|
||||
"preview": "vite build && vite preview",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
-15
@@ -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>
|
||||
|
||||
+2
-2
@@ -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 });
|
||||
|
||||
|
||||
+34
-489
@@ -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));
|
||||
}
|
||||
|
||||
@@ -18,7 +18,13 @@ import { UserContext } from "context/UserContext";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Button, Col, Row } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormatChart, FormatGame, GetGamePTConfig, UserDocument } from "tachi-common";
|
||||
import {
|
||||
FormatChart,
|
||||
FormatGame,
|
||||
GetGamePTConfig,
|
||||
GetScoreEnumConfs,
|
||||
UserDocument,
|
||||
} from "tachi-common";
|
||||
import { ActivityReturn, RecordActivityReturn, SessionReturns } from "types/api-returns";
|
||||
import { UGPT } from "types/react";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
@@ -556,15 +562,21 @@ function SessionShower({ sessionID }: { sessionID: string }) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.isNewScore) {
|
||||
return (
|
||||
score.scoreData.lampIndex > gptConfig.lamps.indexOf(gptConfig.clearLamp) ||
|
||||
score.scoreData.gradeIndex > gptConfig.grades.indexOf(gptConfig.clearGrade)
|
||||
);
|
||||
} else if (e.gradeDelta > 0) {
|
||||
return score.scoreData.gradeIndex > gptConfig.grades.indexOf(gptConfig.clearGrade);
|
||||
} else if (e.lampDelta > 0) {
|
||||
return score.scoreData.lampIndex > gptConfig.lamps.indexOf(gptConfig.clearLamp);
|
||||
const enumMetrics = GetScoreEnumConfs(gptConfig);
|
||||
|
||||
// for all enum metrics, check if this score beats the minimum relevant enum
|
||||
// and is a raise.
|
||||
for (const [metric, conf] of Object.entries(enumMetrics)) {
|
||||
if (!e.isNewScore && e.deltas[metric] <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
// @ts-expect-error its gonna exist buddy
|
||||
score.scoreData.enumIndexes[metric] > conf.values.indexOf(conf.minimumRelevantValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,59 +1,73 @@
|
||||
import { UppercaseFirst } from "util/misc";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import { Badge } from "react-bootstrap";
|
||||
import { GetGamePTConfig, IDStrings, integer } from "tachi-common";
|
||||
import { GameClassSets } from "tachi-common/game-classes";
|
||||
import { Classes, GPTString, GetGPTString, GetGamePTConfig } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
|
||||
export default function ClassBadge<I extends IDStrings = IDStrings>({
|
||||
export default function ClassBadge<GPT extends GPTString = GPTString>({
|
||||
game,
|
||||
playtype,
|
||||
classSet,
|
||||
classValue,
|
||||
showSetOnHover = true,
|
||||
}: {
|
||||
classSet: GameClassSets[I];
|
||||
classValue: integer;
|
||||
classSet: Classes[GPT];
|
||||
classValue: string;
|
||||
showSetOnHover?: boolean;
|
||||
} & GamePT) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const classStyle =
|
||||
// @ts-expect-error hepl i'm trapped in a type factory
|
||||
GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)].classColours[classSet][classValue];
|
||||
|
||||
const data = gptConfig.classHumanisedFormat[classSet][classValue];
|
||||
const data = GetGamePTConfig(game, playtype).classes[classSet].values.find(
|
||||
(e) => e.id === classValue
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
throw new Error(`Unknown class value ${classSet}:${classValue}`);
|
||||
return (
|
||||
<>
|
||||
{classSet} {classValue} (messed up!)
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let badgeComponent;
|
||||
if (data.variant) {
|
||||
|
||||
if (classStyle === null) {
|
||||
badgeComponent = (
|
||||
<Badge className="mx-2" variant={data.variant}>
|
||||
<Badge
|
||||
className="mx-2"
|
||||
style={{
|
||||
backgroundColor: "#131313",
|
||||
}}
|
||||
>
|
||||
{data.display}
|
||||
</Badge>
|
||||
);
|
||||
} else if (data.css) {
|
||||
} else if (typeof classStyle === "string") {
|
||||
badgeComponent = (
|
||||
<Badge className="mx-2" style={data.css}>
|
||||
<Badge className="mx-2" variant={classStyle}>
|
||||
{data.display}
|
||||
</Badge>
|
||||
);
|
||||
} else {
|
||||
badgeComponent = (
|
||||
<Badge className="mx-2" variant="secondary">
|
||||
<Badge className="mx-2" style={classStyle}>
|
||||
{data.display}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.mouseover && showSetOnHover) {
|
||||
if (data.hoverText && showSetOnHover) {
|
||||
return (
|
||||
<QuickTooltip tooltipContent={`${UppercaseFirst(classSet)}: ${data.mouseover}`}>
|
||||
<QuickTooltip tooltipContent={`${UppercaseFirst(classSet)}: ${data.hoverText}`}>
|
||||
{badgeComponent}
|
||||
</QuickTooltip>
|
||||
);
|
||||
} else if (data.mouseover) {
|
||||
return <QuickTooltip tooltipContent={data.mouseover}>{badgeComponent}</QuickTooltip>;
|
||||
} else if (data.hoverText) {
|
||||
return <QuickTooltip tooltipContent={data.hoverText}>{badgeComponent}</QuickTooltip>;
|
||||
} else if (showSetOnHover) {
|
||||
<QuickTooltip tooltipContent={UppercaseFirst(classSet)}>{badgeComponent}</QuickTooltip>;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ export default function ScoreLeaderboard({
|
||||
const [alg, setAlg] = useState(defaultAlg);
|
||||
|
||||
const SelectComponent =
|
||||
gptConfig.scoreRatingAlgs.length > 1 ? (
|
||||
Object.keys(gptConfig.scoreRatingAlgs).length > 1 ? (
|
||||
<Form.Control as="select" value={alg} onChange={(e) => setAlg(e.target.value as any)}>
|
||||
{gptConfig.scoreRatingAlgs.map((e) => (
|
||||
{Object.keys(gptConfig.scoreRatingAlgs).map((e) => (
|
||||
<option key={e} value={e}>
|
||||
{UppercaseFirst(e)}
|
||||
</option>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import RatingSystemPart from "components/tables/cells/RatingSystemPart";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import ApiError from "components/util/ApiError";
|
||||
import ExternalLink from "components/util/ExternalLink";
|
||||
@@ -6,12 +7,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 { UserContext } from "context/UserContext";
|
||||
import { AllLUGPTStatsContext } from "context/AllLUGPTStatsContext";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import React, { useContext } from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ChartDocument, FolderDocument, Game, GetGamePTConfig } from "tachi-common";
|
||||
import { ChartDocument, FolderDocument, Game, GetGPTString } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
|
||||
export default function ChartInfoFormat({
|
||||
@@ -19,9 +21,9 @@ export default function ChartInfoFormat({
|
||||
game,
|
||||
playtype,
|
||||
}: { chart: ChartDocument } & GamePT) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)];
|
||||
|
||||
const withTierlists = gptConfig.tierlists.filter((e) => chart.tierlistInfo[e]);
|
||||
const ratingSystems = gptImpl.ratingSystems;
|
||||
|
||||
const { data, error } = useApiQuery<FolderDocument[]>(
|
||||
`/games/${game}/${playtype}/charts/${chart.chartID}/folders`
|
||||
@@ -71,27 +73,44 @@ export default function ChartInfoFormat({
|
||||
<ChartInfoMiddle chart={chart} game={game} />
|
||||
</Col>
|
||||
<Col xs={12} lg={3}>
|
||||
{withTierlists.length !== 0 ? (
|
||||
<MiniTable headers={["Tierlist Info"]} colSpan={2}>
|
||||
{withTierlists.map((e) => (
|
||||
<tr key={e}>
|
||||
<td>{e}</td>
|
||||
<td>
|
||||
{chart.tierlistInfo[e]!.text}{" "}
|
||||
<Muted>({chart.tierlistInfo[e]!.value.toFixed(2)})</Muted>
|
||||
{chart.tierlistInfo[e]!.individualDifference && (
|
||||
<>
|
||||
<br />
|
||||
<QuickTooltip tooltipContent="Individual Difference - The difficulty of this varies massively between people!">
|
||||
<span>
|
||||
<Icon type="balance-scale-left" />
|
||||
</span>
|
||||
</QuickTooltip>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<RatingSystemPart chart={chart} game={game} />
|
||||
{ratingSystems.length !== 0 ? (
|
||||
<MiniTable headers={["Ratings"]} colSpan={2}>
|
||||
{ratingSystems.map((e) => {
|
||||
// @ts-expect-error bad types
|
||||
const strV = e.toString(chart);
|
||||
// @ts-expect-error bad types
|
||||
const numV = e.toNumber(chart);
|
||||
|
||||
if (
|
||||
strV === null ||
|
||||
strV === undefined ||
|
||||
numV === null ||
|
||||
numV === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={e.name}>
|
||||
<td>{e}</td>
|
||||
<td>
|
||||
{strV} <Muted>({numV.toFixed(2)})</Muted>
|
||||
{/* @ts-expect-error utterly silly types */}
|
||||
{e.idvDifference(chart) && (
|
||||
<>
|
||||
<br />
|
||||
<QuickTooltip tooltipContent="Individual Difference - The difficulty of this varies massively between people!">
|
||||
<span>
|
||||
<Icon type="balance-scale-left" />
|
||||
</span>
|
||||
</QuickTooltip>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</MiniTable>
|
||||
) : (
|
||||
<Muted>No tierlist info.</Muted>
|
||||
|
||||
@@ -21,8 +21,8 @@ export default function FolderInfoHeader({
|
||||
folderDataset: FolderDataset;
|
||||
folderTitle: string;
|
||||
} & GamePT) {
|
||||
const scoreBucket = useBucket(game, playtype);
|
||||
const [view, setView] = useState<"grade" | "lamp">(scoreBucket);
|
||||
const preferredDefaultEnum = useBucket(game, playtype);
|
||||
const [view, setView] = useState<"grade" | "lamp">(preferredDefaultEnum);
|
||||
const [breakdownInfo, setBreakdownInfo] = useState<"graph" | "switchboard">("switchboard");
|
||||
|
||||
return (
|
||||
@@ -66,7 +66,7 @@ export default function FolderInfoHeader({
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
folderDataset={folderDataset}
|
||||
view={view}
|
||||
enumMetric={view}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { ONE_WEEK } from "util/constants/time";
|
||||
import { CreateChartLink } from "util/data";
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { ONE_WEEK } from "util/constants/time";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells";
|
||||
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 React, { useEffect, useMemo, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import React, { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
ChartDocument,
|
||||
FormatChart,
|
||||
Game,
|
||||
GamePTConfig,
|
||||
GetGPTString,
|
||||
GetGamePTConfig,
|
||||
GetScoreMetricConf,
|
||||
IIDX_LAMPS,
|
||||
UserDocument,
|
||||
SessionDocument,
|
||||
SessionScoreInfo,
|
||||
UserDocument,
|
||||
} from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
import { FolderDataset } from "types/tables";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
|
||||
type Props = {
|
||||
folderDataset: FolderDataset;
|
||||
reqUser: UserDocument;
|
||||
view: "grade" | "lamp";
|
||||
enumMetric: string;
|
||||
} & GamePT;
|
||||
|
||||
export default function FolderMinimap(props: Props) {
|
||||
@@ -61,7 +62,7 @@ export default function FolderMinimap(props: Props) {
|
||||
<FolderMinimapMain {...props} recentSession={session} />
|
||||
</div>
|
||||
<div className="d-block d-lg-none">
|
||||
Sadly, Switchboard view doesn't work very well on mobile at the moment.
|
||||
Sadly, Switchboard view doesn't work on mobile at the moment.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -71,40 +72,32 @@ function FolderMinimapMain({
|
||||
game,
|
||||
playtype,
|
||||
folderDataset,
|
||||
view,
|
||||
enumMetric,
|
||||
recentSession,
|
||||
}: Props & {
|
||||
recentSession: { session: SessionDocument; scoreInfo: Array<SessionScoreInfo> } | null;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const [sortAlg, setSortAlg] = useState(view === "grade" ? "score" : "lamp");
|
||||
|
||||
useEffect(() => {
|
||||
if (view === "grade") {
|
||||
setSortAlg("score");
|
||||
} else {
|
||||
setSortAlg("lamp");
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
const getterFn = useMemo<(f: FolderDataset[0]) => number | undefined>(() => {
|
||||
if (sortAlg === "score") {
|
||||
return (c) => c.__related.pb?.scoreData.percent;
|
||||
} else if (sortAlg === "lamp") {
|
||||
return (c) => c.__related.pb?.scoreData.lampIndex;
|
||||
} else if (sortAlg.startsWith("tierlist:")) {
|
||||
const v = sortAlg.split("tierlist:")[1];
|
||||
const conf = GetScoreMetricConf(gptConfig, enumMetric);
|
||||
|
||||
return (c) => c.tierlistInfo[v as keyof ChartDocument["tierlistInfo"]]?.value;
|
||||
if (!conf) {
|
||||
return () => 0; // wut
|
||||
}
|
||||
|
||||
return () => 0;
|
||||
}, [sortAlg]);
|
||||
if (conf.type === "ENUM") {
|
||||
// @ts-expect-error insane dynamic access
|
||||
return (c) => c.__related.pb?.scoreData.enumIndexes[enumMetric];
|
||||
}
|
||||
|
||||
// @ts-expect-error insane dynamic access
|
||||
return (c) => c.__related.pb?.scoreData[enumMetric];
|
||||
}, [enumMetric]);
|
||||
|
||||
const sortedDataset = useMemo(
|
||||
() => folderDataset.slice(0).sort(NumericSOV((a) => getterFn(a) ?? -Infinity, true)),
|
||||
[getterFn, folderDataset, sortAlg]
|
||||
[getterFn, folderDataset, enumMetric]
|
||||
);
|
||||
|
||||
// For the switchboard chart, display a raise icon on stuff the user has recently played.
|
||||
@@ -114,7 +107,19 @@ function FolderMinimapMain({
|
||||
}
|
||||
|
||||
return recentSession.scoreInfo
|
||||
.filter((e) => e.isNewScore || e.gradeDelta > 0 || e.lampDelta > 0)
|
||||
.filter((e) => {
|
||||
if (e.isNewScore) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const v of Object.values(e.deltas)) {
|
||||
if (v >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((e) => e.scoreID);
|
||||
}, [recentSession]);
|
||||
|
||||
@@ -127,37 +132,20 @@ function FolderMinimapMain({
|
||||
key={d.chartID}
|
||||
data={d}
|
||||
gptConfig={gptConfig}
|
||||
view={view}
|
||||
enumMetric={enumMetric}
|
||||
game={game}
|
||||
wasRecent={
|
||||
(d.__related.pb &&
|
||||
recentlyTouched.some(
|
||||
(scoreID) =>
|
||||
d.__related.pb!.composedFrom.scorePB === scoreID ||
|
||||
d.__related.pb!.composedFrom.lampPB === scoreID
|
||||
recentlyTouched.some((scoreID) =>
|
||||
d.__related.pb!.composedFrom.find(
|
||||
(e) => e.scoreID === scoreID
|
||||
)
|
||||
)) ??
|
||||
false
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Divider />
|
||||
<Form.Group>
|
||||
<Form.Label>Sorting switchboard on:</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={sortAlg}
|
||||
onChange={(e) => setSortAlg(e.target.value)}
|
||||
>
|
||||
{view === "grade" && <option value="score">Your Score</option>}
|
||||
{view === "lamp" && <option value="lamp">Your Lamp</option>}
|
||||
{gptConfig.tierlists.map((e) => (
|
||||
<option key={`tierlist:${e}`} value={`tierlist:${e}`}>
|
||||
Tierlist: {e}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -166,16 +154,18 @@ function FolderMinimapMain({
|
||||
function MinimapElement({
|
||||
data,
|
||||
gptConfig,
|
||||
view,
|
||||
enumMetric,
|
||||
game,
|
||||
wasRecent,
|
||||
}: {
|
||||
data: FolderDataset[0];
|
||||
view: "grade" | "lamp";
|
||||
enumMetric: string;
|
||||
gptConfig: GamePTConfig;
|
||||
game: Game;
|
||||
wasRecent: boolean;
|
||||
}) {
|
||||
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, data.playtype)];
|
||||
|
||||
let icon = "level-up-alt";
|
||||
|
||||
// if this user recently cleared mare nectaris
|
||||
@@ -183,7 +173,8 @@ function MinimapElement({
|
||||
if (
|
||||
data.chartID === "924bf011fdd8334b609b02e382123f9f5440d16d" &&
|
||||
data.__related.pb &&
|
||||
data.__related.pb?.scoreData.lampIndex >= IIDX_LAMPS.EASY_CLEAR
|
||||
// @ts-expect-error the above chartID guarantees mare nectaris
|
||||
data.__related.pb?.scoreData.enumIndexes.lamp >= IIDX_LAMPS.EASY_CLEAR
|
||||
) {
|
||||
icon = "hand-middle-finger";
|
||||
}
|
||||
@@ -194,8 +185,8 @@ function MinimapElement({
|
||||
}
|
||||
|
||||
// @ts-expect-error unhinged dynamic access (i dont care)
|
||||
return gptConfig[`${view}Colours`][data.__related.pb.scoreData[view]];
|
||||
}, [data.__related.pb, gptConfig, view]);
|
||||
return gptImpl.enumColours[enumMetric][data.__related.pb.scoreData[enumMetric]];
|
||||
}, [data.__related.pb, gptConfig, enumMetric]);
|
||||
|
||||
return (
|
||||
<QuickTooltip
|
||||
@@ -209,17 +200,10 @@ function MinimapElement({
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<span>{data.__related.pb?.scoreData.lamp ?? "Not Played"}</span>
|
||||
{data.__related.pb && (
|
||||
<>
|
||||
<br />
|
||||
{data.__related.pb.scoreData.grade}
|
||||
<br />
|
||||
<span>
|
||||
{data.__related.pb.scoreData.score} (
|
||||
{data.__related.pb.scoreData.percent.toFixed(2)}%)
|
||||
</span>
|
||||
</>
|
||||
{data.__related.pb ? (
|
||||
<ScoreCoreCells chart={data} game={game} score={data.__related.pb} />
|
||||
) : (
|
||||
<span>Not Played</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function SongChartInfoFormat({
|
||||
chart: ChartDocument | null;
|
||||
game: Game;
|
||||
}) {
|
||||
// lazy casting hack to avoid IDStrings
|
||||
// lazy casting hack to avoid GPTString
|
||||
const props = { song: tempSong, chart: tempChart, game } as any;
|
||||
const { song, chart } = props;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Game, IDStrings, Playtype } from "tachi-common";
|
||||
import { Game, GPTString, Playtype } from "tachi-common";
|
||||
import { GPTUtility } from "types/ugpt";
|
||||
import { BMSCustomTablesTool } from "./tools/BMSCustomTablesTool";
|
||||
import { IIDXEamusementExportTool } from "./tools/IIDXEamusementExportTool";
|
||||
@@ -7,7 +7,7 @@ import { JubilityBreakdownInsight } from "./insights/JubilityBreakdownInsight";
|
||||
import { BMSSieglindeInfoTool } from "./tools/BMSSieglindeInfoTool";
|
||||
|
||||
// What utils does each game support?
|
||||
const GPT_UTILS: Record<IDStrings, Array<GPTUtility>> = {
|
||||
const GPT_UTILS: Record<GPTString, Array<GPTUtility>> = {
|
||||
"bms:7K": [BMSCustomTablesTool, BMSSieglindeInfoTool],
|
||||
"bms:14K": [BMSCustomTablesTool, BMSSieglindeInfoTool],
|
||||
"chunithm:Single": [],
|
||||
@@ -29,7 +29,7 @@ const GPT_UTILS: Record<IDStrings, Array<GPTUtility>> = {
|
||||
};
|
||||
|
||||
export function GetGPTUtils(game: Game, playtype: Playtype) {
|
||||
const idString = `${game}:${playtype}` as IDStrings;
|
||||
const idString = `${game}:${playtype}` as GPTString;
|
||||
|
||||
return GPT_UTILS[idString];
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import { TachiConfig } from "lib/config";
|
||||
import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { BMS_TABLES, ChartDocument, SongDocument } from "tachi-common";
|
||||
import { FormatSieglindeBMS } from "tachi-common/config/internal-utils";
|
||||
import { UGPT } from "types/react";
|
||||
import { GPTUtility } from "types/ugpt";
|
||||
import { FormatSieglindeBMS } from "tachi-common/config/game-support/bms";
|
||||
|
||||
type DatasetElement = ChartDocument<"bms:7K" | "bms:14K"> & { __related: { song: SongDocument } };
|
||||
|
||||
@@ -76,20 +76,20 @@ function Component({ game, playtype, reqUser }: UGPT) {
|
||||
[
|
||||
"EASY CLEAR Sieglinde",
|
||||
"EC sgl.",
|
||||
NumericSOV((x) => x.tierlistInfo["sgl-EC"]?.value ?? -Infinity),
|
||||
NumericSOV((x) => x.data.sglEC ?? -Infinity),
|
||||
],
|
||||
[
|
||||
"HARD CLEAR Sieglinde",
|
||||
"EC sgl.",
|
||||
NumericSOV((x) => x.tierlistInfo["sgl-HC"]?.value ?? -Infinity),
|
||||
NumericSOV((x) => x.data.sglHC ?? -Infinity),
|
||||
],
|
||||
]}
|
||||
rowFunction={(d) => (
|
||||
<tr>
|
||||
<BMSOrPMSDifficultyCell chart={d} game="bms" />
|
||||
<TitleCell song={d.__related.song} game="bms" chart={d} />
|
||||
<td>{FormatSieglindeBMS(d.tierlistInfo["sgl-EC"]?.value ?? 0)}</td>
|
||||
<td>{FormatSieglindeBMS(d.tierlistInfo["sgl-HC"]?.value ?? 0)}</td>
|
||||
<td>{FormatSieglindeBMS(d.data.sglEC ?? 0)}</td>
|
||||
<td>{FormatSieglindeBMS(d.data.sglHC ?? 0)}</td>
|
||||
</tr>
|
||||
)}
|
||||
defaultSortMode="EASY CLEAR Sieglinde"
|
||||
|
||||
@@ -115,7 +115,8 @@ export default function SessionFolderRaiseBreakdown({
|
||||
const chartMap = CreateChartMap(sessionData.charts);
|
||||
const songMap = CreateSongMap(sessionData.songs);
|
||||
|
||||
const preferredScoreBucket = settings?.preferences.scoreBucket ?? gptConfig.scoreBucket;
|
||||
const preferredScoreBucket =
|
||||
settings?.preferences.preferredDefaultEnum ?? gptConfig.preferredDefaultEnum;
|
||||
|
||||
if (selectedTable === "LOADING") {
|
||||
return <Loading />;
|
||||
|
||||
@@ -5,7 +5,7 @@ import ScoreTable from "components/tables/scores/ScoreTable";
|
||||
import Divider from "components/util/Divider";
|
||||
import React, { useState } from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { GetGamePTConfig, ScoreDocument, SessionDocument } from "tachi-common";
|
||||
import { AnySessionRatingAlg, GetGamePTConfig, ScoreDocument, SessionDocument } from "tachi-common";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
import { SetState } from "types/react";
|
||||
@@ -73,15 +73,15 @@ function RatingsOverview({ session }: { session: SessionDocument }) {
|
||||
<Divider />
|
||||
<Card header="Ratings">
|
||||
<div className="d-flex text-center" style={{ justifyContent: "space-evenly" }}>
|
||||
{gptConfig.sessionRatingAlgs.map((e) => (
|
||||
{Object.keys(gptConfig.sessionRatingAlgs).map((e) => (
|
||||
<div key={e}>
|
||||
<div className="display-4">{UppercaseFirst(e)}</div>
|
||||
<div style={{ fontSize: "1.2rem" }}>
|
||||
{FormatSessionRating(
|
||||
session.game,
|
||||
session.playtype,
|
||||
e,
|
||||
session.calculatedData[e]
|
||||
e as AnySessionRatingAlg,
|
||||
session.calculatedData[e as AnySessionRatingAlg]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data";
|
||||
import { PartialArrayRecordAssign } from "util/misc";
|
||||
import { PartialArrayRecordAssign, UppercaseFirst } from "util/misc";
|
||||
import DifficultyCell from "components/tables/cells/DifficultyCell";
|
||||
import LampCell from "components/tables/cells/LampCell";
|
||||
import ScoreCell from "components/tables/cells/ScoreCell";
|
||||
import TitleCell from "components/tables/cells/TitleCell";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import { CommentModal, ModifyScore } from "components/tables/dropdowns/components/ScoreEditButtons";
|
||||
import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells";
|
||||
import Divider from "components/util/Divider";
|
||||
import Icon from "components/util/Icon";
|
||||
import Loading from "components/util/Loading";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import deepmerge from "deepmerge";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import { GPTClientImplementation } from "lib/types";
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import {
|
||||
ChartDocument,
|
||||
GPTString,
|
||||
Game,
|
||||
GamePTConfig,
|
||||
GetGPTString,
|
||||
GetGamePTConfig,
|
||||
Grades,
|
||||
IDStrings,
|
||||
integer,
|
||||
Lamps,
|
||||
GetScoreMetricConf,
|
||||
GetScoreMetrics,
|
||||
ScoreDocument,
|
||||
SessionScoreInfo,
|
||||
SongDocument,
|
||||
TableDocument,
|
||||
integer,
|
||||
} from "tachi-common";
|
||||
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
|
||||
import { SessionReturns } from "types/api-returns";
|
||||
|
||||
type SetScores = (scores: ScoreDocument[]) => void;
|
||||
@@ -57,7 +60,10 @@ export default function SessionRaiseBreakdown({
|
||||
|
||||
return res.body;
|
||||
});
|
||||
const [view, setView] = useState<"lamps" | "both" | "grades">("both");
|
||||
|
||||
// null -> view all in small format
|
||||
// string -> view this specific metric.
|
||||
const [view, setView] = useState<string | null>(null);
|
||||
|
||||
if (error) {
|
||||
return <>{(error as Error).message}</>;
|
||||
@@ -75,17 +81,17 @@ export default function SessionRaiseBreakdown({
|
||||
<div className="col-12 col-lg-6 offset-lg-3">
|
||||
<div className="d-none d-lg-flex justify-content-center">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={view} setValue={setView} id="lamps">
|
||||
<SelectButton value={view} setValue={setView} id="lamp">
|
||||
<Icon type="lightbulb" />
|
||||
Lamps Only
|
||||
</SelectButton>
|
||||
|
||||
<SelectButton value={view} setValue={setView} id="both">
|
||||
<SelectButton value={view} setValue={setView} id={null}>
|
||||
<Icon type="bolt" />
|
||||
Both
|
||||
All
|
||||
</SelectButton>
|
||||
|
||||
<SelectButton value={view} setValue={setView} id="grades">
|
||||
<SelectButton value={view} setValue={setView} id="grade">
|
||||
<Icon type="sort-alpha-up" />
|
||||
Grades Only
|
||||
</SelectButton>
|
||||
@@ -115,122 +121,103 @@ function SessionScoreStatBreakdown({
|
||||
}: {
|
||||
sessionData: SessionReturns;
|
||||
setScores?: SetScores;
|
||||
view: "lamps" | "both" | "grades";
|
||||
view: string | null;
|
||||
}) {
|
||||
const songMap = CreateSongMap(sessionData.songs);
|
||||
const chartMap = CreateChartMap(sessionData.charts);
|
||||
const scoreMap = CreateScoreIDMap(sessionData.scores);
|
||||
const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype);
|
||||
|
||||
const [newLamps, newGrades] = useMemo(() => {
|
||||
const newLamps: Partial<
|
||||
Record<Lamps[IDStrings], { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>
|
||||
> = {};
|
||||
const newGrades: Partial<
|
||||
Record<Grades[IDStrings], { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>
|
||||
const enumMetrics = GetScoreMetrics(gptConfig, "ENUM");
|
||||
|
||||
const newEnums = useMemo(() => {
|
||||
const newEnums: Record<
|
||||
string,
|
||||
Record<string, { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>
|
||||
> = {};
|
||||
|
||||
for (const scoreInfo of sessionData.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
for (const metric of enumMetrics) {
|
||||
newEnums[metric] = {};
|
||||
|
||||
if (!score) {
|
||||
console.error(
|
||||
`Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const scoreInfo of sessionData.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
if (scoreInfo.isNewScore) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, { score, scoreInfo });
|
||||
} else {
|
||||
if (scoreInfo.lampDelta > 0) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
if (!score) {
|
||||
console.error(
|
||||
`Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scoreInfo.gradeDelta > 0) {
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, {
|
||||
score,
|
||||
scoreInfo,
|
||||
});
|
||||
if (scoreInfo.isNewScore || scoreInfo.deltas[metric] > 0) {
|
||||
PartialArrayRecordAssign(
|
||||
newEnums[metric],
|
||||
|
||||
// @ts-expect-error yeah this is fine pls
|
||||
score.scoreData[metric] as string,
|
||||
{
|
||||
score,
|
||||
scoreInfo,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [newLamps, newGrades];
|
||||
return newEnums;
|
||||
}, [view]);
|
||||
|
||||
const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype);
|
||||
const gptImpl =
|
||||
GPT_CLIENT_IMPLEMENTATIONS[
|
||||
GetGPTString(sessionData.session.game, sessionData.session.playtype)
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{view === "both" ? (
|
||||
<>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Lamp", "New Lamps"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
scores={sessionData.scores}
|
||||
setScores={setScores}
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Grade", "New Grades"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
scores={sessionData.scores}
|
||||
setScores={setScores}
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
</>
|
||||
) : view === "grades" ? (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Grade", "New Grades"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
scores={sessionData.scores}
|
||||
setScores={setScores}
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
{view === null ? (
|
||||
<div className="d-flex w-100">
|
||||
{enumMetrics.map((metric) => (
|
||||
<div style={{ flex: 1 }}>
|
||||
<MiniTable
|
||||
headers={[
|
||||
`${UppercaseFirst(metric)}s`,
|
||||
`New ${UppercaseFirst(metric)}s`,
|
||||
]}
|
||||
colSpan={[1, 100]}
|
||||
>
|
||||
<ElementStatTable
|
||||
scores={sessionData.scores}
|
||||
setScores={setScores}
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newEnums[metric]!}
|
||||
game={sessionData.session.game}
|
||||
metric={metric}
|
||||
gptConfig={gptConfig}
|
||||
gptImpl={gptImpl}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Lamps", "New Lamps"]} colSpan={[1, 100]}>
|
||||
<MiniTable
|
||||
headers={[`${UppercaseFirst(view)}s`, `New ${UppercaseFirst(view)}s`]}
|
||||
colSpan={[1, 100]}
|
||||
>
|
||||
<ElementStatTable
|
||||
scores={sessionData.scores}
|
||||
setScores={setScores}
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
counts={newEnums[view]!}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
gptImpl={gptImpl}
|
||||
metric={view}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
@@ -240,7 +227,7 @@ function SessionScoreStatBreakdown({
|
||||
}
|
||||
|
||||
function ElementStatTable({
|
||||
type,
|
||||
metric: metric,
|
||||
counts,
|
||||
gptConfig,
|
||||
songMap,
|
||||
@@ -249,27 +236,26 @@ function ElementStatTable({
|
||||
fullSize = false,
|
||||
scores,
|
||||
setScores,
|
||||
gptImpl,
|
||||
}: {
|
||||
type: "lamp" | "grade";
|
||||
metric: string;
|
||||
setScores?: SetScores;
|
||||
scores: ScoreDocument[];
|
||||
counts: Record<string, { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>;
|
||||
gptConfig: GamePTConfig;
|
||||
gptImpl: GPTClientImplementation<any>;
|
||||
songMap: Map<integer, SongDocument<Game>>;
|
||||
chartMap: Map<string, ChartDocument<IDStrings>>;
|
||||
chartMap: Map<string, ChartDocument<GPTString>>;
|
||||
game: Game;
|
||||
fullSize?: boolean;
|
||||
}) {
|
||||
const tableContents = useMemo(() => {
|
||||
// relements.. haha
|
||||
const relevantElements =
|
||||
type === "lamp"
|
||||
? gptConfig.lamps.slice(gptConfig.lamps.indexOf(gptConfig.clearLamp) - 1).reverse()
|
||||
: gptConfig.grades
|
||||
.slice(gptConfig.grades.indexOf(gptConfig.clearGrade) - 1)
|
||||
.reverse();
|
||||
const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric<string>;
|
||||
|
||||
const colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours;
|
||||
// relements.. haha
|
||||
const relevantElements = conf.values.slice(conf.values.indexOf(conf.minimumRelevantValue));
|
||||
|
||||
const colours = gptImpl.enumColours[metric];
|
||||
|
||||
const tableContents = [];
|
||||
for (const element of relevantElements) {
|
||||
@@ -283,7 +269,6 @@ function ElementStatTable({
|
||||
<tr key={element} className="breakdown-hover-row">
|
||||
<td
|
||||
style={{
|
||||
// @ts-expect-error this is a hack due to the funky type of colours and element.
|
||||
backgroundColor: ChangeOpacity(colours[element], 0.1),
|
||||
}}
|
||||
rowSpan={counts[element]!.length}
|
||||
@@ -298,7 +283,7 @@ function ElementStatTable({
|
||||
fullSize,
|
||||
game,
|
||||
gptConfig,
|
||||
type,
|
||||
metric: metric,
|
||||
scores,
|
||||
setScores,
|
||||
}}
|
||||
@@ -317,7 +302,7 @@ function ElementStatTable({
|
||||
fullSize,
|
||||
game,
|
||||
gptConfig,
|
||||
type,
|
||||
metric: metric,
|
||||
scores,
|
||||
setScores,
|
||||
}}
|
||||
@@ -328,7 +313,7 @@ function ElementStatTable({
|
||||
}
|
||||
|
||||
return tableContents;
|
||||
}, [type, counts, fullSize, game, scores]);
|
||||
}, [metric, counts, fullSize, game, scores]);
|
||||
|
||||
if (tableContents.length === 0) {
|
||||
return (
|
||||
@@ -349,7 +334,7 @@ function BreakdownChartContents({
|
||||
chartMap,
|
||||
fullSize,
|
||||
gptConfig,
|
||||
type,
|
||||
metric,
|
||||
scores,
|
||||
setScores,
|
||||
}: {
|
||||
@@ -360,7 +345,7 @@ function BreakdownChartContents({
|
||||
songMap: Map<integer, SongDocument>;
|
||||
chartMap: Map<string, ChartDocument>;
|
||||
gptConfig: GamePTConfig;
|
||||
type: "lamp" | "grade";
|
||||
metric: string;
|
||||
scores: Array<ScoreDocument>;
|
||||
setScores?: SetScores;
|
||||
}) {
|
||||
@@ -425,25 +410,24 @@ function BreakdownChartContents({
|
||||
let preScoreCell = <td>No Play</td>;
|
||||
|
||||
if (!scoreInfo.isNewScore) {
|
||||
const oldGradeIndex = score.scoreData.gradeIndex - scoreInfo.gradeDelta;
|
||||
const oldLampIndex = score.scoreData.lampIndex - scoreInfo.lampDelta;
|
||||
const newScoreData = score.scoreData;
|
||||
|
||||
for (const [k, d] of Object.entries(scoreInfo.deltas)) {
|
||||
// @ts-expect-error ugh
|
||||
if (typeof score.scoreData[k] === "string") {
|
||||
// @ts-expect-error ugh
|
||||
newScoreData.enumIndexes[k] = score.scoreData.enumIndexes[k] - d;
|
||||
} else {
|
||||
// @ts-expect-error ugh
|
||||
newScoreData[k] = score.scoreData[k] - d;
|
||||
}
|
||||
}
|
||||
|
||||
const mockScore = deepmerge(score, {
|
||||
scoreData: {
|
||||
score: score.scoreData.score - scoreInfo.scoreDelta,
|
||||
percent: score.scoreData.percent - scoreInfo.percentDelta,
|
||||
grade: gptConfig.grades[oldGradeIndex],
|
||||
gradeIndex: oldGradeIndex,
|
||||
lamp: gptConfig.lamps[oldLampIndex],
|
||||
lampIndex: oldLampIndex,
|
||||
},
|
||||
scoreData: newScoreData,
|
||||
}) as ScoreDocument;
|
||||
|
||||
if (type === "grade") {
|
||||
preScoreCell = <ScoreCell score={mockScore} />;
|
||||
} else {
|
||||
preScoreCell = <LampCell score={mockScore} />;
|
||||
}
|
||||
preScoreCell = <ScoreCoreCells chart={chart} game={game} score={mockScore} />;
|
||||
}
|
||||
|
||||
if (score) {
|
||||
@@ -453,11 +437,7 @@ function BreakdownChartContents({
|
||||
<DifficultyCell chart={chart} game={game} />
|
||||
{preScoreCell}
|
||||
<td>⟶</td>
|
||||
{type === "grade" ? (
|
||||
<ScoreCell {...{ score, game, playtype: score.playtype }} />
|
||||
) : (
|
||||
<LampCell score={score} />
|
||||
)}
|
||||
<ScoreCoreCells chart={chart} game={game} score={score} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import Divider from "components/util/Divider";
|
||||
import Muted from "components/util/Muted";
|
||||
import useLUGPTSettings from "components/util/useLUGPTSettings";
|
||||
import React from "react";
|
||||
import { BMS_TABLES, ChartDocument, COLOUR_SET, UGPTSettings } from "tachi-common";
|
||||
import TierlistInfoPart from "./TierlistInfoPart";
|
||||
import { BMS_TABLES, ChartDocument, COLOUR_SET, UGPTSettingsDocument } from "tachi-common";
|
||||
import RatingSystemPart from "./RatingSystemPart";
|
||||
|
||||
export default function BMSOrPMSDifficultyCell({
|
||||
chart,
|
||||
@@ -15,7 +15,9 @@ export default function BMSOrPMSDifficultyCell({
|
||||
chart: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">;
|
||||
game: "bms" | "pms";
|
||||
}) {
|
||||
const { settings } = useLUGPTSettings() as { settings: UGPTSettings<"bms:7K" | "bms:14K"> };
|
||||
const { settings } = useLUGPTSettings() as {
|
||||
settings: UGPTSettingsDocument<"bms:7K" | "bms:14K">;
|
||||
};
|
||||
|
||||
const hasLevel = chart.data.tableFolders.length > 0;
|
||||
|
||||
@@ -31,10 +33,10 @@ export default function BMSOrPMSDifficultyCell({
|
||||
|
||||
if (settings?.preferences.gameSpecific.displayTables) {
|
||||
filteredTables = tables.filter(
|
||||
(e) => !settings.preferences.gameSpecific.displayTables.includes(e.table)
|
||||
(e) => !settings.preferences.gameSpecific.displayTables!.includes(e.table)
|
||||
);
|
||||
tables = tables.filter((e) =>
|
||||
settings.preferences.gameSpecific.displayTables.includes(e.table)
|
||||
settings.preferences.gameSpecific.displayTables!.includes(e.table)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,7 +83,7 @@ export default function BMSOrPMSDifficultyCell({
|
||||
<span>{levelText}</span>
|
||||
)}
|
||||
|
||||
<TierlistInfoPart chart={chart} game={game} />
|
||||
<RatingSystemPart chart={chart} game={game} />
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { IsNotNullish } from "util/misc";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export default function BMSOrPMSLampCell({
|
||||
score,
|
||||
@@ -10,19 +11,17 @@ export default function BMSOrPMSLampCell({
|
||||
| ScoreDocument<"bms:7K" | "bms:14K" | "pms:Keyboard" | "pms:Controller">
|
||||
| PBScoreDocument<"bms:7K" | "bms:14K" | "pms:Keyboard" | "pms:Controller">;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.lampColours[score.scoreData.lamp], 0.2),
|
||||
backgroundColor: ChangeOpacity(GetEnumColour(score, "lamp"), 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.lamp}</strong>
|
||||
{IsNotNullish(score.scoreData.hitMeta.bp) && (
|
||||
{IsNotNullish(score.scoreData.optional.bp) && (
|
||||
<>
|
||||
<br />
|
||||
<small>[BP: {score.scoreData.hitMeta.bp}]</small>
|
||||
<small>[BP: {score.scoreData.optional.bp}]</small>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetGradeFromPercent, IsNullish } from "util/misc";
|
||||
import { IsNullish } from "util/misc";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Divider from "components/util/Divider";
|
||||
import Muted from "components/util/Muted";
|
||||
@@ -6,7 +6,15 @@ import useLUGPTSettings from "components/util/useLUGPTSettings";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import React, { useContext } from "react";
|
||||
import { PoyashiBPI } from "rg-stats";
|
||||
import { ChartDocument, integer, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import {
|
||||
ChartDocument,
|
||||
GetGrade,
|
||||
IIDXLIKE_GBOUNDARIES,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
import DeltaCell from "./DeltaCell";
|
||||
import ScoreCell from "./ScoreCell";
|
||||
@@ -135,22 +143,15 @@ export default function BPICell({
|
||||
function FormatAverage(score: integer, playtype: "SP" | "DP", notecount: integer) {
|
||||
const percent = (100 * score) / (notecount * 2);
|
||||
|
||||
const grade = GetGradeFromPercent("iidx", playtype, percent);
|
||||
const grade = GetGrade(IIDXLIKE_GBOUNDARIES, percent);
|
||||
|
||||
return {
|
||||
score: (
|
||||
<ScoreCell
|
||||
score={
|
||||
{
|
||||
game: "iidx",
|
||||
playtype,
|
||||
scoreData: {
|
||||
grade,
|
||||
percent,
|
||||
score,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
colour={GPT_CLIENT_IMPLEMENTATIONS[`iidx:${playtype}`].enumColours.grade[grade]}
|
||||
grade={grade}
|
||||
percent={percent}
|
||||
score={score}
|
||||
/>
|
||||
),
|
||||
delta: (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GenericFormatGradeDelta } from "util/grade-deltas";
|
||||
import React from "react";
|
||||
import { Game, Grades, IDStrings, Playtypes } from "tachi-common";
|
||||
import { Game, Grades, GPTString, Playtypes } from "tachi-common";
|
||||
|
||||
export default function DeltaCell({
|
||||
game,
|
||||
@@ -13,7 +13,7 @@ export default function DeltaCell({
|
||||
playtype: Playtypes[Game];
|
||||
score: number;
|
||||
percent: number;
|
||||
grade: Grades[IDStrings];
|
||||
grade: Grades[GPTString];
|
||||
}) {
|
||||
if (score === 0) {
|
||||
return <td>N/A</td>;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
GetGamePTConfig,
|
||||
} from "tachi-common";
|
||||
import BMSOrPMSDifficultyCell from "./BMSOrPMSDifficultyCell";
|
||||
import TierlistInfoPart from "./TierlistInfoPart";
|
||||
import RatingSystemPart from "./RatingSystemPart";
|
||||
import USCDifficultyCell from "./USCDifficultyCell";
|
||||
import ITGDifficultyCell from "./ITGDifficultyCell";
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function DifficultyCell({
|
||||
</QuickTooltip>
|
||||
)}
|
||||
{!noTierlist && Object.keys(chart.tierlistInfo).length > 0 && (
|
||||
<TierlistInfoPart chart={chart} game={game} />
|
||||
<RatingSystemPart chart={chart} game={game} />
|
||||
)}
|
||||
{!chart.isPrimary && (
|
||||
<QuickTooltip tooltipContent="This chart is an alternate, old chart.">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IsScore } from "util/asserts";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { IsNotNullish } from "util/misc";
|
||||
import { ChartDocument, GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import { ChartDocument, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export default function IIDXLampCell({
|
||||
sc,
|
||||
@@ -11,33 +12,36 @@ export default function IIDXLampCell({
|
||||
sc: ScoreDocument<"iidx:SP" | "iidx:DP"> | PBScoreDocument<"iidx:SP" | "iidx:DP">;
|
||||
chart: ChartDocument<"iidx:SP" | "iidx:DP">;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig("iidx", sc.playtype);
|
||||
|
||||
let gaugeText = null;
|
||||
|
||||
if (sc.scoreData.hitMeta.gsm) {
|
||||
if (
|
||||
sc.scoreData.optional.gsmEXHard &&
|
||||
sc.scoreData.optional.gsmHard &&
|
||||
sc.scoreData.optional.gsmNormal &&
|
||||
sc.scoreData.optional.gsmEasy
|
||||
) {
|
||||
gaugeText = `EC: ${
|
||||
sc.scoreData.hitMeta.gsm.EASY[sc.scoreData.hitMeta.gsm.EASY.length - 1]
|
||||
}%, NC: ${sc.scoreData.hitMeta.gsm.NORMAL[sc.scoreData.hitMeta.gsm.NORMAL.length - 1]}%`;
|
||||
sc.scoreData.optional.gsmEasy[sc.scoreData.optional.gsmEasy.length - 1]
|
||||
}%, NC: ${sc.scoreData.optional.gsmNormal[sc.scoreData.optional.gsmNormal.length - 1]}%`;
|
||||
} else if (IsScore(sc)) {
|
||||
if (sc.scoreMeta.gauge === "EASY") {
|
||||
gaugeText = `EC: ${sc.scoreData.hitMeta.gauge}%`;
|
||||
gaugeText = `EC: ${sc.scoreData.optional.gauge}%`;
|
||||
} else if (sc.scoreMeta.gauge === "NORMAL") {
|
||||
gaugeText = `NC: ${sc.scoreData.hitMeta.gauge}%`;
|
||||
gaugeText = `NC: ${sc.scoreData.optional.gauge}%`;
|
||||
}
|
||||
}
|
||||
|
||||
let bpText;
|
||||
|
||||
if (IsNotNullish(sc.scoreData.hitMeta.bp)) {
|
||||
bpText = `[BP: ${sc.scoreData.hitMeta.bp}]`;
|
||||
if (IsNotNullish(sc.scoreData.optional.bp)) {
|
||||
bpText = `[BP: ${sc.scoreData.optional.bp}]`;
|
||||
}
|
||||
|
||||
let cbrkCount;
|
||||
let cbrkText;
|
||||
|
||||
if (sc.scoreData.hitMeta.comboBreak) {
|
||||
cbrkCount = sc.scoreData.hitMeta.comboBreak;
|
||||
if (sc.scoreData.optional.comboBreak) {
|
||||
cbrkCount = sc.scoreData.optional.comboBreak;
|
||||
} else if (
|
||||
IsNotNullish(sc.scoreData.judgements.pgreat) &&
|
||||
IsNotNullish(sc.scoreData.judgements.great) &&
|
||||
@@ -57,7 +61,7 @@ export default function IIDXLampCell({
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.lampColours[sc.scoreData.lamp], 0.2),
|
||||
backgroundColor: ChangeOpacity(GetEnumColour(sc, "lamp"), 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.lamp}</strong>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Muted from "components/util/Muted";
|
||||
import React from "react";
|
||||
import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
import TierlistInfoPart from "./TierlistInfoPart";
|
||||
import RatingSystemPart from "./RatingSystemPart";
|
||||
|
||||
export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg:Stamina"> }) {
|
||||
const gptConfig = GetGamePTConfig("itg", "Stamina");
|
||||
@@ -45,7 +45,7 @@ export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg
|
||||
</span>
|
||||
<br />
|
||||
<Muted>{chart.data.charter}</Muted>
|
||||
<TierlistInfoPart chart={chart} game="itg" />
|
||||
<RatingSystemPart chart={chart} game="itg" />
|
||||
</div>
|
||||
</QuickTooltip>
|
||||
</td>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
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);
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export default function JubeatScoreCell({
|
||||
sc,
|
||||
}: {
|
||||
sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">;
|
||||
}) {
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
|
||||
backgroundColor: ChangeOpacity(
|
||||
GPT_CLIENT_IMPLEMENTATIONS["jubeat:Single"].enumColours.grade[
|
||||
sc.scoreData.grade
|
||||
],
|
||||
0.2
|
||||
),
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.grade}</strong>
|
||||
<br />
|
||||
<b>{sc.scoreData.percent.toFixed(2)}%</b>
|
||||
<b>{sc.scoreData.musicRate.toFixed(2)}%</b>
|
||||
<br />
|
||||
{FormatMillions(sc.scoreData.score)}
|
||||
</td>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export default function LampCell({ score }: { score: ScoreDocument | PBScoreDocument }) {
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
|
||||
export default function LampCell({ lamp, colour }: { lamp: string; colour: string }) {
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.lampColours[score.scoreData.lamp], 0.2),
|
||||
backgroundColor: ChangeOpacity(colour, 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.lamp}</strong>
|
||||
<strong>{lamp}</strong>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
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 MillionsScoreCell({ score }: { score: PBScoreDocument | ScoreDocument }) {
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument, integer } from "tachi-common";
|
||||
|
||||
export default function MillionsScoreCell({
|
||||
score,
|
||||
colour,
|
||||
grade,
|
||||
}: {
|
||||
score: integer;
|
||||
grade: string;
|
||||
colour: string;
|
||||
}) {
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[score.scoreData.grade], 0.2),
|
||||
backgroundColor: ChangeOpacity(colour, 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.grade}</strong>
|
||||
<strong>{grade}</strong>
|
||||
<br />
|
||||
{FormatMillions(score.scoreData.score)}
|
||||
{FormatMillions(score)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
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";
|
||||
import { ConfScoreMetric } from "tachi-common/types/metrics";
|
||||
|
||||
export default function PBCompareCell({
|
||||
base,
|
||||
compare,
|
||||
game,
|
||||
compareType,
|
||||
shouldESD,
|
||||
metricConf,
|
||||
metric,
|
||||
}: {
|
||||
base: PBScoreDocument | null;
|
||||
compare: PBScoreDocument | null;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
compareType: "score" | "lamp";
|
||||
shouldESD: boolean;
|
||||
metricConf: ConfScoreMetric;
|
||||
metric: string;
|
||||
}) {
|
||||
let status: "win" | "draw" | "lose";
|
||||
|
||||
@@ -27,10 +26,12 @@ export default function PBCompareCell({
|
||||
status = "lose";
|
||||
} else if (!compare) {
|
||||
status = "win";
|
||||
} else if (compareType === "score") {
|
||||
status = cmp(base.scoreData.score, compare.scoreData.score);
|
||||
} else if (metricConf.type === "ENUM") {
|
||||
// @ts-expect-error yeah this index will work sorry
|
||||
status = cmp(base.scoreData.enumIndexes[metric], compare.scoreData.enumIndexes[metric]);
|
||||
} else {
|
||||
status = cmp(base.scoreData.lampIndex, compare.scoreData.lampIndex);
|
||||
// @ts-expect-error yeah this index will work sorry
|
||||
status = cmp(base.scoreData[metric], compare.scoreData[metric]);
|
||||
}
|
||||
|
||||
let scoreRenderFn = (k: number) => k.toString();
|
||||
@@ -68,14 +69,6 @@ export default function PBCompareCell({
|
||||
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={{
|
||||
@@ -89,7 +82,9 @@ export default function PBCompareCell({
|
||||
borderRight: "1px black double",
|
||||
}}
|
||||
>
|
||||
{compareType === "score" ? (
|
||||
{metricConf.type === "ENUM" ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
{shouldPrioritisePercent ? (
|
||||
<>
|
||||
@@ -114,22 +109,6 @@ export default function PBCompareCell({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{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>
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { ToCDNURL } from "util/api";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export default function PopnLampCell({
|
||||
score,
|
||||
}: {
|
||||
score: ScoreDocument<"popn:9B"> | PBScoreDocument<"popn:9B">;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.lampColours[score.scoreData.lamp], 0.2),
|
||||
backgroundColor: ChangeOpacity(
|
||||
GPT_CLIENT_IMPLEMENTATIONS["popn:9B"].enumColours.lamp[score.scoreData.lamp],
|
||||
0.2
|
||||
),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.lamp}</strong>
|
||||
<br />
|
||||
{score.scoreData.hitMeta.specificClearType && (
|
||||
<img
|
||||
style={{
|
||||
maxWidth: "32px",
|
||||
}}
|
||||
src={ToCDNURL(`/misc/popn/${score.scoreData.hitMeta.specificClearType}.png`)}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
style={{
|
||||
maxWidth: "32px",
|
||||
}}
|
||||
src={ToCDNURL(`/misc/popn/${score.scoreData.clearMedal}.png`)}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Icon from "components/util/Icon";
|
||||
import Muted from "components/util/Muted";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import { ChartDocument, Game, GetGPTString } from "tachi-common";
|
||||
import { GPTRatingSystem } from "lib/types";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
|
||||
export default function RatingSystemPart({ chart, game }: { chart: ChartDocument; game: Game }) {
|
||||
const ratingSystems: Array<GPTRatingSystem<any>> =
|
||||
GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, chart.playtype)].ratingSystems;
|
||||
|
||||
// don't render anything if this game has no other rating systems defined
|
||||
// for this chart
|
||||
if (ratingSystems.filter((e) => typeof e.toNumber(chart) === "number").length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-none d-lg-block">
|
||||
<QuickTooltip
|
||||
tooltipContent={
|
||||
<MiniTable headers={["Ratings"]} colSpan={2}>
|
||||
{ratingSystems.map((e) => {
|
||||
const strV = e.toString(chart);
|
||||
const numV = e.toNumber(chart);
|
||||
|
||||
if (
|
||||
strV === null ||
|
||||
strV === undefined ||
|
||||
numV === null ||
|
||||
numV === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={e.name}>
|
||||
<td>{e}</td>
|
||||
<td>
|
||||
{strV} <Muted>({numV.toFixed(2)})</Muted>
|
||||
{/* @ts-expect-error utterly silly types */}
|
||||
{e.idvDifference(chart) && (
|
||||
<>
|
||||
<br />
|
||||
<QuickTooltip tooltipContent="Individual Difference - The difficulty of this varies massively between people!">
|
||||
<span>
|
||||
<Icon type="balance-scale-left" />
|
||||
</span>
|
||||
</QuickTooltip>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</MiniTable>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Muted>
|
||||
{ratingSystems.map((r) => r.toString(chart as any)).join(" / ")}
|
||||
</Muted>
|
||||
</div>
|
||||
</QuickTooltip>
|
||||
</div>
|
||||
<div className="d-block d-lg-none">
|
||||
<Muted>
|
||||
{ratingSystems.map((r) => (
|
||||
<React.Fragment key={r.name}>
|
||||
{r.toString(chart as any)}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Muted>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
// Lamp cell, but if the lamp is FAILED, display it slightly differently.
|
||||
export default function SDVXLampCell({
|
||||
@@ -10,12 +11,10 @@ export default function SDVXLampCell({
|
||||
| ScoreDocument<"sdvx:Single" | "usc:Controller" | "usc:Keyboard">
|
||||
| PBScoreDocument<"sdvx:Single" | "usc:Controller" | "usc:Keyboard">;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.lampColours[score.scoreData.lamp], 0.2),
|
||||
backgroundColor: ChangeOpacity(GetEnumColour(score, "lamp"), 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.lamp === "FAILED" ? "PLAYED" : score.scoreData.lamp}</strong>
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export default function ScoreCell({
|
||||
score,
|
||||
showScore = true,
|
||||
colour,
|
||||
grade,
|
||||
percent,
|
||||
scoreRenderFn,
|
||||
}: {
|
||||
score: PBScoreDocument | ScoreDocument;
|
||||
score: integer;
|
||||
percent: number;
|
||||
grade: string;
|
||||
colour: string;
|
||||
showScore?: boolean;
|
||||
scoreRenderFn?: (s: number) => string;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[score.scoreData.grade], 0.2),
|
||||
backgroundColor: ChangeOpacity(colour, 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.grade}</strong>
|
||||
<strong>{grade}</strong>
|
||||
<br />
|
||||
{`${score.scoreData.percent.toFixed(2)}%`}
|
||||
{showScore && (
|
||||
<>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
[
|
||||
{scoreRenderFn
|
||||
? scoreRenderFn(score.scoreData.score)
|
||||
: score.scoreData.score}
|
||||
]
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
{`${percent.toFixed(2)}%`}
|
||||
|
||||
<br />
|
||||
<small className="text-muted">[{scoreRenderFn ? scoreRenderFn(score) : score}]</small>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { FormatMillions } from "util/misc";
|
||||
import React from "react";
|
||||
import { Game, PBScoreDocument, ScoreDocument, Playtype } from "tachi-common";
|
||||
import ScoreCell from "./ScoreCell";
|
||||
|
||||
export default function ScoreCellLarge(props: {
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
}) {
|
||||
return <ScoreCell {...props} scoreRenderFn={FormatMillions} />;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Icon from "components/util/Icon";
|
||||
import Muted from "components/util/Muted";
|
||||
import React from "react";
|
||||
import { ChartDocument, Game, GetGamePTConfig } from "tachi-common";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
|
||||
// I hate the fact object.keys doesnt infer types.
|
||||
type K = keyof ChartDocument["tierlistInfo"];
|
||||
|
||||
export default function TierlistInfoPart({ chart, game }: { chart: ChartDocument; game: Game }) {
|
||||
const keys = GetGamePTConfig(game, chart.playtype).tierlists.filter(
|
||||
(e) => chart.tierlistInfo[e]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-none d-lg-block">
|
||||
<QuickTooltip
|
||||
tooltipContent={
|
||||
<MiniTable className="table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tierlist</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map((k) => (
|
||||
<tr key={k}>
|
||||
<td>{k}</td>
|
||||
<td>
|
||||
{chart.tierlistInfo[k]!.text} (
|
||||
{chart.tierlistInfo[k]!.value.toFixed(2)})
|
||||
{chart.tierlistInfo[k]!.individualDifference && (
|
||||
<>
|
||||
<br />
|
||||
<Icon type="balance-scale-left" />
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</MiniTable>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Muted>{keys.map((k) => chart.tierlistInfo[k]!.text).join(" / ")}</Muted>
|
||||
</div>
|
||||
</QuickTooltip>
|
||||
</div>
|
||||
<div className="d-block d-lg-none">
|
||||
<Muted>
|
||||
{keys.map((k) => (
|
||||
<React.Fragment key={k}>
|
||||
{chart.tierlistInfo[k]!.text}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Muted>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Icon from "components/util/Icon";
|
||||
import React from "react";
|
||||
import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common";
|
||||
import TierlistInfoPart from "./TierlistInfoPart";
|
||||
import RatingSystemPart from "./RatingSystemPart";
|
||||
|
||||
export default function USCDifficultyCell({
|
||||
chart,
|
||||
@@ -29,7 +29,7 @@ export default function USCDifficultyCell({
|
||||
}}
|
||||
>
|
||||
<span>{levelText}</span>
|
||||
<TierlistInfoPart chart={chart} game="usc" />
|
||||
<RatingSystemPart chart={chart} game="usc" />
|
||||
{!chart.isPrimary && (
|
||||
<QuickTooltip tooltipContent="This chart is an alternate, old chart.">
|
||||
<div>
|
||||
|
||||
@@ -3,9 +3,15 @@ import useLUGPTSettings from "components/util/useLUGPTSettings";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import React, { useContext } from "react";
|
||||
import { Volforce } from "rg-stats";
|
||||
import { ChartDocument, GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import {
|
||||
ChartDocument,
|
||||
GetGPTString,
|
||||
GetSpecificGPTConfig,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
type VF6IDStrings = "sdvx:Single" | "usc:Keyboard" | "usc:Controller";
|
||||
type VF6GPTString = "sdvx:Single" | "usc:Keyboard" | "usc:Controller";
|
||||
|
||||
const SHORT_LAMPS = {
|
||||
CLEAR: "CLR",
|
||||
@@ -17,11 +23,11 @@ export default function VF6Cell({
|
||||
score,
|
||||
chart,
|
||||
}: {
|
||||
score: ScoreDocument<VF6IDStrings> | PBScoreDocument<VF6IDStrings>;
|
||||
chart: ChartDocument<VF6IDStrings>;
|
||||
score: ScoreDocument<VF6GPTString> | PBScoreDocument<VF6GPTString>;
|
||||
chart: ChartDocument<VF6GPTString>;
|
||||
}) {
|
||||
const { user } = useContext(UserContext);
|
||||
const { settings } = useLUGPTSettings<VF6IDStrings>();
|
||||
const { settings } = useLUGPTSettings<VF6GPTString>();
|
||||
|
||||
if (IsNullish(score.calculatedData.VF6)) {
|
||||
return <td>N/A</td>;
|
||||
@@ -29,13 +35,18 @@ export default function VF6Cell({
|
||||
|
||||
const vf6Target = settings?.preferences.gameSpecific.vf6Target;
|
||||
|
||||
const gptConfig = GetGamePTConfig(score.game, score.playtype);
|
||||
const gptConfig = GetSpecificGPTConfig<VF6GPTString>(
|
||||
GetGPTString(score.game, score.playtype) as VF6GPTString
|
||||
);
|
||||
|
||||
const targets: Record<string, number | null> = {};
|
||||
|
||||
if (vf6Target && score.userID === user?.id) {
|
||||
for (const lamp of ["CLEAR", "EXCESSIVE CLEAR", "ULTIMATE CHAIN"] as const) {
|
||||
if (score.scoreData.lampIndex <= gptConfig.lamps.indexOf(lamp)) {
|
||||
if (
|
||||
score.scoreData.enumIndexes.lamp <=
|
||||
gptConfig.providedMetrics.lamp.values.indexOf(lamp)
|
||||
) {
|
||||
const expectedScore = InverseVF6(vf6Target, lamp, chart.levelNum);
|
||||
|
||||
if (expectedScore === null) {
|
||||
|
||||
@@ -3,20 +3,20 @@ import { SetState } from "types/react";
|
||||
import { ZTableTHProps } from "./TachiTable";
|
||||
|
||||
export default function SelectableCompareType({
|
||||
compareType,
|
||||
setCompareType,
|
||||
metric,
|
||||
setMetric,
|
||||
changeSort,
|
||||
currentSortMode,
|
||||
reverseSort,
|
||||
}: {
|
||||
compareType: "score" | "lamp";
|
||||
setCompareType: SetState<"score" | "lamp">;
|
||||
metric: string;
|
||||
setMetric: SetState<string>;
|
||||
} & ZTableTHProps) {
|
||||
return (
|
||||
<th>
|
||||
<select
|
||||
onChange={(v) => setCompareType(v.target.value as "score" | "lamp")}
|
||||
value={compareType}
|
||||
onChange={(v) => setMetric(v.target.value)}
|
||||
value={metric}
|
||||
style={{
|
||||
backgroundColor: "#131313",
|
||||
border: "none",
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import { UppercaseFirst } from "util/misc";
|
||||
import React from "react";
|
||||
import {
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
Playtypes,
|
||||
ScoreCalculatedDataLookup,
|
||||
} from "tachi-common";
|
||||
import { Game, GetGamePTConfig, GPTString, Playtypes, ScoreRatingAlgorithms } from "tachi-common";
|
||||
import { SetState } from "types/react";
|
||||
import { ZTableTHProps } from "./TachiTable";
|
||||
|
||||
// type AllRatings<I extends IDStrings> =
|
||||
// | ScoreCalculatedDataLookup[I]
|
||||
// | SessionCalculatedDataLookup[I]
|
||||
// | UGSRatingsLookup[I];
|
||||
|
||||
// hack to get everything to work
|
||||
type AllRatings<I> = any;
|
||||
|
||||
export default function SelectableRating<I extends IDStrings>({
|
||||
export default function SelectableRating<GPT extends GPTString>({
|
||||
game,
|
||||
playtype,
|
||||
rating,
|
||||
@@ -30,8 +19,8 @@ export default function SelectableRating<I extends IDStrings>({
|
||||
}: {
|
||||
game: Game;
|
||||
playtype: Playtypes[Game];
|
||||
rating: AllRatings<I>;
|
||||
setRating: SetState<AllRatings<I>>;
|
||||
rating: AllRatings<GPT>;
|
||||
setRating: SetState<AllRatings<GPT>>;
|
||||
mode?: "score" | "session" | "profile";
|
||||
} & ZTableTHProps) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
@@ -48,7 +37,7 @@ export default function SelectableRating<I extends IDStrings>({
|
||||
return (
|
||||
<th>
|
||||
<select
|
||||
onChange={(v) => setRating(v.target.value as AllRatings<I>)}
|
||||
onChange={(v) => setRating(v.target.value as AllRatings<GPT>)}
|
||||
value={rating}
|
||||
style={{
|
||||
backgroundColor: "#131313",
|
||||
@@ -59,7 +48,7 @@ export default function SelectableRating<I extends IDStrings>({
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{gptConfig[key].map((s) => (
|
||||
{Object.keys(gptConfig[key]).map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{UppercaseFirst(s)}
|
||||
</option>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UserContext } from "context/UserContext";
|
||||
import React, { useContext, useMemo, useReducer, useState } from "react";
|
||||
import {
|
||||
ChartDocument,
|
||||
IDStrings,
|
||||
GPTString,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
@@ -27,14 +27,14 @@ import RivalCompare from "./components/RivalCompare";
|
||||
import TargetInfo from "./components/TargetInfo";
|
||||
import { GPTDropdownSettings } from "./GPTDropdownSettings";
|
||||
|
||||
export interface ScoreDropdownProps<I extends IDStrings = IDStrings> {
|
||||
score: ScoreDocument<I> | PBScoreDocument<I>;
|
||||
export interface ScoreDropdownProps {
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
scoreState: ScoreState;
|
||||
pbData: UGPTChartPBComposition<I>;
|
||||
chart: ChartDocument<I>;
|
||||
pbData: UGPTChartPBComposition;
|
||||
chart: ChartDocument;
|
||||
}
|
||||
|
||||
export default function PBDropdown<I extends IDStrings = IDStrings>({
|
||||
export default function PBDropdown({
|
||||
game,
|
||||
playtype,
|
||||
chart,
|
||||
@@ -47,7 +47,7 @@ export default function PBDropdown<I extends IDStrings = IDStrings>({
|
||||
chart: ChartDocument;
|
||||
song: SongDocument;
|
||||
scoreState: ScoreState;
|
||||
defaultView?: "pb" | "scorePB" | "lampPB" | "history" | "debug" | "rivals" | "targets";
|
||||
defaultView?: "pb" | `otherPB::${string}` | "history" | "debug" | "rivals" | "targets";
|
||||
} & GamePT) {
|
||||
const { user: currentUser } = useContext(UserContext);
|
||||
|
||||
@@ -56,11 +56,11 @@ export default function PBDropdown<I extends IDStrings = IDStrings>({
|
||||
|
||||
const [view, setView] = useState(defaultView);
|
||||
|
||||
const { data, error } = useApiQuery<UGPTChartPBComposition<I>>(
|
||||
const { data, error } = useApiQuery<UGPTChartPBComposition>(
|
||||
`/users/${userID}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
|
||||
);
|
||||
|
||||
const { error: histError, data: histData } = useApiQuery<ScoreDocument<I>[]>(
|
||||
const { error: histError, data: histData } = useApiQuery<ScoreDocument[]>(
|
||||
`/users/${userID}/games/${game}/${playtype}/scores/${chart.chartID}`
|
||||
);
|
||||
|
||||
@@ -76,32 +76,28 @@ export default function PBDropdown<I extends IDStrings = IDStrings>({
|
||||
currentUser === null
|
||||
);
|
||||
|
||||
const currentScoreDoc: ScoreDocument<I> | PBScoreDocument<I> | null = useMemo(() => {
|
||||
const currentScoreDoc: ScoreDocument | PBScoreDocument | null = useMemo(() => {
|
||||
if (!data) {
|
||||
// dont worry about this null, it never gets below the rquery checks
|
||||
return null;
|
||||
}
|
||||
|
||||
if (view === "pb") {
|
||||
if (data.pb.composedFrom.lampPB === data.pb.composedFrom.scorePB) {
|
||||
if (data.pb.composedFrom.length === 1) {
|
||||
// scores have more information than PBs.
|
||||
// In this case, the PB is only composed of one score,
|
||||
// so we should default to this instead.
|
||||
return data.scores.filter((e) => e.scoreID === data.pb.composedFrom.lampPB)[0];
|
||||
return data.scores.find((e) => e.scoreID === data.pb.composedFrom[0]!.scoreID)!;
|
||||
}
|
||||
|
||||
return data.pb;
|
||||
} else if (view.startsWith("otherPB::")) {
|
||||
const scoreID = view.split("otherPB::")[1];
|
||||
|
||||
return data.scores.filter((e) => e.scoreID === scoreID)[0];
|
||||
}
|
||||
|
||||
const idMap = {
|
||||
scorePB: data.pb.composedFrom.scorePB,
|
||||
lampPB: data.pb.composedFrom.lampPB,
|
||||
...Object.fromEntries(
|
||||
(data.pb.composedFrom.other ?? []).map((e) => [e.name, e.scoreID])
|
||||
),
|
||||
};
|
||||
|
||||
// @ts-expect-error awful
|
||||
return data.scores.filter((e) => e.scoreID === idMap[view])[0];
|
||||
return null;
|
||||
}, [view, data]);
|
||||
|
||||
if (error) {
|
||||
@@ -116,8 +112,7 @@ export default function PBDropdown<I extends IDStrings = IDStrings>({
|
||||
);
|
||||
}
|
||||
|
||||
// @TODO this doesn't support alternate PBs.
|
||||
const isComposedFromSingleScore = data.pb.composedFrom.lampPB === data.pb.composedFrom.scorePB;
|
||||
const isComposedFromSingleScore = data.pb.composedFrom.length === 1;
|
||||
|
||||
let body;
|
||||
|
||||
@@ -174,14 +169,16 @@ export default function PBDropdown<I extends IDStrings = IDStrings>({
|
||||
</SelectButton>
|
||||
{!isComposedFromSingleScore && (
|
||||
<>
|
||||
<SelectButton setValue={setView} value={view} id="scorePB">
|
||||
<Icon type="star-half-alt" />
|
||||
Best Score
|
||||
</SelectButton>
|
||||
<SelectButton setValue={setView} value={view} id="lampPB">
|
||||
<Icon type="lightbulb" />
|
||||
Best Lamp
|
||||
</SelectButton>
|
||||
{data.pb.composedFrom.map((e) => (
|
||||
<SelectButton
|
||||
setValue={setView}
|
||||
value={view}
|
||||
id={`otherPB::${e.scoreID}`}
|
||||
>
|
||||
<Icon type="star-half-alt" />
|
||||
{e.name}
|
||||
</SelectButton>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<SelectButton setValue={setView} value={view} id="history">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UserSettingsContext } from "context/UserSettingsContext";
|
||||
import React, { useContext, useReducer, useState } from "react";
|
||||
import {
|
||||
ChartDocument,
|
||||
IDStrings,
|
||||
GPTString,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
SongDocument,
|
||||
@@ -32,12 +32,12 @@ export interface ScoreState {
|
||||
setHighlight: SetState<boolean>;
|
||||
}
|
||||
|
||||
export interface ScoreDropdownProps<I extends IDStrings = IDStrings> {
|
||||
score: ScoreDocument<I> | PBScoreDocument<I>;
|
||||
export interface ScoreDropdownProps {
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
scoreState: ScoreState;
|
||||
}
|
||||
|
||||
export default function ScoreDropdown<I extends IDStrings = IDStrings>({
|
||||
export default function ScoreDropdown({
|
||||
game,
|
||||
playtype,
|
||||
user,
|
||||
@@ -65,11 +65,11 @@ export default function ScoreDropdown<I extends IDStrings = IDStrings>({
|
||||
const { user: currentUser } = useContext(UserContext);
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
const { data, error } = useApiQuery<UGPTChartPBComposition<I>>(
|
||||
const { data, error } = useApiQuery<UGPTChartPBComposition>(
|
||||
`/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
|
||||
);
|
||||
|
||||
const { error: histError, data: histData } = useApiQuery<ScoreDocument<I>[]>(
|
||||
const { error: histError, data: histData } = useApiQuery<ScoreDocument[]>(
|
||||
`/users/${user.id}/games/${game}/${playtype}/scores/${chart.chartID}`
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function BMSGraphsComponent({
|
||||
score: ScoreDocument<"bms:7K" | "bms:14K"> | PBScoreDocument<"bms:7K" | "bms:14K">;
|
||||
chart: ChartDocument<"bms:7K" | "bms:14K">;
|
||||
}) {
|
||||
const gaugeHistory = score.scoreData.hitMeta.gaugeHistory;
|
||||
const gaugeHistory = score.scoreData.optional.gaugeHistory;
|
||||
|
||||
const [lamp, setLamp] = useState<LampTypes>(LampToKey(score));
|
||||
|
||||
@@ -126,7 +126,7 @@ function LampToKey(
|
||||
return "EX_HARD";
|
||||
} else if (lamp === "FULL COMBO") {
|
||||
// @hack - attempt to guess what gauge they used?
|
||||
if ((score.scoreData.hitMeta.gaugeHistory?.[0] ?? 0) > 22) {
|
||||
if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) {
|
||||
return "EX_HARD";
|
||||
}
|
||||
return "NORMAL";
|
||||
|
||||
@@ -43,9 +43,9 @@ export function IIDXGraphsComponent({
|
||||
|
||||
let gaugeStatus: "none" | "single" | "gsm" = "none";
|
||||
|
||||
if (score.scoreData.hitMeta.gsm) {
|
||||
if (score.scoreData.optional.gsm) {
|
||||
gaugeStatus = "gsm";
|
||||
} else if (score.scoreData.hitMeta.gaugeHistory) {
|
||||
} else if (score.scoreData.optional.gaugeHistory) {
|
||||
gaugeStatus = "single";
|
||||
}
|
||||
|
||||
@@ -113,9 +113,9 @@ export function IIDXGraphsComponent({
|
||||
</div>
|
||||
<div className="col-12">
|
||||
{gaugeStatus === "gsm" && lamp !== "DAN_GAUGE" ? (
|
||||
<GraphComponent type={lamp} values={score.scoreData.hitMeta.gsm![lamp]} />
|
||||
<GraphComponent type={lamp} values={score.scoreData.optional.gsm![lamp]} />
|
||||
) : gaugeStatus === "single" ? (
|
||||
<GraphComponent type={lamp} values={score.scoreData.hitMeta.gaugeHistory!} />
|
||||
<GraphComponent type={lamp} values={score.scoreData.optional.gaugeHistory!} />
|
||||
) : (
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
@@ -177,7 +177,7 @@ function LampToKey(
|
||||
return "EX_HARD";
|
||||
} else if (lamp === "FULL COMBO") {
|
||||
// @hack - attempt to guess what gauge they used?
|
||||
if ((score.scoreData.hitMeta.gaugeHistory?.[0] ?? 0) > 22) {
|
||||
if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) {
|
||||
return "EX_HARD";
|
||||
}
|
||||
return "NORMAL";
|
||||
|
||||
@@ -9,18 +9,21 @@ export default function JudgementTable({ score }: { score: ScoreDocument | PBSco
|
||||
|
||||
return (
|
||||
<MiniTable className="text-center table-sm" headers={["Judgements"]} colSpan={2}>
|
||||
{gptConfig.judgements.map((j) => (
|
||||
{gptConfig.orderedJudgements.map((j) => (
|
||||
<tr key={j}>
|
||||
<td>{j.toUpperCase()}</td>
|
||||
{/* @ts-expect-error fine access */}
|
||||
<td>{score.scoreData.judgements[j] ?? "No Data."}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td style={{ backgroundColor: ChangeOpacity(COLOUR_SET.red, 0.2) }}>
|
||||
Slow: {score.scoreData.hitMeta.slow ?? "Unknown"}
|
||||
{/* @ts-expect-error it might exist */}
|
||||
Slow: {score.scoreData.optional.slow ?? "Unknown"}
|
||||
</td>
|
||||
<td style={{ backgroundColor: ChangeOpacity(COLOUR_SET.paleBlue, 0.2) }}>
|
||||
Fast: {score.scoreData.hitMeta.fast ?? "Unknown"}
|
||||
{/* @ts-expect-error it might exist */}
|
||||
Fast: {score.scoreData.optional.fast ?? "Unknown"}
|
||||
</td>
|
||||
</tr>
|
||||
</MiniTable>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React from "react";
|
||||
import { ScoreDocument, PBScoreDocument, IDStrings, ChartDocument } from "tachi-common";
|
||||
import { ScoreDocument, PBScoreDocument, GPTString, ChartDocument } from "tachi-common";
|
||||
import { UGPTChartPBComposition } from "types/api-returns";
|
||||
import { ScoreState } from "../ScoreDropdown";
|
||||
|
||||
export default function PBCompare<I extends IDStrings>({
|
||||
export default function PBCompare({
|
||||
data,
|
||||
scoreState,
|
||||
DocComponent,
|
||||
}: {
|
||||
data: UGPTChartPBComposition<I>;
|
||||
data: UGPTChartPBComposition;
|
||||
scoreState: ScoreState;
|
||||
DocComponent: (props: {
|
||||
score: ScoreDocument<I> | PBScoreDocument<I>;
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
scoreState: ScoreState;
|
||||
pbData: UGPTChartPBComposition;
|
||||
forceScoreData: boolean;
|
||||
chart: ChartDocument<I>;
|
||||
chart: ChartDocument;
|
||||
}) => JSX.Element;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Muted from "components/util/Muted";
|
||||
import usePreferredRanking from "components/util/usePreferredRanking";
|
||||
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
|
||||
import React, { useState } from "react";
|
||||
import { Game, IDStrings, ScoreCalculatedDataLookup, Playtype } from "tachi-common";
|
||||
import { Game, GPTString, ScoreRatingAlgorithms, Playtype } from "tachi-common";
|
||||
import { FolderDataset } from "types/tables";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import IndicatorsCell from "../cells/IndicatorsCell";
|
||||
@@ -21,12 +21,12 @@ import { GetGPTCoreHeaders } from "../headers/GameHeaders";
|
||||
import { EmptyHeader, FolderIndicatorHeader } from "../headers/IndicatorHeader";
|
||||
import { CreateRankingHeader } from "../headers/RankingHeader";
|
||||
|
||||
export default function FolderTable<I extends IDStrings = IDStrings>({
|
||||
export default function FolderTable({
|
||||
dataset,
|
||||
game,
|
||||
playtype,
|
||||
}: {
|
||||
dataset: FolderDataset<I>;
|
||||
dataset: FolderDataset;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
@@ -78,15 +78,15 @@ export default function FolderTable<I extends IDStrings = IDStrings>({
|
||||
);
|
||||
}
|
||||
|
||||
function Row<I extends IDStrings = IDStrings>({
|
||||
function Row({
|
||||
data,
|
||||
rating,
|
||||
game,
|
||||
rankingViewMode,
|
||||
}: {
|
||||
data: FolderDataset<I>[0];
|
||||
data: FolderDataset[0];
|
||||
game: Game;
|
||||
rating: ScoreCalculatedDataLookup[I];
|
||||
rating: ScoreRatingAlgorithms[GPTString];
|
||||
rankingViewMode: RankingViewMode;
|
||||
}) {
|
||||
const score = data.__related.pb;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { IDStrings, PBScoreDocument, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common";
|
||||
import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
|
||||
import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
|
||||
import DeltaCell from "../cells/DeltaCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
@@ -10,7 +10,7 @@ export default function BMSCoreCells({
|
||||
rating,
|
||||
}: {
|
||||
sc: PBScoreDocument<"bms:7K" | "bms:14K"> | ScoreDocument<"bms:7K" | "bms:14K">;
|
||||
rating: ScoreCalculatedDataLookup[IDStrings];
|
||||
rating: ScoreRatingAlgorithms[GPTString];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import CHUNITHMJudgementCell from "../cells/CHUNITHMJudgementCell";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import MillionsScoreCell from "../cells/MillionsScoreCell";
|
||||
@@ -14,9 +15,13 @@ export default function CHUNITHMCoreCells({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<MillionsScoreCell
|
||||
score={sc.scoreData.score}
|
||||
grade={sc.scoreData.grade}
|
||||
colour={GetEnumColour(sc, "grade")}
|
||||
/>
|
||||
<CHUNITHMJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import GitadoraJudgementCell from "../cells/GitadoraJudgementCell";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
@@ -18,7 +19,7 @@ export default function GitadoraCoreCells({
|
||||
<>
|
||||
<ScoreCell showScore={false} score={sc} />
|
||||
<GitadoraJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ChartDocument,
|
||||
PBScoreDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
import { ChartDocument, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
|
||||
import BPICell from "../cells/BPICell";
|
||||
import DeltaCell from "../cells/DeltaCell";
|
||||
import IIDXLampCell from "../cells/IIDXLampCell";
|
||||
@@ -17,7 +12,7 @@ export default function IIDXCoreCells({
|
||||
chart,
|
||||
}: {
|
||||
sc: PBScoreDocument<"iidx:SP" | "iidx:DP"> | ScoreDocument<"iidx:SP" | "iidx:DP">;
|
||||
rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"];
|
||||
rating: ScoreRatingAlgorithms["iidx:SP" | "iidx:DP"];
|
||||
chart: ChartDocument<"iidx:SP" | "iidx:DP">;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import ITGJudgementCell from "../cells/ITGJudgementCell";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
@@ -16,7 +17,7 @@ export default function ITGCoreCells({
|
||||
<>
|
||||
<ScoreCell showScore={false} score={sc} />
|
||||
<ITGJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import JubeatJudgementCell from "../cells/JubeatJudgementCell";
|
||||
import JubeatScoreCell from "../cells/JubeatScoreCell";
|
||||
import JubilityCell from "../cells/JubilityCell";
|
||||
@@ -7,7 +8,6 @@ import LampCell from "../cells/LampCell";
|
||||
|
||||
export default function JubeatCoreCells({
|
||||
sc,
|
||||
rating,
|
||||
}: {
|
||||
sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">;
|
||||
rating: keyof ScoreDocument["calculatedData"];
|
||||
@@ -16,7 +16,7 @@ export default function JubeatCoreCells({
|
||||
<>
|
||||
<JubeatScoreCell sc={sc} />
|
||||
<JubeatJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<JubilityCell score={sc} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
import ScoreCell from "../cells/ScoreCell";
|
||||
@@ -16,7 +17,7 @@ export default function MaimaiDXCoreCells({
|
||||
<>
|
||||
<ScoreCell showScore={false} score={sc} />
|
||||
<MaimaiDXJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { IDStrings, PBScoreDocument, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common";
|
||||
import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import MillionsScoreCell from "../cells/MillionsScoreCell";
|
||||
import MusecaJudgementCell from "../cells/MusecaJudgementCell";
|
||||
@@ -10,13 +11,17 @@ export default function MusecaCoreCells({
|
||||
rating,
|
||||
}: {
|
||||
sc: ScoreDocument<"museca:Single"> | PBScoreDocument<"museca:Single">;
|
||||
rating: ScoreCalculatedDataLookup[IDStrings];
|
||||
rating: ScoreRatingAlgorithms[GPTString];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<MillionsScoreCell
|
||||
score={sc.scoreData.score}
|
||||
grade={sc.scoreData.grade}
|
||||
colour={GetEnumColour(sc, "grade")}
|
||||
/>
|
||||
<MusecaJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { IDStrings, PBScoreDocument, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common";
|
||||
import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
|
||||
import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
|
||||
import DeltaCell from "../cells/DeltaCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
@@ -12,7 +12,7 @@ export default function PMSCoreCells({
|
||||
sc:
|
||||
| PBScoreDocument<"pms:Controller" | "pms:Keyboard">
|
||||
| ScoreDocument<"pms:Controller" | "pms:Keyboard">;
|
||||
rating: ScoreCalculatedDataLookup[IDStrings];
|
||||
rating: ScoreRatingAlgorithms[GPTString];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import MillionsScoreCell from "../cells/MillionsScoreCell";
|
||||
import PopnJudgementCell from "../cells/PopnJudgementCell";
|
||||
import PopnLampCell from "../cells/PopnLampCell";
|
||||
@@ -14,7 +15,11 @@ export default function PopnCoreCells({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<MillionsScoreCell
|
||||
score={sc.scoreData.score}
|
||||
grade={sc.scoreData.grade}
|
||||
colour={GetEnumColour(sc, "grade")}
|
||||
/>
|
||||
<PopnJudgementCell score={sc} />
|
||||
<PopnLampCell score={sc} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { ChartDocument, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import MillionsScoreCell from "../cells/MillionsScoreCell";
|
||||
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
|
||||
import SDVXLampCell from "../cells/SDVXLampCell";
|
||||
@@ -14,7 +15,11 @@ export default function SDVXScoreCoreCells({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<MillionsScoreCell
|
||||
score={sc.scoreData.score}
|
||||
grade={sc.scoreData.grade}
|
||||
colour={GetEnumColour(sc, "grade")}
|
||||
/>
|
||||
<SDVXJudgementCell score={sc} />
|
||||
<SDVXLampCell score={sc} />
|
||||
<VF6Cell score={sc} chart={chart} />
|
||||
|
||||
@@ -2,11 +2,13 @@ import React from "react";
|
||||
import {
|
||||
ChartDocument,
|
||||
Game,
|
||||
IDStrings,
|
||||
GPTString,
|
||||
PBScoreDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
ScoreRatingAlgorithms,
|
||||
ScoreDocument,
|
||||
AnyScoreRatingAlg,
|
||||
} from "tachi-common";
|
||||
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
|
||||
import BMSCoreCells from "./BMSCoreCells";
|
||||
import CHUNITHMCoreCells from "./CHUNITHMCoreCells";
|
||||
import IIDXCoreCells from "./IIDXCoreCells";
|
||||
@@ -28,22 +30,28 @@ export default function ScoreCoreCells({
|
||||
}: {
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
chart: ChartDocument;
|
||||
rating: ScoreCalculatedDataLookup[IDStrings];
|
||||
rating?: AnyScoreRatingAlg;
|
||||
game: Game;
|
||||
}): React.ReactChild {
|
||||
}): JSX.Element {
|
||||
const [defaultRating] = useScoreRatingAlg(game, chart.playtype);
|
||||
|
||||
// fallback to this users preferred rating if none provided.
|
||||
// @ts-expect-error whateverr
|
||||
const rt: AnyScoreRatingAlg = rating ?? defaultRating;
|
||||
|
||||
const sc = score as any; // lazy hack
|
||||
|
||||
switch (game) {
|
||||
case "iidx":
|
||||
return (
|
||||
<IIDXCoreCells
|
||||
rating={rating as ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]}
|
||||
rating={rt as ScoreRatingAlgorithms["iidx:SP" | "iidx:DP"]}
|
||||
chart={chart as ChartDocument<"iidx:SP" | "iidx:DP">}
|
||||
sc={sc}
|
||||
/>
|
||||
);
|
||||
case "bms":
|
||||
return <BMSCoreCells sc={sc} rating={rating} />;
|
||||
return <BMSCoreCells sc={sc} rating={rt} />;
|
||||
case "sdvx":
|
||||
case "usc":
|
||||
return (
|
||||
@@ -55,22 +63,22 @@ export default function ScoreCoreCells({
|
||||
/>
|
||||
);
|
||||
case "museca":
|
||||
return <MusecaCoreCells sc={sc} rating={rating} />;
|
||||
return <MusecaCoreCells sc={sc} rating={rt} />;
|
||||
case "wacca":
|
||||
return <WACCACoreCells sc={sc} rating={rating} />;
|
||||
return <WACCACoreCells sc={sc} rating={rt} />;
|
||||
case "popn":
|
||||
return <PopnCoreCells sc={sc} rating={rating} />;
|
||||
return <PopnCoreCells sc={sc} rating={rt} />;
|
||||
case "jubeat":
|
||||
return <JubeatCoreCells sc={sc} rating={rating} />;
|
||||
return <JubeatCoreCells sc={sc} rating={rt} />;
|
||||
case "chunithm":
|
||||
return <CHUNITHMCoreCells sc={sc} rating={rating} />;
|
||||
return <CHUNITHMCoreCells sc={sc} rating={rt} />;
|
||||
case "gitadora":
|
||||
return <GitadoraCoreCells sc={sc} rating={rating} />;
|
||||
return <GitadoraCoreCells sc={sc} rating={rt} />;
|
||||
case "pms":
|
||||
return <PMSCoreCells sc={sc} rating={rating} />;
|
||||
return <PMSCoreCells sc={sc} rating={rt} />;
|
||||
case "itg":
|
||||
return <ITGCoreCells sc={sc} rating={rating} />;
|
||||
return <ITGCoreCells sc={sc} rating={rt} />;
|
||||
case "maimaidx":
|
||||
return <MaimaiDXCoreCells sc={sc} rating={rating} />;
|
||||
return <MaimaiDXCoreCells sc={sc} rating={rt} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GetEnumColour } from "lib/game-implementations";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import MillionsScoreCell from "../cells/MillionsScoreCell";
|
||||
import RatingCell from "../cells/RatingCell";
|
||||
@@ -14,9 +15,13 @@ export default function WACCACoreCells({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<MillionsScoreCell score={sc} />
|
||||
<MillionsScoreCell
|
||||
score={sc.scoreData.score}
|
||||
grade={sc.scoreData.grade}
|
||||
colour={GetEnumColour(sc, "grade")}
|
||||
/>
|
||||
<WaccaJudgementCell score={sc} />
|
||||
<LampCell score={sc} />
|
||||
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,47 +1,37 @@
|
||||
import { IsNotNullish } from "util/misc";
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { ChartDocument, Game } from "tachi-common";
|
||||
import { IsNullish } from "util/misc";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import { GPTRatingSystem } from "lib/types";
|
||||
import { ChartDocument, Game, GetGPTString } from "tachi-common";
|
||||
import { Header } from "../components/TachiTable";
|
||||
|
||||
function CascadingTierlistValue(
|
||||
chart: ChartDocument,
|
||||
...tiers: (keyof ChartDocument["tierlistInfo"])[]
|
||||
) {
|
||||
for (const tier of tiers) {
|
||||
if (IsNotNullish(chart.tierlistInfo[tier])) {
|
||||
return chart.tierlistInfo[tier]!.value;
|
||||
export function CascadingRatingValue(game: Game, chartA: ChartDocument, chartB: ChartDocument) {
|
||||
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, chartA.playtype)];
|
||||
|
||||
if (chartA.levelNum !== chartB.levelNum) {
|
||||
return chartA.levelNum - chartB.levelNum;
|
||||
}
|
||||
|
||||
for (const rating of gptImpl.ratingSystems as GPTRatingSystem<any>[]) {
|
||||
const a = rating.toNumber(chartA);
|
||||
const b = rating.toNumber(chartB);
|
||||
if (a !== b) {
|
||||
if (IsNullish(a)) {
|
||||
return -Infinity;
|
||||
} else if (IsNullish(b)) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
return a! - b!;
|
||||
}
|
||||
}
|
||||
|
||||
return chart.levelNum;
|
||||
// these things are equal on all counts
|
||||
return 0;
|
||||
}
|
||||
|
||||
export default function ChartHeader<D>(
|
||||
game: Game,
|
||||
chartGetter: (k: D) => ChartDocument
|
||||
): Header<D> {
|
||||
let sortFn: (d: D) => number = (k) => chartGetter(k).levelNum;
|
||||
|
||||
if (game === "iidx") {
|
||||
sortFn = (k) => CascadingTierlistValue(chartGetter(k), "kt-HC", "kt-NC");
|
||||
} else if (game === "bms" || game === "pms") {
|
||||
sortFn = (k) => CascadingTierlistValue(chartGetter(k), "sgl-EC", "sgl-HC");
|
||||
} else if (game === "itg") {
|
||||
return [
|
||||
"Chart",
|
||||
"Chart",
|
||||
(a, b) => {
|
||||
const ac = chartGetter(a) as ChartDocument<"itg:Stamina">;
|
||||
const bc = chartGetter(b) as ChartDocument<"itg:Stamina">;
|
||||
|
||||
if (ac.levelNum === bc.levelNum) {
|
||||
return ac.data.displayBPM - bc.data.displayBPM;
|
||||
}
|
||||
|
||||
return ac.levelNum - bc.levelNum;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return ["Chart", "Chart", NumericSOV(sortFn)];
|
||||
return ["Chart", "Chart", (a, b) => CascadingRatingValue(game, chartGetter(a), chartGetter(b))];
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import React from "react";
|
||||
import {
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
GPTString,
|
||||
PBScoreDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
ScoreRatingAlgorithms,
|
||||
ScoreDocument,
|
||||
Playtype,
|
||||
AnyScoreRatingAlg,
|
||||
GetGPTString,
|
||||
} from "tachi-common";
|
||||
import { SetState } from "types/react";
|
||||
import {
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
RivalChartDataset,
|
||||
ScoreDataset,
|
||||
} from "types/tables";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import SelectableRating from "../components/SelectableRating";
|
||||
import { Header, ZTableTHProps } from "../components/TachiTable";
|
||||
|
||||
@@ -26,28 +29,16 @@ export function GetGPTCoreHeaders<
|
||||
>(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
rating: ScoreCalculatedDataLookup[IDStrings],
|
||||
setRating: SetState<ScoreCalculatedDataLookup[IDStrings]>,
|
||||
rating: ScoreRatingAlgorithms[GPTString],
|
||||
setRating: SetState<ScoreRatingAlgorithms[GPTString]>,
|
||||
kMapToScoreOrPB: (k: Dataset[0]) => PBScoreDocument | ScoreDocument | null
|
||||
): Header<Dataset[0]>[] {
|
||||
const ScoreHeader: Header<Dataset[0]> = [
|
||||
"Score",
|
||||
"Score",
|
||||
NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity),
|
||||
];
|
||||
|
||||
const LampHeader: Header<Dataset[0]> = [
|
||||
"Lamp",
|
||||
"Lamp",
|
||||
NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.lampIndex ?? -Infinity),
|
||||
];
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
let RatingHeader: Header<Dataset[0]>;
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (gptConfig.scoreRatingAlgs.length === 1) {
|
||||
const alg = gptConfig.scoreRatingAlgs[0];
|
||||
if (Object.keys(gptConfig.scoreRatingAlgs).length === 1) {
|
||||
const alg = Object.keys(gptConfig.scoreRatingAlgs)[0] as AnyScoreRatingAlg;
|
||||
|
||||
RatingHeader = [
|
||||
UppercaseFirst(alg),
|
||||
@@ -72,108 +63,41 @@ export function GetGPTCoreHeaders<
|
||||
];
|
||||
}
|
||||
|
||||
switch (game) {
|
||||
case "sdvx":
|
||||
case "museca":
|
||||
case "usc":
|
||||
return [
|
||||
ScoreHeader,
|
||||
[
|
||||
"Near - Miss",
|
||||
"Nr. Ms.",
|
||||
NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity),
|
||||
],
|
||||
LampHeader,
|
||||
RatingHeader,
|
||||
];
|
||||
case "iidx":
|
||||
case "bms":
|
||||
case "pms":
|
||||
return [
|
||||
ScoreHeader,
|
||||
[
|
||||
"Deltas",
|
||||
"Deltas",
|
||||
NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity),
|
||||
],
|
||||
LampHeader,
|
||||
RatingHeader,
|
||||
];
|
||||
case "gitadora":
|
||||
case "jubeat":
|
||||
case "wacca":
|
||||
case "chunithm":
|
||||
case "maimaidx":
|
||||
case "itg":
|
||||
return [
|
||||
ScoreHeader,
|
||||
[
|
||||
"Judgements",
|
||||
"Hits",
|
||||
NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity),
|
||||
],
|
||||
LampHeader,
|
||||
RatingHeader,
|
||||
];
|
||||
case "popn":
|
||||
return [
|
||||
ScoreHeader,
|
||||
[
|
||||
"Judgements",
|
||||
"Hits",
|
||||
NumericSOV((x) => kMapToScoreOrPB(x)?.scoreData.percent ?? -Infinity),
|
||||
],
|
||||
[
|
||||
"Lamp",
|
||||
"Lamp",
|
||||
(a, b) => {
|
||||
const aSc = kMapToScoreOrPB(a) as
|
||||
| ScoreDocument<"popn:9B">
|
||||
| PBScoreDocument<"popn:9B">;
|
||||
const bSc = kMapToScoreOrPB(b) as
|
||||
| ScoreDocument<"popn:9B">
|
||||
| PBScoreDocument<"popn:9B">;
|
||||
const implHeaders = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)].scoreHeaders;
|
||||
|
||||
if (!aSc) {
|
||||
return -Infinity;
|
||||
}
|
||||
const outHeaders: Array<Header<Dataset[0]>> = [];
|
||||
|
||||
if (aSc && !bSc) {
|
||||
return Infinity;
|
||||
}
|
||||
for (const header of implHeaders) {
|
||||
if (header[2]) {
|
||||
// replace the sort function with one that correctly resolves
|
||||
// pbs
|
||||
outHeaders.push([
|
||||
header[0],
|
||||
header[1],
|
||||
(a, b) => {
|
||||
const pbA = kMapToScoreOrPB(a);
|
||||
const pbB = kMapToScoreOrPB(b);
|
||||
|
||||
if (aSc.scoreData.lampIndex === bSc.scoreData.lampIndex) {
|
||||
if (
|
||||
!aSc.scoreData.hitMeta.specificClearType ||
|
||||
!bSc.scoreData.hitMeta.specificClearType
|
||||
) {
|
||||
return -Infinity;
|
||||
}
|
||||
if (!pbA) {
|
||||
return -Infinity;
|
||||
}
|
||||
if (!pbB) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
return (
|
||||
popnClearTypeToInt[aSc.scoreData.hitMeta.specificClearType] -
|
||||
popnClearTypeToInt[bSc.scoreData.hitMeta.specificClearType]
|
||||
);
|
||||
}
|
||||
|
||||
return aSc.scoreData.lampIndex - bSc.scoreData.lampIndex;
|
||||
},
|
||||
],
|
||||
RatingHeader,
|
||||
];
|
||||
// typescript is NOT happy with what i've done here
|
||||
// LOL, who cares.
|
||||
// safety is for chumps
|
||||
return (header[2] as any)!(pbA, pbB);
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
outHeaders.push(header as Header<Dataset[0]>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const popnClearTypeToInt = {
|
||||
failedCircle: 0,
|
||||
failedDiamond: 1,
|
||||
failedStar: 2,
|
||||
easyClear: 3,
|
||||
clearCircle: 4,
|
||||
clearDiamond: 5,
|
||||
clearStar: 6,
|
||||
fullComboCircle: 7,
|
||||
fullComboDiamond: 8,
|
||||
fullComboStar: 9,
|
||||
perfect: 10,
|
||||
} as const;
|
||||
// all games have a rating header after their impl header.
|
||||
outHeaders.push(RatingHeader);
|
||||
|
||||
return outHeaders;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import React, { useState } from "react";
|
||||
import {
|
||||
ChartDocument,
|
||||
Game,
|
||||
IDStrings,
|
||||
GPTString,
|
||||
integer,
|
||||
ScoreCalculatedDataLookup,
|
||||
ScoreRatingAlgorithms,
|
||||
ScoreDocument,
|
||||
Playtype,
|
||||
} from "tachi-common";
|
||||
@@ -67,7 +67,7 @@ function Row({
|
||||
game,
|
||||
}: {
|
||||
sc: ScoreDocument;
|
||||
rating: ScoreCalculatedDataLookup[IDStrings];
|
||||
rating: ScoreRatingAlgorithms[GPTString];
|
||||
game: Game;
|
||||
chart: ChartDocument;
|
||||
}) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CreateDefaultPBSearchParams } from "util/tables/create-search";
|
||||
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
|
||||
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
|
||||
import React, { useState } from "react";
|
||||
import { Game, IDStrings, ScoreCalculatedDataLookup, Playtype } from "tachi-common";
|
||||
import { Game, GPTString, ScoreRatingAlgorithms, Playtype, AnyScoreRatingAlg } from "tachi-common";
|
||||
import { PBDataset } from "types/tables";
|
||||
import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
|
||||
import IndexCell from "../cells/IndexCell";
|
||||
@@ -20,7 +20,7 @@ import { EmptyHeader } from "../headers/IndicatorHeader";
|
||||
import { CreateRankingHeader } from "../headers/RankingHeader";
|
||||
import PBLeadingRows from "./PBLeadingRows";
|
||||
|
||||
export default function PBTable<I extends IDStrings = IDStrings>({
|
||||
export default function PBTable({
|
||||
dataset,
|
||||
game,
|
||||
indexCol = true,
|
||||
@@ -31,14 +31,14 @@ export default function PBTable<I extends IDStrings = IDStrings>({
|
||||
alg,
|
||||
defaultRankingViewMode,
|
||||
}: {
|
||||
dataset: PBDataset<I>;
|
||||
dataset: PBDataset;
|
||||
indexCol?: boolean;
|
||||
showPlaycount?: boolean;
|
||||
showUser?: boolean;
|
||||
showChart?: boolean;
|
||||
playtype: Playtype;
|
||||
game: Game;
|
||||
alg?: ScoreCalculatedDataLookup[I];
|
||||
alg?: AnyScoreRatingAlg;
|
||||
defaultRankingViewMode?: RankingViewMode | null;
|
||||
}) {
|
||||
const defaultRating = useScoreRatingAlg(game, playtype);
|
||||
@@ -48,7 +48,7 @@ export default function PBTable<I extends IDStrings = IDStrings>({
|
||||
defaultRankingViewMode ?? "global"
|
||||
);
|
||||
|
||||
const headers: Header<PBDataset<I>[0]>[] = [
|
||||
const headers: Header<PBDataset[0]>[] = [
|
||||
...GetPBLeadingHeaders(
|
||||
showUser,
|
||||
showChart,
|
||||
@@ -98,7 +98,7 @@ export default function PBTable<I extends IDStrings = IDStrings>({
|
||||
);
|
||||
}
|
||||
|
||||
function Row<I extends IDStrings = IDStrings>({
|
||||
function Row({
|
||||
pb,
|
||||
indexCol,
|
||||
showPlaycount,
|
||||
@@ -108,7 +108,7 @@ function Row<I extends IDStrings = IDStrings>({
|
||||
rating,
|
||||
rankingViewMode,
|
||||
}: {
|
||||
pb: PBDataset<I>[0];
|
||||
pb: PBDataset[0];
|
||||
indexCol: boolean;
|
||||
showPlaycount: boolean;
|
||||
showChart: boolean;
|
||||
@@ -116,7 +116,7 @@ function Row<I extends IDStrings = IDStrings>({
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
// ts bug?
|
||||
rating: any; // ScoreCalculatedDataLookup[I];
|
||||
rating: any; // ScoreRatingAlgorithms[I];
|
||||
rankingViewMode: RankingViewMode;
|
||||
}) {
|
||||
const scoreState = usePBState(pb);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 React, { useEffect, useState } from "react";
|
||||
import { Game, GetGamePTConfig, GetScoreMetricConf, Playtype } from "tachi-common";
|
||||
import { ComparePBsDataset } from "types/tables";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import PBCompareCell from "../cells/PBCompareCell";
|
||||
@@ -12,22 +11,26 @@ 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>({
|
||||
export default function ComparePBsTable({
|
||||
dataset,
|
||||
game,
|
||||
playtype,
|
||||
baseUser,
|
||||
compareUser,
|
||||
shouldESD,
|
||||
}: {
|
||||
dataset: ComparePBsDataset<I>;
|
||||
dataset: ComparePBsDataset;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
baseUser: string;
|
||||
compareUser: string;
|
||||
shouldESD: boolean;
|
||||
}) {
|
||||
const [compareType, setCompareType] = useState<"score" | "lamp">("score");
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const [metric, setMetric] = useState<string>(gptConfig.defaultMetric);
|
||||
|
||||
useEffect(() => {
|
||||
setMetric(gptConfig.defaultMetric);
|
||||
}, [gptConfig]);
|
||||
|
||||
const headers: Header<ComparePBsDataset[0]>[] = [
|
||||
ChartHeader(game, (d) => d.chart),
|
||||
@@ -45,26 +48,33 @@ export default function ComparePBsTable<I extends IDStrings = IDStrings>({
|
||||
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);
|
||||
const conf = GetScoreMetricConf(gptConfig, metric);
|
||||
|
||||
if (e !== 0) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
return x.base.scoreData.percent - x.compare.scoreData.percent;
|
||||
if (!conf) {
|
||||
return 0; // wut
|
||||
}
|
||||
|
||||
return x.base.scoreData.lampIndex - x.compare.scoreData.lampIndex;
|
||||
if (conf.type === "ENUM") {
|
||||
return (
|
||||
// @ts-expect-error this will work
|
||||
x.base.scoreData.enumIndexes[metric] -
|
||||
// @ts-expect-error this will work
|
||||
x.compare.scoreData.enumIndexes[metric]
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error this will work
|
||||
x.base.scoreData[metric] -
|
||||
// @ts-expect-error this will work
|
||||
x.compare.scoreData[metric]
|
||||
);
|
||||
}),
|
||||
(thProps: ZTableTHProps) => (
|
||||
<SelectableCompareType
|
||||
key={compareType}
|
||||
compareType={compareType}
|
||||
setCompareType={setCompareType}
|
||||
key={metric}
|
||||
metric={metric}
|
||||
setMetric={setMetric}
|
||||
{...thProps}
|
||||
/>
|
||||
),
|
||||
@@ -74,32 +84,20 @@ export default function ComparePBsTable<I extends IDStrings = IDStrings>({
|
||||
|
||||
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} />
|
||||
)}
|
||||
rowFunction={(data) => <Row data={data} game={game} metric={metric} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Row<I extends IDStrings = IDStrings>({
|
||||
data,
|
||||
game,
|
||||
compareType,
|
||||
shouldESD,
|
||||
}: {
|
||||
data: ComparePBsDataset<I>[0];
|
||||
game: Game;
|
||||
compareType: "score" | "lamp";
|
||||
shouldESD: boolean;
|
||||
}) {
|
||||
function Row({ data, game, metric }: { data: ComparePBsDataset[0]; game: Game; metric: string }) {
|
||||
const metricConf = GetScoreMetricConf(GetGamePTConfig(game, data.chart.playtype), metric)!;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<DifficultyCell chart={data.chart} game={game} alwaysShort />
|
||||
@@ -110,12 +108,12 @@ function Row<I extends IDStrings = IDStrings>({
|
||||
<td colSpan={2}>Not Played</td>
|
||||
)}
|
||||
<PBCompareCell
|
||||
shouldESD={shouldESD}
|
||||
base={data.base}
|
||||
compare={data.compare}
|
||||
game={game}
|
||||
playtype={data.chart.playtype}
|
||||
compareType={compareType}
|
||||
metricConf={metricConf}
|
||||
metric={metric}
|
||||
/>
|
||||
{data.compare ? (
|
||||
<ShortScoreCoreCells score={data.compare} game={game} chart={data.chart} />
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
|
||||
import React, { useState } from "react";
|
||||
import { ChartDocument, Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
|
||||
import {
|
||||
AnyScoreRatingAlg,
|
||||
ChartDocument,
|
||||
Game,
|
||||
GPTString,
|
||||
ScoreRatingAlgorithms,
|
||||
} from "tachi-common";
|
||||
import { RivalChartDataset } from "types/tables";
|
||||
import IndexCell from "../cells/IndexCell";
|
||||
import RankingCell, { RankingViewMode } from "../cells/RankingCell";
|
||||
@@ -15,12 +21,12 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
|
||||
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
|
||||
import { CreateRankingHeader } from "../headers/RankingHeader";
|
||||
|
||||
export default function RivalChartTable<I extends IDStrings = IDStrings>({
|
||||
export default function RivalChartTable({
|
||||
dataset,
|
||||
game,
|
||||
chart,
|
||||
}: {
|
||||
dataset: RivalChartDataset<I>;
|
||||
dataset: RivalChartDataset;
|
||||
game: Game;
|
||||
chart: ChartDocument;
|
||||
}) {
|
||||
@@ -70,16 +76,16 @@ export default function RivalChartTable<I extends IDStrings = IDStrings>({
|
||||
);
|
||||
}
|
||||
|
||||
function Row<I extends IDStrings = IDStrings>({
|
||||
function Row({
|
||||
data,
|
||||
rating,
|
||||
game,
|
||||
chart,
|
||||
rankingViewMode,
|
||||
}: {
|
||||
data: RivalChartDataset<I>[0];
|
||||
data: RivalChartDataset[0];
|
||||
game: Game;
|
||||
rating: ScoreCalculatedDataLookup[I];
|
||||
rating: AnyScoreRatingAlg;
|
||||
chart: ChartDocument;
|
||||
rankingViewMode: RankingViewMode;
|
||||
}) {
|
||||
|
||||
@@ -2,9 +2,10 @@ import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import { CreateDefaultScoreSearchParams } from "util/tables/create-search";
|
||||
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
|
||||
import React, { useState } from "react";
|
||||
import { Game, IDStrings, integer, ScoreCalculatedDataLookup, Playtype } from "tachi-common";
|
||||
import { AnyScoreRatingAlg, Game, Playtype, integer } from "tachi-common";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
|
||||
import IndicatorsCell from "../cells/IndicatorsCell";
|
||||
import TimestampCell from "../cells/TimestampCell";
|
||||
import TitleCell from "../cells/TitleCell";
|
||||
@@ -17,9 +18,8 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
|
||||
import ChartHeader from "../headers/ChartHeader";
|
||||
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
|
||||
import IndicatorHeader, { EmptyHeader } from "../headers/IndicatorHeader";
|
||||
import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
|
||||
|
||||
export default function ScoreTable<I extends IDStrings = IDStrings>({
|
||||
export default function ScoreTable({
|
||||
dataset,
|
||||
pageLen,
|
||||
playtype,
|
||||
@@ -28,18 +28,18 @@ export default function ScoreTable<I extends IDStrings = IDStrings>({
|
||||
alg,
|
||||
noTopDisplayStr,
|
||||
}: {
|
||||
dataset: ScoreDataset<I>;
|
||||
dataset: ScoreDataset;
|
||||
pageLen?: integer;
|
||||
playtype: Playtype;
|
||||
userCol?: boolean;
|
||||
game: Game;
|
||||
alg?: ScoreCalculatedDataLookup[I];
|
||||
alg?: AnyScoreRatingAlg;
|
||||
noTopDisplayStr?: boolean;
|
||||
}) {
|
||||
const defaultRating = useScoreRatingAlg(game, playtype);
|
||||
const [rating, setRating] = useState(alg ?? defaultRating);
|
||||
|
||||
const headers: Header<ScoreDataset<I>[0]>[] = [
|
||||
const headers: Header<ScoreDataset[0]>[] = [
|
||||
ChartHeader(game, (k) => k.__related.chart),
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV((x) => x.__related.song.title)],
|
||||
@@ -74,15 +74,15 @@ export default function ScoreTable<I extends IDStrings = IDStrings>({
|
||||
);
|
||||
}
|
||||
|
||||
function Row<I extends IDStrings = IDStrings>({
|
||||
function Row({
|
||||
sc,
|
||||
rating,
|
||||
playtype,
|
||||
userCol,
|
||||
game,
|
||||
}: {
|
||||
sc: ScoreDataset<I>[0];
|
||||
rating: ScoreCalculatedDataLookup[I];
|
||||
sc: ScoreDataset[0];
|
||||
rating: AnyScoreRatingAlg;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
userCol: boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StrSOV } from "util/sorts";
|
||||
import { SearchFunctions } from "util/ztable/search";
|
||||
import React from "react";
|
||||
import { FormatChart } from "tachi-common";
|
||||
import { FormatChart, GetBMSCourseIndex } from "tachi-common";
|
||||
import { BMSCourseWithRelated, CellsRenderFN } from "types/seeds";
|
||||
import { Header } from "../components/TachiTable";
|
||||
|
||||
@@ -16,7 +16,7 @@ export const SeedsBMSCourseLookupHeaders: Header<BMSCourseWithRelated>[] = [
|
||||
const b2 = `${b.playtype} ${b.set}`;
|
||||
|
||||
if (a2 === b2) {
|
||||
return a.value - b.value;
|
||||
return GetBMSCourseIndex(a) - GetBMSCourseIndex(b);
|
||||
}
|
||||
|
||||
return a2.localeCompare(b2);
|
||||
|
||||
@@ -8,10 +8,10 @@ import { Link } from "react-router-dom";
|
||||
import {
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
GPTString,
|
||||
integer,
|
||||
UserDocument,
|
||||
SessionCalculatedDataLookup,
|
||||
SessionRatingAlgorithms,
|
||||
SessionDocument,
|
||||
Playtype,
|
||||
SessionScoreInfo,
|
||||
@@ -41,7 +41,7 @@ export default function GenericSessionTable({
|
||||
|
||||
const defaultRating = useSessionRatingAlg(game, playtype);
|
||||
|
||||
const [alg, setAlg] = useState<SessionCalculatedDataLookup[IDStrings]>(defaultRating);
|
||||
const [alg, setAlg] = useState<SessionRatingAlgorithms[GPTString]>(defaultRating);
|
||||
|
||||
const headers: Header<SessionDataset[0]>[] = [
|
||||
["Name", "Name", StrSOV((x) => x.name)],
|
||||
@@ -51,7 +51,7 @@ export default function GenericSessionTable({
|
||||
["Timestamp", "Timestamp", NumericSOV((x) => x.timeStarted)],
|
||||
];
|
||||
|
||||
if (gptConfig.sessionRatingAlgs.length > 1) {
|
||||
if (Object.keys(gptConfig.sessionRatingAlgs).length > 1) {
|
||||
headers[2] = [
|
||||
"Rating",
|
||||
"Rating",
|
||||
@@ -108,7 +108,7 @@ function Row({
|
||||
{
|
||||
data: SessionDataset[0];
|
||||
// reqUser: PublicUserDocument;
|
||||
ratingAlg: SessionCalculatedDataLookup[IDStrings];
|
||||
ratingAlg: SessionRatingAlgorithms[GPTString];
|
||||
reqUser: UserDocument;
|
||||
indexCol?: boolean;
|
||||
}) {
|
||||
|
||||
@@ -6,11 +6,18 @@ import Divider from "components/util/Divider";
|
||||
import Select from "components/util/Select";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Col, Form, InputGroup, Modal, Row } from "react-bootstrap";
|
||||
import { FolderDocument, FormatChart, GetGamePTConfig, GoalDocument } from "tachi-common";
|
||||
import {
|
||||
FolderDocument,
|
||||
FormatChart,
|
||||
GetGamePTConfig,
|
||||
GetScoreMetricConf,
|
||||
GoalDocument,
|
||||
} from "tachi-common";
|
||||
import { SongChartsSearch } from "types/api-returns";
|
||||
import { GamePT, SetState } from "types/react";
|
||||
import { RawQuestGoal } from "types/tachi";
|
||||
import { OptionsOrGroups, GroupBase } from "react-select";
|
||||
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
|
||||
import { RenderGoalCriteriaPicker } from "./SetNewGoalModal";
|
||||
|
||||
export default function AddNewGoalForQuestModal({
|
||||
@@ -30,14 +37,16 @@ export default function AddNewGoalForQuestModal({
|
||||
} & GamePT) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const enumConf = GetScoreMetricConf(
|
||||
gptConfig,
|
||||
gptConfig.preferredDefaultEnum
|
||||
) as ConfEnumScoreMetric<string>;
|
||||
|
||||
const [criteria, setCriteria] = useState<GoalDocument["criteria"]>(
|
||||
initialState?.goal.criteria ?? {
|
||||
mode: "single",
|
||||
key: gptConfig.scoreBucket === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex",
|
||||
value:
|
||||
gptConfig.scoreBucket === "grade"
|
||||
? gptConfig.grades.indexOf(gptConfig.clearGrade)
|
||||
: gptConfig.lamps.indexOf(gptConfig.clearLamp),
|
||||
key: gptConfig.preferredDefaultEnum,
|
||||
value: enumConf.values.indexOf(enumConf.minimumRelevantValue),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Select from "components/util/Select";
|
||||
import { TachiConfig } from "lib/config";
|
||||
import React, { useState } from "react";
|
||||
import { Button, Col, Modal, Row } from "react-bootstrap";
|
||||
import { FormatGame, Game, GetGameConfig, IDStrings, Playtype } from "tachi-common";
|
||||
import { FormatGame, Game, GetGameConfig, GPTString, Playtype } from "tachi-common";
|
||||
import { SetState } from "types/react";
|
||||
import { RawQuestDocument } from "types/tachi";
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function AddNewQuestModal({
|
||||
setShow: SetState<boolean>;
|
||||
onCreate: (rawQuest: RawQuestDocument) => void;
|
||||
}) {
|
||||
const [gpt, setGPT] = useState<IDStrings | null>(null);
|
||||
const [gpt, setGPT] = useState<GPTString | null>(null);
|
||||
|
||||
return (
|
||||
<Modal size="xl" show={show} onHide={() => setShow(false)}>
|
||||
|
||||
@@ -35,9 +35,9 @@ export default function SetNewGoalModal({
|
||||
|
||||
const [criteria, setCriteria] = useState<GoalDocument["criteria"]>({
|
||||
mode: "single",
|
||||
key: gptConfig.scoreBucket === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex",
|
||||
key: gptConfig.preferredDefaultEnum === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex",
|
||||
value:
|
||||
gptConfig.scoreBucket === "grade"
|
||||
gptConfig.preferredDefaultEnum === "grade"
|
||||
? gptConfig.grades.indexOf(gptConfig.clearGrade)
|
||||
: gptConfig.lamps.indexOf(gptConfig.clearLamp),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UppercaseFirst } from "util/misc";
|
||||
import { useProfileRatingAlg } from "components/util/useScoreRatingAlg";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IDStrings, integer, ProfileRatingLookup } from "tachi-common";
|
||||
import { GPTString, integer, ProfileRatingAlgorithms } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
|
||||
export default function RankingData({
|
||||
@@ -11,7 +11,7 @@ export default function RankingData({
|
||||
userID,
|
||||
playtype,
|
||||
}: {
|
||||
rankingData: Record<ProfileRatingLookup[IDStrings], { ranking: number; outOf: integer }>;
|
||||
rankingData: Record<ProfileRatingAlgorithms[GPTString], { ranking: number; outOf: integer }>;
|
||||
userID: integer;
|
||||
} & GamePT) {
|
||||
const alg = useProfileRatingAlg(game, playtype);
|
||||
@@ -19,7 +19,7 @@ export default function RankingData({
|
||||
const extendData = [];
|
||||
|
||||
for (const k in rankingData) {
|
||||
const key = k as ProfileRatingLookup[IDStrings];
|
||||
const key = k as ProfileRatingAlgorithms[GPTString];
|
||||
|
||||
if (key !== alg) {
|
||||
extendData.push(
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ShowcaseStatDetails,
|
||||
SongDocument,
|
||||
Playtype,
|
||||
GetScoreMetricConf,
|
||||
} from "tachi-common";
|
||||
import { UGPTPreferenceStatsReturn } from "types/api-returns";
|
||||
import { GamePT, UGPT } from "types/react";
|
||||
@@ -259,14 +260,14 @@ function StatDelta({
|
||||
v1,
|
||||
v2,
|
||||
mode,
|
||||
property,
|
||||
metric: property,
|
||||
game,
|
||||
playtype,
|
||||
}: {
|
||||
v1: number;
|
||||
v2?: number;
|
||||
mode: "folder" | "chart";
|
||||
property: UGPTPreferenceStatsReturn["stat"]["metric"];
|
||||
metric: string;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
@@ -276,11 +277,8 @@ function StatDelta({
|
||||
v2 = 0;
|
||||
}
|
||||
|
||||
let d: string | number = v2 - v1;
|
||||
const formattedV2 = FormatValue(game, playtype, mode, property, v2);
|
||||
if (property === "percent") {
|
||||
d = `${d.toFixed(2)}%`;
|
||||
}
|
||||
const d = FormatValue(game, playtype, mode, property, v2 - v1);
|
||||
|
||||
let colour;
|
||||
if (v2 === v1) {
|
||||
@@ -310,7 +308,7 @@ export function FormatValue(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
mode: "folder" | "chart",
|
||||
prop: "grade" | "lamp" | "score" | "percent" | "playcount",
|
||||
metric: string,
|
||||
value: number
|
||||
) {
|
||||
if (mode === "folder") {
|
||||
@@ -318,42 +316,21 @@ export function FormatValue(
|
||||
}
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const conf = GetScoreMetricConf(gptConfig, metric);
|
||||
|
||||
if (prop === "percent") {
|
||||
return `${value.toFixed(2)}%`;
|
||||
} else if (prop === "grade") {
|
||||
return gptConfig.grades[value];
|
||||
} else if (prop === "lamp") {
|
||||
return gptConfig.lamps[value];
|
||||
if (!conf) {
|
||||
return "UNKNOWN METRIC";
|
||||
}
|
||||
|
||||
if (conf.type === "ENUM") {
|
||||
return conf.values[value];
|
||||
} else if (conf.type === "DECIMAL" || conf.type === "INTEGER") {
|
||||
return conf.formatter(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function FormatPropertyGTE(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
prop: "grade" | "lamp" | "score" | "percent" | "playcount",
|
||||
gte: number
|
||||
) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (gte === null) {
|
||||
return "NO DATA";
|
||||
}
|
||||
|
||||
if (prop === "grade") {
|
||||
return gptConfig.grades[gte];
|
||||
} else if (prop === "lamp") {
|
||||
return gptConfig.lamps[gte];
|
||||
} else if (prop === "score" || prop === "playcount") {
|
||||
return gte;
|
||||
}
|
||||
// else if (prop === "percent") {
|
||||
return gte.toFixed(2);
|
||||
// }
|
||||
}
|
||||
|
||||
export function GetStatName(
|
||||
stat: ShowcaseStatDetails,
|
||||
game: Game,
|
||||
@@ -398,14 +375,14 @@ export function StatDisplay({
|
||||
</Link>
|
||||
<h4>
|
||||
{UppercaseFirst(stat.metric)}:{" "}
|
||||
{FormatPropertyGTE(game, playtype, stat.metric, result.value)}
|
||||
{FormatValue(game, playtype, stat.mode, stat.metric, result.value)}
|
||||
</h4>
|
||||
{user && user.id !== reqUser.id && (
|
||||
<StatDelta
|
||||
v1={statData.result.value}
|
||||
v2={compareData?.result.value}
|
||||
mode={stat.mode}
|
||||
property={stat.metric}
|
||||
metric={stat.metric}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
/>
|
||||
@@ -432,7 +409,7 @@ export function StatDisplay({
|
||||
</Link>
|
||||
<h5>
|
||||
{UppercaseFirst(stat.metric)} >={" "}
|
||||
{FormatPropertyGTE(game, playtype, stat.metric, stat.gte)}
|
||||
{FormatValue(game, playtype, stat.mode, stat.metric, stat.gte)}
|
||||
</h5>
|
||||
<h4>
|
||||
{result.value}
|
||||
@@ -447,7 +424,7 @@ export function StatDisplay({
|
||||
v1={statData.result.value}
|
||||
v2={compareData?.result.value}
|
||||
mode={stat.mode}
|
||||
property={stat.metric}
|
||||
metric={stat.metric}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormatGPTProfileRating, FormatGPTRating, UppercaseFirst } from "util/misc";
|
||||
import { FormatGPTProfileRating, UppercaseFirst } from "util/misc";
|
||||
import ClassBadge from "components/game/ClassBadge";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
@@ -6,22 +6,21 @@ import Divider from "components/util/Divider";
|
||||
import React from "react";
|
||||
import {
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
ScoreCalculatedDataLookup,
|
||||
ProfileRatingLookup,
|
||||
GPTString,
|
||||
ProfileRatingAlgorithms,
|
||||
UserGameStats,
|
||||
Classes,
|
||||
} from "tachi-common";
|
||||
import { GameClassSets } from "tachi-common/game-classes";
|
||||
|
||||
export default function UGPTRatingsTable({ ugs }: { ugs: UserGameStats }) {
|
||||
const gptConfig = GetGamePTConfig(ugs.game, ugs.playtype);
|
||||
|
||||
const ratings = Object.entries(ugs.ratings) as [ProfileRatingLookup[IDStrings], number][];
|
||||
const ratings = Object.entries(ugs.ratings) as [ProfileRatingAlgorithms[GPTString], number][];
|
||||
|
||||
return (
|
||||
<MiniTable className="table-sm text-center" headers={["Player Stats"]} colSpan={2}>
|
||||
<>
|
||||
{(Object.keys(gptConfig.classHumanisedFormat) as GameClassSets[IDStrings][])
|
||||
{(Object.keys(gptConfig.classes) as Classes[GPTString][])
|
||||
.filter((k) => ugs.classes[k] !== undefined)
|
||||
.map((k) => (
|
||||
<tr key={k}>
|
||||
@@ -44,15 +43,13 @@ export default function UGPTRatingsTable({ ugs }: { ugs: UserGameStats }) {
|
||||
<QuickTooltip
|
||||
tooltipContent={
|
||||
<div>
|
||||
{gptConfig.profileRatingAlgDescriptions[k]}
|
||||
{k in gptConfig.scoreRatingAlgDescriptions &&
|
||||
gptConfig.idString !== "itg:Stamina" && (
|
||||
<>
|
||||
<Divider />({UppercaseFirst(k)}:{" "}
|
||||
{/* @ts-expect-error I know better. */}
|
||||
{gptConfig.scoreRatingAlgDescriptions[k]})
|
||||
</>
|
||||
)}
|
||||
{gptConfig.profileRatingAlgs[k].description}
|
||||
{k in gptConfig.scoreRatingAlgs && (
|
||||
<>
|
||||
<Divider />({UppercaseFirst(k)}:{" "}
|
||||
{gptConfig.scoreRatingAlgs[k].description})
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
wide
|
||||
|
||||
@@ -4,11 +4,11 @@ import useLUGPTSettings from "./useLUGPTSettings";
|
||||
export function useBucket(game: Game, playtype: Playtype) {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (!settings?.preferences.scoreBucket) {
|
||||
if (!settings?.preferences.preferredDefaultEnum) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return gptConfig.scoreBucket;
|
||||
return gptConfig.preferredDefaultEnum;
|
||||
}
|
||||
|
||||
return settings.preferences.scoreBucket;
|
||||
return settings.preferences.preferredDefaultEnum;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { UGPTContext } from "context/UGPTContext";
|
||||
import { useContext } from "react";
|
||||
import { IDStrings, UGPTSettings } from "tachi-common";
|
||||
import { GPTString, UGPTSettingsDocument } from "tachi-common";
|
||||
|
||||
export default function useLUGPTSettings<I extends IDStrings>() {
|
||||
export default function useLUGPTSettings<GPT extends GPTString>() {
|
||||
const { loggedInData, setLoggedInData } = useContext(UGPTContext);
|
||||
|
||||
const settings = (loggedInData?.settings ?? null) as UGPTSettings<I> | null;
|
||||
const settings = (loggedInData?.settings ?? null) as UGPTSettingsDocument<GPT> | null;
|
||||
|
||||
const setSettings = (newSettings: UGPTSettings<I>) => {
|
||||
const setSettings = (newSettings: UGPTSettingsDocument<GPT>) => {
|
||||
if (!loggedInData) {
|
||||
throw new Error(`Tried to set settings while nobody was logged in?`);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UGPTSettings } from "tachi-common";
|
||||
import { UGPTSettingsDocument } from "tachi-common";
|
||||
import useLUGPTSettings from "./useLUGPTSettings";
|
||||
|
||||
export default function usePreferredRanking():
|
||||
| UGPTSettings["preferences"]["preferredRanking"]
|
||||
| UGPTSettingsDocument["preferences"]["preferredRanking"]
|
||||
| null {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
import {
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
ScoreCalculatedDataLookup,
|
||||
SessionCalculatedDataLookup,
|
||||
ProfileRatingLookup,
|
||||
GPTString,
|
||||
Playtype,
|
||||
ProfileRatingAlgorithms,
|
||||
ScoreRatingAlgorithms,
|
||||
SessionRatingAlgorithms,
|
||||
} from "tachi-common";
|
||||
import useLUGPTSettings from "./useLUGPTSettings";
|
||||
|
||||
export default function useScoreRatingAlg<I extends IDStrings = IDStrings>(
|
||||
export default function useScoreRatingAlg<GPT extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
): ScoreCalculatedDataLookup[I] {
|
||||
): ScoreRatingAlgorithms[GPT] {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (!settings?.preferences.preferredScoreAlg) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return gptConfig.defaultScoreRatingAlg as ScoreCalculatedDataLookup[I];
|
||||
return gptConfig.defaultScoreRatingAlg as ScoreRatingAlgorithms[GPT];
|
||||
}
|
||||
|
||||
return settings.preferences.preferredScoreAlg as ScoreCalculatedDataLookup[I];
|
||||
return settings.preferences.preferredScoreAlg as ScoreRatingAlgorithms[GPT];
|
||||
}
|
||||
|
||||
export function useSessionRatingAlg<I extends IDStrings = IDStrings>(
|
||||
export function useSessionRatingAlg<GPT extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
): SessionCalculatedDataLookup[I] {
|
||||
): SessionRatingAlgorithms[GPT] {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (!settings?.preferences.preferredSessionAlg) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return gptConfig.defaultSessionRatingAlg as SessionCalculatedDataLookup[I];
|
||||
return gptConfig.defaultSessionRatingAlg as SessionRatingAlgorithms[GPT];
|
||||
}
|
||||
|
||||
return settings.preferences.preferredSessionAlg as SessionCalculatedDataLookup[I];
|
||||
return settings.preferences.preferredSessionAlg as SessionRatingAlgorithms[GPT];
|
||||
}
|
||||
|
||||
export function useProfileRatingAlg<I extends IDStrings = IDStrings>(
|
||||
export function useProfileRatingAlg<GPT extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
): ProfileRatingLookup[I] {
|
||||
): ProfileRatingAlgorithms[GPT] {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (!settings?.preferences.preferredProfileAlg) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return gptConfig.defaultProfileRatingAlg as ProfileRatingLookup[I];
|
||||
return gptConfig.defaultProfileRatingAlg as ProfileRatingAlgorithms[GPT];
|
||||
}
|
||||
|
||||
return settings.preferences.preferredProfileAlg as ProfileRatingLookup[I];
|
||||
return settings.preferences.preferredProfileAlg as ProfileRatingAlgorithms[GPT];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { UserSettings } from "tachi-common";
|
||||
import { UserSettingsDocument } from "tachi-common";
|
||||
import { JustChildren, SetState } from "types/react";
|
||||
import { UserContext } from "./UserContext";
|
||||
|
||||
@@ -8,14 +8,14 @@ import { UserContext } from "./UserContext";
|
||||
* Contains the current user's settings.
|
||||
*/
|
||||
export const UserSettingsContext = createContext<{
|
||||
settings: UserSettings | null;
|
||||
setSettings: SetState<UserSettings | null>;
|
||||
settings: UserSettingsDocument | null;
|
||||
setSettings: SetState<UserSettingsDocument | null>;
|
||||
}>({ settings: null, setSettings: () => void 0 });
|
||||
|
||||
UserSettingsContext.displayName = "UserSettingsContext";
|
||||
|
||||
export function UserSettingsContextProvider({ children }: JustChildren) {
|
||||
const [settings, setSettings] = useState<UserSettings | null>(null);
|
||||
const [settings, setSettings] = useState<UserSettingsDocument | null>(null);
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
@@ -25,7 +25,7 @@ export function UserSettingsContextProvider({ children }: JustChildren) {
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const res = await APIFetchV1<UserSettings>(`/users/${user.id}/settings`);
|
||||
const res = await APIFetchV1<UserSettingsDocument>(`/users/${user.id}/settings`);
|
||||
|
||||
if (res.success) {
|
||||
setSettings(res.body);
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { COLOUR_SET, GPTString, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GITADORA_DORA_IMPL, GITADORA_GITA_IMPL } from "./games/gitadora";
|
||||
import { SDVX_IMPL, USC_IMPL } from "./games/sdvx-usc";
|
||||
import { GPTClientImplementation } from "./types";
|
||||
import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./games/iidx";
|
||||
import { BMS_PMS_IMPL } from "./games/bms-pms";
|
||||
import { bg, bgc } from "./games/_util";
|
||||
|
||||
type GPTClientImplementations = {
|
||||
[GPT in GPTString]: GPTClientImplementation<GPT>;
|
||||
};
|
||||
|
||||
export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
"iidx:SP": IIDX_SP_IMPL,
|
||||
"iidx:DP": IIDX_DP_IMPL,
|
||||
"chunithm:Single": {
|
||||
classColours: {
|
||||
colour: {
|
||||
BLUE: "info",
|
||||
GREEN: bg("green"),
|
||||
ORANGE: bg("orange"),
|
||||
RED: "danger",
|
||||
PURPLE: bg("purple"),
|
||||
COPPER: bg("bronze"),
|
||||
SILVER: "secondary",
|
||||
GOLD: "warning",
|
||||
PLATINUM: bgc("silver", "black"),
|
||||
RAINBOW: "success",
|
||||
},
|
||||
},
|
||||
enumColours: {
|
||||
grade: {
|
||||
D: COLOUR_SET.red,
|
||||
C: COLOUR_SET.purple,
|
||||
B: COLOUR_SET.paleBlue,
|
||||
BB: COLOUR_SET.blue,
|
||||
BBB: COLOUR_SET.vibrantBlue,
|
||||
A: COLOUR_SET.paleGreen,
|
||||
AA: COLOUR_SET.green,
|
||||
AAA: COLOUR_SET.vibrantGreen,
|
||||
S: COLOUR_SET.vibrantOrange,
|
||||
SS: COLOUR_SET.vibrantYellow,
|
||||
SSS: COLOUR_SET.teal,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.paleGreen,
|
||||
"FULL COMBO": COLOUR_SET.paleBlue,
|
||||
"ALL JUSTICE": COLOUR_SET.gold,
|
||||
"ALL JUSTICE CRITICAL": COLOUR_SET.white,
|
||||
},
|
||||
},
|
||||
difficultyColours: {
|
||||
BASIC: COLOUR_SET.blue,
|
||||
ADVANCED: COLOUR_SET.orange,
|
||||
EXPERT: COLOUR_SET.red,
|
||||
MASTER: COLOUR_SET.purple,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x.scoreData.score)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
},
|
||||
"jubeat:Single": {
|
||||
enumColours: {
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
"FULL COMBO": COLOUR_SET.teal,
|
||||
EXCELLENT: COLOUR_SET.white,
|
||||
},
|
||||
grade: {
|
||||
E: COLOUR_SET.gray,
|
||||
D: COLOUR_SET.maroon,
|
||||
C: COLOUR_SET.red,
|
||||
B: COLOUR_SET.blue,
|
||||
A: COLOUR_SET.green,
|
||||
S: COLOUR_SET.gold,
|
||||
SS: COLOUR_SET.orange,
|
||||
SSS: COLOUR_SET.teal,
|
||||
EXC: COLOUR_SET.white,
|
||||
},
|
||||
},
|
||||
classColours: {
|
||||
colour: {
|
||||
BLACK: bg("secondary"),
|
||||
YELLOW_GREEN: bgc("yellowgreen", "black"),
|
||||
GREEN: bg("green"),
|
||||
LIGHT_BLUE: bgc("cyan", "black"),
|
||||
BLUE: bg("info"),
|
||||
VIOLET: bg("violet"),
|
||||
PURPLE: bg("purple"),
|
||||
PINK: bgc("pink", "black"),
|
||||
ORANGE: bg("orange"),
|
||||
GOLD: bg("warning"),
|
||||
},
|
||||
},
|
||||
difficultyColours: {
|
||||
BSC: COLOUR_SET.green,
|
||||
ADV: COLOUR_SET.gold,
|
||||
EXT: COLOUR_SET.red,
|
||||
"HARD BSC": COLOUR_SET.darkGreen,
|
||||
"HARD ADV": COLOUR_SET.orange,
|
||||
"HARD EXT": COLOUR_SET.vibrantRed,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x?.scoreData.score ?? -Infinity)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x?.scoreData.score ?? -Infinity)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp ?? -Infinity)],
|
||||
],
|
||||
},
|
||||
"maimaidx:Single": {
|
||||
enumColours: {
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.green,
|
||||
"FULL COMBO": COLOUR_SET.blue,
|
||||
"FULL COMBO+": COLOUR_SET.paleBlue,
|
||||
"ALL PERFECT": COLOUR_SET.gold,
|
||||
"ALL PERFECT+": COLOUR_SET.teal,
|
||||
},
|
||||
grade: {
|
||||
D: COLOUR_SET.gray,
|
||||
C: COLOUR_SET.red,
|
||||
B: COLOUR_SET.maroon,
|
||||
BB: COLOUR_SET.purple,
|
||||
BBB: COLOUR_SET.paleGreen,
|
||||
A: COLOUR_SET.green,
|
||||
AA: COLOUR_SET.paleBlue,
|
||||
AAA: COLOUR_SET.blue,
|
||||
S: COLOUR_SET.gold,
|
||||
"S+": COLOUR_SET.vibrantYellow,
|
||||
SS: COLOUR_SET.paleOrange,
|
||||
"SS+": COLOUR_SET.orange,
|
||||
SSS: COLOUR_SET.teal,
|
||||
"SSS+": COLOUR_SET.white,
|
||||
},
|
||||
},
|
||||
classColours: {
|
||||
dan: {
|
||||
DAN_1: bg("warning"),
|
||||
DAN_2: bg("warning"),
|
||||
DAN_3: bg("warning"),
|
||||
DAN_4: bg("warning"),
|
||||
DAN_5: bg("warning"),
|
||||
DAN_6: bg("warning"),
|
||||
DAN_7: bg("warning"),
|
||||
DAN_8: bg("warning"),
|
||||
DAN_9: bg("warning"),
|
||||
DAN_10: bg("warning"),
|
||||
|
||||
SHINDAN_1: bg("purple"),
|
||||
SHINDAN_2: bg("purple"),
|
||||
SHINDAN_3: bg("purple"),
|
||||
SHINDAN_4: bg("purple"),
|
||||
SHINDAN_5: bg("purple"),
|
||||
SHINDAN_6: bg("purple"),
|
||||
SHINDAN_7: bg("purple"),
|
||||
SHINDAN_8: bg("purple"),
|
||||
SHINDAN_9: bg("purple"),
|
||||
SHINDAN_10: bg("purple"),
|
||||
|
||||
SHINKAIDEN: bg("purple"),
|
||||
},
|
||||
colour: {
|
||||
WHITE: bgc("white", "black"),
|
||||
BLUE: bgc("cyan", "black"),
|
||||
GREEN: bg("green"),
|
||||
YELLOW: bgc("yellow", "black"),
|
||||
RED: bg("red"),
|
||||
PURPLE: bg("purple"),
|
||||
BRONZE: bg("brown"),
|
||||
SILVER: bg("gray"),
|
||||
GOLD: bg("warning"),
|
||||
PLATINUM: bgc("lightgoldenrodyellow", "black"),
|
||||
|
||||
RAINBOW: {
|
||||
background:
|
||||
"linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)",
|
||||
},
|
||||
},
|
||||
},
|
||||
difficultyColours: {
|
||||
Basic: COLOUR_SET.green,
|
||||
Advanced: COLOUR_SET.orange,
|
||||
Expert: COLOUR_SET.red,
|
||||
Master: COLOUR_SET.purple,
|
||||
"Re:Master": COLOUR_SET.white,
|
||||
"DX Basic": COLOUR_SET.green,
|
||||
"DX Advanced": COLOUR_SET.orange,
|
||||
"DX Expert": COLOUR_SET.red,
|
||||
"DX Master": COLOUR_SET.purple,
|
||||
"DX Re:Master": COLOUR_SET.white,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Percent", "%", NumericSOV((x) => x?.scoreData.percent)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x?.scoreData.percent)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
},
|
||||
"museca:Single": {
|
||||
enumColours: {
|
||||
grade: {
|
||||
没: COLOUR_SET.gray,
|
||||
拙: COLOUR_SET.maroon,
|
||||
凡: COLOUR_SET.red,
|
||||
佳: COLOUR_SET.paleGreen,
|
||||
良: COLOUR_SET.paleBlue,
|
||||
優: COLOUR_SET.green,
|
||||
秀: COLOUR_SET.blue,
|
||||
傑: COLOUR_SET.teal,
|
||||
傑G: COLOUR_SET.gold,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.green,
|
||||
"CONNECT ALL": COLOUR_SET.teal,
|
||||
"PERFECT CONNECT ALL": COLOUR_SET.gold,
|
||||
},
|
||||
},
|
||||
classColours: {},
|
||||
difficultyColours: {
|
||||
Green: COLOUR_SET.green,
|
||||
Yellow: COLOUR_SET.vibrantYellow,
|
||||
Red: COLOUR_SET.red,
|
||||
},
|
||||
ratingSystems: [],
|
||||
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x?.scoreData.score)],
|
||||
["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
},
|
||||
"popn:9B": {
|
||||
enumColours: {
|
||||
grade: {
|
||||
E: COLOUR_SET.gray,
|
||||
D: COLOUR_SET.maroon,
|
||||
C: COLOUR_SET.red,
|
||||
B: COLOUR_SET.blue,
|
||||
A: COLOUR_SET.green,
|
||||
AA: COLOUR_SET.orange,
|
||||
AAA: COLOUR_SET.gold,
|
||||
S: COLOUR_SET.teal,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
"EASY CLEAR": COLOUR_SET.green,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
"FULL COMBO": COLOUR_SET.teal,
|
||||
PERFECT: COLOUR_SET.gold,
|
||||
},
|
||||
clearMedal: {
|
||||
failedCircle: COLOUR_SET.red,
|
||||
failedDiamond: COLOUR_SET.red,
|
||||
failedStar: COLOUR_SET.red,
|
||||
easyClear: COLOUR_SET.green,
|
||||
clearCircle: COLOUR_SET.blue,
|
||||
clearDiamond: COLOUR_SET.blue,
|
||||
clearStar: COLOUR_SET.blue,
|
||||
fullComboCircle: COLOUR_SET.teal,
|
||||
fullComboDiamond: COLOUR_SET.teal,
|
||||
fullComboStar: COLOUR_SET.teal,
|
||||
perfect: COLOUR_SET.gold,
|
||||
},
|
||||
},
|
||||
classColours: {
|
||||
class: {
|
||||
KITTY: bg("brown"),
|
||||
STUDENT: bg("green"),
|
||||
DELINQUENT: bgc("lime", "black"),
|
||||
DETECTIVE: bg("purple"),
|
||||
IDOL: "danger",
|
||||
GENERAL: bg("darkgoldenrod"),
|
||||
HERMIT: "success",
|
||||
GOD: "warning",
|
||||
},
|
||||
},
|
||||
difficultyColours: {
|
||||
Easy: COLOUR_SET.blue,
|
||||
Normal: COLOUR_SET.green,
|
||||
Hyper: COLOUR_SET.orange,
|
||||
EX: COLOUR_SET.red,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x.scoreData.score)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.clearMedal)],
|
||||
],
|
||||
},
|
||||
"wacca:Single": {
|
||||
enumColours: {
|
||||
grade: {
|
||||
D: COLOUR_SET.gray,
|
||||
C: COLOUR_SET.maroon,
|
||||
B: COLOUR_SET.red,
|
||||
A: COLOUR_SET.paleGreen,
|
||||
AA: COLOUR_SET.green,
|
||||
AAA: COLOUR_SET.vibrantGreen,
|
||||
S: COLOUR_SET.gold,
|
||||
"S+": COLOUR_SET.vibrantYellow,
|
||||
SS: COLOUR_SET.paleOrange,
|
||||
"SS+": COLOUR_SET.orange,
|
||||
SSS: COLOUR_SET.pink,
|
||||
"SSS+": COLOUR_SET.vibrantPink,
|
||||
MASTER: COLOUR_SET.white,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
MISSLESS: COLOUR_SET.orange,
|
||||
"FULL COMBO": COLOUR_SET.pink,
|
||||
"ALL MARVELOUS": COLOUR_SET.gold,
|
||||
},
|
||||
},
|
||||
classColours: {
|
||||
colour: {
|
||||
ASH: "secondary",
|
||||
NAVY: bg("darkblue"),
|
||||
YELLOW: bg("orange"),
|
||||
RED: "danger",
|
||||
PURPLE: bg("purple"),
|
||||
BLUE: "info",
|
||||
SILVER: bgc("silver", "black"),
|
||||
GOLD: "warning",
|
||||
RAINBOW: "success",
|
||||
},
|
||||
stageUp: {
|
||||
I: null,
|
||||
II: null,
|
||||
III: null,
|
||||
IV: null,
|
||||
V: null,
|
||||
VI: null,
|
||||
VII: null,
|
||||
VIII: null,
|
||||
IX: null,
|
||||
X: null,
|
||||
XI: null,
|
||||
XII: null,
|
||||
XIII: null,
|
||||
XIV: null,
|
||||
},
|
||||
},
|
||||
difficultyColours: {
|
||||
NORMAL: COLOUR_SET.blue,
|
||||
HARD: COLOUR_SET.gold,
|
||||
EXPERT: COLOUR_SET.pink,
|
||||
INFERNO: COLOUR_SET.purple,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x.scoreData.score)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
},
|
||||
"itg:Stamina": {
|
||||
enumColours: {
|
||||
grade: {
|
||||
F: COLOUR_SET.gray,
|
||||
D: COLOUR_SET.red,
|
||||
C: COLOUR_SET.maroon,
|
||||
B: COLOUR_SET.purple,
|
||||
A: COLOUR_SET.green,
|
||||
S: COLOUR_SET.orange,
|
||||
"★": COLOUR_SET.pink,
|
||||
"★★": COLOUR_SET.vibrantPink,
|
||||
"★★★": COLOUR_SET.teal,
|
||||
"★★★★": COLOUR_SET.white,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
"FULL COMBO": COLOUR_SET.pink,
|
||||
"FULL EXCELLENT COMBO": COLOUR_SET.gold,
|
||||
QUAD: COLOUR_SET.white,
|
||||
},
|
||||
},
|
||||
difficultyColours: {
|
||||
// this game has dynamic difficulties. Formatting is handled elsewhere.
|
||||
},
|
||||
classColours: {},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x.scoreData.finalPercent)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x.scoreData.scorePercent)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
},
|
||||
"gitadora:Dora": GITADORA_DORA_IMPL,
|
||||
"gitadora:Gita": GITADORA_GITA_IMPL,
|
||||
"bms:14K": BMS_PMS_IMPL,
|
||||
"bms:7K": BMS_PMS_IMPL,
|
||||
"pms:Controller": BMS_PMS_IMPL,
|
||||
"pms:Keyboard": BMS_PMS_IMPL,
|
||||
"sdvx:Single": SDVX_IMPL,
|
||||
"usc:Controller": USC_IMPL,
|
||||
"usc:Keyboard": USC_IMPL,
|
||||
};
|
||||
|
||||
export function GetEnumColour(score: ScoreDocument | PBScoreDocument, enumName: string) {
|
||||
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(score.game, score.playtype)];
|
||||
|
||||
// @ts-expect-error lol
|
||||
return gptImpl.enumColours[enumName][score.scoreData[enumName]];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { GPTRatingSystem } from "lib/types";
|
||||
import { CSSProperties } from "react";
|
||||
import { GPTString } from "tachi-common";
|
||||
|
||||
export function CreateRatingSys<GPT extends GPTString>(
|
||||
name: string,
|
||||
description: string,
|
||||
toNumber: GPTRatingSystem<GPT>["toNumber"],
|
||||
toString: GPTRatingSystem<GPT>["toString"],
|
||||
idvDifference: GPTRatingSystem<GPT>["idvDifference"] = () => false
|
||||
): GPTRatingSystem<GPT> {
|
||||
return {
|
||||
description,
|
||||
name,
|
||||
toNumber,
|
||||
toString,
|
||||
idvDifference,
|
||||
};
|
||||
}
|
||||
|
||||
export function bg(bgColour: string): CSSProperties {
|
||||
return { backgroundColor: bgColour };
|
||||
}
|
||||
|
||||
export function bgc(bgColour: string, colour: string): CSSProperties {
|
||||
return { backgroundColor: bgColour, color: colour };
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { IsNullish } from "util/misc";
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { GPTClientImplementation } from "lib/types";
|
||||
import { COLOUR_SET, GPTStrings } from "tachi-common";
|
||||
import { FormatSieglindeBMS } from "tachi-common/config/game-support/bms";
|
||||
import { CreateRatingSys } from "./_util";
|
||||
|
||||
const BMS_PMS_IMPL: GPTClientImplementation<GPTStrings["bms" | "pms"]> = {
|
||||
difficultyColours: {
|
||||
CHART: COLOUR_SET.gray, // lol
|
||||
},
|
||||
enumColours: {
|
||||
grade: {
|
||||
F: COLOUR_SET.gray,
|
||||
E: COLOUR_SET.red,
|
||||
D: COLOUR_SET.maroon,
|
||||
C: COLOUR_SET.purple,
|
||||
B: COLOUR_SET.paleBlue,
|
||||
A: COLOUR_SET.green,
|
||||
AA: COLOUR_SET.blue,
|
||||
AAA: COLOUR_SET.gold,
|
||||
"MAX-": COLOUR_SET.teal,
|
||||
MAX: COLOUR_SET.white,
|
||||
},
|
||||
lamp: {
|
||||
"NO PLAY": COLOUR_SET.gray,
|
||||
FAILED: COLOUR_SET.red,
|
||||
"ASSIST CLEAR": COLOUR_SET.purple,
|
||||
"EASY CLEAR": COLOUR_SET.green,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
"HARD CLEAR": COLOUR_SET.orange,
|
||||
"EX HARD CLEAR": COLOUR_SET.gold,
|
||||
"FULL COMBO": COLOUR_SET.teal,
|
||||
},
|
||||
},
|
||||
classColours: {},
|
||||
ratingSystems: [
|
||||
CreateRatingSys(
|
||||
"Sieglinde EC",
|
||||
"TODO lol",
|
||||
(c) => c.data.sglEC,
|
||||
(c) => (IsNullish(c.data.sglEC) ? null : FormatSieglindeBMS(c.data.sglEC!))
|
||||
),
|
||||
CreateRatingSys(
|
||||
"Sieglinde HC",
|
||||
"TODO lol",
|
||||
(c) => c.data.sglHC,
|
||||
(c) => (IsNullish(c.data.sglHC) ? null : FormatSieglindeBMS(c.data.sglHC!))
|
||||
),
|
||||
],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x.scoreData.percent)],
|
||||
["Deltas", "Deltas", NumericSOV((x) => x.scoreData.percent)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
};
|
||||
|
||||
export const;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { GPTClientImplementation } from "lib/types";
|
||||
import { COLOUR_SET, GPTString, GPTStrings } from "tachi-common";
|
||||
import { bg, bgc } from "./_util";
|
||||
|
||||
const GITADORA_ENUM_COLOURS: GPTClientImplementation<
|
||||
"gitadora:Dora" | "gitadora:Gita"
|
||||
>["enumColours"] = {
|
||||
grade: {
|
||||
C: COLOUR_SET.purple,
|
||||
B: COLOUR_SET.blue,
|
||||
A: COLOUR_SET.green,
|
||||
S: COLOUR_SET.orange,
|
||||
SS: COLOUR_SET.gold,
|
||||
MAX: COLOUR_SET.white,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
"FULL COMBO": COLOUR_SET.teal,
|
||||
EXCELLENT: COLOUR_SET.gold,
|
||||
},
|
||||
};
|
||||
|
||||
const GITADORA_HEADERS: GPTClientImplementation<"gitadora:Dora" | "gitadora:Gita">["scoreHeaders"] =
|
||||
[
|
||||
["Percent", "Percent", NumericSOV((x) => x.scoreData.percent)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x.scoreData.percent)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
|
||||
];
|
||||
|
||||
const GITADORA_COLOURS: GPTClientImplementation<GPTStrings["gitadora"]>["classColours"] = {
|
||||
colour: {
|
||||
WHITE: bgc("white", "black"),
|
||||
ORANGE: bg("orange"),
|
||||
ORANGE_GRD: bg("orange"),
|
||||
YELLOW: "warning",
|
||||
YELLOW_GRD: "warning",
|
||||
GREEN: bg("green"),
|
||||
GREEN_GRD: bg("green"),
|
||||
BLUE: "info",
|
||||
BLUE_GRD: "info",
|
||||
PURPLE: bg("purple"),
|
||||
PURPLE_GRD: bg("purple"),
|
||||
RED: "danger",
|
||||
RED_GRD: "danger",
|
||||
BRONZE: bg("bronze"),
|
||||
SILVER: bgc("silver", "black"),
|
||||
GOLD: bg("gold"),
|
||||
|
||||
RAINBOW: {
|
||||
background:
|
||||
"linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GITADORA_GITA_IMPL: GPTClientImplementation<"gitadora:Gita"> = {
|
||||
enumColours: GITADORA_ENUM_COLOURS,
|
||||
difficultyColours: {
|
||||
BASIC: COLOUR_SET.blue,
|
||||
ADVANCED: COLOUR_SET.orange,
|
||||
EXTREME: COLOUR_SET.red,
|
||||
MASTER: COLOUR_SET.purple,
|
||||
"BASS BASIC": COLOUR_SET.vibrantBlue,
|
||||
"BASS ADVANCED": COLOUR_SET.vibrantOrange,
|
||||
"BASS EXTREME": COLOUR_SET.vibrantRed,
|
||||
"BASS MASTER": COLOUR_SET.vibrantPurple,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: GITADORA_HEADERS,
|
||||
classColours: GITADORA_COLOURS,
|
||||
};
|
||||
|
||||
export const GITADORA_DORA_IMPL: GPTClientImplementation<"gitadora:Dora"> = {
|
||||
enumColours: GITADORA_ENUM_COLOURS,
|
||||
difficultyColours: {
|
||||
BASIC: COLOUR_SET.blue,
|
||||
ADVANCED: COLOUR_SET.orange,
|
||||
EXTREME: COLOUR_SET.red,
|
||||
MASTER: COLOUR_SET.purple,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: GITADORA_HEADERS,
|
||||
classColours: GITADORA_COLOURS,
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { GPTClientImplementation } from "lib/types";
|
||||
import { COLOUR_SET, GPTStrings } from "tachi-common";
|
||||
import { CreateRatingSys, bg, bgc } from "./_util";
|
||||
|
||||
const IIDX_ENUM_COLOURS: GPTClientImplementation<GPTStrings["iidx"]>["enumColours"] = {
|
||||
grade: {
|
||||
F: COLOUR_SET.gray,
|
||||
E: COLOUR_SET.red,
|
||||
D: COLOUR_SET.maroon,
|
||||
C: COLOUR_SET.purple,
|
||||
B: COLOUR_SET.paleBlue,
|
||||
A: COLOUR_SET.green,
|
||||
AA: COLOUR_SET.blue,
|
||||
AAA: COLOUR_SET.gold,
|
||||
"MAX-": COLOUR_SET.teal,
|
||||
MAX: COLOUR_SET.white,
|
||||
},
|
||||
lamp: {
|
||||
"NO PLAY": COLOUR_SET.gray,
|
||||
FAILED: COLOUR_SET.red,
|
||||
"ASSIST CLEAR": COLOUR_SET.purple,
|
||||
"EASY CLEAR": COLOUR_SET.green,
|
||||
CLEAR: COLOUR_SET.blue,
|
||||
"HARD CLEAR": COLOUR_SET.orange,
|
||||
"EX HARD CLEAR": COLOUR_SET.gold,
|
||||
"FULL COMBO": COLOUR_SET.teal,
|
||||
},
|
||||
};
|
||||
|
||||
const IIDX_DIFF_COLOURS: GPTClientImplementation<GPTStrings["iidx"]>["difficultyColours"] = {
|
||||
NORMAL: COLOUR_SET.blue,
|
||||
HYPER: COLOUR_SET.orange,
|
||||
ANOTHER: COLOUR_SET.red,
|
||||
LEGGENDARIA: COLOUR_SET.purple,
|
||||
"All Scratch NORMAL": COLOUR_SET.blue,
|
||||
"All Scratch HYPER": COLOUR_SET.orange,
|
||||
"All Scratch ANOTHER": COLOUR_SET.red,
|
||||
"All Scratch LEGGENDARIA": COLOUR_SET.purple,
|
||||
"Kichiku NORMAL": COLOUR_SET.blue,
|
||||
"Kichiku HYPER": COLOUR_SET.orange,
|
||||
"Kichiku ANOTHER": COLOUR_SET.red,
|
||||
"Kichiku LEGGENDARIA": COLOUR_SET.purple,
|
||||
"Kiraku NORMAL": COLOUR_SET.blue,
|
||||
"Kiraku HYPER": COLOUR_SET.orange,
|
||||
"Kiraku ANOTHER": COLOUR_SET.red,
|
||||
"Kiraku LEGGENDARIA": COLOUR_SET.purple,
|
||||
};
|
||||
|
||||
const IIDX_HEADERS: GPTClientImplementation<"iidx:SP" | "iidx:DP">["scoreHeaders"] = [
|
||||
["Score", "Score", NumericSOV((x) => x.scoreData.percent)],
|
||||
["Deltas", "Deltas", NumericSOV((x) => x.scoreData.percent)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
|
||||
];
|
||||
|
||||
const IIDX_COLOURS: GPTClientImplementation<"iidx:SP" | "iidx:DP">["classColours"] = {
|
||||
dan: {
|
||||
KYU_7: bg("green"),
|
||||
KYU_6: bg("green"),
|
||||
KYU_5: bg("green"),
|
||||
KYU_4: bg("green"),
|
||||
KYU_3: bg("green"),
|
||||
KYU_2: bg("green"),
|
||||
KYU_1: bg("green"),
|
||||
DAN_1: "info",
|
||||
DAN_2: "info",
|
||||
DAN_3: "info",
|
||||
DAN_4: "info",
|
||||
DAN_5: "info",
|
||||
DAN_6: "info",
|
||||
DAN_7: "info",
|
||||
DAN_8: "info",
|
||||
DAN_9: "danger",
|
||||
DAN_10: "danger",
|
||||
CHUUDEN: bgc("silver", "black"),
|
||||
KAIDEN: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const IIDX_SP_IMPL: GPTClientImplementation<"iidx:SP"> = {
|
||||
difficultyColours: IIDX_DIFF_COLOURS,
|
||||
enumColours: IIDX_ENUM_COLOURS,
|
||||
ratingSystems: [
|
||||
CreateRatingSys(
|
||||
"NC Tier",
|
||||
"Tierlist Ratings for Normal Clears.",
|
||||
(c) => c.data.ncTier?.value,
|
||||
(c) => c.data.ncTier?.text,
|
||||
(c) => c.data.ncTier?.individualDifference
|
||||
),
|
||||
CreateRatingSys(
|
||||
"HC Tier",
|
||||
"Tierlist Ratings for Hard Clears.",
|
||||
(c) => c.data.hcTier?.value,
|
||||
(c) => c.data.hcTier?.text,
|
||||
(c) => c.data.hcTier?.individualDifference
|
||||
),
|
||||
CreateRatingSys(
|
||||
"EXHC Tier",
|
||||
"Tierlist Ratings for EX-HARD Clears.",
|
||||
(c) => c.data.exhcTier?.value,
|
||||
(c) => c.data.exhcTier?.text,
|
||||
(c) => c.data.exhcTier?.individualDifference
|
||||
),
|
||||
],
|
||||
scoreHeaders: IIDX_HEADERS,
|
||||
classColours: IIDX_COLOURS,
|
||||
};
|
||||
|
||||
export const IIDX_DP_IMPL: GPTClientImplementation<"iidx:DP"> = {
|
||||
difficultyColours: IIDX_DIFF_COLOURS,
|
||||
enumColours: IIDX_ENUM_COLOURS,
|
||||
ratingSystems: [
|
||||
CreateRatingSys(
|
||||
"DP Tier",
|
||||
"The unofficial DP tiers, taken from https://zasa.sakura.ne.jp/dp/run.php.",
|
||||
(c) => c.data.dpTier?.value,
|
||||
(c) => c.data.dpTier?.text
|
||||
),
|
||||
],
|
||||
scoreHeaders: IIDX_HEADERS,
|
||||
classColours: IIDX_COLOURS,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NumericSOV } from "util/sorts";
|
||||
import { GPTClientImplementation } from "lib/types";
|
||||
import { COLOUR_SET, GPTStrings } from "tachi-common";
|
||||
import { CreateRatingSys } from "./_util";
|
||||
|
||||
const SDVXLIKE_ENUM_COLOURS: GPTClientImplementation<GPTStrings["usc" | "sdvx"]>["enumColours"] = {
|
||||
grade: {
|
||||
D: COLOUR_SET.gray,
|
||||
C: COLOUR_SET.red,
|
||||
B: COLOUR_SET.maroon,
|
||||
A: COLOUR_SET.paleBlue,
|
||||
"A+": COLOUR_SET.blue,
|
||||
AA: COLOUR_SET.paleGreen,
|
||||
"AA+": COLOUR_SET.green,
|
||||
AAA: COLOUR_SET.gold,
|
||||
"AAA+": COLOUR_SET.vibrantOrange,
|
||||
S: COLOUR_SET.teal,
|
||||
PUC: COLOUR_SET.pink,
|
||||
},
|
||||
lamp: {
|
||||
FAILED: COLOUR_SET.red,
|
||||
CLEAR: COLOUR_SET.green,
|
||||
"EXCESSIVE CLEAR": COLOUR_SET.purple,
|
||||
"ULTIMATE CHAIN": COLOUR_SET.teal,
|
||||
"PERFECT ULTIMATE CHAIN": COLOUR_SET.gold,
|
||||
},
|
||||
};
|
||||
|
||||
export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = {
|
||||
enumColours: SDVXLIKE_ENUM_COLOURS,
|
||||
difficultyColours: {
|
||||
NOV: COLOUR_SET.purple,
|
||||
ADV: COLOUR_SET.vibrantYellow,
|
||||
EXH: COLOUR_SET.red,
|
||||
INF: COLOUR_SET.vibrantPink,
|
||||
GRV: COLOUR_SET.orange,
|
||||
HVN: COLOUR_SET.teal,
|
||||
VVD: COLOUR_SET.pink,
|
||||
XCD: COLOUR_SET.blue,
|
||||
MXM: COLOUR_SET.white,
|
||||
},
|
||||
ratingSystems: [
|
||||
CreateRatingSys(
|
||||
"Tierlist",
|
||||
"The unofficial SDVX clearing tierlist",
|
||||
(c) => c.data.clearTier?.value,
|
||||
(c) => c.data.clearTier?.text,
|
||||
(c) => c.data.clearTier?.individualDifference
|
||||
),
|
||||
],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x?.scoreData.score)],
|
||||
["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
};
|
||||
export const USC_IMPL: GPTClientImplementation<GPTStrings["usc"]> = {
|
||||
enumColours: SDVXLIKE_ENUM_COLOURS,
|
||||
difficultyColours: {
|
||||
NOV: COLOUR_SET.purple,
|
||||
ADV: COLOUR_SET.vibrantYellow,
|
||||
EXH: COLOUR_SET.red,
|
||||
INF: COLOUR_SET.vibrantPink,
|
||||
},
|
||||
ratingSystems: [],
|
||||
scoreHeaders: [
|
||||
["Score", "Score", NumericSOV((x) => x?.scoreData.score)],
|
||||
["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)],
|
||||
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Header } from "components/tables/components/TachiTable";
|
||||
import { CSSProperties } from "react";
|
||||
import {
|
||||
ChartDocument,
|
||||
ClassConfigs,
|
||||
Classes,
|
||||
ConfScoreMetrics,
|
||||
Difficulties,
|
||||
ExtractedClasses,
|
||||
GPTString,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
import { ExtractEnumMetricNames, GetEnumValue } from "tachi-common/types/metrics";
|
||||
|
||||
export type GPTEnumColours<GPT extends GPTString> = {
|
||||
// @ts-expect-error this is fine please do not worry
|
||||
[M in ExtractEnumMetricNames<ConfScoreMetrics[GPT]>]: Record<GetEnumValue<GPT, M>, string>;
|
||||
};
|
||||
|
||||
export type GPTDifficultyColours<GPT extends GPTString> = Record<Difficulties[GPT], string>;
|
||||
|
||||
/**
|
||||
* Every GPT has to have some sort of "rating system" for charts defined.
|
||||
* The UI uses this to handle things like sorting on the difficulty cell.
|
||||
*/
|
||||
export type GPTRatingSystem<GPT extends GPTString> = {
|
||||
name: string;
|
||||
description: string;
|
||||
toNumber: (c: ChartDocument<GPT>) => number | null | undefined;
|
||||
toString: (c: ChartDocument<GPT>) => string | null | undefined;
|
||||
|
||||
/**
|
||||
* Does this rating system say this chart has strong individual differences
|
||||
* between players?
|
||||
*/
|
||||
idvDifference: (c: ChartDocument<GPT>) => boolean | null | undefined;
|
||||
};
|
||||
|
||||
export type GPTClassColours<GPT extends GPTString> = {
|
||||
[C in keyof ClassConfigs[GPT]]: {
|
||||
// @ts-expect-error it's kinda cool how TS lets me just uhh
|
||||
// ignore, errors
|
||||
// in the abhorrent stringly based typesystem i have created.
|
||||
// Whatever! This works.
|
||||
|
||||
// string => bootstrap variant
|
||||
// css properties => css
|
||||
// null -> no styling
|
||||
[V in ClassConfigs[GPT][C]["values"][number]["id"]]:
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "secondary"
|
||||
| "primary"
|
||||
| "info"
|
||||
| "success"
|
||||
| null
|
||||
| CSSProperties;
|
||||
};
|
||||
};
|
||||
|
||||
export interface GPTClientImplementation<GPT extends GPTString = GPTString> {
|
||||
enumColours: GPTEnumColours<GPT>;
|
||||
difficultyColours: GPTDifficultyColours<GPT>;
|
||||
classColours: GPTClassColours<GPT>;
|
||||
|
||||
/**
|
||||
* Other than chart.level and chart.levelNum, what other rating systems exist
|
||||
* for this game?
|
||||
*
|
||||
* You can use this to add things like tierlists (chart.data.ncTier, hcTier, etc.)
|
||||
*/
|
||||
ratingSystems: Array<GPTRatingSystem<GPT>>;
|
||||
|
||||
/**
|
||||
* What headers should be used when rendering scores in a table for this game?
|
||||
*/
|
||||
scoreHeaders: Array<Header<ScoreDocument<GPT> | PBScoreDocument<GPT>>>;
|
||||
}
|
||||
@@ -7,13 +7,11 @@ import {
|
||||
GamePTConfig,
|
||||
GoalDocument,
|
||||
GoalSubscriptionDocument,
|
||||
Grades,
|
||||
GPTString,
|
||||
GPTStringToGame,
|
||||
ImportDocument,
|
||||
ImportTrackerFailed,
|
||||
integer,
|
||||
Lamps,
|
||||
PBScoreDocument,
|
||||
UserDocument,
|
||||
ScoreDocument,
|
||||
@@ -22,13 +20,13 @@ import {
|
||||
ShowcaseStatFolder,
|
||||
SongDocument,
|
||||
TableDocument,
|
||||
ProfileRatingLookup,
|
||||
UserGameStats,
|
||||
UserGameStatsSnapshotDocument,
|
||||
QuestDocument,
|
||||
QuestSubscriptionDocument,
|
||||
QuestlineDocument,
|
||||
SessionScoreInfo,
|
||||
ProfileRatingAlgorithms,
|
||||
} from "tachi-common";
|
||||
|
||||
export interface UGPTStatsReturn<GPT extends GPTString = GPTString> {
|
||||
@@ -37,7 +35,7 @@ export interface UGPTStatsReturn<GPT extends GPTString = GPTString> {
|
||||
mostRecentScore: ScoreDocument<GPT>;
|
||||
totalScores: number;
|
||||
rankingData: Record<
|
||||
ProfileRatingLookup[GPT],
|
||||
ProfileRatingAlgorithms[GPT],
|
||||
{
|
||||
ranking: integer;
|
||||
outOf: integer;
|
||||
@@ -93,8 +91,8 @@ export interface UGPTChartPBComposition<GPT extends GPTString = GPTString> {
|
||||
pb: PBScoreDocument<GPT>;
|
||||
}
|
||||
|
||||
export type UGSWithRankingData<GPT extends GPTString = GPTString> = UserGameStats<GPT> & {
|
||||
__rankingData: Record<ProfileRatingLookup[GPT], { outOf: number; ranking: number }>;
|
||||
export type UGSWithRankingData<GPT extends GPTString = GPTString> = UserGameStats & {
|
||||
__rankingData: Record<ProfileRatingAlgorithms[GPT], { outOf: number; ranking: number }>;
|
||||
};
|
||||
|
||||
export interface SongChartsSearch<GPT extends GPTString = GPTString> {
|
||||
@@ -102,9 +100,8 @@ export interface SongChartsSearch<GPT extends GPTString = GPTString> {
|
||||
charts: ChartDocument<GPT>[];
|
||||
}
|
||||
|
||||
export interface FolderStatsInfo<GPT extends GPTString = GPTString> {
|
||||
grades: Record<Grades[GPT], integer>;
|
||||
lamps: Record<Lamps[GPT], integer>;
|
||||
export interface FolderStatsInfo {
|
||||
stats: Record<string, Record<string, integer>>;
|
||||
folderID: string;
|
||||
chartCount: integer;
|
||||
}
|
||||
@@ -169,9 +166,9 @@ export interface ScoreLeaderboardReturns<GPT extends GPTString = GPTString> {
|
||||
pbs: PBScoreDocument<GPT>[];
|
||||
}
|
||||
|
||||
export interface UserLeaderboardReturns<GPT extends GPTString = GPTString> {
|
||||
export interface UserLeaderboardReturns {
|
||||
users: UserDocument[];
|
||||
gameStats: UserGameStats<GPT>[];
|
||||
gameStats: UserGameStats[];
|
||||
}
|
||||
|
||||
export interface UserRecentSummary {
|
||||
@@ -292,13 +289,6 @@ export type SessionFolderRaises = {
|
||||
previousCount: integer; // how many AAAs/HARD CLEARs/whatevers was on this
|
||||
// folder before this session?
|
||||
totalCharts: integer;
|
||||
} & (
|
||||
| {
|
||||
type: "grade";
|
||||
value: Grades[GPTString];
|
||||
}
|
||||
| {
|
||||
type: "lamp";
|
||||
value: Lamps[GPTString];
|
||||
}
|
||||
);
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
Game,
|
||||
GameToGPTString,
|
||||
GPTStrings,
|
||||
GoalDocument,
|
||||
GoalDocumentFolder,
|
||||
GoalDocumentMulti,
|
||||
@@ -70,7 +70,7 @@ type SongSeedsWithRelated = {
|
||||
};
|
||||
|
||||
type ChartSeedsWithRelated = {
|
||||
[G in Game as `charts-${G}.json`]: Array<ChartWithRelated<GameToGPTString[G]>>;
|
||||
[G in Game as `charts-${G}.json`]: Array<ChartWithRelated<GPTStrings[G]>>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,7 @@ export type ChartLeaderboardDataset<GPT extends GPTString = GPTString> = (PBScor
|
||||
};
|
||||
})[];
|
||||
|
||||
export type UGSDataset<GPT extends GPTString = GPTString> = (UserGameStats<GPT> & {
|
||||
export type UGSDataset = (UserGameStats & {
|
||||
__related: {
|
||||
user: UserDocument;
|
||||
index: integer;
|
||||
|
||||
+13
-1
@@ -13,7 +13,19 @@ import {
|
||||
import { GoalsOnChartReturn, GoalsOnFolderReturn } from "types/api-returns";
|
||||
|
||||
export function GetPBs(scoreInfo: SessionScoreInfo[]) {
|
||||
return scoreInfo.filter((e) => e.isNewScore === true || e.lampDelta > 0 || e.scoreDelta > 0);
|
||||
return scoreInfo.filter((e) => {
|
||||
if (e.isNewScore) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const v of Object.values(e.deltas)) {
|
||||
if (v >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export function CreateSongMap<G extends Game = Game>(songs: SongDocument<G>[]) {
|
||||
|
||||
+18
-79
@@ -3,20 +3,16 @@ import toast from "react-hot-toast";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
APIPermissions,
|
||||
AnyProfileRatingAlg,
|
||||
AnySessionRatingAlg,
|
||||
ChartDocument,
|
||||
Game,
|
||||
GamePTConfig,
|
||||
GetGamePTConfig,
|
||||
Grades,
|
||||
GPTString,
|
||||
Playtype,
|
||||
ProfileRatingLookup,
|
||||
QuestDocument,
|
||||
QuestSubscriptionDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
ScoreDocument,
|
||||
SessionCalculatedDataLookup,
|
||||
SessionDocument,
|
||||
integer,
|
||||
} from "tachi-common";
|
||||
|
||||
@@ -56,29 +52,10 @@ export function FormatTables(tables: { table: string; level: string }[]) {
|
||||
return tables.map((e) => `${e.table}${e.level}`).join(", ");
|
||||
}
|
||||
|
||||
export function FormatGPTRating(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
key: ScoreCalculatedDataLookup[GPTString],
|
||||
value: number | null
|
||||
) {
|
||||
if (value === null) {
|
||||
return "No Data.";
|
||||
}
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (gptConfig.scoreRatingAlgFormatters[key]) {
|
||||
return gptConfig.scoreRatingAlgFormatters[key]!(value);
|
||||
}
|
||||
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
export function FormatGPTProfileRating(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
key: ProfileRatingLookup[GPTString],
|
||||
key: AnyProfileRatingAlg,
|
||||
value: number | null
|
||||
) {
|
||||
if (value === null) {
|
||||
@@ -87,8 +64,8 @@ export function FormatGPTProfileRating(
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (gptConfig.profileRatingAlgFormatters[key]) {
|
||||
return gptConfig.profileRatingAlgFormatters[key]!(value);
|
||||
if (gptConfig.profileRatingAlgs[key].formatter) {
|
||||
return gptConfig.profileRatingAlgs[key].formatter!(value);
|
||||
}
|
||||
|
||||
return value.toFixed(2);
|
||||
@@ -97,7 +74,7 @@ export function FormatGPTProfileRating(
|
||||
export function FormatGPTSessionRating(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
key: SessionCalculatedDataLookup[GPTString],
|
||||
key: AnySessionRatingAlg,
|
||||
value: number | null | undefined
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
@@ -106,8 +83,8 @@ export function FormatGPTSessionRating(
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (gptConfig.sessionRatingAlgFormatters[key]) {
|
||||
return gptConfig.sessionRatingAlgFormatters[key]!(value);
|
||||
if (gptConfig.sessionRatingAlgs[key].formatter) {
|
||||
return gptConfig.sessionRatingAlgs[key].formatter!(value);
|
||||
}
|
||||
|
||||
return value.toFixed(2);
|
||||
@@ -191,27 +168,14 @@ export function SelectRightChart(
|
||||
chartID: string,
|
||||
charts: ChartDocument[]
|
||||
) {
|
||||
if (gptConfig.difficulties.includes(chartID as any)) {
|
||||
for (const chart of charts) {
|
||||
if (chart.difficulty === chartID && chart.isPrimary) {
|
||||
return chart;
|
||||
}
|
||||
// try matching difficulty names, if it's not any of those, it's probably a chartID.
|
||||
for (const chart of charts) {
|
||||
if (chart.difficulty === chartID && chart.isPrimary) {
|
||||
return chart;
|
||||
}
|
||||
} else if (gptConfig.gptString === "itg:Stamina") {
|
||||
for (const chart of charts) {
|
||||
if (
|
||||
(chart as ChartDocument<"itg:Stamina">).data.hashGSV3 === chartID &&
|
||||
chart.isPrimary
|
||||
) {
|
||||
return chart;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// else, its a chart ID.
|
||||
for (const chart of charts) {
|
||||
if (chartID === chart.chartID) {
|
||||
return chart;
|
||||
}
|
||||
|
||||
if (chartID === chart.chartID) {
|
||||
return chart;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +251,7 @@ export function FormatScoreRating(
|
||||
return "No Data.";
|
||||
}
|
||||
|
||||
const formatter = GetGamePTConfig(game, playtype).scoreRatingAlgFormatters[rating];
|
||||
const formatter = GetGamePTConfig(game, playtype).scoreRatingAlgs[rating].formatter;
|
||||
|
||||
if (!formatter) {
|
||||
return value.toFixed(2);
|
||||
@@ -299,14 +263,14 @@ export function FormatScoreRating(
|
||||
export function FormatSessionRating(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
rating: keyof SessionDocument["calculatedData"],
|
||||
rating: AnySessionRatingAlg,
|
||||
value: number | null | undefined
|
||||
) {
|
||||
if (value === null || value === undefined) {
|
||||
return "No Data.";
|
||||
}
|
||||
|
||||
const formatter = GetGamePTConfig(game, playtype).sessionRatingAlgFormatters[rating];
|
||||
const formatter = GetGamePTConfig(game, playtype).sessionRatingAlgs[rating].formatter;
|
||||
|
||||
if (!formatter) {
|
||||
return value.toFixed(2);
|
||||
@@ -315,31 +279,6 @@ export function FormatSessionRating(
|
||||
return formatter(value);
|
||||
}
|
||||
|
||||
export function GetGradeFromPercent<GPT extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
percent: number
|
||||
): Grades[GPT] {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const boundaries = gptConfig.gradeBoundaries;
|
||||
const grades = gptConfig.grades;
|
||||
|
||||
if (!boundaries) {
|
||||
throw new Error(
|
||||
`Invalid call to GetGradeFromPercent! GPT ${game}:${playtype} does not use grade boundaries.`
|
||||
);
|
||||
}
|
||||
|
||||
// (hey, this for loop is backwards!)
|
||||
for (let i = boundaries.length; i >= 0; i--) {
|
||||
if (percent >= boundaries[i]) {
|
||||
return grades[i] as Grades[GPT];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Could not resolve grade for percent ${percent} on game ${game}`);
|
||||
}
|
||||
|
||||
export function ConditionalLeadingSpace(maybeStr: string | null) {
|
||||
if (maybeStr === null) {
|
||||
return "";
|
||||
|
||||
@@ -1,46 +1,32 @@
|
||||
import { Game, GetGamePTConfig, Playtypes } from "tachi-common";
|
||||
import { Game, GetGamePTConfig, GetScoreMetricConf, Playtypes } from "tachi-common";
|
||||
|
||||
export function HumanFriendlyStrToGradeIndex(game: Game, playtype: Playtypes[Game]) {
|
||||
export function HumanFriendlyStrToEnumIndex(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
enumMetric: string
|
||||
) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const lowerGrades = gptConfig.grades.map((e) => e.toLowerCase());
|
||||
const conf = GetScoreMetricConf(gptConfig, enumMetric);
|
||||
|
||||
if (conf.type !== "ENUM") {
|
||||
return () => 0; // wut
|
||||
}
|
||||
|
||||
const lowerValues = conf.values.map((e) => e.toLowerCase());
|
||||
|
||||
return (str: string) => {
|
||||
const lowerStr = str.toLowerCase();
|
||||
let partialMatch: number | null = null;
|
||||
|
||||
for (let i = 0; i < gptConfig.grades.length; i++) {
|
||||
const grade = lowerGrades[i];
|
||||
for (let i = 0; i < conf.values.length; i++) {
|
||||
const value = lowerValues[i];
|
||||
|
||||
if (grade === lowerStr) {
|
||||
if (value === lowerStr) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (grade.startsWith(lowerStr) && partialMatch === null) {
|
||||
partialMatch = i;
|
||||
}
|
||||
}
|
||||
|
||||
return partialMatch;
|
||||
};
|
||||
}
|
||||
|
||||
export function HumanFriendlyStrToLampIndex(game: Game, playtype: Playtypes[Game]) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
const lowerLamps = gptConfig.lamps.map((e) => e.toLowerCase());
|
||||
|
||||
return (str: string) => {
|
||||
const lowerStr = str.toLowerCase();
|
||||
let partialMatch: number | null = null;
|
||||
|
||||
for (let i = 0; i < gptConfig.lamps.length; i++) {
|
||||
const lamp = lowerLamps[i];
|
||||
|
||||
if (lamp === lowerStr) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (lamp.startsWith(lowerStr) && partialMatch === null) {
|
||||
if (value.startsWith(lowerStr) && partialMatch === null) {
|
||||
partialMatch = i;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user