diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs b/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs new file mode 100644 index 0000000000..4a59ad9534 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Screens.Select; +using System.Linq; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseBeatmapDetails : TestCase + { + public override string Description => "BeatmapDetails tab of BeatmapDetailArea"; + + private BeatmapDetails details; + + public override void Reset() + { + base.Reset(); + + Add(details = new BeatmapDetails + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(150), + Beatmap = new BeatmapInfo + { + Version = "VisualTest", + Metadata = new BeatmapMetadata + { + Source = "Some guy", + Tags = "beatmap metadata example with a very very long list of tags and not much creativity", + }, + Difficulty = new BeatmapDifficulty + { + CircleSize = 7, + ApproachRate = 3.5f, + OverallDifficulty = 5.7f, + DrainRate = 1, + }, + StarDifficulty = 5.3f, + Metrics = new BeatmapMetrics + { + Ratings = Enumerable.Range(0,10), + Fails = Enumerable.Range(lastRange, 100).Select(i => i % 12 - 6), + Retries = Enumerable.Range(lastRange - 3, 100).Select(i => i % 12 - 6), + }, + }, + }); + + AddRepeatStep("fail values", newRetryAndFailValues, 10); + } + + private int lastRange = 1; + + private void newRetryAndFailValues() + { + details.Beatmap.Metrics.Fails = Enumerable.Range(lastRange, 100).Select(i => i % 12 - 6); + details.Beatmap.Metrics.Retries = Enumerable.Range(lastRange - 3, 100).Select(i => i % 12 - 6); + details.Beatmap = details.Beatmap; + lastRange += 100; + } + } +} \ No newline at end of file diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGraph.cs b/osu.Desktop.VisualTests/Tests/TestCaseGraph.cs new file mode 100644 index 0000000000..7ac795f6f9 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseGraph.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using System.Linq; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseGraph : TestCase + { + public override string Description => "graph"; + + private BarGraph graph; + + public override void Reset() + { + base.Reset(); + + Children = new[] + { + graph = new BarGraph + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f), + }, + }; + + AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1,10).Select(i => (float)i)); + AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i)); + AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i)); + AddStep("Bottom to top", () => graph.Direction = BarDirection.BottomToTop); + AddStep("Top to bottom", () => graph.Direction = BarDirection.TopToBottom); + AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight); + AddStep("Right to left", () => graph.Direction = BarDirection.RightToLeft); + } + } +} \ No newline at end of file diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index f0c137578a..ffafa219a8 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -185,8 +185,10 @@ + + diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Database/BeatmapInfo.cs index 798540bdd8..3e84825919 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Database/BeatmapInfo.cs @@ -41,6 +41,9 @@ namespace osu.Game.Database [OneToOne(CascadeOperations = CascadeOperation.All)] public BeatmapDifficulty Difficulty { get; set; } + [Ignore] + public BeatmapMetrics Metrics { get; set; } + public string Path { get; set; } [JsonProperty("file_md5")] diff --git a/osu.Game/Database/BeatmapMetrics.cs b/osu.Game/Database/BeatmapMetrics.cs new file mode 100644 index 0000000000..91320110d0 --- /dev/null +++ b/osu.Game/Database/BeatmapMetrics.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Game.Database +{ + /// + /// Beatmap metrics based on acculumated online data from community plays. + /// + public class BeatmapMetrics + { + /// + /// Total vote counts of user ratings on a scale of 0..length. + /// + public IEnumerable Ratings { get; set; } + + /// + /// Points of failure on a relative time scale (usually 0..100). + /// + public IEnumerable Fails { get; set; } + + /// + /// Points of retry on a relative time scale (usually 0..100). + /// + public IEnumerable Retries { get; set; } + } +} diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs new file mode 100644 index 0000000000..76b75f1084 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/Bar.cs @@ -0,0 +1,137 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using System; + +namespace osu.Game.Graphics.UserInterface +{ + public class Bar : Container, IHasAccentColour + { + private readonly Box background; + private readonly Box bar; + + private const int resize_duration = 250; + + private const EasingTypes easing = EasingTypes.InOutCubic; + + private float length; + /// + /// Length of the bar, ranges from 0 to 1 + /// + public float Length + { + get + { + return length; + } + set + { + length = MathHelper.Clamp(value, 0, 1); + updateBarLength(); + } + } + + public Color4 BackgroundColour + { + get + { + return background.Colour; + } + set + { + background.Colour = value; + } + } + + public Color4 AccentColour + { + get + { + return bar.Colour; + } + set + { + bar.Colour = value; + } + } + + private BarDirection direction = BarDirection.LeftToRight; + public BarDirection Direction + { + get + { + return direction; + } + set + { + direction = value; + updateBarLength(); + } + } + + public Bar() + { + Children = new[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0,0,0,0) + }, + bar = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0, + }, + }; + } + + private void updateBarLength() + { + switch (direction) + { + case BarDirection.LeftToRight: + case BarDirection.RightToLeft: + bar.ResizeTo(new Vector2(length, 1), resize_duration, easing); + break; + + case BarDirection.TopToBottom: + case BarDirection.BottomToTop: + bar.ResizeTo(new Vector2(1, length), resize_duration, easing); + break; + } + + switch (direction) + { + case BarDirection.LeftToRight: + case BarDirection.TopToBottom: + bar.Anchor = Anchor.TopLeft; + bar.Origin = Anchor.TopLeft; + break; + + case BarDirection.RightToLeft: + case BarDirection.BottomToTop: + bar.Anchor = Anchor.BottomRight; + bar.Origin = Anchor.BottomRight; + break; + } + } + } + + [Flags] + public enum BarDirection + { + LeftToRight = 1 << 0, + RightToLeft = 1 << 1, + TopToBottom = 1 << 2, + BottomToTop = 1 << 3, + + Vertical = TopToBottom | BottomToTop, + Horizontal = LeftToRight | RightToLeft, + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs new file mode 100644 index 0000000000..d0965a1861 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Graphics.UserInterface +{ + public class BarGraph : FillFlowContainer + { + /// + /// Manually sets the max value, if null is instead used + /// + public float? MaxValue { get; set; } + + private BarDirection direction = BarDirection.BottomToTop; + public new BarDirection Direction + { + get + { + return direction; + } + set + { + direction = value; + base.Direction = (direction & BarDirection.Horizontal) > 0 ? FillDirection.Vertical : FillDirection.Horizontal; + foreach (var bar in Children) + { + bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / Children.Count()) : new Vector2(1.0f / Children.Count(), 1); + bar.Direction = direction; + } + } + } + + /// + /// A list of floats that defines the length of each + /// + public IEnumerable Values + { + set + { + List bars = Children.ToList(); + foreach (var bar in value.Select((length, index) => new { Value = length, Bar = bars.Count > index ? bars[index] : null })) + if (bar.Bar != null) + { + bar.Bar.Length = bar.Value / (MaxValue ?? value.Max()); + bar.Bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / value.Count()) : new Vector2(1.0f / value.Count(), 1); + } + else + Add(new Bar + { + RelativeSizeAxes = Axes.Both, + Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / value.Count()) : new Vector2(1.0f / value.Count(), 1), + Length = bar.Value / (MaxValue ?? value.Max()), + Direction = Direction, + }); + //I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards + Remove(Children.Where((bar, index) => index >= value.Count()).ToList()); + } + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index 186739c3cb..ae117254fa 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Select private readonly Container content; protected override Container Content => content; - public readonly Container Details; //todo: replace with a real details view when added + public readonly BeatmapDetails Details; public readonly Leaderboard Leaderboard; private WorkingBeatmap beatmap; @@ -28,6 +28,7 @@ namespace osu.Game.Screens.Select { beatmap = value; Leaderboard.Beatmap = beatmap?.BeatmapInfo; + Details.Beatmap = beatmap?.Beatmap.BeatmapInfo; } } @@ -46,6 +47,7 @@ namespace osu.Game.Screens.Select Details.Show(); Leaderboard.Hide(); break; + default: Details.Hide(); Leaderboard.Show(); @@ -62,9 +64,11 @@ namespace osu.Game.Screens.Select Add(new Drawable[] { - Details = new Container + Details = new BeatmapDetails { RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Alpha = 0, }, Leaderboard = new Leaderboard { @@ -74,4 +78,4 @@ namespace osu.Game.Screens.Select }); } } -} +} \ No newline at end of file diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs new file mode 100644 index 0000000000..a0d15101e0 --- /dev/null +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -0,0 +1,434 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Sprites; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using System.Globalization; +using System.Linq; + +namespace osu.Game.Screens.Select +{ + public class BeatmapDetails : Container + { + private readonly MetadataSegment description; + private readonly MetadataSegment source; + private readonly MetadataSegment tags; + + private readonly DifficultyRow circleSize; + private readonly DifficultyRow drainRate; + private readonly DifficultyRow overallDifficulty; + private readonly DifficultyRow approachRate; + private readonly DifficultyRow stars; + + private readonly Container ratingsContainer; + private readonly Bar ratingsBar; + private readonly OsuSpriteText negativeRatings; + private readonly OsuSpriteText positiveRatings; + private readonly BarGraph ratingsGraph; + + private readonly FillFlowContainer retryFailContainer; + private readonly BarGraph retryGraph; + private readonly BarGraph failGraph; + + private BeatmapInfo beatmap; + public BeatmapInfo Beatmap + { + get + { + return beatmap; + } + set + { + beatmap = value; + if (beatmap == null) return; + + description.Text = beatmap.Version; + source.Text = beatmap.Metadata.Source; + tags.Text = beatmap.Metadata.Tags; + + circleSize.Value = beatmap.Difficulty.CircleSize; + drainRate.Value = beatmap.Difficulty.DrainRate; + overallDifficulty.Value = beatmap.Difficulty.OverallDifficulty; + approachRate.Value = beatmap.Difficulty.ApproachRate; + stars.Value = (float)beatmap.StarDifficulty; + + if (beatmap.Metrics?.Ratings.Any() ?? false) + { + var ratings = beatmap.Metrics.Ratings.ToList(); + ratingsContainer.Show(); + + negativeRatings.Text = ratings.GetRange(0, ratings.Count / 2).Sum().ToString(); + positiveRatings.Text = ratings.GetRange(ratings.Count / 2, ratings.Count / 2).Sum().ToString(); + ratingsBar.Length = (float)ratings.GetRange(0, ratings.Count / 2).Sum() / ratings.Sum(); + + ratingsGraph.Values = ratings.Select(rating => (float)rating); + } + else + ratingsContainer.Hide(); + + if ((beatmap.Metrics?.Retries.Any() ?? false) && beatmap.Metrics.Fails.Any()) + { + var retries = beatmap.Metrics.Retries; + var fails = beatmap.Metrics.Fails; + retryFailContainer.Show(); + + float maxValue = fails.Zip(retries, (fail, retry) => fail + retry).Max(); + failGraph.MaxValue = maxValue; + retryGraph.MaxValue = maxValue; + + failGraph.Values = fails.Select(fail => (float)fail); + retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + MathHelper.Clamp(fail, 0, maxValue)); + } + else + retryFailContainer.Hide(); + } + } + + public BeatmapDetails() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new FillFlowContainer() + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.4f, + Direction = FillDirection.Vertical, + LayoutDuration = 200, + LayoutEasing = EasingTypes.OutQuint, + Padding = new MarginPadding(10) { Top = 25 }, + Children = new [] + { + description = new MetadataSegment("Description"), + source = new MetadataSegment("Source"), + tags = new MetadataSegment("Tags") + }, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.6f, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 15), + Padding = new MarginPadding(10) { Top = 0 }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0,10), + Padding = new MarginPadding(15) { Top = 25 }, + Children = new [] + { + circleSize = new DifficultyRow("Circle Size", 7), + drainRate = new DifficultyRow("HP Drain"), + overallDifficulty = new DifficultyRow("Accuracy"), + approachRate = new DifficultyRow("Approach Rate"), + stars = new DifficultyRow("Star Diffculty"), + }, + }, + }, + }, + ratingsContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + AlwaysPresent = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding + { + Top = 25, + Left = 15, + Right = 15, + }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "User Rating", + Font = @"Exo2.0-Medium", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + ratingsBar = new Bar + { + RelativeSizeAxes = Axes.X, + Height = 5, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + negativeRatings = new OsuSpriteText + { + Font = @"Exo2.0-Regular", + Text = "0", + }, + positiveRatings = new OsuSpriteText + { + Font = @"Exo2.0-Regular", + Text = "0", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + }, + }, + new OsuSpriteText + { + Text = "Rating Spread", + TextSize = 14, + Font = @"Exo2.0-Regular", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + ratingsGraph = new BarGraph + { + RelativeSizeAxes = Axes.X, + Height = 50, + }, + }, + }, + }, + }, + retryFailContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Points of Failure", + Font = @"Exo2.0-Regular", + }, + new Container + { + RelativeSizeAxes = Axes.X, + Size = new Vector2(1/0.6f, 50), + Children = new[] + { + retryGraph = new BarGraph + { + RelativeSizeAxes = Axes.Both, + }, + failGraph = new BarGraph + { + RelativeSizeAxes = Axes.Both, + }, + }, + }, + } + }, + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + description.AccentColour = colour.GrayB; + source.AccentColour = colour.GrayB; + tags.AccentColour = colour.YellowLight; + + stars.AccentColour = colour.Yellow; + + ratingsBar.BackgroundColour = colour.Green; + ratingsBar.AccentColour = colour.YellowDark; + ratingsGraph.Colour = colour.BlueDark; + + failGraph.Colour = colour.YellowDarker; + retryGraph.Colour = colour.Yellow; + } + + private class DifficultyRow : Container, IHasAccentColour + { + private readonly OsuSpriteText name; + private readonly Bar bar; + private readonly OsuSpriteText valueText; + + private readonly float maxValue; + + private float difficultyValue; + public float Value + { + get + { + return difficultyValue; + } + set + { + difficultyValue = value; + bar.Length = value / maxValue; + valueText.Text = value.ToString(CultureInfo.InvariantCulture); + } + } + + public Color4 AccentColour + { + get + { + return bar.AccentColour; + } + set + { + bar.AccentColour = value; + } + } + + public DifficultyRow(string difficultyName, float maxValue = 10) + { + this.maxValue = maxValue; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Children = new Drawable[] + { + name = new OsuSpriteText + { + Font = @"Exo2.0-Regular", + Text = difficultyName, + }, + bar = new Bar + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1, 0.35f), + Padding = new MarginPadding { Left = 100, Right = 25 }, + }, + valueText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = @"Exo2.0-Regular", + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + name.Colour = colour.GrayB; + bar.BackgroundColour = colour.Gray7; + valueText.Colour = colour.GrayB; + } + } + + private class MetadataSegment : Container, IHasAccentColour + { + private readonly OsuSpriteText header; + private readonly FillFlowContainer content; + + public string Text + { + set + { + if (string.IsNullOrEmpty(value)) + Hide(); + else + { + Show(); + if (header.Text == "Tags") + content.Children = value.Split(' ').Select(text => new OsuSpriteText + { + Text = text, + Font = "Exo2.0-Regular", + }); + else + content.Children = new[] + { + new OsuSpriteText + { + Text = value, + Font = "Exo2.0-Regular", + } + }; + } + } + } + + public Color4 AccentColour + { + get + { + return content.Colour; + } + set + { + content.Colour = value; + } + } + + public MetadataSegment(string headerText) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Margin = new MarginPadding { Top = 10 }; + Children = new Drawable[] + { + header = new OsuSpriteText + { + Font = @"Exo2.0-Bold", + Text = headerText, + }, + content = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Spacing = new Vector2(5,0), + Margin = new MarginPadding { Top = header.TextSize } + } + }; + } + } + } +} \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 12410563cd..578704f4f8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -76,6 +76,7 @@ + @@ -84,6 +85,7 @@ + @@ -202,6 +204,8 @@ + +