// 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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { /// /// The component that displays the player's accuracy on the results screen. /// public class AccuracyCircle : CompositeDrawable { /// /// Duration for the transforms causing this component to appear. /// public const double APPEAR_DURATION = 200; /// /// Delay before the accuracy circle starts filling. /// public const double ACCURACY_TRANSFORM_DELAY = 450; /// /// Duration for the accuracy circle fill. /// public const double ACCURACY_TRANSFORM_DURATION = 3000; /// /// Delay after for the rank text (A/B/C/D/S/SS) to appear. /// public const double TEXT_APPEAR_DELAY = ACCURACY_TRANSFORM_DURATION / 2; /// /// Delay before the rank circles start filling. /// public const double RANK_CIRCLE_TRANSFORM_DELAY = 150; /// /// Duration for the rank circle fills. /// public const double RANK_CIRCLE_TRANSFORM_DURATION = 800; /// /// Relative width of the rank circles. /// public const float RANK_CIRCLE_RADIUS = 0.06f; /// /// Relative width of the circle showing the accuracy. /// private const float accuracy_circle_radius = 0.2f; /// /// SS is displayed as a 1% region, otherwise it would be invisible. /// private const double virtual_ss_percentage = 0.01; /// /// The easing for the circle filling transforms. /// public static readonly Easing ACCURACY_TRANSFORM_EASING = Easing.OutPow10; private readonly ScoreInfo score; private readonly bool withFlair; private SmoothCircularProgress accuracyCircle; private SmoothCircularProgress innerMask; private Container badges; private RankText rankText; private SkinnableSound applauseSound; public AccuracyCircle(ScoreInfo score, bool withFlair) { this.score = score; this.withFlair = withFlair; } [BackgroundDependencyLoader] private void load(AudioManager audio) { InternalChildren = new Drawable[] { new SmoothCircularProgress { Name = "Background circle", Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(47), Alpha = 0.5f, InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle Current = { Value = 1 }, }, accuracyCircle = new SmoothCircularProgress { Name = "Accuracy circle", Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#7CF6FF"), Color4Extensions.FromHex("#BAFFA9")), InnerRadius = accuracy_circle_radius, }, new BufferedContainer { Name = "Graded circles", Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Size = new Vector2(0.8f), Padding = new MarginPadding(2), Children = new Drawable[] { new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.X), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 1 } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.S), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 1 - virtual_ss_percentage } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.A), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.95f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.B), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.9f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.C), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.8f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, Colour = OsuColour.ForRank(ScoreRank.D), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.7f } }, new RankNotch(0), new RankNotch((float)(1 - virtual_ss_percentage)), new RankNotch(0.95f), new RankNotch(0.9f), new RankNotch(0.8f), new RankNotch(0.7f), new BufferedContainer { Name = "Graded circle mask", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(1), Blending = new BlendingParameters { Source = BlendingType.DstColor, Destination = BlendingType.OneMinusSrcAlpha, SourceAlpha = BlendingType.One, DestinationAlpha = BlendingType.SrcAlpha }, Child = innerMask = new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, InnerRadius = RANK_CIRCLE_RADIUS - 0.01f, } } } }, badges = new Container { Name = "Rank badges", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, Children = new[] { new RankBadge(1f, getRank(ScoreRank.X)), new RankBadge(0.95f, getRank(ScoreRank.S)), new RankBadge(0.9f, getRank(ScoreRank.A)), new RankBadge(0.8f, getRank(ScoreRank.B)), new RankBadge(0.7f, getRank(ScoreRank.C)), new RankBadge(0.35f, getRank(ScoreRank.D)), } }, rankText = new RankText(score.Rank) }; if (withFlair) { AddInternal(applauseSound = score.Rank >= ScoreRank.A ? new SkinnableSound(new SampleInfo("Results/rankpass", "applause")) : new SkinnableSound(new SampleInfo("Results/rankfail"))); } } private ScoreRank getRank(ScoreRank rank) { foreach (var mod in score.Mods.OfType()) rank = mod.AdjustRank(rank, score.Accuracy); return rank; } protected override void LoadComplete() { base.LoadComplete(); this.ScaleTo(0).Then().ScaleTo(1, APPEAR_DURATION, Easing.OutQuint); using (BeginDelayedSequence(RANK_CIRCLE_TRANSFORM_DELAY, true)) innerMask.FillTo(1f, RANK_CIRCLE_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY, true)) { double targetAccuracy = score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH ? 1 : Math.Min(1 - virtual_ss_percentage, score.Accuracy); accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); foreach (var badge in badges) { if (badge.Accuracy > score.Accuracy) continue; using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) { badge.Appear(); } } using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true)) { this.Delay(-1440).Schedule(() => applauseSound?.Play()); rankText.Appear(); } } } private double inverseEasing(Easing easing, double targetValue) { double test = 0; double result = 0; int count = 2; while (Math.Abs(result - targetValue) > 0.005) { int dir = Math.Sign(targetValue - result); test += dir * 1.0 / count; result = Interpolation.ApplyEasing(easing, test); count++; } return test; } } }