From 8eba94e8c9a31300877fd7b5b8aa895571a35369 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 18 Feb 2019 14:46:32 +0900 Subject: [PATCH] Implement new difficulty calculator for Rulesets.Catch --- .../CatchDifficultyCalculatorTest.cs | 2 +- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Difficulty/CatchDifficultyAttributes.cs | 4 +- .../Difficulty/CatchDifficultyCalculator.cs | 81 ++++++++++ .../Difficulty/CatchDifficultyHitObject.cs | 130 --------------- .../CatchLegacyDifficultyCalculator.cs | 148 ------------------ .../Preprocessing/CatchDifficultyHitObject.cs | 68 ++++++++ .../Difficulty/Skills/Movement.cs | 63 ++++++++ .../Objects/CatchHitObject.cs | 5 + 9 files changed, 221 insertions(+), 282 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs delete mode 100644 osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs delete mode 100644 osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs create mode 100644 osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs create mode 100644 osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 84f4fd9c99..61fb4ca5d1 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests public void Test(double expected, string name) => base.Test(expected, name); - protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(new CatchRuleset(), beatmap); + protected override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap); protected override Ruleset CreateRuleset() => new CatchRuleset(); } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9b2bbc9bf7..c539a47897 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Catch public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o }; - public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchLegacyDifficultyCalculator(this, beatmap); + public override LegacyDifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); public override int? LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index c6518cbf8a..8669543841 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty public double ApproachRate; public int MaxCombo; - public CatchDifficultyAttributes(Mod[] mods, double starRating) - : base(mods, starRating) + public CatchDifficultyAttributes(Mod[] mods) + : base(mods) { } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs new file mode 100644 index 0000000000..99702235d5 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -0,0 +1,81 @@ +// 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 System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; +using osu.Game.Rulesets.Catch.Difficulty.Skills; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Catch.Difficulty +{ + public class CatchDifficultyCalculator : DifficultyCalculator + { + private const double star_scaling_factor = 0.145; + + protected override int SectionLength => 750; + + private readonly float halfCatchWidth; + + public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty); + halfCatchWidth = catcher.CatchWidth * 0.5f; + } + + protected override void PopulateAttributes(DifficultyAttributes attributes, IBeatmap beatmap, Skill[] skills, double timeRate) + { + var catchAttributes = (CatchDifficultyAttributes)attributes; + + // this is the same as osu!, so there's potential to share the implementation... maybe + double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; + + catchAttributes.StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor; + catchAttributes.ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0; + catchAttributes.MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)); + } + + protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double timeRate) + { + CatchHitObject lastObject = null; + + foreach (var hitObject in beatmap.HitObjects.OfType()) + { + if (lastObject == null) + { + lastObject = hitObject; + continue; + } + + switch (hitObject) + { + // We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations. + case Fruit fruit: + yield return new CatchDifficultyHitObject(fruit, lastObject, timeRate, halfCatchWidth); + break; + case JuiceStream _: + foreach (var nested in hitObject.NestedHitObjects.OfType().Where(o => !(o is TinyDroplet))) + yield return new CatchDifficultyHitObject(nested, lastObject, timeRate, halfCatchWidth); + break; + } + + lastObject = hitObject; + } + } + + protected override Skill[] CreateSkills() => new Skill[] + { + new Movement(), + }; + + protected override DifficultyAttributes CreateDifficultyAttributes(Mod[] mods) => new CatchDifficultyAttributes(mods); + } +} diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs deleted file mode 100644 index fb16e117b1..0000000000 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyHitObject.cs +++ /dev/null @@ -1,130 +0,0 @@ -// 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.Catch.Objects; -using osu.Game.Rulesets.Catch.UI; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Difficulty -{ - public class CatchDifficultyHitObject - { - internal static readonly double DECAY_BASE = 0.20; - private const float normalized_hitobject_radius = 41.0f; - private const float absolute_player_positioning_error = 16f; - private readonly float playerPositioningError; - - internal CatchHitObject BaseHitObject; - - /// - /// Measures jump difficulty. CtB doesn't have something like button pressing speed or accuracy - /// - internal double Strain = 1; - - /// - /// This is required to keep track of lazy player movement (always moving only as far as necessary) - /// Without this quick repeat sliders / weirdly shaped streams might become ridiculously overrated - /// - internal float PlayerPositionOffset; - internal float LastMovement; - - internal float NormalizedPosition; - internal float ActualNormalizedPosition => NormalizedPosition + PlayerPositionOffset; - - internal CatchDifficultyHitObject(CatchHitObject baseHitObject, float catcherWidthHalf) - { - BaseHitObject = baseHitObject; - - // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. - float scalingFactor = normalized_hitobject_radius / catcherWidthHalf; - - playerPositioningError = absolute_player_positioning_error; // * scalingFactor; - NormalizedPosition = baseHitObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; - } - - private const double direction_change_bonus = 12.5; - internal void CalculateStrains(CatchDifficultyHitObject previousHitObject, double timeRate) - { - // Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make. - // See Taiko feedback thread. - double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; - double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000); - - // Update new position with lazy movement. - PlayerPositionOffset = - MathHelper.Clamp( - previousHitObject.ActualNormalizedPosition, - NormalizedPosition - (normalized_hitobject_radius - playerPositioningError), - NormalizedPosition + (normalized_hitobject_radius - playerPositioningError)) // Obtain new lazy position, but be stricter by allowing for an error of a certain degree of the player. - - NormalizedPosition; // Subtract HitObject position to obtain offset - - LastMovement = DistanceTo(previousHitObject); - double addition = spacingWeight(LastMovement); - - if (NormalizedPosition < previousHitObject.NormalizedPosition) - { - LastMovement = -LastMovement; - } - - CatchHitObject previousHitCircle = previousHitObject.BaseHitObject; - - double additionBonus = 0; - double sqrtTime = Math.Sqrt(Math.Max(timeElapsed, 25)); - - // Direction changes give an extra point! - if (Math.Abs(LastMovement) > 0.1) - { - if (Math.Abs(previousHitObject.LastMovement) > 0.1 && Math.Sign(LastMovement) != Math.Sign(previousHitObject.LastMovement)) - { - double bonus = direction_change_bonus / sqrtTime; - - // Weight bonus by how - double bonusFactor = Math.Min(playerPositioningError, Math.Abs(LastMovement)) / playerPositioningError; - - // We want time to play a role twice here! - addition += bonus * bonusFactor; - - // Bonus for tougher direction switches and "almost" hyperdashes at this point - if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) - { - additionBonus += 0.3 * bonusFactor; - } - } - - // Base bonus for every movement, giving some weight to streams. - addition += 7.5 * Math.Min(Math.Abs(LastMovement), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtTime; - } - - // Bonus for "almost" hyperdashes at corner points - if (previousHitCircle != null && previousHitCircle.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) - { - if (!previousHitCircle.HyperDash) - { - additionBonus += 1.0; - } - else - { - // After a hyperdash we ARE in the correct position. Always! - PlayerPositionOffset = 0; - } - - addition *= 1.0 + additionBonus * ((10 - previousHitCircle.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10); - } - - addition *= 850.0 / Math.Max(timeElapsed, 25); - - Strain = previousHitObject.Strain * decay + addition; - } - - private static double spacingWeight(float distance) - { - return Math.Pow(distance, 1.3) / 500; - } - - internal float DistanceTo(CatchDifficultyHitObject other) - { - return Math.Abs(ActualNormalizedPosition - other.ActualNormalizedPosition); - } - } -} diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs deleted file mode 100644 index 0a0897d97b..0000000000 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyDifficultyCalculator.cs +++ /dev/null @@ -1,148 +0,0 @@ -// 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 System.Collections.Generic; -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.UI; - -namespace osu.Game.Rulesets.Catch.Difficulty -{ - public class CatchLegacyDifficultyCalculator : LegacyDifficultyCalculator - { - /// - /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP. - /// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain. - /// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage. - /// - private const double strain_step = 750; - - /// - /// The weighting of each strain value decays to this number * it's previous value - /// - private const double decay_weight = 0.94; - - private const double star_scaling_factor = 0.145; - - public CatchLegacyDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) - : base(ruleset, beatmap) - { - } - - protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) - { - if (!beatmap.HitObjects.Any()) - return new CatchDifficultyAttributes(mods, 0); - - var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty); - float halfCatchWidth = catcher.CatchWidth * 0.5f; - - var difficultyHitObjects = new List(); - - foreach (var hitObject in beatmap.HitObjects) - { - switch (hitObject) - { - // We want to only consider fruits that contribute to the combo. Droplets are addressed as accuracy and spinners are not relevant for "skill" calculations. - case Fruit fruit: - difficultyHitObjects.Add(new CatchDifficultyHitObject(fruit, halfCatchWidth)); - break; - case JuiceStream _: - difficultyHitObjects.AddRange(hitObject.NestedHitObjects.OfType().Where(o => !(o is TinyDroplet)).Select(o => new CatchDifficultyHitObject(o, halfCatchWidth))); - break; - } - } - - difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); - - if (!calculateStrainValues(difficultyHitObjects, timeRate)) - return new CatchDifficultyAttributes(mods, 0); - - // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate; - double starRating = Math.Sqrt(calculateDifficulty(difficultyHitObjects, timeRate)) * star_scaling_factor; - - return new CatchDifficultyAttributes(mods, starRating) - { - ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, - MaxCombo = difficultyHitObjects.Count - }; - } - - private bool calculateStrainValues(List objects, double timeRate) - { - CatchDifficultyHitObject lastObject = null; - - if (!objects.Any()) return false; - - // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. - foreach (var currentObject in objects) - { - if (lastObject != null) - currentObject.CalculateStrains(lastObject, timeRate); - - lastObject = currentObject; - } - - return true; - } - - private double calculateDifficulty(List objects, double timeRate) - { - // The strain step needs to be adjusted for the algorithm to be considered equal with speed changing mods - double actualStrainStep = strain_step * timeRate; - - // Find the highest strain value within each strain step - var highestStrains = new List(); - double intervalEndTime = actualStrainStep; - double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval - - CatchDifficultyHitObject previousHitObject = null; - foreach (CatchDifficultyHitObject hitObject in objects) - { - // While we are beyond the current interval push the currently available maximum to our strain list - while (hitObject.BaseHitObject.StartTime > intervalEndTime) - { - highestStrains.Add(maximumStrain); - - // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay - // until the beginning of the next interval. - if (previousHitObject == null) - { - maximumStrain = 0; - } - else - { - double decay = Math.Pow(CatchDifficultyHitObject.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); - maximumStrain = previousHitObject.Strain * decay; - } - - // Go to the next time interval - intervalEndTime += actualStrainStep; - } - - // Obtain maximum strain - maximumStrain = Math.Max(hitObject.Strain, maximumStrain); - - previousHitObject = hitObject; - } - - // Build the weighted sum over the highest strains for each interval - double difficulty = 0; - double weight = 1; - highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. - - foreach (double strain in highestStrains) - { - difficulty += weight * strain; - weight *= decay_weight; - } - - return difficulty; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs new file mode 100644 index 0000000000..5dbd60d700 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -0,0 +1,68 @@ +// 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.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing +{ + public class CatchDifficultyHitObject : DifficultyHitObject + { + private const float normalized_hitobject_radius = 41.0f; + private const float absolute_player_positioning_error = 16f; + + public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject; + + public float MovementDistance { get; private set; } + /// + /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. + /// + public readonly double StrainTime; + + private readonly float scalingFactor; + + private readonly CatchHitObject lastObject; + + public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double timeRate, float halfCatcherWidth) + : base(hitObject, lastObject, timeRate) + { + this.lastObject = (CatchHitObject)lastObject; + + // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. + scalingFactor = normalized_hitobject_radius / halfCatcherWidth; + + setDistances(); + + // Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure + StrainTime = Math.Max(25, DeltaTime); + } + + private void setDistances() + { + float lastPosition = getNormalizedPosition(lastObject); + float currentPosition = getNormalizedPosition(BaseObject); + + // After a hyperdash the player is assumed to be in the correct position + if (lastObject.LazyMovementDistance != null && !lastObject.HyperDash) + lastPosition += lastObject.LazyMovementDistance.Value; + + BaseObject.LazyMovementDistance = + MathHelper.Clamp( + lastPosition, + currentPosition - (normalized_hitobject_radius - absolute_player_positioning_error), + currentPosition + (normalized_hitobject_radius - absolute_player_positioning_error)) // Obtain new lazy position, but be stricter by allowing for an error of a certain degree of the player. + - currentPosition; // Subtract HitObject position to obtain offset + + MovementDistance = Math.Abs(currentPosition - lastPosition + BaseObject.LazyMovementDistance.Value); + + if (currentPosition < lastPosition) + MovementDistance *= -1; + } + + private float getNormalizedPosition(CatchHitObject hitObject) => hitObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; + } +} diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs new file mode 100644 index 0000000000..1626a9be1d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -0,0 +1,63 @@ +// 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.Catch.Difficulty.Preprocessing; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; + +namespace osu.Game.Rulesets.Catch.Difficulty.Skills +{ + public class Movement : Skill + { + private const float absolute_player_positioning_error = 16f; + private const float normalized_hitobject_radius = 41.0f; + private const double direction_change_bonus = 12.5; + + protected override double SkillMultiplier => 850; + protected override double StrainDecayBase => 0.2; + + protected override double DecayWeight => 0.94; + + protected override double StrainValueOf(DifficultyHitObject current) + { + var catchCurrent = (CatchDifficultyHitObject)current; + var catchPrevious = (CatchDifficultyHitObject)Previous[0]; + + var sqrtStrain = Math.Sqrt(catchCurrent.StrainTime); + + double distanceAddition = Math.Pow(Math.Abs(catchCurrent.MovementDistance), 1.3) / 500; + + double bonus = 0; + + if (Math.Abs(catchCurrent.MovementDistance) > 0.1) + { + if (catchPrevious.MovementDistance > 0.1 && Math.Sign(catchCurrent.MovementDistance) != Math.Sign(catchPrevious.MovementDistance)) + { + double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(catchCurrent.MovementDistance)) / absolute_player_positioning_error; + + distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor; + + // Bonus for tougher direction switches and "almost" hyperdashes at this point + if (catchPrevious.BaseObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH) + bonus = 0.3 * bonusFactor; + } + + // Base bonus for every movement, giving some weight to streams. + distanceAddition += 7.5 * Math.Min(Math.Abs(catchCurrent.MovementDistance), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; + } + + // Bonus for "almost" hyperdashes at corner points + if (catchPrevious.BaseObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) + { + if (!catchPrevious.BaseObject.HyperDash) + bonus += 1.0; + + distanceAddition *= 1.0 + bonus * ((10 - catchPrevious.BaseObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10); + } + + return distanceAddition / catchCurrent.StrainTime; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 2153b8dc85..aff18619fb 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -50,6 +50,11 @@ namespace osu.Game.Rulesets.Catch.Objects /// public CatchHitObject HyperDashTarget; + /// + /// The minimum distance to be moved from the last to hit this . + /// + internal float? LazyMovementDistance; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty);