diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs new file mode 100644 index 0000000000..df970c1c46 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -0,0 +1,80 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public class TestSceneGameplayLeaderboard : OsuTestScene + { + private readonly TestGameplayLeaderboard leaderboard; + + private readonly BindableDouble playerScore = new BindableDouble(); + + public TestSceneGameplayLeaderboard() + { + Add(leaderboard = new TestGameplayLeaderboard + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2), + RelativeSizeAxes = Axes.X, + }); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset leaderboard", () => + { + leaderboard.Clear(); + playerScore.Value = 1222333; + }); + + AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" })); + AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); + } + + [Test] + public void TestPlayerScore() + { + var player2Score = new BindableDouble(1234567); + var player3Score = new BindableDouble(1111111); + + AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" })); + AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" })); + + AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); + AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); + AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + + AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500); + AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddAssert("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); + AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + + AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456); + AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddAssert("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); + AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); + } + + private class TestGameplayLeaderboard : GameplayLeaderboard + { + public bool CheckPositionByUsername(string username, int? expectedPosition) + { + var scoreItem = this.FirstOrDefault(i => i.User.Username == username); + + return scoreItem != null && scoreItem.ScorePosition == expectedPosition; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs new file mode 100644 index 0000000000..e53c56b390 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -0,0 +1,66 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class GameplayLeaderboard : FillFlowContainer + { + public GameplayLeaderboard() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Direction = FillDirection.Vertical; + + Spacing = new Vector2(2.5f); + + LayoutDuration = 250; + LayoutEasing = Easing.OutQuint; + } + + /// + /// Adds a player to the leaderboard. + /// + /// The bindable current score of the player. + /// The player. + public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) + { + var scoreItem = addScore(currentScore.Value, user); + currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; + } + + private GameplayLeaderboardScore addScore(double totalScore, User user) + { + var scoreItem = new GameplayLeaderboardScore + { + User = user, + TotalScore = totalScore, + OnScoreChange = updateScores, + }; + + Add(scoreItem); + updateScores(); + + return scoreItem; + } + + private void updateScores() + { + var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + + for (int i = 0; i < Count; i++) + { + SetLayoutPosition(orderedByScore[i], i); + orderedByScore[i].ScorePosition = i + 1; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs new file mode 100644 index 0000000000..4c75f422c9 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -0,0 +1,136 @@ +// 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 Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class GameplayLeaderboardScore : CompositeDrawable + { + private readonly OsuSpriteText positionText, positionSymbol, userString; + private readonly GlowingSpriteText scoreText; + + public Action OnScoreChange; + + private int? scorePosition; + + public int? ScorePosition + { + get => scorePosition; + set + { + scorePosition = value; + + if (scorePosition.HasValue) + positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; + + positionText.FadeTo(scorePosition.HasValue ? 1 : 0); + positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0); + } + } + + private double totalScore; + + public double TotalScore + { + get => totalScore; + set + { + totalScore = value; + scoreText.Text = totalScore.ToString("N0"); + + OnScoreChange?.Invoke(); + } + } + + private User user; + + public User User + { + get => user; + set + { + user = value; + userString.Text = user?.Username; + } + } + + public GameplayLeaderboardScore() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + Masking = true, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Right = 2.5f }, + Spacing = new Vector2(2.5f), + Children = new[] + { + positionText = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), + }, + positionSymbol = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Text = ">", + }, + } + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = 2.5f }, + Spacing = new Vector2(2.5f), + Children = new Drawable[] + { + userString = new OsuSpriteText + { + Size = new Vector2(80, 16), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), + }, + scoreText = new GlowingSpriteText + { + GlowColour = Color4Extensions.FromHex(@"83ccfa"), + Font = OsuFont.Numeric.With(size: 14), + } + } + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + positionText.Colour = colours.YellowLight; + positionSymbol.Colour = colours.Yellow; + } + } +}