1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-22 04:09:54 +08:00

Compare commits

..

343 Commits

381 changed files with 11254 additions and 1863 deletions
+5
View File
@@ -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.
+1 -1
View File
@@ -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();
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

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);
}
}
}
+14 -14
View File
@@ -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);
+1
View File
@@ -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());
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

@@ -1,6 +0,0 @@
[General]
// no version specified means v1
[Fonts]
HitCircleOverlap: 3
ScoreOverlap: 3
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

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);

Some files were not shown because too many files have changed in this diff Show More