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
+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;