From 4b8fa892ea28d262ae31b72c1a7c2d5b71832c6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Apr 2026 20:14:11 +0900 Subject: [PATCH] Tidy up `ResultsScreen` (#37242) Just the bare minimum code quality so I can start working on these classes.. Please push back if this doesn't seem better than what was already there. This is mostly autopilot fixing for me based on how I've been writing code for osu! to date. There are changes to the load process but nothing which should cause issues, I hope. --- .../RankedPlay/ResultScreen.MainPanel.cs | 576 +++++++++++++++ .../Matchmaking/RankedPlay/ResultsScreen.cs | 690 ++---------------- 2 files changed, 621 insertions(+), 645 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultScreen.MainPanel.cs diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultScreen.MainPanel.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultScreen.MainPanel.cs new file mode 100644 index 0000000000..8cd769de82 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultScreen.MainPanel.cs @@ -0,0 +1,576 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Utils; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay +{ + public partial class ResultsScreen + { + private partial class MainPanel : CompositeDrawable + { + public required ScoreInfo PlayerScore { get; init; } + public required ScoreInfo OpponentScore { get; init; } + public required RankedPlayDamageInfo PlayerDamageInfo { get; init; } + public required RankedPlayDamageInfo OpponentDamageInfo { get; init; } + + [Resolved] + private RankedPlayMatchInfo matchInfo { get; set; } = null!; + + [Resolved] + private OsuColour colour { get; set; } = null!; + + private static Vector2 cardSize => new Vector2(950, 550); + + private readonly Bindable cornerPieceVisibility = new Bindable(); + private readonly Bindable scoreBarProgress = new Bindable(); + + private PanelScaffold panelScaffold = null!; + private Box flash = null!; + private ScoreDetails playerScoreDetails = null!; + private ScoreDetails opponentScoreDetails = null!; + private RankedPlayScoreCounter playerScoreCounter = null!; + private RankedPlayScoreCounter opponentScoreCounter = null!; + private RankedPlayScoreCounter damageCounter = null!; + private OsuSpriteText flyingDamageText = null!; + private ScoreBar playerScoreBar = null!; + private ScoreBar opponentScoreBar = null!; + private OsuSpriteText roundNumber = null!; + private RankedPlayUserDisplay playerUserDisplay = null!; + private RankedPlayUserDisplay opponentUserDisplay = null!; + + private RankedPlayDamageInfo losingDamageInfo = null!; + + private Sample resultsAppearSample = null!; + private Sample dmgFlySample = null!; + private Sample dmgHitSample = null!; + private Sample hpDownSample = null!; + private Sample playerAppearSample = null!; + private Sample pseudoScoreCounterSample = null!; + private Sample scoreTickSample = null!; + private Sample gradePassSample = null!; + private Sample gradePassSsSample = null!; + private Sample gradeFailSample = null!; + private Sample gradeFailDSample = null!; + private SampleChannel? playerScoreTickChannel; + private SampleChannel? opponentScoreTickChannel; + private readonly BindableDouble playerScoreTickPitch = new BindableDouble(); + private readonly BindableDouble opponentScoreTickPitch = new BindableDouble(); + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + // this works under the assumption that only one player can receive damage each round + losingDamageInfo = matchInfo.RoomState.Users + .Select(it => it.Value.DamageInfo) + .OfType() + .MaxBy(it => it.Damage)!; + + AddInternal(panelScaffold = new PanelScaffold + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = + [ + new RankedPlayCornerPiece(RankedPlayColourScheme.BLUE, Anchor.BottomLeft) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + State = { BindTarget = cornerPieceVisibility }, + Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.User, Anchor.BottomLeft, RankedPlayColourScheme.BLUE) + { + RelativeSizeAxes = Axes.Both, + Health = { Value = PlayerDamageInfo.OldLife } + } + }, + new RankedPlayCornerPiece(RankedPlayColourScheme.RED, Anchor.BottomRight) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + State = { BindTarget = cornerPieceVisibility }, + Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.User, Anchor.BottomRight, RankedPlayColourScheme.RED) + { + RelativeSizeAxes = Axes.Both, + Health = { Value = OpponentDamageInfo.OldLife } + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 110, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Padding = new MarginPadding { Bottom = 30 }, + Child = roundNumber = new OsuSpriteText + { + Text = $"Round {matchInfo.CurrentRound}", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 36, weight: FontWeight.Bold, typeface: Typeface.TorusAlternate), + Alpha = 0, + }, + }, + new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = cardSize, + Padding = new MarginPadding { Bottom = 110, Top = 60, Horizontal = 60 }, + ColumnDimensions = + [ + new Dimension(), + new Dimension(GridSizeMode.Absolute, 40), + new Dimension(GridSizeMode.Absolute, 60), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.Absolute, 60), + new Dimension(GridSizeMode.Absolute, 40), + new Dimension(), + ], + Content = new Drawable?[][] + { + [ + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + ], + Content = new Drawable[][] + { + [ + playerScoreDetails = new ScoreDetails(PlayerScore, RankedPlayColourScheme.BLUE) + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + ], + [ + playerScoreCounter = new RankedPlayScoreCounter(numDigits(PlayerScore.TotalScore)) + { + Font = OsuFont.GetFont(size: 60, fixedWidth: true), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-4), + Alpha = 0, + AlwaysPresent = true, + } + ] + } + }, + null, + playerScoreBar = new ScoreBar(RankedPlayColourScheme.BLUE) + { + RelativeSizeAxes = Axes.Both, + Height = 0.05f, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + }, + null, + opponentScoreBar = new ScoreBar(RankedPlayColourScheme.RED) + { + RelativeSizeAxes = Axes.Both, + Height = 0.05f, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = + [ + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + ], + Content = new Drawable[][] + { + [ + opponentScoreDetails = new ScoreDetails(OpponentScore, RankedPlayColourScheme.RED) + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + ], + [ + opponentScoreCounter = new RankedPlayScoreCounter(numDigits(OpponentScore.TotalScore)) + { + Font = OsuFont.GetFont(size: 60, fixedWidth: true), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-4), + Alpha = 0, + AlwaysPresent = true, + } + ] + } + }, + ] + } + }, + flash = new Box + { + RelativeSizeAxes = Axes.Both, + }, + ], + BottomOrnament = + { + Size = new Vector2(200, 60), + Alpha = 0, + Children = + [ + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = + [ + damageCounter = new RankedPlayScoreCounter(numDigits(losingDamageInfo.Damage)) + { + Font = OsuFont.GetFont(size: 36, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-2), + }, + flyingDamageText = new OsuSpriteText + { + Text = FormattableString.Invariant($"{losingDamageInfo.Damage:N0}"), + Font = OsuFont.GetFont(size: 36, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-2), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BypassAutoSizeAxes = Axes.Both, + Alpha = 0, + }, + new OsuSpriteText + { + BypassAutoSizeAxes = Axes.Both, + Text = $"{matchInfo.RoomState.DamageMultiplier.ToStandardFormattedString(maxDecimalDigits: 1)}x", + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 42), + Rotation = 30, + Alpha = 0, + Colour = colour.RedLight + }, + ] + }, + new OsuSpriteText + { + Text = Precision.AlmostEquals(matchInfo.RoomState.DamageMultiplier, 1) + ? "Damage" + : $"Damage {matchInfo.RoomState.DamageMultiplier.ToStandardFormattedString(maxDecimalDigits: 1)}x", + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 22), + }, + ] + } + }); + + resultsAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/results-appear"); + dmgFlySample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-fly"); + dmgHitSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-hit"); + hpDownSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/hp-down"); + playerAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/players-appear"); + pseudoScoreCounterSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/pseudo-score-counter"); + scoreTickSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/score-tick"); + gradePassSample = audio.Samples.Get(@"Results/rank-impact-pass"); + gradePassSsSample = audio.Samples.Get(@"Results/rank-impact-pass-ss"); + gradeFailSample = audio.Samples.Get(@"Results/rank-impact-fail"); + gradeFailDSample = audio.Samples.Get(@"Results/rank-impact-fail-d"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + playAnimation(); + } + + private void playAnimation() + { + const double text_movement_duration = 400; + + double delay = 0; + + resultsAppearSample.Play(); + + panelScaffold.FadeIn(100) + .ResizeTo(0) + .ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo) + // deliberately cutting this delay 300ms short so the vertical resize interrupts the horizontal one + .Delay(300) + .ResizeHeightTo(cardSize.Y, 800, Easing.OutExpo); + + flash.Delay(150).FadeOut(600, Easing.Out); + + using (BeginDelayedSequence(700)) + { + roundNumber.FadeIn(600); + playerScoreCounter.FadeIn(600); + opponentScoreCounter.FadeIn(600); + + Schedule(() => + { + cornerPieceVisibility.Value = Visibility.Visible; + playerAppearSample.Play(); + }); + } + + using (BeginDelayedSequence(900)) + { + panelScaffold.BottomOrnament + .FadeIn(300) + .ResizeWidthTo(cardSize.X - 550, 600, Easing.OutExpo); + } + + delay += 1000; + + using (BeginDelayedSequence(delay)) + { + const double score_text_duration = 2000; + + playerScoreCounter.TransformValueTo(PlayerScore.TotalScore, score_text_duration - 500); + opponentScoreCounter.TransformValueTo(OpponentScore.TotalScore, score_text_duration - 500); + + damageCounter.TransformValueTo(losingDamageInfo.Damage, score_text_duration - 500); + + long maxAchievableScore = Math.Max( + Math.Max(PlayerScore.TotalScore, OpponentScore.TotalScore), + 1_000_000 + ); + + float playerScorePercent = (float)PlayerScore.TotalScore / maxAchievableScore; + float opponentScorePercent = (float)OpponentScore.TotalScore / maxAchievableScore; + float maxScorePercent = Math.Max(playerScorePercent, opponentScorePercent); + + playerScoreBar.FadeIn(100); + opponentScoreBar.FadeIn(100); + + playerScoreTickChannel ??= scoreTickSample.GetChannel(); + playerScoreTickChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; + playerScoreTickChannel.Frequency.BindTarget = playerScoreTickPitch; + playerScoreTickPitch.Value = 0.5f; + playerScoreTickChannel.Looping = true; + + opponentScoreTickChannel ??= scoreTickSample.GetChannel(); + opponentScoreTickChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH; + opponentScoreTickChannel.Frequency.BindTarget = opponentScoreTickPitch; + opponentScoreTickPitch.Value = 0.5f; + opponentScoreTickChannel.Looping = true; + + Schedule(() => + { + if (losingDamageInfo.Damage > 0) + pseudoScoreCounterSample.Play(); + + if (PlayerScore.TotalScore > 0) + playerScoreTickChannel.Play(); + + if (OpponentScore.TotalScore > 0) + opponentScoreTickChannel.Play(); + }); + + this.TransformBindableTo(scoreBarProgress, maxScorePercent, score_text_duration, new CubicBezierEasingFunction(easeIn: 0.4, easeOut: 1)); + this.TransformBindableTo(playerScoreTickPitch, 0.5f + playerScorePercent, score_text_duration, Easing.OutCubic); + this.TransformBindableTo(opponentScoreTickPitch, 0.5f + opponentScorePercent, score_text_duration, Easing.OutCubic); + + // safety timeout to ensure scoreTicks don't play forever + Scheduler.AddDelayed(() => + { + if (playerScoreTickChannel != null) + playerScoreTickChannel.Looping = false; + + if (opponentScoreTickChannel != null) + opponentScoreTickChannel.Looping = false; + }, score_text_duration + 500); + + scoreBarProgress.BindValueChanged(e => + { + playerScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, playerScorePercent)); + opponentScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, opponentScorePercent)); + + Schedule(() => + { + if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent) + playerScoreTickChannel.Looping = false; + + if (opponentScoreTickChannel != null && opponentScoreBar.Height >= opponentScorePercent) + opponentScoreTickChannel.Looping = false; + }); + }); + } + + delay += 2200; + + using (BeginDelayedSequence(delay)) + { + playerScoreDetails.FadeIn(300); + opponentScoreDetails.FadeIn(300); + + Schedule(() => + { + SampleChannel playerRankChannel = getRankSample(PlayerScore.Rank).GetChannel(); + playerRankChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; + playerRankChannel.Play(); + + SampleChannel opponentRankChannel = getRankSample(OpponentScore.Rank).GetChannel(); + opponentRankChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH; + opponentRankChannel.Play(); + }); + } + + delay += 800; + + bool playerTookDamage = OpponentScore.TotalScore > PlayerScore.TotalScore; + double loserPanDirection = playerTookDamage ? -OsuGameBase.SFX_STEREO_STRENGTH : OsuGameBase.SFX_STEREO_STRENGTH; + + using (BeginDelayedSequence(delay)) + { + Schedule(() => + { + RankedPlayUserDisplay userDisplay = + PlayerScore.TotalScore > OpponentScore.TotalScore + ? opponentUserDisplay + : playerUserDisplay; + + Vector2 screenSpacePosition = userDisplay.HealthDisplay.ScreenSpaceImpactPosition; + + var position1 = flyingDamageText.Parent!.ToLocalSpace(screenSpacePosition) - flyingDamageText.AnchorPosition; + + damageCounter.FadeOut() + .Delay(200) + .FadeIn(300) + .ScaleTo(0.9f) + .ScaleTo(1f, 300, Easing.OutElasticHalf); + + var dmgFlyChannel = dmgFlySample.GetChannel(); + this.TransformBindableTo(dmgFlyChannel.Balance, loserPanDirection, text_movement_duration, Easing.InCubic); + dmgFlyChannel.Play(); + + flyingDamageText.FadeIn() + .MoveTo(position1, text_movement_duration, Easing.InCubic) + .ScaleTo(0.75f, text_movement_duration, new CubicBezierEasingFunction(easeIn: 0.35, easeOut: 0.5)) + .RotateTo(12 * Math.Sign(position1.X), text_movement_duration, new CubicBezierEasingFunction(easeIn: 0.35, easeOut: 0.5)) + .Then() + .FadeOut(); + + Scheduler.AddDelayed(() => + { + var dmgHitChannel = dmgHitSample.GetChannel(); + dmgHitChannel.Balance.Value = loserPanDirection; + dmgHitChannel.Play(); + + userDisplay.Shake(shakeDuration: 60, shakeMagnitude: 2, maximumLength: 120); + + for (int i = 0; i < 10; i++) + { + var particle = new DamageParticle + { + Size = new Vector2(RNG.NextSingle(5, 15)), + Origin = Anchor.Centre, + Position = ToLocalSpace(screenSpacePosition), + Rotation = RNG.NextSingle(0, 360), + Blending = BlendingParameters.Additive, + }; + + AddInternal(particle); + + particle.FadeOut(600) + .ScaleTo(0, 600) + .RotateTo(particle.Rotation + RNG.NextSingle(-20, 20), 600) + .FadeColour(Color4.Red, 600) + .Expire(); + } + }, text_movement_duration); + }); + } + + delay += text_movement_duration; + + using (BeginDelayedSequence(delay)) + { + Schedule(() => + { + playerUserDisplay.Health.Value = PlayerDamageInfo.NewLife; + opponentUserDisplay.Health.Value = OpponentDamageInfo.NewLife; + + Scheduler.AddDelayed(() => + { + var hpDecreaseChannel = hpDownSample.GetChannel(); + hpDecreaseChannel.Balance.Value = loserPanDirection; + hpDecreaseChannel.Play(); + }, 900); + }); + } + } + + private Sample getRankSample(ScoreRank rank) + { + switch (rank) + { + default: + case ScoreRank.D: + return gradeFailDSample; + + case ScoreRank.C: + case ScoreRank.B: + return gradeFailSample; + + case ScoreRank.A: + case ScoreRank.S: + case ScoreRank.SH: + return gradePassSample; + + case ScoreRank.X: + case ScoreRank.XH: + return gradePassSsSample; + } + } + + private static int numDigits(long value) + { + if (value <= 0) + return 1; + + return (int)Math.Floor(Math.Log10(value)) + 1; + } + + private partial class DamageParticle : Triangle + { + private Vector2 velocity = new Vector2(RNG.NextSingle(-0.3f, 0.3f), RNG.NextSingle(-0.3f, 0.3f)); + + private Vector2 gravity => new Vector2(0, 0.0002f); + + protected override void Update() + { + base.Update(); + + velocity += gravity * (float)Time.Elapsed; + Position += velocity * (float)Time.Elapsed; + } + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs index 70c501943e..93a472f46e 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs @@ -3,37 +3,23 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; 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.Shapes; -using osu.Framework.Graphics.Transforms; using osu.Framework.Localisation; using osu.Framework.Logging; -using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Extensions; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Models; 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; using osu.Game.Rulesets; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay { @@ -49,15 +35,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay [Resolved] private MultiplayerClient client { get; set; } = null!; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - [Resolved] private ScoreManager scoreManager { get; set; } = null!; [Resolved] private RulesetStore rulesets { get; set; } = null!; + [Resolved] + private RankedPlayMatchInfo matchInfo { get; set; } = null!; + [Resolved] private IBindable globalRuleset { get; set; } = null!; @@ -81,51 +67,64 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay loadingSpinner.Show(); - queryScores().FireAndForget(); + fetchFinalScores().FireAndForget(); } - private async Task queryScores() + [Resolved] + private IBindable working { get; set; } = null!; + + private async Task fetchFinalScores() { try { if (client.Room == null) return; - Task beatmapTask = beatmapLookupCache.GetBeatmapAsync(client.Room.CurrentPlaylistItem.BeatmapID); - TaskCompletionSource> scoreTask = new TaskCompletionSource>(); + TaskCompletionSource> scoreLookup = new TaskCompletionSource>(); var request = new IndexPlaylistScoresRequest(client.Room.RoomID, client.Room.Settings.PlaylistItemId); - request.Success += req => scoreTask.SetResult(req.Scores); - request.Failure += scoreTask.SetException; + + request.Success += req => scoreLookup.SetResult(req.Scores); + request.Failure += scoreLookup.SetException; + api.Queue(request); - await Task.WhenAll(beatmapTask, scoreTask.Task).ConfigureAwait(false); + List apiScores = await scoreLookup.Task.ConfigureAwait(false); - APIBeatmap? apiBeatmap = beatmapTask.GetResultSafely(); - List apiScores = scoreTask.Task.GetResultSafely(); + ScoreInfo[] scores = apiScores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, working.Value.BeatmapInfo)).ToArray(); - if (apiBeatmap == null) - return; + Debug.Assert(scores.Length <= 2); - // Reference: PlaylistItemResultsScreen - setScores(apiScores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, new BeatmapInfo + int localUserId = api.LocalUser.Value.OnlineID; + int opponentId = matchInfo.RoomState.Users.Keys.Single(it => it != localUserId); + + ScoreInfo playerScore = scores.SingleOrDefault(s => s.UserID == localUserId) ?? new ScoreInfo { - Difficulty = new BeatmapDifficulty(apiBeatmap.Difficulty), - Metadata = + Rank = ScoreRank.F, + Ruleset = globalRuleset.Value, + User = new APIUser { Id = localUserId } + }; + + ScoreInfo opponentScore = scores.SingleOrDefault(s => s.UserID == opponentId) ?? new ScoreInfo + { + Rank = ScoreRank.F, + Ruleset = globalRuleset.Value, + User = new APIUser { Id = opponentId } + }; + + Schedule(() => + { + LoadComponentAsync(new MainPanel { - Artist = apiBeatmap.Metadata.Artist, - Title = apiBeatmap.Metadata.Title, - Author = new RealmUser - { - Username = apiBeatmap.Metadata.Author.Username, - OnlineID = apiBeatmap.Metadata.Author.OnlineID, - } - }, - DifficultyName = apiBeatmap.DifficultyName, - StarRating = apiBeatmap.StarRating, - Length = apiBeatmap.Length, - BPM = apiBeatmap.BPM - })).ToArray()); + 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[localUserId].DamageInfo!, + OpponentDamageInfo = matchInfo.RoomState.Users[opponentId].DamageInfo!, + }, AddInternal); + }); } catch (Exception e) { @@ -137,604 +136,5 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay Scheduler.Add(() => loadingSpinner.Hide()); } } - - [Resolved] - private RankedPlayMatchInfo matchInfo { get; set; } = null!; - - private void setScores(ScoreInfo[] scores) => Scheduler.Add(() => - { - int playerId = api.LocalUser.Value.OnlineID; - int opponentId = matchInfo.RoomState.Users.Keys.Single(it => it != playerId); - - ScoreInfo playerScore = scores.SingleOrDefault(s => s.UserID == playerId) ?? new ScoreInfo - { - Rank = ScoreRank.F, - Ruleset = globalRuleset.Value, - User = new APIUser { Id = playerId } - }; - - ScoreInfo opponentScore = scores.SingleOrDefault(s => s.UserID == opponentId) ?? new ScoreInfo - { - Rank = ScoreRank.F, - Ruleset = globalRuleset.Value, - User = new APIUser { Id = opponentId } - }; - - 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!, - OpponentDamageInfo = matchInfo.RoomState.Users[opponentId].DamageInfo!, - }); - }); - - private partial class ResultScreenContent : CompositeDrawable - { - public required ScoreInfo PlayerScore { get; init; } - public required ScoreInfo OpponentScore { get; init; } - public required RankedPlayDamageInfo PlayerDamageInfo { get; init; } - public required RankedPlayDamageInfo OpponentDamageInfo { get; init; } - - [Resolved] - private RankedPlayMatchInfo matchInfo { get; set; } = null!; - - [Resolved] - private OsuColour colour { get; set; } = null!; - - private static Vector2 cardSize => new Vector2(950, 550); - - private readonly Bindable cornerPieceVisibility = new Bindable(); - private readonly Bindable scoreBarProgress = new Bindable(); - - private PanelScaffold panelScaffold = null!; - private Box flash = null!; - private ScoreDetails playerScoreDetails = null!; - private ScoreDetails opponentScoreDetails = null!; - private RankedPlayScoreCounter playerScoreCounter = null!; - private RankedPlayScoreCounter opponentScoreCounter = null!; - private RankedPlayScoreCounter damageCounter = null!; - private OsuSpriteText flyingDamageText = null!; - private ScoreBar playerScoreBar = null!; - private ScoreBar opponentScoreBar = null!; - private OsuSpriteText roundNumber = null!; - private RankedPlayUserDisplay playerUserDisplay = null!; - private RankedPlayUserDisplay opponentUserDisplay = null!; - - private RankedPlayDamageInfo losingDamageInfo = null!; - - private Sample resultsAppearSample = null!; - private Sample dmgFlySample = null!; - private Sample dmgHitSample = null!; - private Sample hpDownSample = null!; - private Sample playerAppearSample = null!; - private Sample pseudoScoreCounterSample = null!; - private Sample scoreTickSample = null!; - private Sample gradePassSample = null!; - private Sample gradePassSsSample = null!; - private Sample gradeFailSample = null!; - private Sample gradeFailDSample = null!; - private SampleChannel? playerScoreTickChannel; - private SampleChannel? opponentScoreTickChannel; - private readonly BindableDouble playerScoreTickPitch = new BindableDouble(); - private readonly BindableDouble opponentScoreTickPitch = new BindableDouble(); - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - // this works under the assumption that only one player can receive damage each round - losingDamageInfo = matchInfo.RoomState.Users - .Select(it => it.Value.DamageInfo) - .OfType() - .MaxBy(it => it.Damage)!; - - AddInternal(panelScaffold = new PanelScaffold - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = - [ - new RankedPlayCornerPiece(RankedPlayColourScheme.BLUE, Anchor.BottomLeft) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - State = { BindTarget = cornerPieceVisibility }, - Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.User, Anchor.BottomLeft, RankedPlayColourScheme.BLUE) - { - RelativeSizeAxes = Axes.Both, - Health = { Value = PlayerDamageInfo.OldLife } - } - }, - new RankedPlayCornerPiece(RankedPlayColourScheme.RED, Anchor.BottomRight) - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - State = { BindTarget = cornerPieceVisibility }, - Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.User, Anchor.BottomRight, RankedPlayColourScheme.RED) - { - RelativeSizeAxes = Axes.Both, - Health = { Value = OpponentDamageInfo.OldLife } - } - }, - new Container - { - RelativeSizeAxes = Axes.X, - Height = 110, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Padding = new MarginPadding { Bottom = 30 }, - Child = roundNumber = new OsuSpriteText - { - Text = $"Round {matchInfo.CurrentRound}", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 36, weight: FontWeight.Bold, typeface: Typeface.TorusAlternate), - Alpha = 0, - }, - }, - new GridContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = cardSize, - Padding = new MarginPadding { Bottom = 110, Top = 60, Horizontal = 60 }, - ColumnDimensions = - [ - new Dimension(), - new Dimension(GridSizeMode.Absolute, 40), - new Dimension(GridSizeMode.Absolute, 60), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(GridSizeMode.Absolute, 60), - new Dimension(GridSizeMode.Absolute, 40), - new Dimension(), - ], - Content = new Drawable?[][] - { - [ - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = - [ - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - ], - Content = new Drawable[][] - { - [ - playerScoreDetails = new ScoreDetails(PlayerScore, RankedPlayColourScheme.BLUE) - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - ], - [ - playerScoreCounter = new RankedPlayScoreCounter(numDigits(PlayerScore.TotalScore)) - { - Font = OsuFont.GetFont(size: 60, fixedWidth: true), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(-4), - Alpha = 0, - AlwaysPresent = true, - } - ] - } - }, - null, - playerScoreBar = new ScoreBar(RankedPlayColourScheme.BLUE) - { - RelativeSizeAxes = Axes.Both, - Height = 0.05f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Alpha = 0, - }, - null, - opponentScoreBar = new ScoreBar(RankedPlayColourScheme.RED) - { - RelativeSizeAxes = Axes.Both, - Height = 0.05f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Alpha = 0, - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = - [ - new Dimension(), - new Dimension(GridSizeMode.AutoSize) - ], - Content = new Drawable[][] - { - [ - opponentScoreDetails = new ScoreDetails(OpponentScore, RankedPlayColourScheme.RED) - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - }, - ], - [ - opponentScoreCounter = new RankedPlayScoreCounter(numDigits(OpponentScore.TotalScore)) - { - Font = OsuFont.GetFont(size: 60, fixedWidth: true), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(-4), - Alpha = 0, - AlwaysPresent = true, - } - ] - } - }, - ] - } - }, - flash = new Box - { - RelativeSizeAxes = Axes.Both, - }, - ], - BottomOrnament = - { - Size = new Vector2(200, 60), - Alpha = 0, - Children = - [ - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = - [ - damageCounter = new RankedPlayScoreCounter(numDigits(losingDamageInfo.Damage)) - { - Font = OsuFont.GetFont(size: 36, weight: FontWeight.SemiBold, fixedWidth: true), - Spacing = new Vector2(-2), - }, - flyingDamageText = new OsuSpriteText - { - Text = FormattableString.Invariant($"{losingDamageInfo.Damage:N0}"), - Font = OsuFont.GetFont(size: 36, weight: FontWeight.SemiBold, fixedWidth: true), - Spacing = new Vector2(-2), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BypassAutoSizeAxes = Axes.Both, - Alpha = 0, - }, - new OsuSpriteText - { - BypassAutoSizeAxes = Axes.Both, - Text = $"{matchInfo.RoomState.DamageMultiplier.ToStandardFormattedString(maxDecimalDigits: 1)}x", - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 42), - Rotation = 30, - Alpha = 0, - Colour = colour.RedLight - }, - ] - }, - new OsuSpriteText - { - Text = Precision.AlmostEquals(matchInfo.RoomState.DamageMultiplier, 1) - ? "Damage" - : $"Damage {matchInfo.RoomState.DamageMultiplier.ToStandardFormattedString(maxDecimalDigits: 1)}x", - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 22), - }, - ] - } - }); - - resultsAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/results-appear"); - dmgFlySample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-fly"); - dmgHitSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-hit"); - hpDownSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/hp-down"); - playerAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/players-appear"); - pseudoScoreCounterSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/pseudo-score-counter"); - scoreTickSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/score-tick"); - gradePassSample = audio.Samples.Get(@"Results/rank-impact-pass"); - gradePassSsSample = audio.Samples.Get(@"Results/rank-impact-pass-ss"); - gradeFailSample = audio.Samples.Get(@"Results/rank-impact-fail"); - gradeFailDSample = audio.Samples.Get(@"Results/rank-impact-fail-d"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - double delay = 0; - - appear(ref delay); - - animateCountersAndScoreBars(ref delay); - - showScoreInfo(ref delay); - - updateHealthBars(ref delay); - } - - private void appear(ref double delay) - { - resultsAppearSample.Play(); - - panelScaffold.FadeIn(100) - .ResizeTo(0) - .ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo) - // deliberately cutting this delay 300ms short so the vertical resize interrupts the horizontal one - .Delay(300) - .ResizeHeightTo(cardSize.Y, 800, Easing.OutExpo); - - flash.Delay(150).FadeOut(600, Easing.Out); - - using (BeginDelayedSequence(700)) - { - roundNumber.FadeIn(600); - playerScoreCounter.FadeIn(600); - opponentScoreCounter.FadeIn(600); - - Schedule(() => - { - cornerPieceVisibility.Value = Visibility.Visible; - playerAppearSample.Play(); - }); - } - - using (BeginDelayedSequence(900)) - { - panelScaffold.BottomOrnament - .FadeIn(300) - .ResizeWidthTo(cardSize.X - 550, 600, Easing.OutExpo); - } - - delay += 1000; - } - - private void animateCountersAndScoreBars(ref double delay) - { - using (BeginDelayedSequence(delay)) - { - const double score_text_duration = 2000; - - playerScoreCounter.TransformValueTo(PlayerScore.TotalScore, score_text_duration - 500); - opponentScoreCounter.TransformValueTo(OpponentScore.TotalScore, score_text_duration - 500); - - damageCounter.TransformValueTo(losingDamageInfo.Damage, score_text_duration - 500); - - long maxAchievableScore = Math.Max( - Math.Max(PlayerScore.TotalScore, OpponentScore.TotalScore), - 1_000_000 - ); - - float playerScorePercent = (float)PlayerScore.TotalScore / maxAchievableScore; - float opponentScorePercent = (float)OpponentScore.TotalScore / maxAchievableScore; - float maxScorePercent = Math.Max(playerScorePercent, opponentScorePercent); - - playerScoreBar.FadeIn(100); - opponentScoreBar.FadeIn(100); - - playerScoreTickChannel ??= scoreTickSample.GetChannel(); - playerScoreTickChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; - playerScoreTickChannel.Frequency.BindTarget = playerScoreTickPitch; - playerScoreTickPitch.Value = 0.5f; - playerScoreTickChannel.Looping = true; - - opponentScoreTickChannel ??= scoreTickSample.GetChannel(); - opponentScoreTickChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH; - opponentScoreTickChannel.Frequency.BindTarget = opponentScoreTickPitch; - opponentScoreTickPitch.Value = 0.5f; - opponentScoreTickChannel.Looping = true; - - Schedule(() => - { - if (losingDamageInfo.Damage > 0) - pseudoScoreCounterSample.Play(); - - if (PlayerScore.TotalScore > 0) - playerScoreTickChannel.Play(); - - if (OpponentScore.TotalScore > 0) - opponentScoreTickChannel.Play(); - }); - - this.TransformBindableTo(scoreBarProgress, maxScorePercent, score_text_duration, new CubicBezierEasingFunction(easeIn: 0.4, easeOut: 1)); - this.TransformBindableTo(playerScoreTickPitch, 0.5f + playerScorePercent, score_text_duration, Easing.OutCubic); - this.TransformBindableTo(opponentScoreTickPitch, 0.5f + opponentScorePercent, score_text_duration, Easing.OutCubic); - - // safety timeout to ensure scoreTicks don't play forever - Scheduler.AddDelayed(() => - { - if (playerScoreTickChannel != null) - playerScoreTickChannel.Looping = false; - - if (opponentScoreTickChannel != null) - opponentScoreTickChannel.Looping = false; - }, score_text_duration + 500); - - scoreBarProgress.BindValueChanged(e => - { - playerScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, playerScorePercent)); - opponentScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, opponentScorePercent)); - - Schedule(() => - { - if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent) - playerScoreTickChannel.Looping = false; - - if (opponentScoreTickChannel != null && opponentScoreBar.Height >= opponentScorePercent) - opponentScoreTickChannel.Looping = false; - }); - }); - } - - delay += 2200; - } - - private void updateHealthBars(ref double delay) - { - const double text_movement_duration = 400; - - bool playerTookDamage = OpponentScore.TotalScore > PlayerScore.TotalScore; - double loserPanDirection = playerTookDamage ? -OsuGameBase.SFX_STEREO_STRENGTH : OsuGameBase.SFX_STEREO_STRENGTH; - - using (BeginDelayedSequence(delay)) - { - Schedule(() => - { - RankedPlayUserDisplay userDisplay = - PlayerScore.TotalScore > OpponentScore.TotalScore - ? opponentUserDisplay - : playerUserDisplay; - - Vector2 screenSpacePosition = userDisplay.HealthDisplay.ScreenSpaceImpactPosition; - - var position = flyingDamageText.Parent!.ToLocalSpace(screenSpacePosition) - flyingDamageText.AnchorPosition; - - damageCounter.FadeOut() - .Delay(200) - .FadeIn(300) - .ScaleTo(0.9f) - .ScaleTo(1f, 300, Easing.OutElasticHalf); - - var dmgFlyChannel = dmgFlySample.GetChannel(); - this.TransformBindableTo(dmgFlyChannel.Balance, loserPanDirection, text_movement_duration, Easing.InCubic); - dmgFlyChannel.Play(); - - flyingDamageText.FadeIn() - .MoveTo(position, text_movement_duration, Easing.InCubic) - .ScaleTo(0.75f, text_movement_duration, new CubicBezierEasingFunction(easeIn: 0.35, easeOut: 0.5)) - .RotateTo(12 * Math.Sign(position.X), text_movement_duration, new CubicBezierEasingFunction(easeIn: 0.35, easeOut: 0.5)) - .Then() - .FadeOut(); - - Scheduler.AddDelayed(() => - { - var dmgHitChannel = dmgHitSample.GetChannel(); - dmgHitChannel.Balance.Value = loserPanDirection; - dmgHitChannel.Play(); - - userDisplay.Shake(shakeDuration: 60, shakeMagnitude: 2, maximumLength: 120); - - for (int i = 0; i < 10; i++) - { - var particle = new DamageParticle - { - Size = new Vector2(RNG.NextSingle(5, 15)), - Origin = Anchor.Centre, - Position = ToLocalSpace(screenSpacePosition), - Rotation = RNG.NextSingle(0, 360), - Blending = BlendingParameters.Additive, - }; - - AddInternal(particle); - - particle.FadeOut(600) - .ScaleTo(0, 600) - .RotateTo(particle.Rotation + RNG.NextSingle(-20, 20), 600) - .FadeColour(Color4.Red, 600) - .Expire(); - } - }, text_movement_duration); - }); - } - - delay += text_movement_duration; - - using (BeginDelayedSequence(delay)) - { - Schedule(() => - { - playerUserDisplay.Health.Value = PlayerDamageInfo.NewLife; - opponentUserDisplay.Health.Value = OpponentDamageInfo.NewLife; - - Scheduler.AddDelayed(() => - { - var hpDecreaseChannel = hpDownSample.GetChannel(); - hpDecreaseChannel.Balance.Value = loserPanDirection; - hpDecreaseChannel.Play(); - }, 900); - }); - } - - delay += 400; - } - - private void showScoreInfo(ref double delay) - { - using (BeginDelayedSequence(delay)) - { - playerScoreDetails.FadeIn(300); - opponentScoreDetails.FadeIn(300); - - Schedule(() => - { - SampleChannel playerRankChannel = getRankSample(PlayerScore.Rank).GetChannel(); - playerRankChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH; - playerRankChannel.Play(); - - SampleChannel opponentRankChannel = getRankSample(OpponentScore.Rank).GetChannel(); - opponentRankChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH; - opponentRankChannel.Play(); - }); - } - - delay += 800; - } - - private Sample getRankSample(ScoreRank rank) - { - switch (rank) - { - default: - case ScoreRank.D: - return gradeFailDSample; - - case ScoreRank.C: - case ScoreRank.B: - return gradeFailSample; - - case ScoreRank.A: - case ScoreRank.S: - case ScoreRank.SH: - return gradePassSample; - - case ScoreRank.X: - case ScoreRank.XH: - return gradePassSsSample; - } - } - - private static int numDigits(long value) - { - if (value <= 0) - return 1; - - return (int)Math.Floor(Math.Log10(value)) + 1; - } - - private partial class DamageParticle : Triangle - { - private Vector2 velocity = new Vector2(RNG.NextSingle(-0.3f, 0.3f), RNG.NextSingle(-0.3f, 0.3f)); - - private Vector2 gravity => new Vector2(0, 0.0002f); - - protected override void Update() - { - base.Update(); - - velocity += gravity * (float)Time.Elapsed; - Position += velocity * (float)Time.Elapsed; - } - } - } } }