From 9ed8528a40a60a1c363b5fa1d428450bd31d0477 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Aug 2024 22:40:29 +0900 Subject: [PATCH] Combine fail blocking and forcing logic into a singular mod interface --- .../Mods/ManiaModPerfect.cs | 11 ++++-- .../Mods/OsuModTargetPractice.cs | 15 ++++---- .../TestSceneDrainingHealthProcessor.cs | 2 +- .../Visual/Gameplay/TestSceneFailJudgement.cs | 2 +- .../Rulesets/Mods/IApplicableFailOverride.cs | 37 +++++++++++++++---- osu.Game/Rulesets/Mods/IHasFailCondition.cs | 26 ------------- .../Rulesets/Mods/ModAccuracyChallenge.cs | 2 +- osu.Game/Rulesets/Mods/ModCinema.cs | 5 ++- .../Rulesets/Mods/ModEasyWithExtraLives.cs | 15 +++++--- osu.Game/Rulesets/Mods/ModFailCondition.cs | 7 ++-- osu.Game/Rulesets/Mods/ModNoFail.cs | 7 ++-- osu.Game/Rulesets/Mods/ModPerfect.cs | 13 +++++-- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 10 +++-- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 4 +- osu.Game/Screens/Play/Player.cs | 4 +- 15 files changed, 88 insertions(+), 72 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/IHasFailCondition.cs diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs index abcee50e82..24dcf67bca 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPerfect.cs @@ -9,16 +9,19 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModPerfect : ModPerfect { - public override bool FailCondition(JudgementResult result) + public override FailState CheckFail(JudgementResult? result) { + if (result == null) + return FailState.Allow; + if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type)) - return false; + return FailState.Allow; // Mania allows imperfect "Great" hits without failing. if (result.Judgement.MaxResult == HitResult.Perfect) - return result.Type < HitResult.Great; + return result.Type >= HitResult.Great ? FailState.Allow : FailState.Force; - return result.Type != result.Judgement.MaxResult; + return result.Type != result.Judgement.MaxResult ? FailState.Force : FailState.Allow; } private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs index 56b3feb7c4..5afaec4868 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs @@ -33,7 +33,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, - IApplicableToDifficulty, IHasSeed, IHidesApproachCircles, IHasFailCondition + IApplicableToDifficulty, IHasSeed, IHidesApproachCircles, IApplicableFailOverride { public override string Name => "Target Practice"; public override string Acronym => "TP"; @@ -100,14 +100,15 @@ namespace osu.Game.Rulesets.Osu.Mods #region Sudden Death (IApplicableFailOverride) - public bool PerformFail() => true; - public bool RestartOnFail => false; - // Sudden death - public bool FailCondition(JudgementResult result) - => result.Type.AffectsCombo() - && !result.IsHit; + public FailState CheckFail(JudgementResult? result) + { + if (result == null) + return FailState.Allow; + + return result.Type.AffectsCombo() && !result.IsHit ? FailState.Force : FailState.Allow; + } #endregion diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index be4429b283..7b575183c8 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Gameplay this.type = type; } - public override bool FailCondition(JudgementResult result) => result.Type == type; + public override FailState CheckFail(JudgementResult result) => result?.Type == type ? FailState.Force : FailState.Allow; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index a5931b98e9..964a674108 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override double ScoreMultiplier => 1.0; public override string Acronym => ""; - public override bool FailCondition(JudgementResult result) => true; + public override FailState CheckFail(JudgementResult? result) => FailState.Force; } } } diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs index 8c99d739cb..7c47261a21 100644 --- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs +++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs @@ -1,22 +1,43 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Mods { /// - /// Represents a mod which can override (and block) a fail. + /// Represents a mod which can override failure, by either hard-blocking it, or forcing it immediately. /// public interface IApplicableFailOverride : IApplicableMod { /// - /// Whether we should allow failing at the current point in time. - /// - /// Whether the fail should be allowed to proceed. Return false to block. - bool PerformFail(); - - /// - /// Whether we want to restart on fail. Only used if returns true. + /// Whether we want to restart on fail. /// bool RestartOnFail { get; } + + /// + /// Check the current failure allowance for this mod. + /// + /// The judgement result which should be considered. Importantly, will be null if a failure has already being triggered. + /// The current failure allowance (see ). + FailState CheckFail(JudgementResult? result); + } + + public enum FailState + { + /// + /// Failure is being blocked by this mod. + /// + Block, + + /// + /// Failure is allowed by this mod (but may be triggered by another mod or base behaviour). + /// + Allow, + + /// + /// Failure should be forced immediately. + /// + Force } } diff --git a/osu.Game/Rulesets/Mods/IHasFailCondition.cs b/osu.Game/Rulesets/Mods/IHasFailCondition.cs deleted file mode 100644 index 73c734f858..0000000000 --- a/osu.Game/Rulesets/Mods/IHasFailCondition.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mods -{ - /// - /// Interface for a that specifies its own conditions for failure. - /// - // todo: maybe IHasFailCondition and IApplicableFailOverride should be combined into a single interface. - public interface IHasFailCondition : IApplicableFailOverride - { - /// - /// Determines whether should trigger a failure. Called every time a - /// judgement is applied to . - /// - /// The latest . - /// Whether the fail condition has been met. - /// - /// This method should only be used to trigger failures based on - /// - bool FailCondition(JudgementResult result); - } -} diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 1ee0b8c466..1176a452e0 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Mods public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; - public override bool FailCondition(JudgementResult result) => false; + public override FailState CheckFail(JudgementResult? result) => FailState.Allow; public enum AccuracyMode { diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 0c00eb6ae0..70a0138f8a 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; @@ -48,8 +49,8 @@ namespace osu.Game.Rulesets.Mods player.BreakOverlay.Hide(); } - public bool PerformFail() => false; - public bool RestartOnFail => false; + + public FailState CheckFail(JudgementResult? result) => FailState.Block; } } diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index e101ac440e..891d76e48a 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -7,6 +7,7 @@ using Humanizer; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods @@ -33,18 +34,22 @@ namespace osu.Game.Rulesets.Mods retries = Retries.Value; } - public bool PerformFail() + public bool RestartOnFail => false; + + public FailState CheckFail(JudgementResult? result) { - if (retries == 0) return true; + if (result != null) + return FailState.Block; + + if (retries == 0) + return FailState.Allow; health.Value = health.MaxValue; retries--; - return false; + return FailState.Block; } - public bool RestartOnFail => false; - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { health.BindTo(healthProcessor.Health); diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 8f3dec2d2b..9cb9d25be4 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -9,17 +9,16 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IHasFailCondition + public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride { public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModCinema) }; [SettingSource("Restart on fail", "Automatically restarts when failed.")] public BindableBool Restart { get; } = new BindableBool(); - public virtual bool PerformFail() => true; + public virtual bool AllowFail => true; public virtual bool RestartOnFail => Restart.Value; - private Action? triggerFailureDelegate; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) @@ -32,6 +31,6 @@ namespace osu.Game.Rulesets.Mods /// protected void TriggerFailure() => triggerFailureDelegate?.Invoke(this); - public abstract bool FailCondition(JudgementResult result); + public abstract FailState CheckFail(JudgementResult? result); } } diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 1aaef8eac4..e64c2a078c 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Mods @@ -24,12 +25,12 @@ namespace osu.Game.Rulesets.Mods private readonly Bindable showHealthBar = new Bindable(); + public bool RestartOnFail => false; + /// /// We never fail, 'yo. /// - public bool PerformFail() => false; - - public bool RestartOnFail => false; + public FailState CheckFail(JudgementResult? result) => FailState.Block; public void ReadFromConfig(OsuConfigManager config) { diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index d8020905da..a4c6192f55 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -28,9 +28,16 @@ namespace osu.Game.Rulesets.Mods Restart.Value = Restart.Default = true; } - public override bool FailCondition(JudgementResult result) - => (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type)) - && result.Type != result.Judgement.MaxResult; + public override FailState CheckFail(JudgementResult? result) + { + if (result == null) + return FailState.Allow; + + return (isRelevantResult(result.Judgement.MinResult) || isRelevantResult(result.Judgement.MaxResult) || isRelevantResult(result.Type)) + && result.Type != result.Judgement.MaxResult + ? FailState.Force + : FailState.Allow; + } private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo(); } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 070818e1c3..49b3430f24 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -23,8 +23,12 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); - public override bool FailCondition(JudgementResult result) - => result.Type.AffectsCombo() - && !result.IsHit; + public override FailState CheckFail(JudgementResult? result) + { + if (result == null) + return FailState.Allow; + + return result.Type.AffectsCombo() && !result.IsHit ? FailState.Force : FailState.Allow; + } } } diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index aba78c4379..6d8d3963e0 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -102,9 +102,9 @@ namespace osu.Game.Rulesets.Scoring if (CheckDefaultFailCondition(result)) return true; - foreach (var condition in Mods.Value.OfType()) + foreach (var condition in Mods.Value.OfType()) { - if (condition.FailCondition(result)) + if (condition.CheckFail(result) == FailState.Force) { ModTriggeringFailure = condition as Mod; return true; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bf9ca1c5bd..72e5515bc3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play /// Whether failing should be allowed. /// By default, this checks whether all selected mods allow failing. /// - protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType().All(m => m.PerformFail()); + protected virtual bool CheckModsAllowFailure() => HealthProcessor.Mods.Value.OfType().All(m => m.CheckFail(null) != FailState.Block); public readonly PlayerConfiguration Configuration; @@ -244,7 +244,7 @@ namespace osu.Game.Screens.Play HealthProcessor = gameplayMods.OfType().FirstOrDefault()?.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor ??= ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); - HealthProcessor.Mods.Value = gameplayMods.OrderByDescending(m => m is IHasFailCondition mod && mod.RestartOnFail).ToArray(); + HealthProcessor.Mods.Value = gameplayMods.OrderByDescending(m => m is IApplicableFailOverride mod && mod.RestartOnFail).ToArray(); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor);