diff --git a/client/src/components/game/folder/FolderInfoHeader.tsx b/client/src/components/game/folder/FolderInfoHeader.tsx index 13a6a960d..2e740d2cf 100644 --- a/client/src/components/game/folder/FolderInfoHeader.tsx +++ b/client/src/components/game/folder/FolderInfoHeader.tsx @@ -4,12 +4,14 @@ 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 { GetGamePTConfig, GetScoreEnumConfs, UserDocument } from "tachi-common"; +import React, { useMemo, useState } from "react"; +import { GPTString, GetGamePTConfig, GetScoreEnumConfs, UserDocument } from "tachi-common"; import { GamePT } from "types/react"; import { FolderDataset } from "types/tables"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations"; import FolderMinimap from "./FolderMinimap"; import FolderScoreDistributionChart from "./FolderScoreDistributionChart"; +import FolderScoreAverages from "./FolderScoreAverages"; export default function FolderInfoHeader({ game, @@ -23,53 +25,67 @@ export default function FolderInfoHeader({ folderTitle: string; } & GamePT) { const preferredDefaultEnum = useBucket(game, playtype); - const [enumMetric, setEnumMetric] = useState(preferredDefaultEnum); - const [breakdownInfo, setBreakdownInfo] = useState<"graph" | "switchboard">("switchboard"); + + const [currentGraph, setCurrentGraph] = useState(`${preferredDefaultEnum}-stats`); + + const enumGraphs = ["minimap", "stats"]; const gptConfig = GetGamePTConfig(game, playtype); + const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[`${game}:${playtype}` as GPTString]; const enumConf = GetScoreEnumConfs(gptConfig); + const [metric, type] = useMemo(() => currentGraph.split("-"), [currentGraph]); + return (
- {Object.keys(enumConf).map((k) => ( - - - {UppercaseFirst(k)} Info - - ))} + {enumGraphs.flatMap((g) => + Object.keys(enumConf).flatMap((en) => ( + + {/* @ts-expect-error zzz */} + + {UppercaseFirst(en)} {UppercaseFirst(g)} + + )) + )} + + + Score Averages +
-
- -
- {breakdownInfo === "graph" ? ( + {type === "stats" ? ( - ) : ( + ) : type === "minimap" ? ( + ) : ( + )}
diff --git a/client/src/components/game/folder/FolderScoreAverages.tsx b/client/src/components/game/folder/FolderScoreAverages.tsx new file mode 100644 index 000000000..bb1cd011b --- /dev/null +++ b/client/src/components/game/folder/FolderScoreAverages.tsx @@ -0,0 +1,171 @@ +import { NumericSOV } from "util/sorts"; +import { UppercaseFirst } from "util/misc"; +import React, { useMemo } from "react"; +import { GetGamePTConfig, integer } from "tachi-common"; +import { ConfDecimalScoreMetric, ConfIntegerScoreMetric } from "tachi-common/types/metrics"; +import { UGPT } from "types/react"; +import { FolderDataset } from "types/tables"; +import DebugContent from "components/util/DebugContent"; +import MiniTable from "components/tables/components/MiniTable"; +import Muted from "components/util/Muted"; +import DifficultyCell from "components/tables/cells/DifficultyCell"; +import TitleCell from "components/tables/cells/TitleCell"; +import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells"; +import SongChartInfoFormat from "../songs/SongInfoFormat"; + +export default function FolderScoreAverages({ + folderDataset, + game, + playtype, + reqUser, +}: UGPT & { folderDataset: FolderDataset }) { + const gptConfig = GetGamePTConfig(game, playtype); + + const metrics = { + ...gptConfig.providedMetrics, + ...gptConfig.derivedMetrics, + }; + + const entries = Object.entries(metrics); + + const relevantScoreMetrics = entries + .filter(([k, v]) => { + if (v.type !== "DECIMAL" && v.type !== "INTEGER") { + return false; + } + + const v2 = v as ConfIntegerScoreMetric | ConfDecimalScoreMetric; + + if (v2.chartDependentMax) { + return false; + } + + return true; + }) + .map((e) => e[0]); + + const { unplayed, played, data } = useMemo(() => { + const data: Record = {}; + + const played = folderDataset.filter((e) => e.__related.pb); + + const unplayed = folderDataset.length - played.length; + + for (const metric of relevantScoreMetrics) { + const total = played.reduce( + // @ts-expect-error accessing property unchecked oh nooo + (acc, e) => acc + e.__related.pb?.scoreData[metric] ?? 0, + 0 + ); + + const sorted = played.sort( + // @ts-expect-error accessing property unchecked oh nooo + NumericSOV((k) => k.__related.pb?.scoreData[metric] ?? -Infinity) + ); + + const worst = sorted[0]; + const best = sorted[sorted.length - 1]; + + let avgAll = total / folderDataset.length; + let avgPlayed = total / played.length; + + if (metrics[metric].type === "INTEGER") { + avgAll = Math.floor(avgAll); + avgPlayed = Math.floor(avgPlayed); + } + + data[metric] = { + avgAll, + avgPlayed, + worst, + best, + }; + } + + return { + played, + unplayed, + data, + }; + }, [folderDataset, relevantScoreMetrics]); + + if (relevantScoreMetrics.length === 0) { + return
This game has nothing to show you!
; + } + + if (played.length === 0) { + return
You haven't played anything in this folder!
; + } + + const playRate = (100 * played.length) / folderDataset.length; + + return ( + + + Played + 50 ? "warning" : "danger" + }`} + > + {playRate.toFixed(2)}% + {unplayed > 0 && ( + <> +
+ + ({played.length} played, {unplayed} unplayed) + + + )} + + + {Object.entries(data).map(([k, v]) => ( + <> + + {UppercaseFirst(k)} Average (Played Charts) + {/* @ts-expect-error this won't fail */} + {metrics[k].formatter(v.avgPlayed)} + + + {UppercaseFirst(k)} Average (All Charts) + {/* @ts-expect-error this won't fail */} + {metrics[k].formatter(v.avgAll)} + + + Best {UppercaseFirst(k)} + + + + + + Worst {UppercaseFirst(k)} + + + + + + ))} +
+ ); +}