Esempio n. 1
0
        /// <summary>
        /// Calculates beatmap difficulty and stores it in total, aim, speed, nsingles, nsingles_speed fields.
        /// </summary>
        /// <param name="mods"></param>
        /// <param name="singletapThreshold">
        /// The smallest milliseconds interval that will be considered singletappable. for example, 125ms is 240 1/2
        /// singletaps ((60000 / 240) / 2)
        /// </param>
        /// <returns>Itself</returns>
        public DiffCalc Calc(Mods mods, double singletapThreshold)
        {
            Reset();

            var mapstats = new MapStats {
                CS = Beatmap.CS
            };

            mapstats = MapStats.ModsApply(mods, mapstats, ModApplyFlags.ApplyCS);
            speedMul = mapstats.Speed;

            double radius = (PlayfieldWidth / 16.0) * (1.0 - 0.7 * (mapstats.CS - 5.0) / 5.0);

            //positions are normalized on circle radius so that we can calc as if everything was the same circlesize
            double scalingFactor = 52.0 / radius;

            if (radius < CirclesizeBuffThreshold)
            {
                scalingFactor *= 1.0 + Math.Min(CirclesizeBuffThreshold - radius, 5.0) / 50.0;
            }

            Vector2 normalizedCenter = new Vector2(PlayfieldCenter) * scalingFactor;

            //calculate normalized positions
            for (int i = 0; i < Beatmap.Objects.Count; i++)
            {
                HitObject obj = Beatmap.Objects[i];

                if ((obj.Type & HitObjectType.Spinner) != 0)
                {
                    obj.Normpos = new Vector2(normalizedCenter);
                }
                else
                {
                    Vector2 pos;

                    if ((obj.Type & HitObjectType.Slider) != 0)
                    {
                        pos = ((Slider)obj.Data).Position;
                    }
                    else if ((obj.Type & HitObjectType.Circle) != 0)
                    {
                        pos = ((Circle)obj.Data).Position;
                    }
                    else
                    {
                        //TODO: warn $"W: unknown object type {obj.Type:X8}\n"
                        pos = new Vector2();
                    }

                    obj.Normpos = new Vector2(pos) * scalingFactor;
                }

                if (i >= 2)
                {
                    var     prev1 = Beatmap.Objects[i - 1];
                    var     prev2 = Beatmap.Objects[i - 2];
                    Vector2 v1    = prev2.Normpos - prev1.Normpos;
                    Vector2 v2    = obj.Normpos - prev1.Normpos;
                    double  dot   = Vector2.Dot(v1, v2);
                    double  det   = v1.X * v2.Y - v1.Y * v2.X;
                    obj.Angle = Math.Abs(Math.Atan2(det, dot));
                }
                else
                {
                    obj.Angle = double.NaN;
                }
            }

            //speed and aim stars
            Speed = CalcIndividual(StrainType.Speed);
            Aim   = CalcIndividual(StrainType.Aim);

            Speed = Math.Sqrt(Speed) * StarScalingFactor;
            Aim   = Math.Sqrt(Aim) * StarScalingFactor;
            if ((mods & Mods.TouchDevice) != 0)
            {
                Aim = Math.Pow(Aim, 0.8);
            }

            //total stars
            Total = Aim + Speed + Math.Abs(Speed - Aim) * ExtremeScalingFactor;

            //singletap stats
            for (int i = 1; i < Beatmap.Objects.Count; ++i)
            {
                HitObject prev = Beatmap.Objects[i - 1];
                HitObject curr = Beatmap.Objects[i];

                if (curr.IsSingle)
                {
                    CountSingles++;
                }

                if ((curr.Type & (HitObjectType.Circle | HitObjectType.Slider)) == 0)
                {
                    continue;
                }

                double interval = (curr.Time - prev.Time) / speedMul;

                if (interval >= singletapThreshold)
                {
                    CountSinglesThreshold++;
                }
            }

            return(this);
        }
Esempio n. 2
0
        /// <summary>
        /// calculates ppv2, results are stored in Total, Aim, Speed, Acc, AccPercent.
        /// See: <seealso cref="PPv2Parameters"/>
        /// </summary>
        private PPv2(double aimStars, double speedStars,
                     int maxCombo, int countSliders, int countCircles, int countSpinners, int countObjects,
                     float baseAR, float baseOD, GameMode mode, Mods mods,
                     int combo, double accuracy, int countMiss,
                     int scoreVersion, Beatmap beatmap)
        {
            if (beatmap != null)
            {
                mode          = beatmap.Mode;
                baseAR        = beatmap.AR;
                baseOD        = beatmap.OD;
                maxCombo      = beatmap.GetMaxCombo();
                countSliders  = beatmap.CountSliders;
                countCircles  = beatmap.CountCircles;
                countSpinners = beatmap.CountSpinners;
                countObjects  = beatmap.Objects.Count;
            }

            if (mode != GameMode.Standard)
            {
                throw new InvalidOperationException("this gamemode is not yet supported");
            }

            if (maxCombo <= 0)
            {
                //TODO: warn "W: max_combo <= 0, changing to 1\n"
                maxCombo = 1;
            }

            if (combo < 0)
            {
                combo = maxCombo - countMiss;
            }

            /* accuracy -------------------------------------------- */
            ComputedAccuracy = new Accuracy(accPercent: accuracy, countObjects: countObjects, countMiss: countMiss);

            // reconstruct n300, n100 and n50 from accuracy because we dont really care about real amounts
            int    count300 = ComputedAccuracy.Count300;
            int    count100 = ComputedAccuracy.Count100;
            int    count50  = ComputedAccuracy.Count50;
            double realAcc  = ComputedAccuracy.Value();

            switch (scoreVersion)
            {
            case 1:
                //scorev1 ignores sliders since they are free 300s
                //and for some reason also ignores spinners
                realAcc = new Accuracy(Math.Max(count300 - countSliders - countSpinners, 0), count100, count50,
                                       countMiss).Value();

                realAcc = Math.Max(0.0, realAcc);
                break;

            case 2:
                countCircles = countObjects;
                break;

            default:
                throw new InvalidOperationException($"unsupported scorev{scoreVersion}");
            }

            //global values ---------------------------------------
            double countObjectsOver2K = countObjects / 2000.0;

            double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, countObjectsOver2K);

            if (countObjects > 2000)
            {
                lengthBonus += Math.Log10(countObjectsOver2K) * 0.5;
            }

            double comboBreak = Math.Pow(combo, 0.8) / Math.Pow(maxCombo, 0.8);

            //calculate stats with mods
            var mapstats = new MapStats
            {
                AR = baseAR,
                OD = baseOD
            };

            mapstats = MapStats.ModsApply(mods, mapstats, ModApplyFlags.ApplyAR | ModApplyFlags.ApplyOD);

            /* aim pp ---------------------------------------------- */
            Aim  = GetPPBase(aimStars);
            Aim *= lengthBonus;
            Aim *= comboBreak;

            double aimArBonus = 0.0;

            if (mapstats.AR > 10.33)
            {
                aimArBonus += 0.3 * (mapstats.AR - 10.33);
            }
            else if (mapstats.AR < 8.0)
            {
                aimArBonus += 0.1 * (8.0 - mapstats.AR);
            }

            Aim *= 1.0 + aimArBonus * lengthBonus;

            // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
            if (countMiss > 0)
            {
                Aim *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / countObjects, 0.775), countMiss);
            }

            double hdBonus = 1.0;

            if ((mods & Mods.Hidden) != 0)
            {
                hdBonus += 0.04f * (12.0f - mapstats.AR);
            }

            if ((mods & Mods.Flashlight) != 0)
            {
                double flBonus = 1.0 + 0.35 * Math.Min(1.0, countObjects / 200.0);
                if (countObjects > 200)
                {
                    flBonus += 0.3 * Math.Min(1, (countObjects - 200) / 300.0);
                }

                if (countObjects > 500)
                {
                    flBonus += (countObjects - 500) / 1200.0;
                }

                Aim *= flBonus;
            }

            double accBonus  = accuracy;
            double odSquared = Math.Pow(mapstats.OD, 2);
            double odBonus   = 0.98 + odSquared / 2500.0;

            Aim *= accBonus;
            Aim *= odBonus;
            Aim *= hdBonus;

            /* speed pp -------------------------------------------- */
            Speed  = GetPPBase(speedStars);
            Speed *= lengthBonus;
            Speed *= comboBreak;
            Speed *= hdBonus;

            double speedArBonus = 0.0;

            if (mapstats.AR > 10.33)
            {
                speedArBonus += 0.3 * (mapstats.AR - 10.33);
            }

            Speed *= 1.0 + speedArBonus * lengthBonus;

            // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
            if (countMiss > 0)
            {
                Speed *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / countObjects, 0.775), Math.Pow(countMiss, .875));
            }

            // Scale the speed value with accuracy and OD
            Speed *= (0.95 + odSquared / 750) * Math.Pow(accuracy, (14.5 - Math.Max(mapstats.OD, 8)) / 2);

            // Scale the speed value with # of 50s to punish doubletapping.
            Speed *= Math.Pow(0.98, count50 < countObjects / 500.0 ? 0 : count50 - countObjects / 500.0);

            /* acc pp ---------------------------------------------- */
            Acc = Math.Pow(1.52163, mapstats.OD) * Math.Pow(realAcc, 24.0) * 2.83;

            Acc *= Math.Min(1.15, Math.Pow(countCircles / 1000.0, 0.3));

            if ((mods & Mods.Hidden) != 0)
            {
                Acc *= 1.08;
            }

            if ((mods & Mods.Flashlight) != 0)
            {
                Acc *= 1.02;
            }

            /* total pp -------------------------------------------- */
            double finalMultiplier = 1.12;

            if ((mods & Mods.NoFail) != 0)
            {
                finalMultiplier *= Math.Max(0.9, 1.0 - 0.02 * countMiss);
            }

            if ((mods & Mods.SpunOut) != 0)
            {
                finalMultiplier *= 1.0 - Math.Pow(countSpinners / countObjects, 0.85);
            }

            Total = Math.Pow(Math.Pow(Aim, 1.1) + Math.Pow(Speed, 1.1) + Math.Pow(Acc, 1.1), 1.0 / 1.1) *
                    finalMultiplier;
        }
