checkpoint: CLIENT BUILDS

now db migration and we just start piling on the bug fixes
This commit is contained in:
zkldi
2023-01-19 16:39:43 +00:00
parent 5cbb2d5f8a
commit cd865a66b2
41 changed files with 725 additions and 856 deletions
@@ -9,16 +9,15 @@ import Muted from "components/util/Muted";
import SelectButton from "components/util/SelectButton";
import { UserSettingsContext } from "context/UserSettingsContext";
import { useFormik } from "formik";
import { TachiConfig } from "lib/config";
import React, { useContext, useState } from "react";
import { Alert, Button, Form } from "react-bootstrap";
import { UserDocument, UserSettings } from "tachi-common";
import { UserDocument, UserSettingsDocument } from "tachi-common";
interface Props {
reqUser: UserDocument;
}
export default function UserSettingsPage({ reqUser }: Props) {
export default function UserSettingsDocumentPage({ reqUser }: Props) {
useSetSubheader(
["Users", reqUser.username, "Settings"],
[reqUser],
@@ -82,7 +81,7 @@ function AccountSettings({ reqUser }: { reqUser: UserDocument }) {
confPass: "",
},
onSubmit: async (values) => {
const r = await APIFetchV1<UserSettings>(
const r = await APIFetchV1<UserSettingsDocument>(
`/users/${reqUser.id}/change-password`,
{
method: "POST",
@@ -174,7 +173,7 @@ function PreferencesForm({ reqUser }: { reqUser: UserDocument }) {
deletableScores: settings?.preferences.deletableScores ?? false,
},
onSubmit: async (values) => {
const res = await APIFetchV1<UserSettings>(
const res = await APIFetchV1<UserSettingsDocument>(
`/users/${reqUser.id}/settings`,
{
method: "PATCH",
@@ -1,6 +1,6 @@
import { APIFetchV1, UnsuccessfulAPIFetchResponse } from "util/api";
import { ChangeOpacity } from "util/color-opacity";
import { UppercaseFirst } from "util/misc";
import { FormatGPTProfileRating, IsNotNullish, UppercaseFirst } from "util/misc";
import ClassBadge from "components/game/ClassBadge";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
@@ -21,8 +21,8 @@ import {
UserDocument,
ProfileRatingAlgorithms,
UserGameStats,
Classes,
} from "tachi-common";
import { GameClassSets } from "tachi-common/game-classes";
import { GPTLeaderboard, UGPTLeaderboardAdjacent } from "types/api-returns";
import { GamePT, SetState, UGPT } from "types/react";
@@ -129,24 +129,25 @@ function LeaderboardsPageContent({
</GentleLink>
</td>
<td>
{s.ratings[alg]
? gptConfig.profileRatingAlgFormatters[alg]
? gptConfig.profileRatingAlgFormatters[alg]!(s.ratings[alg]!)
: s.ratings[alg]!.toFixed(2)
{IsNotNullish(s.ratings[alg])
? FormatGPTProfileRating(game, playtype, alg, s.ratings[alg]!)
: "No Data."}
</td>
{/* temp */}
<td>
{Object.entries(s.classes).length
? Object.entries(s.classes).map(([k, v]) => (
<ClassBadge
key={`${k}:${v}`}
classValue={v}
classSet={k as GameClassSets[GPTString]}
game={game}
playtype={playtype}
/>
))
? Object.entries(s.classes).map(
([k, v]) =>
v && (
<ClassBadge
key={`${k}:${v}`}
classValue={v}
classSet={k as Classes[GPTString]}
game={game}
playtype={playtype}
/>
)
)
: "No Classes"}
</td>
</tr>
@@ -318,15 +318,6 @@ function PreferencesForm({ reqUser, game, playtype }: UGPT) {
<Col xs={12} lg={3} className="my-auto">
Expected Profile VF6{" "}
{((formik.values.gameSpecific.vf6Target ?? 0) * 50).toFixed(2)}
<br />
<ClassBadge
game="sdvx"
playtype="Single"
classSet="vfClass"
classValue={ToVolforceClass(
(formik.values.gameSpecific.vf6Target ?? 0) * 50
)}
/>
</Col>
</Row>
@@ -502,67 +493,6 @@ function RenderCurrentStats({
);
}
// Straight up stolen from tachi-server.
enum SDVXVFClasses {
SIENNA_I,
SIENNA_II,
SIENNA_III,
SIENNA_IV,
COBALT_I,
COBALT_II,
COBALT_III,
COBALT_IV,
DANDELION_I,
DANDELION_II,
DANDELION_III,
DANDELION_IV,
CYAN_I,
CYAN_II,
CYAN_III,
CYAN_IV,
SCARLET_I,
SCARLET_II,
SCARLET_III,
SCARLET_IV,
CORAL_I,
CORAL_II,
CORAL_III,
CORAL_IV,
ARGENTO_I,
ARGENTO_II,
ARGENTO_III,
ARGENTO_IV,
ELDORA_I,
ELDORA_II,
ELDORA_III,
ELDORA_IV,
CRIMSON_I,
CRIMSON_II,
CRIMSON_III,
CRIMSON_IV,
IMPERIAL_I,
IMPERIAL_II,
IMPERIAL_III,
IMPERIAL_IV,
}
function ToVolforceClass(vf: number) {
if (vf >= 24) {
return SDVXVFClasses.IMPERIAL_IV;
} else if (vf >= 20) {
// imperial i -> iv has gaps of 1
return SDVXVFClasses.IMPERIAL_I + Math.floor(vf - 20);
} else if (vf >= 14) {
// cyan i -> crimson iv has gaps of 0.25
return SDVXVFClasses.CYAN_I + Math.floor(4 * (vf - 14));
} else if (vf >= 10) {
// cobalt i -> dandelion iv have gaps of 0.5
return SDVXVFClasses.COBALT_I + Math.floor(2 * (vf - 10));
}
return Math.floor(vf / 2.5);
}
function ManageAccount({ reqUser, game, playtype }: UGPT) {
const [password, setPassword] = useState("");
const [deleting, setDeleting] = useState(false);
@@ -178,11 +178,11 @@ function FolderCompare({
<Col xs={12}>
<Divider />
</Col>
<CompareCard
{/* <CompareCard
dataset={dataset}
baseUsername={reqUser.username}
otherUsername={withUser.username}
/>
/> */}
<Col xs={12}>
<Divider />
<Form.Check
@@ -238,120 +238,123 @@ function UserCard({ username, game, playtype }: { username: string } & GamePT) {
);
}
function CompareCard({
dataset,
baseUsername,
otherUsername,
}: {
dataset: ComparePBsDataset;
// temp disabled. needs to be migrated into conf somehow
// as a declaration of "these are the metrics that should appear in the rival compare card".
baseUsername: string;
otherUsername: string;
}) {
// this code sucks
let scoreWin = 0;
let scoreDraw = 0;
let scoreLoss = 0;
let lampWin = 0;
let lampDraw = 0;
let lampLoss = 0;
// function CompareCard({
// dataset,
// baseUsername,
// otherUsername,
// }: {
// dataset: ComparePBsDataset;
for (const d of dataset) {
if (!d.compare && !d.base) {
scoreDraw++;
lampDraw++;
continue;
}
// baseUsername: string;
// otherUsername: string;
// }) {
// // this code sucks
// let scoreWin = 0;
// let scoreDraw = 0;
// let scoreLoss = 0;
// let lampWin = 0;
// let lampDraw = 0;
// let lampLoss = 0;
if (!d.compare) {
scoreWin++;
lampWin++;
continue;
}
// for (const d of dataset) {
// if (!d.compare && !d.base) {
// scoreDraw++;
// lampDraw++;
// continue;
// }
if (!d.base) {
scoreLoss++;
lampLoss++;
continue;
}
// if (!d.compare) {
// scoreWin++;
// lampWin++;
// continue;
// }
if (d.base.scoreData.percent > d.compare.scoreData.percent) {
scoreWin++;
} else if (d.base.scoreData.percent < d.compare.scoreData.percent) {
scoreLoss++;
} else {
scoreDraw++;
}
// if (!d.base) {
// scoreLoss++;
// lampLoss++;
// continue;
// }
if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) {
lampWin++;
} else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) {
lampLoss++;
} else {
lampDraw++;
}
}
// if (d.base.scoreData.percent > d.compare.scoreData.percent) {
// scoreWin++;
// } else if (d.base.scoreData.percent < d.compare.scoreData.percent) {
// scoreLoss++;
// } else {
// scoreDraw++;
// }
return (
<Col xs={12}>
<Card>
<Row>
<Col xs={12} lg={6}>
<MiniTable headers={["Score Win/Loss"]} colSpan={23}>
<tr>
<td>{baseUsername} Win</td>
// if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) {
// lampWin++;
// } else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) {
// lampLoss++;
// } else {
// lampDraw++;
// }
// }
<td style={{ color: COLOUR_SET.green }}>{scoreWin}</td>
// return (
// <Col xs={12}>
// <Card>
// <Row>
// <Col xs={12} lg={6}>
// <MiniTable headers={["Score Win/Loss"]} colSpan={23}>
// <tr>
// <td>{baseUsername} Win</td>
<td style={{ color: COLOUR_SET.green }}>
{((100 * scoreWin) / dataset.length).toFixed(2)}%
</td>
</tr>
<tr>
<td>Draw</td>
<td style={{ color: COLOUR_SET.gold }}>{scoreDraw}</td>
<td style={{ color: COLOUR_SET.gold }}>
{((100 * scoreDraw) / dataset.length).toFixed(2)}%
</td>
</tr>
<tr>
<td>{otherUsername} Win</td>
<td style={{ color: COLOUR_SET.red }}>{scoreLoss}</td>
<td style={{ color: COLOUR_SET.red }}>
{((100 * scoreLoss) / dataset.length).toFixed(2)}%
</td>
</tr>
</MiniTable>
</Col>
<Col xs={12} lg={6}>
<MiniTable headers={["Lamp Win/Loss"]} colSpan={23}>
<tr>
<td>{baseUsername} Win</td>
// <td style={{ color: COLOUR_SET.green }}>{scoreWin}</td>
<td style={{ color: COLOUR_SET.green }}>{lampWin}</td>
// <td style={{ color: COLOUR_SET.green }}>
// {((100 * scoreWin) / dataset.length).toFixed(2)}%
// </td>
// </tr>
// <tr>
// <td>Draw</td>
// <td style={{ color: COLOUR_SET.gold }}>{scoreDraw}</td>
// <td style={{ color: COLOUR_SET.gold }}>
// {((100 * scoreDraw) / dataset.length).toFixed(2)}%
// </td>
// </tr>
// <tr>
// <td>{otherUsername} Win</td>
// <td style={{ color: COLOUR_SET.red }}>{scoreLoss}</td>
// <td style={{ color: COLOUR_SET.red }}>
// {((100 * scoreLoss) / dataset.length).toFixed(2)}%
// </td>
// </tr>
// </MiniTable>
// </Col>
// <Col xs={12} lg={6}>
// <MiniTable headers={["Lamp Win/Loss"]} colSpan={23}>
// <tr>
// <td>{baseUsername} Win</td>
<td style={{ color: COLOUR_SET.green }}>
{((100 * lampWin) / dataset.length).toFixed(2)}%
</td>
</tr>
<tr>
<td>Draw</td>
<td style={{ color: COLOUR_SET.gold }}>{lampDraw}</td>
<td style={{ color: COLOUR_SET.gold }}>
{((100 * lampDraw) / dataset.length).toFixed(2)}%
</td>
</tr>
<tr>
<td>{otherUsername} Win</td>
<td style={{ color: COLOUR_SET.red }}>{lampLoss}</td>
<td style={{ color: COLOUR_SET.red }}>
{((100 * lampLoss) / dataset.length).toFixed(2)}%
</td>
</tr>
</MiniTable>
</Col>
</Row>
</Card>
</Col>
);
}
// <td style={{ color: COLOUR_SET.green }}>{lampWin}</td>
// <td style={{ color: COLOUR_SET.green }}>
// {((100 * lampWin) / dataset.length).toFixed(2)}%
// </td>
// </tr>
// <tr>
// <td>Draw</td>
// <td style={{ color: COLOUR_SET.gold }}>{lampDraw}</td>
// <td style={{ color: COLOUR_SET.gold }}>
// {((100 * lampDraw) / dataset.length).toFixed(2)}%
// </td>
// </tr>
// <tr>
// <td>{otherUsername} Win</td>
// <td style={{ color: COLOUR_SET.red }}>{lampLoss}</td>
// <td style={{ color: COLOUR_SET.red }}>
// {((100 * lampLoss) / dataset.length).toFixed(2)}%
// </td>
// </tr>
// </MiniTable>
// </Col>
// </Row>
// </Card>
// </Col>
// );
// }
@@ -7,15 +7,17 @@ import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import LinkButton from "components/util/LinkButton";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import SelectButton from "components/util/SelectButton";
import useApiQuery from "components/util/query/useApiQuery";
import { useBucket } from "components/util/useBucket";
import useUGPTBase from "components/util/useUGPTBase";
import { UserContext } from "context/UserContext";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import React, { useContext, useMemo, useState } from "react";
import { FolderDocument, GetGamePTConfig, UserDocument } from "tachi-common";
import { FolderDocument, GetGPTString, GetGamePTConfig, GetScoreMetricConf } from "tachi-common";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import { FolderStatsInfo, UGPTFolderSearch } from "types/api-returns";
import { GamePT, UGPT } from "types/react";
import { UGPT } from "types/react";
export default function FoldersSearch({ reqUser, game, playtype }: UGPT) {
const [search, setSearch] = useState("");
@@ -82,34 +84,27 @@ export function FolderInfoComponent({
folder,
}: UGPT & { folder: FolderDocument; folderStats: FolderStatsInfo }) {
const gptConfig = GetGamePTConfig(game, playtype);
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)];
const preferredDefaultEnum = useBucket(game, playtype);
const [elements, setElements] = useState<"grade" | "lamp">(preferredDefaultEnum);
const [metric, setMetric] = useState<string>(preferredDefaultEnum);
const base = useUGPTBase({ reqUser, game, playtype });
const dataset = useMemo(() => {
if (elements === "grade") {
return (
<DistributionTable
colours={gptConfig.gradeColours}
keys={Reverse(gptConfig.grades)}
values={folderStats.grades}
max={folderStats.chartCount}
/>
);
}
const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric<string>;
return (
<DistributionTable
colours={gptConfig.lampColours}
keys={Reverse(gptConfig.lamps)}
values={folderStats.lamps}
// @ts-expect-error hack yeah sorry
colours={gptImpl.enumColours[metric]}
keys={Reverse(conf.values)}
values={folderStats.stats[metric]}
max={folderStats.chartCount}
/>
);
}, [elements]);
}, [metric]);
const { user } = useContext(UserContext);
@@ -139,11 +134,11 @@ export function FolderInfoComponent({
<div className="row text-center">
<div className="col-12">
<div className="btn-group">
<SelectButton value={elements} setValue={setElements} id="grade">
<SelectButton value={metric} setValue={setMetric} id="grade">
<Icon type="sort-alpha-up" />
Grades
</SelectButton>
<SelectButton value={elements} setValue={setElements} id="lamp">
<SelectButton value={metric} setValue={setMetric} id="lamp">
<Icon type="lightbulb" />
Lamps
</SelectButton>
@@ -24,12 +24,16 @@ import {
FolderDocument,
Game,
GamePTConfig,
GetGPTString,
GetGamePTConfig,
GetScoreMetricConf,
Playtype,
TableDocument,
UserDocument,
} from "tachi-common";
import { FolderStatsInfo, UGPTTableReturns } from "types/api-returns";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
interface Props {
reqUser: UserDocument;
@@ -248,17 +252,14 @@ function TableBarChart({
dataMap: Map<string, UGPTFolderStats>;
}) {
const gptConfig = GetGamePTConfig(table.game, table.playtype);
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(table.game, table.playtype)];
const bucket = useBucket(table.game, table.playtype);
const [mode, setMode] = useState<"grades" | "lamps">(bucket === "grade" ? "grades" : "lamps");
const [enumMetric, setEnumMetric] = useState<string>(bucket);
const colours = useMemo(() => {
if (mode === "grades") {
return gptConfig.gradeColours;
}
return gptConfig.lampColours;
}, [mode]);
// @ts-expect-error hack!
const colours = useMemo(() => gptImpl.enumColours[enumMetric], [enumMetric]);
const dataset = useMemo(() => {
const arr = [];
@@ -275,11 +276,12 @@ function TableBarChart({
};
// this is a series of stupid hacks and i dont really care.
for (const key in data.stats[mode]) {
// @ts-expect-error hack!
for (const key in data.stats[enumMetric]) {
// @ts-expect-error stupid key stuff
realData[key] = (100 * data.stats[mode][key]) / data.stats.chartCount;
realData[key] = (100 * data.stats[enumMetric][key]) / data.stats.chartCount;
// @ts-expect-error stupid key stuff
realData[`${key}-count`] = data.stats[mode][key];
realData[`${key}-count`] = data.stats[enumMetric][key];
}
arr.push({
@@ -292,18 +294,18 @@ function TableBarChart({
}
return arr.reverse();
}, [dataMap, mode]);
}, [dataMap, enumMetric]);
return (
<Card header="Overview">
<div className="row">
<div className="col-12 d-flex justify-content-center">
<div className="btn-group">
<SelectButton value={mode} setValue={setMode} id="grades">
<SelectButton value={enumMetric} setValue={setEnumMetric} id="grades">
<Icon type="sort-alpha-up" />
Grades
</SelectButton>
<SelectButton value={mode} setValue={setMode} id="lamps">
<SelectButton value={enumMetric} setValue={setEnumMetric} id="lamps">
<Icon type="lightbulb" />
Lamps
</SelectButton>
@@ -322,11 +324,11 @@ function TableBarChart({
{/* This hack is necessary because otherwise
nivo gets caught in a render loop
and dies */}
{mode === "grades" ? (
{enumMetric === "grades" ? (
<OverviewBarChart
key="grade"
gptConfig={gptConfig}
mode={mode}
mode={enumMetric}
dataset={dataset}
colours={colours}
/>
@@ -334,7 +336,7 @@ function TableBarChart({
<OverviewBarChart
key="lamp"
gptConfig={gptConfig}
mode={mode}
mode={enumMetric}
dataset={dataset}
colours={colours}
/>
@@ -348,12 +350,12 @@ function TableBarChart({
function OverviewBarChart({
gptConfig,
mode,
mode: metric,
dataset,
colours,
}: {
gptConfig: GamePTConfig;
mode: "grades" | "lamps";
mode: string;
dataset: any;
colours: any;
}) {
@@ -367,6 +369,8 @@ function OverviewBarChart({
[dataset]
);
const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric<string>;
return (
<ResponsiveBar
indexBy="folder"
@@ -388,7 +392,7 @@ function OverviewBarChart({
)}
/>
)}
keys={Reverse(gptConfig[mode])}
keys={Reverse(conf.values)}
colors={(k) => ChangeOpacity(colours[k.id], 0.5)}
borderColor={(k) => colours[k.data.id]}
borderWidth={1}
+21 -21
View File
@@ -2,7 +2,7 @@ import { ToCDNURL } from "util/api";
import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts";
import { ChangeOpacity } from "util/color-opacity";
import { CreateChartLink } from "util/data";
import { NumericSOV } from "util/sorts";
import { NumericSOV, StrSOV } from "util/sorts";
import PlaytypeSelect from "app/pages/dashboard/games/_game/PlaytypeSelect";
import GPTChartPage from "app/pages/dashboard/games/_game/_playtype/GPTChartPage";
import GPTDevInfo from "app/pages/dashboard/games/_game/_playtype/GPTDevInfo";
@@ -30,6 +30,7 @@ import {
ChartDocument,
FormatDifficulty,
Game,
GetGPTString,
GetGameConfig,
GetGamePTConfig,
SongDocument,
@@ -43,6 +44,8 @@ import QuestlinePage from "components/game/targets/QuestlinePage";
import QuestsPage from "components/game/targets/QuestsPage";
import ChartRedirector from "app/pages/dashboard/games/_game/_playtype/ChartRedirector";
import QuestPage from "components/game/targets/QuestPage";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import { DifficultyConfig, FixedDifficulties } from "tachi-common/types/game-config-utils";
export default function GameRoutes() {
const { game } = useParams<{ game: string }>();
@@ -265,18 +268,16 @@ function SongInfoHeader({
SongsReturn) {
const gptConfig = GetGamePTConfig(game, playtype);
// accidentally O(n^2) but this is a short list so who cares
const sortedCharts = charts
.slice(0)
.sort(
NumericSOV((x) =>
gptConfig.difficulties.indexOf(
game === "itg"
? (x as ChartDocument<"itg:Stamina">).data.difficultyTag
: x.difficulty
)
)
);
const sortedCharts = charts.slice(0).sort(
gptConfig.difficulties.type === "DYNAMIC"
? StrSOV((x) => x.difficulty)
: NumericSOV((x) =>
// can TS *really* not infer this?
(gptConfig.difficulties as FixedDifficulties<string>).order.indexOf(
x.difficulty
)
)
);
const [ImageCell, setImageCell] = useState<JSX.Element | null>(null);
@@ -293,7 +294,7 @@ function SongInfoHeader({
/>
);
} else if (game === "itg") {
const banner = (song as SongDocument<"itg">).data.banner;
const banner = (song as SongDocument<"itg">).data.originalPack;
if (banner) {
setImageCell(
@@ -385,12 +386,9 @@ function DifficultyButton({
setActiveChart,
activeChart,
}: Props & { chart: ChartDocument }) {
const gptConfig = GetGamePTConfig(game, playtype);
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)];
const diffTag =
game === "itg"
? (chart as ChartDocument<"itg:Stamina">).data.difficultyTag
: chart.difficulty;
const diffTag = chart.difficulty;
return (
<LinkButton
@@ -399,9 +397,11 @@ function DifficultyButton({
key={chart.chartID}
to={CreateChartLink(chart, game)}
style={{
backgroundColor: gptConfig.difficultyColours[diffTag]
// @ts-expect-error hack!
backgroundColor: gptImpl.difficultyColours[diffTag]
? ChangeOpacity(
gptConfig.difficultyColours[diffTag]!,
// @ts-expect-error hack!
gptImpl.difficultyColours[diffTag],
activeChart?.chartID === chart.chartID ? 0.4 : 0.2
)
: undefined,
+10 -10
View File
@@ -4,19 +4,19 @@ import { COLOUR_SET } from "tachi-common";
import ChartTooltip from "./ChartTooltip";
const yAxes = {
EASY: [22, 60, 80, 100],
NORMAL: [22, 60, 80, 100],
HARD: [30, 50, 100],
Easy: [22, 60, 80, 100],
Normal: [22, 60, 80, 100],
Hard: [30, 50, 100],
DAN_GAUGE: [30, 50, 100],
EX_HARD: [30, 50, 100],
EXHard: [30, 50, 100],
};
const colours = {
DAN_GAUGE: COLOUR_SET.gray,
EASY: [COLOUR_SET.green, COLOUR_SET.vibrantRed],
NORMAL: [COLOUR_SET.blue, COLOUR_SET.vibrantRed],
HARD: COLOUR_SET.vibrantRed,
EX_HARD: COLOUR_SET.gold,
Easy: [COLOUR_SET.green, COLOUR_SET.vibrantRed],
Normal: [COLOUR_SET.blue, COLOUR_SET.vibrantRed],
Hard: COLOUR_SET.vibrantRed,
EXHard: COLOUR_SET.gold,
};
export default function IIDXLampChart({
@@ -34,11 +34,11 @@ export default function IIDXLampChart({
height?: number | string;
data: Serie[];
usePercentXAxis?: boolean;
type: "DAN_GAUGE" | "EASY" | "NORMAL" | "HARD" | "EX_HARD";
type: "DAN_GAUGE" | "Normal" | "Easy" | "Hard" | "EXHard";
} & ResponsiveLine["props"]) {
let realData = [];
if (type === "HARD" || type === "EX_HARD" || type === "DAN_GAUGE") {
if (type === "Hard" || type === "EXHard" || type === "DAN_GAUGE") {
realData = data;
} else {
const failSet = [];
@@ -1,10 +1,11 @@
import { UppercaseFirst } from "util/misc";
import Card from "components/layout/page/Card";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import SelectButton from "components/util/SelectButton";
import { useBucket } from "components/util/useBucket";
import React, { useState } from "react";
import { UserDocument } from "tachi-common";
import { GetGamePTConfig, GetScoreEnumConfs, UserDocument } from "tachi-common";
import { GamePT } from "types/react";
import { FolderDataset } from "types/tables";
import FolderMinimap from "./FolderMinimap";
@@ -22,21 +23,23 @@ export default function FolderInfoHeader({
folderTitle: string;
} & GamePT) {
const preferredDefaultEnum = useBucket(game, playtype);
const [view, setView] = useState<"grade" | "lamp">(preferredDefaultEnum);
const [enumMetric, setEnumMetric] = useState<string>(preferredDefaultEnum);
const [breakdownInfo, setBreakdownInfo] = useState<"graph" | "switchboard">("switchboard");
const gptConfig = GetGamePTConfig(game, playtype);
const enumConf = GetScoreEnumConfs(gptConfig);
return (
<Card header={`${reqUser.username}'s ${folderTitle} Breakdown`}>
<div className="col-12 d-flex justify-content-center">
<div className="btn-group">
<SelectButton value={view} setValue={setView} id="grade">
<Icon type="chart-area" />
Score Info
</SelectButton>
<SelectButton value={view} setValue={setView} id="lamp">
<Icon type="lightbulb" />
Lamp Info
</SelectButton>
{Object.keys(enumConf).map((k) => (
<SelectButton value={enumMetric} setValue={setEnumMetric} id={k}>
<Icon type="chart-area" />
{UppercaseFirst(k)} Info
</SelectButton>
))}
</div>
</div>
<div className="col-12 mt-4 d-flex justify-content-center">
@@ -58,7 +61,7 @@ export default function FolderInfoHeader({
game={game}
folderDataset={folderDataset}
playtype={playtype}
view={view}
view={enumMetric}
/>
) : (
<FolderMinimap
@@ -66,7 +69,7 @@ export default function FolderInfoHeader({
game={game}
playtype={playtype}
folderDataset={folderDataset}
enumMetric={view}
enumMetric={enumMetric}
/>
)}
</Card>
@@ -1,150 +1,45 @@
import { CountElements, Reverse } from "util/misc";
import React, { useMemo } from "react";
import { GetGamePTConfig } from "tachi-common";
import { GetGPTString, GetGamePTConfig, GetScoreMetricConf } from "tachi-common";
import { GamePT } from "types/react";
import { FolderDataset } from "types/tables";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import FolderDistributionTable from "./FolderDistributionTable";
type Props = {
folderDataset: FolderDataset;
view: "grade" | "lamp";
view: string;
} & GamePT;
export default function FolderScoreDistributionChart({
game,
playtype,
folderDataset,
view,
view: metric,
}: Props) {
const gptConfig = GetGamePTConfig(game, playtype);
const conf = GetScoreMetricConf(gptConfig, metric) as ConfEnumScoreMetric<string>;
const values = useMemo(() => {
if (view === "grade") {
return CountElements(folderDataset, (x) => x.__related.pb?.scoreData.grade ?? null);
}
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, playtype)];
return CountElements(folderDataset, (x) => x.__related.pb?.scoreData.lamp ?? null);
}, [folderDataset, view]);
const values = useMemo(
// @ts-expect-error hack
() => CountElements(folderDataset, (x) => x.__related.pb?.scoreData[metric] ?? null),
[folderDataset, metric]
);
return (
<div className="row">
<div className="col-12 col-lg-6 offset-lg-3">
{view === "grade" ? (
<FolderDistributionTable
colours={gptConfig.gradeColours}
keys={Reverse(gptConfig.grades)}
max={folderDataset.length}
values={values}
/>
) : (
<FolderDistributionTable
colours={gptConfig.lampColours}
keys={Reverse(gptConfig.lamps)}
max={folderDataset.length}
values={values}
/>
)}
<FolderDistributionTable
// @ts-expect-error this will always work
colours={gptImpl.enumColours[metric]}
keys={Reverse(conf.values)}
max={folderDataset.length}
values={values}
/>
</div>
{/* <div className="col-12 col-lg-8">
<ScoreDistributionChart {...{ game, playtype, folderDataset, view }} />
</div> */}
</div>
);
}
// function ScoreDistributionChart({ game, folderDataset, playtype, view }: Props) {
// if (view === "grade") {
// return <ScoreBarChart game={game} playtype={playtype} folderDataset={folderDataset} />;
// }
// return <>LAMP TODO</>;
// }
// function ScoreBarChart({ game, playtype, folderDataset }: Omit<Props, "view">) {
// const dataMap = CreateChartIDMap(folderDataset);
// const gptConfig = GetGamePTConfig(game, playtype);
// const expScale = GetGradeChartExpScale(game);
// const expFn = ComposeExpFn(expScale);
// const invExpFn = ComposeInverseExpFn(expScale);
// const dataset = [];
// for (const data of folderDataset) {
// const value = data.__related.pb ? expFn(data.__related.pb.scoreData.percent) : 0;
// dataset.push({
// chartID: data.chartID,
// expValue: expFn(value),
// value,
// grade: data.__related.pb?.scoreData.grade,
// });
// }
// dataset.sort(NumericSOV(x => x.expValue));
// return (
// <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"}
// 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;
// const lgv = invExpFn(x);
// for (const [i, gradeBnd] of gptConfig.gradeBoundaries.entries()) {
// if (Math.abs(gradeBnd - lgv) < 0.00005) {
// nearest = i;
// break;
// }
// }
// if (nearest === undefined) {
// return null;
// }
// return gptConfig.grades[nearest];
// },
// }}
// axisBottom={null}
// {...DEFAULT_BAR_PROPS}
// labelSkipWidth={40}
// />
// </div>
// );
// }
@@ -24,11 +24,16 @@ import {
Game,
GamePTConfig,
GetGamePTConfig,
GetGPTString,
GetScoreMetricConf,
GPTString,
integer,
SongDocument,
TableDocument,
} from "tachi-common";
import { SessionFolderRaises, SessionReturns } from "types/api-returns";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
export default function SessionFolderRaiseBreakdown({
sessionData,
@@ -182,7 +187,7 @@ export default function SessionFolderRaiseBreakdown({
chartMap={chartMap}
game={game}
folderRaiseInfo={folderRaiseInfo}
gptConfig={gptConfig}
gptString={GetGPTString(game, playtype)}
/>
))}
</MiniTable>
@@ -205,41 +210,39 @@ export default function SessionFolderRaiseBreakdown({
);
}
const SortRaisesNicely = (gptConfig: GamePTConfig, preferredScoreBucket: "grade" | "lamp") =>
NumericSOV<SessionFolderRaises>((x) => {
const baseValue =
x.type === "grade"
? gptConfig.grades.indexOf(x.value)
: gptConfig.lamps.indexOf(x.value);
const SortRaisesNicely = (gptConfig: GamePTConfig, preferredEnum: string) => {
const conf = GetScoreMetricConf(gptConfig, preferredEnum) as ConfEnumScoreMetric<string>;
return NumericSOV<SessionFolderRaises>((x) => {
const baseValue = conf.values.indexOf(x.value);
// if this is what the user prefers to see, push it to the top
if (x.type === preferredScoreBucket) {
if (x.type === preferredEnum) {
return baseValue + 1_000_000;
}
return baseValue;
}, true);
};
function FolderRaiseRender({
folderRaiseInfo,
game,
songMap,
chartMap,
gptConfig,
gptString,
}: {
folderRaiseInfo: SessionFolderRaises;
chartMap: Map<string, ChartDocument>;
songMap: Map<integer, SongDocument>;
game: Game;
gptConfig: GamePTConfig;
gptString: GPTString;
}) {
let colour;
if (folderRaiseInfo.type === "grade") {
colour = gptConfig.gradeColours[folderRaiseInfo.value];
} else {
colour = gptConfig.lampColours[folderRaiseInfo.value];
}
const colour =
// @ts-expect-error lazy
GPT_CLIENT_IMPLEMENTATIONS[gptString].enumColours[folderRaiseInfo.type][
folderRaiseInfo.value
];
const newTotal = folderRaiseInfo.previousCount + folderRaiseInfo.raisedCharts.length;
@@ -154,15 +154,7 @@ function FormatAverage(score: integer, playtype: "SP" | "DP", notecount: integer
score={score}
/>
),
delta: (
<DeltaCell
game="iidx"
playtype={playtype}
score={score}
percent={percent}
grade={grade}
/>
),
delta: <DeltaCell value={percent} grade={grade} gradeBoundaries={IIDXLIKE_GBOUNDARIES} />,
};
}
@@ -1,29 +1,24 @@
import { GenericFormatGradeDelta } from "util/grade-deltas";
import React from "react";
import { Game, Grades, GPTString, Playtypes } from "tachi-common";
import { GetGradeDeltas, GradeBoundary } from "tachi-common";
export default function DeltaCell({
game,
playtype,
score,
percent,
value,
grade,
gradeBoundaries,
}: {
game: Game;
playtype: Playtypes[Game];
score: number;
percent: number;
grade: Grades[GPTString];
value: number;
grade: string;
gradeBoundaries: Array<GradeBoundary<string>>;
}) {
if (score === 0) {
if (value === 0) {
return <td>N/A</td>;
}
// eslint-disable-next-line prefer-const
let { lower, upper, closer } = GenericFormatGradeDelta(game, playtype, score, percent, grade);
let { lower, upper, closer } = GetGradeDeltas(gradeBoundaries, grade, value);
// (max-)+20 is a stupid statistic. hard override it.
if ((game === "iidx" || game === "bms" || game === "pms") && lower.startsWith("(MAX-)+")) {
if (lower.startsWith("(MAX-)+")) {
closer = "upper";
}
@@ -8,8 +8,10 @@ import {
FormatDifficulty,
FormatDifficultyShort,
Game,
GetGPTString,
GetGamePTConfig,
} from "tachi-common";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import BMSOrPMSDifficultyCell from "./BMSOrPMSDifficultyCell";
import RatingSystemPart from "./RatingSystemPart";
import USCDifficultyCell from "./USCDifficultyCell";
@@ -49,10 +51,13 @@ export default function DifficultyCell({
return <ITGDifficultyCell chart={chart as ChartDocument<"itg:Stamina">} />;
}
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(game, chart.playtype)];
return (
<td
style={{
backgroundColor: ChangeOpacity(gptConfig.difficultyColours[chart.difficulty]!, 0.2),
// @ts-expect-error yawn
backgroundColor: ChangeOpacity(gptImpl.difficultyColours[chart.difficulty]!, 0.2),
minWidth: "80px",
maxWidth: "100px",
}}
@@ -72,7 +77,7 @@ export default function DifficultyCell({
</div>
</QuickTooltip>
)}
{!noTierlist && Object.keys(chart.tierlistInfo).length > 0 && (
{!noTierlist && gptImpl.ratingSystems.length > 0 && (
<RatingSystemPart chart={chart} game={game} />
)}
{!chart.isPrimary && (
@@ -2,18 +2,25 @@ import { ChangeOpacity } from "util/color-opacity";
import QuickTooltip from "components/layout/misc/QuickTooltip";
import Muted from "components/util/Muted";
import React from "react";
import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common";
import { ChartDocument, COLOUR_SET } from "tachi-common";
import MiniTable from "../components/MiniTable";
import RatingSystemPart from "./RatingSystemPart";
export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg:Stamina"> }) {
const gptConfig = GetGamePTConfig("itg", "Stamina");
const COLOUR_LOOKUP = {
Beginner: COLOUR_SET.paleBlue,
Easy: COLOUR_SET.green,
Medium: COLOUR_SET.vibrantYellow,
Hard: COLOUR_SET.red,
Expert: COLOUR_SET.pink,
Edit: COLOUR_SET.gray,
};
export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg:Stamina"> }) {
return (
<td
style={{
backgroundColor: ChangeOpacity(
gptConfig.difficultyColours[chart.data.difficultyTag] ?? COLOUR_SET.gray,
COLOUR_LOOKUP[chart.data.difficultyTag] ?? COLOUR_SET.gray,
0.2
),
minWidth: "80px",
@@ -40,8 +47,8 @@ export default function ITGDifficultyCell({ chart }: { chart: ChartDocument<"itg
>
<div>
<span>
{chart.data.difficultyTag}: {chart.level} [{chart.data.displayBPM.toFixed()}
]
{chart.data.difficultyTag}: {chart.level} [
{chart.data.primaryBPM?.toFixed() ?? "???"}]
</span>
<br />
<Muted>{chart.data.charter}</Muted>
@@ -1,4 +1,3 @@
import { FormatMillions } from "util/misc";
import React from "react";
import { COLOUR_SET, Game, PBScoreDocument, Playtype } from "tachi-common";
import { ConfScoreMetric } from "tachi-common/types/metrics";
@@ -34,41 +33,6 @@ export default function PBCompareCell({
status = cmp(base.scoreData[metric], compare.scoreData[metric]);
}
let scoreRenderFn = (k: number) => k.toString();
let shouldShowPercent = false;
let shouldPrioritisePercent = false;
switch (game) {
case "chunithm":
case "gitadora":
case "usc":
case "sdvx":
case "popn":
case "wacca":
case "museca":
scoreRenderFn = FormatMillions;
shouldShowPercent = false;
break;
case "jubeat":
scoreRenderFn = FormatMillions;
shouldShowPercent = true;
shouldPrioritisePercent = true;
break;
case "bms":
case "iidx":
scoreRenderFn = (k: number) => k.toString();
shouldShowPercent = true;
break;
}
const fmtCmpPercent = fmtNum(
(base?.scoreData.percent ?? 0) - (compare?.scoreData.percent ?? 0),
2,
"%"
);
const cmpScore = (base?.scoreData.score ?? 0) - (compare?.scoreData.score ?? 0);
const cmpLamp = (base?.scoreData.lampIndex ?? 0) - (compare?.scoreData.lampIndex ?? 0);
return (
<td
style={{
@@ -83,33 +47,18 @@ export default function PBCompareCell({
}}
>
{metricConf.type === "ENUM" ? (
<></>
// @ts-expect-error this access is fine
<>{base.scoreData[metric]}</>
) : (
<>
{shouldPrioritisePercent ? (
<>
<strong>{fmtCmpPercent}</strong>
{shouldShowPercent && (
<>
<br />
<small>
({(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)})
</small>
</>
)}
</>
) : (
<>
<strong>{(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)}</strong>
{shouldShowPercent && (
<>
<br />
<small>({fmtCmpPercent})</small>
</>
)}
</>
)}
</>
metricConf.type === "DECIMAL" ||
(metricConf.type === "INTEGER" && (
<strong>
{/* @ts-expect-error this is fine */}
{(base.scoreData[metric] > 0 ? "+" : "") +
// @ts-expect-error this is fine
metricConf.formatter(base.scoreData[metric])}
</strong>
))
)}
</td>
);
@@ -41,7 +41,6 @@ export default function RatingSystemPart({ chart, game }: { chart: ChartDocument
<td>{e}</td>
<td>
{strV} <Muted>({numV.toFixed(2)})</Muted>
{/* @ts-expect-error utterly silly types */}
{e.idvDifference(chart) && (
<>
<br />
@@ -9,7 +9,7 @@ export default function ScoreCell({
percent,
scoreRenderFn,
}: {
score: integer;
score?: integer;
percent: number;
grade: string;
colour: string;
@@ -26,8 +26,14 @@ export default function ScoreCell({
<br />
{`${percent.toFixed(2)}%`}
<br />
<small className="text-muted">[{scoreRenderFn ? scoreRenderFn(score) : score}]</small>
{score !== undefined && (
<>
<br />
<small className="text-muted">
[{scoreRenderFn ? scoreRenderFn(score) : score}]
</small>
</>
)}
</td>
);
}
@@ -32,7 +32,7 @@ export default function TitleCell({
`/misc/popn/banners/${(chart as any).data.inGameID}.png`
)})`;
} else if (game === "itg") {
const banner = (song as SongDocument<"itg">).data.banner;
const banner = (song as SongDocument<"itg">).data.originalPack;
if (banner) {
backgroundImage = `url(${ToCDNURL(
@@ -2,8 +2,10 @@ import { ChangeOpacity } from "util/color-opacity";
import { FormatTables } from "util/misc";
import QuickTooltip from "components/layout/misc/QuickTooltip";
import Icon from "components/util/Icon";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import { GPTClientImplementation } from "lib/types";
import React from "react";
import { ChartDocument, COLOUR_SET, GetGamePTConfig } from "tachi-common";
import { COLOUR_SET, ChartDocument } from "tachi-common";
import RatingSystemPart from "./RatingSystemPart";
export default function USCDifficultyCell({
@@ -15,10 +17,12 @@ export default function USCDifficultyCell({
? `${chart.difficulty} ${chart.level}`
: FormatTables(chart.data.tableFolders);
const gptConfig = GetGamePTConfig("usc", chart.playtype);
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[
`usc:${chart.playtype}`
] as GPTClientImplementation<"usc:Controller">;
const bgColour = ChangeOpacity(
chart.data.isOfficial ? gptConfig.difficultyColours[chart.difficulty]! : COLOUR_SET.teal,
chart.data.isOfficial ? gptImpl.difficultyColours[chart.difficulty]! : COLOUR_SET.teal,
0.2
);
@@ -5,7 +5,7 @@ import React, { useEffect, useState } from "react";
import { Nav } from "react-bootstrap";
import { ChartDocument, PBScoreDocument, ScoreDocument } from "tachi-common";
type LampTypes = "NORMAL" | "EASY" | "HARD" | "EX_HARD";
type LampTypes = "Normal" | "Easy" | "Hard" | "EXHard";
export function BMSGraphsComponent({
score,
@@ -34,34 +34,34 @@ export function BMSGraphsComponent({
<div className="col-12 d-flex justify-content-center">
<Nav variant="pills">
<SelectNav
id="EASY"
id="Easy"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("EASY")}
disabled={shouldDisable("Easy")}
>
Easy
</SelectNav>
<SelectNav
id="NORMAL"
id="Normal"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("NORMAL")}
disabled={shouldDisable("Normal")}
>
Groove
</SelectNav>
<SelectNav
id="HARD"
id="Hard"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("HARD")}
disabled={shouldDisable("Hard")}
>
Hard
</SelectNav>
<SelectNav
id="EX_HARD"
id="EXHard"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("EX_HARD")}
disabled={shouldDisable("EXHard")}
>
Ex Hard
</SelectNav>
@@ -108,32 +108,34 @@ function LampToKey(
if (IsScore(score) && score.scoreMeta.gauge) {
switch (score.scoreMeta.gauge) {
case "EASY":
return "Easy";
case "NORMAL":
return "Normal";
case "HARD":
return score.scoreMeta.gauge;
return "Hard";
case "EX-HARD":
return "EX_HARD";
return "EXHard";
}
}
if (lamp === "CLEAR") {
return "NORMAL";
return "Normal";
} else if (lamp === "EASY CLEAR") {
return "EASY";
return "Easy";
} else if (lamp === "HARD CLEAR") {
return "HARD";
return "Hard";
} else if (lamp === "EX HARD CLEAR") {
return "EX_HARD";
return "EXHard";
} else if (lamp === "FULL COMBO") {
// @hack - attempt to guess what gauge they used?
if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) {
return "EX_HARD";
return "EXHard";
}
return "NORMAL";
return "Normal";
} else if (lamp === "NO PLAY") {
// dan gauge looks like this
return "HARD";
return "Hard";
}
return "NORMAL";
return "Normal";
}
@@ -32,7 +32,7 @@ import { PBScoreDocument, ScoreDocument } from "tachi-common";
// );
// }
type LampTypes = "DAN_GAUGE" | "NORMAL" | "EASY" | "HARD" | "EX_HARD";
type LampTypes = "DAN_GAUGE" | "Normal" | "Easy" | "Hard" | "EXHard";
export function IIDXGraphsComponent({
score,
@@ -43,7 +43,12 @@ export function IIDXGraphsComponent({
let gaugeStatus: "none" | "single" | "gsm" = "none";
if (score.scoreData.optional.gsm) {
if (
score.scoreData.optional.gsmEXHard &&
score.scoreData.optional.gsmHard &&
score.scoreData.optional.gsmNormal &&
score.scoreData.optional.gsmEasy
) {
gaugeStatus = "gsm";
} else if (score.scoreData.optional.gaugeHistory) {
gaugeStatus = "single";
@@ -78,34 +83,34 @@ export function IIDXGraphsComponent({
</SelectNav>
)}
<SelectNav
id="EASY"
id="Easy"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("EASY")}
disabled={shouldDisable("Easy")}
>
Easy
</SelectNav>
<SelectNav
id="NORMAL"
id="Normal"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("NORMAL")}
disabled={shouldDisable("Normal")}
>
Normal
</SelectNav>
<SelectNav
id="HARD"
id="Hard"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("HARD")}
disabled={shouldDisable("Hard")}
>
Hard
</SelectNav>
<SelectNav
id="EX_HARD"
id="EXHard"
value={lamp}
setValue={setLamp}
disabled={shouldDisable("EX_HARD")}
disabled={shouldDisable("EXHard")}
>
Ex Hard
</SelectNav>
@@ -113,7 +118,7 @@ export function IIDXGraphsComponent({
</div>
<div className="col-12">
{gaugeStatus === "gsm" && lamp !== "DAN_GAUGE" ? (
<GraphComponent type={lamp} values={score.scoreData.optional.gsm![lamp]} />
<GraphComponent type={lamp} values={score.scoreData.optional[`gsm${lamp}`]!} />
) : gaugeStatus === "single" ? (
<GraphComponent type={lamp} values={score.scoreData.optional.gaugeHistory!} />
) : (
@@ -157,31 +162,34 @@ function LampToKey(
if (IsScore(score) && score.scoreMeta.gauge) {
switch (score.scoreMeta.gauge) {
case "EASY":
return "Easy";
case "NORMAL":
return "Normal";
case "HARD":
return score.scoreMeta.gauge;
return "Hard";
case "ASSISTED EASY":
return "EASY";
return "Easy";
case "EX-HARD":
return "EX_HARD";
return "EXHard";
}
}
if (lamp === "CLEAR") {
return "NORMAL";
return "Normal";
} else if (lamp === "EASY CLEAR") {
return "EASY";
return "Easy";
} else if (lamp === "HARD CLEAR") {
return "HARD";
return "Hard";
} else if (lamp === "EX HARD CLEAR") {
return "EX_HARD";
return "EXHard";
} else if (lamp === "FULL COMBO") {
// @hack - attempt to guess what gauge they used?
// this could be hard or easy, we actually legitimately do not know in this scenario
if ((score.scoreData.optional.gaugeHistory?.[0] ?? 0) > 22) {
return "EX_HARD";
return "EXHard";
}
return "NORMAL";
return "Normal";
}
return "NORMAL";
return "Normal";
}
@@ -26,14 +26,14 @@ export function ITGGraphsComponent({
</Nav>
</div>
<div className="col-12">
{chart.data.breakdown.npsPerMeasure ? (
{chart.data.npsPerMeasure ? (
<ITGDensityChart
height="200px"
mobileHeight="175px"
data={[
{
id: "chart",
data: chart.data.breakdown.npsPerMeasure.map((e, i) => ({
data: chart.data.npsPerMeasure.map((e, i) => ({
x: i,
y: e,
})),
@@ -7,7 +7,7 @@ import useUGPTBase from "components/util/useUGPTBase";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { Link } from "react-router-dom";
import { ChartDocument, Game } from "tachi-common";
import { ChartDocument, Game, GetGamePTConfig } from "tachi-common";
import { ChartRivalsReturn } from "types/api-returns";
import { RivalChartDataset } from "types/tables";
import { Col } from "react-bootstrap";
@@ -45,6 +45,8 @@ export default function RivalCompare({ chart, game }: { chart: ChartDocument; ga
);
}
const gptConfig = GetGamePTConfig(game, playtype);
const rivalDataset: RivalChartDataset = [...data.rivals, currentUser]
.map((u) => ({
...u,
@@ -52,7 +54,10 @@ export default function RivalCompare({ chart, game }: { chart: ChartDocument; ga
pb: data.pbs.find((p) => p.userID === u.id) ?? null,
},
}))
.sort(NumericSOV((x) => x.__related.pb?.scoreData.percent ?? -Infinity, true))
.sort(
// @ts-expect-error this access is obviously legal
NumericSOV((x) => x.__related.pb?.scoreData[gptConfig.defaultMetric] ?? -Infinity, true)
)
.map((e, index) => ({
...e,
__related: {
@@ -1,5 +1,13 @@
import React from "react";
import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
import {
GPTString,
PBScoreDocument,
ScoreRatingAlgorithms,
ScoreDocument,
IIDXLIKE_GBOUNDARIES,
GetGPTString,
} from "tachi-common";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
import DeltaCell from "../cells/DeltaCell";
import RatingCell from "../cells/RatingCell";
@@ -14,12 +22,19 @@ export default function BMSCoreCells({
}) {
return (
<>
<ScoreCell score={sc} />
<DeltaCell
game="bms"
playtype={sc.playtype}
score={sc.scoreData.score}
<ScoreCell
colour={
// @ts-expect-error lazy
GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(sc.game, sc.playtype)].enumColours
.grade[sc.scoreData.grade]
}
grade={sc.scoreData.grade}
percent={sc.scoreData.percent}
score={sc.scoreData.score}
/>
<DeltaCell
gradeBoundaries={IIDXLIKE_GBOUNDARIES}
value={sc.scoreData.percent}
grade={sc.scoreData.grade}
/>
<BMSOrPMSLampCell score={sc} />
@@ -17,7 +17,11 @@ export default function GitadoraCoreCells({
}) {
return (
<>
<ScoreCell showScore={false} score={sc} />
<ScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
percent={sc.scoreData.percent}
/>
<GitadoraJudgementCell score={sc} />
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
<RatingCell score={sc} rating={rating} />
@@ -1,5 +1,13 @@
import React from "react";
import { ChartDocument, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
import {
ChartDocument,
PBScoreDocument,
ScoreRatingAlgorithms,
ScoreDocument,
IIDXLIKE_GBOUNDARIES,
GetGPTString,
} from "tachi-common";
import { GPT_CLIENT_IMPLEMENTATIONS, GetEnumColour } from "lib/game-implementations";
import BPICell from "../cells/BPICell";
import DeltaCell from "../cells/DeltaCell";
import IIDXLampCell from "../cells/IIDXLampCell";
@@ -17,12 +25,15 @@ export default function IIDXCoreCells({
}) {
return (
<>
<ScoreCell score={sc} />
<DeltaCell
game="iidx"
playtype={sc.playtype}
score={sc.scoreData.score}
<ScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
percent={sc.scoreData.percent}
score={sc.scoreData.score}
/>
<DeltaCell
gradeBoundaries={IIDXLIKE_GBOUNDARIES}
value={sc.scoreData.percent}
grade={sc.scoreData.grade}
/>
<IIDXLampCell sc={sc} chart={chart} />
@@ -15,7 +15,11 @@ export default function ITGCoreCells({
}) {
return (
<>
<ScoreCell showScore={false} score={sc} />
<ScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
percent={sc.scoreData.scorePercent}
/>
<ITGJudgementCell score={sc} />
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
<RatingCell score={sc} rating={rating} />
@@ -15,7 +15,11 @@ export default function MaimaiDXCoreCells({
}) {
return (
<>
<ScoreCell showScore={false} score={sc} />
<ScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
percent={sc.scoreData.percent}
/>
<MaimaiDXJudgementCell score={sc} />
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
<RatingCell score={sc} rating={rating} />
@@ -1,5 +1,13 @@
import React from "react";
import { GPTString, PBScoreDocument, ScoreRatingAlgorithms, ScoreDocument } from "tachi-common";
import {
GPTString,
PBScoreDocument,
ScoreRatingAlgorithms,
ScoreDocument,
IIDXLIKE_GBOUNDARIES,
GetGPTString,
} from "tachi-common";
import { GPT_CLIENT_IMPLEMENTATIONS, GetEnumColour } from "lib/game-implementations";
import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
import DeltaCell from "../cells/DeltaCell";
import RatingCell from "../cells/RatingCell";
@@ -16,12 +24,15 @@ export default function PMSCoreCells({
}) {
return (
<>
<ScoreCell score={sc} />
<DeltaCell
game="pms"
playtype={sc.playtype}
score={sc.scoreData.score}
<ScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
percent={sc.scoreData.percent}
score={sc.scoreData.score}
/>
<DeltaCell
gradeBoundaries={IIDXLIKE_GBOUNDARIES}
value={sc.scoreData.percent}
grade={sc.scoreData.grade}
/>
<BMSOrPMSLampCell score={sc} />
@@ -1,76 +0,0 @@
import IIDXLampCell from "components/tables/cells/IIDXLampCell";
import JubeatScoreCell from "components/tables/cells/JubeatScoreCell";
import LampCell from "components/tables/cells/LampCell";
import MillionsScoreCell from "components/tables/cells/MillionsScoreCell";
import PopnLampCell from "components/tables/cells/PopnLampCell";
import ScoreCell from "components/tables/cells/ScoreCell";
import SDVXLampCell from "components/tables/cells/SDVXLampCell";
import React from "react";
import { ChartDocument, Game, PBScoreDocument, ScoreDocument } from "tachi-common";
import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
import BMSCoreCells from "./BMSCoreCells";
export default function ShortScoreCoreCells({
game,
score,
chart,
}: {
score: ScoreDocument | PBScoreDocument;
chart: ChartDocument;
game: Game;
}) {
const sc = score as any; // lazy hack
if (game === "iidx") {
return (
<>
<ScoreCell score={sc} />
<IIDXLampCell sc={sc} chart={chart as ChartDocument<"iidx:SP" | "iidx:DP">} />
</>
);
} else if (game === "bms" || game === "pms") {
return (
<>
<ScoreCell score={sc} />
<BMSOrPMSLampCell score={sc} />
</>
);
} else if (
game === "sdvx" ||
game === "usc" ||
game === "museca" ||
game === "wacca" ||
game === "chunithm" ||
game === "gitadora"
) {
return (
<>
<MillionsScoreCell score={sc} />
<SDVXLampCell score={sc} />
</>
);
} else if (game === "jubeat") {
return (
<>
<JubeatScoreCell sc={sc} />
<LampCell score={sc} />
</>
);
} else if (game === "popn") {
return (
<>
<MillionsScoreCell score={sc} />
<PopnLampCell score={sc} />
</>
);
} else if (game === "itg") {
return (
<>
<ScoreCell showScore={false} score={sc} />
<LampCell score={sc} />
</>
);
}
return <>No cells defined for {game}. Report this!</>;
}
@@ -8,8 +8,8 @@ import PBCompareCell from "../cells/PBCompareCell";
import TitleCell from "../cells/TitleCell";
import SelectableCompareType from "../components/SelectableCompareType";
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells";
import ChartHeader from "../headers/ChartHeader";
import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
export default function ComparePBsTable({
dataset,
@@ -103,7 +103,7 @@ function Row({ data, game, metric }: { data: ComparePBsDataset[0]; game: Game; m
<DifficultyCell chart={data.chart} game={game} alwaysShort />
<TitleCell game={game} song={data.song} chart={data.chart} />
{data.base ? (
<ShortScoreCoreCells score={data.base} game={game} chart={data.chart} />
<ScoreCoreCells score={data.base} game={game} chart={data.chart} />
) : (
<td colSpan={2}>Not Played</td>
)}
@@ -116,7 +116,7 @@ function Row({ data, game, metric }: { data: ComparePBsDataset[0]; game: Game; m
metric={metric}
/>
{data.compare ? (
<ShortScoreCoreCells score={data.compare} game={game} chart={data.chart} />
<ScoreCoreCells score={data.compare} game={game} chart={data.chart} />
) : (
<td colSpan={2}>Not Played</td>
)}
@@ -10,12 +10,14 @@ import {
FormatChart,
Game,
GetGamePTConfig,
GetScoreMetricConf,
GoalDocument,
Playtype,
SongDocument,
} from "tachi-common";
import { GamePT, SetState, UGPT } from "types/react";
import CheckEdit from "components/util/CheckEdit";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
export default function SetNewGoalModal({
show,
@@ -32,14 +34,15 @@ export default function SetNewGoalModal({
preData: { chart: ChartDocument; song: SongDocument } | FolderDocument;
} & UGPT) {
const gptConfig = GetGamePTConfig(game, playtype);
const conf = GetScoreMetricConf(
gptConfig,
gptConfig.preferredDefaultEnum
) as ConfEnumScoreMetric<string>;
const [criteria, setCriteria] = useState<GoalDocument["criteria"]>({
mode: "single",
key: gptConfig.preferredDefaultEnum === "grade" ? "scoreData.gradeIndex" : "scoreData.lampIndex",
value:
gptConfig.preferredDefaultEnum === "grade"
? gptConfig.grades.indexOf(gptConfig.clearGrade)
: gptConfig.lamps.indexOf(gptConfig.clearLamp),
key: gptConfig.preferredDefaultEnum,
value: conf.values.indexOf(conf.minimumRelevantValue),
});
const charts = useMemo<GoalDocument["charts"]>(
@@ -283,20 +286,18 @@ function CriteriaValuePicker({
onChange: (value: GoalDocument["criteria"]["value"]) => void;
} & GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const conf = GetScoreMetricConf(gptConfig, criteria.key);
switch (criteria.key) {
case "scoreData.score":
return (
<Form.Control
style={{ display: "inline", width: "unset" }}
className="mx-2"
onChange={(e) => onChange(Number(e.target.value))}
type="number"
min={0}
value={criteria.value}
/>
);
case "scoreData.percent":
if (!conf) {
return <>ENOCONF {criteria.key}</>;
}
switch (conf.type) {
case "NULLABLE_GRAPH":
case "GRAPH":
return <>Cannot set goals for graph metrics.</>;
case "DECIMAL":
case "INTEGER":
return (
<Form.Control
style={{ display: "inline", width: "unset" }}
@@ -305,36 +306,22 @@ function CriteriaValuePicker({
type="number"
value={criteria.value}
min={0}
max={gptConfig.percentMax}
/>
);
case "scoreData.gradeIndex":
case "ENUM":
return (
<Select
inline
value={criteria.value.toString()}
setValue={(v) => onChange(Number(v))}
>
{gptConfig.grades.map((e, i) => (
<option key={i} value={i}>
{e}
</option>
))}
</Select>
);
case "scoreData.lampIndex":
return (
<Select
inline
value={criteria.value.toString()}
setValue={(v) => onChange(Number(v))}
>
{gptConfig.lamps.map((e, i) => (
<option key={i} value={i}>
{e}
</option>
))}
{conf.values
.slice(conf.values.indexOf(conf.minimumRelevantValue))
.map((e, i) => (
<option key={i} value={i}>
{e}
</option>
))}
</Select>
);
}
@@ -342,12 +329,16 @@ function CriteriaValuePicker({
function getBaseKeyValue(game: Game, playtype: Playtype, key: GoalDocument["criteria"]["key"]) {
const gptConfig = GetGamePTConfig(game, playtype);
const conf = GetScoreMetricConf(gptConfig, key);
switch (key) {
case "scoreData.gradeIndex":
return gptConfig.grades.indexOf(gptConfig.clearGrade);
case "scoreData.lampIndex":
return gptConfig.lamps.indexOf(gptConfig.clearLamp);
if (!conf) {
// SHOULD NEVER HAPPEN!
return 0;
}
switch (conf.type) {
case "ENUM":
return conf.values.indexOf(conf.minimumRelevantValue);
default:
return 0;
}
+38 -45
View File
@@ -1,5 +1,6 @@
import { APIFetchV1 } from "util/api";
import { CreateSongMap } from "util/data";
import { UppercaseFirst } from "util/misc";
import { useFormik } from "formik";
import React, { ChangeEventHandler, useContext, useEffect, useState } from "react";
import { Button, Form, Modal } from "react-bootstrap";
@@ -11,6 +12,8 @@ import {
UserDocument,
ShowcaseStatDetails,
Playtype,
GetScoreMetricConf,
GetScoreMetrics,
} from "tachi-common";
import { GamePT, SetState } from "types/react";
import { SongChartsSearch } from "types/api-returns";
@@ -48,13 +51,13 @@ export default function UGPTStatCreator({
if (values.mode === "chart") {
stat = {
mode: "chart",
metric: values.property as ShowcaseStatDetails["metric"],
metric: values.metric as ShowcaseStatDetails["metric"],
chartID: values.chartID ?? "",
};
} else if (values.mode === "folder") {
stat = {
mode: "folder",
metric: values.property as "lamp" | "score" | "percent" | "grade",
metric: values.metric as "lamp" | "score" | "percent" | "grade",
folderID: values.folderID ?? "",
gte,
};
@@ -68,7 +71,7 @@ export default function UGPTStatCreator({
});
useEffect(() => {
if (formik.values.mode === "folder" && formik.values.property === "playcount") {
if (formik.values.mode === "folder" && formik.values.metric === "playcount") {
formik.setValues({ ...formik.values, metric: "lamp" });
}
}, [formik.values.mode]);
@@ -143,6 +146,8 @@ export default function UGPTStatCreator({
})();
}, [chartSearch, requesterHasPlayed]);
const gptConfig = GetGamePTConfig(game, playtype);
return (
<Modal show={show} onHide={() => setShow(false)}>
<Modal.Header closeButton>
@@ -167,14 +172,15 @@ export default function UGPTStatCreator({
<Form.Label>Property</Form.Label>
<Form.Control
as="select"
id="property"
value={formik.values.property}
id="metric"
value={formik.values.metric}
onChange={formik.handleChange}
>
<option value="lamp">Lamp</option>
<option value="grade">Grade</option>
<option value="percent">Percent</option>
<option value="score">Score</option>
{GetScoreMetrics(gptConfig, ["DECIMAL", "INTEGER", "ENUM"]).map((e) => (
<option key={e} value={e}>
{UppercaseFirst(e)}
</option>
))}
{formik.values.mode === "chart" && (
<option value="playcount">Playcount</option>
)}
@@ -222,7 +228,7 @@ export default function UGPTStatCreator({
{...{
game,
playtype,
property: formik.values.property,
metric: formik.values.metric,
}}
/>
</Form.Group>
@@ -268,52 +274,39 @@ export default function UGPTStatCreator({
}
function FolderGTESelect({
property,
metric,
game,
playtype,
value,
onChange,
}: {
property: string;
metric: string;
value: number;
onChange: ChangeEventHandler<HTMLSelectElement | HTMLInputElement>;
} & GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const props = { value, onChange };
if (property === "grade") {
return (
<select className="form-control" {...props}>
{gptConfig.grades.map((e, i) => (
<option key={i} value={i}>
{e}
</option>
))}
</select>
);
} else if (property === "lamp" || property === "playcount") {
return (
<select className="form-control" {...props}>
{gptConfig.lamps.map((e, i) => (
<option key={i} value={i}>
{e}
</option>
))}
</select>
);
} else if (property === "percent") {
return (
<input
className="form-control"
type="number"
min={0}
max={gptConfig.percentMax}
{...props}
/>
);
} else if (property === "score") {
return <input className="form-control" type="number" min={0} {...props} />;
const conf = GetScoreMetricConf(gptConfig, metric);
if (!conf) {
return <>error: no conf? what?</>;
}
throw new Error(`Invalid property ${property}.`);
if (conf.type === "ENUM") {
return (
<select className="form-control" {...props}>
{conf.values.map((e, i) => (
<option key={i} value={i}>
{e}
</option>
))}
</select>
);
} else if (conf.type === "GRAPH" || conf.type === "NULLABLE_GRAPH") {
return <>can't set stats for graphs. how'd you get here?</>;
}
return <input className="form-control" type="number" min={0} {...props} />;
}
+5 -5
View File
@@ -4,7 +4,7 @@ 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 { BMS_14K_IMPL, BMS_7K_IMPL, PMS_IMPL } from "./games/bms-pms";
import { bg, bgc } from "./games/_util";
type GPTClientImplementations = {
@@ -397,10 +397,10 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
},
"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,
"bms:14K": BMS_14K_IMPL,
"bms:7K": BMS_7K_IMPL,
"pms:Controller": PMS_IMPL,
"pms:Keyboard": PMS_IMPL,
"sdvx:Single": SDVX_IMPL,
"usc:Controller": USC_IMPL,
"usc:Keyboard": USC_IMPL,
+136 -3
View File
@@ -3,9 +3,9 @@ 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";
import { CreateRatingSys, bg } from "./_util";
const BMS_PMS_IMPL: GPTClientImplementation<GPTStrings["bms" | "pms"]> = {
const BASE_IMPL: GPTClientImplementation<GPTStrings["bms" | "pms"]> = {
difficultyColours: {
CHART: COLOUR_SET.gray, // lol
},
@@ -55,4 +55,137 @@ const BMS_PMS_IMPL: GPTClientImplementation<GPTStrings["bms" | "pms"]> = {
],
};
export const;
export const BMS_7K_IMPL: GPTClientImplementation<"bms:7K"> = {
...BASE_IMPL,
classColours: {
genocideDan: {
NORMAL_1: bg("lightblue"),
NORMAL_2: bg("lightblue"),
NORMAL_3: bg("lightblue"),
NORMAL_4: bg("lightblue"),
NORMAL_5: bg("lightblue"),
NORMAL_6: bg("lightblue"),
NORMAL_7: bg("lightblue"),
NORMAL_8: bg("lightblue"),
NORMAL_9: bg("lightred"),
NORMAL_10: bg("lightred"),
INSANE_1: "info",
INSANE_2: "info",
INSANE_3: "info",
INSANE_4: "info",
INSANE_5: "info",
INSANE_6: "info",
INSANE_7: "info",
INSANE_8: "info",
INSANE_9: bg("red"),
INSANE_10: bg("red"),
INSANE_KAIDEN: bg("teal"),
OVERJOY: bg("purple"),
},
lnDan: {
DAN_1: bg("lightblue"),
DAN_2: bg("lightblue"),
DAN_3: bg("lightblue"),
DAN_4: bg("lightblue"),
DAN_5: bg("lightblue"),
DAN_6: bg("lightblue"),
DAN_7: bg("lightblue"),
DAN_8: bg("lightblue"),
DAN_9: bg("lightred"),
DAN_10: bg("lightred"),
KAIDEN: bg("teal"),
OVERJOY: bg("purple"),
UDON: bg("gold"),
},
scratchDan: {
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",
KAIDEN: "warning",
},
stslDan: {
SL0: null,
SL1: null,
SL2: null,
SL3: null,
SL4: null,
SL5: null,
SL6: null,
SL7: null,
SL8: null,
SL9: null,
SL10: null,
SL11: null,
SL12: null,
ST0: null,
ST1: null,
ST2: null,
ST3: null,
ST4: null,
ST5: null,
ST6: null,
ST7: null,
ST8: null,
ST9: null,
ST10: null,
ST11: null,
},
},
};
export const BMS_14K_IMPL: GPTClientImplementation<"bms:14K"> = {
...BASE_IMPL,
classColours: {
genocideDan: BMS_7K_IMPL.classColours.genocideDan,
stslDan: {
SL0: null,
SL1: null,
SL2: null,
SL3: null,
SL4: null,
SL5: null,
SL6: null,
SL7: null,
SL8: null,
SL9: null,
SL10: null,
SL11: null,
SL12: null,
},
},
};
export const PMS_IMPL: GPTClientImplementation<GPTStrings["pms"]> = {
...BASE_IMPL,
classColours: {
dan: {
INSANE_1: "info",
INSANE_2: "info",
INSANE_3: "info",
INSANE_4: "info",
INSANE_5: "info",
INSANE_6: "info",
INSANE_7: "info",
INSANE_8: "info",
INSANE_9: bg("red"),
INSANE_10: bg("red"),
INSANE_KAIDEN: bg("teal"),
OVERJOY: bg("purple"),
UNDEFINED: "warning",
},
},
};
+1 -1
View File
@@ -1,6 +1,6 @@
import { NumericSOV } from "util/sorts";
import { GPTClientImplementation } from "lib/types";
import { COLOUR_SET, GPTString, GPTStrings } from "tachi-common";
import { COLOUR_SET, GPTStrings } from "tachi-common";
import { bg, bgc } from "./_util";
const GITADORA_ENUM_COLOURS: GPTClientImplementation<
+62 -1
View File
@@ -1,7 +1,7 @@
import { NumericSOV } from "util/sorts";
import { GPTClientImplementation } from "lib/types";
import { COLOUR_SET, GPTStrings } from "tachi-common";
import { CreateRatingSys } from "./_util";
import { CreateRatingSys, bg, bgc } from "./_util";
const SDVXLIKE_ENUM_COLOURS: GPTClientImplementation<GPTStrings["usc" | "sdvx"]>["enumColours"] = {
grade: {
@@ -39,6 +39,64 @@ export const SDVX_IMPL: GPTClientImplementation<"sdvx:Single"> = {
XCD: COLOUR_SET.blue,
MXM: COLOUR_SET.white,
},
classColours: {
dan: {
DAN_1: bg(COLOUR_SET.red),
DAN_2: bg(COLOUR_SET.paleBlue),
DAN_3: bgc("gold", "black"),
DAN_4: bg("gray"),
DAN_5: bgc(COLOUR_SET.teal, "black"),
DAN_6: bg("blue"),
DAN_7: bgc(COLOUR_SET.vibrantPink, "black"),
DAN_8: bgc("pink", "black"),
DAN_9: bgc("white", "black"),
DAN_10: "warning",
DAN_11: "danger",
INF: bgc("purple", "gold"),
},
vfClass: {
SIENNA_I: bg(COLOUR_SET.red),
SIENNA_II: bg(COLOUR_SET.red),
SIENNA_III: bg(COLOUR_SET.red),
SIENNA_IV: bg(COLOUR_SET.red),
COBALT_I: bg(COLOUR_SET.paleBlue),
COBALT_II: bg(COLOUR_SET.paleBlue),
COBALT_III: bg(COLOUR_SET.paleBlue),
COBALT_IV: bg(COLOUR_SET.paleBlue),
DANDELION_I: bgc(COLOUR_SET.gold, "black"),
DANDELION_II: bgc(COLOUR_SET.gold, "black"),
DANDELION_III: bgc(COLOUR_SET.gold, "black"),
DANDELION_IV: bgc(COLOUR_SET.gold, "black"),
CYAN_I: bgc(COLOUR_SET.teal, "black"),
CYAN_II: bgc(COLOUR_SET.teal, "black"),
CYAN_III: bgc(COLOUR_SET.teal, "black"),
CYAN_IV: bgc(COLOUR_SET.teal, "black"),
SCARLET_I: bgc(COLOUR_SET.vibrantPink, "black"),
SCARLET_II: bgc(COLOUR_SET.vibrantPink, "black"),
SCARLET_III: bgc(COLOUR_SET.vibrantPink, "black"),
SCARLET_IV: bgc(COLOUR_SET.vibrantPink, "black"),
CORAL_I: bgc(COLOUR_SET.pink, "black"),
CORAL_II: bgc(COLOUR_SET.pink, "black"),
CORAL_III: bgc(COLOUR_SET.pink, "black"),
CORAL_IV: bgc(COLOUR_SET.pink, "black"),
ARGENTO_I: bgc(COLOUR_SET.white, "black"),
ARGENTO_II: bgc(COLOUR_SET.white, "black"),
ARGENTO_III: bgc(COLOUR_SET.white, "black"),
ARGENTO_IV: bgc(COLOUR_SET.white, "black"),
ELDORA_I: "warning",
ELDORA_II: "warning",
ELDORA_III: "warning",
ELDORA_IV: "warning",
CRIMSON_I: bg(COLOUR_SET.vibrantRed),
CRIMSON_II: bg(COLOUR_SET.vibrantRed),
CRIMSON_III: bg(COLOUR_SET.vibrantRed),
CRIMSON_IV: bg(COLOUR_SET.vibrantRed),
IMPERIAL_I: bg(COLOUR_SET.vibrantPurple),
IMPERIAL_II: bg(COLOUR_SET.vibrantPurple),
IMPERIAL_III: bg(COLOUR_SET.vibrantPurple),
IMPERIAL_IV: bg(COLOUR_SET.vibrantPurple),
},
},
ratingSystems: [
CreateRatingSys(
"Tierlist",
@@ -68,4 +126,7 @@ export const USC_IMPL: GPTClientImplementation<GPTStrings["usc"]> = {
["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)],
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
],
classColours: {
vfClass: SDVX_IMPL.classColours.vfClass,
},
};
-87
View File
@@ -1,87 +0,0 @@
import { Game, GetGamePTConfig, Grades, GPTString, integer, Playtypes } from "tachi-common";
export function AbsoluteGradeDelta<GPT extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
percent: number,
gradeOrIndex: Grades[GPT] | integer
) {
const gptConfig = GetGamePTConfig(game, playtype);
if (!gptConfig.gradeBoundaries) {
return 0;
}
const maxScore = Math.round(score * (100 / percent));
const gradeIndex =
typeof gradeOrIndex === "number" ? gradeOrIndex : gptConfig.grades.indexOf(gradeOrIndex);
const gradeScore = Math.ceil((gptConfig.gradeBoundaries[gradeIndex] * maxScore) / 100);
return score - gradeScore;
}
export function RelativeGradeDelta<GPT extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
percent: number,
grade: Grades[GPT],
relativeIndex: integer
) {
const gptConfig = GetGamePTConfig(game, playtype);
const nextGradeIndex = gptConfig.grades.indexOf(grade) + relativeIndex;
if (nextGradeIndex < 0 || nextGradeIndex >= gptConfig.grades.length) {
return null;
}
return {
grade: gptConfig.grades[nextGradeIndex],
delta: AbsoluteGradeDelta(game, playtype, score, percent, nextGradeIndex),
};
}
function WrapGrade(grade: string) {
if (grade.endsWith("-") || grade.endsWith("+")) {
return `(${grade})`;
}
return grade;
}
export function GenericFormatGradeDelta<GPT extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
percent: number,
grade: Grades[GPT],
formatNumFn: (n: number) => number = (s) => s
): {
lower: string;
upper?: string;
closer: "lower" | "upper";
} {
const upper = RelativeGradeDelta(game, playtype, score, percent, grade, 1);
const lower = AbsoluteGradeDelta(game, playtype, score, percent, grade);
const formatLower = `${WrapGrade(grade)}+${formatNumFn(lower)}`;
if (!upper) {
return {
lower: formatLower,
closer: "lower",
};
}
const formatUpper = `${WrapGrade(upper.grade)}${formatNumFn(upper.delta)}`;
return {
lower: formatLower,
upper: formatUpper,
closer: upper.delta + lower < 0 ? "lower" : "upper",
};
}
+1 -1
View File
@@ -128,7 +128,7 @@ export function EscapeStringRegexp(string: string) {
return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d");
}
export function Reverse<T>(arr: T[]): T[] {
export function Reverse<T>(arr: readonly T[]): T[] {
return arr.slice(0).reverse();
}
+1 -1
View File
@@ -9,7 +9,7 @@ export function HumanFriendlyStrToEnumIndex(
const conf = GetScoreMetricConf(gptConfig, enumMetric);
if (conf.type !== "ENUM") {
if (conf?.type !== "ENUM") {
return () => 0; // wut
}