feat: insights infrastructure + jubility as poc

This commit is contained in:
zkldi
2022-12-19 16:42:25 +00:00
parent 335f9703c2
commit 2114b43664
15 changed files with 348 additions and 58 deletions
@@ -144,9 +144,9 @@ function AlgSelector({
function useFetchPBs(url: string, reqUser: UserDocument) {
const { data, error } = useQuery(url, async () => {
const res = await APIFetchV1<{
pbs: PBScoreDocument<"iidx:SP">[];
charts: ChartDocument<"iidx:SP">[];
songs: SongDocument<"iidx">[];
pbs: PBScoreDocument[];
charts: ChartDocument[];
songs: SongDocument[];
}>(url);
if (!res.success) {
@@ -0,0 +1,80 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import { GetGPTInsights } from "components/tools-insights/ToolsAndInsights";
import Divider from "components/util/Divider";
import LinkButton from "components/util/LinkButton";
import React from "react";
import { Col, Row } from "react-bootstrap";
import { Link, Route, Switch } from "react-router-dom";
import { FormatGame, GetGameConfig } from "tachi-common";
import { UGPT } from "types/react";
export default function InsightsPage({ reqUser, game, playtype }: UGPT) {
const gameConfig = GetGameConfig(game);
useSetSubheader(
["Users", reqUser.username, "Games", gameConfig.name, playtype, "Insights"],
[reqUser, game, playtype],
`${reqUser.username}'s ${FormatGame(game, playtype)} Insights`
);
const insights = GetGPTInsights(game, playtype);
if (insights.length === 0) {
return (
<div className="text-center w-100">
{FormatGame(game, playtype)} has no insights. How'd you get here!
</div>
);
}
return (
<Row>
<Switch>
{insights.map((tool) => (
<Route
key={tool.urlPath}
exact
path={`/u/:userID/games/:game/:playtype/insights/${tool.urlPath}`}
>
<Col xs={12} className="mt-4">
<Card
header={tool.name}
footer={
<Link
to={`/u/${reqUser.username}/games/${game}/${playtype}/insights`}
className="text-muted text-hover-white"
>
&lt; Back to all insights...
</Link>
}
>
{tool.description}
</Card>
<Divider />
</Col>
<Col xs={12}>{tool.component({ reqUser, game, playtype })}</Col>
</Route>
))}
<Route exact path="/u/:userID/games/:game/:playtype/insights/">
{insights.map((tool) => (
<Col key={tool.urlPath} xs={12} className="my-4" lg={6}>
<Card
header={tool.name}
footer={
<div className="d-flex w-100 justify-content-end">
<LinkButton to={`insights/${tool.urlPath}`}>
View
</LinkButton>
</div>
}
>
{tool.description}
</Card>
</Col>
))}
</Route>
</Switch>
</Row>
);
}
@@ -1,6 +1,6 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import { GetGPTTools } from "components/tools/Tools";
import { GetGPTTools } from "components/tools-insights/ToolsAndInsights";
import Divider from "components/util/Divider";
import LinkButton from "components/util/LinkButton";
import React from "react";
+4
View File
@@ -32,6 +32,7 @@ import { UGPTStatsReturn } from "types/api-returns";
import UserImportsPage from "app/pages/dashboard/users/UserImportsPage";
import { TargetsContextProvider } from "context/TargetsContext";
import ToolsPage from "app/pages/dashboard/users/games/_game/_playtype/tools/ToolsPage";
import InsightsPage from "app/pages/dashboard/users/games/_game/_playtype/insights/InsightsPage";
import ScoresPage from "../pages/dashboard/users/games/_game/_playtype/ScoresPage";
import UserPage from "../pages/dashboard/users/UserPage";
@@ -267,6 +268,9 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: UserDocument; game
<Route path="/u/:userID/games/:game/:playtype/tools">
<ToolsPage reqUser={reqUser} game={game} playtype={playtype} />
</Route>
<Route path="/u/:userID/games/:game/:playtype/insights">
<InsightsPage reqUser={reqUser} game={game} playtype={playtype} />
</Route>
<RequireAuthAsUserParam>
<Route exact path="/u/:userID/games/:game/:playtype/settings">
<UGPTSettingsPage reqUser={reqUser} game={game} playtype={playtype} />
@@ -0,0 +1,67 @@
import { Game, GetIDString, IDStrings, Playtype } from "tachi-common";
import { GPTToolOrInsight } from "types/ugpt";
import { BMSCustomTablesTool } from "./tools/BMSCustomTablesTool";
import { IIDXEamusementExportTool } from "./tools/IIDXEamusementExportTool";
import { IIDXPlaylistsTool } from "./tools/IIDXPlaylistsTool";
import { JubilityBreakdownInsight } from "./insights/JubilityBreakdownInsight";
// A tool is something that users can use, like exporting to a specific format
// or having things integrate with the game in a neat way.
// Tools are **NOT** displayed on someone elses profile. For that, you want
// Insights (Fancy score renderers, basically).
// What tools does each game support?
const GPT_TOOLS: Record<IDStrings, Array<GPTToolOrInsight>> = {
"bms:7K": [BMSCustomTablesTool],
"bms:14K": [BMSCustomTablesTool],
"chunithm:Single": [],
"gitadora:Dora": [],
"gitadora:Gita": [],
"iidx:DP": [IIDXEamusementExportTool, IIDXPlaylistsTool],
"iidx:SP": [IIDXEamusementExportTool, IIDXPlaylistsTool],
"itg:Stamina": [],
"jubeat:Single": [],
"museca:Single": [],
"pms:Controller": [],
"pms:Keyboard": [],
"popn:9B": [],
"sdvx:Single": [],
"usc:Controller": [],
"usc:Keyboard": [],
"wacca:Single": [],
};
// An insight is a page for a unique kind of data viz that tachi (generically) does not
// pick up. Good examples of this would be something like viewing what goes into your
// jubility.
const GPT_INSIGHTS: Record<IDStrings, Array<GPTToolOrInsight>> = {
"bms:7K": [],
"bms:14K": [],
"chunithm:Single": [],
"gitadora:Dora": [],
"gitadora:Gita": [],
"iidx:DP": [],
"iidx:SP": [],
"itg:Stamina": [],
"jubeat:Single": [JubilityBreakdownInsight],
"museca:Single": [],
"pms:Controller": [],
"pms:Keyboard": [],
"popn:9B": [],
"sdvx:Single": [],
"usc:Controller": [],
"usc:Keyboard": [],
"wacca:Single": [],
};
export function GetGPTTools(game: Game, playtype: Playtype) {
const idString = GetIDString(game, playtype);
return GPT_TOOLS[idString];
}
export function GetGPTInsights(game: Game, playtype: Playtype) {
const idString = GetIDString(game, playtype);
return GPT_INSIGHTS[idString];
}
@@ -0,0 +1,123 @@
import { CreateChartMap } from "util/data";
import { NumericSOV } from "util/sorts";
import Card from "components/layout/page/Card";
import PBTable from "components/tables/pbs/PBTable";
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 usePreferredRanking from "components/util/usePreferredRanking";
import React from "react";
import { Col, Row } from "react-bootstrap";
import { ChartDocument, CreateSongMap, PBScoreDocument, SongDocument } from "tachi-common";
import { UGPT } from "types/react";
import { PBDataset } from "types/tables";
import { GPTToolOrInsight } from "types/ugpt";
function Component({ game, playtype, reqUser }: UGPT) {
const { data, error } = useApiQuery<{
songs: Array<SongDocument>;
charts: Array<ChartDocument>;
pickUp: Array<PBScoreDocument>;
other: Array<PBScoreDocument>;
}>(`/users/${reqUser.id}/games/${game}/${playtype}/jubility`);
const preferredRanking = usePreferredRanking();
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const songMap = CreateSongMap(data.songs);
const chartMap = CreateChartMap(data.charts);
const pickUpDataset: PBDataset = [];
const normalDataset: PBDataset = [];
const sortedJubilities = [...data.pickUp, ...data.other]
.map((e) => e.calculatedData.jubility)
.sort(NumericSOV((x) => x ?? -Infinity, true));
for (const pb of data.pickUp) {
const song = songMap.get(pb.songID);
const chart = chartMap.get(pb.chartID);
if (!song) {
continue;
}
if (!chart) {
continue;
}
pickUpDataset.push({
...pb,
__related: {
chart,
song,
index: sortedJubilities.indexOf(pb.calculatedData.jubility),
},
});
}
for (const pb of data.other) {
const song = songMap.get(pb.songID);
const chart = chartMap.get(pb.chartID);
if (!song) {
continue;
}
if (!chart) {
continue;
}
normalDataset.push({
...pb,
__related: {
chart,
song,
index: sortedJubilities.indexOf(pb.calculatedData.jubility),
},
});
}
return (
<Row>
<Col xs={12}>
<Card header="Pick-Up">
<PBTable
game={game}
playtype={playtype}
dataset={pickUpDataset}
defaultRankingViewMode={preferredRanking}
indexCol
alg="jubility"
/>
</Card>
<Divider />
<Card header="Others">
<PBTable
game={game}
playtype={playtype}
dataset={normalDataset}
defaultRankingViewMode={preferredRanking}
indexCol
alg="jubility"
/>
</Card>
</Col>
</Row>
);
}
export const JubilityBreakdownInsight: GPTToolOrInsight = {
name: "Jubility Breakdown",
urlPath: "jubility",
description: `See what PBs are going into profile jubility!`,
component: Component,
};
@@ -8,7 +8,7 @@ import { TachiConfig } from "lib/config";
import React from "react";
import { Col, Row } from "react-bootstrap";
import { UGPT } from "types/react";
import { GPTTool } from "types/ugpt";
import { GPTToolOrInsight } from "types/ugpt";
import Icon from "components/util/Icon";
import Divider from "components/util/Divider";
@@ -66,7 +66,7 @@ function Component({ game, playtype, reqUser }: UGPT) {
);
}
export const BMSCustomTablesTool: GPTTool = {
export const BMSCustomTablesTool: GPTToolOrInsight = {
name: `${TachiConfig.name} BMS Tables`,
urlPath: "custom-tables",
description: `${TachiConfig.name} has its own BMS tables that you can use in-game!`,
@@ -8,7 +8,7 @@ import { TachiConfig } from "lib/config";
import React from "react";
import { Alert, Button, Col, Row } from "react-bootstrap";
import { UGPT } from "types/react";
import { GPTTool } from "types/ugpt";
import { GPTToolOrInsight } from "types/ugpt";
function Component({ game, playtype, reqUser }: UGPT) {
const { data, error } = useApiQuery<string>(
@@ -60,7 +60,7 @@ function Component({ game, playtype, reqUser }: UGPT) {
);
}
export const IIDXEamusementExportTool: GPTTool = {
export const IIDXEamusementExportTool: GPTToolOrInsight = {
name: `e-amusement CSV Export`,
urlPath: "eam-csv-export",
description: `Export your ${TachiConfig.name} scores into e-amusement format.`,
@@ -8,7 +8,7 @@ import { TachiConfig } from "lib/config";
import React from "react";
import { Col, Row } from "react-bootstrap";
import { UGPT } from "types/react";
import { GPTTool } from "types/ugpt";
import { GPTToolOrInsight } from "types/ugpt";
import Icon from "components/util/Icon";
import Divider from "components/util/Divider";
@@ -76,7 +76,7 @@ function Component({ game, playtype, reqUser }: UGPT) {
);
}
export const IIDXPlaylistsTool: GPTTool = {
export const IIDXPlaylistsTool: GPTToolOrInsight = {
name: `${TachiConfig.name} IIDX Playlists`,
urlPath: "custom-tables",
description: `${TachiConfig.name} has its own IIDX playlists that you can use in-game via playlister!`,
-37
View File
@@ -1,37 +0,0 @@
import { Game, GetIDString, IDStrings, Playtype } from "tachi-common";
import { GPTTool } from "types/ugpt";
import { BMSCustomTablesTool } from "./BMSCustomTablesTool";
import { IIDXEamusementExportTool } from "./IIDXEamusementExportTool";
import { IIDXPlaylistsTool } from "./IIDXPlaylistsTool";
// A tool is something that users can use, like exporting to a specific format
// or having things integrate with the game in a neat way.
// Tools are **NOT** displayed on someone elses profile. For that, you want
// Insights (Fancy score renderers, basically).
// What tools does each game support?
const GPT_TOOLS: Record<IDStrings, Array<GPTTool>> = {
"bms:7K": [BMSCustomTablesTool],
"bms:14K": [BMSCustomTablesTool],
"chunithm:Single": [],
"gitadora:Dora": [],
"gitadora:Gita": [],
"iidx:DP": [IIDXEamusementExportTool, IIDXPlaylistsTool],
"iidx:SP": [IIDXEamusementExportTool, IIDXPlaylistsTool],
"itg:Stamina": [],
"jubeat:Single": [],
"museca:Single": [],
"pms:Controller": [],
"pms:Keyboard": [],
"popn:9B": [],
"sdvx:Single": [],
"usc:Controller": [],
"usc:Keyboard": [],
"wacca:Single": [],
};
export function GetGPTTools(game: Game, playtype: Playtype) {
const idString = GetIDString(game, playtype);
return GPT_TOOLS[idString];
}
+14 -4
View File
@@ -10,11 +10,10 @@ import React, { useContext } from "react";
import { Button } from "react-bootstrap";
import { Game, Playtype, UserDocument } from "tachi-common";
import { UGPTStatsReturn } from "types/api-returns";
import { UserContext } from "context/UserContext";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import { UserSettingsContext } from "context/UserSettingsContext";
import FollowUserButton from "components/util/FollowUserButton";
import { GetGPTTools } from "components/tools/Tools";
import { GetGPTInsights, GetGPTTools } from "components/tools-insights/ToolsAndInsights";
import ProfileBadges from "./ProfileBadges";
import ProfilePicture from "./ProfilePicture";
import RankingData from "./UGPTRankingData";
@@ -201,10 +200,21 @@ export function UGPTBottomNav({
<NavItem key="sessions" to={`${baseUrl}/sessions`}>
Sessions
</NavItem>,
];
if (GetGPTInsights(game, playtype).length > 0) {
navItems.push(
<NavItem key="insights" to={`${baseUrl}/insights`}>
Insights
</NavItem>
);
}
navItems.push(
<NavItem key="leaderboard" to={`${baseUrl}/leaderboard`}>
Leaderboard
</NavItem>,
];
</NavItem>
);
if (isRequestedUser) {
navItems.push(
+1 -1
View File
@@ -1,7 +1,7 @@
import React from "react";
import { UGPT } from "./react";
export interface GPTTool {
export interface GPTToolOrInsight {
urlPath: string;
name: string;
description: React.ReactChild;
@@ -390,12 +390,7 @@ async function GetBestJubilityOnSongs(
const CURRENT_JUBEAT_HOT_VERSION: GPTSupportedVersions["jubeat:Single"] = "festo";
async function CalculateJubility(
game: Game,
playtype: Playtype,
userID: integer,
_logger: KtLogger
): Promise<number> {
export async function GetPBsForJubility(userID: integer) {
const hotSongs = await db.songs.jubeat.find(
{ "data.displayVersion": CURRENT_JUBEAT_HOT_VERSION },
{ projection: { id: 1 } }
@@ -415,6 +410,17 @@ async function CalculateJubility(
GetBestJubilityOnSongs(coldSongIDs, userID, "jubeat", "Single", 30),
]);
return { bestHotScores, bestScores };
}
async function CalculateJubility(
game: Game,
playtype: Playtype,
userID: integer,
_logger: KtLogger
): Promise<number> {
const { bestHotScores, bestScores } = await GetPBsForJubility(userID);
let jubility = 0;
jubility = jubility + bestHotScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0);
@@ -0,0 +1,35 @@
import { Router } from "express";
import { GetPBsForJubility } from "lib/score-import/framework/user-game-stats/rating";
import { GetRelevantSongsAndCharts } from "utils/db";
import { GetUser } from "utils/req-tachi-data";
const router: Router = Router({ mergeParams: true });
/**
* Retrieve the PBs that went into this users jubility ranking.
*
* @name GET /api/v1/users/:userID/games/jubeat/Single/jubility
*/
router.get("/Single/jubility", async (req, res) => {
const user = GetUser(req);
const { bestHotScores, bestScores } = await GetPBsForJubility(user.id);
const { songs, charts } = await GetRelevantSongsAndCharts(
[...bestHotScores, ...bestScores],
"jubeat"
);
return res.status(200).json({
success: true,
description: `Retrieved scores that went into this users jubility.`,
body: {
songs,
charts,
pickUp: bestHotScores,
other: bestScores,
},
});
});
export default router;
@@ -4,11 +4,13 @@
import bmsRouter from "./bms/router";
import iidxRouter from "./iidx/router";
import jubeatRouter from "./jubeat/router";
import { Router } from "express";
const router: Router = Router({ mergeParams: true });
router.use("/bms", bmsRouter);
router.use("/iidx", iidxRouter);
router.use("/jubeat", jubeatRouter);
export default router;