Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs
4916 views
// Copyright (c) ppy Pty Ltd <[email protected]>. 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;

namespace osu.Game.Screens.Play.HUD
{
    public partial class DrawableGameplayLeaderboard : CompositeDrawable, ISerialisableDrawable
    {
        protected readonly FillFlowContainer<DrawableGameplayLeaderboardScore> Flow;

        private bool requiresScroll;
        private readonly OsuScrollContainer scroll;

        public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; }

        public bool AlwaysShown { get; init; }

        [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CollapseDuringGameplay), nameof(SkinnableComponentStrings.CollapseDuringGameplayDescription))]
        public Bindable<bool> CollapseDuringGameplay { get; } = new BindableBool(true);

        private readonly Bindable<bool> expanded = new BindableBool();

        [Resolved]
        private Player? player { get; set; }

        [Resolved]
        private IGameplayLeaderboardProvider leaderboardProvider { get; set; } = null!;

        private readonly IBindableList<GameplayLeaderboardScore> scores = new BindableList<GameplayLeaderboardScore>();
        private readonly Bindable<bool> configVisibility = new Bindable<bool>();
        private readonly IBindable<LocalUserPlayingState> userPlayingState = new Bindable<LocalUserPlayingState>();
        private readonly IBindable<bool> holdingForHUD = new Bindable<bool>();

        /// <summary>
        /// Create a new leaderboard.
        /// </summary>
        public DrawableGameplayLeaderboard()
        {
            // Extra lenience is applied so the scores don't get cut off from the left due to elastic easing transforms.
            float xOffset = DrawableGameplayLeaderboardScore.SHEAR_WIDTH + DrawableGameplayLeaderboardScore.ELASTIC_WIDTH_LENIENCE;

            Width = 260 + xOffset;
            Height = 300;

            InternalChildren = new Drawable[]
            {
                scroll = new InputDisabledScrollContainer
                {
                    ClampExtension = 0,
                    RelativeSizeAxes = Axes.Both,
                    Child = Flow = new FillFlowContainer<DrawableGameplayLeaderboardScore>
                    {
                        Alpha = 0f,
                        RelativeSizeAxes = Axes.X,
                        X = xOffset,
                        AutoSizeAxes = Axes.Y,
                        Direction = FillDirection.Vertical,
                        Spacing = new Vector2(2.5f),
                        LayoutDuration = 450,
                        LayoutEasing = Easing.OutQuint,
                    }
                }
            };
        }

        [BackgroundDependencyLoader]
        private void load(OsuConfigManager config, GameplayState? gameplayState, HUDOverlay? hudOverlay)
        {
            config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);

            if (gameplayState != null)
                userPlayingState.BindTo(gameplayState.PlayingState);

            if (hudOverlay != null)
                holdingForHUD.BindTo(hudOverlay.HoldingForHUD);
        }

        protected override void LoadComplete()
        {
            base.LoadComplete();

            scores.BindTo(leaderboardProvider.Scores);
            scores.BindCollectionChanged((_, _) =>
            {
                Clear();
                foreach (var score in scores)
                    Add(score);
            }, true);

            configVisibility.BindValueChanged(_ => Scheduler.AddOnce(updateState));
            userPlayingState.BindValueChanged(_ => Scheduler.AddOnce(updateState));
            holdingForHUD.BindValueChanged(_ => Scheduler.AddOnce(updateState));
            CollapseDuringGameplay.BindValueChanged(_ => Scheduler.AddOnce(updateState));
            updateState();
        }

        private void updateState()
        {
            // prevents weird delay in the flow correctly appearing when toggling the leaderboard on.
            if (Flow.Alpha < 1)
                scroll.ScrollToStart(false);

            Flow.FadeTo(player?.Configuration.ShowLeaderboard != false && (configVisibility.Value || AlwaysShown) ? 1 : 0, 100, Easing.OutQuint);
            expanded.Value = !CollapseDuringGameplay.Value || userPlayingState.Value != LocalUserPlayingState.Playing || holdingForHUD.Value;
        }

        /// <summary>
        /// Adds a player to the leaderboard.
        /// </summary>
        public void Add(GameplayLeaderboardScore score)
        {
            var drawable = CreateLeaderboardScoreDrawable(score);

            if (score.Tracked)
            {
                if (TrackedScore != null)
                    throw new InvalidOperationException("Cannot track more than one score.");

                TrackedScore = drawable;
            }

            drawable.Expanded.BindTo(expanded);

            Flow.Add(drawable);
            drawable.ScorePosition.BindValueChanged(_ => Scheduler.AddOnce(sort));
            drawable.DisplayOrder.BindValueChanged(_ => Scheduler.AddOnce(sort), true);
        }

        public void Clear()
        {
            Flow.Clear();
            TrackedScore = null;
            scroll.ScrollToStart(false);
        }

        protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(GameplayLeaderboardScore score) =>
            new DrawableGameplayLeaderboardScore(score);

        protected override void Update()
        {
            base.Update();

            // limit leaderboard dimensions to a sane minimum.
            Width = Math.Max(Width, Flow.X + DrawableGameplayLeaderboardScore.MIN_WIDTH);
            Height = Math.Max(Height, DrawableGameplayLeaderboardScore.PANEL_HEIGHT);

            requiresScroll = Flow.DrawHeight > Height;

            if (requiresScroll && TrackedScore != null)
            {
                double scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;

                scroll.ScrollTo(scrollTarget);
            }

            const float panel_height = DrawableGameplayLeaderboardScore.PANEL_HEIGHT;

            float fadeBottom = (float)(scroll.Current + scroll.DrawHeight);
            float fadeTop = (float)(scroll.Current + panel_height);

            if (scroll.IsScrolledToStart()) fadeTop -= panel_height;
            if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;

            // logic is mostly shared with Leaderboard, copied here for simplicity.
            foreach (var c in Flow)
            {
                float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y;
                float bottomY = topY + panel_height;

                bool requireTopFade = requiresScroll && topY <= fadeTop;
                bool requireBottomFade = requiresScroll && bottomY >= fadeBottom;

                if (!requireTopFade && !requireBottomFade)
                    c.Colour = Color4.White;
                else if (topY > fadeBottom + panel_height || bottomY < fadeTop - panel_height)
                    c.Colour = Color4.Transparent;
                else
                {
                    if (requireBottomFade)
                    {
                        c.Colour = ColourInfo.GradientVertical(
                            Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / panel_height, 1)),
                            Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / panel_height, 1)));
                    }
                    else if (requiresScroll)
                    {
                        c.Colour = ColourInfo.GradientVertical(
                            Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / panel_height, 1)),
                            Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / panel_height, 1)));
                    }
                }
            }
        }

        private void sort()
        {
            foreach (var score in Flow.ToArray())
                Flow.SetLayoutPosition(score, score.DisplayOrder.Value);
        }

        private partial class InputDisabledScrollContainer : OsuScrollContainer
        {
            public InputDisabledScrollContainer()
            {
                ScrollbarVisible = false;
            }

            public override bool HandlePositionalInput => false;
            public override bool HandleNonPositionalInput => false;
        }

        public bool UsesFixedAnchor { get; set; }
    }
}