diff --git a/osu.Android.props b/osu.Android.props index 8de516240f..454bb46059 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index e14df62af1..ade24b8740 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -6,9 +6,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Timing; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { @@ -31,7 +33,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }; foreach (var (userId, _) in clocks) + { SpectatorClient.StartPlay(userId, 0); + OnlinePlayDependencies.Client.AddUser(new User { Id = userId }); + } }); AddStep("create leaderboard", () => @@ -41,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var scoreProcessor = new OsuScoreProcessor(); scoreProcessor.ApplyBeatmap(playable); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add); }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index e9fae32335..65b1d6d53a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -8,6 +8,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Rulesets.UI; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play; @@ -26,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private MultiSpectatorScreen spectatorScreen; - private readonly List playingUserIds = new List(); + private readonly List playingUsers = new List(); private BeatmapSetInfo importedSet; private BeatmapInfo importedBeatmap; @@ -41,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [SetUp] - public new void Setup() => Schedule(() => playingUserIds.Clear()); + public new void Setup() => Schedule(() => playingUsers.Clear()); [Test] public void TestDelayedStart() @@ -51,8 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true); - playingUserIds.Add(PLAYER_1_ID); - playingUserIds.Add(PLAYER_2_ID); + playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID)); + playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID)); }); loadSpectateScreen(false); @@ -78,6 +80,38 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait a bit", 20); } + [Test] + public void TestTeamDisplay() + { + AddStep("start players", () => + { + var player1 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true); + player1.MatchState = new TeamVersusUserState + { + TeamID = 0, + }; + + var player2 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true); + player2.MatchState = new TeamVersusUserState + { + TeamID = 1, + }; + + SpectatorClient.StartPlay(player1.UserID, importedBeatmapId); + SpectatorClient.StartPlay(player2.UserID, importedBeatmapId); + + playingUsers.Add(player1); + playingUsers.Add(player2); + }); + + loadSpectateScreen(); + + sendFrames(PLAYER_1_ID, 1000); + sendFrames(PLAYER_2_ID, 1000); + + AddWaitStep("wait a bit", 20); + } + [Test] public void TestTimeDoesNotProgressWhileAllPlayersPaused() { @@ -254,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Ruleset.Value = importedBeatmap.Ruleset; - LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray())); + LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray())); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); @@ -269,7 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true); SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); - playingUserIds.Add(id); + playingUsers.Add(new MultiplayerRoomUser(id)); } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 8121492a0b..3317ddc767 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; @@ -51,12 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer OsuScoreProcessor scoreProcessor; Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var multiplayerUsers = new List(); foreach (var user in users) { SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true); + multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true)); } Children = new Drawable[] @@ -64,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer scoreProcessor = new OsuScoreProcessor(), }; - scoreProcessor.ApplyBeatmap(playable); + scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray()) + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs new file mode 100644 index 0000000000..dfaf2f1dc3 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -0,0 +1,121 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.OnlinePlay; +using osu.Game.Tests.Visual.Spectator; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene + { + private static IEnumerable users => Enumerable.Range(0, 16); + + public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient => + (TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient; + + protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies(); + + protected class TestDependencies : MultiplayerTestSceneDependencies + { + protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient(); + } + + private MultiplayerGameplayLeaderboard leaderboard; + private GameplayMatchScoreDisplay gameplayScoreDisplay; + + protected override Room CreateRoom() + { + var room = base.CreateRoom(); + room.Type.Value = MatchType.TeamVersus; + return room; + } + + [SetUpSteps] + public override void SetUpSteps() + { + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result); + + AddStep("create leaderboard", () => + { + leaderboard?.Expire(); + + OsuScoreProcessor scoreProcessor; + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var multiplayerUsers = new List(); + + foreach (var user in users) + { + SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + var roomUser = OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true); + + roomUser.MatchState = new TeamVersusUserState + { + TeamID = RNG.Next(0, 2) + }; + + multiplayerUsers.Add(roomUser); + } + + Children = new Drawable[] + { + scoreProcessor = new OsuScoreProcessor(), + }; + + scoreProcessor.ApplyBeatmap(playableBeatmap); + + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, gameplayLeaderboard => + { + LoadComponentAsync(new MatchScoreDisplay + { + Team1Score = { BindTarget = leaderboard.TeamScores[0] }, + Team2Score = { BindTarget = leaderboard.TeamScores[1] } + }, Add); + + LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Team1Score = { BindTarget = leaderboard.TeamScores[0] }, + Team2Score = { BindTarget = leaderboard.TeamScores[1] } + }, Add); + + Add(gameplayLeaderboard); + }); + }); + + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); + } + + [Test] + public void TestScoreUpdates() + { + AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100); + AddToggleStep("switch compact mode", expanded => + { + leaderboard.Expanded.Value = expanded; + gameplayScoreDisplay.Expanded.Value = expanded; + }); + } + } +} diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs similarity index 98% rename from osu.Game.Tests/Visual/TestSceneOsuGame.cs rename to osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index c52d846a68..26641214b1 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; -using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -30,10 +29,9 @@ using osu.Game.Skinning; using osu.Game.Utils; using osuTK.Graphics; -namespace osu.Game.Tests.Visual +namespace osu.Game.Tests.Visual.Navigation { [TestFixture] - [HeadlessTest] public class TestSceneOsuGame : OsuTestScene { private IReadOnlyList requiredGameDependencies => new[] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs index 53693d1b70..3b43f8485a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneContextMenu.cs @@ -1,6 +1,8 @@ // 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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -68,13 +70,40 @@ namespace osu.Game.Tests.Visual.UserInterface ); } - private class MyContextMenuContainer : Container, IHasContextMenu + private static MenuItem[] makeMenu() { - public MenuItem[] ContextMenuItems => new MenuItem[] + return new MenuItem[] { new OsuMenuItem(@"Some option"), new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted), new OsuMenuItem(@"Another option"), + new OsuMenuItem(@"Nested option >") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Sub-One"), + new OsuMenuItem(@"Sub-Two"), + new OsuMenuItem(@"Sub-Three"), + new OsuMenuItem(@"Sub-Nested option >") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Double Sub-One"), + new OsuMenuItem(@"Double Sub-Two"), + new OsuMenuItem(@"Double Sub-Three"), + new OsuMenuItem(@"Sub-Sub-Nested option >") + { + Items = new MenuItem[] + { + new OsuMenuItem(@"Too Deep One"), + new OsuMenuItem(@"Too Deep Two"), + new OsuMenuItem(@"Too Deep Three"), + } + } + } + } + } + }, new OsuMenuItem(@"Choose me please"), new OsuMenuItem(@"And me too"), new OsuMenuItem(@"Trying to fill"), @@ -82,17 +111,29 @@ namespace osu.Game.Tests.Visual.UserInterface }; } + private class MyContextMenuContainer : Container, IHasContextMenu + { + public MenuItem[] ContextMenuItems => makeMenu(); + } + private class AnotherContextMenuContainer : Container, IHasContextMenu { - public MenuItem[] ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems { - new OsuMenuItem(@"Simple option"), - new OsuMenuItem(@"Simple very very long option"), - new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), - new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), - new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), - new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), - }; + get + { + List items = makeMenu().ToList(); + items.AddRange(new MenuItem[] + { + new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), + }); + + return items.ToArray(); + } + } } } } diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs index acd5d53310..11b5cc7556 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Tests.Components public TestSceneMatchScoreDisplay() { - Add(new MatchScoreDisplay + Add(new TournamentMatchScoreDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs similarity index 97% rename from osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs rename to osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs index 695c6d6f3e..994dee4da0 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -16,7 +16,8 @@ using osuTK; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class MatchScoreDisplay : CompositeDrawable + // TODO: Update to derive from osu-side class? + public class TournamentMatchScoreDisplay : CompositeDrawable { private const float bar_height = 18; @@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly Drawable score1Bar; private readonly Drawable score2Bar; - public MatchScoreDisplay() + public TournamentMatchScoreDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index f61506d7f2..540b45eb56 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tournament.Screens.Gameplay }, } }, - scoreDisplay = new MatchScoreDisplay + scoreDisplay = new TournamentMatchScoreDisplay { Y = -147, Anchor = Anchor.BottomCentre, @@ -148,7 +148,7 @@ namespace osu.Game.Tournament.Screens.Gameplay } private ScheduledDelegate scheduledOperation; - private MatchScoreDisplay scoreDisplay; + private TournamentMatchScoreDisplay scoreDisplay; private TourneyState lastState; private MatchHeader header; diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index cd0e601a2f..7a43fee013 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -26,8 +26,8 @@ namespace osu.Game.Tournament { public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; - public static readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414"); - public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA"); + public static readonly Color4 COLOUR_RED = new OsuColour().TeamColourRed; + public static readonly Color4 COLOUR_BLUE = new OsuColour().TeamColourBlue; public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff"); public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index fbb3fa0e6c..171ad4ee65 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -1,6 +1,7 @@ // 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.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -9,6 +10,14 @@ namespace osu.Game.Graphics.Cursor { public class OsuContextMenuContainer : ContextMenuContainer { - protected override Menu CreateMenu() => new OsuContextMenu(); + [Cached] + private OsuContextMenuSamples samples = new OsuContextMenuSamples(); + + public OsuContextMenuContainer() + { + AddInternal(samples); + } + + protected override Menu CreateMenu() => new OsuContextMenu(true); } } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 1f87c06dd2..d7cfc4094c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -130,6 +130,9 @@ namespace osu.Game.Graphics return Gray(brightness > 0.5f ? 0.2f : 0.9f); } + public readonly Color4 TeamColourRed = Color4Extensions.FromHex("#AA1414"); + public readonly Color4 TeamColourBlue = Color4Extensions.FromHex("#1462AA"); + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 8c7b44f952..cf201b18b4 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -3,6 +3,7 @@ using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; @@ -14,7 +15,14 @@ namespace osu.Game.Graphics.UserInterface { private const int fade_duration = 250; - public OsuContextMenu() + [Resolved] + private OsuContextMenuSamples samples { get; set; } + + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. + private bool wasOpened; + private readonly bool playClickSample; + + public OsuContextMenu(bool playClickSample = false) : base(Direction.Vertical) { MaskingContainer.CornerRadius = 5; @@ -28,16 +36,38 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; MaxHeight = 250; + + this.playClickSample = playClickSample; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { BackgroundColour = colours.ContextMenuGray; } - protected override void AnimateOpen() => this.FadeIn(fade_duration, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(fade_duration, Easing.OutQuint); + protected override void AnimateOpen() + { + this.FadeIn(fade_duration, Easing.OutQuint); + + if (playClickSample) + samples.PlayClickSample(); + + if (!wasOpened) + samples.PlayOpenSample(); + + wasOpened = true; + } + + protected override void AnimateClose() + { + this.FadeOut(fade_duration, Easing.OutQuint); + + if (wasOpened) + samples.PlayCloseSample(); + + wasOpened = false; + } protected override Menu CreateSubMenu() => new OsuContextMenu(); } diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs new file mode 100644 index 0000000000..d67ea499e5 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuContextMenuSamples.cs @@ -0,0 +1,35 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +using osu.Framework.Graphics; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuContextMenuSamples : Component + { + private Sample sampleClick; + private Sample sampleOpen; + private Sample sampleClose; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, AudioManager audio) + { + sampleClick = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); + sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + } + + public void PlayClickSample() => Scheduler.AddOnce(playClickSample); + private void playClickSample() => sampleClick.Play(); + + public void PlayOpenSample() => Scheduler.AddOnce(playOpenSample); + private void playOpenSample() => sampleOpen.Play(); + + public void PlayCloseSample() => Scheduler.AddOnce(playCloseSample); + private void playCloseSample() => sampleClose.Play(); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs new file mode 100644 index 0000000000..20a88545c5 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayMatchScoreDisplay.cs @@ -0,0 +1,40 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class GameplayMatchScoreDisplay : MatchScoreDisplay + { + public Bindable Expanded = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scale = new Vector2(0.5f); + + Expanded.BindValueChanged(expandedChanged, true); + } + + private void expandedChanged(ValueChangedEvent expanded) + { + if (expanded.NewValue) + { + Score1Text.FadeIn(500, Easing.OutQuint); + Score2Text.FadeIn(500, Easing.OutQuint); + this.ResizeWidthTo(2, 500, Easing.OutQuint); + } + else + { + Score1Text.FadeOut(500, Easing.OutQuint); + Score2Text.FadeOut(500, Easing.OutQuint); + this.ResizeWidthTo(1, 500, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 561fa220c8..9fa19aaf21 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -475,16 +475,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override Screen CreateGameplayScreen() { Debug.Assert(client.LocalUser != null); + Debug.Assert(client.Room != null); int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); + MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray(); switch (client.LocalUser.State) { case MultiplayerUserState.Spectating: - return new MultiSpectatorScreen(userIds); + return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray()); default: - return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); + return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, users)); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index b54a4a7726..3ba7b8b982 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -3,9 +3,12 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; @@ -34,16 +37,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerGameplayLeaderboard leaderboard; - private readonly int[] userIds; + private readonly MultiplayerRoomUser[] users; private LoadingLayer loadingDisplay; + private FillFlowContainer leaderboardFlow; /// /// Construct a multiplayer player. /// /// The playlist item to be played. - /// The users which are participating in this game. - public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds) + /// The users which are participating in this game. + public MultiplayerPlayer(PlaylistItem playlistItem, MultiplayerRoomUser[] users) : base(playlistItem, new PlayerConfiguration { AllowPause = false, @@ -51,14 +55,41 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AllowSkipping = false, }) { - this.userIds = userIds; + this.users = users; } [BackgroundDependencyLoader] private void load() { + if (!LoadedBeatmapSuccessfully) + return; + + HUDOverlay.Add(leaderboardFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + }); + // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, users), l => + { + if (!LoadedBeatmapSuccessfully) + return; + + ((IBindable)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud); + + leaderboardFlow.Add(l); + + if (leaderboard.TeamScores.Count >= 2) + { + LoadComponentAsync(new GameplayMatchScoreDisplay + { + Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, + Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, + Expanded = { BindTarget = HUDOverlay.ShowHud }, + }, leaderboardFlow.Add); + } + }); HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); } @@ -67,6 +98,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadAsyncComplete(); + if (!LoadedBeatmapSuccessfully) + return; + if (!ValidForResume) return; // token retrieval may have failed. @@ -92,13 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.Room != null); } - protected override void LoadComplete() - { - base.LoadComplete(); - - ((IBindable)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud); - } - protected override void StartGameplay() { // block base call, but let the server know we are ready to start. @@ -118,6 +145,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void Update() { base.Update(); + + if (!LoadedBeatmapSuccessfully) + return; + adjustLeaderboardPosition(); } @@ -125,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { const float padding = 44; // enough margin to avoid the hit error display. - leaderboard.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); + leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); } private void onMatchStarted() => Scheduler.Add(() => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs index 55c4270c70..1614828a78 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs @@ -4,6 +4,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Timing; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -11,8 +12,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds) - : base(scoreProcessor, userIds) + public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) + : base(scoreProcessor, users) { } @@ -32,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ((SpectatingTrackedUserData)data).Clock = null; } - protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor); + protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, scoreProcessor); protected override void Update() { @@ -47,8 +48,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [CanBeNull] public IClock Clock; - public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor) - : base(userId, scoreProcessor) + public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) + : base(user, scoreProcessor) { } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 56ed7a9564..d10917259d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Spectate; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate @@ -45,20 +46,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private PlayerArea currentAudioSource; private bool canStartMasterClock; + private readonly MultiplayerRoomUser[] users; + /// /// Creates a new . /// - /// The players to spectate. - public MultiSpectatorScreen(int[] userIds) - : base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray()) + /// The players to spectate. + public MultiSpectatorScreen(MultiplayerRoomUser[] users) + : base(users.Select(u => u.UserID).ToArray()) { - instances = new PlayerArea[UserIds.Count]; + this.users = users; + + instances = new PlayerArea[Users.Count]; } [BackgroundDependencyLoader] private void load() { Container leaderboardContainer; + Container scoreDisplayContainer; + masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); InternalChildren = new[] @@ -67,28 +74,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate masterClockContainer.WithChild(new GridContainer { RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, Content = new[] { new Drawable[] { - leaderboardContainer = new Container + scoreDisplayContainer = new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y }, - grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = new[] + { + new Drawable[] + { + leaderboardContainer = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } + } + } + } } } }) }; - for (int i = 0; i < UserIds.Count; i++) + for (int i = 0; i < Users.Count; i++) { - grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock)); + grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer.GameplayClock)); syncManager.AddPlayerClock(instances[i].GameplayClock); } @@ -97,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); scoreProcessor.ApplyBeatmap(playableBeatmap); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray()) + LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users) { Expanded = { Value = true }, Anchor = Anchor.CentreLeft, @@ -108,6 +131,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate leaderboard.AddClock(instance.UserId, instance.GameplayClock); leaderboardContainer.Add(leaderboard); + + if (leaderboard.TeamScores.Count == 2) + { + LoadComponentAsync(new MatchScoreDisplay + { + Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, + Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, + }, scoreDisplayContainer.Add); + } }); } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 34efeab54c..63cb4f89f5 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -48,10 +48,9 @@ namespace osu.Game.Screens.Play.HUD /// public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked) { - var drawable = new GameplayLeaderboardScore(user, isTracked) - { - Expanded = { BindTarget = Expanded }, - }; + var drawable = CreateLeaderboardScoreDrawable(user, isTracked); + + drawable.Expanded.BindTo(Expanded); base.Add(drawable); drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); @@ -61,6 +60,9 @@ namespace osu.Game.Screens.Play.HUD return drawable; } + protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) => + new GameplayLeaderboardScore(user, isTracked); + public sealed override void Add(GameplayLeaderboardScore drawable) { throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 10476e5565..433bf78e9b 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -54,6 +54,10 @@ namespace osu.Game.Screens.Play.HUD public BindableInt Combo { get; } = new BindableInt(); public BindableBool HasQuit { get; } = new BindableBool(); + public Color4? BackgroundColour { get; set; } + + public Color4? TextColour { get; set; } + private int? scorePosition; public int? ScorePosition @@ -331,19 +335,19 @@ namespace osu.Game.Screens.Play.HUD if (scorePosition == 1) { widthExtension = true; - panelColour = Color4Extensions.FromHex("7fcc33"); - textColour = Color4.White; + panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33"); + textColour = TextColour ?? Color4.White; } else if (trackedPlayer) { widthExtension = true; - panelColour = Color4Extensions.FromHex("ffd966"); - textColour = Color4Extensions.FromHex("2e576b"); + panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); + textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); } else { - panelColour = Color4Extensions.FromHex("3399cc"); - textColour = Color4.White; + panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc"); + textColour = TextColour ?? Color4.White; } this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic); diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs new file mode 100644 index 0000000000..c77b872786 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs @@ -0,0 +1,176 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class MatchScoreDisplay : CompositeDrawable + { + private const float bar_height = 18; + private const float font_size = 50; + + public BindableInt Team1Score = new BindableInt(); + public BindableInt Team2Score = new BindableInt(); + + protected MatchScoreCounter Score1Text; + protected MatchScoreCounter Score2Text; + + private Drawable score1Bar; + private Drawable score2Bar; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new Box + { + Name = "top bar red (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = colours.TeamColourRed, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight + }, + new Box + { + Name = "top bar blue (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = colours.TeamColourBlue, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft + }, + score1Bar = new Box + { + Name = "top bar red", + RelativeSizeAxes = Axes.X, + Height = bar_height, + Width = 0, + Colour = colours.TeamColourRed, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight + }, + score2Bar = new Box + { + Name = "top bar blue", + RelativeSizeAxes = Axes.X, + Height = bar_height, + Width = 0, + Colour = colours.TeamColourBlue, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = font_size + bar_height, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + Score1Text = new MatchScoreCounter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + Score2Text = new MatchScoreCounter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Team1Score.BindValueChanged(_ => updateScores()); + Team2Score.BindValueChanged(_ => updateScores()); + } + + private void updateScores() + { + Score1Text.Current.Value = Team1Score.Value; + Score2Text.Current.Value = Team2Score.Value; + + int comparison = Team1Score.Value.CompareTo(Team2Score.Value); + + if (comparison > 0) + { + Score1Text.Winning = true; + Score2Text.Winning = false; + } + else if (comparison < 0) + { + Score1Text.Winning = false; + Score2Text.Winning = true; + } + else + { + Score1Text.Winning = false; + Score2Text.Winning = false; + } + + var winningBar = Team1Score.Value > Team2Score.Value ? score1Bar : score2Bar; + var losingBar = Team1Score.Value <= Team2Score.Value ? score1Bar : score2Bar; + + var diff = Math.Max(Team1Score.Value, Team2Score.Value) - Math.Min(Team1Score.Value, Team2Score.Value); + + losingBar.ResizeWidthTo(0, 400, Easing.OutQuint); + winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + Score1Text.X = -Math.Max(5 + Score1Text.DrawWidth / 2, score1Bar.DrawWidth); + Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth); + } + + protected class MatchScoreCounter : ScoreCounter + { + private OsuSpriteText displayedSpriteText; + + public MatchScoreCounter() + { + Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; + } + + public bool Winning + { + set => updateFont(value); + } + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + displayedSpriteText = s; + displayedSpriteText.Spacing = new Vector2(-6); + updateFont(false); + }); + + private void updateFont(bool winning) + => 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); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 7ee77759b0..3f9258930e 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -7,12 +7,17 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; +using osu.Game.Users; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -21,6 +26,11 @@ namespace osu.Game.Screens.Play.HUD { protected readonly Dictionary UserScores = new Dictionary(); + public readonly SortedDictionary TeamScores = new SortedDictionary(); + + [Resolved] + private OsuColour colours { get; set; } + [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -31,21 +41,24 @@ namespace osu.Game.Screens.Play.HUD private UserLookupCache userLookupCache { get; set; } private readonly ScoreProcessor scoreProcessor; - private readonly IBindableList playingUsers; + private readonly MultiplayerRoomUser[] playingUsers; private Bindable scoringMode; + private readonly IBindableList playingUserIds = new BindableList(); + + private bool hasTeams => TeamScores.Count > 0; + /// /// Construct a new leaderboard. /// /// A score processor instance to handle score calculation for scores of users in the match. - /// IDs of all users in this match. - public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + /// IDs of all users in this match. + public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users) { // todo: this will eventually need to be created per user to support different mod combinations. this.scoreProcessor = scoreProcessor; - // todo: this will likely be passed in as User instances. - playingUsers = new BindableList(userIds); + playingUsers = users; } [BackgroundDependencyLoader] @@ -53,14 +66,17 @@ namespace osu.Game.Screens.Play.HUD { scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); - foreach (var userId in playingUsers) + foreach (var user in playingUsers) { - var trackedUser = CreateUserData(userId, scoreProcessor); + var trackedUser = CreateUserData(user, scoreProcessor); trackedUser.ScoringMode.BindTo(scoringMode); - UserScores[userId] = trackedUser; + UserScores[user.UserID] = trackedUser; + + if (trackedUser.Team is int team && !TeamScores.ContainsKey(team)) + TeamScores.Add(team, new BindableInt()); } - userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => + userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(users => Schedule(() => { foreach (var user in users.Result) { @@ -83,23 +99,50 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. - foreach (int userId in playingUsers) + foreach (var user in playingUsers) { - spectatorClient.WatchUser(userId); + spectatorClient.WatchUser(user.UserID); - if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId)) - usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); + if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID)) + usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID })); } // bind here is to support players leaving the match. // new players are not supported. - playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); - playingUsers.BindCollectionChanged(usersChanged); + playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); + playingUserIds.BindCollectionChanged(usersChanged); // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). spectatorClient.OnNewFrames += handleIncomingFrames; } + protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new TrackedUserData(user, scoreProcessor); + + protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) + { + var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked); + + if (UserScores[user.Id].Team is int team) + { + leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f); + leaderboardScore.TextColour = Color4.White; + } + + return leaderboardScore; + } + + private Color4 getTeamColour(int team) + { + switch (team) + { + case 0: + return colours.TeamColourRed; + + default: + return colours.TeamColourBlue; + } + } + private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -124,9 +167,26 @@ namespace osu.Game.Screens.Play.HUD trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); trackedData.UpdateScore(); + + updateTotals(); }); - protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); + private void updateTotals() + { + if (!hasTeams) + return; + + foreach (var scores in TeamScores.Values) scores.Value = 0; + + foreach (var u in UserScores.Values) + { + if (u.Team == null) + continue; + + if (TeamScores.TryGetValue(u.Team.Value, out var team)) + team.Value += (int)u.Score.Value; + } + } protected override void Dispose(bool isDisposing) { @@ -136,7 +196,7 @@ namespace osu.Game.Screens.Play.HUD { foreach (var user in playingUsers) { - spectatorClient.StopWatchingUser(user); + spectatorClient.StopWatchingUser(user.UserID); } spectatorClient.OnNewFrames -= handleIncomingFrames; @@ -145,7 +205,7 @@ namespace osu.Game.Screens.Play.HUD protected class TrackedUserData { - public readonly int UserId; + public readonly MultiplayerRoomUser User; public readonly ScoreProcessor ScoreProcessor; public readonly BindableDouble Score = new BindableDouble(); @@ -157,9 +217,11 @@ namespace osu.Game.Screens.Play.HUD public readonly List Frames = new List(); - public TrackedUserData(int userId, ScoreProcessor scoreProcessor) + public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID; + + public TrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) { - UserId = userId; + User = user; ScoreProcessor = scoreProcessor; ScoringMode.BindValueChanged(_ => UpdateScore()); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index b6eafe496f..f0a68ea078 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -24,9 +24,9 @@ namespace osu.Game.Screens.Spectate /// public abstract class SpectatorScreen : OsuScreen { - protected IReadOnlyList UserIds => userIds; + protected IReadOnlyList Users => users; - private readonly List userIds = new List(); + private readonly List users = new List(); [Resolved] private BeatmapManager beatmaps { get; set; } @@ -50,17 +50,17 @@ namespace osu.Game.Screens.Spectate /// /// Creates a new . /// - /// The users to spectate. - protected SpectatorScreen(params int[] userIds) + /// The users to spectate. + protected SpectatorScreen(params int[] users) { - this.userIds.AddRange(userIds); + this.users.AddRange(users); } protected override void LoadComplete() { base.LoadComplete(); - userLookupCache.GetUsersAsync(userIds.ToArray()).ContinueWith(users => Schedule(() => + userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(users => Schedule(() => { foreach (var u in users.Result) { @@ -207,7 +207,7 @@ namespace osu.Game.Screens.Spectate { onUserStateRemoved(userId); - userIds.Remove(userId); + users.Remove(userId); userMap.Remove(userId); spectatorClient.StopWatchingUser(userId); diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 42345b7266..f259784170 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -36,24 +36,29 @@ namespace osu.Game.Tests.Visual.Multiplayer { if (joinRoom) { - var room = new Room - { - Name = { Value = "test name" }, - Playlist = - { - new PlaylistItem - { - Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } - } - } - }; + var room = CreateRoom(); RoomManager.CreateRoom(room); SelectedRoom.Value = room; } }); + protected virtual Room CreateRoom() + { + return new Room + { + Name = { Value = "test name" }, + Playlist = + { + new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + } + } + }; + } + public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 59283084db..e6219fcb85 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index c8d3d150db..9904946363 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +