checkpoint: insanity ascends (35)

127 errors to go
This commit is contained in:
zkldi
2023-01-19 16:39:42 +00:00
parent 2b0e65f0c8
commit 5cbb2d5f8a
131 changed files with 2106 additions and 1886 deletions
+2 -1
View File
@@ -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",
@@ -125,8 +125,6 @@ function FolderCompare({
withUser: UserDocument;
folder: FolderDocument;
} & GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const { data: baseData, error: baseError } = useApiQuery<UGPTFolderReturns>(
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folder.folderID}`
);
@@ -136,11 +134,6 @@ function FolderCompare({
);
const [shouldIncludeNotPlayed, setShouldIncludeNotPlayed] = useState(false);
const [shouldESD, setShouldESD] = useState(gptConfig.supportsESD);
useEffect(() => {
setShouldESD(gptConfig.supportsESD);
}, [gptConfig]);
const dataset = useMemo(() => {
// i *LOVE* the rules of hooks! they're so convenient!
@@ -197,13 +190,6 @@ function FolderCompare({
onChange={() => setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)}
label="Include charts without plays?"
/>
{gptConfig.supportsESD && (
<Form.Check
checked={shouldESD}
onChange={() => setShouldESD(!shouldESD)}
label="Sort score differences intelligently? (i.e. Treat the difference between 70% and 71% as smaller than the difference between 99% and 100%)"
/>
)}
<Divider />
</Col>
<ComparePBsTable
@@ -211,7 +197,6 @@ function FolderCompare({
compareUser={withUser.username}
dataset={dataset}
game={game}
shouldESD={shouldESD}
playtype={playtype}
/>
</Row>
@@ -83,9 +83,9 @@ export function FolderInfoComponent({
}: UGPT & { folder: FolderDocument; folderStats: FolderStatsInfo }) {
const gptConfig = GetGamePTConfig(game, playtype);
const scoreBucket = useBucket(game, playtype);
const preferredDefaultEnum = useBucket(game, playtype);
const [elements, setElements] = useState<"grade" | "lamp">(scoreBucket);
const [elements, setElements] = useState<"grade" | "lamp">(preferredDefaultEnum);
const base = useUGPTBase({ reqUser, game, playtype });
@@ -1,47 +1,35 @@
import { ONE_DAY } from "util/constants/time";
import { CreateChartIDMap, CreateChartLink, CreateSongMap } from "util/data";
import { DistinctArr, NO_OP } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { GetScaleAchievedFn } from "util/tierlist";
import { FormatDate, FormatTime } from "util/time";
import { ChangeOpacity } from "util/color-opacity";
import FolderInfoHeader from "components/game/folder/FolderInfoHeader";
import QuickTooltip from "components/layout/misc/QuickTooltip";
import Card from "components/layout/page/Card";
import FolderTable from "components/tables/folders/FolderTable";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import Muted from "components/util/Muted";
import useApiQuery from "components/util/query/useApiQuery";
import ReferToUser from "components/util/ReferToUser";
import SelectLinkButton from "components/util/SelectLinkButton";
import useApiQuery from "components/util/query/useApiQuery";
import useUGPTBase from "components/util/useUGPTBase";
import { useFormik } from "formik";
import React, { useEffect, useMemo, useState } from "react";
import { Col, Form, Row } from "react-bootstrap";
import { Form } from "react-bootstrap";
import { Link, Route, Switch, useParams } from "react-router-dom";
import {
ChartDocument,
ChartTierlistInfo,
FormatDifficulty,
FormatDifficultyShort,
Game,
GetGamePTConfig,
GPTTierlists,
IDStrings,
integer,
UserDocument,
GetScoreEnumConfs,
Playtype,
ScoreDocument,
SongDocument,
Playtype,
COLOUR_SET,
UserDocument,
integer,
} from "tachi-common";
import { UGPTFolderReturns } from "types/api-returns";
import { FolderDataset } from "types/tables";
import MiniTable from "components/tables/components/MiniTable";
import DifficultyCell from "components/tables/cells/DifficultyCell";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import FolderComparePage from "./FolderComparePage";
import FolderQuestsPage from "./FolderQuestsPage";
@@ -58,8 +46,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}`
);
const gptConfig = GetGamePTConfig(game, playtype);
const folderDataset = useMemo(() => {
if (!data) {
return null;
@@ -124,15 +110,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
<Icon type="table" />
Normal View
</SelectLinkButton>
{gptConfig.tierlists.length !== 0 &&
// temp: tierlist view sucks for BMS and PMS
game !== "bms" &&
game !== "pms" && (
<SelectLinkButton to={`${base}/tierlist`}>
<Icon type="sort-alpha-up" />
Tierlist View
</SelectLinkButton>
)}
<SelectLinkButton to={`${base}/timeline`}>
<Icon type="stream" />
Timeline View
@@ -155,15 +132,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
<Route exact path={base}>
<FolderTable dataset={folderDataset} game={game} playtype={playtype} />
</Route>
<Route exact path={`${base}/tierlist`}>
<TierlistBreakdown
folderDataset={folderDataset}
game={game}
playtype={playtype}
reqUser={reqUser}
data={data}
/>
</Route>
<Route exact path={`${base}/timeline`}>
<TimelineView
game={game}
@@ -196,20 +164,17 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID: string }) {
const gptConfig = GetGamePTConfig(game, playtype);
const [type, setType] = useState<"lamp" | "grade">(gptConfig.scoreBucket);
const [value, setValue] = useState<integer>(
type === "grade"
? gptConfig.grades.indexOf(gptConfig.clearGrade)
: gptConfig.lamps.indexOf(gptConfig.clearLamp)
);
const enumConfs = GetScoreEnumConfs(gptConfig);
const [selectedEnum, setSelectedEnum] = useState<string>(gptConfig.preferredDefaultEnum);
const [enumConf, setEnumConf] = useState<ConfEnumScoreMetric<string>>(enumConfs[selectedEnum]!);
const [value, setValue] = useState<string>(enumConf.minimumRelevantValue);
useEffect(() => {
setValue(
type === "grade"
? gptConfig.grades.indexOf(gptConfig.clearGrade)
: gptConfig.lamps.indexOf(gptConfig.clearLamp)
);
}, [type]);
setValue(enumConfs[selectedEnum]!.minimumRelevantValue);
setEnumConf(enumConfs[selectedEnum]!);
}, [selectedEnum]);
return (
<>
@@ -225,36 +190,33 @@ function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID:
<div className="col-12 col-lg-6">
<Form.Control
as="select"
value={type}
onChange={(e) => setType(e.target.value as "grade" | "lamp")}
value={selectedEnum}
onChange={(e) => setSelectedEnum(e.target.value)}
>
<option value="grade">Grades</option>
<option value="lamp">Lamps</option>
{Object.keys(enumConfs).map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
</div>
<div className="col-12 col-lg-6">
<Form.Control
as="select"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
onChange={(e) => setValue(e.target.value)}
>
{type === "grade"
? gptConfig.grades.map((e, i) => (
<option key={e} value={i}>
{e}
</option>
))
: gptConfig.lamps.map((e, i) => (
<option key={e} value={i}>
{e}
</option>
))}
{enumConf.values
.slice(enumConf.values.indexOf(enumConf.minimumRelevantValue))
.map((e) => (
<option key={e}>{e}</option>
))}
</Form.Control>
</div>
</div>
</Card>
<Divider />
<TimelineMain {...{ reqUser, game, playtype, folderID, type, value }} />
<TimelineMain
{...{ reqUser, game, playtype, folderID, enumMetric: selectedEnum, value }}
/>
</>
);
}
@@ -264,19 +226,19 @@ function TimelineMain({
game,
playtype,
folderID,
type,
enumMetric: enumMetric,
value,
}: Props & {
folderID: string;
type: "grade" | "lamp";
value: integer;
enumMetric: string;
value: string;
}) {
const { data, error } = useApiQuery<{
scores: ScoreDocument[];
songs: SongDocument[];
charts: ChartDocument[];
}>(
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${type}`
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}/timeline?criteriaValue=${value}&criteriaType=${enumMetric}`
);
if (error) {
@@ -411,420 +373,3 @@ function TimelineElement({
</div>
);
}
type InfoProps = Props & {
folderDataset: FolderDataset;
data: UGPTFolderReturns;
};
function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps) {
const gptConfig = GetGamePTConfig(game, playtype);
const formik = useFormik({
initialValues: {
__hideAchieved: false,
...Object.fromEntries(gptConfig.tierlists.map((e) => [e, true])),
} as { __hideAchieved: boolean } & Partial<Record<GPTTierlists[IDStrings], true>>,
onSubmit: NO_OP,
});
const tierlistInfo = useMemo(
() => FolderDatasetToTierlistInfo(folderDataset, game, playtype, formik.values),
[formik.values]
);
const dataMap = CreateChartIDMap(folderDataset);
return (
<Row>
<Col xs={12}>
<Card header="Tierlist View Configuration">
<span>Here you can select what tierlist blocks to show!</span>
<Divider />
{gptConfig.tierlists.length > 1 &&
gptConfig.tierlists.map((e) => (
<>
<Form.Check
key={e}
type="checkbox"
id={e}
checked={formik.values[e]}
onChange={formik.handleChange}
label={e}
/>
<Form.Text>{gptConfig.tierlistDescriptions[e]}</Form.Text>
</>
))}
<Form.Check
type="checkbox"
id="__hideAchieved"
checked={formik.values.__hideAchieved}
onChange={formik.handleChange}
label="Hide Achieved"
/>
<Form.Text>Hide achieved elements of the tierlist.</Form.Text>
</Card>
</Col>
<Col xs={12}>
<Divider />
</Col>
<Col xs={12}>
<TierlistInfoLadder
tierlistInfo={tierlistInfo}
dataMap={dataMap}
game={game}
playtype={playtype}
reqUser={reqUser}
/>
</Col>
</Row>
);
}
const NO_TIERLIST_DATA_VALUE = {
text: "No Tierlist Info",
value: 0,
};
function TierlistInfoLadder({
tierlistInfo,
dataMap,
game,
playtype,
reqUser,
}: {
tierlistInfo: NullableTierlistInfo[];
dataMap: Map<string, FolderDataset[0]>;
game: Game;
playtype: Playtype;
reqUser: UserDocument;
}) {
const buckets: TierlistInfo[][] = useMemo(() => {
const buckets: TierlistInfo[][] = [];
let currentBucket: TierlistInfo[] = [];
const noDataBucket: TierlistInfo[] = [];
let lastValue;
for (const tl of tierlistInfo) {
if (tl.data === null) {
noDataBucket.push({
achieved: tl.achieved,
chartID: tl.chartID,
data: NO_TIERLIST_DATA_VALUE,
key: tl.key,
});
continue;
}
// no longer nullable
const tlx = tl as TierlistInfo;
if (lastValue && tlx.data.value !== lastValue) {
buckets.push(currentBucket);
currentBucket = [tlx];
} else {
currentBucket.push(tlx);
}
lastValue = tlx.data.value;
}
if (currentBucket.length) {
buckets.push(currentBucket);
}
buckets.push(noDataBucket);
return buckets;
}, [tierlistInfo]);
const gptConfig = GetGamePTConfig(game, playtype);
for (const bucket of buckets) {
bucket.sort(NumericSOV((x) => gptConfig.tierlists.indexOf(x.key), true));
}
if (tierlistInfo.length === 0) {
return <Row className="justify-content-center">Got no tierlist data to show you!</Row>;
}
return (
<Row className="text-center">
{buckets
.filter((e) => e.length > 0)
.map((bucket, i) => (
<React.Fragment key={i}>
<Col className="ladder-header" xs={12}>
{bucket[0].data!.value} (
{DistinctArr(
bucket.map((e) =>
gptConfig.tierlists.length === 1
? e.data.text
: `${e.key}: ${e.data.text}`
)
).join(", ")}
)
</Col>
<TierlistBucket {...{ bucket, dataMap, game, playtype, reqUser }} />
</React.Fragment>
))}
</Row>
);
}
function TierlistBucket({
bucket,
dataMap,
game,
reqUser,
}: {
dataMap: Map<string, FolderDataset[0]>;
game: Game;
playtype: Playtype;
reqUser: UserDocument;
bucket: TierlistInfo[];
}) {
// xs view is tabular
if (window.screen.width <= 576) {
return (
<MiniTable>
{bucket.map((tierlistInfo, i) => (
<TierlistInfoBucketValues
tierlistInfo={tierlistInfo}
key={`${tierlistInfo.chartID}-${tierlistInfo.key}`}
game={game}
dataMap={dataMap}
bucket={bucket}
i={i}
reqUser={reqUser}
/>
))}
</MiniTable>
);
}
return (
<>
{bucket.map((tierlistInfo, i) => (
<TierlistInfoBucketValues
tierlistInfo={tierlistInfo}
key={`${tierlistInfo.chartID}-${tierlistInfo.key}`}
game={game}
dataMap={dataMap}
bucket={bucket}
i={i}
reqUser={reqUser}
/>
))}
</>
);
}
function TierlistInfoBucketValues({
tierlistInfo,
game,
dataMap,
bucket,
i,
reqUser,
}: {
tierlistInfo: TierlistInfo;
bucket: TierlistInfo[];
dataMap: Map<string, FolderDataset[0]>;
game: Game;
i: integer;
reqUser: UserDocument;
}) {
const data = dataMap.get(tierlistInfo.chartID)!;
const lastKey = bucket[i - 1];
let statusClass;
switch (tierlistInfo.achieved) {
case AchievedStatuses.ACHIEVED:
statusClass = "achieved";
break;
case AchievedStatuses.FAILED:
statusClass = "unachieved";
break;
case AchievedStatuses.NOT_PLAYED:
case AchievedStatuses.SCORE_BASED:
statusClass = "";
}
// xs view
if (window.screen.width <= 576) {
return (
<tr>
<DifficultyCell game={game} chart={data} alwaysShort noTierlist />
<td className="text-left">
<Link className="gentle-link" to={CreateChartLink(data, game)}>
{data.__related.song.title}
</Link>{" "}
<br />
<div>
{tierlistInfo.key} ({tierlistInfo.data.text})
{tierlistInfo.data.individualDifference && (
<span className="ml-1">
<Icon type="balance-scale-left" />
</span>
)}
</div>
</td>
<TierlistInfoCell tierlistInfo={tierlistInfo} />
</tr>
);
}
return (
<>
{lastKey && lastKey.key !== tierlistInfo.key && <Col xl={12} className="my-2" />}
<Col
className={`ladder-element ${
i % 12 < 6 ? "ladder-element-dark" : ""
} ladder-element-${statusClass} d-none d-sm-block`}
xs={12}
sm={6}
md={4}
lg={3}
xl={2}
>
<Link className="gentle-link" to={CreateChartLink(data, game)}>
{data.__related.song.title}
</Link>{" "}
{FormatDifficultyShort(data, game)}
<Divider className="my-2" />
{tierlistInfo.key} ({tierlistInfo.data.text})
{tierlistInfo.data.individualDifference && (
<>
<br />
<div className="mt-1">
<QuickTooltip tooltipContent="Individual Difference - The difficulty of this varies massively between people!">
<span>
<Icon type="balance-scale-left" />
</span>
</QuickTooltip>
</div>
</>
)}
<Divider className="my-2" />
<Muted>
<ReferToUser reqUser={reqUser} />{" "}
{tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED
? "Not Played"
: tierlistInfo.achieved === AchievedStatuses.SCORE_BASED
? data.__related.pb!.scoreData.grade
: data.__related.pb!.scoreData.lamp}
</Muted>
</Col>
</>
);
}
function TierlistInfoCell({ tierlistInfo }: { tierlistInfo: TierlistInfo }) {
let colour;
let text;
if (
tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED ||
tierlistInfo.achieved === AchievedStatuses.FAILED
) {
colour = COLOUR_SET.red;
text = "✗";
} else {
colour = COLOUR_SET.green;
text = "✓";
}
return (
<td
style={{
backgroundColor: ChangeOpacity(colour, 0.2),
width: "60px",
minWidth: "60px",
maxWidth: "60px",
}}
>
{text}
</td>
);
}
interface NullableTierlistInfo {
chartID: string;
key: GPTTierlists[IDStrings];
data: ChartTierlistInfo | null;
achieved: AchievedStatuses;
}
interface TierlistInfo {
chartID: string;
key: GPTTierlists[IDStrings];
data: ChartTierlistInfo;
achieved: AchievedStatuses;
}
enum AchievedStatuses {
NOT_PLAYED,
FAILED,
ACHIEVED,
SCORE_BASED,
}
function FolderDatasetToTierlistInfo(
folderDataset: FolderDataset,
game: Game,
playtype: Playtype,
options: Partial<Record<GPTTierlists[IDStrings], boolean>> & { __hideAchieved: boolean }
) {
const tierlistInfo: NullableTierlistInfo[] = [];
const tierlistKeys: GPTTierlists[IDStrings][] = [];
for (const k in options) {
if (k === "__hideAchieved") {
continue;
}
const key = k as GPTTierlists[IDStrings];
if (options[key]) {
tierlistKeys.push(key);
}
}
for (const data of folderDataset) {
for (const key of tierlistKeys) {
let achieved: AchievedStatuses;
if (!data.__related.pb) {
achieved = AchievedStatuses.NOT_PLAYED;
} else {
const fn = GetScaleAchievedFn(game, playtype, key);
if (fn) {
achieved = fn(data.__related.pb)
? AchievedStatuses.ACHIEVED
: AchievedStatuses.FAILED;
} else {
achieved = AchievedStatuses.SCORE_BASED;
}
}
if (options.__hideAchieved && achieved === AchievedStatuses.ACHIEVED) {
continue;
}
tierlistInfo.push({
chartID: data.chartID,
key,
data: data.tierlistInfo[key] ?? null,
achieved,
});
}
}
return tierlistInfo.sort(NumericSOV((x) => (x.data ? x.data.value : -Infinity), true));
}
+22 -10
View File
@@ -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;
+31 -17
View File
@@ -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;
+3 -3
View File
@@ -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>
+15 -14
View File
@@ -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>
+18 -7
View File
@@ -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;
}) {
+8 -8
View File
@@ -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(
+18 -41
View File
@@ -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)} &gt;={" "}
{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
+3 -3
View File
@@ -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];
}
+5 -5
View File
@@ -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);
+414
View File
@@ -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]];
}
+27
View File
@@ -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 };
}
+58
View File
@@ -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;
+86
View File
@@ -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,
};
+123
View File
@@ -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,
};
+71
View File
@@ -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)],
],
};
+79
View File
@@ -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>>>;
}
+11 -21
View File
@@ -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;
};
+2 -2
View File
@@ -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]>>;
};
/**
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 "";
+17 -31
View File
@@ -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