diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index b9ba6364af..89ba73ba51 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double JumpDistance { get; private set; } /// - /// Normalized Vector from the end position of the previous to the start position of this . + /// Minimum distance from the end position of the previous to the start position of this . /// - public Vector2 JumpVector { get; private set; } + public double MovementDistance { get; private set; } - /// + /// JumpTravel /// Normalized distance between the start and end position of the previous . /// public double TravelDistance { get; private set; } @@ -37,6 +37,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double? Angle { get; private set; } + /// + /// Milliseconds elapsed since the end time of the Previous , with a minimum of 25ms. + /// + public double MovementTime { get; private set; } + + /// + /// Milliseconds elapsed since from the start time of the Previous to the end time of the same Previous , with a minimum of 25ms. + /// + public double TravelTime { get; private set; } + /// /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. /// @@ -51,13 +61,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing this.lastLastObject = (OsuHitObject)lastLastObject; this.lastObject = (OsuHitObject)lastObject; - setDistances(); - // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects. StrainTime = Math.Max(DeltaTime, 25); + + setDistances(clockRate); } - private void setDistances() + private void setDistances(double clockRate) { // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. float scalingFactor = normalized_radius / (float)BaseObject.Radius; @@ -72,6 +82,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { computeSliderCursorPosition(lastSlider); TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; + TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 0); + MovementTime = Math.Max(StrainTime - TravelTime, 0); + MovementDistance = Math.Max(0, Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length - 0) * scalingFactor; } Vector2 lastCursorPosition = getEndCursorPosition(lastObject); @@ -79,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // Don't need to jump to reach spinners if (!(BaseObject is Spinner)) { - JumpVector = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor); - JumpDistance = JumpVector.Length; + JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + MovementDistance = Math.Min(JumpDistance, MovementDistance); } if (lastLastObject != null) @@ -104,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyEndPosition = slider.StackedPosition; - float approxFollowCircleRadius = (float)(slider.Radius * 3); + float approxFollowCircleRadius = (float)(slider.Radius * 2.4); var computeVertex = new Action(t => { double progress = (t - slider.StartTime) / slider.SpanDuration; @@ -117,6 +130,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value; float dist = diff.Length; + slider.LazyTravelTime = t - slider.StartTime; + if (dist > approxFollowCircleRadius) { // The cursor would be outside the follow circle, we need to move it diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 0101dcaee2..e162b25f10 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; using osu.Framework.Utils; -using osuTK; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -23,13 +22,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override int HistoryLength => 2; - private const double wide_angle_multiplier = 1.0; - private const double acute_angle_multiplier = 1.0; - private const double rhythm_variance_multiplier = 1.0; + private const double wide_angle_multiplier = 1.5; + private const double acute_angle_multiplier = 2.0; private double currentStrain = 1; - private double skillMultiplier => 26.25; + private double skillMultiplier => 23.25; private double strainDecayBase => 0.15; private double strainValueOf(DifficultyHitObject current) @@ -41,75 +39,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var osuPrevObj = (OsuDifficultyHitObject)Previous[0]; var osuLastObj = (OsuDifficultyHitObject)Previous[1]; - var currVector = Vector2.Divide(osuCurrObj.JumpVector, (float)osuCurrObj.StrainTime); - var prevVector = Vector2.Divide(osuPrevObj.JumpVector, (float)osuPrevObj.StrainTime); + double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start iwth the base distance / time - // Start with regular velocity. - double aimStrain = currVector.Length; - - if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // Rhythms are the same. + if (osuPrevObj.BaseObject is Slider) // If object is a slider { - if (osuCurrObj.Angle != null) + double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to next note + double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to lazy end. + + currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. + } + + double prevVelocity = osuPrevObj.JumpDistance / osuPrevObj.StrainTime; // do the same for the previous velocity. + + if (osuLastObj.BaseObject is Slider) + { + double movementVelocity = osuPrevObj.MovementDistance / osuPrevObj.MovementTime; + double travelVelocity = osuPrevObj.TravelDistance / osuPrevObj.TravelTime; + + prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); + } + + double angleBonus = 0; + + double aimStrain = currVelocity; // Start strain with regular velocity. + + if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // If rhythms are the same. + { + if (osuCurrObj.Angle != null && osuPrevObj.Angle != null) { - double angle = osuCurrObj.Angle.Value; + double currAngle = osuCurrObj.Angle.Value; + double prevAngle = osuPrevObj.Angle.Value; // Rewarding angles, take the smaller velocity as base. - double angleBonus = Math.Min(currVector.Length, prevVector.Length); + angleBonus = Math.Min(currVelocity, prevVelocity); - double wideAngleBonus = calcWideAngleBonus(angle); - double acuteAngleBonus = calcAcuteAngleBonus(angle); + double wideAngleBonus = calcWideAngleBonus(currAngle); + double acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (osuCurrObj.StrainTime > 100) + if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2. acuteAngleBonus = 0; else - { - acuteAngleBonus *= Math.Min(2, Math.Pow((100 - osuCurrObj.StrainTime) / 15, 1.5)); - wideAngleBonus *= Math.Pow(osuCurrObj.StrainTime / 100, 6); - } + acuteAngleBonus *= calcAcuteAngleBonus(prevAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. + * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime + * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 + * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Min(100, osuCurrObj.JumpDistance) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). - if (acuteAngleBonus > wideAngleBonus) - angleBonus = Math.Min(angleBonus, 150 / osuCurrObj.StrainTime) * Math.Min(1, Math.Pow(Math.Min(osuCurrObj.JumpDistance, osuPrevObj.JumpDistance) / 150, 2)); + wideAngleBonus *= angleBonus * (1 - Math.Pow(calcWideAngleBonus(prevAngle), 3)); // Penalize wide angles if they're repeated, reducing the penalty as the prevAngle gets more acute. - angleBonus *= Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier); - - // add in angle velocity. - aimStrain += angleBonus; + angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the anglebuffs together. } } - else // There is a rhythm change - { - // Rewarding rhythm, take the smaller velocity as base. - double rhythmBonus = Math.Min(currVector.Length, prevVector.Length); - if (osuCurrObj.StrainTime + 10 < osuPrevObj.StrainTime && osuPrevObj.StrainTime > osuLastObj.StrainTime + 10) - // Don't want to reward for a rhythm change back to back (unless its a double, which is why this only checks for fast -> slow -> fast). - rhythmBonus = 0; - - aimStrain += rhythmBonus * rhythm_variance_multiplier; // add in rhythm velocity. - } + aimStrain += angleBonus; // Add in angle bonus. return aimStrain; } - private double calcWideAngleBonus(double angle) - { - if (angle < Math.PI / 3) - return 0; - if (angle < 2 * Math.PI / 3) - return Math.Pow(Math.Sin(1.5 * (angle - Math.PI / 3)), 2); + private double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2); - return 0.25 + 0.75 * Math.Pow(Math.Sin(1.5 * (Math.PI - angle)), 2); - } - - private double calcAcuteAngleBonus(double angle) - { - if (angle < Math.PI / 3) - return 0.5 + 0.5 * Math.Pow(Math.Sin(1.5 * angle), 2); - if (angle < 2 * Math.PI / 3) - return Math.Pow(Math.Sin(1.5 * (2 * Math.PI / 3 - angle)), 2); - - return 0; - } + private double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle); private double applyDiminishingExp(double val) => Math.Pow(val, 0.99); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1d2666f46b..1d494c2917 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// internal float LazyTravelDistance; + /// + /// The time taken by the cursor upon completion of this if it was hit + /// with as few movements as possible. This is set and used by difficulty calculation. + /// + internal double LazyTravelTime; + public List> NodeSamples { get; set; } = new List>(); [JsonIgnore]