Skip to content

Commit

Permalink
feat(ongeki): graphs (#1214)
Browse files Browse the repository at this point in the history
* feat: implement graphs

* fix: remove a dbg field

* fix: implicit anys

I don't know why it doesn't complain about other files.
I don't know why the server check failed. Works on my machine.
  • Loading branch information
nyairobi authored Dec 10, 2024
1 parent 469de78 commit 4dc26a4
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 0 deletions.
139 changes: 139 additions & 0 deletions client/src/components/charts/OngekiScoreChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { TACHI_LINE_THEME } from "util/constants/chart-theme";
import React from "react";
import { ResponsiveLine, Serie } from "@nivo/line";
import { COLOUR_SET } from "tachi-common";
import ChartTooltip from "./ChartTooltip";

const formatTime = (s: number) =>
`${Math.floor(s / 60)
.toString()
.padStart(2, "0")}:${Math.floor(s % 60)
.toString()
.padStart(2, "0")}`;

const scoreToLamp = (s: number) => {
switch (s) {
case 970000:
return "S";
case 990000:
return "SS";
case 1000000:
return "SSS";
case 1007500:
return "SSS+";
}
return "";
};

const typeSpecificParams = (t: "Score" | "Bells" | "Life", maxBells: number) => {
const minBells = Math.min(-maxBells, -1);
switch (t) {
case "Score":
return {
yScale: { type: "linear", min: 970000, max: 1010000 },
yFormat: ">-,.0f",
axisLeft: {
tickValues: [970000, 990000, 1000000, 1007500, 1010000],
format: scoreToLamp,
},
gridYValues: [970000, 980000, 990000, 1000000, 1007500, 1010000],
colors: COLOUR_SET.blue,
areaBaselineValue: 970000,
tooltip: (d: any) => (
<ChartTooltip>
{d.point.data.y === 970000 ? "≤ " : ""}
{d.point.data.yFormatted} @ {formatTime(d.point.data.x)}
</ChartTooltip>
),
};
case "Bells":
return {
yScale: { type: "linear", min: minBells, max: 0, stacked: false },
enableGridY: false,
axisLeft: { format: (e: number) => Math.floor(e) === e && e },
colors: COLOUR_SET.vibrantYellow,
areaBaselineValue: minBells,
tooltip: (d: any) => (
<ChartTooltip>
MAX{d.point.data.y === 0 ? "" : d.point.data.y} @{" "}
{formatTime(d.point.data.x)}
</ChartTooltip>
),
};
case "Life":
return {
colors: COLOUR_SET.green,
enableGridY: false,
yScale: { type: "linear", min: 0, max: 100 },
axisLeft: { format: (d: number) => `${d}%` },
areaBaselineValue: 0,
tooltip: (d: any) => (
<ChartTooltip>
{d.point.data.y}% @ {formatTime(d.point.data.x)}
</ChartTooltip>
),
};
default:
return {};
}
};

export default function OngekiScoreChart({
width = "100%",
height = "100%",
mobileHeight = "100%",
mobileWidth = width,
type,
maxBells,
data,
}: {
mobileHeight?: number | string;
mobileWidth?: number | string;
width?: number | string;
height?: number | string;
type: "Score" | "Bells" | "Life";
maxBells: number;
data: Serie[];
} & ResponsiveLine["props"]) {
const realData =
type === "Score"
? [
{
id: "Score",
data: data[0].data.map(({ x, y }) => ({
x,
y: y && y < 970000 ? 970000 : y,
})),
},
]
: data;
const component = (
<ResponsiveLine
data={realData}
margin={{ top: 30, bottom: 50, left: 50, right: 50 }}
xScale={{ type: "linear", min: 0, max: data[0].data.length }}
motionConfig="stiff"
crosshairType="x"
enablePoints={false}
useMesh={true}
enableGridX={false}
theme={TACHI_LINE_THEME}
axisBottom={{ format: (d: number) => formatTime(d) }}
curve="linear"
legends={[]}
enableArea
{...(typeSpecificParams(type, maxBells) as any)}
/>
);

return (
<>
<div className="d-block d-md-none" style={{ height: mobileHeight, width: mobileWidth }}>
{component}
</div>
<div className="d-none d-md-block" style={{ height, width }}>
{component}
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BMSGraphsComponent } from "./components/BMSScoreDropdownParts";
import { IIDXGraphsComponent } from "./components/IIDXScoreDropdownParts";
import { ITGGraphsComponent } from "./components/ITGScoreDropdownParts";
import { JubeatGraphsComponent } from "./components/JubeatScoreDropdownParts";
import { OngekiGraphsComponent } from "./components/OngekiScoreDropdownParts";

export function GPTDropdownSettings(game: Game, playtype: Playtype): any {
if (game === "iidx") {
Expand All @@ -27,6 +28,11 @@ export function GPTDropdownSettings(game: Game, playtype: Playtype): any {
renderScoreInfo: true,
GraphComponent: JubeatGraphsComponent as any,
};
} else if (game === "ongeki") {
return {
renderScoreInfo: true,
GraphComponent: OngekiGraphsComponent as any,
};
}

return {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import SelectNav from "components/util/SelectNav";
import React, { useState } from "react";
import { Nav } from "react-bootstrap";
import { PBScoreDocument, ScoreData, ScoreDocument } from "tachi-common";
import OngekiScoreChart from "components/charts/OngekiScoreChart";

type ChartTypes = "Score" | "Bells" | "Life";

export function OngekiGraphsComponent({
score,
}: {
score: ScoreDocument<"ongeki:Single"> | PBScoreDocument<"ongeki:Single">;
}) {
const [chart, setChart] = useState<ChartTypes>("Score");
const available =
score.scoreData.optional.scoreGraph &&
score.scoreData.optional.bellGraph &&
score.scoreData.optional.lifeGraph &&
score.scoreData.optional.totalBellCount !== null &&
score.scoreData.optional.totalBellCount !== undefined;

return (
<>
<div className="col-12 d-flex justify-content-center">
<Nav variant="pills">
<SelectNav id="Score" value={chart} setValue={setChart} disabled={!available}>
Score
</SelectNav>
<SelectNav id="Bells" value={chart} setValue={setChart} disabled={!available}>
Bells
</SelectNav>
<SelectNav id="Life" value={chart} setValue={setChart} disabled={!available}>
Life
</SelectNav>
</Nav>
</div>
<div className="col-12">
{available ? (
<GraphComponent type={chart} scoreData={score.scoreData} />
) : (
"No charts available"
)}
</div>
</>
);
}

function GraphComponent({
type,
scoreData,
}: {
type: ChartTypes;
scoreData: ScoreData<"ongeki:Single">;
}) {
const values =
type === "Score"
? scoreData.optional.scoreGraph!
: type === "Bells"
? scoreData.optional.bellGraph!
: scoreData.optional.lifeGraph!;
return (
<OngekiScoreChart
height="360px"
mobileHeight="175px"
type={type}
maxBells={scoreData.optional.totalBellCount!}
data={[
{
id: type,
data: values.map((e, i) => ({ x: i, y: e })),
},
]}
/>
);
}
16 changes: 16 additions & 0 deletions common/src/config/game-support/ongeki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@ export const ONGEKI_SINGLE_CONF = {
description: "The Platinum Score value. Only exists in MASTER and LUNATIC charts.",
partOfScoreID: true,
},
scoreGraph: {
type: "NULLABLE_GRAPH",
validate: p.isBetween(0, 1010000),
description: "The history of the projected score, queried in one-second intervals.",
},
bellGraph: {
type: "NULLABLE_GRAPH",
validate: p.isBetween(-10000, 0),
description:
"The history of the number of bells missed, queried in one-second intervals.",
},
lifeGraph: {
type: "NULLABLE_GRAPH",
validate: p.isBetween(0, 100),
description: "The life gauge history, queried in one-second intervals.",
},
},

scoreRatingAlgs: {
Expand Down

0 comments on commit 4dc26a4

Please sign in to comment.