mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-05 21:38:06 +03:00
These updates suck
This commit is contained in:
+1
-1
@@ -75,7 +75,7 @@
|
||||
"resolve": "1.12.0",
|
||||
"resolve-url-loader": "3.1.0",
|
||||
"socicon": "3.0.5",
|
||||
"tachi-common": "0.2.12"
|
||||
"tachi-common": "0.2.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/humanize-duration": "3.25.1",
|
||||
|
||||
Generated
+4
-4
@@ -77,7 +77,7 @@ specifiers:
|
||||
serve: 11.2.0
|
||||
socicon: 3.0.5
|
||||
style-loader: 1.0.0
|
||||
tachi-common: 0.2.12
|
||||
tachi-common: 0.2.15
|
||||
terser-webpack-plugin: 1.4.1
|
||||
ts-pnp: 1.1.4
|
||||
typescript: 4.3.5
|
||||
@@ -142,7 +142,7 @@ dependencies:
|
||||
resolve: 1.12.0
|
||||
resolve-url-loader: 3.1.0
|
||||
socicon: 3.0.5
|
||||
tachi-common: 0.2.12
|
||||
tachi-common: 0.2.15
|
||||
|
||||
devDependencies:
|
||||
'@types/humanize-duration': 3.25.1
|
||||
@@ -11178,8 +11178,8 @@ packages:
|
||||
strip-ansi: 6.0.0
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.2.12:
|
||||
resolution: {integrity: sha512-xwbQUZefLUs1sus3nrrys84qJyRMWLl/Mb4UzFYd74n0ZmOejrxpwIb18ILWww4J/IxQurPd4Id+DCP12SEW1w==}
|
||||
/tachi-common/0.2.15:
|
||||
resolution: {integrity: sha512-p7nt7pupl2JWNb20K7n18YOBtxUYeoyDsE6YbPVVyPvY1oj74a2YxALpmajGV2pbVAkdywrJS94In2sQUGzJtw==}
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
transitivePeerDependencies:
|
||||
|
||||
+400
-74
@@ -1,22 +1,38 @@
|
||||
import { ResponsiveBar } from "@nivo/bar";
|
||||
import { BarChartTooltip } from "components/charts/ChartTooltip";
|
||||
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 React, { useMemo } from "react";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import { useFormik } from "formik";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Col, Form, Row } from "react-bootstrap";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Game, GetGamePTConfig, PublicUserDocument } from "tachi-common";
|
||||
import {
|
||||
ChartTierlistInfo,
|
||||
FormatDifficultyShort,
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
GPTTierlists,
|
||||
IDStrings,
|
||||
PublicUserDocument,
|
||||
} from "tachi-common";
|
||||
import { UGPTFolderReturns } from "types/api-returns";
|
||||
import { FolderDataset } from "types/tables";
|
||||
import { Playtype } from "types/tachi";
|
||||
import { DEFAULT_BAR_PROPS } from "util/charts";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { CreateChartIDMap, CreateSongMap } from "util/data";
|
||||
import { ComposeExpFn, ComposeInverseExpFn } from "util/misc";
|
||||
import { ComposeExpFn, ComposeInverseExpFn, IsNullish, NO_OP } from "util/misc";
|
||||
import { GetGradeChartExpScale } from "util/scales";
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import { GetScaleAchievedFn } from "util/tierlist";
|
||||
|
||||
interface Props {
|
||||
reqUser: PublicUserDocument;
|
||||
@@ -31,6 +47,10 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
|
||||
`/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}`
|
||||
);
|
||||
|
||||
const [mode, setMode] = useState<"normal" | "ladder">("normal");
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const folderDataset = useMemo(() => {
|
||||
if (!data) {
|
||||
return null;
|
||||
@@ -56,6 +76,22 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
|
||||
return folderDataset;
|
||||
}, [data]);
|
||||
|
||||
const folderInfoHeader = useMemo(() => {
|
||||
if (!folderDataset || !data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FolderInfoHeader
|
||||
folderDataset={folderDataset}
|
||||
data={data}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
/>
|
||||
);
|
||||
}, [folderDataset]);
|
||||
|
||||
if (isLoading || !data || !folderDataset) {
|
||||
return <Loading />;
|
||||
}
|
||||
@@ -69,35 +105,326 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
|
||||
<div className="col-12">
|
||||
<Divider className="mb-4" />
|
||||
</div>
|
||||
<div className="col-12">{folderInfoHeader}</div>
|
||||
<div className="col-12">
|
||||
<FolderInfoHeader {...{ folderDataset, data, game, playtype, reqUser }} />
|
||||
<Divider className="my-4" />
|
||||
</div>
|
||||
<div className="col-12 d-flex">
|
||||
<div className="btn-group mx-auto">
|
||||
<SelectButton value={mode} setValue={setMode} id="normal">
|
||||
<Icon type="table" />
|
||||
Normal View
|
||||
</SelectButton>
|
||||
{gptConfig.tierlists.length && (
|
||||
<SelectButton value={mode} setValue={setMode} id="ladder">
|
||||
<Icon type="sort-alpha-up" />
|
||||
Tierlist View
|
||||
</SelectButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<Divider className="my-4" />
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<FolderTable
|
||||
dataset={folderDataset}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
indexCol={false}
|
||||
/>
|
||||
{mode === "normal" ? (
|
||||
<FolderTable
|
||||
dataset={folderDataset}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
indexCol={false}
|
||||
/>
|
||||
) : (
|
||||
<TierlistBreakdown
|
||||
folderDataset={folderDataset}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FolderInfoHeader({
|
||||
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 = FolderDatasetToTierlistInfo(folderDataset, game, playtype, 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 className="my-4" />
|
||||
{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 className="my-4" />
|
||||
</Col>
|
||||
<Col xs={12}>
|
||||
<TierlistInfoLadder
|
||||
tierlistInfo={tierlistInfo}
|
||||
dataMap={dataMap}
|
||||
game={game}
|
||||
playtype={playtype}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function TierlistInfoLadder({
|
||||
tierlistInfo,
|
||||
dataMap,
|
||||
game,
|
||||
playtype,
|
||||
reqUser,
|
||||
folderDataset,
|
||||
data,
|
||||
}: Props & { folderDataset: FolderDataset; data: UGPTFolderReturns }) {
|
||||
}: {
|
||||
tierlistInfo: TierlistInfo[];
|
||||
dataMap: Map<string, FolderDataset[0]>;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
const buckets: TierlistInfo[][] = useMemo(() => {
|
||||
const buckets: TierlistInfo[][] = [];
|
||||
let currentBucket: TierlistInfo[] = [];
|
||||
|
||||
let lastValue;
|
||||
for (const tl of tierlistInfo) {
|
||||
if (lastValue && tl.data.value !== lastValue) {
|
||||
buckets.push(currentBucket);
|
||||
currentBucket = [tl];
|
||||
} else {
|
||||
currentBucket.push(tl);
|
||||
}
|
||||
lastValue = tl.data.value;
|
||||
}
|
||||
|
||||
if (currentBucket.length) {
|
||||
buckets.push(currentBucket);
|
||||
}
|
||||
|
||||
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.map((bucket, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<Col className="ladder-header" xs={12}>
|
||||
{bucket[0].data.value}
|
||||
</Col>
|
||||
|
||||
{bucket.map((tierlistInfo, i) => {
|
||||
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 = "";
|
||||
}
|
||||
|
||||
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}`}
|
||||
key={tierlistInfo.chartID}
|
||||
xs={6}
|
||||
sm={6}
|
||||
md={4}
|
||||
lg={3}
|
||||
xl={2}
|
||||
>
|
||||
{data.__related.song.title} {FormatDifficultyShort(data, game)}
|
||||
<Divider className="my-2" />
|
||||
{tierlistInfo.key}{" "}
|
||||
{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>
|
||||
{tierlistInfo.achieved === AchievedStatuses.NOT_PLAYED
|
||||
? "Not Played"
|
||||
: tierlistInfo.achieved === AchievedStatuses.SCORE_BASED
|
||||
? `You have: ${data.__related.pb!.scoreData.grade}`
|
||||
: `You have: ${data.__related.pb!.scoreData.lamp}`}
|
||||
</Muted>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
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: TierlistInfo[] = [];
|
||||
|
||||
const tierlistKeys: GPTTierlists[IDStrings][] = [];
|
||||
|
||||
for (const k in options) {
|
||||
const key = k as GPTTierlists[IDStrings];
|
||||
if (options[key]) {
|
||||
tierlistKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const data of folderDataset) {
|
||||
for (const key of tierlistKeys) {
|
||||
if (IsNullish(data.tierlistInfo[key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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]!,
|
||||
achieved,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tierlistInfo.sort(NumericSOV(x => x.data.value, true));
|
||||
}
|
||||
|
||||
function FolderInfoHeader({ game, playtype, reqUser, folderDataset, data }: InfoProps) {
|
||||
return (
|
||||
<Card header={`${reqUser.username}'s ${data.folder.title} Breakdown`}>
|
||||
<ScoreDistributionChart
|
||||
game={game}
|
||||
data={data}
|
||||
folderDataset={folderDataset}
|
||||
playtype={playtype}
|
||||
reqUser={reqUser}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ScoreDistributionChart({ game, playtype, reqUser, folderDataset, data }: InfoProps) {
|
||||
const dataMap = CreateChartIDMap(folderDataset);
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const dataset = [];
|
||||
|
||||
const expScale = 1;
|
||||
const expScale = GetGradeChartExpScale(game);
|
||||
|
||||
const expFn = ComposeExpFn(expScale);
|
||||
const invExpFn = ComposeInverseExpFn(expScale);
|
||||
@@ -114,70 +441,69 @@ function FolderInfoHeader({
|
||||
|
||||
dataset.sort(NumericSOV(x => x.expValue));
|
||||
|
||||
const dataMap = CreateChartIDMap(folderDataset);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return (
|
||||
<Card header={`${reqUser.username}'s ${data.folder.title} Breakdown`}>
|
||||
<div style={{ height: 400 }}>
|
||||
<ResponsiveBar
|
||||
indexBy="chartID"
|
||||
tooltip={d => (
|
||||
<BarChartTooltip
|
||||
point={d}
|
||||
renderFn={d => {
|
||||
const data = dataMap.get(d.indexValue as string)!;
|
||||
<div style={{ height: 400 }}>
|
||||
<ResponsiveBar
|
||||
indexBy="chartID"
|
||||
tooltip={d => (
|
||||
<BarChartTooltip
|
||||
point={d}
|
||||
renderFn={d => {
|
||||
const data = dataMap.get(d.indexValue as string)!;
|
||||
|
||||
return (
|
||||
<div className="w-100 text-center">
|
||||
{data.__related.song.title}
|
||||
<br />
|
||||
{data.__related.pb?.scoreData.percent.toFixed(2)}%
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
key={"value"}
|
||||
return (
|
||||
<div className="w-100 text-center">
|
||||
{data.__related.song.title}
|
||||
<br />
|
||||
{data.__related.pb?.scoreData.percent.toFixed(2)}%
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
key={"value"}
|
||||
colors={k => {
|
||||
if (!k.data.grade) {
|
||||
return "black";
|
||||
}
|
||||
// @ts-expect-error temp
|
||||
return ChangeOpacity(gptConfig.gradeColours[k.data.grade], 0.5);
|
||||
}}
|
||||
// @ts-expect-error temp
|
||||
borderColor={k => gptConfig.gradeColours[k.data.grade]}
|
||||
borderWidth={1}
|
||||
padding={0.2}
|
||||
// @ts-expect-error temp
|
||||
data={dataset}
|
||||
minValue={0}
|
||||
maxValue={expFn(100)}
|
||||
margin={{ left: 50, top: 20, bottom: 20 }}
|
||||
valueFormat={e => `${invExpFn(e).toFixed(2)}%`}
|
||||
axisLeft={{
|
||||
tickValues: gptConfig.gradeBoundaries.map(e => (e === 0 ? 0 : expFn(e))),
|
||||
format: x => {
|
||||
let nearest;
|
||||
|
||||
colors={k => ChangeOpacity(gptConfig.gradeColours[k.data.grade], 0.5)}
|
||||
// @ts-expect-error temp
|
||||
const lgv = invExpFn(x);
|
||||
|
||||
borderColor={k => gptConfig.gradeColours[k.data.grade]}
|
||||
borderWidth={1}
|
||||
padding={0.2}
|
||||
// @ts-expect-error temp
|
||||
data={dataset}
|
||||
minValue={0}
|
||||
maxValue={expFn(100)}
|
||||
margin={{ left: 50, top: 20, bottom: 20 }}
|
||||
axisLeft={{
|
||||
tickValues: gptConfig.gradeBoundaries.map(e => (e === 0 ? 0 : expFn(e))),
|
||||
format: x => {
|
||||
let nearest;
|
||||
|
||||
const lgv = invExpFn(x);
|
||||
|
||||
for (const [i, gradeBnd] of gptConfig.gradeBoundaries.entries()) {
|
||||
if (Math.abs(gradeBnd - lgv) < 0.00005) {
|
||||
nearest = i;
|
||||
break;
|
||||
}
|
||||
for (const [i, gradeBnd] of gptConfig.gradeBoundaries.entries()) {
|
||||
if (Math.abs(gradeBnd - lgv) < 0.00005) {
|
||||
nearest = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearest === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (nearest === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return gptConfig.grades[nearest];
|
||||
},
|
||||
}}
|
||||
axisBottom={null}
|
||||
{...DEFAULT_BAR_PROPS}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
return gptConfig.grades[nearest];
|
||||
},
|
||||
}}
|
||||
axisBottom={null}
|
||||
{...DEFAULT_BAR_PROPS}
|
||||
labelSkipWidth={40}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,9 @@ function GPTSearchResult({
|
||||
playtype
|
||||
)} profile, if they have one.`}
|
||||
>
|
||||
<Icon type="exchange-alt" />
|
||||
<span>
|
||||
<Icon type="exchange-alt" />
|
||||
</span>
|
||||
</QuickTooltip>
|
||||
</Link>
|
||||
</strong>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function BMSDifficultyCell({
|
||||
}}
|
||||
>
|
||||
<span>{levelText}</span>
|
||||
<TierlistInfoPart chart={chart} />
|
||||
<TierlistInfoPart chart={chart} game="bms" />
|
||||
{!chart.isPrimary && (
|
||||
<QuickTooltip tooltipContent="This chart is an alternate, old chart.">
|
||||
<div>
|
||||
|
||||
@@ -30,16 +30,14 @@ export default function DifficultyCell({
|
||||
}}
|
||||
>
|
||||
{!alwaysShort && (
|
||||
<>
|
||||
<span className="d-none d-lg-block">{FormatDifficulty(chart, game)}</span>
|
||||
{Object.keys(chart.tierlistInfo).length > 0 && (
|
||||
<TierlistInfoPart chart={chart} />
|
||||
)}
|
||||
</>
|
||||
<span className="d-none d-lg-block">{FormatDifficulty(chart, game)}</span>
|
||||
)}
|
||||
<span className={!alwaysShort ? "d-lg-none" : ""}>
|
||||
{FormatDifficultyShort(chart, game)}
|
||||
</span>
|
||||
{Object.keys(chart.tierlistInfo).length > 0 && (
|
||||
<TierlistInfoPart chart={chart} game={game} />
|
||||
)}
|
||||
{!chart.isPrimary && (
|
||||
<QuickTooltip tooltipContent="This chart is an alternate, old chart.">
|
||||
<div>
|
||||
|
||||
@@ -2,47 +2,61 @@ 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 } from "tachi-common";
|
||||
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 }: { chart: ChartDocument }) {
|
||||
const keys = Object.keys(chart.tierlistInfo) as K[];
|
||||
export default function TierlistInfoPart({ chart, game }: { chart: ChartDocument; game: Game }) {
|
||||
const keys = GetGamePTConfig(game, chart.playtype).tierlists.filter(e => chart.tierlistInfo[e]);
|
||||
|
||||
return (
|
||||
<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})
|
||||
{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 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})
|
||||
{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>
|
||||
</QuickTooltip>
|
||||
<div className="d-block d-lg-none">
|
||||
<Muted>
|
||||
{keys.map(k => (
|
||||
<>
|
||||
{chart.tierlistInfo[k]!.text} <br />
|
||||
</>
|
||||
))}
|
||||
</Muted>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ export default function FilterDirectivesIndicator<D>({
|
||||
component={
|
||||
<>
|
||||
You can use{" "}
|
||||
<a rel="noreferrer" href="#">
|
||||
<a
|
||||
rel="noreferrer"
|
||||
href="https://tachi.readthedocs.io/en/latest/user/filter-directives/"
|
||||
>
|
||||
<strong>Filter Directives</strong>
|
||||
</a>{" "}
|
||||
inside this filter box.
|
||||
@@ -79,7 +82,10 @@ export default function FilterDirectivesIndicator<D>({
|
||||
<p>
|
||||
This is a slightly advanced feature for{" "}
|
||||
<HoverText hover="nerds">power users</HoverText>. You can read about it{" "}
|
||||
<a href="#">here</a>.<br />
|
||||
<a href="https://tachi.readthedocs.io/en/latest/user/filter-directives/">
|
||||
here
|
||||
</a>
|
||||
.<br />
|
||||
The quick explanation is that you can use directives like{" "}
|
||||
<code>Title:conflict Score:>=100</code> to perform advanced filters!
|
||||
</p>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function BMSFolderTable({
|
||||
const headers: Header<FolderDataset<"bms:7K" | "bms:14K">[0]>[] = [
|
||||
[
|
||||
"Chart",
|
||||
"Ch.",
|
||||
"Chart",
|
||||
NumericSOV(
|
||||
x =>
|
||||
x.tierlistInfo["sgl-EC"]?.value ?? x.tierlistInfo["sgl-HC"]?.value ?? x.levelNum
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function GenericFolderTable({
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const headers: Header<FolderDataset[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.levelNum)],
|
||||
FolderIndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.__related.pb?.scoreData.percent ?? -Infinity)],
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function IIDXFolderTable({
|
||||
const headers: Header<FolderDataset<"iidx:SP" | "iidx:DP">[0]>[] = [
|
||||
[
|
||||
"Chart",
|
||||
"Ch.",
|
||||
"Chart",
|
||||
NumericSOV(
|
||||
x => x.tierlistInfo["kt-NC"]?.value ?? x.tierlistInfo["kt-HC"]?.value ?? x.levelNum
|
||||
),
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function MusecaFolderTable({
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
const headers: Header<FolderDataset<"museca:Single">[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.levelNum)],
|
||||
FolderIndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.__related.pb?.scoreData.percent ?? -Infinity)],
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function SDVXFolderTable({
|
||||
playtype: Playtype;
|
||||
}) {
|
||||
const headers: Header<FolderDataset<"sdvx:Single">[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.levelNum)],
|
||||
FolderIndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.__related.pb?.scoreData.percent ?? -Infinity)],
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function BMSPBTable({
|
||||
const headers: Header<PBDataset<"bms:7K" | "bms:14K">[0]>[] = [
|
||||
[
|
||||
"Chart",
|
||||
"Ch.",
|
||||
"Chart",
|
||||
NumericSOV(
|
||||
x =>
|
||||
x.__related.chart.tierlistInfo["sgl-EC"]?.value ??
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function GenericPBTable({
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const headers: Header<PBDataset[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function IIDXPBTable({
|
||||
const headers: Header<PBDataset<"iidx:SP" | "iidx:DP">[0]>[] = [
|
||||
[
|
||||
"Chart",
|
||||
"Ch.",
|
||||
"Chart",
|
||||
NumericSOV(
|
||||
x =>
|
||||
x.__related.chart.tierlistInfo["kt-NC"]?.value ??
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function MusecaPBTable({
|
||||
playtype: "Single";
|
||||
}) {
|
||||
const headers: Header<PBDataset<"museca:Single">[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function SDVXPBTable({
|
||||
playtype: "7K" | "14K";
|
||||
}) {
|
||||
const headers: Header<PBDataset<"sdvx:Single">[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function BMSScoreTable({
|
||||
[
|
||||
[
|
||||
"Chart",
|
||||
"Ch.",
|
||||
"Chart",
|
||||
NumericSOV(
|
||||
x =>
|
||||
x.__related.chart.tierlistInfo["sgl-EC"]?.value ??
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function GenericScoreTable({
|
||||
dataset={dataset}
|
||||
pageLen={pageLen}
|
||||
headers={[
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function IIDXScoreTable({
|
||||
[
|
||||
[
|
||||
"Chart",
|
||||
"Ch.",
|
||||
"Chart",
|
||||
NumericSOV(
|
||||
x =>
|
||||
x.__related.chart.tierlistInfo["kt-NC"]?.value ??
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function MusecaScoreTable({
|
||||
pageLen={pageLen}
|
||||
headers={
|
||||
[
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function SDVXScoreTable({
|
||||
pageLen={pageLen}
|
||||
headers={
|
||||
[
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
IndicatorHeader,
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
|
||||
|
||||
@@ -64,7 +64,7 @@ export function UserHeaderBody({ reqUser }: { reqUser: PublicUserDocument }) {
|
||||
href="https://steamcommunity.com/id/"
|
||||
mode="steam"
|
||||
/>
|
||||
<ConditionalSocialMediaRender href="https://twitch.tv/" mode="twitch" />
|
||||
<ConditionalSocialMediaRender href="https://twitCharttv/" mode="twitch" />
|
||||
<ConditionalSocialMediaRender href="https://twitter.com/" mode="twitter" />
|
||||
<ConditionalSocialMediaRender
|
||||
href="https://youtube.com/channel/"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Game } from "tachi-common";
|
||||
|
||||
const scales: Record<Game, number> = {
|
||||
iidx: 2,
|
||||
bms: 2,
|
||||
chunithm: 1,
|
||||
ddr: 1,
|
||||
gitadora: 1,
|
||||
maimai: 1,
|
||||
museca: 5,
|
||||
sdvx: 5,
|
||||
usc: 5,
|
||||
};
|
||||
|
||||
export function GetGradeChartExpScale(game: Game) {
|
||||
return scales[game];
|
||||
}
|
||||
@@ -63,6 +63,7 @@ export const loggedInSplashes = [
|
||||
"Play ULTRAKILL.",
|
||||
"A Cutting-Edge Score Tracker (heh).",
|
||||
"Winnipeg is a warm, nice place to live.",
|
||||
"I accidentally the score tracker.",
|
||||
];
|
||||
|
||||
// Stuff that's bad for the kids, you know?
|
||||
@@ -70,4 +71,5 @@ export const contentiousSplashes = [
|
||||
"Formerly Chucks",
|
||||
"Node.js Is Bad Ass Rock Star Tech",
|
||||
"i'm obligated to ask for you to seek immediate psychological attention",
|
||||
"Is IIDX still a good game or are you all basically stupid?",
|
||||
];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Game, GPTTierlists, IDStrings, IIDX_LAMPS, ScoreDocument } from "tachi-common";
|
||||
import { Playtype } from "types/tachi";
|
||||
|
||||
type ScaleAchievedFns = {
|
||||
[I in IDStrings]: {
|
||||
[K in GPTTierlists[I]]: ((k: ScoreDocument<I>) => boolean) | null;
|
||||
};
|
||||
};
|
||||
|
||||
const ScaleNameAchievedFns: ScaleAchievedFns = {
|
||||
"bms:14K": {
|
||||
"sgl-EC": k => k.scoreData.lampIndex >= IIDX_LAMPS.EASY_CLEAR,
|
||||
"sgl-HC": k => k.scoreData.lampIndex >= IIDX_LAMPS.HARD_CLEAR,
|
||||
},
|
||||
"bms:7K": {
|
||||
"sgl-EC": k => k.scoreData.lampIndex >= IIDX_LAMPS.EASY_CLEAR,
|
||||
"sgl-HC": k => k.scoreData.lampIndex >= IIDX_LAMPS.HARD_CLEAR,
|
||||
},
|
||||
"chunithm:Single": {},
|
||||
"ddr:DP": {},
|
||||
"ddr:SP": {},
|
||||
"gitadora:Dora": {},
|
||||
"gitadora:Gita": {},
|
||||
"iidx:DP": {},
|
||||
"iidx:SP": {
|
||||
"kt-NC": k => k.scoreData.lampIndex >= IIDX_LAMPS.CLEAR,
|
||||
"kt-HC": k => k.scoreData.lampIndex >= IIDX_LAMPS.HARD_CLEAR,
|
||||
"kt-EXHC": k => k.scoreData.lampIndex >= IIDX_LAMPS.EX_HARD_CLEAR,
|
||||
},
|
||||
"maimai:Single": {},
|
||||
"museca:Single": {
|
||||
"tachi-score": null,
|
||||
},
|
||||
"sdvx:Single": {},
|
||||
"usc:Single": {},
|
||||
};
|
||||
|
||||
export function GetScaleAchievedFn(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
tierlist: GPTTierlists[IDStrings]
|
||||
) {
|
||||
const scl = ScaleNameAchievedFns[`${game}:${playtype}` as IDStrings];
|
||||
|
||||
if (!scl) {
|
||||
throw new Error(`Invalid game + pt combination ${game}:${playtype}. Can't find scale.`);
|
||||
}
|
||||
|
||||
// @ts-expect-error hack haha
|
||||
return scl[tierlist];
|
||||
}
|
||||
Reference in New Issue
Block a user