2024-01-23 16:08:29 -05:00
|
|
|
// 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.
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Linq;
|
2024-01-23 20:40:32 -05:00
|
|
|
using osu.Game.Beatmaps;
|
|
|
|
using osu.Game.Rulesets.Objects.Legacy;
|
2024-01-23 16:08:29 -05:00
|
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
|
|
using osu.Game.Rulesets.Scoring;
|
|
|
|
using osu.Game.Screens.Ranking.Statistics;
|
|
|
|
using osuTK;
|
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Osu.Statistics
|
|
|
|
{
|
|
|
|
/// <summary>
|
2024-01-23 23:48:12 -05:00
|
|
|
/// Displays the aim error statistic for a given play.
|
2024-01-23 16:08:29 -05:00
|
|
|
/// </summary>
|
|
|
|
public partial class AimError : SimpleStatisticItem<double?>
|
|
|
|
{
|
|
|
|
private readonly List<Vector2> hitPoints = new List<Vector2>();
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Creates and computes an <see cref="AimError"/> statistic.
|
|
|
|
/// </summary>
|
2024-01-23 23:48:12 -05:00
|
|
|
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the aim error based on.</param>
|
|
|
|
/// <param name="playableBeatmap">The <see cref="IBeatmap"/> containing the radii of the circles, used to compute the variance of misses.</param>
|
2024-01-23 20:40:32 -05:00
|
|
|
public AimError(IEnumerable<HitEvent> hitEvents, IBeatmap playableBeatmap)
|
2024-01-23 16:08:29 -05:00
|
|
|
: base("Aim Error")
|
|
|
|
{
|
2024-01-23 20:40:32 -05:00
|
|
|
Value = calculateAimError(hitEvents, playableBeatmap);
|
2024-01-23 16:08:29 -05:00
|
|
|
}
|
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
private double? calculateAimError(IEnumerable<HitEvent> hitEvents, IBeatmap playableBeatmap)
|
2024-01-23 16:08:29 -05:00
|
|
|
{
|
2024-01-24 18:08:34 -05:00
|
|
|
IEnumerable<HitEvent> hitCircleEvents = hitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
|
2024-01-23 16:08:29 -05:00
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
double nonMissCount = hitCircleEvents.Count(e => e.Result.IsHit());
|
|
|
|
double missCount = hitCircleEvents.Count() - nonMissCount;
|
|
|
|
|
|
|
|
if (nonMissCount == 0)
|
2024-01-23 16:08:29 -05:00
|
|
|
return null;
|
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
foreach (var e in hitCircleEvents)
|
2024-01-23 16:08:29 -05:00
|
|
|
{
|
2024-01-23 20:40:32 -05:00
|
|
|
if (e.Position == null)
|
2024-01-23 16:08:29 -05:00
|
|
|
continue;
|
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
hitPoints.Add((e.Position - ((OsuHitObject)e.HitObject).StackedEndPosition).Value);
|
2024-01-23 16:08:29 -05:00
|
|
|
}
|
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
double radius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(playableBeatmap.Difficulty.CircleSize, true);
|
2024-01-23 16:08:29 -05:00
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
// We don't get data for miss locations, so we estimate the total variance using the Rayleigh distribution.
|
2024-01-24 15:18:07 -05:00
|
|
|
// Deriving the Rayleigh distribution in this form results in a 2 in the denominator,
|
|
|
|
// but it is removed to take the variance across both axes, instead of across just one.
|
2024-01-24 18:08:34 -05:00
|
|
|
double variance = (missCount * Math.Pow(radius, 2) + hitPoints.Sum(point => point.LengthSquared)) / nonMissCount;
|
2024-01-23 16:08:29 -05:00
|
|
|
|
2024-01-23 20:40:32 -05:00
|
|
|
return Math.Sqrt(variance) * 10;
|
2024-01-23 16:08:29 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2");
|
|
|
|
}
|
|
|
|
}
|