Merge pull request #10251 from smoogipoo/additional-hit-results

This commit is contained in:
Dean Herbert 2020-10-01 12:19:48 +09:00 committed by GitHub
commit 9d07dce5e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 235 additions and 21 deletions

View File

@ -11,9 +11,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -55,6 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
highAccuracyColour = colours.GreenLight; highAccuracyColour = colours.GreenLight;
} }
/// <summary>
/// The statistics that appear in the table, in order of appearance.
/// </summary>
private readonly List<HitResult> statisticResultTypes = new List<HitResult>();
private bool showPerformancePoints; private bool showPerformancePoints;
public void DisplayScores(IReadOnlyList<ScoreInfo> scores, bool showPerformanceColumn) public void DisplayScores(IReadOnlyList<ScoreInfo> scores, bool showPerformanceColumn)
@ -65,11 +72,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return; return;
showPerformancePoints = showPerformanceColumn; showPerformancePoints = showPerformanceColumn;
statisticResultTypes.Clear();
for (int i = 0; i < scores.Count; i++) for (int i = 0; i < scores.Count; i++)
backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); 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(); Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular();
} }
@ -79,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
backgroundFlow.Clear(); backgroundFlow.Clear();
} }
private TableColumn[] createHeaders(ScoreInfo score) private TableColumn[] createHeaders(IReadOnlyList<ScoreInfo> scores)
{ {
var columns = new List<TableColumn> var columns = new List<TableColumn>
{ {
@ -92,10 +100,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn("max combo", Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120)) 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)) // All statistics across all scores, unordered.
columns.Add(new TableColumn(statistic.Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); 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<HitResult>())
{
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) if (showPerformancePoints)
columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); 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 content.Add(new OsuSpriteText
{ {
Text = $"{kvp.Value}", Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}",
Font = OsuFont.GetFont(size: text_size), Font = OsuFont.GetFont(size: text_size),
Colour = kvp.Value == 0 ? Color4.Gray : Color4.White Colour = stat.count == 0 ? Color4.Gray : Color4.White
}); });
} }

View File

@ -117,7 +117,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
ppColumn.Text = $@"{value.PP:N0}"; 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; modsColumn.Mods = value.Mods;
if (scoreManager != null) 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 private class InfoColumn : CompositeDrawable

View File

@ -2,15 +2,18 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.ComponentModel; using System.ComponentModel;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
{ {
[HasOrderedElements]
public enum HitResult public enum HitResult
{ {
/// <summary> /// <summary>
/// Indicates that the object has not been judged yet. /// Indicates that the object has not been judged yet.
/// </summary> /// </summary>
[Description(@"")] [Description(@"")]
[Order(14)]
None, None,
/// <summary> /// <summary>
@ -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). /// "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).
/// </remarks> /// </remarks>
[Description(@"Miss")] [Description(@"Miss")]
[Order(5)]
Miss, Miss,
[Description(@"Meh")] [Description(@"Meh")]
[Order(4)]
Meh, Meh,
/// <summary> /// <summary>
/// Optional judgement. /// Optional judgement.
/// </summary> /// </summary>
[Description(@"OK")] [Description(@"OK")]
[Order(3)]
Ok, Ok,
[Description(@"Good")] [Description(@"Good")]
[Order(2)]
Good, Good,
[Description(@"Great")] [Description(@"Great")]
[Order(1)]
Great, Great,
/// <summary> /// <summary>
/// Optional judgement. /// Optional judgement.
/// </summary> /// </summary>
[Description(@"Perfect")] [Description(@"Perfect")]
[Order(0)]
Perfect, Perfect,
/// <summary> /// <summary>
/// Indicates small tick miss. /// Indicates small tick miss.
/// </summary> /// </summary>
[Order(11)]
SmallTickMiss, SmallTickMiss,
/// <summary> /// <summary>
/// Indicates a small tick hit. /// Indicates a small tick hit.
/// </summary> /// </summary>
[Description(@"S Tick")]
[Order(7)]
SmallTickHit, SmallTickHit,
/// <summary> /// <summary>
/// Indicates a large tick miss. /// Indicates a large tick miss.
/// </summary> /// </summary>
[Order(10)]
LargeTickMiss, LargeTickMiss,
/// <summary> /// <summary>
/// Indicates a large tick hit. /// Indicates a large tick hit.
/// </summary> /// </summary>
LargeTickHit [Description(@"L Tick")]
[Order(6)]
LargeTickHit,
/// <summary>
/// Indicates a small bonus.
/// </summary>
[Description("S Bonus")]
[Order(9)]
SmallBonus,
/// <summary>
/// Indicates a large bonus.
/// </summary>
[Description("L Bonus")]
[Order(8)]
LargeBonus,
/// <summary>
/// Indicates a miss that should be ignored for scoring purposes.
/// </summary>
[Order(13)]
IgnoreMiss,
/// <summary>
/// Indicates a hit that should be ignored for scoring purposes.
/// </summary>
[Order(12)]
IgnoreHit,
}
public static class HitResultExtensions
{
/// <summary>
/// Whether a <see cref="HitResult"/> increases/decreases the combo, and affects the combo portion of the score.
/// </summary>
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;
}
}
/// <summary>
/// Whether a <see cref="HitResult"/> affects the accuracy portion of the score.
/// </summary>
public static bool AffectsAccuracy(this HitResult result)
=> IsScorable(result) && !IsBonus(result);
/// <summary>
/// Whether a <see cref="HitResult"/> should be counted as bonus score.
/// </summary>
public static bool IsBonus(this HitResult result)
{
switch (result)
{
case HitResult.SmallBonus:
case HitResult.LargeBonus:
return true;
default:
return false;
}
}
/// <summary>
/// Whether a <see cref="HitResult"/> represents a successful hit.
/// </summary>
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;
}
}
/// <summary>
/// Whether a <see cref="HitResult"/> is scorable.
/// </summary>
public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss;
} }
} }