Esempio n. 3
0
        /// <summary>
        /// calculates ppv2, results are stored in Total, Aim, Speed, Acc, AccPercent.
        /// See: <seealso cref="PPv2Parameters"/>
        /// </summary>
        private PPv2(double aimStars, double speedStars,
                     int maxCombo, int countSliders, int countCircles, int countObjects,
                     float baseAR, float baseOD, GameMode mode, Mods mods,
                     int combo, int count300, int count100, int count50, int countMiss,
                     int scoreVersion, Beatmap beatmap)
        {
            if (beatmap != null)
            {
                mode         = beatmap.Mode;
                baseAR       = beatmap.AR;
                baseOD       = beatmap.OD;
                maxCombo     = beatmap.GetMaxCombo();
                countSliders = beatmap.CountSliders;
                countCircles = beatmap.CountCircles;
                countObjects = beatmap.Objects.Count;
            }

            if (mode != GameMode.Standard)
            {
                throw new InvalidOperationException("this gamemode is not yet supported");
            }

            if (maxCombo <= 0)
            {
                //TODO: warn "W: max_combo <= 0, changing to 1\n"
                maxCombo = 1;
            }

            if (combo < 0)
            {
                combo = maxCombo - countMiss;
            }

            if (count300 < 0)
            {
                count300 = countObjects - count100 - count50 - countMiss;
            }

            /* accuracy -------------------------------------------- */
            ComputedAccuracy = new Accuracy(count300, count100, count50, countMiss);
            double accuracy = ComputedAccuracy.Value();
            double realAcc  = accuracy;

            switch (scoreVersion)
            {
            case 1:
                //scorev1 ignores sliders since they are free 300s
                //and for some reason also ignores spinners
                int countSpinners = countObjects - countSliders - countCircles;

                realAcc = new Accuracy(Math.Max(count300 - countSliders - countSpinners, 0), count100, count50, countMiss).Value();

                realAcc = Math.Max(0.0, realAcc);
                break;

            case 2:
                countCircles = countObjects;
                break;

            default:
                throw new InvalidOperationException($"unsupported scorev{scoreVersion}");
            }

            //global values ---------------------------------------
            double countObjectsOver2K = countObjects / 2000.0;

            double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, countObjectsOver2K);

            if (countObjects > 2000)
            {
                lengthBonus += Math.Log10(countObjectsOver2K) * 0.5;
            }

            double missPenality = Math.Pow(0.97, countMiss);
            double comboBreak   = Math.Pow(combo, 0.8) / Math.Pow(maxCombo, 0.8);

            //calculate stats with mods
            var mapstats = new MapStats {
                AR = baseAR,
                OD = baseOD
            };

            mapstats = MapStats.ModsApply(mods, mapstats, ModApplyFlags.ApplyAR | ModApplyFlags.ApplyOD);

            /* ar bonus -------------------------------------------- */
            double arBonus = 1.0;

            if (mapstats.AR > 10.33)
            {
                arBonus += 0.45 * (mapstats.AR - 10.33);
            }
            else if (mapstats.AR < 8.0)
            {
                double lowArBonus = 0.01 * (8.0 - mapstats.AR);

                if ((mods & Mods.Hidden) != 0)
                {
                    lowArBonus *= 2.0;
                }

                arBonus += lowArBonus;
            }

            /* aim pp ---------------------------------------------- */
            Aim  = GetPPBase(aimStars);
            Aim *= lengthBonus;
            Aim *= missPenality;
            Aim *= comboBreak;
            Aim *= arBonus;

            if ((mods & Mods.Hidden) != 0)
            {
                Aim *= 1.18;
            }

            if ((mods & Mods.Flashlight) != 0)
            {
                Aim *= 1.45 * lengthBonus;
            }

            double accBonus = 0.5 + accuracy / 2.0;
            double odBonus  = 0.98 + (mapstats.OD * mapstats.OD) / 2500.0;

            Aim *= accBonus;
            Aim *= odBonus;

            /* speed pp -------------------------------------------- */
            Speed  = GetPPBase(speedStars);
            Speed *= lengthBonus;
            Speed *= missPenality;
            Speed *= comboBreak;
            Speed *= accBonus;
            Speed *= odBonus;

            /* acc pp ---------------------------------------------- */
            Acc = Math.Pow(1.52163, mapstats.OD) * Math.Pow(realAcc, 24.0) * 2.83;

            Acc *= Math.Min(1.15, Math.Pow(countCircles / 1000.0, 0.3));

            if ((mods & Mods.Hidden) != 0)
            {
                Acc *= 1.02;
            }

            if ((mods & Mods.Flashlight) != 0)
            {
                Acc *= 1.02;
            }

            /* total pp -------------------------------------------- */
            double finalMultiplier = 1.12;

            if ((mods & Mods.NoFail) != 0)
            {
                finalMultiplier *= 0.90;
            }

            if ((mods & Mods.SpunOut) != 0)
            {
                finalMultiplier *= 0.95;
            }

            Total = Math.Pow(Math.Pow(Aim, 1.1) + Math.Pow(Speed, 1.1) + Math.Pow(Acc, 1.1), 1.0 / 1.1) * finalMultiplier;
        }
Esempio n. 4
0
        /// <summary>
        /// applies mods to mapstats.
        /// <p><blockquote><pre>
        /// var mapstats = new MapStats();
        /// mapstats.AR = 9;
        /// Koohii.mods_apply(Mods.DoubleTime, mapstats, ModApplyFlags.ApplyAR);
        /// // mapstats.AR is now 10.33, mapstats.speed is 1.5
        /// </pre></blockquote></p>
        /// </summary>
        /// <param name="mods"></param>
        /// <param name="mapstats">the base beatmap stats</param>
        /// <param name="flags">
        /// enum that specifies which stats to modify. only the stats specified here need to be initialized in mapstats
        /// </param>
        /// <returns>mapstats</returns>
        public static MapStats ModsApply(Mods mods, MapStats mapstats,
                                         ModApplyFlags flags = ModApplyFlags.ApplyAR | ModApplyFlags.ApplyCS | ModApplyFlags.ApplyHP | ModApplyFlags.ApplyOD)
        {
            mapstats.Speed = 1.0f;

            if ((mods & Mods.MapChanging) == 0)
            {
                return(mapstats);
            }

            if ((mods & (Mods.DoubleTime | Mods.Nightcore)) != 0)
            {
                mapstats.Speed = 1.5f;
            }

            if ((mods & Mods.HalfTime) != 0)
            {
                mapstats.Speed *= 0.75f;
            }

            float odArHpMultiplier = 1.0f;

            if ((mods & Mods.Hardrock) != 0)
            {
                odArHpMultiplier = 1.4f;
            }

            if ((mods & Mods.Easy) != 0)
            {
                odArHpMultiplier *= 0.5f;
            }

            if ((flags & ModApplyFlags.ApplyAR) != 0)
            {
                mapstats.AR *= odArHpMultiplier;

                //convert AR into milliseconds window
                double arms = mapstats.AR < 5.0f ?
                              AR0Ms - ARMsStep1 * mapstats.AR
                    : AR5Ms - ARMsStep2 * (mapstats.AR - 5.0f);

                //stats must be capped to 0-10 before HT/DT which brings
                //them to a range of -4.42->11.08 for OD and -5->11 for AR
                arms  = Math.Min(AR0Ms, Math.Max(AR10Ms, arms));
                arms /= mapstats.Speed;

                mapstats.AR = (float)(
                    arms > AR5Ms
                    ? (AR0Ms - arms) / ARMsStep1
                    : 5.0 + (AR5Ms - arms) / ARMsStep2
                    );
            }

            if ((flags & ModApplyFlags.ApplyOD) != 0)
            {
                mapstats.OD *= odArHpMultiplier;
                double odms = OD0Ms - Math.Ceiling(ODMsStep * mapstats.OD);
                odms        = Math.Min(OD0Ms, Math.Max(OD10Ms, odms));
                odms       /= mapstats.Speed;
                mapstats.OD = (float)((OD0Ms - odms) / ODMsStep);
            }

            if ((flags & ModApplyFlags.ApplyCS) != 0)
            {
                if ((mods & Mods.Hardrock) != 0)
                {
                    mapstats.CS *= 1.3f;
                }

                if ((mods & Mods.Easy) != 0)
                {
                    mapstats.CS *= 0.5f;
                }

                mapstats.CS = Math.Min(10.0f, mapstats.CS);
            }

            if ((flags & ModApplyFlags.ApplyHP) != 0)
            {
                mapstats.HP = Math.Min(10.0f, mapstats.HP * odArHpMultiplier);
            }

            return(mapstats);
        }