diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 4f7c52860f..19fef9eb54 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -84,10 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps yield break; foreach (ManiaHitObject obj in objects) - { - obj.HitWindows = original.HitWindows; yield return obj; - } } private readonly List prevNoteTimes = new List(max_notes_for_density); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 1483283283..2517839355 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -59,7 +59,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty double starRating = calculateDifficulty() * star_scaling_factor; - categoryDifficulty?.Add("Strain", starRating); + if (categoryDifficulty != null) + categoryDifficulty["Strain"] = starRating; return starRating; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs new file mode 100644 index 0000000000..e6e3028d62 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -0,0 +1,126 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + public class ManiaPerformanceCalculator : PerformanceCalculator + { + private Mod[] mods; + + // Score after being scaled by non-difficulty-increasing mods + private double scaledScore; + + private int countPerfect; + private int countGreat; + private int countGood; + private int countOk; + private int countMeh; + private int countMiss; + + public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + mods = Score.Mods; + scaledScore = Score.TotalScore; + countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]); + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); + + if (mods.Any(m => !m.Ranked)) + return 0; + + IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; + + // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. + // The specific number has no intrinsic meaning and can be adjusted as needed. + double multiplier = 0.8; + + if (mods.Any(m => m is ModNoFail)) + multiplier *= 0.9; + if (mods.Any(m => m is ModEasy)) + multiplier *= 0.5; + + double strainValue = computeStrainValue(); + double accValue = computeAccuracyValue(strainValue); + double totalValue = + Math.Pow( + Math.Pow(strainValue, 1.1) + + Math.Pow(accValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + if (categoryDifficulty != null) + { + categoryDifficulty["Strain"] = strainValue; + categoryDifficulty["Accuracy"] = accValue; + } + + return totalValue; + } + + private double computeStrainValue() + { + // Obtain strain difficulty + double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0; + + // Longer maps are worth more + strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + + if (scaledScore <= 500000) + strainValue = 0; + else if (scaledScore <= 600000) + strainValue *= (scaledScore - 500000) / 100000 * 0.3; + else if (scaledScore <= 700000) + strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25; + else if (scaledScore <= 800000) + strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20; + else if (scaledScore <= 900000) + strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15; + else + strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1; + + return strainValue; + } + + private double computeAccuracyValue(double strainValue) + { + double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + if (hitWindowGreat <= 0) + return 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accuracyValue = Math.Max(0.0, 0.2 - (hitWindowGreat - 34) * 0.006667) + * strainValue + * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + // accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + + return accuracyValue; + } + + private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b1702de537..02ecb3afda 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania { @@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Mania { public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); + public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); public override IEnumerable ConvertLegacyMods(LegacyMods mods) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3661c0be04..8791e8ed86 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -204,6 +204,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private class DrawableTailNote : DrawableNote { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps + /// + private const double release_window_lenience = 1.5; + private readonly DrawableHoldNote holdNote; public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action) @@ -216,6 +223,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForJudgements(bool userTriggered, double timeOffset) { + // Factor in the release lenience + timeOffset /= release_window_lenience; + if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 12e3d2de51..22fa93a308 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The tail note of the hold. /// - public readonly Note Tail = new TailNote(); + public readonly Note Tail = new Note(); /// /// The time between ticks of this hold. @@ -94,25 +94,5 @@ namespace osu.Game.Rulesets.Mania.Objects }); } } - - /// - /// The tail of the hold note. - /// - private class TailNote : Note - { - /// - /// Lenience of release hit windows. This is to make cases where the hold note release - /// is timed alongside presses of other hit objects less awkward. - /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps - /// - private const double release_window_lenience = 1.5; - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - HitWindows *= release_window_lenience; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 4f0e02ff0d..e183098a51 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -1,8 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects.Types; using osu.Game.Rulesets.Objects; @@ -12,12 +10,6 @@ namespace osu.Game.Rulesets.Mania.Objects { public virtual int Column { get; set; } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - HitWindows.AllowsPerfect = true; - HitWindows.AllowsOk = true; - } + protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitWindows.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs similarity index 88% rename from osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitWindows.cs rename to osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs index 131492ea12..063b626af1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Objects.Legacy.Mania +namespace osu.Game.Rulesets.Mania.Objects { - public class ConvertHitWindows : HitWindows + public class ManiaHitWindows : HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { @@ -21,6 +22,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public override void SetDifficulty(double difficulty) { + AllowsPerfect = true; + AllowsOk = true; + Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 4369a31b2c..80eb808f6e 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -40,8 +40,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps RepeatSamples = curveData.RepeatSamples, RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false, - HitWindows = original.HitWindows + NewCombo = comboData?.NewCombo ?? false }; } else if (endTimeData != null) @@ -51,8 +50,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, EndTime = endTimeData.EndTime, - Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2, - HitWindows = original.HitWindows + Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2 }; } else @@ -62,8 +60,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false, - HitWindows = original.HitWindows + NewCombo = comboData?.NewCombo ?? false }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 2b7b7783e2..54126b934f 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -71,5 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects } public virtual void OffsetPosition(Vector2 offset) => Position += offset; + + protected override HitWindows CreateHitWindows() => new OsuHitWindows(); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitWindows.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs similarity index 90% rename from osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitWindows.cs rename to osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs index fd86173372..8405498554 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Objects.Legacy.Osu +namespace osu.Game.Rulesets.Osu.Objects { - public class ConvertHitWindows : HitWindows + public class OsuHitWindows : HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 93f1b7faf7..11586e340b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; [NonParallelizable] - [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")] - [TestCase("slider-generating-drumroll", false)] + [TestCase("basic")] + [TestCase("slider-generating-drumroll")] public new void Test(string name) { base.Test(name); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index abb2193235..41972b5d20 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -98,12 +98,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps double distance = distanceData.Distance * spans * legacy_velocity_multiplier; // The velocity of the taiko hit object - calculated as the velocity of a drum roll - double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; + double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; // The duration of the taiko hit object double taikoDuration = distance / taikoVelocity; // The velocity of the osu! hit object - calculated as the velocity of a slider - double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; + double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; @@ -132,8 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = j, Samples = currentSamples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } else @@ -142,8 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = j, Samples = currentSamples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } @@ -158,8 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Samples = obj.Samples, IsStrong = strong, Duration = taikoDuration, - TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4, - HitWindows = obj.HitWindows + TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 }; } } @@ -173,8 +170,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Samples = obj.Samples, IsStrong = strong, Duration = endTimeData.Duration, - RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier), - HitWindows = obj.HitWindows + RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier) }; } else @@ -187,8 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } else @@ -197,8 +192,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index acff0d286d..57e1e65064 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty @@ -35,6 +36,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } + public TaikoDifficultyCalculator(IBeatmap beatmap, Mod[] mods) + : base(beatmap, mods) + { + } + public override double Calculate(Dictionary categoryDifficulty = null) { // Fill our custom DifficultyHitObject class, that carries additional information @@ -51,10 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double starRating = calculateDifficulty() * star_scaling_factor; if (categoryDifficulty != null) - { - categoryDifficulty.Add("Strain", starRating); - categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); - } + categoryDifficulty["Strain"] = starRating; return starRating; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs new file mode 100644 index 0000000000..9c9cd1f0fb --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -0,0 +1,111 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty +{ + public class TaikoPerformanceCalculator : PerformanceCalculator + { + private readonly int beatmapMaxCombo; + + private Mod[] mods; + private int countGreat; + private int countGood; + private int countMeh; + private int countMiss; + + public TaikoPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + beatmapMaxCombo = beatmap.HitObjects.Count(h => h is Hit); + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + mods = Score.Mods; + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); + + // Don't count scores made with supposedly unranked mods + if (mods.Any(m => !m.Ranked)) + return 0; + + // Custom multipliers for NoFail and SpunOut. + double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (mods.Any(m => m is ModNoFail)) + multiplier *= 0.90; + + if (mods.Any(m => m is ModHidden)) + multiplier *= 1.10; + + double strainValue = computeStrainValue(); + double accuracyValue = computeAccuracyValue(); + double totalValue = + Math.Pow( + Math.Pow(strainValue, 1.1) + + Math.Pow(accuracyValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + if (categoryDifficulty != null) + { + categoryDifficulty["Strain"] = strainValue; + categoryDifficulty["Accuracy"] = accuracyValue; + } + + return totalValue; + } + + private double computeStrainValue() + { + double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes["Strain"] / 0.0075) - 4.0, 2.0) / 100000.0; + + // Longer maps are worth more + double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); + strainValue *= lengthBonus; + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + strainValue *= Math.Pow(0.985, countMiss); + + // Combo scaling + if (beatmapMaxCombo > 0) + strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(beatmapMaxCombo, 0.5), 1.0); + + if (mods.Any(m => m is ModHidden)) + strainValue *= 1.025; + + if (mods.Any(m => m is ModFlashlight)) + // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. + strainValue *= 1.05 * lengthBonus; + + // Scale the speed value with accuracy _slightly_ + return strainValue * Score.Accuracy; + } + + private double computeAccuracyValue() + { + double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + if (hitWindowGreat <= 0) + return 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accValue = Math.Pow(150.0 / hitWindowGreat, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + } + + private int totalHits => countGreat + countGood + countMeh + countMiss; + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 63de096238..ffbbe28f2e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -27,5 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// Strong hit objects give more points for hitting the hit object with both keys. /// public bool IsStrong; + + protected override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitWindows.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs similarity index 90% rename from osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitWindows.cs rename to osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs index 6fbf7e122f..289f084a45 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Objects.Legacy.Taiko +namespace osu.Game.Rulesets.Taiko.Objects { - public class ConvertHitWindows : HitWindows + public class TaikoHitWindows : HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 35dc17c0e2..abaa8db597 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Difficulty; @@ -144,7 +145,9 @@ namespace osu.Game.Rulesets.Taiko public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap, mods); + + public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score); public override int? LegacyID => 1; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 9fd7a0156d..07d9c80061 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Difficulty private readonly Dictionary attributes = new Dictionary(); protected IDictionary Attributes => attributes; + protected readonly Ruleset Ruleset; protected readonly IBeatmap Beatmap; protected readonly Score Score; @@ -23,9 +24,9 @@ namespace osu.Game.Rulesets.Difficulty protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) { - Score = score; - + Ruleset = ruleset; Beatmap = beatmap; + Score = score; var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); diffCalc.Calculate(attributes); diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 7610593d6a..3717209860 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -135,39 +135,5 @@ namespace osu.Game.Rulesets.Objects /// The time offset. /// Whether the can be hit at any point in the future from this time offset. public bool CanBeHit(double timeOffset) => timeOffset <= HalfWindowFor(HitResult.Meh); - - /// - /// Multiplies all hit windows by a value. - /// - /// The hit windows to multiply. - /// The value to multiply each hit window by. - public static HitWindows operator *(HitWindows windows, double value) - { - windows.Perfect *= value; - windows.Great *= value; - windows.Good *= value; - windows.Ok *= value; - windows.Meh *= value; - windows.Miss *= value; - - return windows; - } - - /// - /// Divides all hit windows by a value. - /// - /// The hit windows to divide. - /// The value to divide each hit window by. - public static HitWindows operator /(HitWindows windows, double value) - { - windows.Perfect /= value; - windows.Great /= value; - windows.Good /= value; - windows.Ok /= value; - windows.Meh /= value; - windows.Miss /= value; - - return windows; - } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs index 939d3b9c93..ea4e7f6907 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index 22abc64b60..86a10fd363 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public double Duration => EndTime - StartTime; - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs index 6bca5b717c..a8d7b23df1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index 1dc826af9b..5a443c2ac2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public float X { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index 23955b2d23..f015272b2c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index 35b8c1c7dd..ec5a002bbb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index 73b8369aca..0141785f31 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -21,6 +21,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs index 11db086778..5e9786c84a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko { public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs index 95c69222b5..8a9a0db0a7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko { public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index 7baea212ea..4c8807a1d3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko public double Duration => EndTime - StartTime; - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } }