diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 3a1ff34fb9..c591b79b29 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects); + Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects, true); Vector2 delta = Vector2.Zero; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 9a5d3c3bc1..d5f3137769 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -81,12 +81,8 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); - originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 - ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) - : GeometryUtils.GetConvexHull(objectsInScale.Keys); + OriginalSurroundingQuad = GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); + originalConvexHull = GeometryUtils.GetConvexHull(objectsInScale.Keys); defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1; } @@ -312,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void moveSelectionInBounds() { - Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys); + Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys, true); Vector2 delta = Vector2.Zero; diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs similarity index 84% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs index 6fba5af070..93a33bdd95 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionGrid.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectGrid.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect; using osu.Game.Tests.Visual.OnlinePlay; @@ -17,11 +18,11 @@ using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneSelectionGrid : OnlinePlayTestScene + public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene { private MultiplayerPlaylistItem[] items = null!; - private SelectionGrid grid = null!; + private BeatmapSelectGrid grid = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; @@ -58,7 +59,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { base.SetUpSteps(); - AddStep("add grid", () => Child = grid = new SelectionGrid + AddStep("add grid", () => Child = grid = new BeatmapSelectGrid { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -82,6 +83,22 @@ namespace osu.Game.Tests.Visual.Matchmaking { // test scene is weird. }); + + AddStep("add selection 1", () => grid.ChildrenOfType().First().AddUser(new APIUser + { + Id = 6411631, + Username = "Maarvin", + }, isOwnUser: true)); + AddStep("add selection 2", () => grid.ChildrenOfType().Skip(5).First().AddUser(new APIUser + { + Id = 2, + Username = "peppy", + })); + AddStep("add selection 3", () => grid.ChildrenOfType().Skip(10).First().AddUser(new APIUser + { + Id = 1040328, + Username = "smoogipoo", + })); } [Test] @@ -154,7 +171,7 @@ namespace osu.Game.Tests.Visual.Matchmaking var (candidateItems, _) = pickRandomItems(count); grid.TransferCandidatePanelsToRollContainer(candidateItems); - grid.Delay(SelectionGrid.ARRANGE_DELAY) + grid.Delay(BeatmapSelectGrid.ARRANGE_DELAY) .Schedule(() => grid.ArrangeItemsForRollAnimation()); }); @@ -162,7 +179,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("display roll order", () => { - var panels = grid.ChildrenOfType().ToArray(); + var panels = grid.ChildrenOfType().ToArray(); for (int i = 0; i < panels.Length; i++) { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs similarity index 88% rename from osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs rename to osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs index 6745802b30..2de4d6d7ea 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneSelectionPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneBeatmapSelectPanel.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Matchmaking { - public partial class TestSceneSelectionPanel : MultiplayerTestScene + public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); @@ -20,9 +20,9 @@ namespace osu.Game.Tests.Visual.Matchmaking [Test] public void TestBeatmapPanel() { - SelectionPanel? panel = null; + BeatmapSelectPanel? panel = null; - AddStep("add panel", () => Child = panel = new SelectionPanel(new MultiplayerPlaylistItem()) + AddStep("add panel", () => Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs deleted file mode 100644 index ba7e27b753..0000000000 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneMatchmakingScreenStack.cs +++ /dev/null @@ -1,113 +0,0 @@ -// 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 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.Online.Multiplayer.MatchTypes.Matchmaking; -using osu.Game.Online.Rooms; -using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.Match; -using osu.Game.Tests.Visual.Multiplayer; - -namespace osu.Game.Tests.Visual.Matchmaking -{ - public partial class TestSceneMatchmakingScreenStack : MultiplayerTestScene - { - private const int user_count = 8; - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("join room", () => - { - var room = CreateDefaultRoom(MatchType.Matchmaking); - room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - })).ToArray(); - - JoinRoom(room); - }); - - WaitForJoined(); - - AddStep("add carousel", () => - { - Child = new ScreenMatchmaking.ScreenStack - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }; - }); - - AddStep("join users", () => - { - var users = Enumerable.Range(1, user_count).Select(i => new MultiplayerRoomUser(i) - { - User = new APIUser - { - Username = $"Player {i}" - } - }).ToArray(); - - foreach (var user in users) - MultiplayerClient.AddUser(user); - }); - } - - [Test] - public void TestChangeStage() - { - for (int round = 1; round <= 2; round++) - { - AddLabel($"Round {round}"); - - int r = round; - changeStage(MatchmakingStage.RoundWarmupTime, state => state.CurrentRound = r); - changeStage(MatchmakingStage.UserBeatmapSelect); - changeStage(MatchmakingStage.ServerBeatmapFinalised, state => - { - MultiplayerPlaylistItem[] beatmaps = Enumerable.Range(1, 8).Select(i => new MultiplayerPlaylistItem - { - ID = i, - BeatmapID = i, - StarRating = i / 10.0, - }).ToArray(); - - state.CandidateItems = beatmaps.Select(b => b.ID).ToArray(); - state.CandidateItem = beatmaps[0].ID; - }, waitTime: 35); - - changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload); - changeStage(MatchmakingStage.GameplayWarmupTime); - changeStage(MatchmakingStage.Gameplay); - changeStage(MatchmakingStage.ResultsDisplaying); - } - - changeStage(MatchmakingStage.Ended, state => - { - int localUserId = API.LocalUser.Value.OnlineID; - - state.Users[localUserId].Placement = 1; - state.Users[localUserId].Rounds[1].Placement = 1; - state.Users[localUserId].Rounds[1].TotalScore = 1; - state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1; - }); - } - - private void changeStage(MatchmakingStage stage, Action? prepare = null, int waitTime = 5) - { - AddStep($"stage: {stage}", () => MultiplayerClient.MatchmakingChangeStage(stage, prepare).WaitSafely()); - AddWaitStep("wait", waitTime); - } - } -} diff --git a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs index a7c14cfd94..1ef5e2edc1 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestScenePlayerPanel.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestScenePlayerPanel : MultiplayerTestScene { - private MatchmakingUserPanel panel = null!; + private PlayerPanel panel = null!; public override void SetUpSteps() { @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Matchmaking AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking))); WaitForJoined(); - AddStep("add panel", () => Child = panel = new MatchmakingUserPanel(new MultiplayerRoomUser(1) + AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1) { User = new APIUser { diff --git a/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs index 28d45d5f38..c8b1d55028 100644 --- a/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs +++ b/osu.Game.Tests/Visual/Matchmaking/TestSceneUserPanelOverlay.cs @@ -14,13 +14,14 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.Match; using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Matchmaking { public partial class TestSceneUserPanelOverlay : MultiplayerTestScene { - private UserPanelOverlay list = null!; + private PlayerPanelOverlay list = null!; public override void SetUpSteps() { @@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Matchmaking Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Size = new Vector2(0.8f), - Child = list = new UserPanelOverlay() + Child = list = new PlayerPanelOverlay() }); } @@ -117,10 +118,10 @@ namespace osu.Game.Tests.Visual.Matchmaking }); }); - AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddUntilStep("two panels displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(2)); AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 })); - AddUntilStep("one panel displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddUntilStep("one panel displayed", () => this.ChildrenOfType().Count(), () => Is.EqualTo(1)); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index 3b9a07437a..f678ec372a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Cursor; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -86,6 +87,64 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } } + [Test] + public void TestRanks() + { + for (int i = -1; i <= 7; i++) + { + ScoreRank rank = (ScoreRank)i; + + AddStep($"display rank {rank}", () => + { + ContentContainer.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + }, + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new[] + { + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)) + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), + KeyboardSelected = { Value = true }, + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), + Expanded = { Value = true }, + }, + new PanelGroupRankDisplay + { + Item = new CarouselItem(new RankDisplayGroupDefinition(rank)), + Expanded = { Value = true }, + KeyboardSelected = { Value = true }, + }, + }, + } + } + }; + }); + } + } + protected override Drawable CreateContent() { return new OsuContextMenuContainer diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index 54f8d656fe..8cd0ac965a 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards public const float TRANSITION_DURATION = 340; public const float CORNER_RADIUS = 8; - protected const float WIDTH = 345; + public const float WIDTH = 345; public IBindable Expanded { get; } @@ -77,25 +77,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards containingInputManager = GetContainingInputManager(); - Action = () => - { - if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) - { - switch (DownloadTracker.State.Value) - { - case DownloadState.NotDownloaded: - if (!BeatmapSet.Availability.DownloadDisabled) - beatmaps?.Download(BeatmapSet, preferNoVideo.Value); - break; + if (Action == null) + throw new InvalidOperationException($"An action should be assigned to this {nameof(BeatmapCard)}. To use the default, assign {nameof(DefaultAction)}."); + } - case DownloadState.LocallyAvailable: - game?.PresentBeatmap(BeatmapSet); - break; - } + protected void DefaultAction() + { + if (containingInputManager?.CurrentState.Keyboard.ShiftPressed == true) + { + switch (DownloadTracker.State.Value) + { + case DownloadState.NotDownloaded: + if (!BeatmapSet.Availability.DownloadDisabled) beatmaps?.Download(BeatmapSet, preferNoVideo.Value); + break; + + case DownloadState.LocallyAvailable: + game?.PresentBeatmap(BeatmapSet); + break; } - else - beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); - }; + } + else + beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs index deb56bb281..a57f3e7ce7 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs @@ -21,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo) + public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false) { InternalChildren = new Drawable[] { @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards { RelativeSizeAxes = Axes.Both, }, - cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), 500, 500) + cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000) { RelativeSizeAxes = Axes.Both, Colour = Colour4.Transparent diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs index 9428984115..75fdc7d7e8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs @@ -46,6 +46,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, allowExpansion) { content = new BeatmapCardContent(height); + + Action = DefaultAction; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs index 62108fe6f5..c23a03aabe 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNano.cs @@ -54,6 +54,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, false) { content = new BeatmapCardContent(height); + + Action = DefaultAction; } [BackgroundDependencyLoader] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs index 505a6fcdae..ac9ee94f56 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs @@ -47,6 +47,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards : base(beatmapSet, allowExpansion) { content = new BeatmapCardContent(HEIGHT); + + Action = DefaultAction; } [BackgroundDependencyLoader] diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs index 1f6f638618..4a7054588e 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -35,11 +35,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo) + public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo, bool keepLoaded = false) { InternalChildren = new Drawable[] { - new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List, keepLoaded ? 0 : 500, keepLoaded ? double.MaxValue : 1000) { RelativeSizeAxes = Axes.Both, OnlineInfo = onlineInfo diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs index d2c077d010..3d732b6683 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs @@ -16,10 +16,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private readonly Bindable state = new Bindable(); private readonly APIBeatmapSet beatmapSet; + private readonly bool allowNavigationToBeatmap; - public GoToBeatmapButton(APIBeatmapSet beatmapSet) + public GoToBeatmapButton(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap) { this.beatmapSet = beatmapSet; + this.allowNavigationToBeatmap = allowNavigationToBeatmap; } [BackgroundDependencyLoader(true)] @@ -27,7 +29,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons { Action = () => game?.PresentBeatmap(beatmapSet); Icon.Icon = FontAwesome.Solid.AngleDoubleRight; - TooltipText = "Go to beatmap"; } protected override void LoadComplete() @@ -40,8 +41,31 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void updateState() { - Enabled.Value = state.Value == DownloadState.LocallyAvailable; - this.FadeTo(Enabled.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + bool available = state.Value == DownloadState.LocallyAvailable; + Enabled.Value = allowNavigationToBeatmap && available; + + float alpha; + + if (available && allowNavigationToBeatmap) + { + TooltipText = "Go to beatmap"; + Enabled.Value = true; + alpha = 1f; + } + else if (available) + { + TooltipText = string.Empty; + Enabled.Value = false; + alpha = 0.3f; + } + else + { + TooltipText = string.Empty; + Enabled.Value = false; + alpha = 0; + } + + this.FadeTo(alpha, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index 56d405ce3c..8262e787d8 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -30,7 +30,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards set { buttonsExpandedWidth = value; - buttonArea.Width = value; if (IsLoaded) updateState(); } @@ -67,7 +66,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public CollapsibleButtonContainer(APIBeatmapSet beatmapSet) + public CollapsibleButtonContainer(APIBeatmapSet beatmapSet, bool allowNavigationToBeatmap = true, bool keepBackgroundLoaded = false) { downloadTracker = new BeatmapDownloadTracker(beatmapSet); @@ -116,14 +115,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards RelativeSizeAxes = Axes.Both, Height = 0.5f, }, - new GoToBeatmapButton(beatmapSet) - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - State = { BindTarget = downloadTracker.State }, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - } } } }, @@ -135,7 +126,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Masking = true, Children = new Drawable[] { - new BeatmapCardContentBackground(beatmapSet) + new BeatmapCardContentBackground(beatmapSet, keepBackgroundLoaded) { RelativeSizeAxes = Axes.Both, Dimmed = { BindTarget = ShowDetails } @@ -152,6 +143,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards } } }; + + buttons.Add(new GoToBeatmapButton(beatmapSet, allowNavigationToBeatmap) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + State = { BindTarget = downloadTracker.State }, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + }); } protected override void LoadComplete() @@ -165,6 +165,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards private void updateState() { + buttonArea.Width = buttonsExpandedWidth; + float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth; float mainAreaWidth = Width - buttonAreaWidth; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs new file mode 100644 index 0000000000..8fbf8491d6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapCardMatchmaking.cs @@ -0,0 +1,354 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Beatmaps.Drawables.Cards.Statistics; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class BeatmapCardMatchmaking : BeatmapCard + { + private readonly APIBeatmap beatmap; + + protected override Drawable IdleContent => idleBottomContent; + protected override Drawable DownloadInProgressContent => downloadProgressBar; + + public const float HEIGHT = 80; + + [Cached] + private readonly BeatmapCardContent content; + + private BeatmapCardThumbnail thumbnail = null!; + private CollapsibleButtonContainer buttonContainer = null!; + + private FillFlowContainer statisticsContainer = null!; + + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardMatchmaking(APIBeatmap beatmap) + : base(beatmap.BeatmapSet!, false) + { + this.beatmap = beatmap; + content = new BeatmapCardContent(HEIGHT); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Width = WIDTH; + Height = HEIGHT; + + FillFlowContainer leftIconArea = null!; + FillFlowContainer titleBadgeArea = null!; + GridContainer artistContainer = null!; + + Child = content.With(c => + { + c.MainContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true) + { + Name = @"Left (icon) area", + Size = new Vector2(HEIGHT), + Padding = new MarginPadding { Right = CORNER_RADIUS }, + Child = leftIconArea = new FillFlowContainer + { + Margin = new MarginPadding(4), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) + } + }, + buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true) + { + X = HEIGHT - CORNER_RADIUS, + Width = WIDTH - HEIGHT + CORNER_RADIUS, + FavouriteState = { BindTarget = FavouriteState }, + ButtonsCollapsedWidth = 0, + ButtonsExpandedWidth = 24, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new TruncatingSpriteText + { + Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), + Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + titleBadgeArea = new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + } + } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new TruncatingSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 1 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(BeatmapSet.Author); + }), + } + }, + new Container + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + idleBottomContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2), + AlwaysPresent = true, + Children = new Drawable[] + { + statisticsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(8, 0), + Alpha = 0, + AlwaysPresent = true, + ChildrenEnumerable = createStatistics() + }, + new Container + { + Masking = true, + CornerRadius = CORNER_RADIUS, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Padding = new MarginPadding(2), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + Children = new Drawable[] + { + new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(0.875f), + }, + new TruncatingSpriteText + { + Text = beatmap.DifficultyName, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + }, + } + }, + } + }, + downloadProgressBar = new BeatmapCardDownloadProgressBar + { + RelativeSizeAxes = Axes.X, + Height = 5, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { BindTarget = DownloadTracker.State }, + Progress = { BindTarget = DownloadTracker.Progress } + } + } + } + } + } + } + }; + c.Expanded.BindTarget = Expanded; + }); + + if (BeatmapSet.HasVideo) + leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) }); + + if (BeatmapSet.HasStoryboard) + leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) }); + + if (BeatmapSet.FeaturedInSpotlight) + { + titleBadgeArea.Add(new SpotlightBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (BeatmapSet.HasExplicitContent) + { + titleBadgeArea.Add(new ExplicitContentBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }); + } + + if (BeatmapSet.TrackId != null) + { + artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 4 } + }; + } + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + private IEnumerable createStatistics() + { + var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet); + if (hypesStatistic != null) + yield return hypesStatistic; + + var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet); + if (nominationsStatistic != null) + yield return nominationsStatistic; + + yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState }; + yield return new PlayCountStatistic(BeatmapSet); + + var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet); + if (dateStatistic != null) + yield return dateStatistic; + } + + protected override void UpdateState() + { + base.UpdateState(); + + bool showDetails = IsHovered; + + buttonContainer.ShowDetails.Value = showDetails; + thumbnail.Dimmed.Value = showDetails; + + statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); + } + + public override MenuItem[] ContextMenuItems + { + get + { + var items = base.ContextMenuItems.ToList(); + + foreach (var button in buttonContainer.Buttons) + { + if (button.Enabled.Value) + items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick())); + } + + return items.ToArray(); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs index bd75514b30..4d19890993 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectGrid.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { - public partial class SelectionGrid : CompositeDrawable + public partial class BeatmapSelectGrid : CompositeDrawable { public const double ARRANGE_DELAY = 200; @@ -30,17 +30,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private const double arrange_duration = 1000; private const double roll_duration = 4000; private const double present_beatmap_delay = 1200; - private const float panel_spacing = 20; + private const float panel_spacing = 4; public event Action? ItemSelected; [Resolved] private IAPIProvider api { get; set; } = null!; - private readonly Dictionary panelLookup = new Dictionary(); + private readonly Dictionary panelLookup = new Dictionary(); private readonly PanelGridContainer panelGridContainer; - private readonly Container rollContainer; + private readonly Container rollContainer; private readonly OsuScrollContainer scroll; private bool allowSelection = true; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect private Sample? swooshSample; private double? lastSamplePlayback; - public SelectionGrid() + public BeatmapSelectGrid() { InternalChildren = new Drawable[] { @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect Spacing = new Vector2(panel_spacing) }, }, - rollContainer = new Container + rollContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -108,9 +108,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public void AddItem(MultiplayerPlaylistItem item) { - var panel = panelLookup[item.ID] = new SelectionPanel(item) + var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item) { - Size = new Vector2(300, 70), AllowSelection = allowSelection, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -176,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect var rng = new Random(); - var remainingPanels = new List(); + var remainingPanels = new List(); foreach (var panel in panelGridContainer.Children.ToArray()) { @@ -216,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { var panel = rollContainer.Children[i]; - var position = positions[i] * (SelectionPanel.SIZE + new Vector2(panel_spacing)); + var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing)); panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f)); @@ -285,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex) numSteps++; - SelectionPanel? lastPanel = null; + BeatmapSelectPanel? lastPanel = null; for (int i = 0; i < numSteps; i++) { @@ -330,7 +329,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { rollContainer.ChangeChildDepth(panel, float.MinValue); - panel.ShowBorder(); + panel.ShowChosenBorder(); panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); @@ -346,7 +345,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect PresentRolledBeatmap(finalItem); } - private partial class PanelGridContainer : FillFlowContainer + private partial class PanelGridContainer : FillFlowContainer { public bool LayoutDisabled; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs new file mode 100644 index 0000000000..3266e39905 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/BeatmapSelectPanel.cs @@ -0,0 +1,341 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect +{ + public partial class BeatmapSelectPanel : Container + { + public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT); + + public bool AllowSelection { get; set; } + + public readonly MultiplayerPlaylistItem Item; + + public Action? Action { private get; init; } + + private const float border_width = 3; + + private Container scaleContainer = null!; + private AvatarOverlay selectionOverlay = null!; + private Drawable lighting = null!; + + private Container border = null!; + private Container mainContent = null!; + + public override bool PropagatePositionalInputSubTree => AllowSelection; + + public BeatmapSelectPanel(MultiplayerPlaylistItem item) + { + Item = item; + Size = SIZE; + } + + [BackgroundDependencyLoader] + private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider) + { + InternalChild = scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new[] + { + mainContent = new Container + { + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + lighting = new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + selectionOverlay = new AvatarOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + } + }, + border = new Container + { + Alpha = 0, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + CornerExponent = 10, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + BorderThickness = border_width, + BorderColour = colourProvider.Light1, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 40, + Roundness = 300, + Colour = colourProvider.Light3.Opacity(0.1f), + }, + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + } + }, + } + }; + lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => + { + var beatmap = b.GetResultSafely()!; + beatmap.StarRating = Item.StarRating; + + mainContent.Add(new BeatmapCardMatchmaking(beatmap) + { + Depth = float.MaxValue, + Action = () => Action?.Invoke(Item), + }); + })); + } + + public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); + public bool RemoveUser(APIUser user) => selectionOverlay.RemoveUser(user.Id); + + protected override bool OnHover(HoverEvent e) + { + lighting.FadeTo(0.2f, 50) + .Then() + .FadeTo(0.1f, 300); + + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + lighting.FadeOut(200); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); + return true; + } + + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + + if (e.Button == MouseButton.Left) + { + scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); + } + } + + protected override bool OnClick(ClickEvent e) + { + lighting.FadeTo(0.5f, 50) + .Then() + .FadeTo(0.1f, 400); + + // pass through to let the beatmap card handle actual click. + return false; + } + + public void ShowChosenBorder() + { + border.FadeTo(1, 1000, Easing.OutQuint); + } + + public void ShowBorder() + { + border.FadeTo(1, 80, Easing.OutQuint) + .Then() + .FadeTo(0.7f, 800, Easing.OutQuint); + } + + public void HideBorder() + { + border.FadeOut(500, Easing.OutQuint); + } + + public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) + { + scaleContainer + .FadeOut() + .MoveToY(distance) + .Delay(delay) + .FadeIn(duration / 2) + .MoveToY(0, duration, Easing.OutExpo); + } + + public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) + { + AllowSelection = false; + + scaleContainer.Delay(delay) + .ScaleTo(0, duration, easing) + .FadeOut(duration); + + this.Delay(delay + duration).FadeOut().Expire(); + } + + private partial class AvatarOverlay : CompositeDrawable + { + private readonly Container avatars; + + private Sample? userAddedSample; + private double? lastSamplePlayback; + + public AvatarOverlay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = avatars = new Container + { + AutoSizeAxes = Axes.X, + Height = SelectionAvatar.AVATAR_SIZE, + }; + + Padding = new MarginPadding(5); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); + } + + public bool AddUser(APIUser user, bool isOwnUser) + { + if (avatars.Any(a => a.User.Id == user.Id)) + return false; + + var avatar = new SelectionAvatar(user, isOwnUser); + + avatars.Add(avatar); + + if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) + { + userAddedSample?.Play(); + lastSamplePlayback = Time.Current; + } + + updateAvatarLayout(); + + avatar.FinishTransforms(); + + return true; + } + + public bool RemoveUser(int id) + { + if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar) + return false; + + avatar.PopOutAndExpire(); + avatars.ChangeChildDepth(avatar, float.MaxValue); + + updateAvatarLayout(); + + return true; + } + + private void updateAvatarLayout() + { + const double stagger = 30; + const float spacing = 4; + + double delay = 0; + float x = 0; + + for (int i = avatars.Count - 1; i >= 0; i--) + { + var avatar = avatars[i]; + + if (avatar.Expired) + continue; + + avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); + + x -= avatar.LayoutSize.X + spacing; + + delay += stagger; + } + } + + public partial class SelectionAvatar : CompositeDrawable + { + public const float AVATAR_SIZE = 30; + + public APIUser User { get; } + + public bool Expired { get; private set; } + + private readonly MatchmakingAvatar avatar; + + public SelectionAvatar(APIUser user, bool isOwnUser) + { + User = user; + Size = new Vector2(AVATAR_SIZE); + + InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + avatar.ScaleTo(0) + .ScaleTo(1, 500, Easing.OutElasticHalf) + .FadeIn(200); + } + + public void PopOutAndExpire() + { + avatar.ScaleTo(0, 400, Easing.OutExpo); + + this.FadeOut(100).Expire(); + Expired = true; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs deleted file mode 100644 index 1a51ddac64..0000000000 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SelectionPanel.cs +++ /dev/null @@ -1,502 +0,0 @@ -// 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 osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Database; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Rooms; -using osu.Game.Overlays; -using osuTK; -using osuTK.Input; - -namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect -{ - public partial class SelectionPanel : Container - { - public static readonly Vector2 SIZE = new Vector2(300, 70); - - private const float corner_radius = 6; - private const float border_width = 3; - - public readonly MultiplayerPlaylistItem Item; - - private readonly Container scaleContainer; - private readonly BeatmapPanel beatmapPanel; - private readonly AvatarOverlay selectionOverlay; - private readonly Container border; - private readonly Box flash; - - public bool AllowSelection; - - public Action? Action; - - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - - public override bool PropagatePositionalInputSubTree => AllowSelection; - - public SelectionPanel(MultiplayerPlaylistItem item) - { - Item = item; - Size = SIZE; - - InternalChildren = new Drawable[] - { - scaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-border_width), - Child = border = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = corner_radius + border_width, - Alpha = 0, - Child = new Box { RelativeSizeAxes = Axes.Both }, - } - }, - beatmapPanel = new BeatmapPanel - { - RelativeSizeAxes = Axes.Both, - OverlayLayer = - { - Children = new[] - { - flash = new Box - { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - } - } - }, - selectionOverlay = new AvatarOverlay - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10 }, - Origin = Anchor.CentreLeft, - }, - } - }, - new HoverClickSounds(), - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() => - { - var beatmap = b.GetResultSafely()!; - - beatmap.StarRating = Item.StarRating; - - beatmapPanel.Beatmap = beatmap; - })); - } - - public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser); - - public bool RemoveUser(int userId) => selectionOverlay.RemoveUser(userId); - - public bool RemoveUser(APIUser user) => RemoveUser(user.Id); - - protected override bool OnHover(HoverEvent e) - { - flash.FadeTo(0.2f, 50) - .Then() - .FadeTo(0.1f, 300); - - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - - flash.FadeOut(200); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button == MouseButton.Left) - { - scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo); - return true; - } - - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - base.OnMouseUp(e); - - if (e.Button == MouseButton.Left) - { - scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf); - } - } - - protected override bool OnClick(ClickEvent e) - { - Action?.Invoke(Item); - - flash.FadeTo(0.5f, 50) - .Then() - .FadeTo(0.1f, 400); - - return true; - } - - public void ShowBorder() => border.Show(); - - public void HideBorder() => border.Hide(); - - public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200) - { - scaleContainer - .FadeOut() - .MoveToY(distance) - .Delay(delay) - .FadeIn(duration / 2) - .MoveToY(0, duration, Easing.OutExpo); - } - - public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic) - { - AllowSelection = false; - - scaleContainer.Delay(delay) - .ScaleTo(0, duration, easing) - .FadeOut(duration); - - this.Delay(delay + duration).FadeOut().Expire(); - } - - // TODO: combine following two classes with above implementation for simplicity? - private partial class BeatmapPanel : CompositeDrawable - { - public readonly Container OverlayLayer = new Container { RelativeSizeAxes = Axes.Both }; - - public APIBeatmap? Beatmap - { - get => beatmap; - set - { - if (beatmap?.OnlineID == value?.OnlineID) - return; - - beatmap = value; - - if (IsLoaded) - updateContent(); - } - } - - private APIBeatmap? beatmap; - - private Container content = null!; - private UpdateableOnlineBeatmapSetCover cover = null!; - - public BeatmapPanel(APIBeatmap? beatmap = null) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Masking = true; - CornerRadius = 6; - - InternalChildren = new Drawable[] - { - cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.Card, timeBeforeLoad: 0, timeBeforeUnload: 10000) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal( - colourProvider.Background4.Opacity(0.7f), - colourProvider.Background4.Opacity(0.4f) - ) - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - OverlayLayer, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateContent(); - FinishTransforms(true); - } - - private void updateContent() - { - foreach (var child in content.Children) - child.FadeOut(300).Expire(); - - cover.OnlineInfo = beatmap?.BeatmapSet; - - if (beatmap != null) - { - var panelContent = new BeatmapPanelContent(beatmap) - { - RelativeSizeAxes = Axes.Both, - }; - - content.Add(panelContent); - - panelContent.FadeInFromZero(300); - } - } - - private partial class BeatmapPanelContent : CompositeDrawable - { - private readonly APIBeatmap beatmap; - - public BeatmapPanelContent(APIBeatmap beatmap) - { - this.beatmap = beatmap; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Horizontal = 12 }, - Children = new Drawable[] - { - new TruncatingSpriteText - { - Text = new RomanisableString(beatmap.Metadata.TitleUnicode, beatmap.Metadata.TitleUnicode), - Font = OsuFont.Default.With(size: 19, weight: FontWeight.SemiBold), - RelativeSizeAxes = Axes.X, - }, - new TextFlowContainer(s => - { - s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold); - }).With(d => - { - d.RelativeSizeAxes = Axes.X; - d.AutoSizeAxes = Axes.Y; - d.AddText("by "); - d.AddText(new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); - }), - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 6 }, - Spacing = new Vector2(4), - Children = new Drawable[] - { - new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - new TruncatingSpriteText - { - Text = beatmap.DifficultyName, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - }, - }, - }; - } - } - } - - private partial class AvatarOverlay : CompositeDrawable - { - private readonly Dictionary avatars = new Dictionary(); - - private readonly Container avatarContainer; - - private Sample? userAddedSample; - private double? lastSamplePlayback; - - public new Axes AutoSizeAxes - { - get => base.AutoSizeAxes; - set => base.AutoSizeAxes = value; - } - - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - - public AvatarOverlay() - { - InternalChild = avatarContainer = new Container(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - avatarContainer.AutoSizeAxes = AutoSizeAxes; - avatarContainer.RelativeSizeAxes = RelativeSizeAxes; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready"); - } - - public bool AddUser(APIUser user, bool isOwnUser) - { - if (avatars.ContainsKey(user.Id)) - return false; - - var avatar = new SelectionAvatar(user, isOwnUser) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }; - - avatarContainer.Add(avatars[user.Id] = avatar); - - if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) - { - userAddedSample?.Play(); - lastSamplePlayback = Time.Current; - } - - updateLayout(); - - avatar.FinishTransforms(); - - return true; - } - - public bool RemoveUser(int id) - { - if (!avatars.Remove(id, out var avatar)) - return false; - - avatar.PopOutAndExpire(); - avatarContainer.ChangeChildDepth(avatar, float.MaxValue); - - updateLayout(); - - return true; - } - - private void updateLayout() - { - const double stagger = 30; - const float spacing = 4; - - double delay = 0; - float x = 0; - - for (int i = avatarContainer.Count - 1; i >= 0; i--) - { - var avatar = avatarContainer[i]; - - if (avatar.Expired) - continue; - - avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter); - - x -= avatar.LayoutSize.X + spacing; - - delay += stagger; - } - } - - public partial class SelectionAvatar : CompositeDrawable - { - public bool Expired { get; private set; } - - private readonly Container content; - - public SelectionAvatar(APIUser user, bool isOwnUser) - { - Size = new Vector2(30); - - InternalChildren = new Drawable[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Child = new MatchmakingAvatar(user, isOwnUser) - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - content.ScaleTo(0) - .ScaleTo(1, 500, Easing.OutElasticHalf) - .FadeIn(200); - } - - public void PopOutAndExpire() - { - content.ScaleTo(0, 400, Easing.OutExpo); - - this.FadeOut(100).Expire(); - Expired = true; - } - } - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs index cf86deeb3e..4b34125517 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/BeatmapSelect/SubScreenBeatmapSelect.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Split; public override Drawable PlayersDisplayArea { get; } - private readonly SelectionGrid selectionGrid; + private readonly BeatmapSelectGrid beatmapSelectGrid; [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 200 }, - Child = selectionGrid = new SelectionGrid + Child = beatmapSelectGrid = new BeatmapSelectGrid { RelativeSizeAxes = Axes.Both, }, @@ -37,8 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5 }, - Child = PlayersDisplayArea = Empty().With(d => + Child = PlayersDisplayArea = new Container().With(d => { d.RelativeSizeAxes = Axes.Both; }) @@ -55,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect foreach (var item in client.Room!.Playlist) onItemAdded(item); - selectionGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); + beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID); client.MatchmakingItemSelected += onItemSelected; client.MatchmakingItemDeselected += onItemDeselected; @@ -66,22 +65,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect if (item.Expired) return; - selectionGrid.AddItem(item); + beatmapSelectGrid.AddItem(item); }); private void onItemSelected(int userId, long itemId) { var user = client.Room!.Users.First(it => it.UserID == userId).User!; - selectionGrid.SetUserSelection(user, itemId, true); + beatmapSelectGrid.SetUserSelection(user, itemId, true); } private void onItemDeselected(int userId, long itemId) { var user = client.Room!.Users.First(it => it.UserID == userId).User!; - selectionGrid.SetUserSelection(user, itemId, false); + beatmapSelectGrid.SetUserSelection(user, itemId, false); } - public void RollFinalBeatmap(long[] candidateItems, long finalItem) => selectionGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); + public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs index faf32c6604..53db2114c7 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingAvatar.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { /// /// A circular player avatar used in matchmaking displays. - /// Is part of a but can also be used in isolation for a more ambient/decorative user display. + /// Is part of a but can also be used in isolation for a more ambient/decorative user display. /// public partial class MatchmakingAvatar : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs index ce4b471df4..f18a33c830 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/MatchmakingUserPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanel.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match /// A panel used throughout matchmaking to represent a user, including local information like their /// rank and high level statistics in the matchmaking system. /// - public partial class MatchmakingUserPanel : UserPanel + public partial class PlayerPanel : UserPanel { public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100); public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200); @@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private bool horizontal; - public MatchmakingUserPanel(MultiplayerRoomUser user) + public PlayerPanel(MultiplayerRoomUser user) : base(user.User!) { RoomUser = user; @@ -66,6 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { Masking = true; CornerRadius = 10; + CornerExponent = 10; Add(scaleContainer = new Container { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs similarity index 93% rename from osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs rename to osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs index a938dadae0..ba6021469f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/UserPanelOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/PlayerPanelOverlay.cs @@ -18,12 +18,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match /// A component which maintains the layout of the players in a matchmaking room. /// Can be controlled to display the panels in a certain location and in multiple styles. /// - public partial class UserPanelOverlay : CompositeDrawable + public partial class PlayerPanelOverlay : CompositeDrawable { [Resolved] private MultiplayerClient client { get; set; } = null!; - private Container panels = null!; + private Container panels = null!; private PlayerPanelCellContainer gridLayout = null!; private PlayerPanelCellContainer splitLayoutLeft = null!; private PlayerPanelCellContainer splitLayoutRight = null!; @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match gridLayout = new PlayerPanelCellContainer { RelativeSizeAxes = Axes.Both, - Spacing = new Vector2(20, 5), + Spacing = new Vector2(20), }, splitLayoutLeft = new PlayerPanelCellContainer { @@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Vertical, - Spacing = new Vector2(20, 5), + Spacing = new Vector2(5), }, splitLayoutRight = new PlayerPanelCellContainer { @@ -58,9 +58,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Vertical, - Spacing = new Vector2(20, 5), + Spacing = new Vector2(5), }, - panels = new Container + panels = new Container { RelativeSizeAxes = Axes.Both } @@ -110,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private void onUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => { - panels.Add(new MatchmakingUserPanel(user) + panels.Add(new PlayerPanel(user) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -215,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match [Resolved] private MultiplayerClient client { get; set; } = null!; - public void AcquirePanels(MatchmakingUserPanel[] panels) + public void AcquirePanels(PlayerPanel[] panels) { while (Count < panels.Length) { @@ -259,10 +259,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private partial class PlayerPanelCell : Drawable { - private MatchmakingUserPanel? panel; + private PlayerPanel? panel; private bool isAnimating; - public void AcquirePanel(MatchmakingUserPanel panel) + public void AcquirePanel(PlayerPanel panel) { this.panel = panel; isAnimating = true; @@ -280,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (panel?.Parent == null) return; - Size = panel.Horizontal ? MatchmakingUserPanel.SIZE_HORIZONTAL : MatchmakingUserPanel.SIZE_VERTICAL; + Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL; Size *= panel.Scale; var targetPos = getFinalPosition(); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs index 29a1acb2b8..c1f436e0c9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/ScreenMatchmaking.ScreenStack.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private MultiplayerClient client { get; set; } = null!; private Framework.Screens.ScreenStack screenStack = null!; - private UserPanelOverlay playersList = null!; + private PlayerPanelOverlay playersList = null!; [BackgroundDependencyLoader] private void load() @@ -45,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match screenStack = new Framework.Screens.ScreenStack(), } }, - playersList = new UserPanelOverlay + playersList = new PlayerPanelOverlay { DisplayArea = this }, diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs index f97bf9fe68..301cac1437 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.StageSegment.cs @@ -42,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match private Sample? countdownTickSample; private double? lastSamplePlayback; + private Container mainContent = null!; + public bool Active { get; private set; } public float Progress => progressBar.Width; @@ -49,10 +51,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match public StageSegment(int? round, MatchmakingStage stage, LocalisableString displayText) { Round = round; + this.stage = stage; this.displayText = displayText; AutoSizeAxes = Axes.Both; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; } [BackgroundDependencyLoader] @@ -74,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match Icon = FontAwesome.Solid.ArrowRight, Margin = new MarginPadding { Horizontal = 10 } }, - new Container + mainContent = new Container { Masking = true, CornerRadius = 5, @@ -178,6 +184,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match if (wasActive) progressBar.Width = 1; + mainContent.ScaleTo(Active ? 1.3f : 1, 500, Easing.OutQuint); + bool isPreparing = (stage == MatchmakingStage.RoundWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsJoin) || (stage == MatchmakingStage.GameplayWarmupTime && roomState.Stage == MatchmakingStage.WaitingForClientsBeatmapDownload) || diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs index db302163a5..e383df71c9 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Match/StageDisplay.cs @@ -57,17 +57,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match { scroll = new StageScrollContainer { - ScrollbarOverlapsContent = false, ScrollbarVisible = false, ClampExtension = 0, RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 36, + Height = HEIGHT, Child = flow = new FillFlowContainer { Padding = new MarginPadding { Horizontal = 2000 }, AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Direction = FillDirection.Horizontal, }, }, @@ -226,8 +225,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match round = value.Value; - this.ScaleTo(6, 500, Easing.OutQuart) - .MoveToY(-300, 500, Easing.OutQuart) + this.ScaleTo(6, 1000, Easing.OutPow10) + .MoveToY(-300, 1000, Easing.OutPow10) .Then() .MoveToY(0, 500, Easing.InQuart) .ScaleTo(1, 500, Easing.InQuart); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 52d5989c8f..d2b18b2f33 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -788,9 +789,11 @@ namespace osu.Game.Screens.SelectV2 private readonly DrawablePool setPanelPool = new DrawablePool(100); private readonly DrawablePool groupPanelPool = new DrawablePool(100); private readonly DrawablePool starsGroupPanelPool = new DrawablePool(11); + private readonly DrawablePool ranksGroupPanelPool = new DrawablePool(9); private void setupPools() { + AddInternal(ranksGroupPanelPool); AddInternal(starsGroupPanelPool); AddInternal(groupPanelPool); AddInternal(beatmapPanelPool); @@ -829,6 +832,9 @@ namespace osu.Game.Screens.SelectV2 case StarDifficultyGroupDefinition: return starsGroupPanelPool.Get(); + case RankDisplayGroupDefinition: + return ranksGroupPanelPool.Get(); + case GroupDefinition: return groupPanelPool.Get(); @@ -1085,6 +1091,11 @@ namespace osu.Game.Screens.SelectV2 /// public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); + /// + /// Defines a grouping header for a set of carousel items grouped by achieved rank. + /// + public record RankDisplayGroupDefinition(ScoreRank Rank) : GroupDefinition(-(int)Rank, Rank.GetDescription()); + /// /// Used to represent a portion of a under a . /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 69f5596578..37ea7b7497 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -433,7 +433,7 @@ namespace osu.Game.Screens.SelectV2 private IEnumerable defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary topRankMapping) { if (topRankMapping.TryGetValue(beatmap.ID, out var rank)) - return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield(); + return new RankDisplayGroupDefinition(rank).Yield(); return new GroupDefinition(int.MaxValue, "Unplayed").Yield(); } diff --git a/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs new file mode 100644 index 0000000000..95e8b5f43b --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroupRankDisplay.cs @@ -0,0 +1,226 @@ +// 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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Scoring; +using osuTK; +using osuTK.Graphics; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroupRankDisplay : Panel + { + public const float HEIGHT = PanelGroup.HEIGHT; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Drawable iconContainer = null!; + private Box backgroundBorder = null!; + private Box contentBackground = null!; + private OsuSpriteText starRatingText = null!; + private CircularContainer countPill = null!; + private OsuSpriteText countText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = PanelGroup.HEIGHT; + + Icon = iconContainer = new Container + { + AlwaysPresent = true, + RelativeSizeAxes = Axes.Y, + Alpha = 0f, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + }, + }; + + Background = backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Highlight1, + }; + + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5) + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + UseFullGlyphHeight = false, + Font = OsuFont.Style.Heading2, + } + } + }, + countPill = new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + countText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + private Color4 rankColour; + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var group = (RankDisplayGroupDefinition)Item.Model; + ScoreRank rank = group.Rank; + + rankColour = OsuColour.ForRank(rank); + + AccentColour = rankColour; + backgroundBorder.Colour = rankColour; + contentBackground.Colour = rankColour.Darken(1f); + glow.Colour = ColourInfo.GradientHorizontal(rankColour, rankColour.Opacity(0f)); + + switch (rank) + { + case ScoreRank.SH: + case ScoreRank.XH: + starRatingText.Colour = DrawableRank.GetRankNameColour(rank); + iconContainer.Colour = colourProvider.Background5; + break; + + case ScoreRank.X: + case ScoreRank.S: + starRatingText.Colour = DrawableRank.GetRankNameColour(rank); + iconContainer.Colour = colourProvider.Background5; + break; + + case ScoreRank.F: + starRatingText.Colour = DrawableRank.GetRankNameColour(rank); + iconContainer.Colour = colourProvider.Content1; + break; + + default: + starRatingText.Colour = Color4.White; + iconContainer.Colour = colourProvider.Background5; + break; + } + + starRatingText.Text = group.Title; + + ColourInfo colour = ColourInfo.GradientHorizontal(rankColour.Darken(0.6f), rankColour.Darken(0.8f)); + + triangles.Colour = colour; + + countText.Text = Item.NestedItemCount.ToLocalisableString(@"N0"); + + onExpanded(); + } + + private void onExpanded() + { + const float duration = 500; + + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); + } + + protected override void Update() + { + base.Update(); + + // Move the count pill in the opposite direction to keep it pinned to the screen regardless of the X position of TopLevelContent. + countPill.X = -TopLevelContent.X; + } + + public override MenuItem[] ContextMenuItems + { + get + { + if (Item == null) + return Array.Empty(); + + return new MenuItem[] + { + new OsuMenuItem(Expanded.Value ? WebCommonStrings.ButtonsCollapse.ToSentence() : WebCommonStrings.ButtonsExpand.ToSentence(), MenuItemType.Highlighted, () => TriggerClick()) + }; + } + } + } +} diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index eac86a9c02..185b1cc4f1 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -144,8 +144,9 @@ namespace osu.Game.Utils /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable hitObjects) => - GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects)); + /// Whether to only include the start and end positions of the slider, or include every control point in the slider. + public static Quad GetSurroundingQuad(IEnumerable hitObjects, bool startAndEndOnly = false) => + GetSurroundingQuad(startAndEndOnly ? enumerateStartAndEndPositions(hitObjects) : enumeratePositions(hitObjects)); /// /// Returns the points that make up the convex hull of the provided points. @@ -202,7 +203,7 @@ namespace osu.Game.Utils } public static List GetConvexHull(IEnumerable hitObjects) => - GetConvexHull(enumerateStartAndEndPositions(hitObjects)); + GetConvexHull(enumeratePositions(hitObjects)); private static IEnumerable enumerateStartAndEndPositions(IEnumerable hitObjects) => hitObjects.SelectMany(h => @@ -220,6 +221,17 @@ namespace osu.Game.Utils return new[] { h.Position }; }); + private static IEnumerable enumeratePositions(IEnumerable hitObjects) => + hitObjects.SelectMany(h => + { + if (h is IHasPath path) + { + return path.Path.ControlPoints.Select(p => h.Position + p.Position); + } + + return new[] { h.Position }; + }); + #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle