diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index d3ac5ce50c36..d2f9cc0bad37 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IHidesApproachCircles + public class OsuModHidden : ModHidden, IHidesApproachCircles, IAdjustableWhenReplay { [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] public Bindable OnlyFadeApproachCircles { get; } = new BindableBool(); @@ -30,6 +30,8 @@ public class OsuModHidden : ModHidden, IHidesApproachCircles public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; + public BindableBool IsDisabled { get; } = new BindableBool(); + protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); public override void ApplyToBeatmap(IBeatmap beatmap) @@ -62,6 +64,9 @@ protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility) { + if (IsDisabled.Value) + return; + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) return; diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModDisable.cs b/osu.Game.Tests/Visual/Mods/TestSceneModDisable.cs new file mode 100644 index 000000000000..b16ef70f84a1 --- /dev/null +++ b/osu.Game.Tests/Visual/Mods/TestSceneModDisable.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Visual.Mods +{ + public partial class TestSceneModDisable : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + [Test] + public void TestDisableAvailableInReplay() + { + CreateModTest(new ModTestData + { + Autoplay = true, + Mod = new OsuModFlashlight(), + PassCondition = () => getMod().IsDisabled.Value, + }); + + clickMod(); + } + + [Test] + public void TestDisableAvailableNotInReplay() + { + CreateModTest(new ModTestData + { + Autoplay = false, + Mod = new OsuModFlashlight(), + PassCondition = () => !getMod().IsDisabled.Value, + }); + + clickMod(); + } + + private void clickMod() + where T : IAdjustableWhenReplay + { + AddStep("click mod", () => Player.ChildrenOfType().Single(icon => icon.Mod is T).TriggerClick()); + } + + private T getMod() + where T : IAdjustableWhenReplay + => (T)Player.ChildrenOfType().Single(icon => icon.Mod is T).Mod; + } +} diff --git a/osu.Game/Rulesets/Mods/IAdjustableWhenReplay.cs b/osu.Game/Rulesets/Mods/IAdjustableWhenReplay.cs new file mode 100644 index 000000000000..11615181a76c --- /dev/null +++ b/osu.Game/Rulesets/Mods/IAdjustableWhenReplay.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; + +namespace osu.Game.Rulesets.Mods +{ + public interface IAdjustableWhenReplay : IApplicableMod + { + BindableBool IsDisabled { get; } + } +} diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 794be5c134ba..84c757ddc653 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods { - public abstract class ModDoubleTime : ModRateAdjust + public abstract class ModDoubleTime : ModRateAdjust, IAdjustableWhenReplay { public override string Name => "Double Time"; public override string Acronym => "DT"; @@ -44,12 +44,15 @@ public abstract class ModDoubleTime : ModRateAdjust } } + public BindableBool IsDisabled { get; } = new BindableBool(); + private readonly RateAdjustModHelper rateAdjustHelper; protected ModDoubleTime() { rateAdjustHelper = new RateAdjustModHelper(SpeedChange); rateAdjustHelper.HandleAudioAdjustments(AdjustPitch); + rateAdjustHelper.DisableSpeedChange.BindTo(IsDisabled); } public override void ApplyToTrack(IAdjustableAudioComponent track) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 884066d7ab74..853ac3cfebcf 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -50,7 +50,7 @@ public abstract class ModFlashlight : Mod public abstract float DefaultFlashlightSize { get; } } - public abstract partial class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor + public abstract partial class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor, IAdjustableWhenReplay where T : HitObject { public const double FLASHLIGHT_FADE_DURATION = 800; @@ -100,10 +100,17 @@ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) // NegativeInfinity is not used to allow one more thing drawn on top (used in replay analysis overlay in osu!). Depth = float.MinValue, }); + + IsDisabled.BindValueChanged(_ => + { + flashlight.FadeTo(IsDisabled.Value ? 0.3f : 1.0f, 50); + }, true); } protected abstract Flashlight CreateFlashlight(); + public BindableBool IsDisabled { get; } = new BindableBool(); + public abstract partial class Flashlight : Drawable { public readonly BindableInt Combo = new BindableInt(); diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index ae15ea5179a6..130cb65ff80d 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods { - public abstract class ModHalfTime : ModRateAdjust + public abstract class ModHalfTime : ModRateAdjust, IAdjustableWhenReplay { public override string Name => "Half Time"; public override string Acronym => "HT"; @@ -44,12 +44,15 @@ public abstract class ModHalfTime : ModRateAdjust } } + public BindableBool IsDisabled { get; } = new BindableBool(); + private readonly RateAdjustModHelper rateAdjustHelper; protected ModHalfTime() { rateAdjustHelper = new RateAdjustModHelper(SpeedChange); rateAdjustHelper.HandleAudioAdjustments(AdjustPitch); + rateAdjustHelper.DisableSpeedChange.BindTo(IsDisabled); } public override void ApplyToTrack(IAdjustableAudioComponent track) diff --git a/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs b/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs index 8a647dd25aef..4748182a85c8 100644 --- a/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs +++ b/osu.Game/Rulesets/Mods/RateAdjustModHelper.cs @@ -14,6 +14,14 @@ public class RateAdjustModHelper : IApplicableToTrack { public readonly IBindableNumber SpeedChange; + private readonly BindableNumber speedChange = new BindableNumber(); + + /// + /// Use with mods containing . + /// When it set to true, speed change will be 1.0. + /// + public BindableBool DisableSpeedChange = new BindableBool(); + private IAdjustableAudioComponent? track; private BindableBool? adjustPitch; @@ -25,6 +33,14 @@ public class RateAdjustModHelper : IApplicableToTrack public RateAdjustModHelper(IBindableNumber speedChange) { SpeedChange = speedChange; + + SpeedChange.BindValueChanged(_ => updateSpeedChange(), true); + DisableSpeedChange.BindValueChanged(_ => updateSpeedChange(), true); + } + + private void updateSpeedChange() + { + speedChange.Value = DisableSpeedChange.Value ? 1 : SpeedChange.Value; } /// @@ -39,8 +55,8 @@ public void HandleAudioAdjustments(BindableBool adjustPitch) // When switching between pitch adjust, we need to update adjustments to time-shift or frequency-scale. adjustPitch.BindValueChanged(adjustPitchSetting => { - track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), speedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), speedChange); AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; diff --git a/osu.Game/Rulesets/UI/ClickableModIcon.cs b/osu.Game/Rulesets/UI/ClickableModIcon.cs new file mode 100644 index 000000000000..a93cb6eab123 --- /dev/null +++ b/osu.Game/Rulesets/UI/ClickableModIcon.cs @@ -0,0 +1,28 @@ +// 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 osu.Game.Rulesets.Mods; +using osu.Framework.Input.Events; + +namespace osu.Game.Rulesets.UI +{ + /// + /// Display the specified mod at a fixed size. + /// + public partial class ClickableModIcon : ModIcon + { + public Action? Action; + + public ClickableModIcon(IMod mod, bool showTooltip = true, bool showExtendedInformation = true) + : base(mod, showTooltip, showExtendedInformation) + { + } + + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(); + return true; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/HudModDisplay.cs b/osu.Game/Screens/Play/HUD/HudModDisplay.cs new file mode 100644 index 000000000000..df2898257b68 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/HudModDisplay.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class HudModDisplay : ModDisplay + { + private readonly Bindable replayLoaded = new Bindable(); + + public HudModDisplay(Bindable replayLoaded, bool showExtendedInformation = true) + : base(showExtendedInformation) + { + this.replayLoaded.BindTo(replayLoaded); + } + + protected override ModIcon CreateModIcon(Mod mod, bool showExtendedInformation) + { + var modIcon = new ClickableModIcon(mod, showExtendedInformation); + + if (modIcon.Mod is IAdjustableWhenReplay dmod) + { + dmod.IsDisabled.BindValueChanged(_ => modIcon.Colour = dmod.IsDisabled.Value ? OsuColour.Gray(0.7f) : Color4.White, true); + + modIcon.Action = () => + { + if (!replayLoaded.Value) + return; + + dmod.IsDisabled.Toggle(); + }; + } + + return modIcon; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 986bc525ccd3..94d1ac835115 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -101,9 +101,11 @@ private void updateDisplay(ValueChangedEvent> mods) iconsContainer.Clear(); foreach (Mod mod in mods.NewValue.AsOrdered()) - iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(MOD_ICON_SCALE) }); + iconsContainer.Add(CreateModIcon(mod, showExtendedInformation).With(m => m.Scale = new Vector2(MOD_ICON_SCALE))); } + protected virtual ModIcon CreateModIcon(Mod mod, bool showExtendedInformation) => new ModIcon(mod, showExtendedInformation); + private void updateExpansionMode(double duration = 500) { switch (expansionMode) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7da996c20897..3fc334e1fb38 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -380,7 +380,7 @@ protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) Origin = Anchor.BottomRight, }; - protected ModDisplay CreateModsContainer() => new ModDisplay + protected ModDisplay CreateModsContainer() => new HudModDisplay(replayLoaded) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight,