UNPKG

rg-stats

Version:

A library for calculating various rhythm game stats.

316 lines (273 loc) 9.28 kB
import { GetEntriesAsArray } from "../util/misc"; import { ThrowIf } from "../util/throw-if"; import { integer } from "../util/types"; export type SDVXGrades = "D" | "C" | "B" | "A" | "A+" | "AA" | "AA+" | "AAA" | "AAA+" | "S"; export type SDVXLamps = | "FAILED" | "CLEAR" | "EXCESSIVE CLEAR" | "MAXXIVE CLEAR" | "ULTIMATE CHAIN" | "PERFECT ULTIMATE CHAIN"; const VF4GradeCoefficients: Record<SDVXGrades, number> = { 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, }; const VF5GradeCoefficients: Record<SDVXGrades, number> = { S: 105, "AAA+": 102, AAA: 100, "AA+": 97, AA: 94, // everything below this point is marked with a (?) // in bemaniwiki, so maybe it can't be trusted? "A+": 91, A: 88, B: 85, C: 82, D: 80, }; const VF5LampCoefficients: Record<SDVXLamps, number> = { "PERFECT ULTIMATE CHAIN": 110, "ULTIMATE CHAIN": 105, "MAXXIVE CLEAR": 104, "EXCESSIVE CLEAR": 102, CLEAR: 100, FAILED: 50, }; /** * Calculate VOLFORCE as it's defined in SDVX4. * * @param score - The user's score. Between 0 and 10million. * @param level - The level of the chart. Between 0 and 20, but this is not enforced. */ export function calculateVF4(score: integer, level: integer) { AssertProvidedScore(score); const grade = SDVXScoreToGrade(score); const gradeCoefficient = VF4GradeCoefficients[grade]; return Math.floor(25 * (level + 1) * (score / 10_000_000) * gradeCoefficient); } /** * Given a VF4 value and a chart level, return what score is needed to get that * VF4. * * If the score needed is greater than 10million, this function will throw. ** * @param vf4 - The VF4 to invert. * @param level - The level of the chart you're inverting for. */ export function inverseVF4(vf4: integer, level: integer) { const scoreTimesGradeCoef = (10_000_000 * vf4) / (25 * (level + 1)); const score = AttemptGradeCoefficientDivide(scoreTimesGradeCoef, VF4GradeCoefficients); ThrowIf(score === null, `A VF4 of ${vf4} is not possible on a chart with level ${level}`, { vf4, level, }); return score; } /** * Calculate VOLFORCE as it's defined in SDVX5. * * @param score - The user's score. Between 0 and 10million. * @param level - The level of the chart. Between 0 and 20, but this is not enforced. */ export function calculateVF5(score: integer, lamp: SDVXLamps, level: integer) { AssertProvidedScore(score); const unroundedVF5 = CalculateWholeVF5(score, lamp, level); return Math.floor(unroundedVF5 / 10000) / 100; } /** * Given a VF5 value and a chart level, return what score is needed to get that * VF5. * * If the score needed is greater than 10million, this function will throw. * * @param vf5 - The VF5 to invert. * @param lamp - The lamp for this score. This is necessary to know, as lampCoefficient * plays a part in VF5. * @param level - The level of the chart you're inverting for. */ export function inverseVF5( vf5: number, // Exclude PUC as input. It doesn't make sense as input, since the answer would // always be 10million. lamp: Exclude<SDVXLamps, "PERFECT ULTIMATE CHAIN">, level: integer ) { const score = InvertUnroundedVF5(vf5, lamp, level); ThrowIf(score === null, `A VF5 of ${vf5} is not possible on a chart with level ${level}.`, { vf5, level, }); return score; } /** * Calculate VOLFORCE as it's defined in SDVX6. * * @param score - The user's score. Between 0 and 10million. * @param level - The level of the chart. Between 0 and 20, but this is not enforced. */ export function calculateVF6(score: integer, lamp: SDVXLamps, level: integer) { AssertProvidedScore(score); const unroundedVF5 = CalculateWholeVF5(score, lamp, level); // VF6 is just unroundedVF5 to 3 decimal places instead of 2. return Math.floor(unroundedVF5 / 1000) / 1000; } /** * Given a VF6 value and a chart level, return what score is needed to get that * VF5. * * If the score needed is greater than 10million, this function will throw. ** * @param vf6 - The VF6 to invert. * @param lamp - The lamp for this score. This is necessary to know, as lampCoefficient * plays a part in VF6. Passing "PERFECT ULTIMATE CHAIN" as a lamp is invalid, as inverting * it into a score makes no sense. * @param level - The level of the chart you're inverting for. */ export function inverseVF6( vf6: number, // Exclude PUC as input. It doesn't make sense as input, since the answer would // always be 10million. lamp: Exclude<SDVXLamps, "PERFECT ULTIMATE CHAIN">, level: integer ) { // note: this function is actually identical to inverseVF5, but with the caveat // that the error message is different. const score = InvertUnroundedVF5(vf6, lamp, level); ThrowIf(score === null, `A VF6 of ${vf6} is not possible on a chart with level ${level}.`, { vf6, level, }); // guaranteed to not be null return score!; } /** * Calculate VF5 without performing any rounding. This is useful because VF5 * is floored to 2 decimal places, wherease VF6 is floored to 3. This lets us * reuse the same algorithm. */ function CalculateWholeVF5(score: integer, lamp: SDVXLamps, level: integer) { const grade = SDVXScoreToGrade(score); const gradeCoefficient = VF5GradeCoefficients[grade]; const lampCoefficient = VF5LampCoefficients[lamp]; return level * 2 * (score / 10_000_000) * gradeCoefficient * lampCoefficient; } /** * Attempt to invert VF5 into a score. * * @returns The score if it was possible to be achieved. Else, it returns null. */ function InvertUnroundedVF5(vf5: number, lamp: SDVXLamps, level: integer) { // Note: PERFECT ULTIMATE CHAIN is never passed into this function from typescript // as the calling functions Exclude<T> it from the lamps. // However, a JS caller may call it like this anyway, so we mayaswell throw. ThrowIf( lamp === "PERFECT ULTIMATE CHAIN", "PERFECT ULTIMATE CHAIN as a lampCoefficient does not make sense for an inversion, since the answer would always be 10million.", { lamp } ); const lampCoefficient = VF5LampCoefficients[lamp]; const scoreTimesGradeCoef = (1_000_000 * 10_000_000 * vf5) / (2 * level * lampCoefficient); const score = AttemptGradeCoefficientDivide(scoreTimesGradeCoef, VF5GradeCoefficients); return score; } /** * Convert a SDVX percent to the grade it represents. * @param score - The score to convert - between 0 and 10million. * @returns A string representing a grade. */ function SDVXScoreToGrade(score: integer): SDVXGrades { if (score < 7_000_000) { return "D"; } else if (score < 8_000_000) { return "C"; } else if (score < 8_700_000) { return "B"; } else if (score < 9_000_000) { return "A"; } else if (score < 9_300_000) { return "A+"; } else if (score < 9_500_000) { return "AA"; } else if (score < 9_700_000) { return "AA+"; } else if (score < 9_800_000) { return "AAA"; } else if (score < 9_900_000) { return "AAA+"; } return "S"; } /** * Given a SDVX grade, return the lower and upper bounds for scoring in this grade. * This is used to invert the gradeCoefficient function in volforce. * * Bounds are returned as lower <= k < upper. */ function SDVXGetGradeBoundaries(grade: SDVXGrades): { lower: integer; upper: integer } { if (grade === "S") { return { lower: 9_900_000, upper: 10_000_000 }; } else if (grade === "AAA+") { return { lower: 9_800_000, upper: 9_900_000 }; } else if (grade === "AAA") { return { lower: 9_700_000, upper: 9_800_000 }; } else if (grade === "AA+") { return { lower: 9_500_000, upper: 9_700_000 }; } else if (grade === "AA") { return { lower: 9_300_000, upper: 9_500_000 }; } else if (grade === "A+") { return { lower: 9_000_000, upper: 9_300_000 }; } else if (grade === "A") { return { lower: 8_700_000, upper: 9_000_000 }; } else if (grade === "B") { return { lower: 8_000_000, upper: 8_700_000 }; } else if (grade === "C") { return { lower: 7_000_000, upper: 8_000_000 }; } return { lower: 0, upper: 7_000_000 }; } /** * Assert necessary things about a provided score. */ function AssertProvidedScore(score: integer) { ThrowIf(score > 10_000_000, "Score cannot be greater than 10million", { score }); ThrowIf.negative(score, "Score cannot be negative", { score }); } /** * Go through all of the gradeBoundaries for a game and use them as guesses for score * values. * * This means we try dividing by all the gradeCoefficients until we find one * where the resulting score would have the same grade as the given coefficient. * * Used for inverting VF. * * @param scoreTimesGradeCoef - The expected score multiplied by the gradeCoefficient. * @param coefficients - A record of SDVXGrade -> gradeCoefficient * @returns The score divided by the gradeCoefficient. If not possible, this returns * null. */ function AttemptGradeCoefficientDivide( scoreTimesGradeCoef: number, coefficients: Record<SDVXGrades, number> ) { for (const [grade, gradeCoef] of GetEntriesAsArray(coefficients).reverse()) { const maybeScore = scoreTimesGradeCoef / gradeCoef; const { lower, upper } = SDVXGetGradeBoundaries(grade); if (maybeScore <= lower) { return lower; } else if (maybeScore < upper || (maybeScore === upper && upper === 10_000_000)) { return Math.round(maybeScore); } } return null; }