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; - } - } - } } }