2019-01-24 16:43:03 +08: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.
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
2019-08-30 17:35:06 +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.
2019-08-30 17:35:06 +08:00
/// </summary>
/// <returns></returns>
2019-09-06 14:24:00 +08:00
public IEnumerable < ( HitResult result , double length ) > GetAllAvailableWindows ( )
2019-08-30 17:35:06 +08:00
{
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 ) ) ;
2019-08-30 17:35:06 +08:00
}
}
2018-04-13 17:19:50 +08:00
/// <summary>
2018-12-12 14:11:03 +08:00
/// Check whether it is possible to achieve the provided <see cref="HitResult"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-12-12 14:10:47 +08:00
/// <param name="result">The result type to check.</param>
/// <returns>Whether the <see cref="HitResult"/> can be achieved.</returns>
2019-09-06 14:37:30 +08:00
public virtual bool IsHitResultAllowed ( HitResult result ) = > true ;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-05-11 14:30:26 +08:00
/// 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 ) ;
2018-12-10 00:38:19 +08:00
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 :
throw new ArgumentException ( nameof ( result ) ) ;
}
}
/// <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 . Great :
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
}
}