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