UNPKG

osu-pp-calculator

Version:

calculates performance points for given beatmap

154 lines (120 loc) 5.58 kB
import BeatmapModifier from './mods'; import DifficultyCalculator, {BeatmapDifficulty} from './diff-calc'; import Beatmap from './beatmap'; namespace PPCalculator { // turns the beatmaps' strain attributes into a larger value, suitable // for pp calc. not 100% sure what is going on here, but it probably makes // strain values scale a bit exponentially. const calculateBaseStrain = (strain: number): number => Math.pow(5.0 * Math.max(1.0, strain * 14.8148148) - 4.0, 3.0) * 0.00001; const accuracyCalc = (c300: number, c100: number, c50: number, misses: number): number => { const totalHits: number = c300 + c100 + c50 + misses; let accuracy: number = 0.0; if (totalHits > 0) { accuracy = ( c50 * 50.0 + c100 * 100.0 + c300 * 300.0) / (totalHits * 300.0); } return accuracy; } const calc100Count = (accuracy: number, maxHits: number, misses: number) => Math.round(-3 / 2 * ((accuracy - 1) * maxHits + misses)); export function calculate(beatmap: Beatmap, accuracyPercent: number = 100, modifiers: number = BeatmapModifier.None, combo: number = -1, misses: number = -1, scoreVersion: number = 1): number { beatmap.applyMods(modifiers); const diff: BeatmapDifficulty = DifficultyCalculator.calculate(beatmap); accuracyPercent = Math.max(0.0, Math.min(100.0, accuracyPercent)); const c100: number = calc100Count(accuracyPercent / 100.0, beatmap.hitObjects.length, misses); const c300: number = beatmap.hitObjects.length - c100; return calculateWithCounts(diff.aim, diff.speed, beatmap, modifiers, combo, misses, c300, c100, 0, scoreVersion); } export function calculateWithCounts(aim: number, speed: number, beatmap: Beatmap, modifiers: number = BeatmapModifier.None, combo: number = -1, misses: number = 0, c300: number = -1, c100: number = 0, c50: number = 0, scoreVersion: number = 1): number { if (!beatmap.combo) throw new Error("Max combo cannot be zero"); if (scoreVersion != 1 && scoreVersion != 2) throw new Error("This score version does not exist or isn't supported"); let overallDifficulty: number = beatmap.overallDifficulty; let approachRate: number = beatmap.approachRate; let circles: number = beatmap.circleCount; if (c300 <= 0) c300 = beatmap.hitObjects.length - c100 - c50 - misses; combo = combo <= 0 ? beatmap.combo : combo; const totalHits: number = c300 + c100 + c50 + misses; // accuracy (not in percentage, ranges between 0 and 1) const accuracy: number = accuracyCalc(c300, c100, c50, misses); // length bonus (reused in speed pp) const totalHitsOver2k: number = totalHits / 2000.0; const lengthBonus: number = 0.95 + 0.4 * Math.min(1.0, totalHitsOver2k) + (totalHits > 2000 ? Math.log(totalHitsOver2k) / Math.LN10 * 0.5 : 0.0); // miss penality (reused in speed pp) const missPenalty: number = Math.pow(0.97, misses); // combo break penality (reused in speed pp) const comboBreakPenalty: number = Math.pow(combo, 0.8) / Math.pow(beatmap.combo, 0.8); let approachRateBonus: number = 1.0; // high ar bonus if (approachRate > 10.33) { approachRateBonus += 0.45 * (approachRate - 10.33); } // low ar bonus else if (approachRate < 8.0) { let lowArBonus: number = 0.01 * (8.0 - approachRate); if (modifiers & BeatmapModifier.Hidden) { lowArBonus *= 2.0; } approachRateBonus += lowArBonus; } // accuracy bonus (bad aim can lead to bad accuracy, reused in speed for same reason) const accuracyBonus: number = 0.5 + accuracy / 2.0; // od bonus (low od is easy to accuracy even with shit aim, reused in speed ...) const overallDifficultyBonus: number = 0.98 + Math.pow(overallDifficulty, 2) / 2500.0; const aimValue: number = calculateBaseStrain(aim) * lengthBonus * approachRateBonus * accuracyBonus * overallDifficultyBonus * missPenalty * comboBreakPenalty * (modifiers & BeatmapModifier.Hidden ? 1.18 : 1) * (modifiers & BeatmapModifier.Flashlight ? 1.45 * lengthBonus : 1); const speedValue: number = calculateBaseStrain(speed) * lengthBonus * missPenalty * comboBreakPenalty * accuracyBonus * overallDifficultyBonus; let realAccuracy: number = 0.0; // accuracy calculation changes from scorev1 to scorev2 if (scoreVersion == 2) { circles = totalHits; realAccuracy = accuracy; } else { // scorev1 ignores sliders since they are free 300s if (circles) { realAccuracy = ( (c300 - (totalHits - circles)) * 300.0 + c100 * 100.0 + c50 * 50.0 ) / (circles * 300); } // can go negative if we miss everything realAccuracy = Math.max(0.0, realAccuracy); } // arbitrary values tom crafted out of trial and error const accuracyValue: number = Math.pow(1.52163, overallDifficulty) * Math.pow(realAccuracy, 24.0) * 2.83 * Math.min(1.15, Math.pow(circles / 1000.0, 0.3)) * (modifiers & BeatmapModifier.Hidden ? 1.02 : 1) * (modifiers & BeatmapModifier.Flashlight ? 1.02 : 1); const finalMultiplier: number = 1.12 * (modifiers & BeatmapModifier.NoFail ? 0.90 : 1) * (modifiers & BeatmapModifier.SpunOut ? 0.95 : 1); return Math.pow( Math.pow(aimValue, 1.1) + Math.pow(speedValue, 1.1) + Math.pow(accuracyValue, 1.1), 1.0 / 1.1 ) * finalMultiplier; } }; export {PPCalculator}; export {Beatmap};