mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
Refactor hit windows class structure to reduce rigidity
This change pulls back a significant degree of overspecialisation and rigidity in the class structure of `HitWindows` to make subsequent changes to hit windows, whose purpose is to improve replay playback accuracy, possible to do cleanly. Notably: - `HitWindows` is full abstract now. In a few use cases, and as a reference for ruleset implementors, `DefaultHitWindows` is provided as a separate class instead. This fixes the weirdness wherein `HitWindows` always declared 6 fields for result types but some of them would never be set to a non-zero value or read. - `HitWindow.GetRanges()` is deleted because it is overspecialised and prevents being able to adjust hitwindows by ±0.5ms cleanly which will be required later. The fallout of this is that the assertion that used `GetRanges()` in the `HitWindows` ctor must use something else now, and the closest thing to it was `GetAllAvailableWindows()`, which didn't return the miss window - so I made it return the miss window and fixed the one consumer that didn't want it (bar hit error meter) to skip it. - Diff also contains some clean-up around `DifficultyRange` to unify handling of it.
This commit is contained in:
@@ -1,15 +1,30 @@
|
||||
// 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.Linq;
|
||||
using System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
public class ManiaHitWindows : HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
|
||||
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
|
||||
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
|
||||
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
|
||||
private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121);
|
||||
private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158);
|
||||
|
||||
private readonly double multiplier;
|
||||
|
||||
private double perfect;
|
||||
private double great;
|
||||
private double good;
|
||||
private double ok;
|
||||
private double meh;
|
||||
private double miss;
|
||||
|
||||
public ManiaHitWindows()
|
||||
: this(1)
|
||||
{
|
||||
@@ -36,11 +51,41 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
|
||||
new DifficultyRange(
|
||||
r.Result,
|
||||
r.Min * multiplier,
|
||||
r.Average * multiplier,
|
||||
r.Max * multiplier)).ToArray();
|
||||
public override void SetDifficulty(double difficulty)
|
||||
{
|
||||
perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier;
|
||||
great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier;
|
||||
good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier;
|
||||
ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier;
|
||||
meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier;
|
||||
miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier;
|
||||
}
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return perfect;
|
||||
|
||||
case HitResult.Great:
|
||||
return great;
|
||||
|
||||
case HitResult.Good:
|
||||
return good;
|
||||
|
||||
case HitResult.Ok:
|
||||
return ok;
|
||||
|
||||
case HitResult.Meh:
|
||||
return meh;
|
||||
|
||||
case HitResult.Miss:
|
||||
return miss;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,15 +476,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private class TestHitWindows : HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange[] ranges =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 500, 500, 500),
|
||||
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
|
||||
};
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => ranges;
|
||||
public override void SetDifficulty(double difficulty) { }
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Great:
|
||||
return 500;
|
||||
|
||||
case HitResult.Miss:
|
||||
return early_miss_window;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
||||
@@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// <summary>
|
||||
/// The RPM required to clear the spinner at ODs [ 0, 5, 10 ].
|
||||
/// </summary>
|
||||
private static readonly (int min, int mid, int max) clear_rpm_range = (90, 150, 225);
|
||||
private static readonly DifficultyRange clear_rpm_range = new DifficultyRange(90, 150, 225);
|
||||
|
||||
/// <summary>
|
||||
/// The RPM required to complete the spinner and receive full score at ODs [ 0, 5, 10 ].
|
||||
/// </summary>
|
||||
private static readonly (int min, int mid, int max) complete_rpm_range = (250, 380, 430);
|
||||
private static readonly DifficultyRange complete_rpm_range = new DifficultyRange(250, 380, 430);
|
||||
|
||||
public double EndTime
|
||||
{
|
||||
|
||||
@@ -373,10 +373,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
preempt /= rate;
|
||||
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
|
||||
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, OsuHitWindows.GREAT_WINDOW_RANGE);
|
||||
greatHitWindow /= rate;
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, OsuHitWindows.GREAT_WINDOW_RANGE);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuHitWindows : HitWindows
|
||||
{
|
||||
public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(80, 50, 20);
|
||||
public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(140, 100, 60);
|
||||
public static readonly DifficultyRange MEH_WINDOW_RANGE = new DifficultyRange(200, 150, 100);
|
||||
|
||||
/// <summary>
|
||||
/// osu! ruleset has a fixed miss window regardless of difficulty settings.
|
||||
/// </summary>
|
||||
public const double MISS_WINDOW = 400;
|
||||
|
||||
internal static readonly DifficultyRange[] OSU_RANGES =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 80, 50, 20),
|
||||
new DifficultyRange(HitResult.Ok, 140, 100, 60),
|
||||
new DifficultyRange(HitResult.Meh, 200, 150, 100),
|
||||
new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
|
||||
};
|
||||
private double great;
|
||||
private double ok;
|
||||
private double meh;
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
{
|
||||
@@ -34,6 +36,32 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
|
||||
public override void SetDifficulty(double difficulty)
|
||||
{
|
||||
great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE);
|
||||
ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE);
|
||||
meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MEH_WINDOW_RANGE);
|
||||
}
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Great:
|
||||
return great;
|
||||
|
||||
case HitResult.Ok:
|
||||
return ok;
|
||||
|
||||
case HitResult.Meh:
|
||||
return meh;
|
||||
|
||||
case HitResult.Miss:
|
||||
return MISS_WINDOW;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 });
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 });
|
||||
|
||||
var hitWindows = new HitWindows();
|
||||
var hitWindows = new DefaultHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
public class TaikoHitWindows : HitWindows
|
||||
{
|
||||
internal static readonly DifficultyRange[] TAIKO_RANGES =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 50, 35, 20),
|
||||
new DifficultyRange(HitResult.Ok, 120, 80, 50),
|
||||
new DifficultyRange(HitResult.Miss, 135, 95, 70),
|
||||
};
|
||||
public static readonly DifficultyRange GREAT_WINDOW_RANGE = new DifficultyRange(50, 35, 20);
|
||||
public static readonly DifficultyRange OK_WINDOW_RANGE = new DifficultyRange(120, 80, 50);
|
||||
public static readonly DifficultyRange MISS_WINDOW_RANGE = new DifficultyRange(135, 95, 70);
|
||||
|
||||
private double great;
|
||||
private double ok;
|
||||
private double miss;
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
{
|
||||
@@ -27,6 +30,29 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => TAIKO_RANGES;
|
||||
public override void SetDifficulty(double difficulty)
|
||||
{
|
||||
great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, GREAT_WINDOW_RANGE);
|
||||
ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, OK_WINDOW_RANGE);
|
||||
miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, MISS_WINDOW_RANGE);
|
||||
}
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Great:
|
||||
return great;
|
||||
|
||||
case HitResult.Ok:
|
||||
return ok;
|
||||
|
||||
case HitResult.Miss:
|
||||
return miss;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,10 +274,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
|
||||
var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE);
|
||||
greatHitWindow /= rate;
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, TaikoHitWindows.GREAT_WINDOW_RANGE);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
|
||||
protected override HitWindows CreateHitWindows() => new HitWindows();
|
||||
protected override HitWindows CreateHitWindows() => new DefaultHitWindows();
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
public void TestResultIfOnlyParentHitWindowIsEmpty()
|
||||
{
|
||||
var testObject = new TestHitObject(HitWindows.Empty);
|
||||
HitObject nested = new TestHitObject(new HitWindows());
|
||||
HitObject nested = new TestHitObject(new DefaultHitWindows());
|
||||
testObject.AddNested(nested);
|
||||
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
|
||||
|
||||
@@ -43,8 +43,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestResultIfParentHitWindowsIsNotEmpty()
|
||||
{
|
||||
var testObject = new TestHitObject(new HitWindows());
|
||||
HitObject nested = new TestHitObject(new HitWindows());
|
||||
var testObject = new TestHitObject(new DefaultHitWindows());
|
||||
HitObject nested = new TestHitObject(new DefaultHitWindows());
|
||||
testObject.AddNested(nested);
|
||||
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
HitObject nested = new TestHitObject(HitWindows.Empty);
|
||||
firstObject.AddNested(nested);
|
||||
|
||||
var secondObject = new TestHitObject(new HitWindows());
|
||||
var secondObject = new TestHitObject(new DefaultHitWindows());
|
||||
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject, secondObject };
|
||||
|
||||
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
|
||||
|
||||
@@ -65,30 +65,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
StartTime = t += spacing,
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
StartTime = t += spacing,
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
StartTime = t += spacing,
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
StartTime = t += spacing,
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
StartTime = t += spacing,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
|
||||
|
||||
@@ -92,8 +92,8 @@ namespace osu.Game.Beatmaps
|
||||
/// </list>
|
||||
/// </param>
|
||||
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
|
||||
static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
|
||||
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
|
||||
static double DifficultyRange(double difficulty, DifficultyRange range)
|
||||
=> DifficultyRange(difficulty, range.Min, range.Mid, range.Max);
|
||||
|
||||
/// <summary>
|
||||
/// Inverse function to <see cref="DifficultyRange(double,double,double,double)"/>.
|
||||
@@ -110,5 +110,23 @@ namespace osu.Game.Beatmaps
|
||||
? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5
|
||||
: (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inverse function to <see cref="DifficultyRange(double,osu.Game.Beatmaps.DifficultyRange)"/>.
|
||||
/// Maps a value returned by the function above back to the difficulty that produced it.
|
||||
/// </summary>
|
||||
/// <param name="difficultyValue">The difficulty-dependent value to be unmapped.</param>
|
||||
/// <param name="range">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
|
||||
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
|
||||
static double InverseDifficultyRange(double difficultyValue, DifficultyRange range)
|
||||
=> InverseDifficultyRange(difficultyValue, range.Min, range.Mid, range.Max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a piecewise-linear difficulty curve for a given gameplay quantity.
|
||||
/// </summary>
|
||||
/// <param name="Min">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
|
||||
/// <param name="Mid">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
|
||||
/// <param name="Max">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
|
||||
public record struct DifficultyRange(double Min, double Mid, double Max);
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
protected virtual HitWindows CreateHitWindows() => new HitWindows();
|
||||
protected virtual HitWindows CreateHitWindows() => new DefaultHitWindows();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="HitObject"/> can be judged.
|
||||
|
||||
66
osu.Game/Rulesets/Scoring/DefaultHitWindows.cs
Normal file
66
osu.Game/Rulesets/Scoring/DefaultHitWindows.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
/// <summary>
|
||||
/// An example implementation of <see cref="HitWindows"/>.
|
||||
/// Not meaningfully used, provided mostly as a reference to ruleset implementors.
|
||||
/// </summary>
|
||||
public class DefaultHitWindows : HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
|
||||
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
|
||||
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
|
||||
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
|
||||
private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121);
|
||||
private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158);
|
||||
|
||||
private double perfect;
|
||||
private double great;
|
||||
private double good;
|
||||
private double ok;
|
||||
private double meh;
|
||||
private double miss;
|
||||
|
||||
public override void SetDifficulty(double difficulty)
|
||||
{
|
||||
perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range);
|
||||
great = IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range);
|
||||
good = IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range);
|
||||
ok = IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range);
|
||||
meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range);
|
||||
miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range);
|
||||
}
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return perfect;
|
||||
|
||||
case HitResult.Great:
|
||||
return great;
|
||||
|
||||
case HitResult.Good:
|
||||
return good;
|
||||
|
||||
case HitResult.Ok:
|
||||
return ok;
|
||||
|
||||
case HitResult.Meh:
|
||||
return meh;
|
||||
|
||||
case HitResult.Miss:
|
||||
return miss;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
@@ -13,35 +12,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <summary>
|
||||
/// A structure containing timing data for hit window based gameplay.
|
||||
/// </summary>
|
||||
public class HitWindows
|
||||
public abstract class HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange[] base_ranges =
|
||||
{
|
||||
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),
|
||||
};
|
||||
|
||||
private double perfect;
|
||||
private double great;
|
||||
private double good;
|
||||
private double ok;
|
||||
private double meh;
|
||||
private double miss;
|
||||
|
||||
/// <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 { get; } = new EmptyHitWindows();
|
||||
|
||||
public HitWindows()
|
||||
protected HitWindows()
|
||||
{
|
||||
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)}.");
|
||||
var availableWindows = GetAllAvailableWindows();
|
||||
Debug.Assert(availableWindows.Any(r => r.result == HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain {nameof(HitResult.Miss)}");
|
||||
Debug.Assert(availableWindows.Any(r => r.result != HitResult.Miss), $"{nameof(GetAllAvailableWindows)} should always contain at least one result type other than {nameof(HitResult.Miss)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +47,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
|
||||
{
|
||||
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
|
||||
for (var result = HitResult.Miss; result <= HitResult.Perfect; ++result)
|
||||
{
|
||||
if (IsHitResultAllowed(result))
|
||||
yield return (result, WindowFor(result));
|
||||
@@ -82,40 +65,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Sets hit windows with values that correspond to a difficulty parameter.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The parameter.</param>
|
||||
public void SetDifficulty(double difficulty)
|
||||
{
|
||||
foreach (var range in GetRanges())
|
||||
{
|
||||
double value = IBeatmapDifficultyInfo.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
public abstract void SetDifficulty(double difficulty);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="HitResult"/> for a time offset.
|
||||
@@ -141,35 +91,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
/// <param name="result">The expected <see cref="HitResult"/>.</param>
|
||||
/// <returns>One half of the hit window for <paramref name="result"/>.</returns>
|
||||
public double WindowFor(HitResult result)
|
||||
{
|
||||
if (!IsHitResultAllowed(result))
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, $@"{result} is not an allowed result.");
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return perfect;
|
||||
|
||||
case HitResult.Great:
|
||||
return great;
|
||||
|
||||
case HitResult.Good:
|
||||
return good;
|
||||
|
||||
case HitResult.Ok:
|
||||
return ok;
|
||||
|
||||
case HitResult.Meh:
|
||||
return meh;
|
||||
|
||||
case HitResult.Miss:
|
||||
return miss;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
public abstract double WindowFor(HitResult 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.
|
||||
@@ -179,41 +101,13 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <returns>Whether the <see cref="HitObject"/> can be hit at any point in the future from this time offset.</returns>
|
||||
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;
|
||||
|
||||
private 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) => true;
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => ranges;
|
||||
}
|
||||
}
|
||||
public override void SetDifficulty(double difficulty) { }
|
||||
|
||||
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;
|
||||
public override double WindowFor(HitResult result) => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
const int bar_width = 2;
|
||||
const float chevron_size = 8;
|
||||
|
||||
hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
||||
hitWindows = HitWindows.GetAllAvailableWindows().Where(w => w.result.IsHit()).ToArray();
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
|
||||
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Utility
|
||||
|
||||
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
}, null, null);
|
||||
|
||||
Hit?.Invoke(HitEvent.Value);
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace osu.Game.Screens.Utility
|
||||
|
||||
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
HitWindows = new DefaultHitWindows(),
|
||||
}, null, null);
|
||||
|
||||
Hit?.Invoke(HitEvent.Value);
|
||||
|
||||
Reference in New Issue
Block a user