diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs
index 1736912e1f..068564d50c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs
@@ -38,7 +38,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; i++)
- yield return new OsuDifficultyHitObject(objects[i], objects[i - 1], timeRate);
+ {
+ var lastLast = i > 1 ? objects[i - 2] : null;
+ var last = objects[i - 1];
+ var current = objects[i];
+
+ yield return new OsuDifficultyHitObject(lastLast, last, current, timeRate);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 91d6c3d7a3..4e9ac26dc5 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -40,14 +40,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
///
public double StrainTime { get; private set; }
+ ///
+ /// Angle the player has to take to hit this .
+ /// Calculated as the angle between the circles (current-2, current-1, current).
+ ///
+ public double? Angle { get; private set; }
+
+ private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
private readonly double timeRate;
///
/// Initializes the object calculating extra data required for difficulty calculation.
///
- public OsuDifficultyHitObject(OsuHitObject currentObject, OsuHitObject lastObject, double timeRate)
+ public OsuDifficultyHitObject(OsuHitObject lastLastObject, OsuHitObject lastObject, OsuHitObject currentObject, double timeRate)
{
+ this.lastLastObject = lastLastObject;
this.lastObject = lastObject;
this.timeRate = timeRate;
@@ -68,20 +76,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus;
}
- Vector2 lastCursorPosition = lastObject.StackedPosition;
-
- var lastSlider = lastObject as Slider;
- if (lastSlider != null)
+ if (lastObject is Slider lastSlider)
{
computeSliderCursorPosition(lastSlider);
- lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
-
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
}
+ Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
+
// Don't need to jump to reach spinners
if (!(BaseObject is Spinner))
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+
+ if (lastLastObject != null)
+ {
+ Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
+
+ Vector2 v1 = lastLastCursorPosition - lastObject.StackedPosition;
+ Vector2 v2 = BaseObject.StackedPosition - lastCursorPosition;
+
+ float dot = Vector2.Dot(v1, v2);
+ float det = v1.X * v2.Y - v1.Y * v2.X;
+
+ Angle = Math.Abs(Math.Atan2(det, dot));
+ }
}
private void setTimingValues()
@@ -127,5 +145,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
computeVertex(time);
computeVertex(slider.EndTime);
}
+
+ private Vector2 getEndCursorPosition(OsuHitObject hitObject)
+ {
+ Vector2 pos = hitObject.StackedPosition;
+
+ var slider = hitObject as Slider;
+ if (slider != null)
+ {
+ computeSliderCursorPosition(slider);
+ pos = slider.LazyEndPosition ?? pos;
+ }
+
+ return pos;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index 9e5f13de62..b5e57985e9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -11,10 +11,39 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Aim : Skill
{
+ private const double angle_bonus_begin = Math.PI / 3;
+ private const double timing_threshold = 107;
+
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
protected override double StrainValueOf(OsuDifficultyHitObject current)
- => (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.StrainTime;
+ {
+ double result = 0;
+
+ const double scale = 90;
+
+ double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
+
+ if (Previous.Count > 0)
+ {
+ if (current.Angle != null && current.Angle.Value > angle_bonus_begin)
+ {
+ var angleBonus = Math.Sqrt(
+ Math.Max(Previous[0].JumpDistance - scale, 0)
+ * Math.Pow(Math.Sin(current.Angle.Value - angle_bonus_begin), 2)
+ * Math.Max(current.JumpDistance - scale, 0));
+ result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, Previous[0].StrainTime);
+ }
+ }
+
+ double jumpDistanceExp = applyDiminishingExp(current.JumpDistance);
+ double travelDistanceExp = applyDiminishingExp(current.TravelDistance);
+
+ return Math.Max(
+ result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(current.StrainTime, timing_threshold),
+ (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / current.StrainTime
+ );
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs
index 436d3d0853..2f23552eb9 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public abstract class Skill
{
+ protected const double SINGLE_SPACING_THRESHOLD = 125;
+ protected const double STREAM_SPACING_THRESHOLD = 110;
+
///
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
///
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 809632806b..e78691ce53 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
@@ -10,30 +11,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
public class Speed : Skill
{
+ private const double angle_bonus_begin = 5 * Math.PI / 6;
+ private const double pi_over_4 = Math.PI / 4;
+ private const double pi_over_2 = Math.PI / 2;
+
protected override double SkillMultiplier => 1400;
protected override double StrainDecayBase => 0.3;
- private const double single_spacing_threshold = 125;
- private const double stream_spacing_threshold = 110;
- private const double almost_diameter = 90;
+ private const double min_speed_bonus = 75; // ~200BPM
+ private const double max_speed_bonus = 45; // ~330BPM
+ private const double speed_balancing_factor = 40;
protected override double StrainValueOf(OsuDifficultyHitObject current)
{
- double distance = current.TravelDistance + current.JumpDistance;
+ double distance = Math.Min(SINGLE_SPACING_THRESHOLD, current.TravelDistance + current.JumpDistance);
+ double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime);
- double speedValue;
- if (distance > single_spacing_threshold)
- speedValue = 2.5;
- else if (distance > stream_spacing_threshold)
- speedValue = 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold);
- else if (distance > almost_diameter)
- speedValue = 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter);
- else if (distance > almost_diameter / 2)
- speedValue = 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2);
- else
- speedValue = 0.95;
+ double speedBonus = 1.0;
+ if (deltaTime < min_speed_bonus)
+ speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2);
- return speedValue / current.StrainTime;
+ double angleBonus = 1.0;
+ if (current.Angle != null && current.Angle.Value < angle_bonus_begin)
+ {
+ angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - current.Angle.Value)), 2) / 3.57;
+ if (current.Angle.Value < pi_over_2)
+ {
+ angleBonus = 1.28;
+ if (distance < 90 && current.Angle.Value < pi_over_4)
+ angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
+ else if (distance < 90)
+ angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - current.Angle.Value) / pi_over_4);
+ }
+ }
+
+ return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / SINGLE_SPACING_THRESHOLD, 3.5)) / current.StrainTime;
}
}
}