Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.SpreadDisplay.cs
4610 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;

namespace osu.Game.Screens.SelectV2
{
    public partial class PanelBeatmapStandalone
    {
        public partial class SpreadDisplay : OsuAnimatedButton
        {
            public Bindable<BeatmapInfo?> Beatmap { get; } = new Bindable<BeatmapInfo?>();
            public Bindable<StarDifficulty> StarDifficulty { get; } = new Bindable<StarDifficulty>();

            protected override Colour4 DimColour => Colour4.White;

            private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
            private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();

            private const double transition_duration = 200;

            [Resolved]
            private Bindable<RulesetInfo> ruleset { get; set; } = null!;

            [Resolved]
            private OsuColour colours { get; set; } = null!;

            private FillFlowContainer preceding = null!;
            public Circle Current { get; private set; } = null!;
            private FillFlowContainer succeeding = null!;

            private OsuSpriteText countText = null!;
            private SpriteIcon icon = null!;

            public SpreadDisplay()
            {
                AutoSizeAxes = Axes.X;
                RelativeSizeAxes = Axes.Y;
                Content.CornerRadius = 5;

                Action = () =>
                {
                    if (Beatmap.Value != null)
                        scopedBeatmapSet.Value = Beatmap.Value.BeatmapSet!;
                };
            }

            [BackgroundDependencyLoader]
            private void load(ISongSelect? songSelect, OsuConfigManager configManager)
            {
                Add(new FillFlowContainer
                {
                    AutoSizeAxes = Axes.Both,
                    Anchor = Anchor.CentreLeft,
                    Origin = Anchor.CentreLeft,
                    Direction = FillDirection.Horizontal,
                    Spacing = new Vector2(5),
                    Padding = new MarginPadding { Horizontal = 5 },
                    Children = new Drawable[]
                    {
                        new FillFlowContainer
                        {
                            AutoSizeAxes = Axes.Both,
                            Direction = FillDirection.Horizontal,
                            Spacing = new Vector2(2),
                            Children = new Drawable[]
                            {
                                preceding = new FillFlowContainer
                                {
                                    Anchor = Anchor.CentreLeft,
                                    Origin = Anchor.CentreLeft,
                                    AutoSizeAxes = Axes.Both,
                                    Direction = FillDirection.Horizontal,
                                    Spacing = new Vector2(1),
                                    Alpha = 0.5f,
                                },
                                Current = new Circle
                                {
                                    Size = new Vector2(7, 12),
                                    Anchor = Anchor.CentreLeft,
                                    Origin = Anchor.CentreLeft,
                                },
                                succeeding = new FillFlowContainer
                                {
                                    Anchor = Anchor.CentreLeft,
                                    Origin = Anchor.CentreLeft,
                                    AutoSizeAxes = Axes.Both,
                                    Direction = FillDirection.Horizontal,
                                    Spacing = new Vector2(1),
                                    Alpha = 0.5f,
                                }
                            }
                        },
                        countText = new OsuSpriteText
                        {
                            Anchor = Anchor.CentreLeft,
                            Origin = Anchor.CentreLeft,
                            Font = OsuFont.Style.Caption2,
                        },
                        icon = new SpriteIcon
                        {
                            Size = new Vector2(12),
                            Anchor = Anchor.CentreLeft,
                            Origin = Anchor.CentreLeft,
                            Icon = FontAwesome.Solid.Eye,
                            Alpha = 0,
                        }
                    }
                });

                if (songSelect != null)
                    scopedBeatmapSet.BindTo(songSelect.ScopedBeatmapSet);

                configManager.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmaps);
            }

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

                Beatmap.BindValueChanged(_ => updateBeatmap());
                StarDifficulty.BindValueChanged(_ => updateBeatmap());
                showConvertedBeatmaps.BindValueChanged(_ => updateBeatmap());
                scopedBeatmapSet.BindValueChanged(_ => updateBeatmap(), true);
                Enabled.BindValueChanged(_ => updateAppearance(), true);
                FinishTransforms(true);
            }

