diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs index f7cc9885c79a..de784157e9b4 100644 --- a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; using osu.Game.Tests.Visual.Multiplayer; @@ -20,11 +22,19 @@ public partial class TestSceneRankedPlayUserDisplay : MultiplayerTestScene Value = 1_000_000, }; + public TestSceneRankedPlayUserDisplay() + { + AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value); + } + public override void SetUpSteps() { base.SetUpSteps(); - AddStep("add display", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue) + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay))); + WaitForJoined(); + + AddStep("add display", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Blue) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -36,7 +46,7 @@ public override void SetUpSteps() [Test] public void TesUserDisplay() { - AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue) + AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Blue) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -44,15 +54,30 @@ public void TesUserDisplay() Health = { BindTarget = health } }); - AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Red) + AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Red) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(256, 72), Health = { BindTarget = health } }); + } - AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value); + [Test] + public void TestBeatmapState() + { + float progress = 0; + + AddStep("set unavailable", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); + AddStep("set downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress = 0))); + AddUntilStep("increment progress", () => + { + progress += RNG.NextSingle(0.1f); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress)); + return progress >= 1; + }); + AddStep("set to importing", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); + AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs index 0195c47945f4..aa5b303aad49 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -17,7 +18,10 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; @@ -42,6 +46,13 @@ public partial class RankedPlayUserDisplay : CompositeDrawable private BufferedContainer grayScaleContainer = null!; + private OsuSpriteText beatmapState = null!; + + private BeatmapAvailability availability = BeatmapAvailability.Unknown(); + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + [Resolved] private RankedPlayCornerPiece? cornerPiece { get; set; } @@ -61,6 +72,10 @@ private void load() ? -OsuGame.SHEAR : OsuGame.SHEAR; + var beatmapStateAnchor = (contentAnchor & Anchor.x0) != 0 + ? Anchor.CentreLeft + : Anchor.CentreRight; + InternalChildren = [ new CircularContainer @@ -103,15 +118,33 @@ private void load() Anchor = contentAnchor, Origin = contentAnchor, }, - new OsuSpriteText + new FillFlowContainer { - Name = "Username", - Text = user.Username, + Name = "Username/beatmap state container", + AutoSizeAxes = Axes.Both, Anchor = contentAnchor, Origin = contentAnchor, + Direction = FillDirection.Horizontal, Padding = new MarginPadding { Horizontal = 4, Vertical = 6 }, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold), - UseFullGlyphHeight = false, + Spacing = new Vector2(5, 0), + Children = + [ + new OsuSpriteText + { + Name = "Username", + Text = user.Username, + Anchor = contentAnchor, + Origin = contentAnchor, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold), + UseFullGlyphHeight = false, + }, + beatmapState = new OsuSpriteText + { + Anchor = beatmapStateAnchor, + Origin = beatmapStateAnchor, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }, + ], }, ] } @@ -129,6 +162,46 @@ protected override void LoadComplete() grayScaleContainer.GrayscaleTo(e.NewValue <= 0 ? 1 : 0, 300); cornerPiece?.OnHealthChanged(e.NewValue); }); + + client.RoomUpdated += onRoomUpdated; + } + + private void onRoomUpdated() + { + var user = client.Room?.Users.SingleOrDefault(u => u.UserID == userId); + + if (user == null || availability == user.BeatmapAvailability) + return; + + availability = user.BeatmapAvailability; + + if (availability.State is DownloadState.NotDownloaded or DownloadState.Downloading or DownloadState.Importing) + beatmapState.FadeIn(50); + else + beatmapState.FadeOut(50); + + switch (availability.State) + { + case DownloadState.NotDownloaded: + beatmapState.Text = "Missing Beatmap"; + break; + + case DownloadState.Downloading: + double progress = Math.Clamp(availability.DownloadProgress ?? 0, 0, 1); + beatmapState.Text = $"Downloading... ({progress:P0})"; + break; + + case DownloadState.Importing: + beatmapState.Text = "Importing..."; + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + client.RoomUpdated -= onRoomUpdated; } public partial class HealthBar : CompositeDrawable diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs index b76f8a7944fa..746492de712d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/EndedScreen.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -36,8 +38,12 @@ public partial class EndedScreen : RankedPlaySubScreen private OsuTextFlowContainer localRatingText = null!; private OsuTextFlowContainer opponentRatingText = null!; + private Sample winSample = null!; + private Sample loseSample = null!; + private Sample drawSample = null!; + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { CenterColumn.Child = new FillFlowContainer { @@ -172,6 +178,10 @@ private void load(OsuColour colours) } }; + winSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/win"); + loseSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/lose"); + drawSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/draw"); + RankedPlayUserInfo localUser = matchInfo.RoomState.Users[Client.LocalUser!.UserID]; RankedPlayUserInfo otherUser = matchInfo.RoomState.Users.Values.Single(u => u != localUser); @@ -179,16 +189,19 @@ private void load(OsuColour colours) { titleText.Text = "DRAW"; titleText.Colour = titleSeparator.Colour = colours.Orange1; + drawSample.Play(); } else if (matchInfo.RoomState.WinningUserId == Client.LocalUser!.UserID) { titleText.Text = "VICTORY"; titleText.Colour = titleSeparator.Colour = colours.Green1; + winSample.Play(); } else { titleText.Text = "DEFEAT"; titleText.Colour = titleSeparator.Colour = colours.Red1; + loseSample.Play(); } localRatingText.AddText("Your Rating: ", s => s.Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular)); diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs index 129ffc61dc7d..5f7298d5167f 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.cs @@ -6,6 +6,8 @@ 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; @@ -205,8 +207,24 @@ private partial class ResultScreenContent : CompositeDrawable 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() + private void load(AudioManager audio) { // this works under the assumption that only one player can receive damage each round losingDamageInfo = matchInfo.RoomState.Users @@ -417,6 +435,18 @@ private void load() ] } }); + + 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() @@ -436,6 +466,8 @@ protected override void LoadComplete() private void appear(ref double delay) { + resultsAppearSample.Play(); + panelScaffold.FadeIn(100) .ResizeTo(0) .ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo) @@ -451,7 +483,11 @@ private void appear(ref double delay) playerScoreCounter.FadeIn(600); opponentScoreCounter.FadeIn(600); - Schedule(() => cornerPieceVisibility.Value = Visibility.Visible); + Schedule(() => + { + cornerPieceVisibility.Value = Visibility.Visible; + playerAppearSample.Play(); + }); } using (BeginDelayedSequence(900)) @@ -487,12 +523,57 @@ private void animateCountersAndScoreBars(ref double delay) 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; + }); }); } @@ -503,6 +584,9 @@ 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(() => @@ -522,6 +606,10 @@ private void updateHealthBars(ref double delay) .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)) @@ -531,6 +619,10 @@ private void updateHealthBars(ref double delay) 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++) @@ -564,6 +656,13 @@ private void updateHealthBars(ref double delay) { playerUserDisplay.Health.Value = PlayerDamageInfo.NewLife; opponentUserDisplay.Health.Value = OpponentDamageInfo.NewLife; + + Scheduler.AddDelayed(() => + { + var hpDecreaseChannel = hpDownSample.GetChannel(); + hpDecreaseChannel.Balance.Value = loserPanDirection; + hpDecreaseChannel.Play(); + }, 900); }); } @@ -576,11 +675,45 @@ private void showScoreInfo(ref double 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) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0755aeca145e..4c680a5d4a57 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -40,7 +40,7 @@ - +