diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs new file mode 100644 index 0000000000..2c8c151e7f --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFadeIn.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModFadeIn : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [TestCase(0.5f)] + [TestCase(0.1f)] + [TestCase(0.7f)] + public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModFadeIn { Coverage = { Value = coverage } }, PassCondition = () => true }); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs new file mode 100644 index 0000000000..204f26f151 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHidden.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public partial class TestSceneManiaModHidden : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [TestCase(0.5f)] + [TestCase(0.2f)] + [TestCase(0.8f)] + public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModHidden { Coverage = { Value = coverage } }, PassCondition = () => true }); + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index c6e9c339f4..196514c7b1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; @@ -18,5 +19,13 @@ namespace osu.Game.Rulesets.Mania.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; + + public override BindableNumber Coverage { get; } = new BindableFloat(0.5f) + { + Precision = 0.1f, + MinValue = 0.1f, + MaxValue = 0.7f, + Default = 0.5f, + }; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index eeb6e94fc7..f23cb335a5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Mania.UI; +using osu.Framework.Bindables; namespace osu.Game.Rulesets.Mania.Mods { @@ -13,6 +14,14 @@ namespace osu.Game.Rulesets.Mania.Mods public override LocalisableString Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; + public override BindableNumber Coverage { get; } = new BindableFloat(0.5f) + { + Precision = 0.1f, + MinValue = 0.2f, + MaxValue = 0.8f, + Default = 0.5f, + }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs index 6a94e5d371..09abe8d7f4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs @@ -3,8 +3,10 @@ using System; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Mods /// protected abstract CoverExpandDirection ExpandDirection { get; } + [SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")] + public abstract BindableNumber Coverage { get; } + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; @@ -36,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Mods { c.RelativeSizeAxes = Axes.Both; c.Direction = ExpandDirection; - c.Coverage = 0.5f; + c.Coverage = Coverage.Value; })); } } diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20230117.osk b/osu.Game.Tests/Resources/Archives/modified-default-20230117.osk new file mode 100644 index 0000000000..810bc4edf0 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-default-20230117.osk differ diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 39e8ec5215..5c4f59ba2c 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -44,7 +44,9 @@ namespace osu.Game.Tests.Skins // Covers TextElement and BeatmapInfoDrawable "Archives/modified-default-20221102.osk", // Covers BPM counter. - "Archives/modified-default-20221205.osk" + "Archives/modified-default-20221205.osk", + // Covers judgement counter. + "Archives/modified-default-20230117.osk" }; /// diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs new file mode 100644 index 0000000000..d104e25df0 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneJudgementCounter.cs @@ -0,0 +1,144 @@ +// 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.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD.JudgementCounter; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public partial class TestSceneJudgementCounter : OsuTestScene + { + private ScoreProcessor scoreProcessor = null!; + private JudgementTally judgementTally = null!; + private TestJudgementCounterDisplay counterDisplay = null!; + + private readonly Bindable lastJudgementResult = new Bindable(); + + private int iteration; + + [SetUpSteps] + public void SetupSteps() => AddStep("Create components", () => + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + scoreProcessor = new ScoreProcessor(ruleset); + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) }, + Children = new Drawable[] + { + judgementTally = new JudgementTally(), + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) }, + Child = counterDisplay = new TestJudgementCounterDisplay + { + Margin = new MarginPadding { Top = 100 }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + } + }, + }; + }); + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + + private void applyOneJudgement(HitResult result) + { + lastJudgementResult.Value = new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000 + }, new OsuJudgement()) + { + Type = result, + }; + scoreProcessor.ApplyResult(lastJudgementResult.Value); + + iteration++; + } + + [Test] + public void TestAddJudgementsToCounters() + { + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2); + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2); + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2); + } + + [Test] + public void TestAddWhilstHidden() + { + AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2); + AddAssert("Check value added whilst hidden", () => hiddenCount() == 2); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + } + + [Test] + public void TestChangeFlowDirection() + { + AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); + AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); + } + + [Test] + public void TestToggleJudgementNames() + { + AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false); + AddWaitStep("wait some", 2); + AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0); + AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = true); + AddWaitStep("wait some", 2); + AddAssert("Assert shown", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 1); + } + + [Test] + public void TestHideMaxValue() + { + AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false); + AddWaitStep("wait some", 2); + AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType().First().Alpha == 0); + AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true); + } + + [Test] + public void TestCycleDisplayModes() + { + AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple); + AddWaitStep("wait some", 2); + AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 0); + AddStep("Show normal judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Normal); + AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); + AddWaitStep("wait some", 2); + AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 1); + } + + private int hiddenCount() + { + var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit); + return num.Result.ResultCount.Value; + } + + private partial class TestJudgementCounterDisplay : JudgementCounterDisplay + { + public new FillFlowContainer CounterFlow => base.CounterFlow; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs new file mode 100644 index 0000000000..7675d0cc4f --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -0,0 +1,82 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + public partial class JudgementCounter : VisibilityContainer + { + public BindableBool ShowName = new BindableBool(); + public Bindable Direction = new Bindable(); + + public readonly JudgementTally.JudgementCount Result; + + public JudgementCounter(JudgementTally.JudgementCount result) => Result = result; + + public OsuSpriteText ResultName = null!; + private FillFlowContainer flowContainer = null!; + private JudgementRollingCounter counter = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IBindable ruleset) + { + AutoSizeAxes = Axes.Both; + + InternalChild = flowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + counter = new JudgementRollingCounter + { + Current = Result.ResultCount + }, + ResultName = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.Numeric.With(size: 8), + Text = ruleset.Value.CreateInstance().GetDisplayNameForHitResult(Result.Type) + } + } + }; + + var result = Result.Type; + + Colour = result.IsBasic() ? colours.ForHitResult(Result.Type) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter; + } + + protected override void LoadComplete() + { + ShowName.BindValueChanged(value => + ResultName.FadeTo(value.NewValue ? 1 : 0, JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint), true); + + Direction.BindValueChanged(direction => + { + flowContainer.Direction = direction.NewValue; + changeAnchor(direction.NewValue == FillDirection.Vertical ? Anchor.TopLeft : Anchor.BottomLeft); + + void changeAnchor(Anchor anchor) => counter.Anchor = ResultName.Anchor = counter.Origin = ResultName.Origin = anchor; + }, true); + + base.LoadComplete(); + } + + protected override void PopIn() => this.FadeIn(JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(100); + + private sealed partial class JudgementRollingCounter : RollingCounter + { + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true, size: 16)); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs new file mode 100644 index 0000000000..4ec90edeb0 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -0,0 +1,139 @@ +// 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.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + public partial class JudgementCounterDisplay : CompositeDrawable, ISkinnableDrawable + { + public const int TRANSFORM_DURATION = 250; + + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Display mode")] + public Bindable Mode { get; set; } = new Bindable(); + + [SettingSource("Counter direction")] + public Bindable FlowDirection { get; set; } = new Bindable(); + + [SettingSource("Show judgement names")] + public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true); + + [SettingSource("Show max judgement")] + public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true); + + [Resolved] + private JudgementTally tally { get; set; } = null!; + + protected FillFlowContainer CounterFlow = null!; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + InternalChild = CounterFlow = new FillFlowContainer + { + Direction = getFillDirection(FlowDirection.Value), + Spacing = new Vector2(10), + AutoSizeAxes = Axes.Both + }; + + foreach (var result in tally.Results) + CounterFlow.Add(createCounter(result)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + FlowDirection.BindValueChanged(direction => + { + var convertedDirection = getFillDirection(direction.NewValue); + + CounterFlow.Direction = convertedDirection; + + foreach (var counter in CounterFlow.Children) + counter.Direction.Value = convertedDirection; + }, true); + + Mode.BindValueChanged(_ => updateMode(), true); + + ShowMaxJudgement.BindValueChanged(value => + { + var firstChild = CounterFlow.Children.FirstOrDefault(); + firstChild.FadeTo(value.NewValue ? 1 : 0, TRANSFORM_DURATION, Easing.OutQuint); + }, true); + } + + private void updateMode() + { + foreach (var counter in CounterFlow.Children) + { + if (shouldShow(counter)) + counter.Show(); + else + counter.Hide(); + } + + bool shouldShow(JudgementCounter counter) + { + if (counter.Result.Type.IsBasic()) + return true; + + switch (Mode.Value) + { + case DisplayMode.Simple: + return false; + + case DisplayMode.Normal: + return !counter.Result.Type.IsBonus(); + + case DisplayMode.All: + return true; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + private FillDirection getFillDirection(Direction flow) + { + switch (flow) + { + case Direction.Horizontal: + return FillDirection.Horizontal; + + case Direction.Vertical: + return FillDirection.Vertical; + + default: + throw new ArgumentOutOfRangeException(nameof(flow), flow, @"Unsupported direction"); + } + } + + private JudgementCounter createCounter(JudgementTally.JudgementCount info) => + new JudgementCounter(info) + { + State = { Value = Visibility.Hidden }, + ShowName = { BindTarget = ShowJudgementNames } + }; + + public enum DisplayMode + { + Simple, + Normal, + All + } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs new file mode 100644 index 0000000000..e9e3fde92a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementTally.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + /// + /// Keeps track of judgements for a current play session, exposing bindable counts which can + /// be used for display purposes. + /// + public partial class JudgementTally : CompositeDrawable + { + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } = null!; + + public List Results = new List(); + + [BackgroundDependencyLoader] + private void load(IBindable ruleset) + { + foreach (var result in ruleset.Value.CreateInstance().GetHitResults()) + { + Results.Add(new JudgementCount + { + Type = result.result, + ResultCount = new BindableInt() + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); + scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); + } + + private void updateCount(JudgementResult judgement, bool revert) + { + foreach (JudgementCount result in Results.Where(result => result.Type == judgement.Type)) + result.ResultCount.Value = revert ? result.ResultCount.Value - 1 : result.ResultCount.Value + 1; + } + + public struct JudgementCount + { + public HitResult Type { get; set; } + + public BindableInt ResultCount { get; set; } + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 076d43c077..4d1f0b96b6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.ClicksPerSecond; +using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Skinning; using osuTK; using osu.Game.Localisation; @@ -59,6 +60,9 @@ namespace osu.Game.Screens.Play [Cached] private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; + [Cached] + private readonly JudgementTally tally; + public Bindable ShowHealthBar = new Bindable(true); private readonly DrawableRuleset drawableRuleset; @@ -104,6 +108,8 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { CreateFailingLayer(), + //Needs to be initialized before skinnable drawables. + tally = new JudgementTally(), mainComponents = new MainComponentsContainer { AlwaysPresent = true,