diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 56866765b6..968355c377 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -11,9 +11,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Users.Drawables;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -55,6 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
highAccuracyColour = colours.GreenLight;
}
+ ///
+ /// The statistics that appear in the table, in order of appearance.
+ ///
+ private readonly List statisticResultTypes = new List();
+
private bool showPerformancePoints;
public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn)
@@ -65,11 +72,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
showPerformancePoints = showPerformanceColumn;
+ statisticResultTypes.Clear();
for (int i = 0; i < scores.Count; i++)
backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height));
- Columns = createHeaders(scores.FirstOrDefault());
+ Columns = createHeaders(scores);
Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular();
}
@@ -79,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
backgroundFlow.Clear();
}
- private TableColumn[] createHeaders(ScoreInfo score)
+ private TableColumn[] createHeaders(IReadOnlyList scores)
{
var columns = new List
{
@@ -92,10 +100,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn("max combo", Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120))
};
- foreach (var statistic in score.SortedStatistics.Take(score.SortedStatistics.Count() - 1))
- columns.Add(new TableColumn(statistic.Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ // All statistics across all scores, unordered.
+ var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet();
- columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95)));
+ foreach (var result in OrderAttributeUtils.GetValuesInOrder())
+ {
+ if (!allScoreStatistics.Contains(result))
+ continue;
+
+ columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ statisticResultTypes.Add(result);
+ }
if (showPerformancePoints)
columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30)));
@@ -148,13 +163,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
};
- foreach (var kvp in score.SortedStatistics)
+ var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result);
+
+ foreach (var result in statisticResultTypes)
{
+ if (!availableStatistics.TryGetValue(result, out var stat))
+ stat = (result, 0, null);
+
content.Add(new OsuSpriteText
{
- Text = $"{kvp.Value}",
+ Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}",
Font = OsuFont.GetFont(size: text_size),
- Colour = kvp.Value == 0 ? Color4.Gray : Color4.White
+ Colour = stat.count == 0 ? Color4.Gray : Color4.White
});
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index 3a842d0a43..05789e1fc0 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
ppColumn.Text = $@"{value.PP:N0}";
- statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value));
+ statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount));
modsColumn.Mods = value.Mods;
if (scoreManager != null)
@@ -125,9 +125,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
- private TextColumn createStatisticsColumn(HitResult hitResult, int count) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width)
+ private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width)
{
- Text = count.ToString()
+ Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}"
};
private class InfoColumn : CompositeDrawable
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index b057af2a50..2b9b1a6c8e 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -2,15 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Scoring
{
+ [HasOrderedElements]
public enum HitResult
{
///
/// Indicates that the object has not been judged yet.
///
[Description(@"")]
+ [Order(14)]
None,
///
@@ -21,47 +24,156 @@ namespace osu.Game.Rulesets.Scoring
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
///
[Description(@"Miss")]
+ [Order(5)]
Miss,
[Description(@"Meh")]
+ [Order(4)]
Meh,
///
/// Optional judgement.
///
[Description(@"OK")]
+ [Order(3)]
Ok,
[Description(@"Good")]
+ [Order(2)]
Good,
[Description(@"Great")]
+ [Order(1)]
Great,
///
/// Optional judgement.
///
[Description(@"Perfect")]
+ [Order(0)]
Perfect,
///
/// Indicates small tick miss.
///
+ [Order(11)]
SmallTickMiss,
///
/// Indicates a small tick hit.
///
+ [Description(@"S Tick")]
+ [Order(7)]
SmallTickHit,
///
/// Indicates a large tick miss.
///
+ [Order(10)]
LargeTickMiss,
///
/// Indicates a large tick hit.
///
- LargeTickHit
+ [Description(@"L Tick")]
+ [Order(6)]
+ LargeTickHit,
+
+ ///
+ /// Indicates a small bonus.
+ ///
+ [Description("S Bonus")]
+ [Order(9)]
+ SmallBonus,
+
+ ///
+ /// Indicates a large bonus.
+ ///
+ [Description("L Bonus")]
+ [Order(8)]
+ LargeBonus,
+
+ ///
+ /// Indicates a miss that should be ignored for scoring purposes.
+ ///
+ [Order(13)]
+ IgnoreMiss,
+
+ ///
+ /// Indicates a hit that should be ignored for scoring purposes.
+ ///
+ [Order(12)]
+ IgnoreHit,
+ }
+
+ public static class HitResultExtensions
+ {
+ ///
+ /// Whether a increases/decreases the combo, and affects the combo portion of the score.
+ ///
+ public static bool AffectsCombo(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Miss:
+ case HitResult.Meh:
+ case HitResult.Ok:
+ case HitResult.Good:
+ case HitResult.Great:
+ case HitResult.Perfect:
+ case HitResult.LargeTickHit:
+ case HitResult.LargeTickMiss:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Whether a affects the accuracy portion of the score.
+ ///
+ public static bool AffectsAccuracy(this HitResult result)
+ => IsScorable(result) && !IsBonus(result);
+
+ ///
+ /// Whether a should be counted as bonus score.
+ ///
+ public static bool IsBonus(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.SmallBonus:
+ case HitResult.LargeBonus:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Whether a represents a successful hit.
+ ///
+ public static bool IsHit(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.None:
+ case HitResult.IgnoreMiss:
+ case HitResult.Miss:
+ case HitResult.SmallTickMiss:
+ case HitResult.LargeTickMiss:
+ return false;
+
+ default:
+ return true;
+ }
+ }
+
+ ///
+ /// Whether a is scorable.
+ ///
+ public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss;
}
}
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index efcf1737c9..4ed3f92e25 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
+using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
@@ -147,8 +148,6 @@ namespace osu.Game.Scoring
[JsonProperty("statistics")]
public Dictionary Statistics = new Dictionary();
- public IOrderedEnumerable> SortedStatistics => Statistics.OrderByDescending(pair => pair.Key);
-
[JsonIgnore]
[Column("Statistics")]
public string StatisticsJson
@@ -186,6 +185,48 @@ namespace osu.Game.Scoring
[JsonProperty("position")]
public int? Position { get; set; }
+ public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
+ {
+ foreach (var key in OrderAttributeUtils.GetValuesInOrder())
+ {
+ if (key.IsBonus())
+ continue;
+
+ int value = Statistics.GetOrDefault(key);
+
+ switch (key)
+ {
+ case HitResult.SmallTickHit:
+ {
+ int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss);
+ if (total > 0)
+ yield return (key, value, total);
+
+ break;
+ }
+
+ case HitResult.LargeTickHit:
+ {
+ int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss);
+ if (total > 0)
+ yield return (key, value, total);
+
+ break;
+ }
+
+ case HitResult.SmallTickMiss:
+ case HitResult.LargeTickMiss:
+ break;
+
+ default:
+ if (value > 0 || key == HitResult.Miss)
+ yield return (key, value, null);
+
+ break;
+ }
+ }
+ }
+
[Serializable]
protected class DeserializedMod : IMod
{
diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
index 0b85eeafa8..95ece1a9fb 100644
--- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
@@ -116,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Contracted
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
- ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString()))
+ ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount))
},
new FillFlowContainer
{
@@ -198,6 +199,9 @@ namespace osu.Game.Screens.Ranking.Contracted
};
}
+ private Drawable createStatistic(HitResult result, int count, int? maxCount)
+ => createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}");
+
private Drawable createStatistic(string key, string value) => new Container
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 0033cd1f43..ebab8c88f6 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -65,8 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded
};
var bottomStatistics = new List();
- foreach (var stat in score.SortedStatistics)
- bottomStatistics.Add(new HitResultStatistic(stat.Key, stat.Value));
+
+ foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay())
+ bottomStatistics.Add(new HitResultStatistic(key, value, maxCount));
statisticDisplays.AddRange(topStatistics);
statisticDisplays.AddRange(bottomStatistics);
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs
index e820831809..fc01f5e9c4 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
public class CounterStatistic : StatisticDisplay
{
private readonly int count;
+ private readonly int? maxCount;
private RollingCounter counter;
@@ -24,10 +26,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
///
/// The name of the statistic.
/// The value to display.
- public CounterStatistic(string header, int count)
+ /// The maximum value of . Not displayed if null.
+ public CounterStatistic(string header, int count, int? maxCount = null)
: base(header)
{
this.count = count;
+ this.maxCount = maxCount;
}
public override void Appear()
@@ -36,7 +40,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
counter.Current.Value = count;
}
- protected override Drawable CreateContent() => counter = new Counter();
+ protected override Drawable CreateContent()
+ {
+ var container = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Child = counter = new Counter
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ }
+ };
+
+ if (maxCount != null)
+ {
+ container.Add(new OsuSpriteText
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Font = OsuFont.Torus.With(size: 12, fixedWidth: true),
+ Spacing = new Vector2(-2, 0),
+ Text = $"/{maxCount}"
+ });
+ }
+
+ return container;
+ }
private class Counter : RollingCounter
{
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
index faa4a6a96c..a86033713f 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
@@ -12,8 +12,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
{
private readonly HitResult result;
- public HitResultStatistic(HitResult result, int count)
- : base(result.GetDescription(), count)
+ public HitResultStatistic(HitResult result, int count, int? maxCount = null)
+ : base(result.GetDescription(), count, maxCount)
{
this.result = result;
}
diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs
index 31cced6ce4..704d01e479 100644
--- a/osu.Game/Tests/TestScoreInfo.cs
+++ b/osu.Game/Tests/TestScoreInfo.cs
@@ -37,6 +37,12 @@ namespace osu.Game.Tests
Statistics[HitResult.Meh] = 50;
Statistics[HitResult.Good] = 100;
Statistics[HitResult.Great] = 300;
+ Statistics[HitResult.SmallTickHit] = 50;
+ Statistics[HitResult.SmallTickMiss] = 25;
+ Statistics[HitResult.LargeTickHit] = 100;
+ Statistics[HitResult.LargeTickMiss] = 50;
+ Statistics[HitResult.SmallBonus] = 10;
+ Statistics[HitResult.SmallBonus] = 50;
Position = 1;
}