1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 10:17:38 +08:00
osu-lazer/osu.Game/Rulesets/Scoring/HitWindows.cs

227 lines
8.0 KiB
C#
Raw Normal View History

// 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.
2018-04-13 17:19:50 +08:00
using System;
using System.Collections.Generic;
2019-10-09 18:08:31 +08:00
using System.Diagnostics;
2019-10-09 18:23:37 +08:00
using System.Linq;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps;
2019-09-06 14:24:00 +08:00
using osu.Game.Rulesets.Objects;
2018-04-13 17:19:50 +08:00
2019-09-06 14:24:00 +08:00
namespace osu.Game.Rulesets.Scoring
2018-04-13 17:19:50 +08:00
{
2019-09-06 14:24:00 +08:00
/// <summary>
/// A structure containing timing data for hit window based gameplay.
/// </summary>
2018-04-13 17:19:50 +08:00
public class HitWindows
{
2019-09-06 14:24:00 +08:00
private static readonly DifficultyRange[] base_ranges =
2018-04-13 17:19:50 +08:00
{
2019-09-06 14:24:00 +08:00
new DifficultyRange(HitResult.Perfect, 22.4D, 19.4D, 13.9D),
new DifficultyRange(HitResult.Great, 64, 49, 34),
new DifficultyRange(HitResult.Good, 97, 82, 67),
new DifficultyRange(HitResult.Ok, 127, 112, 97),
new DifficultyRange(HitResult.Meh, 151, 136, 121),
new DifficultyRange(HitResult.Miss, 188, 173, 158),
2018-04-13 17:19:50 +08:00
};
2019-09-06 14:24:00 +08:00
private double perfect;
private double great;
private double good;
private double ok;
private double meh;
private double miss;
2018-04-13 17:19:50 +08:00
2019-10-09 18:08:31 +08:00
/// <summary>
/// An empty <see cref="HitWindows"/> with only <see cref="HitResult.Miss"/> and <see cref="HitResult.Perfect"/>.
/// No time values are provided (meaning instantaneous hit or miss).
/// </summary>
public static HitWindows Empty => new EmptyHitWindows();
public HitWindows()
{
2019-10-09 18:23:37 +08:00
Debug.Assert(GetRanges().Any(r => r.Result == HitResult.Miss), $"{nameof(GetRanges)} should always contain {nameof(HitResult.Miss)}");
Debug.Assert(GetRanges().Any(r => r.Result != HitResult.Miss), $"{nameof(GetRanges)} should always contain at least one result type other than {nameof(HitResult.Miss)}.");
2019-10-09 18:08:31 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
2018-12-12 18:15:59 +08:00
/// Retrieves the <see cref="HitResult"/> with the largest hit window that produces a successful hit.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-12-12 18:15:59 +08:00
/// <returns>The lowest allowed successful <see cref="HitResult"/>.</returns>
protected HitResult LowestSuccessfulHitResult()
{
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
{
if (IsHitResultAllowed(result))
return result;
}
return HitResult.None;
}
2018-04-13 17:19:50 +08:00
/// <summary>
2019-09-06 14:24:00 +08:00
/// Retrieves a mapping of <see cref="HitResult"/>s to their timing windows for all allowed <see cref="HitResult"/>s.
/// </summary>
2019-09-06 14:24:00 +08:00
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
{
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
{
if (IsHitResultAllowed(result))
2019-09-06 14:24:00 +08:00
yield return (result, WindowFor(result));
}
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Check whether it is possible to achieve the provided <see cref="HitResult"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="result">The result type to check.</param>
/// <returns>Whether the <see cref="HitResult"/> can be achieved.</returns>
public virtual bool IsHitResultAllowed(HitResult result) => true;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Sets hit windows with values that correspond to a difficulty parameter.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="difficulty">The parameter.</param>
2019-09-06 14:24:00 +08:00
public void SetDifficulty(double difficulty)
2018-04-13 17:19:50 +08:00
{
2019-09-06 14:24:00 +08:00
foreach (var range in GetRanges())
{
var value = BeatmapDifficulty.DifficultyRange(difficulty, (range.Min, range.Average, range.Max));
switch (range.Result)
{
case HitResult.Miss:
miss = value;
break;
case HitResult.Meh:
meh = value;
break;
case HitResult.Ok:
ok = value;
break;
case HitResult.Good:
good = value;
break;
case HitResult.Great:
great = value;
break;
case HitResult.Perfect:
perfect = value;
break;
}
}
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Retrieves the <see cref="HitResult"/> for a time offset.
/// </summary>
/// <param name="timeOffset">The time offset.</param>
/// <returns>The hit result, or <see cref="HitResult.None"/> if <paramref name="timeOffset"/> doesn't result in a judgement.</returns>
public HitResult ResultFor(double timeOffset)
{
timeOffset = Math.Abs(timeOffset);
for (var result = HitResult.Perfect; result >= HitResult.Miss; --result)
2018-12-06 20:04:54 +08:00
{
2019-09-06 14:24:00 +08:00
if (IsHitResultAllowed(result) && timeOffset <= WindowFor(result))
2018-12-06 20:04:54 +08:00
return result;
}
2018-04-13 17:19:50 +08:00
return HitResult.None;
}
/// <summary>
2019-09-06 14:24:00 +08:00
/// Retrieves the hit window for a <see cref="HitResult"/>.
/// This is the number of +/- milliseconds allowed for the requested result (so the actual hittable range is double this).
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="result">The expected <see cref="HitResult"/>.</param>
/// <returns>One half of the hit window for <paramref name="result"/>.</returns>
2019-09-06 14:24:00 +08:00
public double WindowFor(HitResult result)
2018-04-13 17:19:50 +08:00
{
switch (result)
{
case HitResult.Perfect:
2019-09-06 14:24:00 +08:00
return perfect;
2019-04-01 11:44:46 +08:00
2018-04-13 17:19:50 +08:00
case HitResult.Great:
2019-09-06 14:24:00 +08:00
return great;
2019-04-01 11:44:46 +08:00
2018-04-13 17:19:50 +08:00
case HitResult.Good:
2019-09-06 14:24:00 +08:00
return good;
2019-04-01 11:44:46 +08:00
2018-04-13 17:19:50 +08:00
case HitResult.Ok:
2019-09-06 14:24:00 +08:00
return ok;
2019-04-01 11:44:46 +08:00
2018-04-13 17:19:50 +08:00
case HitResult.Meh:
2019-09-06 14:24:00 +08:00
return meh;
2019-04-01 11:44:46 +08:00
2018-04-13 17:19:50 +08:00
case HitResult.Miss:
2019-09-06 14:24:00 +08:00
return miss;
2019-04-01 11:44:46 +08:00
2018-04-13 17:19:50 +08:00
default:
2019-11-28 22:21:21 +08:00
throw new ArgumentException("Unknown enum member", nameof(result));
2018-04-13 17:19:50 +08:00
}
}
/// <summary>
/// Given a time offset, whether the <see cref="HitObject"/> can ever be hit in the future with a non-<see cref="HitResult.Miss"/> result.
2019-04-25 16:36:17 +08:00
/// This happens if <paramref name="timeOffset"/> is less than what is required for <see cref="LowestSuccessfulHitResult"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="timeOffset">The time offset.</param>
/// <returns>Whether the <see cref="HitObject"/> can be hit at any point in the future from this time offset.</returns>
2019-09-06 14:24:00 +08:00
public bool CanBeHit(double timeOffset) => timeOffset <= WindowFor(LowestSuccessfulHitResult());
/// <summary>
/// Retrieve a valid list of <see cref="DifficultyRange"/>s representing hit windows.
/// Defaults are provided but can be overridden to customise for a ruleset.
/// </summary>
protected virtual DifficultyRange[] GetRanges() => base_ranges;
2019-10-09 18:08:31 +08:00
public class EmptyHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Perfect, 0, 0, 0),
new DifficultyRange(HitResult.Miss, 0, 0, 0),
};
public override bool IsHitResultAllowed(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
2019-10-09 18:08:31 +08:00
case HitResult.Miss:
return true;
}
return false;
}
protected override DifficultyRange[] GetRanges() => ranges;
}
2019-09-06 14:24:00 +08:00
}
public struct DifficultyRange
{
public readonly HitResult Result;
public double Min;
public double Average;
public double Max;
public DifficultyRange(HitResult result, double min, double average, double max)
{
Result = result;
Min = min;
Average = average;
Max = max;
}
2018-04-13 17:19:50 +08:00
}
}