diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 17fe09f2c6..0441c5641e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true)); + AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -83,19 +84,38 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 })); } + [Test] + public void TestMaxHeight() + { + int playerNumber = 1; + AddRepeatStep("add 3 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 3); + checkHeight(4); + + AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4); + checkHeight(8); + + AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4); + checkHeight(8); + + void checkHeight(int panelCount) + => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); + } + private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user); private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false) { - var leaderboardScore = leaderboard.AddPlayer(user, isTracked); + var leaderboardScore = leaderboard.Add(user, isTracked); leaderboardScore.TotalScore.BindTo(score); } private class TestGameplayLeaderboard : GameplayLeaderboard { + public float Spacing => Flow.Spacing.Y; + public bool CheckPositionByUsername(string username, int? expectedPosition) { - var scoreItem = this.FirstOrDefault(i => i.User?.Username == username); + var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username); return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs new file mode 100644 index 0000000000..ff06d4d9c7 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -0,0 +1,51 @@ +// 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 NUnit.Framework; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerResults : ScreenTestScene + { + [Test] + public void TestDisplayResults() + { + MultiplayerResultsScreen screen = null; + + AddStep("show results screen", () => + { + var rulesetInfo = new OsuRuleset().RulesetInfo; + var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; + + var score = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + Beatmap = beatmapInfo, + User = new User { Username = "Test user" }, + Date = DateTimeOffset.Now, + OnlineScoreID = 12345, + Ruleset = rulesetInfo, + }; + + PlaylistItem playlistItem = new PlaylistItem + { + BeatmapID = beatmapInfo.ID, + }; + + Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem)); + }); + + AddUntilStep("wait for loaded", () => screen.IsLoaded); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs new file mode 100644 index 0000000000..0a8bda7ec0 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -0,0 +1,61 @@ +// 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.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerTeamResults : ScreenTestScene + { + [TestCase(7483253, 1048576)] + [TestCase(1048576, 7483253)] + [TestCase(1048576, 1048576)] + public void TestDisplayTeamResults(int team1Score, int team2Score) + { + MultiplayerResultsScreen screen = null; + + AddStep("show results screen", () => + { + var rulesetInfo = new OsuRuleset().RulesetInfo; + var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; + + var score = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + Beatmap = beatmapInfo, + User = new User { Username = "Test user" }, + Date = DateTimeOffset.Now, + OnlineScoreID = 12345, + Ruleset = rulesetInfo, + }; + + PlaylistItem playlistItem = new PlaylistItem + { + BeatmapID = beatmapInfo.ID, + }; + + SortedDictionary teamScores = new SortedDictionary + { + { 0, new BindableInt(team1Score) }, + { 1, new BindableInt(team2Score) } + }; + + Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, playlistItem, teamScores)); + }); + + AddUntilStep("wait for loaded", () => screen.IsLoaded); + } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 60a0d5a0ac..6c7adcc806 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -101,7 +101,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.HitLighting, true); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); - SetDefault(OsuSetting.ShowProgressGraph, true); + SetDefault(OsuSetting.ShowDifficultyGraph, true); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.KeyOverlay, false); @@ -217,7 +217,7 @@ namespace osu.Game.Configuration AlwaysPlayFirstComboBreak, FloatingComments, HUDVisibilityMode, - ShowProgressGraph, + ShowDifficultyGraph, ShowHealthDisplayWhenCantFail, FadePlayfieldWhenHealthLow, MouseDisableButtons, diff --git a/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs new file mode 100644 index 0000000000..111c068bbd --- /dev/null +++ b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs @@ -0,0 +1,24 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class MultiplayerTeamResultsScreenStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerTeamResultsScreen"; + + /// + /// "Team {0} wins!" + /// + public static LocalisableString TeamWins(string winner) => new TranslatableString(getKey(@"team_wins"), @"Team {0} wins!", winner); + + /// + /// "The teams are tied!" + /// + public static LocalisableString TheTeamsAreTied => new TranslatableString(getKey(@"the_teams_are_tied"), @"The teams are tied!"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 064065ab00..8f16d22c4c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -31,6 +31,15 @@ namespace osu.Game.Online.Multiplayer /// The user. Task UserLeft(MultiplayerRoomUser user); + /// + /// Signals that a user has been kicked from the room. + /// + /// + /// This will also be sent to the user that was kicked. + /// + /// The user. + Task UserKicked(MultiplayerRoomUser user); + /// /// Signal that the host of the room has changed. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4607211cdf..2a0635c98c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -389,6 +389,18 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user) + { + if (LocalUser == null) + return Task.CompletedTask; + + if (user.Equals(LocalUser)) + LeaveRoom(); + + // TODO: also inform users of the kick operation. + return ((IMultiplayerClient)this).UserLeft(user); + } + Task IMultiplayerClient.HostChanged(int userId) { if (Room == null) diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 55477a9fc7..c38a648a6a 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -50,6 +50,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + connection.On(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked); connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 3f1034759e..757698e1aa 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -78,10 +78,10 @@ namespace osu.Game.Overlays.BeatmapSet Direction = FillDirection.Horizontal, Children = new[] { - length = new Statistic(FontAwesome.Regular.Clock, "Length") { Width = 0.25f }, - bpm = new Statistic(FontAwesome.Regular.Circle, "BPM") { Width = 0.25f }, - circleCount = new Statistic(FontAwesome.Regular.Circle, "Circle Count") { Width = 0.25f }, - sliderCount = new Statistic(FontAwesome.Regular.Circle, "Slider Count") { Width = 0.25f }, + length = new Statistic(BeatmapStatisticsIconType.Length, "Length") { Width = 0.25f }, + bpm = new Statistic(BeatmapStatisticsIconType.Bpm, "BPM") { Width = 0.25f }, + circleCount = new Statistic(BeatmapStatisticsIconType.Circles, "Circle Count") { Width = 0.25f }, + sliderCount = new Statistic(BeatmapStatisticsIconType.Sliders, "Slider Count") { Width = 0.25f }, }, }; } @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapSet set => this.value.Text = value; } - public Statistic(IconUsage icon, string name) + public Statistic(BeatmapStatisticsIconType icon, string name) { TooltipText = name; RelativeSizeAxes = Axes.X; @@ -133,8 +133,16 @@ namespace osu.Game.Overlays.BeatmapSet { Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, - Icon = icon, - Size = new Vector2(12), + Icon = FontAwesome.Regular.Circle, + Size = new Vector2(10), + Rotation = 0, + Colour = Color4Extensions.FromHex(@"f7dd55"), + }, + new BeatmapStatisticIcon(icon) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Size = new Vector2(10), Colour = Color4Extensions.FromHex(@"f7dd55"), Scale = new Vector2(0.8f), }, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 353292606f..69aa57082a 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Show difficulty graph on progress bar", - Current = config.GetBindable(OsuSetting.ShowProgressGraph) + Current = config.GetBindable(OsuSetting.ShowDifficultyGraph) }, new SettingsCheckbox { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 72ba4d62fb..6277afa8bb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -277,7 +277,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer isConnected.BindValueChanged(connected => { if (!connected.NewValue) - Schedule(this.Exit); + handleRoomLost(); }, true); currentRoom.BindValueChanged(room => @@ -287,7 +287,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // the room has gone away. // this could mean something happened during the join process, or an external connection issue occurred. // one specific scenario is where the underlying room is created, but the signalr server returns an error during the join process. this triggers a PartRoom operation (see https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs#L97) - Schedule(this.Exit); + handleRoomLost(); } }, true); } @@ -451,9 +451,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onRoomUpdated() { + // may happen if the client is kicked or otherwise removed from the room. + if (client.Room == null) + { + handleRoomLost(); + return; + } + Scheduler.AddOnce(UpdateMods); } + private void handleRoomLost() => Schedule(() => + { + if (this.IsCurrentScreen()) + this.Exit(); + else + ValidForResume = false; + }); + private void onLoadRequested() { if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 3ba7b8b982..ca1a3710ab 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -181,7 +181,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(RoomId.Value != null); - return new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem); + return leaderboard.TeamScores.Count == 2 + ? new MultiplayerTeamResultsScreen(score, RoomId.Value.Value, PlaylistItem, leaderboard.TeamScores) + : new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs new file mode 100644 index 0000000000..14a779dedf --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs @@ -0,0 +1,152 @@ +// 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.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; +using osu.Game.Online.Rooms; +using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerTeamResultsScreen : MultiplayerResultsScreen + { + private readonly SortedDictionary teamScores; + + private Container winnerBackground; + private Drawable winnerText; + + public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary teamScores) + : base(score, roomId, playlistItem) + { + if (teamScores.Count != 2) + throw new NotSupportedException(@"This screen currently only supports 2 teams"); + + this.teamScores = teamScores; + } + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + const float winner_background_half_height = 250; + + VerticalScrollContent.Anchor = VerticalScrollContent.Origin = Anchor.TopCentre; + VerticalScrollContent.Scale = new Vector2(0.9f); + VerticalScrollContent.Y = 75; + + var redScore = teamScores.First().Value; + var blueScore = teamScores.Last().Value; + + LocalisableString winner; + Colour4 winnerColour; + + int comparison = redScore.Value.CompareTo(blueScore.Value); + + if (comparison < 0) + { + // team name should eventually be coming from the multiplayer match state. + winner = MultiplayerTeamResultsScreenStrings.TeamWins(@"Blue"); + winnerColour = colours.TeamColourBlue; + } + else if (comparison > 0) + { + // team name should eventually be coming from the multiplayer match state. + winner = MultiplayerTeamResultsScreenStrings.TeamWins(@"Red"); + winnerColour = colours.TeamColourRed; + } + else + { + winner = MultiplayerTeamResultsScreenStrings.TheTeamsAreTied; + winnerColour = Colour4.White.Opacity(0.5f); + } + + AddRangeInternal(new Drawable[] + { + new MatchScoreDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Team1Score = { BindTarget = redScore }, + Team2Score = { BindTarget = blueScore }, + }, + winnerBackground = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = winner_background_half_height, + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0), Colour4.Black.Opacity(0.4f)) + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = winner_background_half_height, + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0.4f), Colour4.Black.Opacity(0)) + } + } + }, + (winnerText = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.Torus.With(size: 80, weight: FontWeight.Bold), + Text = winner, + Blending = BlendingParameters.Additive + }).WithEffect(new GlowEffect + { + Colour = winnerColour, + }).With(e => + { + e.Anchor = Anchor.Centre; + e.Origin = Anchor.Centre; + }) + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + using (BeginDelayedSequence(300)) + { + const double fade_in_duration = 600; + + winnerText.FadeInFromZero(fade_in_duration, Easing.InQuint); + winnerBackground.FadeInFromZero(fade_in_duration, Easing.InQuint); + + winnerText + .ScaleTo(10) + .ScaleTo(1, 600, Easing.InQuad) + .Then() + .ScaleTo(1.02f, 1600, Easing.OutQuint) + .FadeOut(5000, Easing.InQuad); + winnerBackground.Delay(2200).FadeOut(2000); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 63cb4f89f5..871555e5a3 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -6,29 +6,58 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Users; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public class GameplayLeaderboard : FillFlowContainer + public class GameplayLeaderboard : CompositeDrawable { + private readonly int maxPanels; private readonly Cached sorting = new Cached(); public Bindable Expanded = new Bindable(); - public GameplayLeaderboard() + protected readonly FillFlowContainer Flow; + + private bool requiresScroll; + private readonly OsuScrollContainer scroll; + + private GameplayLeaderboardScore trackedScore; + + /// + /// Create a new leaderboard. + /// + /// The maximum panels to show at once. Defines the maximum height of this component. + public GameplayLeaderboard(int maxPanels = 8) { + this.maxPanels = maxPanels; + Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; - Direction = FillDirection.Vertical; - - Spacing = new Vector2(2.5f); - - LayoutDuration = 250; - LayoutEasing = Easing.OutQuint; + InternalChildren = new Drawable[] + { + scroll = new InputDisabledScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = Flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + X = GameplayLeaderboardScore.SHEAR_WIDTH, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2.5f), + LayoutDuration = 450, + LayoutEasing = Easing.OutQuint, + } + } + }; } protected override void LoadComplete() @@ -46,26 +75,87 @@ namespace osu.Game.Screens.Play.HUD /// Whether the player should be tracked on the leaderboard. /// Set to true for the local player or a player whose replay is currently being played. /// - public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked) + public ILeaderboardScore Add([CanBeNull] User user, bool isTracked) { var drawable = CreateLeaderboardScoreDrawable(user, isTracked); + if (isTracked) + { + if (trackedScore != null) + throw new InvalidOperationException("Cannot track more than one score."); + + trackedScore = drawable; + } + drawable.Expanded.BindTo(Expanded); - base.Add(drawable); + Flow.Add(drawable); drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); - Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y); + int displayCount = Math.Min(Flow.Count, maxPanels); + Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); + requiresScroll = displayCount != Flow.Count; return drawable; } + public void Clear() + { + Flow.Clear(); + trackedScore = null; + scroll.ScrollToStart(false); + } + protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) => new GameplayLeaderboardScore(user, isTracked); - public sealed override void Add(GameplayLeaderboardScore drawable) + protected override void Update() { - throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); + base.Update(); + + if (requiresScroll && trackedScore != null) + { + float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2; + scroll.ScrollTo(scrollTarget, false); + } + + const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT; + + float fadeBottom = scroll.Current + scroll.DrawHeight; + float fadeTop = scroll.Current + panel_height; + + if (scroll.Current <= 0) fadeTop -= panel_height; + if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height; + + // logic is mostly shared with Leaderboard, copied here for simplicity. + foreach (var c in Flow.Children) + { + float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y; + float bottomY = topY + panel_height; + + bool requireTopFade = requiresScroll && topY <= fadeTop; + bool requireBottomFade = requiresScroll && bottomY >= fadeBottom; + + if (!requireTopFade && !requireBottomFade) + c.Colour = Color4.White; + else if (topY > fadeBottom + panel_height || bottomY < fadeTop - panel_height) + c.Colour = Color4.Transparent; + else + { + if (requireBottomFade) + { + c.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / panel_height, 1)), + Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / panel_height, 1))); + } + else if (requiresScroll) + { + c.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / panel_height, 1)), + Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / panel_height, 1))); + } + } + } } private void sort() @@ -73,15 +163,26 @@ namespace osu.Game.Screens.Play.HUD if (sorting.IsValid) return; - var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); + var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList(); - for (int i = 0; i < Count; i++) + for (int i = 0; i < Flow.Count; i++) { - SetLayoutPosition(orderedByScore[i], i); + Flow.SetLayoutPosition(orderedByScore[i], i); orderedByScore[i].ScorePosition = i + 1; } sorting.Validate(); } + + private class InputDisabledScrollContainer : OsuScrollContainer + { + public InputDisabledScrollContainer() + { + ScrollbarVisible = false; + } + + public override bool HandlePositionalInput => false; + public override bool HandleNonPositionalInput => false; + } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 433bf78e9b..85cf9d1966 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -81,7 +81,10 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] public User User { get; } - private readonly bool trackedPlayer; + /// + /// Whether this score is the local user or a replay player (and should be focused / always visible). + /// + public readonly bool Tracked; private Container mainFillContainer; @@ -97,11 +100,11 @@ namespace osu.Game.Screens.Play.HUD /// Creates a new . /// /// The score's player. - /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore([CanBeNull] User user, bool trackedPlayer) + /// Whether the player is the local user or a replay player. + public GameplayLeaderboardScore([CanBeNull] User user, bool tracked) { User = user; - this.trackedPlayer = trackedPlayer; + Tracked = tracked; AutoSizeAxes = Axes.X; Height = PANEL_HEIGHT; @@ -338,7 +341,7 @@ namespace osu.Game.Screens.Play.HUD panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33"); textColour = TextColour ?? Color4.White; } - else if (trackedPlayer) + else if (Tracked) { widthExtension = true; panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs index c77b872786..68e3f0df7d 100644 --- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -104,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); Team1Score.BindValueChanged(_ => updateScores()); - Team2Score.BindValueChanged(_ => updateScores()); + Team2Score.BindValueChanged(_ => updateScores(), true); } private void updateScores() @@ -171,6 +172,8 @@ namespace osu.Game.Screens.Play.HUD => displayedSpriteText.Font = winning ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0"); } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 3f9258930e..19cb6aeb50 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD var trackedUser = UserScores[user.Id]; - var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id); + var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); leaderboardScore.TotalScore.BindTo(trackedUser.Score); leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD continue; if (TeamScores.TryGetValue(u.Team.Value, out var team)) - team.Value += (int)u.Score.Value; + team.Value += (int)Math.Round(u.Score.Value); } } diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index f28622f42e..6aa7e017ce 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Play Objects = drawableRuleset.Objects; } - config.BindWith(OsuSetting.ShowProgressGraph, ShowGraph); + config.BindWith(OsuSetting.ShowDifficultyGraph, ShowGraph); graph.FillColour = bar.FillColour = colours.BlueLighter; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index b458d7c17f..d44d1f2cc9 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -40,6 +40,8 @@ namespace osu.Game.Screens.Ranking protected ScorePanelList ScorePanelList { get; private set; } + protected VerticalScrollContainer VerticalScrollContent { get; private set; } + [Resolved(CanBeNull = true)] private Player player { get; set; } @@ -77,7 +79,7 @@ namespace osu.Game.Screens.Ranking { new Drawable[] { - new VerticalScrollContainer + VerticalScrollContent = new VerticalScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, @@ -343,7 +345,7 @@ namespace osu.Game.Screens.Ranking { } - private class VerticalScrollContainer : OsuScrollContainer + protected class VerticalScrollContainer : OsuScrollContainer { protected override Container Content => content; @@ -351,6 +353,8 @@ namespace osu.Game.Screens.Ranking public VerticalScrollContainer() { + Masking = false; + base.Content.Add(content = new Container { RelativeSizeAxes = Axes.X }); } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 3779523094..5b4e077100 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -35,6 +35,7 @@ namespace osu.Game.Screens.Select { public class BeatmapInfoWedge : VisibilityContainer { + public const float BORDER_THICKNESS = 2.5f; private const float shear_width = 36.75f; private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); @@ -59,7 +60,7 @@ namespace osu.Game.Screens.Select Shear = wedged_container_shear; Masking = true; BorderColour = new Color4(221, 255, 255, 255); - BorderThickness = 2.5f; + BorderThickness = BORDER_THICKNESS; Alpha = 0; EdgeEffect = new EdgeEffectParameters { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 0e113a71bc..bb3df0d4e0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -203,6 +203,7 @@ namespace osu.Game.Screens.Select Margin = new MarginPadding { Right = left_area_padding, + Left = -BeatmapInfoWedge.BORDER_THICKNESS, // Hide the left border }, }, new Container diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index a28b4140ca..67b79d7390 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); - return ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.UserID == userId)); + return ((IMultiplayerClient)this).UserKicked(Room.Users.Single(u => u.UserID == userId)); } public override async Task ChangeSettings(MultiplayerRoomSettings settings)