From 4c0657f4ca88769fcc3b1cb4fa2dcc8bb862f9e3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 11 Mar 2026 17:04:51 +0900 Subject: [PATCH] Ranked Play: Add countdown timer to all screens (#36930) There's been [some feedback](https://discord.com/channels/90072389919997952/1476979671886205060/1477868178775216148) that there's no timer during some screens. Personally I feel like some screens shouldn't require it, but I get it. The results screen is going to be the most controversial one, where I've decided to shift the content down a bit - it would otherwise overlap with the progress bar. Another option is to make the progress bar shorter, but I feel like that makes things inconsistent. image Regarding implementation, I'm not entirely sure on it being added to every subscreen via `RankedPlaySubScreen` instead of just a single component at a top level. I did it this way because the colour scheme seems annoying to change without recreating the entire component (or otherwise all of its children) anyway. --- .../Components/RankedPlayStageDisplay.cs | 77 ++++++++++++++++--- .../Matchmaking/RankedPlay/DiscardScreen.cs | 16 ++-- .../Matchmaking/RankedPlay/EndedScreen.cs | 4 + .../Matchmaking/RankedPlay/GameplayScreen.cs | 4 + .../RankedPlay/GameplayWarmupScreen.cs | 4 + .../RankedPlay/Intro/IntroScreen.cs | 5 ++ .../RankedPlay/OpponentPickScreen.cs | 12 +-- .../Matchmaking/RankedPlay/PickScreen.cs | 10 +-- .../RankedPlay/RankedPlaySubScreen.cs | 27 ++++++- .../Matchmaking/RankedPlay/ResultsScreen.cs | 20 ++--- 10 files changed, 137 insertions(+), 42 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayStageDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayStageDisplay.cs index d0fb4f9332..8bcd66292a 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayStageDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayStageDisplay.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -21,24 +20,20 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components { - public partial class RankedPlayStageDisplay : CompositeDrawable + public partial class RankedPlayStageDisplay : VisibilityContainer { - public required LocalisableString Heading { get; init; } - - public required LocalisableString Caption { get; init; } - - public Color4? CaptionColour { get; init; } - [Resolved] private MultiplayerClient client { get; set; } = null!; private readonly RankedPlayColourScheme colourScheme; private Drawable headingTextBackground = null!; - private OsuSpriteText headingText = null!; private Drawable progressBar = null!; private OsuSpriteText progressText = null!; + private OsuSpriteText? headingText; + private OsuSpriteText? captionText; + private DateTimeOffset countdownStartTime; private DateTimeOffset countdownEndTime; @@ -50,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { const float phase_text_background_height = 55; Vector2 progressBarSize = new Vector2(300, 25); @@ -162,7 +157,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components } ] }, - new OsuSpriteText + captionText = new OsuSpriteText { Margin = new MarginPadding { @@ -176,6 +171,54 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components }; } + private LocalisableString heading; + + /// + /// Heading text to be displayed indicating the purpose of the current stage. + /// + public LocalisableString Heading + { + get => heading; + set + { + heading = value; + if (headingText != null) + headingText.Text = value; + } + } + + private LocalisableString caption; + + /// + /// Subtitle text to be displayed indicating the action a user should take in the current stage. + /// + public LocalisableString Caption + { + get => caption; + set + { + caption = value; + if (captionText != null) + captionText.Text = value; + } + } + + private Color4? captionColour; + + /// + /// Overrides the default caption colour from the colour scheme with a custom one. + /// + public Color4? CaptionColour + { + get => captionColour; + set + { + captionColour = value; + if (captionText != null) + captionText.Colour = value ?? colourScheme.Primary; + } + } + protected override void LoadComplete() { base.LoadComplete(); @@ -194,7 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components { base.Update(); - headingTextBackground.Width = headingText.DrawWidth + 80; + headingTextBackground.Width = headingText!.DrawWidth + 80; TimeSpan duration = countdownEndTime - countdownStartTime; TimeSpan remaining = countdownEndTime - DateTimeOffset.Now; @@ -226,6 +269,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components countdownEndTime = DateTimeOffset.Now; }); + protected override void PopIn() + { + this.FadeIn(); + } + + protected override void PopOut() + { + this.FadeOut(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/DiscardScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/DiscardScreen.cs index c044a0d4c6..69da87aaa0 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/DiscardScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/DiscardScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -33,6 +34,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay public CardFlow CenterRow { get; private set; } = null!; + protected override LocalisableString StageHeading => "Discard Phase"; + protected override LocalisableString StageCaption => "Replace cards from your hand"; + private PlayerHandOfCards playerHand = null!; private ShearedButton discardButton = null!; private OsuTextFlowContainer explainer = null!; @@ -57,6 +61,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay private DateTimeOffset stageEndTime; private TimeSpan stageDuration; + public DiscardScreen() + { + StageDisplay.CaptionColour = Color4.White; + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { @@ -72,13 +81,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new RankedPlayStageDisplay(RankedPlayColourScheme.Blue) - { - Heading = "Discard Phase", - Caption = "Replace cards from your hand", - CaptionColour = Color4.White, - Margin = new MarginPadding { Top = 60 }, - }, discardButton = new ShearedButton { Name = "Discard Button", diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs index ba0549e5fa..b76f8a7944 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -24,6 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay /// public Action? ExitRequested { get; init; } + protected override LocalisableString StageHeading => "Results"; + protected override LocalisableString StageCaption => string.Empty; + [Resolved] private RankedPlayMatchInfo matchInfo { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayScreen.cs index 8a558b24e3..251c304995 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayScreen.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -12,6 +13,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { public partial class GameplayScreen : RankedPlaySubScreen { + protected override LocalisableString StageHeading => "Gameplay"; + protected override LocalisableString StageCaption => string.Empty; + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs index 80f4bb4ad5..c01a732c74 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/GameplayWarmupScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Database; using osu.Game.Graphics.Containers; @@ -24,6 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { public override bool ShowBeatmapBackground => true; + protected override LocalisableString StageHeading => "Gameplay"; + protected override LocalisableString StageCaption => string.Empty; + [Cached(typeof(IBindable))] private readonly Bindable lastLookupResult = new Bindable(); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs index 87fac18026..7cfed62015 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Intro/IntroScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -20,9 +21,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro { public partial class IntroScreen : RankedPlaySubScreen { + protected override LocalisableString StageHeading => string.Empty; + protected override LocalisableString StageCaption => string.Empty; + public IntroScreen() { CornerPieceVisibility.Value = Visibility.Hidden; + CountdownVisibility.Value = Visibility.Hidden; } [Resolved] diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/OpponentPickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/OpponentPickScreen.cs index 122b3ff8dd..aa8f392890 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/OpponentPickScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/OpponentPickScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; @@ -22,6 +23,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { public CardFlow CenterRow { get; private set; } = null!; + protected override LocalisableString StageHeading => "Pick Phase"; + protected override LocalisableString StageCaption => "Waiting for your opponent..."; + + protected override RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Red; + private PlayerHandOfCards playerHand = null!; private OpponentHandOfCards opponentHand = null!; @@ -46,12 +52,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new RankedPlayStageDisplay(RankedPlayColourScheme.Red) - { - Heading = "Pick Phase", - Caption = "Waiting for your opponent...", - Margin = new MarginPadding { Top = 60 }, - }, ]; CenterColumn.Children = diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/PickScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/PickScreen.cs index 0a232322ca..2d94e28ff3 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/PickScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/PickScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Online.Multiplayer; @@ -27,6 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay public CardFlow CenterRow { get; private set; } = null!; + protected override LocalisableString StageHeading => "Pick Phase"; + protected override LocalisableString StageCaption => "It's your turn to play a card!"; + private PlayerHandOfCards playerHand = null!; private OpponentHandOfCards opponentHand = null!; @@ -65,12 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new RankedPlayStageDisplay(RankedPlayColourScheme.Blue) - { - Heading = "Pick Phase", - Caption = "It's your turn to play a card!", - Margin = new MarginPadding { Top = 60 }, - }, ]; CenterColumn.Children = diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlaySubScreen.cs index b19340107f..46aa1d20cb 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/RankedPlaySubScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; using osuTK; @@ -18,19 +19,34 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay public const float CENTERED_CARD_SCALE = 1.2f; public readonly Bindable CornerPieceVisibility = new Bindable(Visibility.Visible); + protected readonly Bindable CountdownVisibility = new Bindable(Visibility.Visible); public virtual bool ShowBeatmapBackground => false; + /// + /// Heading text to be displayed indicating the purpose of the current stage. + /// + protected abstract LocalisableString StageHeading { get; } + + /// + /// Subtitle text to be displayed indicating the action a user should take in the current stage. + /// + protected abstract LocalisableString StageCaption { get; } + + /// + /// The colour scheme commonly used for components of this screen. + /// + protected virtual RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Blue; + [Resolved] private MultiplayerClient client { get; set; } = null!; protected MultiplayerClient Client => client; protected override Container Content { get; } - protected readonly Container CenterColumn; - protected readonly FillFlowContainer ButtonsContainer; + protected readonly RankedPlayStageDisplay StageDisplay; protected RankedPlaySubScreen() { @@ -62,6 +78,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay Direction = FillDirection.Vertical, Spacing = new Vector2(8) }, + StageDisplay = new RankedPlayStageDisplay(ColourScheme) + { + Heading = StageHeading, + Caption = StageCaption, + Margin = new MarginPadding { Top = 60 }, + State = { BindTarget = CountdownVisibility } + }, ]; } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs index 872920272a..129ffc61dc 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Transforms; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -36,6 +37,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { public partial class ResultsScreen : RankedPlaySubScreen { + protected override LocalisableString StageHeading => "Results"; + protected override LocalisableString StageCaption => string.Empty; + public override bool ShowBeatmapBackground => true; [Resolved] @@ -63,14 +67,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { CornerPieceVisibility.Value = Visibility.Hidden; - InternalChildren = new Drawable[] + AddInternal(loadingSpinner = new LoadingSpinner { - loadingSpinner = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - }; + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); } protected override void LoadComplete() @@ -160,6 +161,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay AddInternal(new ResultScreenContent { + RelativeSizeAxes = Axes.Both, + // A little bit of room for the countdown timer... + Margin = new MarginPadding { Top = 45 }, PlayerScore = playerScore, OpponentScore = opponentScore, PlayerDamageInfo = matchInfo.RoomState.Users[playerId].DamageInfo!, @@ -210,8 +214,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay .OfType() .MaxBy(it => it.Damage)!; - RelativeSizeAxes = Axes.Both; - AddInternal(panelScaffold = new PanelScaffold { Anchor = Anchor.Centre,