diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index e0f7820262..8a0b8250d5 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()), new CatchModHidden(), new CatchModFlashlight(), + new ModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0cec0ee075..d324682989 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()), new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new ManiaModFlashlight(), + new ModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs new file mode 100644 index 0000000000..5b79753632 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModAccuracyChallenge : ModAccuracyChallenge + { + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 79a566e33c..48056a49de 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new OsuModHidden(), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), - new OsuModStrictTracking() + new OsuModStrictTracking(), + new OsuModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index fe12cf9765..1d0f772db0 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -144,6 +144,7 @@ namespace osu.Game.Rulesets.Taiko new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()), new TaikoModHidden(), new TaikoModFlashlight(), + new ModAccuracyChallenge(), }; case ModType.Conversion: diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs new file mode 100644 index 0000000000..59fbfe49b6 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Judgements; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcessor + { + public override string Name => "Accuracy Challenge"; + + public override string Acronym => "AC"; + + public override LocalisableString Description => "Fail if your accuracy drops too low!"; + + public override ModType Type => ModType.DifficultyIncrease; + + public override double ScoreMultiplier => 1.0; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModEasyWithExtraLives), typeof(ModPerfect) }).ToArray(); + + public override bool RequiresConfiguration => false; + + public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); + + [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider))] + public BindableNumber MinimumAccuracy { get; } = new BindableDouble + { + MinValue = 0.60, + MaxValue = 0.99, + Precision = 0.01, + Default = 0.9, + Value = 0.9, + }; + + private ScoreProcessor scoreProcessor = null!; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor; + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + { + if (!result.Type.AffectsAccuracy()) + return false; + + return getAccuracyWithImminentResultAdded(result) < MinimumAccuracy.Value; + } + + private double getAccuracyWithImminentResultAdded(JudgementResult result) + { + var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo }; + + // This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value. + // Hopefully we can improve this in the future. + scoreProcessor.PopulateScore(score); + score.Statistics[result.Type]++; + + return scoreProcessor.ComputeAccuracy(score); + } + } + + public partial class PercentSlider : OsuSliderBar + { + public PercentSlider() + { + DisplayAsPercentage = true; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index c4396e440e..e101ac440e 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using Humanizer; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -19,6 +21,7 @@ namespace osu.Game.Rulesets.Mods }; public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray(); private int retries; diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 804f23b6b7..6f0bb7ad3b 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; public override LocalisableString Description => "SS or quit."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray(); protected ModPerfect() { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index ff0d91c0dd..b5f42ec2cc 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual double ClassicScoreMultiplier => 36; - private readonly Ruleset ruleset; + /// + /// The ruleset this score processor is valid for. + /// + public readonly Ruleset Ruleset; + private readonly double accuracyPortion; private readonly double comboPortion; @@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring public ScoreProcessor(Ruleset ruleset) { - this.ruleset = ruleset; + Ruleset = ruleset; accuracyPortion = DefaultAccuracyPortion; comboPortion = DefaultComboPortion; @@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring [Pure] public double ComputeAccuracy(ScoreInfo scoreInfo) { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); + if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) + throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); // We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap. extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); @@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring [Pure] public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) { - if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) - throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); + if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) + throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); extractScoringValues(scoreInfo, out var current, out var maximum); @@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring break; default: - maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; + maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; break; }