diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 276a0c3410..946b625608 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay public Drawable OverlayContent => InternalChild; - public Drawable FadingContent => (OverlayContent as Container)?.Child; + public new Drawable FadingContent => (OverlayContent as Container)?.Child; } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs new file mode 100644 index 0000000000..a1b28e2544 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSkipOverlay.cs @@ -0,0 +1,73 @@ +// 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.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public partial class TestSceneMultiplayerSkipOverlay : MultiplayerTestScene + { + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add skip overlay", () => + { + GameplayClockContainer gameplayClockContainer; + + var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0) + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new MultiplayerSkipOverlay(120000) + }, + }; + + gameplayClockContainer.Start(); + }); + + AddStep("set playing state", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Playing)); + } + + [Test] + public void TestSkip() + { + for (int i = 0; i < 4; i++) + { + int i2 = i; + + AddStep($"join user {i2}", () => + { + MultiplayerClient.AddUser(new APIUser + { + Id = i2, + Username = $"User {i2}" + }); + + MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing); + }); + } + + AddStep("local user votes", () => MultiplayerClient.VoteToSkip().WaitSafely()); + + for (int i = 0; i < 4; i++) + { + int i2 = i; + AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkip(i2).WaitSafely()); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 406f38ea72..24dfa59ed3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -148,6 +148,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.Room != null); } + protected override SkipOverlay CreateSkipOverlay(double startTime) => new MultiplayerSkipOverlay(startTime); + protected override void StartGameplay() { // We can enter this screen one of two ways: diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs new file mode 100644 index 0000000000..ccda0e8690 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerSkipOverlay.cs @@ -0,0 +1,114 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerSkipOverlay : SkipOverlay + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + private Drawable votedIcon = null!; + private OsuSpriteText countText = null!; + + public MultiplayerSkipOverlay(double startTime) + : base(startTime) + { + } + + [BackgroundDependencyLoader] + private void load() + { + 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) + } + ]); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.UserStateChanged += onUserStateChanged; + client.UserVotedToSkip += onUserVotedToSkip; + + updateText(); + } + + private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) + { + Schedule(updateText); + } + + private void onUserVotedToSkip(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); + } + }); + + private void updateText() + { + if (client.Room == null) + return; + + int countTotal = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing); + int countSkipped = client.Room.Users.Count(u => u.State == MultiplayerUserState.Playing && u.VotedToSkip); + int countRequired = countTotal / 2 + 1; + + countText.Text = $"{Math.Min(countRequired, countSkipped)} / {countRequired}"; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2927d8a720..b712a451c5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Play private BreakTracker breakTracker; - private SkipOverlay skipIntroOverlay; + protected SkipOverlay SkipIntroOverlay { get; private set; } private SkipOverlay skipOutroOverlay; protected ScoreProcessor ScoreProcessor { get; private set; } @@ -500,10 +500,10 @@ namespace osu.Game.Screens.Play }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), - skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) + SkipIntroOverlay = CreateSkipOverlay(DrawableRuleset.GameplayStartTime).With(o => { - RequestSkip = RequestIntroSkip - }, + o.RequestSkip = RequestIntroSkip; + }), skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0) { RequestSkip = () => progressToResults(false), @@ -522,13 +522,15 @@ namespace osu.Game.Screens.Play if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) { - skipIntroOverlay.Expire(); + SkipIntroOverlay.Expire(); skipOutroOverlay.Expire(); } return container; } + protected virtual SkipOverlay CreateSkipOverlay(double startTime) => new SkipOverlay(startTime); + private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { updateGameplayState(); @@ -1158,7 +1160,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Reset(startClock: true); if (Configuration.AutomaticallySkipIntro) - skipIntroOverlay.SkipWhenReady(); + SkipIntroOverlay.SkipWhenReady(); } public override void OnSuspending(ScreenTransitionEvent e) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index be8517d9a0..700ea2e532 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -38,20 +38,21 @@ namespace osu.Game.Screens.Play private readonly double startTime; public Action RequestSkip; + + protected FadeContainer FadingContent { get; private set; } + private Button button; private ButtonContainer buttonContainer; private Circle remainingTimeBox; - private FadeContainer fadeContainer; private double displayTime; - private bool isClickable; private bool skipQueued; [Resolved] private IGameplayClock gameplayClock { get; set; } - internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; + internal bool IsButtonVisible => FadingContent.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -77,7 +78,7 @@ namespace osu.Game.Screens.Play InternalChild = buttonContainer = new ButtonContainer { RelativeSizeAxes = Axes.Both, - Child = fadeContainer = new FadeContainer + Child = FadingContent = new FadeContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -107,13 +108,13 @@ namespace osu.Game.Screens.Play public override void Hide() { base.Hide(); - fadeContainer.Hide(); + FadingContent.Hide(); } public override void Show() { base.Show(); - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); } protected override void LoadComplete() @@ -136,7 +137,7 @@ namespace osu.Game.Screens.Play RequestSkip?.Invoke(); }; - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); } /// @@ -183,7 +184,7 @@ namespace osu.Game.Screens.Play protected override bool OnMouseMove(MouseMoveEvent e) { if (isClickable && !e.HasAnyButtonPressed) - fadeContainer.TriggerShow(); + FadingContent.TriggerShow(); return base.OnMouseMove(e); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 242bbe3083..fcee7e5b44 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -563,7 +563,12 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task VoteToSkip() { - return Task.CompletedTask; + return UserVoteToSkip(api.LocalUser.Value.OnlineID); + } + + public async Task UserVoteToSkip(int userId) + { + await ((IMultiplayerClient)this).UserVotedToSkip(userId); } protected override Task CreateRoomInternal(MultiplayerRoom room)