mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-09 07:09:57 +03:00
feat: O.N.G.E.K.I. Re:Fresh rating algorithm (#1263)
* feat: Re:Fresh support, rebased for the 6th time * fix: migration * fix: seeds * fix: add tests and make them work * fix: misc corrections * rebased * chore: bump rg-stats * maybe a better migration * attempt manual lint * fix: star formatters * fix: re-add name overrides * rename them again, more consistently * revert rbreak * finish renaming * i may be stupid * refresh insights * insight fixes * fix: re-add platinum graphs * delete score._id * fix: server test * fix: tiebreaks * fix: what * fix: don't kill p-score if migration is reapplied I wonder why this could be needed * fix: maybe notify about any errors * docs: redefine starRating * docs: typo * fix(client): shrink graphs when necessary * fix(client): use compact starfield in the insight * feat(client): separate column for hits * fix(client): add vertical-align * fix: validate p-score properly * fix: recolor AB+ to blue, actually * fix(client): insight readability improvements * feat(client): nicer insight lamps * feat(client): OngekiScoreRatingCell * fix(client): use OngekiScoreRatingCell in insight --------- Co-authored-by: zk <ktchidev@gmail.com>
This commit is contained in:
@@ -107,33 +107,53 @@ const strokeColor = (
|
||||
return `hsl(0, 0%, ${isLight ? 35 : 67}%)`;
|
||||
};
|
||||
|
||||
const limitScoreGraph = (game: Game, data: Serie[]) => {
|
||||
const limitScoreGraph = (game: Game, data: Serie[], minval: number) => {
|
||||
for (const val of data[0].data) {
|
||||
if (typeof val.y !== "number") {
|
||||
break;
|
||||
}
|
||||
if (game === "ongeki" && val.y < 970_000) {
|
||||
// 969999 will be used to represent values below S
|
||||
if (val.y < minval) {
|
||||
// minval - 0.001 will be used to represent values below S
|
||||
// Without this, the line would cross the bottom axis
|
||||
// which looks very bad
|
||||
val.y = 969_999;
|
||||
}
|
||||
if (game === "chunithm" && val.y < 990_000) {
|
||||
val.y = 989_999;
|
||||
}
|
||||
if (game === "maimaidx" && val.y < 97) {
|
||||
val.y = 96.9999;
|
||||
val.y = minval - 0.001;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const bellFloor = (data: Datum[], totalBellCount: number) => {
|
||||
const scaleFloor = (data: Datum[], maximum: number | undefined) => {
|
||||
if (maximum === undefined) {
|
||||
return 0;
|
||||
}
|
||||
// Scale the chart so that it drops by 3/4, unless it's a flatline
|
||||
const lowestValue = Number(data.filter((v) => v.y !== null).pop()?.y ?? 0);
|
||||
return lowestValue === 0
|
||||
? -totalBellCount
|
||||
: clamp(-totalBellCount, Math.floor(lowestValue * 1.333), -1);
|
||||
return lowestValue === 0 ? -maximum : clamp(-maximum, Math.floor(lowestValue * 1.333), -1);
|
||||
};
|
||||
|
||||
const platinumTooltip = (d: Datum, starValues: number[]) => {
|
||||
if (d.point.data.y < starValues[0]) {
|
||||
return (
|
||||
<>
|
||||
{"< 1★"} @ {formatTime(d.point.data.x)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
for (let i = 1; i < 6; ++i) {
|
||||
if (d.point.data.y < starValues[i]) {
|
||||
return (
|
||||
<>
|
||||
MAX{d.point.data.y} ({i < 5 ? i + 1 : "虹"}
|
||||
{"★"}-{starValues[i] - d.point.data.y}) @ {formatTime(d.point.data.x)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
MAX{d.point.data.y === 0 ? "" : d.point.data.y} @ {formatTime(d.point.data.x)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function GekichumaiScoreChart({
|
||||
@@ -143,7 +163,7 @@ export default function GekichumaiScoreChart({
|
||||
mobileWidth = width,
|
||||
type,
|
||||
difficulty,
|
||||
totalBellCount,
|
||||
maximumAbsoluteValue,
|
||||
data,
|
||||
game,
|
||||
duration,
|
||||
@@ -152,12 +172,12 @@ export default function GekichumaiScoreChart({
|
||||
mobileWidth?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
type: "Score" | "Bells" | "Life";
|
||||
type: "Score" | "Platinum" | "Bells" | "Life";
|
||||
difficulty:
|
||||
| Difficulties["ongeki:Single"]
|
||||
| Difficulties["chunithm:Single"]
|
||||
| Difficulties["maimaidx:Single"];
|
||||
totalBellCount?: number;
|
||||
maximumAbsoluteValue?: number;
|
||||
data: Serie[];
|
||||
game: "chunithm" | "ongeki" | "maimaidx";
|
||||
duration: number;
|
||||
@@ -183,12 +203,16 @@ export default function GekichumaiScoreChart({
|
||||
}
|
||||
} else if (type === "Bells") {
|
||||
color = COLOUR_SET.vibrantYellow;
|
||||
} else {
|
||||
} else if (type === "Life") {
|
||||
color = COLOUR_SET.vibrantGreen;
|
||||
} else {
|
||||
color = COLOUR_SET.white;
|
||||
}
|
||||
|
||||
const gradientId = type === "Score" ? difficulty : type;
|
||||
|
||||
data.length = Math.min(data.length, duration + 1);
|
||||
|
||||
const commonProps: Omit<LineSvgProps, "data"> = {
|
||||
margin: { top: 30, bottom: 50, left: 50, right: 50 },
|
||||
enableGridX: false,
|
||||
@@ -224,21 +248,23 @@ export default function GekichumaiScoreChart({
|
||||
component = (
|
||||
<ResponsiveLine
|
||||
{...commonProps}
|
||||
data={limitScoreGraph(game, data)}
|
||||
yScale={{ type: "linear", min: 970000, max: 1010000 }}
|
||||
data={limitScoreGraph(game, data, 970_000)}
|
||||
yScale={{ type: "linear", min: 970_000, max: 1010_000 }}
|
||||
yFormat={">-,.0f"}
|
||||
axisLeft={{
|
||||
tickValues: [970000, 990000, 1000000, 1007500, 1010000],
|
||||
tickValues: [970_000, 990_000, 1000_000, 1007_500, 1010_000],
|
||||
format: getScoreYAxisNotch(game),
|
||||
}}
|
||||
gridYValues={[970000, 980000, 990000, 1000000, 1007500, 1010000]}
|
||||
gridYValues={[970_000, 980_000, 990_000, 1000_000, 1007_500, 1010_000]}
|
||||
enableGridY={true}
|
||||
colors={strokeColor(difficulty)}
|
||||
areaBaselineValue={970000}
|
||||
tooltip={(d: PointTooltipProps) => (
|
||||
<ChartTooltip>
|
||||
{d.point.data.y === 969999 ? "< 970,000 " : d.point.data.yFormatted}@{" "}
|
||||
{formatTime(d.point.data.x)}
|
||||
{typeof d.point.data.y === "number" && d.point.data.y < 970_000
|
||||
? "< 970,000 "
|
||||
: d.point.data.yFormatted}
|
||||
@ {formatTime(d.point.data.x)}
|
||||
</ChartTooltip>
|
||||
)}
|
||||
/>
|
||||
@@ -247,7 +273,7 @@ export default function GekichumaiScoreChart({
|
||||
component = (
|
||||
<ResponsiveLine
|
||||
{...commonProps}
|
||||
data={limitScoreGraph(game, data)}
|
||||
data={limitScoreGraph(game, data, 990_000)}
|
||||
yScale={{ type: "linear", min: 990_000, max: 1010_000 }}
|
||||
yFormat={">-,.0f"}
|
||||
axisLeft={{
|
||||
@@ -260,8 +286,10 @@ export default function GekichumaiScoreChart({
|
||||
areaBaselineValue={990000}
|
||||
tooltip={(d: PointTooltipProps) => (
|
||||
<ChartTooltip>
|
||||
{d.point.data.y === 989_999 ? "< 990,000 " : d.point.data.yFormatted}@{" "}
|
||||
{formatTime(d.point.data.x)}
|
||||
{typeof d.point.data.y === "number" && d.point.data.y < 990_000
|
||||
? "< 990,000 "
|
||||
: d.point.data.yFormatted}
|
||||
@ {formatTime(d.point.data.x)}
|
||||
</ChartTooltip>
|
||||
)}
|
||||
/>
|
||||
@@ -270,7 +298,7 @@ export default function GekichumaiScoreChart({
|
||||
component = (
|
||||
<ResponsiveLine
|
||||
{...commonProps}
|
||||
data={limitScoreGraph(game, data)}
|
||||
data={limitScoreGraph(game, data, 97)}
|
||||
yScale={{ type: "linear", min: 97, max: 101 }}
|
||||
yFormat={">-,.4f"}
|
||||
axisLeft={{
|
||||
@@ -283,7 +311,9 @@ export default function GekichumaiScoreChart({
|
||||
areaBaselineValue={97}
|
||||
tooltip={(d: PointTooltipProps) => (
|
||||
<ChartTooltip>
|
||||
{d.point.data.y === 96.9999 ? "< 97% " : `${d.point.data.yFormatted}% `}
|
||||
{typeof d.point.data.y === "number" && d.point.data.y < 97
|
||||
? "< 97% "
|
||||
: `${d.point.data.yFormatted}% `}
|
||||
@ {formatTime(d.point.data.x)}
|
||||
</ChartTooltip>
|
||||
)}
|
||||
@@ -297,14 +327,14 @@ export default function GekichumaiScoreChart({
|
||||
data={data}
|
||||
yScale={{
|
||||
type: "linear",
|
||||
min: bellFloor(data[0].data, totalBellCount!),
|
||||
min: scaleFloor(data[0].data, maximumAbsoluteValue),
|
||||
max: 0,
|
||||
stacked: false,
|
||||
}}
|
||||
enableGridY={false}
|
||||
axisLeft={{ format: (e: number) => Math.floor(e) === e && e }}
|
||||
colors={strokeColor("BELLS")}
|
||||
areaBaselineValue={bellFloor(data[0].data, totalBellCount!)}
|
||||
areaBaselineValue={scaleFloor(data[0].data, maximumAbsoluteValue)}
|
||||
tooltip={(d: PointTooltipProps) => (
|
||||
<ChartTooltip>
|
||||
MAX{d.point.data.y === 0 ? "" : d.point.data.y} @{" "}
|
||||
@@ -333,6 +363,45 @@ export default function GekichumaiScoreChart({
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (type === "Platinum") {
|
||||
const mav = maximumAbsoluteValue ?? 0; // shorthand
|
||||
const starValues = [
|
||||
Math.floor(mav * 0.94) - mav,
|
||||
Math.floor(mav * 0.95) - mav,
|
||||
Math.floor(mav * 0.96) - mav,
|
||||
Math.floor(mav * 0.97) - mav,
|
||||
Math.floor(mav * 0.98) - mav,
|
||||
Math.floor(mav * 0.99) - mav,
|
||||
];
|
||||
component = (
|
||||
<ResponsiveLine
|
||||
{...commonProps}
|
||||
data={limitScoreGraph(game, data, starValues[0])}
|
||||
yScale={{ type: "linear", min: starValues[0], max: 0 }}
|
||||
yFormat={">-,.0f"}
|
||||
axisLeft={{
|
||||
tickValues: starValues,
|
||||
format: (count: number) => {
|
||||
if (count === starValues[5]) {
|
||||
return "虹★";
|
||||
}
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
if (count === starValues[i]) {
|
||||
return `${i + 1}★`;
|
||||
}
|
||||
}
|
||||
return "Error";
|
||||
},
|
||||
}}
|
||||
gridYValues={starValues}
|
||||
enableGridY={true}
|
||||
colors={strokeColor("LUNATIC")}
|
||||
areaBaselineValue={starValues[0]}
|
||||
tooltip={(d: PointTooltipProps) => (
|
||||
<ChartTooltip>{platinumTooltip(d, starValues)}</ChartTooltip>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,10 @@ import { BMSCustomTablesTool } from "./tools/BMSCustomTablesTool";
|
||||
import { IIDXEamusementExportTool } from "./tools/IIDXEamusementExportTool";
|
||||
import { IIDXPlaylistsTool } from "./tools/IIDXPlaylistsTool";
|
||||
import { JubilityBreakdownInsight } from "./insights/JubilityBreakdownInsight";
|
||||
import { ONGEKIBreakdownInsight } from "./insights/ONGEKIBreakdownInsight";
|
||||
import {
|
||||
ONGEKIClassicBreakdownInsight,
|
||||
ONGEKIRefreshBreakdownInsight,
|
||||
} from "./insights/ONGEKIBreakdownInsight";
|
||||
import { BMSSieglindeInfoTool } from "./tools/BMSSieglindeInfoTool";
|
||||
|
||||
// What utils does each game support?
|
||||
@@ -29,7 +32,7 @@ const GPT_UTILS: Record<GPTString, Array<GPTUtility>> = {
|
||||
"wacca:Single": [],
|
||||
"maimaidx:Single": [],
|
||||
"maimai:Single": [],
|
||||
"ongeki:Single": [ONGEKIBreakdownInsight],
|
||||
"ongeki:Single": [ONGEKIRefreshBreakdownInsight, ONGEKIClassicBreakdownInsight],
|
||||
"ddr:SP": [],
|
||||
"ddr:DP": [],
|
||||
};
|
||||
|
||||
@@ -9,13 +9,15 @@ import useApiQuery from "components/util/query/useApiQuery";
|
||||
import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import {
|
||||
AnyScoreRatingAlg,
|
||||
ChartDocument,
|
||||
COLOUR_SET,
|
||||
CreateSongMap,
|
||||
FmtNum,
|
||||
Game,
|
||||
GPTString,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
Playtype,
|
||||
SongDocument,
|
||||
} from "tachi-common";
|
||||
import { UGPT } from "types/react";
|
||||
@@ -24,8 +26,12 @@ import { GPTUtility } from "types/ugpt";
|
||||
import DifficultyCell from "components/tables/cells/DifficultyCell";
|
||||
import TachiTable from "components/tables/components/TachiTable";
|
||||
import GentleLink from "components/util/GentleLink";
|
||||
import Divider from "components/util/Divider";
|
||||
import { StarField } from "components/tables/cells/OngekiPlatinumCell";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import OngekiScoreRatingCell from "components/tables/cells/OngekiScoreRatingCell";
|
||||
|
||||
function Component({ game, playtype, reqUser }: UGPT) {
|
||||
function ComponentClassic({ game, playtype, reqUser }: UGPT) {
|
||||
if (game !== "ongeki" || playtype !== "Single") {
|
||||
return <></>;
|
||||
}
|
||||
@@ -43,54 +49,20 @@ function Component({ game, playtype, reqUser }: UGPT) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const songMap = CreateSongMap<"ongeki">(data.songs);
|
||||
const chartMap = CreateChartMap<"ongeki:Single">(data.charts);
|
||||
const flatDataset: PBDataset<"ongeki:Single"> = CreateFlatDataset(data, "rating", 45);
|
||||
const compoundDataset = ColumnMerge<"ongeki:Single">(flatDataset, 3);
|
||||
|
||||
const flatDataset: PBDataset<"ongeki:Single"> = [];
|
||||
const compoundDataset: PBDataset<"ongeki:Single">[] = [[]];
|
||||
|
||||
const sortedRatings = data.pbs
|
||||
.map((e) => e.calculatedData.rating)
|
||||
.sort(NumericSOV((x) => x ?? -Infinity, true));
|
||||
|
||||
for (const pb of data.pbs.slice(0, 45)) {
|
||||
const song = songMap.get(pb.songID);
|
||||
const chart = chartMap.get(pb.chartID);
|
||||
|
||||
if (song === undefined || chart === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flatDataset.push({
|
||||
...pb,
|
||||
__related: {
|
||||
chart,
|
||||
song,
|
||||
index: sortedRatings.indexOf(pb.calculatedData.rating),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const d of flatDataset) {
|
||||
const back = compoundDataset[compoundDataset.length - 1];
|
||||
back.push(d);
|
||||
if (back.length === 3) {
|
||||
compoundDataset.push([]);
|
||||
}
|
||||
}
|
||||
const classicRating =
|
||||
Math.floor(
|
||||
flatDataset.reduce((a, e) => a + Math.round((e.calculatedData.rating ?? 0) * 100), 0) /
|
||||
flatDataset.length
|
||||
) / 100;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<Card
|
||||
header={`${reqUser.username}'s Classic NaiveRating (${(
|
||||
Math.floor(
|
||||
flatDataset.reduce(
|
||||
(a, e) => a + (e.calculatedData.rating ?? 0) * 100,
|
||||
0
|
||||
) / flatDataset.length
|
||||
) / 100
|
||||
).toFixed(2)})`}
|
||||
header={`${reqUser.username}'s NaiveRatingClassic: ${classicRating.toFixed(2)}`}
|
||||
>
|
||||
<TachiTable
|
||||
dataset={compoundDataset}
|
||||
@@ -100,7 +72,143 @@ function Component({ game, playtype, reqUser }: UGPT) {
|
||||
noTopDisplayStr
|
||||
noBottomDisplayPager
|
||||
rowFunction={(pbs) => (
|
||||
<CompactRow pbs={pbs} game={game} playtype={playtype} />
|
||||
<CompactRow
|
||||
pbs={pbs}
|
||||
game={game}
|
||||
scoreField={(pb) => FormatMillions(pb.scoreData.score)}
|
||||
ratingField={(pb) => pb.calculatedData.rating?.toFixed(2) ?? "0.00"}
|
||||
lampField={(pb) => (
|
||||
<ShortLamp
|
||||
noteLamp={pb.scoreData.noteLamp}
|
||||
bellLamp={pb.scoreData.bellLamp}
|
||||
grade={pb.scoreData.grade}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function ComponentRefresh({ game, playtype, reqUser }: UGPT) {
|
||||
if (game !== "ongeki" || playtype !== "Single") {
|
||||
return <></>;
|
||||
}
|
||||
const query1 = useApiQuery<{
|
||||
pbs: Array<PBScoreDocument<"ongeki:Single">>;
|
||||
songs: Array<SongDocument<"ongeki">>;
|
||||
charts: Array<ChartDocument<"ongeki:Single">>;
|
||||
}>(`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best?alg=scoreRating`);
|
||||
|
||||
const query2 = useApiQuery<{
|
||||
pbs: Array<PBScoreDocument<"ongeki:Single">>;
|
||||
songs: Array<SongDocument<"ongeki">>;
|
||||
charts: Array<ChartDocument<"ongeki:Single">>;
|
||||
}>(`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best?alg=starRating`);
|
||||
|
||||
if (query1.error) {
|
||||
return <ApiError error={query1.error} />;
|
||||
}
|
||||
|
||||
if (query2.error) {
|
||||
return <ApiError error={query2.error} />;
|
||||
}
|
||||
|
||||
if (!query1.data || !query2.data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const flatDatasetScore: PBDataset<"ongeki:Single"> = CreateFlatDataset(
|
||||
query1.data,
|
||||
"scoreRating",
|
||||
60
|
||||
);
|
||||
const flatDatasetStar: PBDataset<"ongeki:Single"> = CreateFlatDataset(
|
||||
query2.data,
|
||||
"starRating",
|
||||
51
|
||||
);
|
||||
|
||||
const datasetScore = ColumnMerge<"ongeki:Single">(flatDatasetScore, 3);
|
||||
const datasetStar = ColumnMerge<"ongeki:Single">(flatDatasetStar, 3);
|
||||
|
||||
const scoreR1k = Math.floor(
|
||||
flatDatasetScore.reduce(
|
||||
(a, e) => a + Math.round((e.calculatedData.scoreRating ?? 0) * 1000),
|
||||
0
|
||||
) / 60
|
||||
);
|
||||
const starR1k = Math.floor(
|
||||
flatDatasetStar
|
||||
.slice(0, 50)
|
||||
.reduce((a, e) => a + Math.round((e.calculatedData.starRating ?? 0) * 1000), 0) / 50
|
||||
);
|
||||
const finalRating = ((Math.floor(scoreR1k * 1.2) + starR1k) / 1000.0).toFixed(3);
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs={12}>
|
||||
<Card
|
||||
header={`${reqUser.username}'s NaiveRatingRefresh: ${(scoreR1k / 1000).toFixed(
|
||||
3
|
||||
)} x 1.2 + ${(starR1k / 1000).toFixed(3)} = ${finalRating}`}
|
||||
>
|
||||
<TachiTable
|
||||
dataset={datasetScore}
|
||||
headers={[]}
|
||||
entryName="Errors"
|
||||
pageLen={100}
|
||||
noTopDisplayStr
|
||||
noBottomDisplayPager
|
||||
rowFunction={(pbs) => (
|
||||
<CompactRow
|
||||
pbs={pbs}
|
||||
game={game}
|
||||
scoreField={(pb) => FormatMillions(pb.scoreData.score)}
|
||||
ratingField={(pb) => <OngekiScoreRatingCell score={pb} />}
|
||||
lampField={(pb) => (
|
||||
<ShortLamp
|
||||
noteLamp={pb.scoreData.noteLamp}
|
||||
bellLamp={pb.scoreData.bellLamp}
|
||||
grade={pb.scoreData.grade}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Divider />
|
||||
<TachiTable
|
||||
dataset={datasetStar}
|
||||
headers={[]}
|
||||
entryName="Errors"
|
||||
pageLen={100}
|
||||
noTopDisplayStr
|
||||
noBottomDisplayPager
|
||||
rowFunction={(pbs) => (
|
||||
<CompactRow
|
||||
pbs={pbs}
|
||||
game={game}
|
||||
count={50}
|
||||
scoreField={(pb, chart) =>
|
||||
`${FmtNum(pb.scoreData.platinumScore)}/${FmtNum(
|
||||
chart.data.maxPlatScore
|
||||
)}`
|
||||
}
|
||||
ratingField={(pb) =>
|
||||
pb.calculatedData.starRating?.toFixed(3) ?? "0.000"
|
||||
}
|
||||
lampField={(pb) => (
|
||||
<>
|
||||
<StarField
|
||||
stars={pb.scoreData.platinumStars}
|
||||
compact={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
@@ -112,21 +220,42 @@ function Component({ game, playtype, reqUser }: UGPT) {
|
||||
function CompactRow({
|
||||
pbs,
|
||||
game,
|
||||
playtype,
|
||||
count,
|
||||
scoreField,
|
||||
ratingField,
|
||||
lampField,
|
||||
}: {
|
||||
pbs: PBDataset<"ongeki:Single">[0][];
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
count?: number;
|
||||
scoreField: (
|
||||
pb: PBScoreDocument<"ongeki:Single">,
|
||||
chart: ChartDocument<"ongeki:Single">
|
||||
) => JSX.Element | string;
|
||||
ratingField: (pb: PBScoreDocument<"ongeki:Single">) => JSX.Element | string;
|
||||
lampField: (pb: PBScoreDocument<"ongeki:Single">) => JSX.Element | string;
|
||||
}) {
|
||||
return (
|
||||
<tr>
|
||||
{pbs.map((pb) => {
|
||||
const index = pb.__related.index;
|
||||
const chart = pb.__related.chart;
|
||||
|
||||
if (count !== undefined && index > count) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<IndexCellCustom index={pb.__related.index} />
|
||||
<IndexCellCustom index={index} />
|
||||
<DifficultyCell alwaysShort game={game} chart={chart} />
|
||||
<CompactCell pb={pb} chart={chart} song={pb.__related.song} />
|
||||
<CompactCell
|
||||
pb={pb}
|
||||
chart={chart}
|
||||
song={pb.__related.song}
|
||||
scoreField={scoreField}
|
||||
ratingField={ratingField}
|
||||
lampField={lampField}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
@@ -138,19 +267,30 @@ function CompactCell({
|
||||
pb,
|
||||
chart,
|
||||
song,
|
||||
scoreField,
|
||||
ratingField,
|
||||
lampField,
|
||||
}: {
|
||||
pb: PBScoreDocument<"ongeki:Single">;
|
||||
chart: ChartDocument<"ongeki:Single">;
|
||||
song: SongDocument<"ongeki">;
|
||||
song: SongDocument;
|
||||
scoreField: (
|
||||
pb: PBScoreDocument<"ongeki:Single">,
|
||||
chart: ChartDocument<"ongeki:Single">
|
||||
) => JSX.Element | string;
|
||||
ratingField: (pb: PBScoreDocument<"ongeki:Single">) => JSX.Element | string;
|
||||
lampField: (pb: PBScoreDocument<"ongeki:Single">) => JSX.Element | string;
|
||||
}) {
|
||||
// Third-party scripts may find this useful
|
||||
const className = `c-${chart.data.inGameID}`;
|
||||
|
||||
return (
|
||||
<td style={{ width: "300px" }} className={className}>
|
||||
<div style={{ textAlign: "left" }}>
|
||||
<div style={{ textAlign: "left" }} className="d-flex flex-column gap-2">
|
||||
<div>
|
||||
<GentleLink to={CreateChartLink(chart, "ongeki")}>{song.title}</GentleLink>
|
||||
<GentleLink to={CreateChartLink(chart, "ongeki")}>
|
||||
<span style={{ fontSize: "120%" }}>{song.title}</span>
|
||||
</GentleLink>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
@@ -159,9 +299,9 @@ function CompactCell({
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<span>{FormatMillions(pb.scoreData.score)}</span>
|
||||
<span>{pb.calculatedData.rating?.toFixed(2)}</span>
|
||||
<span>{ShortLamp(pb.scoreData.noteLamp, pb.scoreData.bellLamp)}</span>
|
||||
<span>{scoreField(pb, chart)}</span>
|
||||
<strong style={{ fontSize: "105%" }}>{ratingField(pb)}</strong>
|
||||
<span>{lampField(pb)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -184,26 +324,123 @@ function IndexCellCustom({ index }: { index: integer }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ShortLamp(noteLamp: string, bellLamp: string) {
|
||||
let res = "";
|
||||
function ShortLamp({
|
||||
noteLamp,
|
||||
bellLamp,
|
||||
grade,
|
||||
}: {
|
||||
noteLamp: string;
|
||||
bellLamp: string;
|
||||
grade: "D" | "C" | "B" | "A" | "AA" | "AAA" | "S" | "SS" | "BB" | "BBB" | "SSS" | "SSS+";
|
||||
}) {
|
||||
let color1 = COLOUR_SET.gray;
|
||||
let text1 = "";
|
||||
let color2 = COLOUR_SET.gray;
|
||||
let text2 = "";
|
||||
const color3 = GPT_CLIENT_IMPLEMENTATIONS["ongeki:Single"].enumColours.grade[grade];
|
||||
|
||||
if (noteLamp === "ALL BREAK+") {
|
||||
return "ABFB+";
|
||||
}
|
||||
if (noteLamp === "ALL BREAK") {
|
||||
res = "AB";
|
||||
color1 = COLOUR_SET.vibrantBlue;
|
||||
text1 = "AB+";
|
||||
} else if (noteLamp === "ALL BREAK") {
|
||||
color1 = COLOUR_SET.white;
|
||||
text1 = "AB";
|
||||
} else if (noteLamp === "FULL COMBO") {
|
||||
res = "FC";
|
||||
color1 = COLOUR_SET.gold;
|
||||
text1 = "FC";
|
||||
}
|
||||
|
||||
if (bellLamp === "FULL BELL") {
|
||||
res += "FB";
|
||||
color2 = COLOUR_SET.gold;
|
||||
text2 = "FB";
|
||||
}
|
||||
return res;
|
||||
|
||||
const style = {
|
||||
width: "2em",
|
||||
height: "2em",
|
||||
fontSize: "90%",
|
||||
paddingTop: "3px",
|
||||
};
|
||||
|
||||
return (
|
||||
<span className="d-flex flex-row gap-1 text-center text-nowrap align-items-center">
|
||||
<span
|
||||
className="rounded-circle"
|
||||
style={{ ...style, backgroundColor: ChangeOpacity(color1, 0.2) }}
|
||||
>
|
||||
{text1}
|
||||
</span>
|
||||
<span
|
||||
className="rounded-circle"
|
||||
style={{ ...style, backgroundColor: ChangeOpacity(color2, 0.2) }}
|
||||
>
|
||||
{text2}
|
||||
</span>
|
||||
<span
|
||||
className="rounded-circle"
|
||||
style={{ ...style, backgroundColor: ChangeOpacity(color3, 0.2) }}
|
||||
>
|
||||
{grade}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export const ONGEKIBreakdownInsight: GPTUtility = {
|
||||
function CreateFlatDataset<T extends GPTString>(data: any, alg: AnyScoreRatingAlg, count: number) {
|
||||
const flatDataset: PBDataset<T> = [];
|
||||
|
||||
const songMap = CreateSongMap(data.songs);
|
||||
const chartMap = CreateChartMap<T>(data.charts);
|
||||
|
||||
const sortedRatings = data.pbs
|
||||
.map((e: PBScoreDocument) => e.calculatedData[alg])
|
||||
.sort(NumericSOV((x: number) => x ?? -Infinity, true));
|
||||
|
||||
for (const pb of data.pbs.slice(0, count)) {
|
||||
const song = songMap.get(pb.songID);
|
||||
const chart = chartMap.get(pb.chartID);
|
||||
|
||||
if (song === undefined || chart === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flatDataset.push({
|
||||
...pb,
|
||||
__related: {
|
||||
chart,
|
||||
song,
|
||||
index: sortedRatings.indexOf(pb.calculatedData[alg]),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return flatDataset;
|
||||
}
|
||||
|
||||
function ColumnMerge<T extends GPTString>(flatDataset: PBDataset<T>, columns: number) {
|
||||
const compoundDataset: PBDataset<T>[] = [[]];
|
||||
for (const d of flatDataset) {
|
||||
const back = compoundDataset[compoundDataset.length - 1];
|
||||
back.push(d);
|
||||
if (back.length === columns) {
|
||||
compoundDataset.push([]);
|
||||
}
|
||||
}
|
||||
return compoundDataset.filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
export const ONGEKIClassicBreakdownInsight: GPTUtility = {
|
||||
name: "O.N.G.E.K.I. Classic Rating Breakdown",
|
||||
urlPath: "rating",
|
||||
description: `See what PBs are going into your classic profile rating!`,
|
||||
component: Component,
|
||||
urlPath: "classic-rating",
|
||||
description: `See what PBs are going into your NaiveRatingClassic!`,
|
||||
component: ComponentClassic,
|
||||
personalUseOnly: false,
|
||||
};
|
||||
|
||||
export const ONGEKIRefreshBreakdownInsight: GPTUtility = {
|
||||
name: "O.N.G.E.K.I. Refresh Rating Breakdown",
|
||||
urlPath: "refresh-rating",
|
||||
description: `See what PBs are going into your NaiveRatingRefresh!`,
|
||||
component: ComponentRefresh,
|
||||
personalUseOnly: false,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import { COLOUR_SET } from "tachi-common";
|
||||
|
||||
export default function OngekiDamageCell({ damage }: { damage: number | null | undefined }) {
|
||||
return (
|
||||
<td style={{ verticalAlign: "middle" }}>
|
||||
<strong style={{ color: COLOUR_SET.red }}>{damage ?? "?"}</strong>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default function OngekiJudgementCell({
|
||||
}
|
||||
|
||||
return (
|
||||
<td>
|
||||
<td style={{ verticalAlign: "middle" }}>
|
||||
<strong>
|
||||
<div>
|
||||
<span style={{ color: COLOUR_SET.gold }}>{judgements.cbreak}</span>-
|
||||
@@ -27,14 +27,9 @@ export default function OngekiJudgementCell({
|
||||
<span style={{ color: COLOUR_SET.vibrantBlue }}>{judgements.hit}</span>-
|
||||
<span style={{ color: COLOUR_SET.gray }}>{judgements.miss}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ color: COLOUR_SET.vibrantYellow }}>
|
||||
{score.scoreData.optional.bellCount ?? "?"}/
|
||||
{score.scoreData.optional.totalBellCount ?? "?"}
|
||||
</span>
|
||||
<span style={{ color: COLOUR_SET.red, marginLeft: "0.5em" }}>
|
||||
{score.scoreData.optional.damage ?? "?"}
|
||||
</span>
|
||||
<div style={{ color: COLOUR_SET.vibrantYellow }}>
|
||||
{score.scoreData.optional.bellCount ?? "?"}/
|
||||
{score.scoreData.optional.totalBellCount ?? "?"}
|
||||
</div>
|
||||
</strong>
|
||||
</td>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function OngekiLampCell({
|
||||
bellLamp,
|
||||
colour,
|
||||
}: {
|
||||
noteLamp: "ALL BREAK" | "FULL COMBO" | "CLEAR" | "LOSS";
|
||||
noteLamp: "ALL BREAK+" | "ALL BREAK" | "FULL COMBO" | "CLEAR" | "LOSS";
|
||||
bellLamp: "FULL BELL" | "NONE";
|
||||
colour: string;
|
||||
}) {
|
||||
@@ -32,6 +32,7 @@ export default function OngekiLampCell({
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
verticalAlign: "middle",
|
||||
background:
|
||||
bellLamp === "FULL BELL"
|
||||
? `linear-gradient(-45deg, ${lowCorner} 0%,${lowCorner} 12%,${low} 12%,${low} 100%)`
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
import React from "react";
|
||||
import { Difficulties, integer } from "tachi-common";
|
||||
import { FmtStars, FmtStarsCompact, integer } from "tachi-common";
|
||||
|
||||
export function StarField({
|
||||
stars: stars,
|
||||
compact: compact,
|
||||
}: {
|
||||
stars: integer;
|
||||
compact: boolean;
|
||||
}) {
|
||||
if (stars < 6) {
|
||||
return <>{compact ? FmtStarsCompact(stars) : FmtStars(stars)}</>;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(30deg, #f0788a 5%, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a 85%)",
|
||||
color: "transparent",
|
||||
backgroundClip: "text",
|
||||
}}
|
||||
>
|
||||
★★★★★
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OngekiPlatinumCell({
|
||||
platScore: platScore,
|
||||
platinumScore: platinumScore,
|
||||
maxPlatScore: maxPlatScore,
|
||||
difficulty,
|
||||
stars: stars,
|
||||
}: {
|
||||
platScore: integer | null | undefined;
|
||||
platinumScore: integer;
|
||||
maxPlatScore: integer;
|
||||
difficulty: Difficulties["ongeki:Single"];
|
||||
stars: number;
|
||||
}) {
|
||||
if (difficulty !== "MASTER" && difficulty !== "LUNATIC") {
|
||||
return <td>N/A</td>;
|
||||
}
|
||||
|
||||
if (platScore === null || platScore === undefined) {
|
||||
return <td>Unknown</td>;
|
||||
}
|
||||
const percentage = Math.round((platinumScore * 10000.0) / maxPlatScore) / 100.0;
|
||||
|
||||
return (
|
||||
<td>
|
||||
<strong>MAX-{maxPlatScore - platScore}</strong>
|
||||
{platScore !== undefined && (
|
||||
<>
|
||||
<br />
|
||||
<small className="text-body-secondary">
|
||||
[{platScore}/{maxPlatScore}]
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
<div className="d-flex flex-column">
|
||||
<strong>{percentage.toFixed(2)}%</strong>
|
||||
<StarField stars={stars} compact={false} />
|
||||
<small className="text-body-secondary">
|
||||
[{platinumScore}/{maxPlatScore}]
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { FormatMillions, FormatScoreRating } from "util/misc";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import React from "react";
|
||||
import { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
import LampCell from "./LampCell";
|
||||
|
||||
export default function OngekiScoreRatingCell({
|
||||
score,
|
||||
}: {
|
||||
score: ScoreDocument<"ongeki:Single"> | PBScoreDocument<"ongeki:Single">;
|
||||
}) {
|
||||
const ratingValue = score.calculatedData.scoreRating ?? 0;
|
||||
const noteLamp = score.scoreData.noteLamp;
|
||||
const bellLamp = score.scoreData.bellLamp;
|
||||
const grade = score.scoreData.grade;
|
||||
|
||||
const gradeColor = ChangeOpacity(
|
||||
GPT_CLIENT_IMPLEMENTATIONS["ongeki:Single"].enumColours.grade[score.scoreData.grade],
|
||||
0.2
|
||||
);
|
||||
|
||||
const gradeRating = grade === "SSS+" ? 0.3 : grade === "SSS" ? 0.2 : grade === "SS" ? 0.1 : 0;
|
||||
const noteRating =
|
||||
noteLamp === "ALL BREAK+"
|
||||
? 0.35
|
||||
: noteLamp === "ALL BREAK"
|
||||
? 0.3
|
||||
: noteLamp === "FULL COMBO"
|
||||
? 0.1
|
||||
: 0;
|
||||
const bellRating = bellLamp === "FULL BELL" ? 0.05 : 0;
|
||||
const techRating = ratingValue - gradeRating - noteRating - bellRating;
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuickTooltip
|
||||
wide
|
||||
tooltipContent={
|
||||
<>
|
||||
<MiniTable headers={["Result", "Rating"]}>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: gradeColor,
|
||||
}}
|
||||
>
|
||||
{FormatMillions(score.scoreData.score)}
|
||||
</td>
|
||||
<td>{techRating.toFixed(3)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<LampCell
|
||||
lamp={noteLamp}
|
||||
colour={
|
||||
GPT_CLIENT_IMPLEMENTATIONS["ongeki:Single"].enumColours
|
||||
.noteLamp[noteLamp]
|
||||
}
|
||||
/>
|
||||
<td>{noteRating.toFixed(3)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<LampCell
|
||||
lamp={bellLamp}
|
||||
colour={
|
||||
GPT_CLIENT_IMPLEMENTATIONS["ongeki:Single"].enumColours
|
||||
.bellLamp[bellLamp]
|
||||
}
|
||||
/>
|
||||
<td>{bellRating.toFixed(3)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: gradeColor,
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.grade}</strong>
|
||||
</td>
|
||||
<td>{gradeRating.toFixed(3)}</td>
|
||||
</tr>
|
||||
</MiniTable>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<td>
|
||||
<div className="underline-on-hover">
|
||||
{FormatScoreRating(score.game, score.playtype, "scoreRating", ratingValue)}
|
||||
</div>
|
||||
</td>
|
||||
</QuickTooltip>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import React, { useState } from "react";
|
||||
import { Nav } from "react-bootstrap";
|
||||
import {
|
||||
ChartDocument,
|
||||
Difficulties,
|
||||
PBScoreDocument,
|
||||
ScoreData,
|
||||
ScoreDocument,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
import GekichumaiScoreChart from "components/charts/GekichumaiScoreChart";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
|
||||
type ChartType = "Score" | "Bells" | "Life";
|
||||
type ChartType = "Score" | "Platinum" | "Bells" | "Life";
|
||||
|
||||
export function OngekiGraphsComponent({
|
||||
score,
|
||||
@@ -29,6 +28,9 @@ export function OngekiGraphsComponent({
|
||||
score.scoreData.optional.totalBellCount !== null &&
|
||||
score.scoreData.optional.totalBellCount !== undefined;
|
||||
|
||||
// Platinum graphs were added later so they need a separate check
|
||||
const availablePlat = score.scoreData.optional.platinumGraph;
|
||||
|
||||
if (!available) {
|
||||
return <Box message="No charts available" />;
|
||||
}
|
||||
@@ -48,6 +50,14 @@ export function OngekiGraphsComponent({
|
||||
<SelectNav id="Score" value={graph} setValue={setGraph} disabled={!available}>
|
||||
Score
|
||||
</SelectNav>
|
||||
<SelectNav
|
||||
id="Platinum"
|
||||
value={graph}
|
||||
setValue={setGraph}
|
||||
disabled={!availablePlat}
|
||||
>
|
||||
P-Score
|
||||
</SelectNav>
|
||||
<SelectNav id="Bells" value={graph} setValue={setGraph} disabled={!available}>
|
||||
Bells
|
||||
</SelectNav>
|
||||
@@ -61,7 +71,7 @@ export function OngekiGraphsComponent({
|
||||
type={graph}
|
||||
scoreData={score.scoreData}
|
||||
song={song}
|
||||
difficulty={chart.difficulty}
|
||||
chart={chart}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -72,26 +82,30 @@ function GraphComponent({
|
||||
type,
|
||||
scoreData,
|
||||
song,
|
||||
difficulty,
|
||||
chart,
|
||||
}: {
|
||||
type: ChartType;
|
||||
scoreData: ScoreData<"ongeki:Single">;
|
||||
song: SongDocument<"ongeki">;
|
||||
difficulty: Difficulties["ongeki:Single"];
|
||||
chart: ChartDocument<"ongeki:Single">;
|
||||
}) {
|
||||
const values =
|
||||
type === "Score"
|
||||
? scoreData.optional.scoreGraph!
|
||||
: type === "Bells"
|
||||
? scoreData.optional.bellGraph!
|
||||
: scoreData.optional.lifeGraph!;
|
||||
: type === "Life"
|
||||
? scoreData.optional.lifeGraph!
|
||||
: scoreData.optional.platinumGraph!;
|
||||
return (
|
||||
<GekichumaiScoreChart
|
||||
height="360px"
|
||||
mobileHeight="175px"
|
||||
type={type}
|
||||
totalBellCount={scoreData.optional.totalBellCount!}
|
||||
difficulty={difficulty}
|
||||
maximumAbsoluteValue={
|
||||
type === "Bells" ? scoreData.optional.totalBellCount! : chart.data.maxPlatScore
|
||||
}
|
||||
difficulty={chart.difficulty}
|
||||
data={[
|
||||
{
|
||||
id: type,
|
||||
|
||||
@@ -21,7 +21,9 @@ import React from "react";
|
||||
import OngekiLampCell from "components/tables/cells/OngekiLampCell";
|
||||
import OngekiPlatinumCell from "components/tables/cells/OngekiPlatinumCell";
|
||||
import CHUNITHMLampCell from "components/tables/cells/CHUNITHMLampCell";
|
||||
import { bgc, RAINBOW_EX_GRADIENT, RAINBOW_GRADIENT } from "./games/_util";
|
||||
import OngekiDamageCell from "components/tables/cells/OngekiDamageCell";
|
||||
import OngekiScoreRatingCell from "components/tables/cells/OngekiScoreRatingCell";
|
||||
import { bgc, RAINBOW_EX_GRADIENT, RAINBOW_GRADIENT, RAINBOW_SHINY_GRADIENT } from "./games/_util";
|
||||
import { BMS_14K_IMPL, BMS_7K_IMPL, PMS_IMPL } from "./games/bms-pms";
|
||||
import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./games/iidx";
|
||||
import { GPTClientImplementation } from "./types";
|
||||
@@ -772,11 +774,9 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
SILVER: bgc("gray", "var(--bs-light)"),
|
||||
GOLD: bgc("var(--bs-warning)", "var(--bs-dark)"),
|
||||
PLATINUM: bgc("silver", "var(--bs-dark)"),
|
||||
RAINBOW: {
|
||||
background:
|
||||
"linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)",
|
||||
color: "var(--bs-dark)",
|
||||
},
|
||||
RAINBOW: RAINBOW_GRADIENT,
|
||||
RAINBOW_SHINY: RAINBOW_SHINY_GRADIENT,
|
||||
RAINBOW_EX: RAINBOW_EX_GRADIENT,
|
||||
},
|
||||
},
|
||||
enumColours: {
|
||||
@@ -799,6 +799,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
CLEAR: COLOUR_SET.green,
|
||||
"FULL COMBO": COLOUR_SET.gold,
|
||||
"ALL BREAK": COLOUR_SET.white,
|
||||
"ALL BREAK+": COLOUR_SET.vibrantBlue,
|
||||
},
|
||||
bellLamp: {
|
||||
NONE: COLOUR_SET.gray,
|
||||
@@ -815,26 +816,32 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
ratingSystems: [],
|
||||
ratingAlgNameOverrides: {
|
||||
score: {
|
||||
rating: "Classic Rating",
|
||||
rating: "ClassicRating",
|
||||
},
|
||||
session: {
|
||||
rating: "Classic Rating",
|
||||
naiveRating: "NaiveClassicRating",
|
||||
},
|
||||
profile: {
|
||||
naiveRating: "Classic NaiveRating",
|
||||
naiveRating: "NaiveRatingClassic",
|
||||
},
|
||||
},
|
||||
scoreHeaders: [
|
||||
[
|
||||
"Score",
|
||||
"Score",
|
||||
NumericSOV(
|
||||
(x) => x.scoreData.score * 10000 + (x.scoreData.optional.platScore ?? 0)
|
||||
),
|
||||
NumericSOV((x) => x.scoreData.score * 10000 + x.scoreData.platinumScore),
|
||||
],
|
||||
[
|
||||
"Platinum Score",
|
||||
"P-Score",
|
||||
NumericSOV((x) => x.scoreData.platinumStars * 100000 + x.scoreData.platinumScore),
|
||||
],
|
||||
["Judgements", "Notes", NumericSOV((x) => x.scoreData.judgements.cbreak ?? 0)],
|
||||
[
|
||||
"Hits",
|
||||
"Hits",
|
||||
NumericSOV((x) => x.scoreData.optional.damage ?? Number.MAX_SAFE_INTEGER, true),
|
||||
],
|
||||
// TODO: this should be sorted by %
|
||||
["Platinum Score", "P-Score", NumericSOV((x) => x.scoreData.optional.platScore ?? 0)],
|
||||
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
|
||||
[
|
||||
"Lamp",
|
||||
"Lamp",
|
||||
@@ -852,11 +859,12 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
colour={GetEnumColour(sc, "grade")}
|
||||
/>
|
||||
<OngekiPlatinumCell
|
||||
platScore={sc.scoreData.optional.platScore}
|
||||
platinumScore={sc.scoreData.platinumScore}
|
||||
maxPlatScore={chart.data.maxPlatScore}
|
||||
difficulty={chart.difficulty}
|
||||
stars={sc.scoreData.platinumStars}
|
||||
/>
|
||||
<OngekiJudgementCell score={sc} />
|
||||
<OngekiDamageCell damage={sc.scoreData.optional.damage} />
|
||||
<OngekiLampCell
|
||||
noteLamp={sc.scoreData.noteLamp}
|
||||
bellLamp={sc.scoreData.bellLamp}
|
||||
@@ -864,7 +872,15 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
|
||||
/>
|
||||
</>
|
||||
),
|
||||
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
|
||||
ratingCell: ({ sc, rating }) => (
|
||||
<>
|
||||
{rating === "scoreRating" ? (
|
||||
<OngekiScoreRatingCell score={sc} />
|
||||
) : (
|
||||
<RatingCell score={sc} rating={rating} />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
"arcaea:Touch": ARCAEA_TOUCH_IMPL,
|
||||
"gitadora:Dora": GITADORA_DORA_IMPL,
|
||||
|
||||
@@ -36,6 +36,12 @@ export const RAINBOW_GRADIENT = {
|
||||
color: "var(--bs-dark)",
|
||||
} as const;
|
||||
|
||||
export const RAINBOW_SHINY_GRADIENT = {
|
||||
background:
|
||||
"linear-gradient(-45deg, #ff8697, #ff9dbf, #9e81d0, #87caff, #7dae80, #ffffa7, #ffd88a, #ffab8d, #ff8697)",
|
||||
color: "var(--bs-dark)",
|
||||
} as const;
|
||||
|
||||
export const RAINBOW_EX_GRADIENT = {
|
||||
background: "linear-gradient(-45deg, #0fa091, #0f98d5, #67087f, #d9007e, #f56e06)",
|
||||
color: "var(--bs-light)",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { FmtNum } from "../../utils/util";
|
||||
import { FmtNum, FmtStars } from "../../utils/util";
|
||||
import { ClassValue, ToDecimalPlaces } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
@@ -24,16 +24,18 @@ export const ONGEKI_CONF = {
|
||||
} as const satisfies INTERNAL_GAME_CONFIG;
|
||||
|
||||
export const OngekiColours = [
|
||||
ClassValue("BLUE", "青", "Blue: 0 - 1.99 Rating"),
|
||||
ClassValue("GREEN", "緑", "Green: 2 - 3.99 Rating"),
|
||||
ClassValue("ORANGE", "橙", "Orange: 4 - 6.99 Rating"),
|
||||
ClassValue("RED", "赤", "Red: 7 - 9.99 Rating"),
|
||||
ClassValue("PURPLE", "紫", "Purple: 10 - 11.99 Rating"),
|
||||
ClassValue("COPPER", "銅", "Copper: 12 - 12.99 Rating"),
|
||||
ClassValue("SILVER", "銀", "Silver: 13 - 13.99 Rating"),
|
||||
ClassValue("GOLD", "金", "Gold: 14.00 - 14.49 Rating"),
|
||||
ClassValue("PLATINUM", "鉑", "Platinum: 14.50 - 14.99 Rating"),
|
||||
ClassValue("RAINBOW", "虹", "Rainbow: >=15 Rating"),
|
||||
ClassValue("BLUE", "水", "Blue: 0.000~3.999 Rating"),
|
||||
ClassValue("GREEN", "緑", "Green: 4.000~6.999 Rating"),
|
||||
ClassValue("ORANGE", "橙", "Orange: 7.000~8.999 Rating"),
|
||||
ClassValue("RED", "赤", "Red: 9.000~10.999 Rating"),
|
||||
ClassValue("PURPLE", "紫", "Purple: 11.000~12.999 Rating"),
|
||||
ClassValue("COPPER", "銅", "Copper: 13.000~14.999 Rating"),
|
||||
ClassValue("SILVER", "銀", "Silver: 15.000~16.999 Rating"),
|
||||
ClassValue("GOLD", "金", "Gold: 17.000~17.999 Rating"),
|
||||
ClassValue("PLATINUM", "鉑", "Platinum: 18.000~18.999 Rating"),
|
||||
ClassValue("RAINBOW", "虹", "Rainbow: 19.000~19.999 Rating"),
|
||||
ClassValue("RAINBOW_SHINY", "虹(光)", "Rainbow Shiny: 20.000~20.999 Rating"),
|
||||
ClassValue("RAINBOW_EX", "虹(極)", "Rainbow Extreme: 21.000~ Rating"),
|
||||
];
|
||||
|
||||
export const ONGEKI_SINGLE_CONF = {
|
||||
@@ -47,7 +49,7 @@ export const ONGEKI_SINGLE_CONF = {
|
||||
},
|
||||
noteLamp: {
|
||||
type: "ENUM",
|
||||
values: ["LOSS", "CLEAR", "FULL COMBO", "ALL BREAK"],
|
||||
values: ["LOSS", "CLEAR", "FULL COMBO", "ALL BREAK", "ALL BREAK+"],
|
||||
minimumRelevantValue: "CLEAR",
|
||||
description: "The primary lamp. A clear is either a draw or a win.",
|
||||
},
|
||||
@@ -57,6 +59,13 @@ export const ONGEKI_SINGLE_CONF = {
|
||||
minimumRelevantValue: "FULL BELL",
|
||||
description: "Tracks whether all bells in the chart have been collected.",
|
||||
},
|
||||
platinumScore: {
|
||||
type: "INTEGER",
|
||||
chartDependentMax: true,
|
||||
formatter: FmtNum,
|
||||
description:
|
||||
"The Platinum Score value, similar to the scoring system used in beatmania IIDX.",
|
||||
},
|
||||
},
|
||||
|
||||
derivedMetrics: {
|
||||
@@ -66,6 +75,12 @@ export const ONGEKI_SINGLE_CONF = {
|
||||
minimumRelevantValue: "A",
|
||||
description: "The grade this score was.",
|
||||
},
|
||||
platinumStars: {
|
||||
type: "INTEGER",
|
||||
validate: p.isBetween(0, 6),
|
||||
formatter: FmtStars,
|
||||
description: "The number of platinum stars of this score",
|
||||
},
|
||||
},
|
||||
|
||||
defaultMetric: "score",
|
||||
@@ -93,18 +108,16 @@ export const ONGEKI_SINGLE_CONF = {
|
||||
formatter: FmtNum,
|
||||
description: "The total number of bells.",
|
||||
},
|
||||
platScore: {
|
||||
type: "INTEGER",
|
||||
chartDependentMax: true,
|
||||
formatter: FmtNum,
|
||||
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.",
|
||||
},
|
||||
platinumGraph: {
|
||||
type: "NULLABLE_GRAPH",
|
||||
validate: p.isBetween(-100000, 0),
|
||||
description: "The Platinum Score history, queried in one-second intervals.",
|
||||
},
|
||||
bellGraph: {
|
||||
type: "NULLABLE_GRAPH",
|
||||
validate: p.isBetween(-10000, 0),
|
||||
@@ -124,12 +137,30 @@ export const ONGEKI_SINGLE_CONF = {
|
||||
"A rating value of this score, capping at +2.0 at SSS+. This is identical to the system used in bright MEMORY and earlier versions.",
|
||||
formatter: ToDecimalPlaces(2),
|
||||
},
|
||||
scoreRating: {
|
||||
description:
|
||||
"A rating value of this score, capping at +2.7 at 1,010,000. This is identical to the system used in Re:Fresh.",
|
||||
formatter: ToDecimalPlaces(3),
|
||||
},
|
||||
starRating: {
|
||||
description:
|
||||
"A rating value of this score, based on stars derived from Platinum Score. This is identical to the system used in Re:Fresh.",
|
||||
formatter: ToDecimalPlaces(3),
|
||||
},
|
||||
},
|
||||
sessionRatingAlgs: {
|
||||
naiveRating: {
|
||||
description: "The average of your best 10 classic ratings this session.",
|
||||
formatter: ToDecimalPlaces(2),
|
||||
},
|
||||
naiveScoreRating: {
|
||||
description: "The average of your best 10 score ratings this session.",
|
||||
formatter: ToDecimalPlaces(3),
|
||||
},
|
||||
starRating: {
|
||||
description: "The average of your best 10 star ratings this session.",
|
||||
formatter: ToDecimalPlaces(3),
|
||||
},
|
||||
},
|
||||
profileRatingAlgs: {
|
||||
naiveRating: {
|
||||
@@ -137,6 +168,11 @@ export const ONGEKI_SINGLE_CONF = {
|
||||
"The average of your best 45 classic ratings. This is a simpler variant of the rating algorithm used in bright MEMORY and earlier versions, without distinguishing between new and old charts, and without taking recent scores into account.",
|
||||
formatter: ToDecimalPlaces(2),
|
||||
},
|
||||
naiveRatingRefresh: {
|
||||
description:
|
||||
"A weighted sum of the average of your best 60 score ratings, and your best 50 star ratings. This is a simpler variant of the rating algorithm used in Re:Fresh, without distinguishing between new and old charts.",
|
||||
formatter: ToDecimalPlaces(3),
|
||||
},
|
||||
},
|
||||
|
||||
defaultScoreRatingAlg: "rating",
|
||||
|
||||
@@ -579,6 +579,7 @@ export enum ONGEKI_NOTE_LAMPS {
|
||||
CLEAR = 1,
|
||||
FULL_COMBO = 2,
|
||||
ALL_BREAK = 3,
|
||||
ALL_BREAK_PLUS = 4,
|
||||
}
|
||||
|
||||
export enum ONGEKI_BELL_LAMPS {
|
||||
@@ -612,4 +613,6 @@ export enum ONGEKI_COLOURS {
|
||||
GOLD = 7,
|
||||
PLATINUM = 8,
|
||||
RAINBOW = 9,
|
||||
RAINBOW_SHINY = 10,
|
||||
RAINBOW_EX = 11,
|
||||
}
|
||||
|
||||
@@ -287,6 +287,28 @@ export function FmtPercent(v: number, dp = 2) {
|
||||
return `${v.toFixed(dp)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number into "★★★☆☆"
|
||||
*/
|
||||
export function FmtStars(num: number) {
|
||||
if (num > 5) {
|
||||
return "★★★★★(虹)";
|
||||
}
|
||||
|
||||
return "★".repeat(num) + "☆".repeat(5 - num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number into "★★★"
|
||||
*/
|
||||
export function FmtStarsCompact(num: number) {
|
||||
if (num > 5) {
|
||||
return "★★★★★(虹)";
|
||||
}
|
||||
|
||||
return "★".repeat(num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a number of 12834 into "12834" instead of "12,834".
|
||||
*/
|
||||
|
||||
@@ -14,8 +14,9 @@ For more information on what metrics are and how they work, see [TODO]!
|
||||
| Metric Name | Type | Description |
|
||||
| :: | :: | :: |
|
||||
| `score` | Integer | Known in-game as 'Technical Score'. It ranges between 0 and 1,010,000, where notes are worth 950,000, and bells 60,000. |
|
||||
| `noteLamp` | "LOSS", "CLEAR", "FULL COMBO", "ALL BREAK" | The primary lamp. A clear is a draw or a win in-game. |
|
||||
| `noteLamp` | "LOSS", "CLEAR", "FULL COMBO", "ALL BREAK", "ALL BREAK+" | The primary lamp. A clear is a draw or a win in-game. |
|
||||
| `bellLamp` | "NONE", "FULL BELL" | Tracks whether all bells in the chart have been collected. |
|
||||
| `platinumScore` | Integer | The Platinum Score value. |
|
||||
|
||||
### Derived Metrics
|
||||
|
||||
@@ -33,14 +34,13 @@ For more information on what metrics are and how they work, see [TODO]!
|
||||
| `damage` | Integer | The number of damage ticks received. |
|
||||
| `bellCount` | Integer | The number of bells collected. |
|
||||
| `totalBellCount` | Integer | The maximum number of bells that could have been obtained at the time of the play's end. |
|
||||
| `platScore` | Integer | The Platinum Score value. Only exists in MASTER and LUNATIC charts. |
|
||||
|
||||
## Judgements
|
||||
|
||||
The following judgements are defined:
|
||||
|
||||
- `cbreak`
|
||||
- `break`
|
||||
- `cbreak` (critical break)
|
||||
- `break` (red/regular break)
|
||||
- `hit`
|
||||
- `miss`
|
||||
|
||||
@@ -51,18 +51,23 @@ The following judgements are defined:
|
||||
| Name | Description |
|
||||
| :: | :: |
|
||||
| `rating` | A rating value of this score, capping at +2.0 at SSS+. This is identical to the system used in bright MEMORY and earlier versions. |
|
||||
| `scoreRating` | A rating value of this score, capping at +2.7 at 1,010,000. This is identical to the system used in Re:Fresh. |
|
||||
| `starRating` | A rating value of this score, based on stars derived from Platinum Score. This is identical to the system used in Re:Fresh. |
|
||||
|
||||
### Session Rating Algorithms
|
||||
|
||||
| Name | Description |
|
||||
| :: | :: |
|
||||
| `naiveRating` | The average of your best 10 classic ratings this session. |
|
||||
| `naiveScoreRating` | The average of your best 10 score ratings this session. |
|
||||
| `starRating` | The average of your best 10 star ratings this session. |
|
||||
|
||||
### Profile Rating Algorithms
|
||||
|
||||
| Name | Description |
|
||||
| :: | :: |
|
||||
| `naiveRating` | The average of your best 45 classic ratings. This is a simpler variant of the rating algorithm used in bright MEMORY and earlier versions, without distinguishing between new and old charts, and without taking recent scores into account. |
|
||||
| `naiveRatingRefresh` | A weighted sum of the average of your best 60 score ratings, and your best 50 star ratings. This is a simpler variant of the rating algorithm used in Re:Fresh, without distinguishing between new and old charts. |
|
||||
|
||||
## Difficulties
|
||||
|
||||
@@ -82,8 +87,11 @@ The following judgements are defined:
|
||||
|
||||
| ID | Pretty Name |
|
||||
| :: | :: |
|
||||
| `brightMemory2Omni` | bright MEMORY Act.2 Omnimix |
|
||||
| `brightMemory3` | bright MEMORY Act.3 |
|
||||
| `brightMemory3Omni` | bright MEMORY Act.3 Omnimix |
|
||||
| `refresh` | Re:Fresh |
|
||||
| `refreshOmni` | Re:Fresh Omnimix |
|
||||
|
||||
## Supported Match Types
|
||||
|
||||
|
||||
Generated
+4
@@ -10238,6 +10238,10 @@ packages:
|
||||
resolution: {integrity: sha512-KQxSjnHXvb2B9Z+y/OwtUzStIjyXsn78c1XSF0t5L9G5UsVjyyO250eq4ih00X1WjvPRE9AtC4h38gBZEWClkw==}
|
||||
dev: false
|
||||
|
||||
/rg-stats@1.0.0:
|
||||
resolution: {integrity: sha512-Itt3OJden5riCkSY7o5whJTqK/DQNteJdQ+AXZ2upNNESOO/7JnWx+n/jJiSimR+XOE/rSggTxXLujHcE4Z0nA==}
|
||||
dev: false
|
||||
|
||||
/rimraf@2.4.5:
|
||||
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
|
||||
+1
-1
@@ -126,4 +126,4 @@
|
||||
"node": "20",
|
||||
"pnpm": "8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { ONGEKI_IMPL } from "./ongeki";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
@@ -13,15 +15,18 @@ const baseMetrics: ProvidedMetrics["ongeki:Single"] = {
|
||||
noteLamp: "CLEAR",
|
||||
bellLamp: "FULL BELL",
|
||||
score: 1_001_500,
|
||||
platinumScore: 970,
|
||||
};
|
||||
|
||||
const scoreData: ScoreData<"ongeki:Single"> = {
|
||||
noteLamp: "CLEAR",
|
||||
bellLamp: "FULL BELL",
|
||||
score: 1_001_500,
|
||||
platinumScore: 970,
|
||||
platinumStars: 4,
|
||||
grade: "SSS",
|
||||
judgements: {},
|
||||
optional: { enumIndexes: {}, bellCount: 100, platScore: 1000 },
|
||||
optional: { enumIndexes: {}, bellCount: 100 },
|
||||
enumIndexes: {
|
||||
grade: ONGEKI_GRADES.SSS,
|
||||
noteLamp: ONGEKI_NOTE_LAMPS.CLEAR,
|
||||
@@ -34,9 +39,9 @@ const mockPB = mkMockPB("ongeki", "Single", TestingOngekiChart, scoreData);
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("ONGEKI Implementation", (t) => {
|
||||
t.test("Grade Deriver", (t) => {
|
||||
const f = (score: number, expected: any) =>
|
||||
t.test("ONGEKI Implementation", (t: any) => {
|
||||
t.test("Grade Deriver", (t: any) => {
|
||||
const f = (score: number, expected: string) =>
|
||||
t.equal(
|
||||
ONGEKI_IMPL.derivers.grade(dmf(baseMetrics, { score }), TestingOngekiChart),
|
||||
expected,
|
||||
@@ -60,11 +65,47 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Rating Calc", (t) => {
|
||||
t.test("Star Deriver", (t: any) => {
|
||||
const f = (platinumScore: number, expected: number) =>
|
||||
t.equal(
|
||||
ONGEKI_IMPL.derivers.platinumStars(
|
||||
dmf(baseMetrics, { platinumScore }),
|
||||
TestingOngekiChart
|
||||
),
|
||||
expected,
|
||||
`A score of ${platinumScore.toLocaleString()} should result in stars=${expected}.`
|
||||
);
|
||||
|
||||
f(939, 0);
|
||||
f(940, 1);
|
||||
f(950, 2);
|
||||
f(960, 3);
|
||||
f(970, 4);
|
||||
f(980, 5);
|
||||
f(990, 6);
|
||||
f(1000, 6);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Rating Calc", (t: any) => {
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreCalcs.rating(scoreData, TestingOngekiChart),
|
||||
12.1,
|
||||
"Basic rating check"
|
||||
"Basic classic rating check"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreCalcs.scoreRating(scoreData, TestingOngekiChart),
|
||||
12.1,
|
||||
"Basic score rating check"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
ONGEKI_IMPL.scoreCalcs.starRating(scoreData, TestingOngekiChart),
|
||||
Math.floor(scoreData.platinumStars * (TestingOngekiChart.levelNum * 10) ** 2) /
|
||||
100000.0,
|
||||
"Basic star rating check"
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -72,36 +113,80 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
|
||||
t.todo("Session Calcs");
|
||||
|
||||
t.test("Profile Calcs", (t) => {
|
||||
t.test("Profile Calcs", (t: any) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const mockPBs = async (ratings: Array<number>) => {
|
||||
const mockPBs = async (
|
||||
ratings: Array<number>,
|
||||
field: "rating" | "scoreRating" | "starRating"
|
||||
) => {
|
||||
await Promise.all(
|
||||
ratings.map((rating, idx) =>
|
||||
db["personal-bests"].insert({
|
||||
ratings.map((rating, idx) => {
|
||||
const ratings =
|
||||
// Leave me and my ternaries alone
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
field === "rating"
|
||||
? { rating, scoreRating: 0, starRating: 0 }
|
||||
: field === "scoreRating"
|
||||
? { rating: 0, scoreRating: rating, starRating: 0 }
|
||||
: { rating: 0, scoreRating: 0, starRating: rating };
|
||||
|
||||
return db["personal-bests"].insert({
|
||||
...TestingOngekiScorePB,
|
||||
chartID: `TEST${idx}`,
|
||||
chartID: `TEST${field}${idx}`,
|
||||
calculatedData: {
|
||||
...TestingOngekiScorePB.calculatedData,
|
||||
rating,
|
||||
...ratings,
|
||||
},
|
||||
})
|
||||
)
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
t.test("Floating-point edge case", async (t) => {
|
||||
await mockPBs(Array(45).fill(16.27));
|
||||
t.test("Floating-point edge case", async (t: any) => {
|
||||
await mockPBs(Array(45).fill(16.27), "rating");
|
||||
await mockPBs(Array(60).fill(16.271), "scoreRating");
|
||||
|
||||
t.equal(await ONGEKI_IMPL.profileCalcs.naiveRating("ongeki", "Single", 1), 16.27);
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
19.525
|
||||
);
|
||||
|
||||
await mockPBs([1, 1, 0, 0], "starRating");
|
||||
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
19.565
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Profile with fewer than 45 scores", async (t) => {
|
||||
await mockPBs([16, 16, 16, 16]);
|
||||
t.test("Profile with few scores #1", async (t: any) => {
|
||||
await mockPBs([16, 16, 16, 16], "rating");
|
||||
await mockPBs([16, 16, 16, 16], "scoreRating");
|
||||
|
||||
t.equal(await ONGEKI_IMPL.profileCalcs.naiveRating("ongeki", "Single", 1), 1.42);
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
1.279
|
||||
);
|
||||
|
||||
await mockPBs([1, 1, 0, 0], "starRating");
|
||||
|
||||
t.equal(
|
||||
await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1),
|
||||
1.319
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Profile with few scores #2", async (t: any) => {
|
||||
await mockPBs([1, 1, 0, 0], "starRating");
|
||||
|
||||
t.equal(await ONGEKI_IMPL.profileCalcs.naiveRatingRefresh("ongeki", "Single", 1), 0.04);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -109,40 +194,49 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Colour Deriver", (t) => {
|
||||
const f = (v: number | null, expected: any) =>
|
||||
t.test("Colour Deriver", (t: any) => {
|
||||
const f = (v: number | null, expected: string | null) =>
|
||||
t.equal(
|
||||
ONGEKI_IMPL.classDerivers.colour({ naiveRating: v }),
|
||||
ONGEKI_IMPL.classDerivers.colour({ naiveRatingRefresh: v }),
|
||||
expected,
|
||||
`A naiveRating of ${v} should result in ${expected}.`
|
||||
`A naiveRatingRefresh of ${v} should result in ${expected}.`
|
||||
);
|
||||
|
||||
f(null, null);
|
||||
f(0, "BLUE");
|
||||
f(2, "GREEN");
|
||||
f(4, "ORANGE");
|
||||
f(7, "RED");
|
||||
f(10, "PURPLE");
|
||||
f(12, "COPPER");
|
||||
f(13, "SILVER");
|
||||
f(14, "GOLD");
|
||||
f(14.5, "PLATINUM");
|
||||
f(15, "RAINBOW");
|
||||
f(4, "GREEN");
|
||||
f(7, "ORANGE");
|
||||
f(9, "RED");
|
||||
f(11, "PURPLE");
|
||||
f(13, "COPPER");
|
||||
f(15, "SILVER");
|
||||
f(17, "GOLD");
|
||||
f(18, "PLATINUM");
|
||||
f(19, "RAINBOW");
|
||||
f(20, "RAINBOW_SHINY");
|
||||
f(21, "RAINBOW_EX");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Goal Formatters", (t) => {
|
||||
t.test("Criteria", (t) => {
|
||||
t.test("Goal Formatters", (t: any) => {
|
||||
t.test("Criteria", (t: any) => {
|
||||
t.equal(
|
||||
ONGEKI_IMPL.goalCriteriaFormatters.score(1_008_182),
|
||||
"Get a score of 1,008,182 on"
|
||||
);
|
||||
|
||||
t.equal(
|
||||
ONGEKI_IMPL.goalCriteriaFormatters.platinumScore(1500),
|
||||
"Get 1,500 Platinum Score on"
|
||||
);
|
||||
|
||||
t.equal(ONGEKI_IMPL.goalCriteriaFormatters.platinumStars(3), "Get ★★★☆☆ on");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Progress", (t) => {
|
||||
t.test("Progress", (t: any) => {
|
||||
const f = (
|
||||
k: keyof typeof ONGEKI_IMPL.goalProgressFormatters,
|
||||
modifant: Partial<ScoreData<"ongeki:Single">>,
|
||||
@@ -160,15 +254,18 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
);
|
||||
|
||||
f("grade", { grade: "S", score: 987_342 }, ONGEKI_GRADES.S, "SS-2.7K");
|
||||
f("score", { score: 982_123 }, 1_000_000, "982,123");
|
||||
f("noteLamp", { noteLamp: "CLEAR" }, ONGEKI_NOTE_LAMPS.CLEAR, "CLEAR");
|
||||
f("bellLamp", { bellLamp: "FULL BELL" }, ONGEKI_BELL_LAMPS.FULL_BELL, "FULL BELL");
|
||||
f("score", { score: 982_123 }, 1_000_000, "982,123");
|
||||
f("platinumScore", { platinumScore: 1234 }, 2345, "1,234");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Out Of", (t) => {
|
||||
t.test("Out Of", (t: any) => {
|
||||
t.equal(ONGEKI_IMPL.goalOutOfFormatters.score(1_001_003), "1,001,003");
|
||||
t.equal(ONGEKI_IMPL.goalOutOfFormatters.score(983_132), "983,132");
|
||||
t.equal(ONGEKI_IMPL.goalOutOfFormatters.platinumScore(1234), "1,234");
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -176,10 +273,10 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("PB Merging", (t) => {
|
||||
t.test("PB Merging", (t: any) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should join best lamp", async (t) => {
|
||||
t.test("Should join best lamp", async (t: any) => {
|
||||
await db.scores.insert(mockScore);
|
||||
await db.scores.insert(
|
||||
dmf(mockScore, {
|
||||
@@ -211,21 +308,46 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should join platinum score", async (t: any) => {
|
||||
await db.scores.insert(mockScore);
|
||||
await db.scores.insert(
|
||||
dmf(mockScore, {
|
||||
scoreID: "bestPlatinum",
|
||||
scoreData: {
|
||||
score: 0,
|
||||
platinumScore: 990,
|
||||
platinumStars: 6,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
t.hasStrict(await CreatePBDoc("ongeki:Single", 1, TestingOngekiChart, logger), {
|
||||
composedFrom: [
|
||||
{ name: "Best Score" },
|
||||
{ name: "Best Platinum Score", scoreID: "bestPlatinum" },
|
||||
],
|
||||
scoreData: {
|
||||
score: mockScore.scoreData.score,
|
||||
platinumScore: 990,
|
||||
platinumStars: 6,
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Ranking", (t) => {
|
||||
t.test("Ranking", (t: any) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should tiebreak according to platinum score", async (t) => {
|
||||
t.test("Should tiebreak according to platinum score", async (t: any) => {
|
||||
await db["personal-bests"].insert({
|
||||
...TestingOngekiScorePB,
|
||||
scoreData: {
|
||||
...TestingOngekiScorePB.scoreData,
|
||||
optional: {
|
||||
...TestingOngekiScorePB.scoreData.optional,
|
||||
platScore: 1000,
|
||||
},
|
||||
platinumScore: 1000,
|
||||
},
|
||||
timeAchieved: 3,
|
||||
userID: 1,
|
||||
@@ -235,10 +357,7 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
...TestingOngekiScorePB,
|
||||
scoreData: {
|
||||
...TestingOngekiScorePB.scoreData,
|
||||
optional: {
|
||||
...TestingOngekiScorePB.scoreData.optional,
|
||||
platScore: 1001,
|
||||
},
|
||||
platinumScore: 1001,
|
||||
},
|
||||
timeAchieved: 2,
|
||||
userID: 2,
|
||||
@@ -248,10 +367,7 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
...TestingOngekiScorePB,
|
||||
scoreData: {
|
||||
...TestingOngekiScorePB.scoreData,
|
||||
optional: {
|
||||
...TestingOngekiScorePB.scoreData.optional,
|
||||
platScore: 999,
|
||||
},
|
||||
platinumScore: 999,
|
||||
},
|
||||
timeAchieved: 1,
|
||||
userID: 3,
|
||||
@@ -267,9 +383,9 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
|
||||
for (const pb of pbs) {
|
||||
t.strictSame(pb.rankingData.outOf, 3);
|
||||
if (pb.scoreData.optional.platScore === 999) {
|
||||
if (pb.scoreData.platinumScore === 999) {
|
||||
t.strictSame(pb.rankingData.rank, 3);
|
||||
} else if (pb.scoreData.optional.platScore === 1000) {
|
||||
} else if (pb.scoreData.platinumScore === 1000) {
|
||||
t.strictSame(pb.rankingData.rank, 2);
|
||||
} else {
|
||||
t.strictSame(pb.rankingData.rank, 1);
|
||||
@@ -279,16 +395,13 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should not tiebreak if tech scores differ", async (t) => {
|
||||
t.test("Should not tiebreak if tech scores differ", async (t: any) => {
|
||||
await db["personal-bests"].insert({
|
||||
...TestingOngekiScorePB,
|
||||
scoreData: {
|
||||
...TestingOngekiScorePB.scoreData,
|
||||
score: 1010000,
|
||||
optional: {
|
||||
...TestingOngekiScorePB.scoreData.optional,
|
||||
platScore: 0,
|
||||
},
|
||||
platinumScore: 0,
|
||||
},
|
||||
userID: 1,
|
||||
});
|
||||
@@ -298,10 +411,7 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
scoreData: {
|
||||
...TestingOngekiScorePB.scoreData,
|
||||
score: 1009999,
|
||||
optional: {
|
||||
...TestingOngekiScorePB.scoreData.optional,
|
||||
platScore: 1001,
|
||||
},
|
||||
platinumScore: 1001,
|
||||
},
|
||||
userID: 2,
|
||||
});
|
||||
@@ -322,8 +432,8 @@ t.test("ONGEKI Implementation", (t) => {
|
||||
t.strictSame(pb1?.rankingData.outOf, 2);
|
||||
t.strictSame(pb1?.rankingData.rank, 1);
|
||||
t.strictSame(pb2?.rankingData.rank, 2);
|
||||
t.strictSame(pb1?.scoreData.optional.platScore, 0);
|
||||
t.strictSame(pb2?.scoreData.optional.platScore, 1001);
|
||||
t.strictSame(pb1?.scoreData.platinumScore, 0);
|
||||
t.strictSame(pb2?.scoreData.platinumScore, 1001);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -4,25 +4,22 @@ import { CreatePBMergeFor } from "game-implementations/utils/pb-merge";
|
||||
import { ProfileAvgBestN } from "game-implementations/utils/profile-calc";
|
||||
import { SessionAvgBest10For } from "game-implementations/utils/session-calc";
|
||||
import { ONGEKIRating } from "rg-stats";
|
||||
import { ONGEKI_GBOUNDARIES, FmtNum, GetGrade } from "tachi-common";
|
||||
import { ONGEKI_GBOUNDARIES, FmtNum, GetGrade, FmtStars, FmtStarsCompact } from "tachi-common";
|
||||
import { IsNullish } from "utils/misc";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
import type { ChartDocument, Game, integer, Playtype } from "tachi-common";
|
||||
|
||||
const isBonusTrack = (inGameID: number) => inGameID >= 7000 && inGameID < 8000;
|
||||
const isUnranked = (chart: ChartDocument<"ongeki:Single">) =>
|
||||
(chart.data.inGameID >= 7000 && chart.data.inGameID < 8000) || chart.levelNum === 0.0;
|
||||
|
||||
const starCount = (platinumScore: number, maxPlatinumScore: number) => {
|
||||
const pct = Math.floor((platinumScore / maxPlatinumScore) * 100);
|
||||
|
||||
return Math.max(0, Math.min(pct, 99) - 93);
|
||||
};
|
||||
|
||||
export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
chartSpecificValidators: {
|
||||
platScore: (platScore, chart) => {
|
||||
if (platScore < 0) {
|
||||
return `Platinum Score must be non-negative. Got ${platScore}`;
|
||||
}
|
||||
|
||||
if (platScore > chart.data.maxPlatScore) {
|
||||
return `Platinum Score is too large: ${platScore}>${chart.data.maxPlatScore}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
bellCount: (bellCount) => {
|
||||
if (bellCount < 0) {
|
||||
return `Bell Count must be non-negative. Got ${bellCount}`;
|
||||
@@ -42,47 +39,93 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
return `Damage must be non-negative. Got ${damage}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
platinumScore: (platinumScore, chart) => {
|
||||
if (platinumScore < 0) {
|
||||
return `Platinum Score must be non-negative. Got ${platinumScore}`;
|
||||
}
|
||||
|
||||
if (platinumScore > chart.data.maxPlatScore) {
|
||||
return `Platinum Score must not exceed the chart's maximum Platinum Score. Got ${platinumScore}/${chart.data.maxPlatScore}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
derivers: {
|
||||
grade: ({ score }) => GetGrade(ONGEKI_GBOUNDARIES, score),
|
||||
platinumStars: ({ platinumScore }, chart) =>
|
||||
starCount(platinumScore, chart.data.maxPlatScore),
|
||||
},
|
||||
scoreCalcs: {
|
||||
rating: (scoreData, chart) =>
|
||||
isBonusTrack(chart.data.inGameID) || chart.levelNum === 0.0
|
||||
isUnranked(chart) ? 0 : ONGEKIRating.calculate(scoreData.score, chart.levelNum),
|
||||
scoreRating: (scoreData, chart) =>
|
||||
isUnranked(chart)
|
||||
? 0
|
||||
: ONGEKIRating.calculate(scoreData.score, chart.levelNum),
|
||||
: ONGEKIRating.calculateRefresh(
|
||||
chart.levelNum,
|
||||
scoreData.score,
|
||||
scoreData.score === 1010000 ? "ALL BREAK+" : scoreData.noteLamp,
|
||||
scoreData.bellLamp === "FULL BELL"
|
||||
),
|
||||
starRating: (scoreData, chart) =>
|
||||
isUnranked(chart)
|
||||
? 0
|
||||
: ONGEKIRating.calculatePlatinum(
|
||||
chart.levelNum,
|
||||
starCount(scoreData.platinumScore, chart.data.maxPlatScore)
|
||||
),
|
||||
},
|
||||
sessionCalcs: {
|
||||
naiveRating: SessionAvgBest10For("rating"),
|
||||
naiveScoreRating: SessionAvgBest10For("scoreRating"),
|
||||
starRating: SessionAvgBest10For("starRating"),
|
||||
},
|
||||
sessionCalcs: { naiveRating: SessionAvgBest10For("rating") },
|
||||
profileCalcs: {
|
||||
naiveRating: ProfileAvgBestN("rating", 45, false, 100),
|
||||
naiveRatingRefresh: async (game: Game, playtype: Playtype, userID: integer) => {
|
||||
const [score, star] = await Promise.all([
|
||||
ProfileAvgBestN("scoreRating", 60, false, 1000)(game, playtype, userID),
|
||||
ProfileAvgBestN("starRating", 50, false, 1000)(game, playtype, userID),
|
||||
]);
|
||||
|
||||
const score1k = Math.round((score ?? 0) * 1000);
|
||||
const star1k = Math.round((star ?? 0) * 1000);
|
||||
|
||||
return (Math.floor(score1k * 1.2) + star1k) / 1000.0;
|
||||
},
|
||||
},
|
||||
classDerivers: {
|
||||
colour: (ratings) => {
|
||||
const rating = ratings.naiveRating;
|
||||
const rating = ratings.naiveRatingRefresh;
|
||||
|
||||
if (IsNullish(rating)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rating >= 15) {
|
||||
if (rating >= 21) {
|
||||
return "RAINBOW_EX";
|
||||
} else if (rating >= 20) {
|
||||
return "RAINBOW_SHINY";
|
||||
} else if (rating >= 19) {
|
||||
return "RAINBOW";
|
||||
} else if (rating >= 14.5) {
|
||||
} else if (rating >= 18) {
|
||||
return "PLATINUM";
|
||||
} else if (rating >= 14) {
|
||||
} else if (rating >= 17) {
|
||||
return "GOLD";
|
||||
} else if (rating >= 13) {
|
||||
} else if (rating >= 15) {
|
||||
return "SILVER";
|
||||
} else if (rating >= 12) {
|
||||
} else if (rating >= 13) {
|
||||
return "COPPER";
|
||||
} else if (rating >= 10) {
|
||||
} else if (rating >= 11) {
|
||||
return "PURPLE";
|
||||
} else if (rating >= 7) {
|
||||
} else if (rating >= 9) {
|
||||
return "RED";
|
||||
} else if (rating >= 4) {
|
||||
} else if (rating >= 7) {
|
||||
return "ORANGE";
|
||||
} else if (rating >= 2) {
|
||||
} else if (rating >= 4) {
|
||||
return "GREEN";
|
||||
}
|
||||
|
||||
@@ -91,6 +134,8 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
},
|
||||
goalCriteriaFormatters: {
|
||||
score: GoalFmtScore,
|
||||
platinumScore: (val: number) => `Get ${val.toLocaleString("en-GB")} Platinum Score on`,
|
||||
platinumStars: (val: number) => `Get ${FmtStars(val)} on`,
|
||||
},
|
||||
goalProgressFormatters: {
|
||||
grade: (pb, gradeIndex) =>
|
||||
@@ -103,13 +148,18 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
noteLamp: (pb) => pb.scoreData.noteLamp,
|
||||
bellLamp: (pb) => pb.scoreData.bellLamp,
|
||||
score: (pb) => FmtNum(pb.scoreData.score),
|
||||
platinumScore: (pb) => FmtNum(pb.scoreData.platinumScore),
|
||||
platinumStars: (pb) => FmtStarsCompact(pb.scoreData.platinumStars),
|
||||
},
|
||||
goalOutOfFormatters: {
|
||||
score: GoalOutOfFmtScore,
|
||||
platinumScore: GoalOutOfFmtScore,
|
||||
platinumStars: () => FmtStarsCompact(6),
|
||||
},
|
||||
pbMergeFunctions: [
|
||||
CreatePBMergeFor("largest", "optional.platScore", "Best Platinum Score", (base, score) => {
|
||||
base.scoreData.optional.platScore = score.scoreData.optional.platScore;
|
||||
CreatePBMergeFor("largest", "platinumScore", "Best Platinum Score", (base, score) => {
|
||||
base.scoreData.platinumScore = score.scoreData.platinumScore;
|
||||
base.scoreData.platinumStars = score.scoreData.platinumStars;
|
||||
}),
|
||||
CreatePBMergeFor("largest", "enumIndexes.noteLamp", "Best Note Lamp", (base, score) => {
|
||||
base.scoreData.noteLamp = score.scoreData.noteLamp;
|
||||
@@ -120,15 +170,34 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
],
|
||||
defaultMergeRefName: "Best Score",
|
||||
scoreValidators: [
|
||||
(s) => {
|
||||
(s, chart) => {
|
||||
let { hit, miss } = s.scoreData.judgements;
|
||||
let rbreak = s.scoreData.judgements.break;
|
||||
|
||||
hit ??= 0;
|
||||
miss ??= 0;
|
||||
rbreak ??= 0;
|
||||
|
||||
if (s.scoreData.noteLamp === "ALL BREAK+") {
|
||||
if (hit + miss + rbreak > 0) {
|
||||
return "Cannot have an ALL BREAK+ if not all hits were critical break or better.";
|
||||
}
|
||||
|
||||
if (s.scoreData.score < 1010000) {
|
||||
return "Cannot have an ALL BREAK+ if the score is not 1,010,000";
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
s.scoreData.score === 1010000 &&
|
||||
(s.scoreData.noteLamp !== "ALL BREAK+" || s.scoreData.bellLamp !== "FULL BELL")
|
||||
) {
|
||||
return "Cannot have a perfect score without FBAB+";
|
||||
}
|
||||
|
||||
if (s.scoreData.noteLamp === "ALL BREAK") {
|
||||
if (hit + miss > 0) {
|
||||
return "Cannot have an ALL BREAK if not all hits were justice or better.";
|
||||
return "Cannot have an ALL BREAK if not all hits were break or better.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +206,14 @@ export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
|
||||
return "Cannot have a FULL COMBO if the score has misses.";
|
||||
}
|
||||
}
|
||||
|
||||
if (s.scoreData.bellLamp === "FULL BELL" && s.scoreData.noteLamp === "LOSS") {
|
||||
return "Cannot have a LOSS with a FULL BELL.";
|
||||
}
|
||||
|
||||
if (s.scoreData.platinumScore > chart.data.maxPlatScore) {
|
||||
return `Cannot have ${s.scoreData.platinumScore}/${chart.data.maxPlatScore} Platinum Score.`;
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import KtRatingToCuratorSkill from "./migrations/ktRating-to-curator-skill";
|
||||
import MarkFervidexAsWithoutIntent from "./migrations/mark-fervidex-as-without-intent";
|
||||
import MarkKsHookAsWithoutIntent from "./migrations/mark-kshook-as-without-intent";
|
||||
import NullLR2HookFailedBPs from "./migrations/null-lr2hook-failed-bps";
|
||||
import OngekiV2 from "./migrations/ongeki-v2";
|
||||
import RemoveAbove100pc from "./migrations/remove-above-100pc";
|
||||
import RemoveIIDXBeginners from "./migrations/remove-iidx-beginners";
|
||||
import RemoveIIDX2dxtraBeginners from "./migrations/remove-iidx-extra-beginners";
|
||||
@@ -73,7 +74,8 @@ if (Environment.nodeEnv !== "test") {
|
||||
KtRatingToCuratorSkill,
|
||||
RemoveIIDX2dxtraBeginners,
|
||||
RmHot,
|
||||
ChunithmLampSplit
|
||||
ChunithmLampSplit,
|
||||
OngekiV2
|
||||
// SdvxMaxxiveLampRearrange,
|
||||
// SdvxMaxxiveGoals
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import db from "external/mongo/db";
|
||||
import { rootLogger } from "lib/logger/logger";
|
||||
import UpdateScore from "lib/score-mutation/update-score";
|
||||
import { ONGEKI_NOTE_LAMPS, type ScoreDocument } from "tachi-common";
|
||||
import { EfficientDBIterate } from "utils/efficient-db-iterate";
|
||||
import type { Migration } from "utils/types";
|
||||
|
||||
const migration: Migration = {
|
||||
id: "ongeki-v2",
|
||||
up: async () => {
|
||||
const failedScores = [];
|
||||
|
||||
await EfficientDBIterate(
|
||||
db.scores,
|
||||
(score) => {
|
||||
return score as ScoreDocument<"ongeki:Single">;
|
||||
},
|
||||
async (scores: Array<ScoreDocument<"ongeki:Single">>) => {
|
||||
for (const score of scores) {
|
||||
// @ts-expect-error just incase
|
||||
delete score._id;
|
||||
|
||||
try {
|
||||
const newScore = {
|
||||
...score,
|
||||
};
|
||||
|
||||
if (newScore.scoreData.score === 1010000) {
|
||||
newScore.scoreData.noteLamp = "ALL BREAK+";
|
||||
newScore.scoreData.enumIndexes.noteLamp =
|
||||
ONGEKI_NOTE_LAMPS.ALL_BREAK_PLUS;
|
||||
}
|
||||
|
||||
if (
|
||||
"platScore" in newScore.scoreData.optional &&
|
||||
typeof newScore.scoreData.optional.platScore === "number"
|
||||
) {
|
||||
newScore.scoreData.platinumScore =
|
||||
newScore.scoreData.optional.platScore;
|
||||
delete newScore.scoreData.optional.platScore;
|
||||
} else {
|
||||
newScore.scoreData.platinumScore ??= 0;
|
||||
}
|
||||
|
||||
await UpdateScore(score, newScore);
|
||||
} catch (err) {
|
||||
rootLogger.warn(`Error applying ${score.scoreID}:`);
|
||||
rootLogger.warn(err);
|
||||
rootLogger.warn("Continuing through the error.");
|
||||
|
||||
failedScores.push(score.scoreID);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ game: "ongeki" }
|
||||
);
|
||||
|
||||
if (failedScores.length > 0) {
|
||||
throw new Error(
|
||||
`${failedScores.length} failed to be migrated. Resolve these manually, please.`
|
||||
);
|
||||
}
|
||||
},
|
||||
down: () => {
|
||||
throw new Error(`Reverting this change is not possible.`);
|
||||
},
|
||||
};
|
||||
|
||||
export default migration;
|
||||
@@ -186,7 +186,7 @@ async function GetSortedPBs(game: Game, playtype: Playtype, chartID: string) {
|
||||
{
|
||||
sort: {
|
||||
[`scoreData.score`]: -1,
|
||||
[`scoreData.optional.platScore`]: -1,
|
||||
[`scoreData.platinumScore`]: -1,
|
||||
timeAchieved: 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ t.test("#ConvertAPIMytOngeki", (t) => {
|
||||
score: 1003385,
|
||||
noteLamp: "CLEAR",
|
||||
bellLamp: "FULL BELL",
|
||||
platinumScore: 893,
|
||||
judgements: {
|
||||
cbreak: 967,
|
||||
break: 19,
|
||||
@@ -81,7 +82,6 @@ t.test("#ConvertAPIMytOngeki", (t) => {
|
||||
damage: 0,
|
||||
bellCount: 174,
|
||||
totalBellCount: 174,
|
||||
platScore: 893,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -163,20 +163,6 @@ t.test("#ConvertAPIMytOngeki", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("platinum score should be null if value is 0", async (t) => {
|
||||
t.hasStrict(
|
||||
await convert({
|
||||
info: {
|
||||
platinumScore: 0,
|
||||
},
|
||||
}),
|
||||
{
|
||||
dryScore: { scoreData: { optional: { platScore: null } } },
|
||||
}
|
||||
);
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject unspecified clear status", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
|
||||
@@ -28,7 +28,8 @@ const DIFFICULTIES = {
|
||||
|
||||
function getNoteLamp(
|
||||
comboStatus: number,
|
||||
clearStatus: number
|
||||
clearStatus: number,
|
||||
techScore: number
|
||||
): ScoreData<"ongeki:Single">["noteLamp"] | undefined {
|
||||
if (
|
||||
comboStatus === OngekiComboStatus.ONGEKI_COMBO_STATUS_UNSPECIFIED ||
|
||||
@@ -37,6 +38,10 @@ function getNoteLamp(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (techScore === 1010000) {
|
||||
return "ALL BREAK+";
|
||||
}
|
||||
|
||||
if (comboStatus === OngekiComboStatus.ONGEKI_COMBO_STATUS_ALL_BREAK) {
|
||||
return "ALL BREAK";
|
||||
}
|
||||
@@ -77,7 +82,7 @@ const ConvertAPIMytOngeki: ConverterFunction<MytOngekiScore, EmptyObject> = asyn
|
||||
);
|
||||
}
|
||||
|
||||
const noteLamp = getNoteLamp(data.info.comboStatus, data.info.clearStatus);
|
||||
const noteLamp = getNoteLamp(data.info.comboStatus, data.info.clearStatus, data.info.techScore);
|
||||
|
||||
if (noteLamp === undefined) {
|
||||
throw new InvalidScoreFailure(
|
||||
@@ -114,6 +119,7 @@ const ConvertAPIMytOngeki: ConverterFunction<MytOngekiScore, EmptyObject> = asyn
|
||||
score: data.info.techScore,
|
||||
noteLamp,
|
||||
bellLamp: data.info.isFullBell ? "FULL BELL" : "NONE",
|
||||
platinumScore: data.info.platinumScore,
|
||||
judgements: {
|
||||
cbreak: data.judge.judgeCriticalBreak,
|
||||
break: data.judge.judgeBreak,
|
||||
@@ -124,10 +130,6 @@ const ConvertAPIMytOngeki: ConverterFunction<MytOngekiScore, EmptyObject> = asyn
|
||||
damage: data.judge.damageCount,
|
||||
bellCount: data.judge.bellCount,
|
||||
totalBellCount: data.judge.totalBellCount,
|
||||
// Set platinum score to explicitly null if the value is 0
|
||||
// There is no distinction between 0 and null/undefined in gRPC
|
||||
// Handles playlogs from older versions when Platinum Score didn't exist
|
||||
platScore: data.info.platinumScore || null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1429,12 +1429,14 @@ export const TestingOngekiScorePB: PBScoreDocument<"ongeki:Single"> = {
|
||||
isPrimary: true,
|
||||
scoreData: {
|
||||
score: 1010000,
|
||||
noteLamp: "ALL BREAK",
|
||||
noteLamp: "ALL BREAK+",
|
||||
bellLamp: "FULL BELL",
|
||||
platinumScore: 1000,
|
||||
platinumStars: 6,
|
||||
grade: "SSS+",
|
||||
enumIndexes: {
|
||||
grade: 11,
|
||||
noteLamp: 3,
|
||||
noteLamp: 4,
|
||||
bellLamp: 1,
|
||||
},
|
||||
judgements: {},
|
||||
|
||||
Reference in New Issue
Block a user