Compare commits
343 Commits
@@ -19,6 +19,11 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
|
||||
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
|
||||
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
|
||||
#license header
|
||||
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.903.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1008.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.IO;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkDifficultyCalculation : BenchmarkTest
|
||||
{
|
||||
private DifficultyCalculator osuCalculator = null!;
|
||||
private DifficultyCalculator taikoCalculator = null!;
|
||||
private DifficultyCalculator catchCalculator = null!;
|
||||
private DifficultyCalculator maniaCalculator = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
using var resources = new DllResourceStore(typeof(TestResources).Assembly);
|
||||
|
||||
using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz");
|
||||
using var archiveReader = new ZipArchiveReader(archive);
|
||||
|
||||
var osuBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Gamu) [Insane].osu");
|
||||
var taikoBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (MMzz) [Oni].osu");
|
||||
var catchBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Deif) [Salad].osu");
|
||||
var maniaBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (ExPew) [Another].osu");
|
||||
|
||||
osuCalculator = new OsuRuleset().CreateDifficultyCalculator(osuBeatmap);
|
||||
taikoCalculator = new TaikoRuleset().CreateDifficultyCalculator(taikoBeatmap);
|
||||
catchCalculator = new CatchRuleset().CreateDifficultyCalculator(catchBeatmap);
|
||||
maniaCalculator = new ManiaRuleset().CreateDifficultyCalculator(maniaBeatmap);
|
||||
}
|
||||
|
||||
private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatmapName)
|
||||
{
|
||||
using var beatmapStream = new MemoryStream();
|
||||
archiveReader.GetStream(beatmapName).CopyTo(beatmapStream);
|
||||
|
||||
beatmapStream.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new LineBufferedReader(beatmapStream);
|
||||
|
||||
var decoder = Beatmaps.Formats.Decoder.GetDecoder<Beatmap>(reader);
|
||||
return new FlatWorkingBeatmap(decoder.Decode(reader));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyOsu() => osuCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyTaiko() => taikoCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyCatch() => catchCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyMania() => maniaCalculator.Calculate();
|
||||
|
||||
[Benchmark]
|
||||
public void CalculateDifficultyOsuHundredTimes()
|
||||
{
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
osuCalculator.Calculate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,2 +0,0 @@
|
||||
[General]
|
||||
// no version specified means v1
|
||||
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTinyDropletMissPreservesCatcherState()
|
||||
public void TestTinyDropletMissChangesCatcherState()
|
||||
{
|
||||
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
|
||||
{
|
||||
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}));
|
||||
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
|
||||
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
|
||||
// catcher state and hyper dash state is preserved
|
||||
checkState(CatcherAnimationState.Kiai);
|
||||
// catcher state is changed but hyper dash state is preserved
|
||||
checkState(CatcherAnimationState.Fail);
|
||||
checkHyperDash(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
@@ -51,15 +51,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.35) / Math.Pow(catchAttributes.MaxCombo, 0.35), 1.0);
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
double clockRate = ModUtils.CalculateRateWithMods(score.Mods);
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
|
||||
{
|
||||
public static class MovementEvaluator
|
||||
{
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
|
||||
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
|
||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||
|
||||
double edgeDashBonus = 0;
|
||||
|
||||
// Direction change bonus.
|
||||
if (Math.Abs(catchCurrent.DistanceMoved) > 0.1)
|
||||
{
|
||||
if (current.Index >= 1 && Math.Abs(catchLast.DistanceMoved) > 0.1 && Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchLast.DistanceMoved))
|
||||
{
|
||||
double bonusFactor = Math.Min(50, Math.Abs(catchCurrent.DistanceMoved)) / 50;
|
||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(catchLast.DistanceMoved)) / 70, 0.38);
|
||||
|
||||
distanceAddition += direction_change_bonus / Math.Sqrt(catchLast.StrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||
}
|
||||
|
||||
// Base bonus for every movement, giving some weight to streams.
|
||||
distanceAddition += 12.5 * Math.Min(Math.Abs(catchCurrent.DistanceMoved), CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2)
|
||||
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and NORMALIZED_HALF_CATCHER_WIDTH)
|
||||
if (current.Index >= 2 && Math.Abs(catchCurrent.ExactDistanceMoved) <= CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2
|
||||
&& catchCurrent.ExactDistanceMoved == -catchLast.ExactDistanceMoved && catchLast.ExactDistanceMoved == -catchLastLast.ExactDistanceMoved
|
||||
&& catchCurrent.StrainTime == catchLast.StrainTime && catchLast.StrainTime == catchLastLast.StrainTime)
|
||||
distanceAddition = 0;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,49 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
{
|
||||
public class CatchDifficultyHitObject : DifficultyHitObject
|
||||
{
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
public const float NORMALIZED_HALF_CATCHER_WIDTH = 41.0f;
|
||||
private const float absolute_player_positioning_error = 16.0f;
|
||||
|
||||
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
|
||||
|
||||
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of <see cref="BaseObject"/>.
|
||||
/// </summary>
|
||||
public readonly float NormalizedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of <see cref="LastObject"/>.
|
||||
/// </summary>
|
||||
public readonly float LastNormalizedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of the player required to catch <see cref="BaseObject"/>, assuming the player moves as little as possible.
|
||||
/// </summary>
|
||||
public float PlayerPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized position of the player after catching <see cref="LastObject"/>.
|
||||
/// </summary>
|
||||
public float LastPlayerPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance between <see cref="LastPlayerPosition"/> and <see cref="PlayerPosition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||
/// </remarks>
|
||||
public float DistanceMoved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance the player has to move from <see cref="LastPlayerPosition"/> in order to catch <see cref="BaseObject"/> at its <see cref="NormalizedPosition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
|
||||
/// </remarks>
|
||||
public float ExactDistanceMoved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
|
||||
/// </summary>
|
||||
@@ -29,13 +63,35 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||
float scalingFactor = NORMALIZED_HALF_CATCHER_WIDTH / halfCatcherWidth;
|
||||
|
||||
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
|
||||
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
|
||||
|
||||
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
|
||||
StrainTime = Math.Max(40, DeltaTime);
|
||||
|
||||
setMovementState();
|
||||
}
|
||||
|
||||
private void setMovementState()
|
||||
{
|
||||
LastPlayerPosition = Index == 0 ? LastNormalizedPosition : ((CatchDifficultyHitObject)Previous(0)).PlayerPosition;
|
||||
|
||||
PlayerPosition = Math.Clamp(
|
||||
LastPlayerPosition,
|
||||
NormalizedPosition - (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error),
|
||||
NormalizedPosition + (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error)
|
||||
);
|
||||
|
||||
DistanceMoved = PlayerPosition - LastPlayerPosition;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
ExactDistanceMoved = NormalizedPosition - LastPlayerPosition;
|
||||
|
||||
// After a hyperdash we ARE in the correct position. Always!
|
||||
if (LastObject.HyperDash)
|
||||
PlayerPosition = NormalizedPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// 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.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -11,10 +10,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
{
|
||||
public class Movement : StrainDecaySkill
|
||||
{
|
||||
private const float absolute_player_positioning_error = 16f;
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.2;
|
||||
|
||||
@@ -24,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected readonly float HalfCatcherWidth;
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
/// </summary>
|
||||
@@ -49,80 +38,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var catchCurrent = (CatchDifficultyHitObject)current;
|
||||
|
||||
lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
|
||||
|
||||
float playerPosition = Math.Clamp(
|
||||
lastPlayerPosition.Value,
|
||||
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
|
||||
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
|
||||
);
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
double sqrtStrain = Math.Sqrt(weightedStrainTime);
|
||||
|
||||
double edgeDashBonus = 0;
|
||||
|
||||
// Direction change bonus.
|
||||
if (Math.Abs(distanceMoved) > 0.1)
|
||||
{
|
||||
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
|
||||
{
|
||||
double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
|
||||
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
|
||||
|
||||
distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
|
||||
}
|
||||
|
||||
// Base bonus for every movement, giving some weight to streams.
|
||||
distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
|
||||
}
|
||||
|
||||
// Bonus for edge dashes.
|
||||
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
|
||||
{
|
||||
if (!catchCurrent.LastObject.HyperDash)
|
||||
edgeDashBonus += 5.7;
|
||||
else
|
||||
{
|
||||
// After a hyperdash we ARE in the correct position. Always!
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,20 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
|
||||
}
|
||||
|
||||
// droplet doesn't affect the catcher state
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (hitObject is not Banana)
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
|
||||
// droplet doesn't affect hyperdash state
|
||||
if (hitObject is TinyDroplet) return;
|
||||
|
||||
// if a hyper fruit was already handled this frame, just go where it says to go.
|
||||
@@ -244,19 +257,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
else
|
||||
SetHyperDashState();
|
||||
}
|
||||
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (!(hitObject is Banana))
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRevertResult(JudgementResult result)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||
{
|
||||
public class IndividualStrainEvaluator
|
||||
{
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
|
||||
// We award a bonus if this note starts and ends before the end of another hold note.
|
||||
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||
{
|
||||
if (maniaPrevious is null)
|
||||
continue;
|
||||
|
||||
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||
{
|
||||
holdFactor = 1.25;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 2.0 * holdFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
|
||||
{
|
||||
public class OverallStrainEvaluator
|
||||
{
|
||||
private const double release_threshold = 30;
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
bool isOverlapping = false;
|
||||
|
||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
|
||||
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
|
||||
{
|
||||
if (maniaPrevious is null)
|
||||
continue;
|
||||
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(maniaPrevious.EndTime, startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, maniaPrevious.EndTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1);
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - maniaPrevious.EndTime));
|
||||
}
|
||||
|
||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// holdAddition
|
||||
// ^
|
||||
// 1.0 + - - - - - -+-----------
|
||||
// | /
|
||||
// 0.5 + - - - - -/ Sigmoid Curve
|
||||
// | /|
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
return (1 + holdAddition) * holdFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
|
||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
List<DifficultyHitObject>[] perColumnObjects = new List<DifficultyHitObject>[totalColumns];
|
||||
|
||||
for (int column = 0; column < totalColumns; column++)
|
||||
perColumnObjects[column] = new List<DifficultyHitObject>();
|
||||
|
||||
for (int i = 1; i < sortedObjects.Length; i++)
|
||||
objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));
|
||||
{
|
||||
var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count);
|
||||
objects.Add(currentObject);
|
||||
perColumnObjects[currentObject.Column].Add(currentObject);
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
@@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
|
||||
{
|
||||
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
||||
|
||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||
private readonly List<DifficultyHitObject>[] perColumnObjects;
|
||||
|
||||
private readonly int columnIndex;
|
||||
|
||||
public readonly int Column;
|
||||
|
||||
// The hit object earlier in time than this note in each column
|
||||
public readonly ManiaDifficultyHitObject?[] PreviousHitObjects;
|
||||
|
||||
public readonly double ColumnStrainTime;
|
||||
|
||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, List<DifficultyHitObject>[] perColumnObjects, int index)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
int totalColumns = perColumnObjects.Length;
|
||||
this.perColumnObjects = perColumnObjects;
|
||||
Column = BaseObject.Column;
|
||||
columnIndex = perColumnObjects[Column].Count;
|
||||
PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns];
|
||||
ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime;
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1];
|
||||
|
||||
for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++)
|
||||
PreviousHitObjects[i] = prevNote.PreviousHitObjects[i];
|
||||
|
||||
// intentionally depends on processing order to match live.
|
||||
PreviousHitObjects[prevNote.Column] = prevNote;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The previous object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||
/// </summary>
|
||||
/// <param name="backwardsIndex">The number of notes to go back.</param>
|
||||
/// <returns>The object in this column <paramref name="backwardsIndex"/> notes back, or null if this is the first note in the column.</returns>
|
||||
public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex)
|
||||
{
|
||||
int index = columnIndex - (backwardsIndex + 1);
|
||||
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
|
||||
/// </summary>
|
||||
/// <param name="forwardsIndex">The number of notes to go forward.</param>
|
||||
/// <returns>The object in this column <paramref name="forwardsIndex"/> notes forward, or null if this is the last note in the column.</returns>
|
||||
public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex)
|
||||
{
|
||||
int index = columnIndex + (forwardsIndex + 1);
|
||||
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
{
|
||||
private const double individual_decay_base = 0.125;
|
||||
private const double overall_decay_base = 0.30;
|
||||
private const double release_threshold = 30;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 1;
|
||||
|
||||
private readonly double[] startTimes;
|
||||
private readonly double[] endTimes;
|
||||
private readonly double[] individualStrains;
|
||||
|
||||
private double individualStrain;
|
||||
private double highestIndividualStrain;
|
||||
private double overallStrain;
|
||||
|
||||
public Strain(Mod[] mods, int totalColumns)
|
||||
: base(mods)
|
||||
{
|
||||
startTimes = new double[totalColumns];
|
||||
endTimes = new double[totalColumns];
|
||||
individualStrains = new double[totalColumns];
|
||||
overallStrain = 1;
|
||||
}
|
||||
@@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
var maniaCurrent = (ManiaDifficultyHitObject)current;
|
||||
double startTime = maniaCurrent.StartTime;
|
||||
double endTime = maniaCurrent.EndTime;
|
||||
int column = maniaCurrent.BaseObject.Column;
|
||||
bool isOverlapping = false;
|
||||
|
||||
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
|
||||
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
|
||||
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
|
||||
individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base);
|
||||
individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
for (int i = 0; i < endTimes.Length; ++i)
|
||||
{
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
|
||||
// Take the hardest individualStrain for notes that happen at the same time (in a chord).
|
||||
// This is to ensure the order in which the notes are processed does not affect the resultant total strain.
|
||||
highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column];
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
|
||||
}
|
||||
|
||||
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
|
||||
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
|
||||
// holdAddition
|
||||
// ^
|
||||
// 1.0 + - - - - - -+-----------
|
||||
// | /
|
||||
// 0.5 + - - - - -/ Sigmoid Curve
|
||||
// | /|
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
|
||||
|
||||
// Decay and increase individualStrains in own column
|
||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
||||
individualStrains[column] += 2.0 * holdFactor;
|
||||
|
||||
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
|
||||
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
|
||||
|
||||
// Decay and increase overallStrain
|
||||
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
|
||||
overallStrain += (1 + holdAddition) * holdFactor;
|
||||
|
||||
// Update startTimes and endTimes arrays
|
||||
startTimes[column] = startTime;
|
||||
endTimes[column] = endTime;
|
||||
overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base);
|
||||
overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
|
||||
return individualStrain + overallStrain - CurrentStrain;
|
||||
return highestIndividualStrain + overallStrain - CurrentStrain;
|
||||
}
|
||||
|
||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)
|
||||
=> applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) =>
|
||||
applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||
|
||||
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
return new ManiaArgonSkinTransformer(skin, beatmap);
|
||||
|
||||
case DefaultLegacySkin:
|
||||
case RetroSkin:
|
||||
return new ManiaClassicSkinTransformer(skin, beatmap);
|
||||
|
||||
case LegacySkin:
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
private Drawable noteAnimation = null!;
|
||||
|
||||
private float? minimumColumnWidth;
|
||||
private float? widthForNoteHeightScale;
|
||||
|
||||
public LegacyNotePiece()
|
||||
{
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||
{
|
||||
minimumColumnWidth = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
|
||||
widthForNoteHeightScale = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale))?.Value;
|
||||
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
@@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
// The height is scaled to the minimum column width, if provided.
|
||||
float minimumWidth = minimumColumnWidth ?? DrawWidth;
|
||||
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth);
|
||||
float noteHeight = widthForNoteHeightScale ?? DrawWidth;
|
||||
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, noteHeight), texture.DisplayWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
|
||||
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
|
||||
[TestCase(0.13841532030395723d, 2, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(9.6779746353001634d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
|
||||
[TestCase(9.6491691624112761d, 239, "diffcalc-test")]
|
||||
[TestCase(1.756936832498702d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.57771197086735004d, 4, "very-fast-slider")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
|
||||
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 865 B |
|
Before Width: | Height: | Size: 771 B |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,6 +0,0 @@
|
||||
[General]
|
||||
// no version specified means v1
|
||||
|
||||
[Fonts]
|
||||
HitCircleOverlap: 3
|
||||
ScoreOverlap: 3
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 644 KiB |
@@ -191,6 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestRewind()
|
||||
{
|
||||
AddStep("set manual clock", () => manualClock = new ManualClock
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
public static class AimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 2.6;
|
||||
private const double acute_angle_multiplier = 2.55;
|
||||
private const double slider_multiplier = 1.35;
|
||||
private const double velocity_change_multiplier = 0.75;
|
||||
private const double wiggle_multiplier = 1.02;
|
||||
@@ -34,12 +34,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||
var osuLast2Obj = (OsuDifficultyHitObject)current.Previous(2);
|
||||
|
||||
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
|
||||
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
|
||||
|
||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||
if (osuLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
@@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
}
|
||||
|
||||
// As above, do the same for the previous hitobject.
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.AdjustedDeltaTime;
|
||||
|
||||
if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance)
|
||||
{
|
||||
@@ -69,59 +70,77 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
|
||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||
|
||||
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
|
||||
if (Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) < 1.25 * Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime)) // If rhythms are the same.
|
||||
{
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
// Penalize angle repetition.
|
||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||
|
||||
// Apply full wide angle bonus for distance more than one diameter
|
||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||
|
||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||
acuteAngleBonus *= angleBonus *
|
||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.AdjustedDeltaTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||
}
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
wiggleBonus = angleBonus
|
||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
|
||||
// Penalize angle repetition.
|
||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||
|
||||
// Apply full wide angle bonus for distance more than one diameter
|
||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
wiggleBonus = angleBonus
|
||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
|
||||
if (osuLast2Obj != null)
|
||||
{
|
||||
// If objects just go back and forth through a middle point - don't give as much wide bonus
|
||||
// Use Previous(2) and Previous(0) because angles calculation is done prevprev-prev-curr, so any object's angle's center point is always the previous object
|
||||
var lastBaseObject = (OsuHitObject)osuLastObj.BaseObject;
|
||||
var last2BaseObject = (OsuHitObject)osuLast2Obj.BaseObject;
|
||||
|
||||
float distance = (last2BaseObject.StackedPosition - lastBaseObject.StackedPosition).Length;
|
||||
|
||||
if (distance < 1)
|
||||
{
|
||||
wideAngleBonus *= 1 - 0.35 * (1 - distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||
{
|
||||
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
||||
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
|
||||
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
|
||||
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.AdjustedDeltaTime;
|
||||
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.AdjustedDeltaTime;
|
||||
|
||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
|
||||
double distRatio = DifficultyCalculationUtils.Smoothstep(Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity), 0, 1);
|
||||
|
||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
|
||||
double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), Math.Abs(prevVelocity - currVelocity));
|
||||
|
||||
velocityChangeBonus = overlapVelocityBuff * distRatio;
|
||||
|
||||
// Penalize for rhythm changes.
|
||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
|
||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime) / Math.Max(osuCurrObj.AdjustedDeltaTime, osuLastObj.AdjustedDeltaTime), 2);
|
||||
}
|
||||
|
||||
if (osuLastObj.BaseObject is Slider)
|
||||
@@ -131,9 +150,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
}
|
||||
|
||||
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||
aimStrain += velocityChangeBonus * velocity_change_multiplier;
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
||||
// Add in acute angle bonus or wide angle bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
||||
|
||||
// Apply high circle size bonus
|
||||
aimStrain *= osuCurrObj.SmallCircleBonus;
|
||||
|
||||
// Add in additional slider velocity bonus.
|
||||
if (withSliderTravelDistance)
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
cumulativeStrainTime += lastObj.AdjustedDeltaTime;
|
||||
|
||||
if (!(currentObj.BaseObject is Spinner))
|
||||
{
|
||||
@@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
if (osuCurrent.BaseObject is Slider osuSlider)
|
||||
{
|
||||
// Invert the scaling factor to determine the true travel distance independent of circle size.
|
||||
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
|
||||
double pixelTravelDistance = osuCurrent.LazyTravelDistance / scalingFactor;
|
||||
|
||||
// Reward sliders based on velocity.
|
||||
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
|
||||
|
||||