diff --git a/server/package.json b/server/package.json index 35467a1c9..3eb966d92 100644 --- a/server/package.json +++ b/server/package.json @@ -79,6 +79,7 @@ "prudence": "0.9.7", "rate-limit-redis": "2.1.0", "redis": "3.1.2", + "rg-stats": "^0.2.0", "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", "seq-logging": "1.1.1", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 5f649d4e6..32cca65a8 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -59,6 +59,7 @@ specifiers: prudence: 0.9.7 rate-limit-redis: 2.1.0 redis: 3.1.2 + rg-stats: ^0.2.0 rimraf: 3.0.2 safe-json-stringify: 1.2.0 seq-logging: 1.1.1 @@ -104,6 +105,7 @@ dependencies: prudence: 0.9.7 rate-limit-redis: 2.1.0 redis: 3.1.2 + rg-stats: 0.2.0 rimraf: 3.0.2 safe-json-stringify: 1.2.0 seq-logging: 1.1.1 @@ -4976,6 +4978,10 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true + /rg-stats/0.2.0: + resolution: {integrity: sha512-d+IFYlryQMZjMYVJgL0vIZCdUpkYhUC9vzfMHOs/HNEeM5jjlVLDyuiru4UJlc67XoqGgby2Mp6IkcFoRXz4Gw==} + dev: false + /rimraf/2.4.5: resolution: {integrity: sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=} hasBin: true 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 b33d968f4..9fa8b37f0 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 @@ -11,17 +11,20 @@ import { } from "tachi-common"; import { DryScore } from "../common/types"; import { - CalculateBPI, - CalculateCHUNITHMRating, - CalculateGITADORASkill, - CalculateJubility, CalculateKTLampRatingIIDX, CalculateKTRating, CalculateMFCP, CalculateSieglinde, - CalculateVF6, - CalculateWACCARate, } from "./stats"; +import { + PoyashiBPI, + Volforce, + CHUNITHMRating, + GITADORASkill, + Jubility, + PopnClassPoints, + WACCARate, +} from "rg-stats"; export async function CreateCalculatedData( dryScore: DryScore, @@ -53,8 +56,8 @@ type CalculatedDataFunctions = { }; const CalculatedDataFunctions: CalculatedDataFunctions = { - "iidx:SP": CalculateDataIIDXSP, - "iidx:DP": CalculateDataIIDXDP, + "iidx:SP": CalculateDataIIDX, + "iidx:DP": CalculateDataIIDX, "sdvx:Single": CalculateDataSDVXorUSC, "popn:9B": CalculateDataPopn, "museca:Single": CalculateDataMuseca, @@ -92,18 +95,18 @@ export async function CalculateDataForGamePT( type CalculatedData = Required["calculatedData"]>; -function CalculateDataIIDXSP( +function CalculateDataIIDX( dryScore: DryScore, - chart: ChartDocument<"iidx:SP">, + chart: ChartDocument<"iidx:SP" | "iidx:DP">, logger: KtLogger ): CalculatedData<"iidx:SP"> { let bpi; if (chart.data.kaidenAverage && chart.data.worldRecord) { - bpi = CalculateBPI( + bpi = PoyashiBPI.calculate( + dryScore.scoreData.score, chart.data.kaidenAverage, chart.data.worldRecord, - dryScore.scoreData.score, (chart as ChartDocument<"iidx:SP">).data.notecount * 2, chart.data.bpiCoefficient ); @@ -115,34 +118,10 @@ function CalculateDataIIDXSP( return { BPI: bpi, - ktLampRating: CalculateKTLampRatingIIDX(dryScore, "SP", chart as ChartDocument<"iidx:SP">), - }; -} - -function CalculateDataIIDXDP( - dryScore: DryScore, - chart: ChartDocument<"iidx:SP" | "iidx:DP">, - logger: KtLogger -): CalculatedData<"iidx:DP"> { - let bpi; - - if (chart.data.kaidenAverage && chart.data.worldRecord) { - bpi = CalculateBPI( - chart.data.kaidenAverage, - chart.data.worldRecord, - dryScore.scoreData.score, - (chart as ChartDocument<"iidx:DP">).data.notecount * 2, - chart.data.bpiCoefficient - ); - - // kesdc = esd === null ? null : CalculateKESDC(BPIData.kesd, esd); disabled - } else { - bpi = null; - } - - return { - BPI: bpi, - ktLampRating: CalculateKTLampRatingIIDX(dryScore, "DP", chart as ChartDocument<"iidx:DP">), + ktLampRating: CalculateKTLampRatingIIDX( + dryScore, + chart as ChartDocument<"iidx:SP" | "iidx:DP"> + ), }; } @@ -159,17 +138,13 @@ function CalculateDataSDVXorUSC( return { VF6: null }; } - const VF6 = CalculateVF6( - dryScore.scoreData.grade as Grades["sdvx:Single"], + const VF6 = Volforce.calculateVF6( + dryScore.scoreData.score, dryScore.scoreData.lamp as Lamps["sdvx:Single"], - dryScore.scoreData.percent, - chart.levelNum, - logger + chart.levelNum ); - return { - VF6, - }; + return { VF6 }; } function CalculateDataMuseca( @@ -188,7 +163,7 @@ function CalculateDataJubeat( logger: KtLogger ): CalculatedData<"jubeat:Single"> { return { - jubility: CalculateJubility( + jubility: Jubility.calculate( dryScore.scoreData.score, dryScore.scoreData.percent, chart.levelNum @@ -201,7 +176,7 @@ function CalculateDataCHUNITHM( chart: ChartDocument ): CalculatedData<"chunithm:Single"> { return { - rating: CalculateCHUNITHMRating(dryScore, chart), + rating: CHUNITHMRating.calculate(dryScore.scoreData.score, chart.levelNum), }; } @@ -221,7 +196,7 @@ function CalculateDataGitadora( chart: ChartDocument ): CalculatedData<"gitadora:Gita" | "gitadora:Dora"> { return { - skill: CalculateGITADORASkill(dryScore, chart), + skill: GITADORASkill.calculate(dryScore.scoreData.percent, chart.levelNum), }; } @@ -255,32 +230,21 @@ function CalculateDataWACCA( chart: ChartDocument, logger: KtLogger ): CalculatedData<"wacca:Single"> { - const rate = CalculateWACCARate(dryScore.scoreData.score, chart.levelNum); + const rate = WACCARate.calculate(dryScore.scoreData.score, chart.levelNum); return { rate }; } function CalculateDataPopn( - dryScore: DryScore, - chart: ChartDocument, + dryScore: DryScore<"popn:9B">, + chart: ChartDocument<"popn:9B">, logger: KtLogger ): CalculatedData<"popn:9B"> { - const score = dryScore.scoreData.score; - const lamp = dryScore.scoreData.lamp; - - if (score <= 50000) { - return { classPoints: 0 }; - } - - let clearBonus = 0; - - if (lamp === "CLEAR" || lamp === "EASY CLEAR") { - clearBonus = 3000; - } else if (lamp === "FULL COMBO" || lamp === "PERFECT") { - clearBonus = 5000; - } - return { - classPoints: (10_000 * chart.levelNum + score - 50_000 + clearBonus) / 5440, + classPoints: PopnClassPoints.calculate( + dryScore.scoreData.score, + dryScore.scoreData.lamp, + chart.levelNum + ), }; } diff --git a/server/src/lib/score-import/framework/calculated-data/stats.test.ts b/server/src/lib/score-import/framework/calculated-data/stats.test.ts index 34af47d4f..8dadf4cc8 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.test.ts @@ -1,212 +1,9 @@ import CreateLogCtx from "lib/logger/logger"; import { ChartDocument, Difficulties, Lamps, ScoreDocument } from "tachi-common"; import t from "tap"; -import { isApproximately } from "test-utils/asserts"; -import { CHUNITHMBBKKChart, Testing511SPA, TestingIIDXSPDryScore } from "test-utils/test-data"; +import { Testing511SPA, TestingIIDXSPDryScore } from "test-utils/test-data"; import { DryScore } from "../common/types"; -import { - CalculateBPI, - CalculateCHUNITHMRating, - CalculateGITADORASkill, - CalculateKTLampRatingIIDX, - CalculateMFCP, - CalculateVF6, - CalculateWACCARate, -} from "./stats"; -import deepmerge from "deepmerge"; - -t.test("#CalculateBPI", (t) => { - t.test("AA BPI tests", (t) => { - // data accurate as of 14/04/2021 - const WR_AA = 3650; - const KAVG_AA = 3204; - const MAX_AA = 1834 * 2; - const COEF_AA = 1.25945; - - // AA, the song. - function AA_BPI(yourEx: number) { - return CalculateBPI(KAVG_AA, WR_AA, yourEx, MAX_AA, COEF_AA); - } - - t.equal(AA_BPI(KAVG_AA), 0, "A score of KAVG should be exactly 0BPI"); - isApproximately( - t, - AA_BPI(3393), - 10.02, - "A score with 3393 should be approximately 10.02BPI" - ); - isApproximately( - t, - AA_BPI(3481), - 20.09, - "A score with 3481 should be approximately 20.09BPI" - ); - isApproximately( - t, - AA_BPI(3535), - 30.01, - "A score with 3535 should be approximately 30.01BPI" - ); - isApproximately( - t, - AA_BPI(3572), - 40.18, - "A score with 3572 should be approximately 40.18BPI" - ); - isApproximately(t, AA_BPI(3597), 50.1, "A score with 3597 should be approximately 50.1BPI"); - isApproximately( - t, - AA_BPI(3615), - 60.12, - "A score with 3615 should be approximately 60.12BPI" - ); - isApproximately( - t, - AA_BPI(3628), - 70.11, - "A score with 3628 should be approximately 70.11BPI" - ); - isApproximately( - t, - AA_BPI(3638), - 80.62, - "A score with 3638 should be approximately 80.62BPI" - ); - isApproximately( - t, - AA_BPI(3645), - 90.59, - "A score with 3645 should be approximately 90.59BPI" - ); - t.equal(AA_BPI(WR_AA), 100, "A score of WR should be exactly 100BPI"); - - isApproximately(t, AA_BPI(3041), -5, "A score with 3628 should be approximately -5BPI"); - isApproximately( - t, - AA_BPI(2886), - -9.99, - "A score with 3638 should be approximately -9.99BPI" - ); - - t.equal(AA_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI"); - - isApproximately( - t, - AA_BPI(MAX_AA), - 244.56, - "A score of MAX should be approximately 244.56BPI" - ); - - t.end(); - }); - - t.test("COLOSSEUM BPI Tests", (t) => { - const WR_AFT = 2312; - const KAVG_CL = 2131; - const MAX_CL = 2318; - const COEF_CL = -1; - - function CL_BPI(yourEx: number) { - return CalculateBPI(KAVG_CL, WR_AFT, yourEx, MAX_CL, COEF_CL); - } - - isApproximately(t, CL_BPI(2307), 79.63); - - t.end(); - }); - - // This song has both no co-efficient and is an sp11, so it serves - // as another interesting test. - t.test("Afterimage d'automne BPI tests", (t) => { - // data accurate as of 14/04/2021 - const WR_AFT = 2891; - const KAVG_AFT = 2497; - const MAX_AFT = 1480 * 2; - const COEF_AFT = null; - - function AFT_BPI(yourEx: number) { - return CalculateBPI(KAVG_AFT, WR_AFT, yourEx, MAX_AFT, COEF_AFT); - } - - t.equal(AFT_BPI(KAVG_AFT), 0, "A score of KAVG should be exactly 0BPI"); - isApproximately(t, AFT_BPI(2606), 10, "A score with 2606 should be approximately 10BPI"); - isApproximately( - t, - AFT_BPI(2675), - 20.07, - "A score with 2675 should be approximately 20.07BPI" - ); - isApproximately( - t, - AFT_BPI(2727), - 30.18, - "A score with 2727 should be approximately 30.18BPI" - ); - isApproximately( - t, - AFT_BPI(2767), - 40.12, - "A score with 2767 should be approximately 40.12BPI" - ); - isApproximately( - t, - AFT_BPI(2799), - 50.06, - "A score with 2799 should be approximately 50.06BPI" - ); - isApproximately( - t, - AFT_BPI(2826), - 60.43, - "A score with 2826 should be approximately 60.43BPI" - ); - isApproximately( - t, - AFT_BPI(2847), - 70.3, - "A score with 2847 should be approximately 70.3BPI" - ); - isApproximately( - t, - AFT_BPI(2865), - 80.57, - "A score with 2865 should be approximately 80.57BPI" - ); - isApproximately( - t, - AFT_BPI(2879), - 90.18, - "A score with 2879 should be approximately 90.18BPI" - ); - t.equal(AFT_BPI(WR_AFT), 100, "A score of WR should be exactly 100BPI"); - - isApproximately( - t, - AFT_BPI(2423), - -4.99, - "A score with 2423 should be approximately -4.99BPI" - ); - isApproximately( - t, - AFT_BPI(2354), - -10.04, - "A score with 2354 should be approximately -10.04BPI" - ); - - t.equal(AFT_BPI(0), -15, "Excessively bad (vs kavg) scores should cap at -15BPI"); - - isApproximately( - t, - AFT_BPI(MAX_AFT), - 431.57, - "A score of MAX should be approximately 431.57BPI" - ); - - t.end(); - }); - - t.end(); -}); +import { CalculateKTLampRatingIIDX, CalculateMFCP } from "./stats"; t.test("#CalculateKTLampRatingIIDX", (t) => { function c( @@ -238,61 +35,61 @@ t.test("#CalculateKTLampRatingIIDX", (t) => { } t.equal( - CalculateKTLampRatingIIDX(s("CLEAR"), "SP", c(10.5, 10.6, 10.7)), + CalculateKTLampRatingIIDX(s("CLEAR"), c(10.5, 10.6, 10.7)), 10.5, "Should return NC if the score was NC." ); t.equal( - CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(10.5, 10.6, 10.7)), + CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(10.5, 10.6, 10.7)), 10.6, "Should return HC if the score was HC." ); t.equal( - CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), "SP", c(10.5, 10.6, 10.7)), + CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), c(10.5, 10.6, 10.7)), 10.7, "Should return EXHC if the score was EXHC." ); t.equal( - CalculateKTLampRatingIIDX(s("CLEAR"), "SP", c(null, 10.6, 10.7)), + CalculateKTLampRatingIIDX(s("CLEAR"), c(null, 10.6, 10.7)), 0, "Should return 0 if the score was NC but no NC was available." ); t.equal( - CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(null, null, 10.7)), + CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(null, null, 10.7)), 0, "Should return 0 if the score was HC but no HC or NC was available." ); t.equal( - CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(10.5, null, 10.7)), + CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(10.5, null, 10.7)), 10.5, "Should return NC if the score was HC but no HC was available." ); t.equal( - CalculateKTLampRatingIIDX(s("HARD CLEAR"), "SP", c(10.9, 10.5, 10.7)), + CalculateKTLampRatingIIDX(s("HARD CLEAR"), c(10.9, 10.5, 10.7)), 10.9, "Should return NC if the score was HC but NC was worth more." ); t.equal( - CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), "SP", c(10.9, 10.5, 10.7)), + CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), c(10.9, 10.5, 10.7)), 10.9, "Should return NC if the score was EXHC but NC was worth more." ); t.equal( - CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), "SP", c(10.4, 10.9, 10.7)), + CalculateKTLampRatingIIDX(s("EX HARD CLEAR"), c(10.4, 10.9, 10.7)), 10.9, "Should return HC if the score was EXHC but HC was worth more." ); t.equal( - CalculateKTLampRatingIIDX(s("CLEAR"), "SP", c(null, null, null)), + CalculateKTLampRatingIIDX(s("CLEAR"), c(null, null, null)), 10, "Should return chart level if the chart has no tierlist data." ); @@ -300,29 +97,6 @@ t.test("#CalculateKTLampRatingIIDX", (t) => { t.end(); }); -t.test("#CalculateGITADORARating", (t) => { - function TestGitadoraRating(percent: number, levelNum: number) { - return CalculateGITADORASkill( - { scoreData: { percent } } as DryScore, - { levelNum } as ChartDocument - ); - } - - t.equal( - TestGitadoraRating(89.48, 3.4), - 60.84, - "Test GITADORA Rating function aligns with game (1)" - ); - - t.equal( - TestGitadoraRating(70.76, 5.8), - 82.08, - "Test GITADORA Rating function aligns with game (2)" - ); - - t.end(); -}); - const logger = CreateLogCtx(__filename); t.test("#CalculateMFCP", (t) => { @@ -436,103 +210,6 @@ t.test("#CalculateMFCP", (t) => { t.end(); }); -const bbkk = CHUNITHMBBKKChart; - -// unit testing a mathematical function is a square-round-hole problem. -// that said, we can blast some stuff at it we know to be correct, -// and just hope we're right. -t.test("#CalculateCHUNITHMRating", (t) => { - const chart = deepmerge(bbkk, { levelNum: 12.5 }); - - // cutoffs - t.equal( - CalculateCHUNITHMRating({ scoreData: { score: 1_010_000 } } as DryScore, chart), - 12.5 + 2 - ); - t.equal( - CalculateCHUNITHMRating({ scoreData: { score: 1_005_000 } } as DryScore, chart), - 12.5 + 1.5 - ); - t.equal( - CalculateCHUNITHMRating({ scoreData: { score: 1_000_000 } } as DryScore, chart), - 12.5 + 1 - ); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 975_000 } } as DryScore, chart), 12.5); - t.equal( - CalculateCHUNITHMRating({ scoreData: { score: 925_000 } } as DryScore, chart), - 12.5 - 3 - ); - t.equal( - CalculateCHUNITHMRating({ scoreData: { score: 900_000 } } as DryScore, chart), - 12.5 - 5 - ); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 800_000 } } as DryScore, chart), 3.75); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 0 } } as DryScore, chart), 0); - - // inbetweens - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 987_000 } } as DryScore, chart), 12.98); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_008_000 } } as DryScore, chart), 14.5); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 1_003_000 } } as DryScore, chart), 13.8); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 999_000 } } as DryScore, chart), 13.46); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 980_000 } } as DryScore, chart), 12.7); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 950_000 } } as DryScore, chart), 11); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 920_000 } } as DryScore, chart), 9.1); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 810_000 } } as DryScore, chart), 4.12); - t.equal(CalculateCHUNITHMRating({ scoreData: { score: 50_000 } } as DryScore, chart), 0); - - t.end(); -}); - -// Random assertions plucked from bemaniwiki. -t.test("#CalculateVF6", (t) => { - t.equal(CalculateVF6("S", "CLEAR", 99, 16, logger), 0.332); - - t.equal(CalculateVF6("AAA+", "ULTIMATE CHAIN", 98, 17, logger), 0.356); - - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 16, logger), 0.369); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 17, logger), 0.392); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 18, logger), 0.415); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 19, logger), 0.438); - t.equal(CalculateVF6("S", "PERFECT ULTIMATE CHAIN", 100, 20, logger), 0.462); - - t.end(); -}); - -t.test("#CalculateWACCARate", (t) => { - // General Tests - t.equal(CalculateWACCARate(1_000_000, 10.2), 40.8); - t.equal(CalculateWACCARate(990_000, 10.2), 40.8, "990K and 1M should give the same rate."); - t.equal(CalculateWACCARate(980_000, 10.7), 40.125); - t.equal( - CalculateWACCARate(1_000_000, 0), - 0, - "A chart with level of 0 should always be worth 0." - ); - t.equal( - CalculateWACCARate(975_000, 10), - CalculateWACCARate(970_000, 10), - "Should be no difference between 975k and 970k." - ); - t.equal(CalculateWACCARate(960_000, 9), 29.25); - t.equal(CalculateWACCARate(950_000, 8), 24); - t.equal(CalculateWACCARate(940_000, 8), 22); - t.equal(CalculateWACCARate(920_000, 8), 20); - t.equal(CalculateWACCARate(900_000, 8), 16); - t.equal(CalculateWACCARate(850_000, 8), 12); - t.equal(CalculateWACCARate(500, 10), 10); - t.equal(CalculateWACCARate(0, 10), 10); - t.equal(CalculateWACCARate(849_999, 10), 10); - - // A bunch of assertions from cg505's account - t.equal(CalculateWACCARate(990_084, 13.2), 52.8); - t.equal(CalculateWACCARate(984_040, 12.9), 48.375); - t.equal(CalculateWACCARate(950_326, 12.2), 36.6); - t.equal(CalculateWACCARate(997_719, 4), 16); - t.equal(CalculateWACCARate(906440, 13.8), 27.6); - - t.end(); -}); - t.skip("#CalculateKTRating", (t) => { // t.test("Should call the success calculator if percent > pivotPercent", async (t) => { // const r = await CalculateKTRating( diff --git a/server/src/lib/score-import/framework/calculated-data/stats.ts b/server/src/lib/score-import/framework/calculated-data/stats.ts index 697d5d411..0836420a0 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.ts @@ -12,144 +12,6 @@ import { import { RoundToNDecimalPlaces } from "utils/misc"; import { DryScore } from "../common/types"; -/** - * Calculates the in-game CHUNITHM rating for a score. - */ -export function CalculateCHUNITHMRating(dryScore: DryScore, chartData: ChartDocument) { - const score = dryScore.scoreData.score; - const levelBase = chartData.levelNum * 100; - - let val = 0; - - if (score >= 1_007_500) { - val = levelBase + 200; - } else if (score >= 1_005_000) { - val = levelBase + 150 + ((score - 1_005_000) * 10) / 500; - } else if (score >= 1_000_000) { - val = levelBase + 100 + ((score - 1_000_000) * 5) / 500; - } else if (score >= 975_000) { - val = levelBase + ((score - 975_000) * 2) / 500; - } else if (score >= 925_000) { - val = levelBase - 300 + ((score - 925_000) * 3) / 500; - } else if (score >= 900_000) { - val = levelBase - 500 + ((score - 900_000) * 4) / 500; - } else if (score >= 800_000) { - val = (levelBase - 500) / 2 + ((score - 800_000) * ((levelBase - 500) / 2)) / 100_000; - } - - return Math.max(Math.floor(val) / 100, 0); -} - -export function CalculateJubility(score: number, musicRate: number, level: number) { - if (score < 700_000) { - return 0; - } - - return level * 12.5 * (musicRate / 99); -} - -export function CalculateWACCARate(score: number, levelNum: number) { - let scoreCoef = 1; - - // See https://github.com/TNG-dev/tachi-server/issues/598. - // This is genuinely how the game works. - if (score >= 990_000) { - scoreCoef = 4; - } else if (score >= 980_000) { - scoreCoef = 3.75; - } else if (score >= 970_000) { - scoreCoef = 3.5; - } else if (score >= 960_000) { - scoreCoef = 3.25; - } else if (score >= 950_000) { - scoreCoef = 3; - } else if (score >= 940_000) { - scoreCoef = 2.75; - } else if (score >= 920_000) { - scoreCoef = 2.5; - } else if (score >= 900_000) { - scoreCoef = 2; - } else if (score >= 850_000) { - scoreCoef = 1.5; - } - - // That's... it?? - // How discrete. How boring! - return RoundToNDecimalPlaces(scoreCoef * levelNum, 3); -} - -/** - * Calculates the in-game GITADORA rating for a score. - */ -export function CalculateGITADORASkill(dryScore: DryScore, chartData: ChartDocument) { - const trueRating = (dryScore.scoreData.percent / 100) * chartData.levelNum * 20; - const flooredRating = Math.floor(trueRating * 100) / 100; - return flooredRating; -} - -/** - * Calculates the PikaGreatFunction, used in BPI. I have no idea what this does. - * @returns - */ -function BPIPikaGreatFn(score: integer, max: integer) { - return score === max ? max * 0.8 : 1 + (score / max - 0.5) / (1 - score / max); -} - -/** - * Oh boy. - * - * Calculates the "Beat Performance Index" of an IIDX score. This algorithm has many issues, - * but is a direct port of Poyashi's implementation for consistencies sake. - * https://github.com/potakusan/iidx_score_manager/blob/f21ba6b85fcc0bf8b7ca888fa2239a3951a9c9c2/src/components/bpi/index.tsx#L120 - * - * @param kaidenEx The kaiden average EX score. - * @param wrEx The world record's EX score. - * @param yourEx Your EX score. - * @param max The maximum amount of EX achievable on this chart. - * @param powCoef What power the BPI should be raised to. This is arbitrary, and assigned on a per-song basis. Defaults to 1.175. - * @returns A number between -15 and 100. Unless your score is better than the world record, in which case - * returns can be above 100. - */ -export function CalculateBPI( - kaidenEx: integer, - wrEx: integer, - yourEx: integer, - max: integer, - pc: number | null -) { - let powCoef = pc ?? 1.175; - if (powCoef === -1) { - powCoef = 1.175; - } - - const yourPGF = BPIPikaGreatFn(yourEx, max); - const kaidenPGF = BPIPikaGreatFn(kaidenEx, max); - const wrPGF = BPIPikaGreatFn(wrEx, max); - - // no idea what these var names are - const _s_ = yourPGF / kaidenPGF; - const _z_ = wrPGF / kaidenPGF; - - const isBetterThanKavg = yourEx >= kaidenEx; - - // this line of code isn't mine, and that's why it's *really* bad here. - const bpi = - Math.round( - (isBetterThanKavg ? 100 : -100) * - Math.pow( - (isBetterThanKavg ? Math.log(_s_) : -Math.log(_s_)) / Math.log(_z_), - powCoef - ) * - 100 - ) / 100; - - if (bpi < -15) { - return -15; - } - - return bpi; -} - /** * Calculate Marvelous Full Combo Points. This algorithm * is used in LIFE4, and described here: @@ -190,63 +52,6 @@ export function CalculateMFCP(dryScore: DryScore, chartData: ChartDocument, logg return null; } -const VF5GradeCoefficients = { - PUC: 1.05, - S: 1.05, - "AAA+": 1.02, - AAA: 1.0, - "AA+": 0.97, - AA: 0.94, - "A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki. - A: 0.88, - B: 0.85, - C: 0.82, - D: 0.8, -}; - -const VF5LampCoefficients = { - "PERFECT ULTIMATE CHAIN": 1.1, - "ULTIMATE CHAIN": 1.05, - "EXCESSIVE CLEAR": 1.02, - CLEAR: 1.0, - FAILED: 0.5, -}; - -function FloorToNDP(number: number, dp: integer) { - const mul = 10 ** dp; - return Math.floor(number * mul) / mul; -} - -export function CalculateVF6( - grade: Grades["sdvx:Single"], - lamp: Lamps["sdvx:Single"], - per: number, - levelNum: number, - logger: KtLogger -) { - const gradeCoefficient = VF5GradeCoefficients[grade]; - const lampCoefficient = VF5LampCoefficients[lamp]; - - if (!lampCoefficient) { - logger.warn(`Invalid lamp of ${lamp} passed to CalculateVF5. Returning null.`); - return null; - } - - if (!gradeCoefficient) { - logger.warn(`Invalid grade of ${grade} passed to CalculateVF5. Returning null.`); - return null; - } - - const percent = per / 100; - if (!levelNum || !percent) { - return 0; - } - - const realVF6 = (levelNum * 2 * percent * gradeCoefficient * lampCoefficient) / 100; - - return FloorToNDP(realVF6, 3); -} - interface RatingParameters { failHarshnessMultiplier: number; pivotPercent: number; @@ -351,7 +156,6 @@ export function CalculateKTRating( export function CalculateKTLampRatingIIDX( dryScore: DryScore, - playtype: Playtypes[Game], chart: ChartDocument<"iidx:SP" | "iidx:DP"> ) { const ncValue = chart.tierlistInfo["kt-NC"]?.value ?? 0; @@ -359,12 +163,12 @@ export function CalculateKTLampRatingIIDX( const exhcValue = Math.max(chart.tierlistInfo["kt-EXHC"]?.value ?? 0, hcValue); if (!exhcValue && !hcValue && !ncValue) { - return LampRatingNoTierlistInfo(dryScore, "iidx", playtype, chart); + return LampRatingNoTierlistInfo(dryScore, "iidx", chart.playtype, chart); } const lamp = dryScore.scoreData.lamp; - const gptConfig = GetGamePTConfig("iidx", playtype); + const gptConfig = GetGamePTConfig("iidx", chart.playtype); const lampIndex = gptConfig.lamps.indexOf(lamp); @@ -413,58 +217,3 @@ export function CalculateSieglinde(chart: ChartDocument, lampIndex: integer) { return 0; } - -// deprecated calcs - -// const VF4GradeCoefficients = { -// S: 1.0, -// "AAA+": 0.99, -// AAA: 0.98, -// "AA+": 0.97, -// AA: 0.96, -// "A+": 0.95, -// A: 0.94, -// B: 0.93, -// C: 0.92, -// D: 0.91, -// }; - -// export function CalculateVF4( -// grade: Grades["sdvx:Single"], -// per: number, -// levelNum: number, -// logger: KtLogger -// ) { -// const multiplier = 25; - -// const gradeCoefficient = VF4GradeCoefficients[grade]; - -// if (!gradeCoefficient) { -// logger.warn(`Invalid grade of ${grade} passed to CalculateVF4. Returning null.`); -// return null; -// } - -// const percent = per / 100; -// if (!levelNum || !percent) { -// return 0; -// } - -// return Math.floor(multiplier * (levelNum + 1) * percent * gradeCoefficient); -// } - -// // VF5 is just VF6 but to three decimal places instead of four. -// export function CalculateVF5( -// grade: Grades["sdvx:Single"], -// lamp: Lamps["sdvx:Single"], -// per: number, -// levelNum: number, -// logger: KtLogger -// ) { -// const vf6 = CalculateVF6(grade, lamp, per, levelNum, logger); - -// if (vf6 === null) { -// return null; -// } - -// return FloorToNDP(vf6, 2); -// } diff --git a/server/src/lib/score-import/framework/pb/game-specific-merge.ts b/server/src/lib/score-import/framework/pb/game-specific-merge.ts index 3004d8732..aa98f74c4 100644 --- a/server/src/lib/score-import/framework/pb/game-specific-merge.ts +++ b/server/src/lib/score-import/framework/pb/game-specific-merge.ts @@ -2,8 +2,8 @@ import db from "external/mongo/db"; import { KtLogger } from "lib/logger/logger"; import { PBScoreDocument, ScoreDocument } from "tachi-common"; import { FindChartWithChartID } from "utils/queries/charts"; -import { CalculateVF6 } from "../calculated-data/stats"; import { InternalFailure } from "../common/converter-failures"; +import { Volforce } from "rg-stats"; export async function IIDXMergeFn( pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">, @@ -100,12 +100,10 @@ export async function USCMergeFn( throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`); } - pbDoc.calculatedData.VF6 = CalculateVF6( - pbDoc.scoreData.grade, + pbDoc.calculatedData.VF6 = Volforce.calculateVF6( + pbDoc.scoreData.score, pbDoc.scoreData.lamp, - pbDoc.scoreData.percent, - chart.levelNum, - logger + chart.levelNum ); return true; @@ -133,12 +131,10 @@ export async function SDVXMergeFn( throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`); } - pbDoc.calculatedData.VF6 = CalculateVF6( - pbDoc.scoreData.grade, + pbDoc.calculatedData.VF6 = Volforce.calculateVF6( + pbDoc.scoreData.score, pbDoc.scoreData.lamp, - pbDoc.scoreData.percent, - chart.levelNum, - logger + chart.levelNum ); return true; diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index fd0f7002a..ebaee6dbd 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -348,7 +348,7 @@ export const TestingSDVXSingleDryScore: DryScore<"sdvx:Single"> = { service: "e-amusement", game: "sdvx", scoreData: { - score: 95000000, + score: 9_500_000, lamp: "EXCESSIVE CLEAR", percent: 95, grade: "AA+",