1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 19:54:15 +08:00
Files
osu-lazer/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/ResultsScreen.MainPanel.cs
T
Dan Balasescu 7c114cd69a Update CodeFileSanity (#37293)
Has been failing on master for some time now because .NET 6 is not
supported anymore.
2026-04-15 01:10:54 +09:00

577 lines
28 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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<Visibility> cornerPieceVisibility = new Bindable<Visibility>();
private readonly Bindable<float> scoreBarProgress = new Bindable<float>();
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<RankedPlayDamageInfo>()
.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;
}
}
}
}
}