1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-22 09:50:24 +08:00

Add current stage overlay to ranked play (#37202)

Displays the stage name and details like currently picking player
and damage multiplayer where applicable.

Currently only shown on the discard and pick stages.

# Move user retrieval to `RankedPlayScreen`

This is done because I need the relevant `APIUser` instances in order to
pass them to the overlay component. `RankedPlayScreen` seems like the
appropriate place to manage the overlays since it manages the stage
subscreens, hence the need to access the `APIUser`s in here.

# Add current stage overlay to ranked play

The actual change of this PR. Very much a dev design.


https://github.com/user-attachments/assets/2388e934-2fc7-4e15-9947-9f98412765d2

---------

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Krzysztof Gutkowski
2026-04-05 20:00:58 +02:00
committed by GitHub
Unverified
parent b08a86f539
commit 9a56aed1e9
16 changed files with 353 additions and 27 deletions
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
{
State = { BindTarget = visibility },
Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
}
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
{
State = { BindTarget = visibility },
Child = new RankedPlayUserDisplay(2, Anchor.TopRight, RankedPlayColourScheme.Red)
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.TopRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
}
@@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneRankedPlayStageOverlay : RankedPlayTestScene
{
private Container content = null!;
protected override Container<Drawable> Content => content;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create components", () => base.Content.Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new RankedPlayBackground
{
RelativeSizeAxes = Axes.Both,
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
}
});
}
[Test]
public void TestBasic()
{
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
}
[Test]
public void TestLongUsername()
{
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 226597,
Username = "WWWWWWWWWWWWWWWWWWWW",
},
Multiplier = 2,
});
}
[Test]
public void TestColourScheme()
{
AddStep("create blue", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
AddStep("create red", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Red)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
}
}
}
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
@@ -34,7 +35,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay)));
WaitForJoined();
AddStep("add display", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
AddStep("add display", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -46,7 +47,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
[Test]
public void TesUserDisplay()
{
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -54,7 +55,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
Health = { BindTarget = health }
});
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Red)
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Red)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -5,7 +5,6 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -40,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
[Resolved]
private UserLookupCache users { get; set; } = null!;
private readonly int userId;
private readonly APIUser user;
private readonly Anchor contentAnchor;
private readonly RankedPlayColourScheme colourScheme;
@@ -56,9 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
[Resolved]
private RankedPlayCornerPiece? cornerPiece { get; set; }
public RankedPlayUserDisplay(int userId, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
public RankedPlayUserDisplay(APIUser user, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
{
this.userId = userId;
this.user = user;
this.contentAnchor = contentAnchor;
this.colourScheme = colourScheme;
}
@@ -66,8 +65,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
[BackgroundDependencyLoader]
private void load()
{
APIUser user = users.GetUserAsync(userId).GetResultSafely()!;
var shear = contentAnchor == Anchor.TopLeft || contentAnchor == Anchor.BottomRight
? -OsuGame.SHEAR
: OsuGame.SHEAR;
@@ -168,12 +165,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
private void onRoomUpdated()
{
var user = client.Room?.Users.SingleOrDefault(u => u.UserID == userId);
var multiplayerUser = client.Room?.Users.SingleOrDefault(u => u.UserID == user.Id);
if (user == null || availability == user.BeatmapAvailability)
if (multiplayerUser == null || availability == multiplayerUser.BeatmapAvailability)
return;
availability = user.BeatmapAvailability;
availability = multiplayerUser.BeatmapAvailability;
if (availability.State is DownloadState.NotDownloaded or DownloadState.Downloading or DownloadState.Importing)
beatmapState.FadeIn(50);
@@ -34,7 +34,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Discard Phase";
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Discard Phase";
protected override LocalisableString StageCaption => "Replace cards from your hand";
private PlayerHandOfCards playerHand = null!;
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
/// </summary>
public Action<bool>? ExitRequested { get; init; }
protected override LocalisableString StageHeading => "Results";
public override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
[Resolved]
@@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class GameplayScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => "Gameplay";
public override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[BackgroundDependencyLoader]
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public override bool ShowBeatmapBackground => true;
protected override LocalisableString StageHeading => "Gameplay";
public override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[Cached(typeof(IBindable<SongSelect.BeatmapSetLookupResult?>))]
@@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
{
public partial class IntroScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => string.Empty;
public override LocalisableString StageHeading => string.Empty;
protected override LocalisableString StageCaption => string.Empty;
public IntroScreen()
@@ -23,7 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Pick Phase";
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "Waiting for your opponent...";
protected override RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Red;
@@ -28,7 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Pick Phase";
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "It's your turn to play a card!";
private PlayerHandOfCards playerHand = null!;
@@ -74,6 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public bool IsOwnTurn => RoomState.ActiveUserId == client.LocalUser?.UserID;
public bool IsOpponentTurn => RoomState.ActiveUserId == OpponentId;
public int CurrentRound => RoomState.CurrentRound;
public int OpponentId => RoomState.Users.Keys.Single(u => u != client.LocalUser?.UserID);
@@ -7,6 +7,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.Cursor;
@@ -14,8 +15,10 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
@@ -56,6 +59,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private UserLookupCache users { get; set; } = null!;
[Resolved]
private IDialogOverlay dialogOverlay { get; set; } = null!;
@@ -69,6 +75,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private QueueController? controller { get; set; }
private readonly MultiplayerRoom room;
private APIUser localUser = null!;
private APIUser opponentUser = null!;
private readonly Container stageOverlayContainer;
private readonly Container<RankedPlaySubScreen> screenContainer;
private readonly RankedPlayChatDisplay chat;
@@ -130,6 +141,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
}
}
},
stageOverlayContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
overlayContainer = new CardDetailsOverlayContainer(),
particleContainer = new SongPreviewParticleContainer(),
backgroundMusic = new BackgroundMusicManager()
@@ -154,11 +169,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
int localUserId = api.LocalUser.Value.OnlineID;
int opponentUserId = ((RankedPlayRoomState)client.Room!.MatchState!).Users.Keys.Single(it => it != localUserId);
localUser = users.GetUserAsync(localUserId).GetResultSafely()!;
opponentUser = users.GetUserAsync(opponentUserId).GetResultSafely()!;
AddRangeInternal([
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
{
State = { BindTarget = cornerPieceVisibility },
Child = new RankedPlayUserDisplay(localUserId, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = new RankedPlayUserDisplay(localUser, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
Health = { BindTarget = matchInfo.PlayerHealth }
@@ -167,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
{
State = { BindTarget = cornerPieceVisibility },
Child = new RankedPlayUserDisplay(opponentUserId, Anchor.TopRight, RankedPlayColourScheme.Red)
Child = new RankedPlayUserDisplay(opponentUser, Anchor.TopRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
Health = { BindTarget = matchInfo.OpponentHealth }
@@ -199,6 +217,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
cornerPieceVisibility.BindTo(screen.CornerPieceVisibility);
showBeatmapBackground.Value = screen.ShowBeatmapBackground;
if (screen.ShowStageOverlay)
{
APIUser? pickingUser = null;
double? multiplier = matchInfo.Stage.Value < RankedPlayStage.CardPlay ? null : matchInfo.RoomState.DamageMultiplier;
RankedPlayColourScheme colourScheme = RankedPlayColourScheme.Blue;
if (matchInfo.Stage.Value == RankedPlayStage.CardPlay && matchInfo.RoomState.ActiveUser != null)
{
pickingUser = matchInfo.IsOwnTurn ? localUser : opponentUser;
colourScheme = matchInfo.IsOwnTurn ? RankedPlayColourScheme.Blue : RankedPlayColourScheme.Red;
}
stageOverlayContainer.Add(new RankedPlayStageOverlay(screen.StageHeading, colourScheme)
{
PickingUser = pickingUser,
Multiplier = multiplier,
});
}
};
}
@@ -0,0 +1,186 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class RankedPlayStageOverlay : CompositeDrawable
{
private readonly LocalisableString stageName;
private readonly RankedPlayColourScheme colourScheme;
public APIUser? PickingUser { get; init; }
public double? Multiplier { get; init; }
private FillFlowContainer displayContainer = null!;
private FillFlowContainer detailsContainer = null!;
private CircularContainer avatarContainer = null!;
public RankedPlayStageOverlay(LocalisableString stageName, RankedPlayColourScheme colourScheme)
{
this.stageName = stageName;
this.colourScheme = colourScheme;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
Alpha = 0.4f,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4Extensions.FromHex("#000"),
},
displayContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new Container
{
Width = 500,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = OsuGame.SHEAR,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourScheme.Surface.Darken(0.1f),
Alpha = 0.8f,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -OsuGame.SHEAR,
Padding = new MarginPadding { Vertical = 20 },
Font = OsuFont.TorusAlternate.With(size: 72),
Shadow = false,
Text = stageName,
},
},
},
detailsContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(40, 0),
},
},
}
},
};
if (PickingUser != null)
{
detailsContainer.Add(new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
avatarContainer = new CircularContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(32),
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourScheme.Surface,
Alpha = 0.5f,
},
},
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 32),
Text = $"{PickingUser.Username}'s pick",
Colour = colourScheme.Primary,
},
},
});
}
if (Multiplier != null)
{
detailsContainer.Add(new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 32),
Text = $"{Multiplier:N0}x damage",
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (PickingUser != null)
LoadComponentAsync(new DrawableAvatar(PickingUser), a => avatarContainer.Add(a));
const int duration = 500;
const int time_visible = 1500;
const Easing easing = Easing.OutQuint;
this.FadeInFromZero(300, easing);
displayContainer
.ScaleTo(0.9f)
.ScaleTo(1f, duration, easing);
using (BeginDelayedSequence(time_visible))
{
this.FadeOut(duration, easing)
.Expire();
displayContainer
.ScaleTo(0.9f, duration, easing);
}
}
}
}
@@ -23,10 +23,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public virtual bool ShowBeatmapBackground => false;
/// <summary>
/// Whether a fullscreen overlay displaying the current stage (and any additional
/// information like the currently picking player and/or the damage multiplier)
/// should be displayed upon entering this screen.
/// </summary>
public virtual bool ShowStageOverlay => false;
/// <summary>
/// Heading text to be displayed indicating the purpose of the current stage.
/// </summary>
protected abstract LocalisableString StageHeading { get; }
public abstract LocalisableString StageHeading { get; }
/// <summary>
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class ResultsScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => "Results";
public override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
public override bool ShowBeatmapBackground => true;
@@ -243,7 +243,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
State = { BindTarget = cornerPieceVisibility },
Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.UserID, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.User, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
Health = { Value = PlayerDamageInfo.OldLife }
@@ -254,7 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
State = { BindTarget = cornerPieceVisibility },
Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.UserID, Anchor.BottomRight, RankedPlayColourScheme.Red)
Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.User, Anchor.BottomRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
Health = { Value = OpponentDamageInfo.OldLife }