            private void updateBeatmap()
            {
                if (Beatmap.Value == null || scopedBeatmapSet.Value != null)
                {
                    this.FadeOut(transition_duration, Easing.OutQuint);
                    return;
                }

                preceding.Clear();
                succeeding.Clear();

                var otherStarDifficulties = Beatmap.Value.BeatmapSet!.Beatmaps
                                                   .Except([Beatmap.Value])
                                                   .Where(b => b.AllowGameplayWithRuleset(ruleset.Value, showConvertedBeatmaps.Value))
                                                   .OrderBy(b => b.StarRating)
                                                   .Select(b => b.StarRating)
                                                   .ToList();
                this.FadeTo(otherStarDifficulties.Count > 0 ? 1 : 0, transition_duration, Easing.OutQuint);

                if (otherStarDifficulties.Count == 0)
                    return;

                const int max_difficulties_total = 11;

                int startIndex;
                int endIndex;

                if (otherStarDifficulties.Count <= max_difficulties_total)
                {
                    startIndex = 0;
                    endIndex = otherStarDifficulties.Count - 1;
                }
                else
                {
                    startIndex = otherStarDifficulties.BinarySearch(StarDifficulty.Value.Stars);
                    if (startIndex < 0)
                        startIndex = ~startIndex - 1;

                    startIndex = Math.Clamp(startIndex - max_difficulties_total / 2, 0, otherStarDifficulties.Count - 1);
                    endIndex = Math.Clamp(startIndex + max_difficulties_total, 0, otherStarDifficulties.Count - 1);
                }

                for (int i = startIndex; i <= endIndex; i++)
                {
                    double otherStarDifficulty = otherStarDifficulties[i];
                    var target = otherStarDifficulty < StarDifficulty.Value.Stars ? preceding : succeeding;

                    var circle = new Circle
                    {
                        Size = new Vector2(5, 10),
                        Anchor = Anchor.CentreLeft,
                        Origin = Anchor.CentreLeft,
                        Colour = colours.ForStarDifficulty(otherStarDifficulty)
                    };
                    target.Add(circle);
                    target.SetLayoutPosition(circle, (float)otherStarDifficulty);
                }

                int countNotShown = otherStarDifficulties.Count - (preceding.Count + succeeding.Count);
                countText.Alpha = countNotShown > 0 ? 1 : 0;
                countText.Text = $@"+{countNotShown}";

                if (startIndex > 0)
                {
                    for (int i = 0; i < preceding.Count; ++i)
                    {
                        var dot = preceding[i];
                        dot.Alpha = (1 + 4 * (float)(i + 1) / preceding.Count) / 5;
                    }
                }

                if (endIndex < otherStarDifficulties.Count - 1)
                {
                    for (int i = 0; i < succeeding.Count; ++i)
                    {
                        var dot = succeeding[i];
                        dot.Alpha = (1 + 4 * (float)(succeeding.Count - i) / succeeding.Count) / 5;
                    }
                }
            }

            protected override bool OnMouseDown(MouseDownEvent e)
            {
                if (!Enabled.Value)
                    return false;

                base.OnMouseDown(e);
                return true;
            }

            protected override bool OnClick(ClickEvent e)
            {
                if (!Enabled.Value)
                    return false;

                return base.OnClick(e);
            }

            protected override bool OnHover(HoverEvent e)
            {
                updateAppearance();
                return base.OnHover(e);
            }

            protected override void OnHoverLost(HoverLostEvent e)
            {
                updateAppearance();
                base.OnHoverLost(e);
            }

            private void updateAppearance()
            {
                bool isInteractable = Enabled.Value && IsHovered;

                HoverColour = isInteractable ? Colour4.White.Opacity(0.1f) : Colour4.Transparent;
                preceding.FadeTo(isInteractable ? 1 : 0.5f, transition_duration, Easing.OutQuint);
                succeeding.FadeTo(isInteractable ? 1 : 0.5f, transition_duration, Easing.OutQuint);
                icon.FadeTo(isInteractable ? 1 : 0, transition_duration, Easing.OutQuint);
            }
        }
    }
}