feat: add itg calculations

This commit is contained in:
zkldi
2022-06-12 20:44:33 +01:00
parent 489237fec3
commit efab17a015
5 changed files with 95 additions and 7 deletions
@@ -13,8 +13,10 @@ import {
PoyashiBPI,
Volforce,
WACCARate,
ITGHighestUnbroken,
} from "rg-stats";
import { GetGamePTConfig } from "tachi-common";
import { IsNullish } from "utils/misc";
import type { DryScore } from "../common/types";
import type { KtLogger } from "lib/logger/logger";
import type { ChartDocument, Game, IDStrings, Lamps, Playtypes, ScoreDocument } from "tachi-common";
@@ -68,6 +70,7 @@ const CalculatedDataFunctions: CalculatedDataFunctionsType = {
"jubeat:Single": CalculateDataJubeat,
"pms:Keyboard": CalculateDataPMSorBMS,
"pms:Controller": CalculateDataPMSorBMS,
"itg:Stamina": CalculateDataITGStamina,
};
// Creates Game-Specific calculatedData for the provided game & playtype.
@@ -239,3 +242,33 @@ function CalculateDataPopn(
),
};
}
function CalculateDataITGStamina(
dryScore: DryScore<"itg:Stamina">,
chart: ChartDocument<"itg:Stamina">
): CalculatedData<"itg:Stamina"> {
// If the user failed -- and we don't know when, return null for everything.
if (dryScore.scoreData.lamp === "FAILED" && IsNullish(dryScore.scoreData.hitMeta.diedAt)) {
return {
blockRating: null,
highest32: null,
highest256: null,
};
}
return {
blockRating: chart.levelNum,
highest32: ITGHighestUnbroken.calculateFromNPSPerMeasure(
chart.data.breakdown.npsPerMeasure,
chart.data.breakdown.notesPerMeasure,
dryScore.scoreData.hitMeta.diedAt,
32
),
highest256: ITGHighestUnbroken.calculateFromNPSPerMeasure(
chart.data.breakdown.npsPerMeasure,
chart.data.breakdown.notesPerMeasure,
dryScore.scoreData.hitMeta.diedAt,
256
),
};
}
@@ -1,6 +1,7 @@
import type {
Game,
IDStrings,
integer,
Playtype,
ScoreDocument,
SessionCalculatedDataLookup,
@@ -9,20 +10,18 @@ import type {
type ScoreCalculatedDataOnly = Pick<ScoreDocument, "calculatedData">;
const RELEVANT_SCORES = 10;
function AverageBest10(vals: Array<number | null | undefined>) {
function AverageBestN(vals: Array<number | null | undefined>, n = 10) {
const numbers = vals.filter((e) => typeof e === "number") as Array<number>;
if (numbers.length < RELEVANT_SCORES) {
if (numbers.length < n) {
return null;
}
return (
numbers
.sort((a, b) => b - a)
.slice(0, RELEVANT_SCORES)
.reduce((a, e) => a + e, 0) / RELEVANT_SCORES
.slice(0, n)
.reduce((a, e) => a + e, 0) / n
);
}
@@ -40,7 +39,21 @@ function AvgBest10Map(
arr: Array<ScoreCalculatedDataOnly>,
prop: keyof ScoreDocument["calculatedData"]
) {
return AverageBest10(arr.map((e) => e.calculatedData[prop]));
return AverageBestN(
arr.map((e) => e.calculatedData[prop]),
10
);
}
function AvgBestNMap(
arr: Array<ScoreCalculatedDataOnly>,
prop: keyof ScoreDocument["calculatedData"],
n: integer
) {
return AverageBestN(
arr.map((e) => e.calculatedData[prop]),
n
);
}
type SessionCalcDataFn = (
@@ -107,6 +120,11 @@ function GetGPTSessionCalcDataFn(game: Game, playtype: Playtype): SessionCalcDat
});
case "maimai:Single":
return () => ({});
case "itg:Stamina":
return (scd) => ({
blockRating: AvgBestNMap(scd, "blockRating", 5),
average32Speed: AvgBestNMap(scd, "highest32", 5),
});
}
}
@@ -67,6 +67,7 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = {
Single: CalculateJubeatColour,
},
pms: null,
itg: null,
};
/**
@@ -153,6 +153,13 @@ function GetGPTRatingFunction(game: Game, playtype: Playtype): RatingFunction {
jubility: await CalculateJubility(g, p, u, l),
naiveJubility: await LazySumN("jubility", 60)(g, p, u),
});
case "itg:Stamina":
// HighestX is equivalent to summing 1.
return async (g, p, u) => ({
highestBlock: await LazySumN("blockRating", 1, true)(g, p, u),
highest32: await LazySumN("highest32", 1, true)(g, p, u),
highest256: await LazySumN("highest256", 1, true)(g, p, u),
});
}
}
@@ -14,6 +14,7 @@ import {
FindChartWithPTDF,
FindChartWithPTDFVersion,
FindDDRChartOnSongHash,
FindITGChartOnHash,
} from "utils/queries/charts";
import { FindSongOnID, FindSongOnTitleInsensitive } from "utils/queries/songs";
import type { DryScore } from "../../../framework/common/types";
@@ -172,6 +173,34 @@ export async function ResolveMatchTypeToKTData(
return { chart, song };
}
case "itgChartHash": {
if (game !== "itg") {
throw new InvalidScoreFailure(`Cannot use itgChartHash lookup on ${game}.`);
}
const chart = await FindITGChartOnHash(data.identifier);
if (!chart) {
throw new KTDataNotFoundFailure(
`Cannot find chart for itgChartHash ${data.identifier}.`,
importType,
data,
context
);
}
const song = await FindSongOnID(game, chart.songID);
if (!song) {
logger.severe(`ITG songID ${chart.songID} has charts but no parent song.`);
throw new InternalFailure(
`ITG songID ${chart.songID} has charts but no parent song.`
);
}
return { song, chart };
}
case "ddrSongHash": {
if (game !== "ddr") {
throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`);