diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs index 059af2484d..c7ce67d168 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -1,9 +1,11 @@ // 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.Extensions; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; @@ -33,6 +35,9 @@ namespace osu.Game.Tests.Visual.Multiplayer Children = new Drawable[] { new MultiplayerSkipOverlay(120000) + { + RequestSkip = () => MultiplayerClient.VoteToSkipIntro().WaitSafely(), + } }, }; @@ -47,26 +52,83 @@ namespace osu.Game.Tests.Visual.Multiplayer { for (int i = 0; i < 4; i++) { - int i2 = i; + int userId = i; - AddStep($"join user {i2}", () => + AddStep($"join user {userId}", () => { MultiplayerClient.AddUser(new APIUser { - Id = i2, - Username = $"User {i2}" + Id = userId, + Username = $"User {userId}" }); - MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing); + MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing); }); } - AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely()); + AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely()); + AddStep("local user votes", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("user 1 votes", () => MultiplayerClient.UserVoteToSkipIntro(1).WaitSafely()); + } + [Test] + public void TestLeavingBeforeLocalVote() + { for (int i = 0; i < 4; i++) { - int i2 = i; - AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely()); + int userId = i; + + AddStep($"join user {userId}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = userId, + Username = $"User {userId}" + }); + + MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing); + }); + } + + AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely()); + AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); + AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 })); + AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 })); + AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 })); + } + + [Test] + public void TestLeavingAfterLocalVote() + { + for (int i = 0; i < 4; i++) + { + int userId = i; + + AddStep($"join user {userId}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = userId, + Username = $"User {userId}" + }); + + MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing); + }); + } + + AddStep("local user votes", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely()); + AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); + AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 })); + AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 })); + AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 })); + } + + public partial class TestMultiplayerSkipOverlay : MultiplayerSkipOverlay + { + public TestMultiplayerSkipOverlay() + : base(120000) + { } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs index 35e85c3273..9e237483fe 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -4,15 +4,24 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osuTK; using osuTK.Graphics; @@ -23,90 +32,49 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private MultiplayerClient client { get; set; } = null!; - private Drawable votedIcon = null!; - private OsuSpriteText countText = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + + private Button skipButton = null!; public MultiplayerSkipOverlay(double startTime) : base(startTime) { } - [BackgroundDependencyLoader] - private void load() + protected override OsuClickableContainer CreateButton() => skipButton = new Button { - FadingContent.AddRange( - [ - votedIcon = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Position = new Vector2(50, 0), - Size = new Vector2(20), - Alpha = 0, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Green - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.5f), - Icon = FontAwesome.Solid.Check - } - } - }, - countText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - Position = new Vector2(0.75f, 0), - Font = OsuFont.Default.With(size: 36, weight: FontWeight.Bold) - } - ]); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; protected override void LoadComplete() { base.LoadComplete(); + skipButton.Enabled.BindValueChanged(e => + { + RemainingTimeBox.Colour = e.NewValue ? colours.Orange3 : Button.COLOUR_GRAY; + }, true); + client.UserLeft += onUserLeft; client.UserStateChanged += onUserStateChanged; client.UserVotedToSkipIntro += onUserVotedToSkipIntro; - updateText(); + updateCount(); } - private void onUserLeft(MultiplayerRoomUser user) - { - Schedule(updateText); - } + private void onUserLeft(MultiplayerRoomUser user) => Schedule(updateCount); - private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) - { - Schedule(updateText); - } + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) => Schedule(updateCount); private void onUserVotedToSkipIntro(int userId) => Schedule(() => { - updateText(); - - countText.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); - - if (userId == client.LocalUser?.UserID) - { - votedIcon.ScaleTo(1.5f).ScaleTo(1, 200, Easing.OutSine); - votedIcon.FadeInFromZero(100); - } + FadingContent.TriggerShow(); + updateCount(); }); - private void updateText() + private void updateCount() { if (client.Room == null || client.Room.Settings.AutoSkip) return; @@ -115,7 +83,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkipIntro); int countRequired = countTotal / 2 + 1; - countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; + skipButton.SkippedCount.Value = Math.Min(countRequired, countSkipped); + skipButton.RequiredCount.Value = countRequired; } protected override void Dispose(bool isDisposing) @@ -129,5 +98,218 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.UserVotedToSkipIntro -= onUserVotedToSkipIntro; } } + + public partial class Button : OsuClickableContainer + { + private const float chevron_y = 0.4f; + private const float secondary_y = 0.7f; + + public static readonly Color4 COLOUR_GRAY = OsuColour.Gray(0.4f); + + private Box background = null!; + private Box box = null!; + private TrianglesV2 triangles = null!; + private OsuSpriteText countText = null!; + private OsuSpriteText skipText = null!; + private AspectContainer aspect = null!; + + private FillFlowContainer chevrons = null!; + + private Sample sampleConfirm = null!; + + public readonly BindableInt SkippedCount = new BindableInt(); + public readonly BindableInt RequiredCount = new BindableInt(); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public Button() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleConfirm = audio.Samples.Get(@"UI/submit-select"); + + Children = new Drawable[] + { + background = new Box + { + Alpha = 0.2f, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + aspect = new AspectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 0.6f, + Masking = true, + CornerRadius = 15, + Children = new Drawable[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + }, + countText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + RelativePositionAxes = Axes.Y, + Y = 0.35f, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 24), + Origin = Anchor.Centre, + }, + chevrons = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + RelativePositionAxes = Axes.Y, + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Children = new[] + { + new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight }, + new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight }, + new SpriteIcon { Size = new Vector2(15), Shadow = true, Icon = FontAwesome.Solid.ChevronRight }, + } + }, + skipText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + RelativePositionAxes = Axes.Y, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), + Origin = Anchor.Centre, + Text = @"SKIP", + Y = secondary_y, + }, + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SkippedCount.BindValueChanged(_ => updateCount()); + RequiredCount.BindValueChanged(_ => updateCount(), true); + Enabled.BindValueChanged(_ => updateColours(), true); + + FinishTransforms(true); + } + + private void updateChevronsSpacing() + { + if (SkippedCount.Value > 0 && RequiredCount.Value > 1) + chevrons.TransformSpacingTo(new Vector2(-5f), 500, Easing.OutQuint); + else + chevrons.TransformSpacingTo(IsHovered ? new Vector2(5f) : new Vector2(0f), 500, Easing.OutQuint); + } + + private void updateCount() + { + if (SkippedCount.Value > 0 && RequiredCount.Value > 1) + { + countText.FadeIn(300, Easing.OutQuint); + countText.Text = $"{SkippedCount.Value} / {RequiredCount.Value}"; + + chevrons.ScaleTo(0.5f, 300, Easing.OutQuint) + .MoveTo(new Vector2(-11, secondary_y), 300, Easing.OutQuint); + + skipText.MoveToX(11f, 300, Easing.OutQuint); + } + else + { + countText.FadeOut(300, Easing.OutQuint); + + chevrons.ScaleTo(1f, 300, Easing.OutQuint) + .MoveTo(new Vector2(0, chevron_y), 300, Easing.OutQuint); + + skipText.MoveToX(0f, 300, Easing.OutQuint); + } + + updateChevronsSpacing(); + updateColours(); + } + + private void updateColours() + { + if (!Enabled.Value) + { + box.FadeColour(COLOUR_GRAY, 500, Easing.OutQuint); + triangles.FadeColour(ColourInfo.GradientVertical(COLOUR_GRAY.Lighten(0.2f), COLOUR_GRAY), 500, Easing.OutQuint); + } + else + { + box.FadeColour(IsHovered ? colours.Orange3.Lighten(0.2f) : colours.Orange3, 500, Easing.OutQuint); + triangles.FadeColour(ColourInfo.GradientVertical(colours.Orange3.Lighten(0.2f), colours.Orange3), 500, Easing.OutQuint); + } + } + + protected override bool OnHover(HoverEvent e) + { + if (Enabled.Value) + { + updateChevronsSpacing(); + updateColours(); + background.FadeTo(0.4f, 500, Easing.OutQuint); + } + + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateChevronsSpacing(); + updateColours(); + background.FadeTo(0.2f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (Enabled.Value) + aspect.ScaleTo(0.75f, 2000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (Enabled.Value) + aspect.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Enabled.Value) + return false; + + sampleConfirm.Play(); + + box.FlashColour(Color4.White, 500, Easing.OutQuint); + aspect.ScaleTo(1.2f, 2000, Easing.OutQuint); + + base.OnClick(e); + + Enabled.Value = false; + return true; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + countText.Scale = new Vector2(Math.Min(0.85f * aspect.DrawWidth / countText.DrawWidth, 1)); + } + } } } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 700ea2e532..361de71103 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -10,7 +10,9 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -19,6 +21,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; @@ -41,12 +44,18 @@ namespace osu.Game.Screens.Play protected FadeContainer FadingContent { get; private set; } - private Button button; + private OsuClickableContainer button; + private ButtonContainer buttonContainer; - private Circle remainingTimeBox; + protected Circle RemainingTimeBox { get; private set; } private double displayTime; + + /// + /// Becomes when the overlay starts fading out. + /// private bool isClickable; + private bool skipQueued; [Resolved] @@ -83,17 +92,13 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - button = new Button - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - remainingTimeBox = new Circle + button = CreateButton(), + RemainingTimeBox = new Circle { Height = 5, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = colours.Yellow, + Colour = colours.Orange3, RelativeSizeAxes = Axes.X } } @@ -101,6 +106,12 @@ namespace osu.Game.Screens.Play }; } + protected virtual OsuClickableContainer CreateButton() => new Button + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + private const double fade_time = 300; private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME; @@ -174,10 +185,13 @@ namespace osu.Game.Screens.Play double progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); - remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); + RemainingTimeBox.Width = (float)Interpolation.Lerp(RemainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); isClickable = progress > 0; - button.Enabled.Value = isClickable; + + if (!isClickable) + button.Enabled.Value = false; + buttonContainer.State.Value = isClickable ? Visibility.Visible : Visibility.Hidden; } @@ -220,7 +234,7 @@ namespace osu.Game.Screens.Play float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime); float newWidth = 1 - Math.Clamp(progress, 0, 1); - remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); + RemainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 3.5, Easing.OutQuint); } public partial class FadeContainer : Container, IStateful @@ -328,8 +342,8 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuColour colours, AudioManager audio) { - colourNormal = colours.Yellow; - colourHover = colours.YellowDark; + colourNormal = colours.Orange3; + colourHover = colours.Orange3.Lighten(0.2f); sampleConfirm = audio.Samples.Get(@"UI/submit-select"); @@ -356,6 +370,11 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Colour = colourNormal, }, + new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(colourNormal.Lighten(0.2f), colourNormal) + }, flow = new FillFlowContainer { Anchor = Anchor.TopCentre,