mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 12:53:11 +08:00
Merge pull request #25415 from Poyo-SSB-forks/unify-ur
Change unstable rate calculation to account for rate-change mods
This commit is contained in:
commit
30309cdf11
@ -20,7 +20,7 @@ namespace osu.Game.Tests.NonVisual.Ranking
|
||||
public void TestDistributedHits()
|
||||
{
|
||||
var events = Enumerable.Range(-5, 11)
|
||||
.Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null));
|
||||
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null));
|
||||
|
||||
var unstableRate = new UnstableRate(events);
|
||||
|
||||
@ -33,14 +33,46 @@ namespace osu.Game.Tests.NonVisual.Ranking
|
||||
{
|
||||
var events = new[]
|
||||
{
|
||||
new HitEvent(-100, HitResult.Miss, new HitObject(), null, null),
|
||||
new HitEvent(0, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null),
|
||||
new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null),
|
||||
new HitEvent(0, 1.0, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null),
|
||||
};
|
||||
|
||||
var unstableRate = new UnstableRate(events);
|
||||
|
||||
Assert.AreEqual(0, unstableRate.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStaticRateChange()
|
||||
{
|
||||
var events = new[]
|
||||
{
|
||||
new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null),
|
||||
};
|
||||
|
||||
var unstableRate = new UnstableRate(events);
|
||||
|
||||
Assert.AreEqual(10 * 100, unstableRate.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDynamicRateChange()
|
||||
{
|
||||
var events = new[]
|
||||
{
|
||||
new HitEvent(-50, 0.5, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(75, 0.75, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(-100, 1.0, HitResult.Great, new HitObject(), null, null),
|
||||
new HitEvent(125, 1.25, HitResult.Great, new HitObject(), null, null),
|
||||
};
|
||||
|
||||
var unstableRate = new UnstableRate(events);
|
||||
|
||||
Assert.AreEqual(10 * 100, unstableRate.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -56,6 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
scoreProcessor.RevertResult(
|
||||
new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
|
||||
{
|
||||
GameplayRate = 1.0,
|
||||
TimeOffset = 25,
|
||||
Type = HitResult.Perfect,
|
||||
});
|
||||
@ -80,6 +82,27 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("UR = 250", () => counter.Current.Value == 250.0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStaticRateChange()
|
||||
{
|
||||
AddStep("Create Display", recreateDisplay);
|
||||
|
||||
AddRepeatStep("Set UR to 250 at 1.5x", () => applyJudgement(25, true, 1.5), 4);
|
||||
|
||||
AddUntilStep("UR = 250/1.5", () => counter.Current.Value == Math.Round(250.0 / 1.5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDynamicRateChange()
|
||||
{
|
||||
AddStep("Create Display", recreateDisplay);
|
||||
|
||||
AddRepeatStep("Set UR to 100 at 1.0x", () => applyJudgement(10, true, 1.0), 4);
|
||||
AddRepeatStep("Bring UR to 100 at 1.5x", () => applyJudgement(15, true, 1.5), 4);
|
||||
|
||||
AddUntilStep("UR = 100", () => counter.Current.Value == 100.0);
|
||||
}
|
||||
|
||||
private void recreateDisplay()
|
||||
{
|
||||
Clear();
|
||||
@ -92,7 +115,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
private void applyJudgement(double offsetMs, bool alt)
|
||||
private void applyJudgement(double offsetMs, bool alt, double gameplayRate = 1.0)
|
||||
{
|
||||
double placement = offsetMs;
|
||||
|
||||
@ -105,6 +128,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
|
||||
{
|
||||
TimeOffset = placement,
|
||||
GameplayRate = gameplayRate,
|
||||
Type = HitResult.Perfect,
|
||||
});
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
[Test]
|
||||
public void TestAroundCentre()
|
||||
{
|
||||
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, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -57,12 +57,12 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
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),
|
||||
new HitEvent(-7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(-6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(-5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
new HitEvent(7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null),
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
: offset > 16 ? HitResult.Good
|
||||
: offset > 8 ? HitResult.Great
|
||||
: HitResult.Perfect;
|
||||
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||
return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null);
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
: offset > 8 ? HitResult.Great
|
||||
: HitResult.Perfect;
|
||||
|
||||
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||
return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null);
|
||||
});
|
||||
var narrow = CreateDistributedHitEvents(0, 50).Select(h =>
|
||||
{
|
||||
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
: offset > 10 ? HitResult.Good
|
||||
: offset > 5 ? HitResult.Great
|
||||
: HitResult.Perfect;
|
||||
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
|
||||
return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null);
|
||||
});
|
||||
createTest(wide.Concat(narrow).ToList());
|
||||
}
|
||||
@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
[Test]
|
||||
public void TestZeroTimeOffset()
|
||||
{
|
||||
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
|
||||
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -129,9 +129,9 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
createTest(Enumerable.Range(0, 100).Select(i =>
|
||||
{
|
||||
if (i % 2 == 0)
|
||||
return new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null);
|
||||
return new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null);
|
||||
|
||||
return new HitEvent(30, HitResult.Miss, placeholder_object, placeholder_object, null);
|
||||
return new HitEvent(30, 1.0, HitResult.Miss, placeholder_object, placeholder_object, null);
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null));
|
||||
hitEvents.Add(new HitEvent(centre + i - range, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null));
|
||||
}
|
||||
|
||||
return hitEvents;
|
||||
|
@ -54,6 +54,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </remarks>
|
||||
public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime();
|
||||
|
||||
/// <summary>
|
||||
/// The gameplay rate at the time this <see cref="JudgementResult"/> occurred.
|
||||
/// </summary>
|
||||
public double? GameplayRate { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo prior to this <see cref="JudgementResult"/> occurring.
|
||||
/// </summary>
|
||||
|
@ -704,6 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
|
||||
Result.RawTime = Time.Current;
|
||||
Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate;
|
||||
|
||||
if (Result.HasResult)
|
||||
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
|
||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public readonly double TimeOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The true gameplay rate at the time of the event.
|
||||
/// </summary>
|
||||
public readonly double? GameplayRate;
|
||||
|
||||
/// <summary>
|
||||
/// The hit result.
|
||||
/// </summary>
|
||||
@ -46,12 +51,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
/// <param name="timeOffset">The time offset from the end of <paramref name="hitObject"/> at which the event occurs.</param>
|
||||
/// <param name="result">The <see cref="HitResult"/>.</param>
|
||||
/// <param name="gameplayRate">The true gameplay rate at the time of the event.</param>
|
||||
/// <param name="hitObject">The <see cref="HitObject"/> that triggered the event.</param>
|
||||
/// <param name="lastHitObject">The previous <see cref="HitObject"/>.</param>
|
||||
/// <param name="position">A position corresponding to the event.</param>
|
||||
public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position)
|
||||
public HitEvent(double timeOffset, double? gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position)
|
||||
{
|
||||
TimeOffset = timeOffset;
|
||||
GameplayRate = gameplayRate;
|
||||
Result = result;
|
||||
HitObject = hitObject;
|
||||
LastHitObject = lastHitObject;
|
||||
@ -63,6 +70,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
/// <param name="positionOffset">The positional offset.</param>
|
||||
/// <returns>The new <see cref="HitEvent"/>.</returns>
|
||||
public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset);
|
||||
public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, GameplayRate, Result, HitObject, LastHitObject, positionOffset);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
@ -18,7 +19,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </returns>
|
||||
public static double? CalculateUnstableRate(this IEnumerable<HitEvent> hitEvents)
|
||||
{
|
||||
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
|
||||
Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null));
|
||||
|
||||
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
|
||||
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray();
|
||||
return 10 * standardDeviation(timeOffsets);
|
||||
}
|
||||
|
||||
|
@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="result">The <see cref="JudgementResult"/> to describe.</param>
|
||||
/// <returns>The <see cref="HitEvent"/>.</returns>
|
||||
protected virtual HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null);
|
||||
=> new HitEvent(result.TimeOffset, result.GameplayRate, result.Type, result.HitObject, lastHitObject, null);
|
||||
|
||||
protected sealed override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
|
@ -224,7 +224,7 @@ namespace osu.Game.Screens.Utility
|
||||
.FadeOut(duration)
|
||||
.ScaleTo(1.5f, duration);
|
||||
|
||||
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject
|
||||
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
}, null, null);
|
||||
|
@ -186,7 +186,7 @@ namespace osu.Game.Screens.Utility
|
||||
.FadeOut(duration / 2)
|
||||
.ScaleTo(1.5f, duration / 2);
|
||||
|
||||
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject
|
||||
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject
|
||||
{
|
||||
HitWindows = new HitWindows(),
|
||||
}, null, null);
|
||||
|
Loading…
Reference in New Issue
Block a user