diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts index 410469bef..8130ee411 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts @@ -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 + ), + }; +} diff --git a/server/src/lib/score-import/framework/sessions/calculated-data.ts b/server/src/lib/score-import/framework/sessions/calculated-data.ts index 98731c01d..483de3a5c 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.ts @@ -1,6 +1,7 @@ import type { Game, IDStrings, + integer, Playtype, ScoreDocument, SessionCalculatedDataLookup, @@ -9,20 +10,18 @@ import type { type ScoreCalculatedDataOnly = Pick; -const RELEVANT_SCORES = 10; - -function AverageBest10(vals: Array) { +function AverageBestN(vals: Array, n = 10) { const numbers = vals.filter((e) => typeof e === "number") as Array; - 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, 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, + 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), + }); } } diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.ts b/server/src/lib/score-import/framework/user-game-stats/classes.ts index 0a0811da9..865f4d835 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.ts @@ -67,6 +67,7 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = { Single: CalculateJubeatColour, }, pms: null, + itg: null, }; /** diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.ts b/server/src/lib/score-import/framework/user-game-stats/rating.ts index d722172d5..0ad94f750 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.ts @@ -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), + }); } } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts index bf68a85bd..f431c28bc 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts @@ -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}.`);