mirror of
https://github.com/osukey/osukey.git
synced 2025-05-17 03:27:21 +09:00
Merge pull request #20143 from acid-chicken/feat/stats/colored-td
Show judgement colours in hit distribution graph
This commit is contained in:
commit
096d1c3ff3
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
{
|
{
|
||||||
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
|
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
|
||||||
{
|
{
|
||||||
private HitEventTimingDistributionGraph graph;
|
private HitEventTimingDistributionGraph graph = null!;
|
||||||
|
|
||||||
private static readonly HitObject placeholder_object = new HitCircle();
|
private static readonly HitObject placeholder_object = new HitCircle();
|
||||||
|
|
||||||
@ -43,6 +41,65 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
|
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSparse()
|
||||||
|
{
|
||||||
|
createTest(new List<HitEvent>
|
||||||
|
{
|
||||||
|
new HitEvent(-7, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||||
|
new HitEvent(-6, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||||
|
new HitEvent(-5, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||||
|
new HitEvent(5, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||||
|
new HitEvent(6, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||||
|
new HitEvent(7, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVariousTypesOfHitResult()
|
||||||
|
{
|
||||||
|
createTest(CreateDistributedHitEvents(0, 50).Select(h =>
|
||||||
|
{
|
||||||
|
double offset = Math.Abs(h.TimeOffset);
|
||||||
|
HitResult result = offset > 36 ? HitResult.Miss
|
||||||
|
: offset > 32 ? HitResult.Meh
|
||||||
|
: offset > 24 ? HitResult.Ok
|
||||||
|
: offset > 16 ? HitResult.Good
|
||||||
|
: offset > 8 ? HitResult.Great
|
||||||
|
: HitResult.Perfect;
|
||||||
|
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||||
|
}).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleWindowsOfHitResult()
|
||||||
|
{
|
||||||
|
var wide = CreateDistributedHitEvents(0, 50).Select(h =>
|
||||||
|
{
|
||||||
|
double offset = Math.Abs(h.TimeOffset);
|
||||||
|
HitResult result = offset > 36 ? HitResult.Miss
|
||||||
|
: offset > 32 ? HitResult.Meh
|
||||||
|
: offset > 24 ? HitResult.Ok
|
||||||
|
: offset > 16 ? HitResult.Good
|
||||||
|
: offset > 8 ? HitResult.Great
|
||||||
|
: HitResult.Perfect;
|
||||||
|
|
||||||
|
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||||
|
});
|
||||||
|
var narrow = CreateDistributedHitEvents(0, 50).Select(h =>
|
||||||
|
{
|
||||||
|
double offset = Math.Abs(h.TimeOffset);
|
||||||
|
HitResult result = offset > 25 ? HitResult.Miss
|
||||||
|
: offset > 20 ? HitResult.Meh
|
||||||
|
: offset > 15 ? HitResult.Ok
|
||||||
|
: offset > 10 ? HitResult.Good
|
||||||
|
: offset > 5 ? HitResult.Great
|
||||||
|
: HitResult.Perfect;
|
||||||
|
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||||
|
});
|
||||||
|
createTest(wide.Concat(narrow).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestZeroTimeOffset()
|
public void TestZeroTimeOffset()
|
||||||
{
|
{
|
||||||
|
@ -102,26 +102,31 @@ namespace osu.Game.Graphics
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the colour for a <see cref="HitResult"/>.
|
/// Retrieves the colour for a <see cref="HitResult"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Color4 ForHitResult(HitResult judgement)
|
public Color4 ForHitResult(HitResult result)
|
||||||
{
|
{
|
||||||
switch (judgement)
|
switch (result)
|
||||||
{
|
{
|
||||||
case HitResult.Perfect:
|
case HitResult.SmallTickMiss:
|
||||||
case HitResult.Great:
|
case HitResult.LargeTickMiss:
|
||||||
return Blue;
|
case HitResult.Miss:
|
||||||
|
return Red;
|
||||||
case HitResult.Ok:
|
|
||||||
case HitResult.Good:
|
|
||||||
return Green;
|
|
||||||
|
|
||||||
case HitResult.Meh:
|
case HitResult.Meh:
|
||||||
return Yellow;
|
return Yellow;
|
||||||
|
|
||||||
case HitResult.Miss:
|
case HitResult.Ok:
|
||||||
return Red;
|
return Green;
|
||||||
|
|
||||||
|
case HitResult.Good:
|
||||||
|
return GreenLight;
|
||||||
|
|
||||||
|
case HitResult.SmallTickHit:
|
||||||
|
case HitResult.LargeTickHit:
|
||||||
|
case HitResult.Great:
|
||||||
|
return Blue;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Color4.White;
|
return BlueLight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Scoring
|
namespace osu.Game.Rulesets.Scoring
|
||||||
@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
public static class HitResultExtensions
|
public static class HitResultExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly IList<HitResult> order = EnumExtensions.GetValuesInOrder<HitResult>().ToList();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a <see cref="HitResult"/> increases the combo.
|
/// Whether a <see cref="HitResult"/> increases the combo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -282,6 +284,13 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
Debug.Assert(minResult <= maxResult);
|
Debug.Assert(minResult <= maxResult);
|
||||||
return result > minResult && result < maxResult;
|
return result > minResult && result < maxResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ordered index of a <see cref="HitResult"/>. Used for consistent order when displaying hit results to the user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The <see cref="HitResult"/> to get the index of.</param>
|
||||||
|
/// <returns>The index of <paramref name="result"/>.</returns>
|
||||||
|
public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result);
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
@ -59,30 +59,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
|
|
||||||
protected Color4 GetColourForHitResult(HitResult result)
|
protected Color4 GetColourForHitResult(HitResult result)
|
||||||
{
|
{
|
||||||
switch (result)
|
return colours.ForHitResult(result);
|
||||||
{
|
|
||||||
case HitResult.SmallTickMiss:
|
|
||||||
case HitResult.LargeTickMiss:
|
|
||||||
case HitResult.Miss:
|
|
||||||
return colours.Red;
|
|
||||||
|
|
||||||
case HitResult.Meh:
|
|
||||||
return colours.Yellow;
|
|
||||||
|
|
||||||
case HitResult.Ok:
|
|
||||||
return colours.Green;
|
|
||||||
|
|
||||||
case HitResult.Good:
|
|
||||||
return colours.GreenLight;
|
|
||||||
|
|
||||||
case HitResult.SmallTickHit:
|
|
||||||
case HitResult.LargeTickHit:
|
|
||||||
case HitResult.Great:
|
|
||||||
return colours.Blue;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return colours.BlueLight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -7,7 +7,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -57,7 +56,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
|
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] bins;
|
private IDictionary<HitResult, int>[] bins;
|
||||||
private double binSize;
|
private double binSize;
|
||||||
private double hitOffset;
|
private double hitOffset;
|
||||||
|
|
||||||
@ -69,7 +68,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
if (hitEvents == null || hitEvents.Count == 0)
|
if (hitEvents == null || hitEvents.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bins = new int[total_timing_distribution_bins];
|
bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
|
||||||
|
|
||||||
binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins);
|
binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins);
|
||||||
|
|
||||||
@ -89,7 +88,8 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
{
|
{
|
||||||
bool roundUp = true;
|
bool roundUp = true;
|
||||||
|
|
||||||
Array.Clear(bins, 0, bins.Length);
|
foreach (var bin in bins)
|
||||||
|
bin.Clear();
|
||||||
|
|
||||||
foreach (var e in hitEvents)
|
foreach (var e in hitEvents)
|
||||||
{
|
{
|
||||||
@ -110,23 +110,23 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
|
|
||||||
// may be out of range when applying an offset. for such cases we can just drop the results.
|
// may be out of range when applying an offset. for such cases we can just drop the results.
|
||||||
if (index >= 0 && index < bins.Length)
|
if (index >= 0 && index < bins.Length)
|
||||||
bins[index]++;
|
{
|
||||||
|
bins[index].TryGetValue(e.Result, out int value);
|
||||||
|
bins[index][e.Result] = ++value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (barDrawables != null)
|
if (barDrawables != null)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < barDrawables.Length; i++)
|
for (int i = 0; i < barDrawables.Length; i++)
|
||||||
{
|
{
|
||||||
barDrawables[i].UpdateOffset(bins[i]);
|
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int maxCount = bins.Max();
|
int maxCount = bins.Max(b => b.Values.Sum());
|
||||||
barDrawables = new Bar[total_timing_distribution_bins];
|
barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
|
||||||
|
|
||||||
for (int i = 0; i < barDrawables.Length; i++)
|
|
||||||
barDrawables[i] = new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index);
|
|
||||||
|
|
||||||
Container axisFlow;
|
Container axisFlow;
|
||||||
|
|
||||||
@ -209,50 +209,97 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
|
|
||||||
private class Bar : CompositeDrawable
|
private class Bar : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly float value;
|
private float totalValue => values.Sum(v => v.Value);
|
||||||
private readonly float maxValue;
|
private float basalHeight => BoundingBox.Width / BoundingBox.Height;
|
||||||
|
private float availableHeight => 1 - basalHeight;
|
||||||
|
|
||||||
private readonly Circle boxOriginal;
|
private readonly IReadOnlyList<KeyValuePair<HitResult, int>> values;
|
||||||
|
private readonly float maxValue;
|
||||||
|
private readonly bool isCentre;
|
||||||
|
|
||||||
|
private Circle[] boxOriginals;
|
||||||
private Circle boxAdjustment;
|
private Circle boxAdjustment;
|
||||||
|
|
||||||
private const float minimum_height = 0.05f;
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
public Bar(float value, float maxValue, bool isCentre)
|
public Bar(IDictionary<HitResult, int> values, float maxValue, bool isCentre)
|
||||||
{
|
{
|
||||||
this.value = value;
|
this.values = values.OrderBy(v => v.Key.GetIndexForOrderedDisplay()).ToList();
|
||||||
this.maxValue = maxValue;
|
this.maxValue = maxValue;
|
||||||
|
this.isCentre = isCentre;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
Masking = true;
|
Masking = true;
|
||||||
|
}
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
if (values.Any())
|
||||||
{
|
{
|
||||||
boxOriginal = new Circle
|
boxOriginals = values.Select((v, i) => new Circle
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.BottomCentre,
|
Anchor = Anchor.BottomCentre,
|
||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
Colour = isCentre ? Color4.White : Color4Extensions.FromHex("#66FFCC"),
|
Colour = isCentre && i == 0 ? Color4.White : colours.ForHitResult(v.Key),
|
||||||
Height = minimum_height,
|
Height = 0,
|
||||||
},
|
}).ToArray();
|
||||||
};
|
// The bars of the stacked bar graph will be processed (stacked) from the bottom, which is the base position,
|
||||||
|
// to the top, and the bottom bar should be drawn more toward the front by design,
|
||||||
|
// while the drawing order is from the back to the front, so the order passed to `InternalChildren` is the opposite.
|
||||||
|
InternalChildren = boxOriginals.Reverse().ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A bin with no value draws a grey dot instead.
|
||||||
|
InternalChildren = boxOriginals = new[]
|
||||||
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.BottomCentre,
|
||||||
|
Origin = Anchor.BottomCentre,
|
||||||
|
Colour = isCentre ? Color4.White : Color4.Gray,
|
||||||
|
Height = 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const double duration = 300;
|
private const double duration = 300;
|
||||||
|
|
||||||
|
private float offsetForValue(float value)
|
||||||
|
{
|
||||||
|
return availableHeight * value / maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float heightForValue(float value)
|
||||||
|
{
|
||||||
|
return basalHeight + offsetForValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
float height = Math.Clamp(value / maxValue, minimum_height, 1);
|
foreach (var boxOriginal in boxOriginals)
|
||||||
|
boxOriginal.Height = basalHeight;
|
||||||
|
|
||||||
if (height > minimum_height)
|
float offsetValue = 0;
|
||||||
boxOriginal.ResizeHeightTo(height, duration, Easing.OutQuint);
|
|
||||||
|
for (int i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
|
||||||
|
boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint);
|
||||||
|
offsetValue -= values[i].Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateOffset(float adjustment)
|
public void UpdateOffset(float adjustment)
|
||||||
{
|
{
|
||||||
bool hasAdjustment = adjustment != value && adjustment / maxValue >= minimum_height;
|
bool hasAdjustment = adjustment != totalValue;
|
||||||
|
|
||||||
if (boxAdjustment == null)
|
if (boxAdjustment == null)
|
||||||
{
|
{
|
||||||
@ -271,7 +318,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
boxAdjustment.ResizeHeightTo(Math.Clamp(adjustment / maxValue, minimum_height, 1), duration, Easing.OutQuint);
|
boxAdjustment.ResizeHeightTo(heightForValue(adjustment), duration, Easing.OutQuint);
|
||||||
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
|
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user