View File

@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -147,8 +148,6 @@ namespace osu.Game.Scoring
[JsonProperty("statistics")] [JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>(); public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>();
public IOrderedEnumerable<KeyValuePair<HitResult, int>> SortedStatistics => Statistics.OrderByDescending(pair => pair.Key);
[JsonIgnore] [JsonIgnore]
[Column("Statistics")] [Column("Statistics")]
public string StatisticsJson public string StatisticsJson
@ -186,6 +185,48 @@ namespace osu.Game.Scoring
[JsonProperty("position")] [JsonProperty("position")]
public int? Position { get; set; } public int? Position { get; set; }
public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
{
foreach (var key in OrderAttributeUtils.GetValuesInOrder<HitResult>())
{
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] [Serializable]
protected class DeserializedMod : IMod protected class DeserializedMod : IMod
{ {

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Users; using osu.Game.Users;
@ -116,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Contracted
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5), 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 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 private Drawable createStatistic(string key, string value) => new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -65,8 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded
}; };
var bottomStatistics = new List<StatisticDisplay>(); var bottomStatistics = new List<StatisticDisplay>();
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(topStatistics);
statisticDisplays.AddRange(bottomStatistics); statisticDisplays.AddRange(bottomStatistics);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
public class CounterStatistic : StatisticDisplay public class CounterStatistic : StatisticDisplay
{ {
private readonly int count; private readonly int count;
private readonly int? maxCount;
private RollingCounter<int> counter; private RollingCounter<int> counter;
@ -24,10 +26,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
/// </summary> /// </summary>
/// <param name="header">The name of the statistic.</param> /// <param name="header">The name of the statistic.</param>
/// <param name="count">The value to display.</param> /// <param name="count">The value to display.</param>
public CounterStatistic(string header, int count) /// <param name="maxCount">The maximum value of <paramref name="count"/>. Not displayed if null.</param>
public CounterStatistic(string header, int count, int? maxCount = null)
: base(header) : base(header)
{ {
this.count = count; this.count = count;
this.maxCount = maxCount;
} }
public override void Appear() public override void Appear()
@ -36,7 +40,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
counter.Current.Value = count; 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<int> private class Counter : RollingCounter<int>
{ {

View File

@ -12,8 +12,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
{ {
private readonly HitResult result; private readonly HitResult result;
public HitResultStatistic(HitResult result, int count) public HitResultStatistic(HitResult result, int count, int? maxCount = null)
: base(result.GetDescription(), count) : base(result.GetDescription(), count, maxCount)
{ {
this.result = result; this.result = result;
} }

View File

@ -37,6 +37,12 @@ namespace osu.Game.Tests
Statistics[HitResult.Meh] = 50; Statistics[HitResult.Meh] = 50;
Statistics[HitResult.Good] = 100; Statistics[HitResult.Good] = 100;
Statistics[HitResult.Great] = 300; 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; Position = 1;
} }