diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 8c179fe9a9..3f26647f86 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - for (int i = 0; i < 9; i++) + for (int i = 0; i < 11; i++) { int count = i + 1; AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count); @@ -104,12 +104,22 @@ namespace osu.Game.Rulesets.Catch.Tests }) }, 1); + createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0, spacingAfterGroup: 400); + createObjects(() => new TestJuiceStream(left_x) + { + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(0, 300)) + }) + }, count: 1, spacingAfterGroup: 150); + createObjects(() => new Fruit { X = left_x }, count: 1, spacing: 0, spacingAfterGroup: 400); + createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0); + return beatmap; - void createObjects(Func createObject, int count = 3) + void createObjects(Func createObject, int count = 3, float spacing = 140, float spacingAfterGroup = 700) { - const float spacing = 140; - for (int i = 0; i < count; i++) { var hitObject = createObject(); @@ -117,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(hitObject); } - startTime += 700; + startTime += spacingAfterGroup; } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index f009c10a9c..1f05d66b86 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -38,5 +39,25 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } }; } + + /// + /// Enumerate all s, sorted by their start times. + /// + /// + /// If multiple objects have the same start time, the ordering is preserved (it is a stable sorting). + /// + public static IEnumerable GetPalpableObjects(IEnumerable hitObjects) + { + return hitObjects.SelectMany(selectPalpableObjects).OrderBy(h => h.StartTime); + + IEnumerable selectPalpableObjects(HitObject h) + { + if (h is PalpableCatchHitObject palpable) + yield return palpable; + + foreach (var nested in h.NestedHitObjects.OfType()) + yield return nested; + } + } } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 02d4cdbb94..200018f28b 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -2,7 +2,6 @@ // 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.Objects; @@ -208,24 +207,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void initialiseHyperDash(IBeatmap beatmap) { - List palpableObjects = new List(); - - foreach (var currentObject in beatmap.HitObjects) - { - if (currentObject is Fruit fruitObject) - palpableObjects.Add(fruitObject); - - if (currentObject is JuiceStream) - { - foreach (var juice in currentObject.NestedHitObjects) - { - if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet)) - palpableObjects.Add(palpableObject); - } - } - } - - palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + var palpableObjects = CatchBeatmap.GetPalpableObjects(beatmap.HitObjects) + .Where(h => h is Fruit || (h is Droplet && h is not TinyDroplet)) + .ToArray(); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2; @@ -237,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int lastDirection = 0; double lastExcess = halfCatcherWidth; - for (int i = 0; i < palpableObjects.Count - 1; i++) + for (int i = 0; i < palpableObjects.Length - 1; i++) { var currentObject = palpableObjects[i]; var nextObject = palpableObjects[i + 1]; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index b826c1f546..f12c41a415 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Skills; using osu.Game.Rulesets.Catch.Mods; @@ -56,13 +57,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty List objects = new List(); // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. - foreach (var hitObject in beatmap.HitObjects - .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj }) - .Cast() - .OrderBy(x => x.StartTime)) + foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects)) { // We want to only consider fruits that contribute to the combo. - if (hitObject is BananaShower || hitObject is TinyDroplet) + if (hitObject is Banana || hitObject is TinyDroplet) continue; if (lastObject != null) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 0c2c157d10..147850a9b7 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -126,6 +126,7 @@ namespace osu.Game.Rulesets.Catch.UI private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR; + private double? lastHyperDashStartTime; private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; @@ -233,16 +234,23 @@ namespace osu.Game.Rulesets.Catch.UI // droplet doesn't affect the catcher state if (hitObject is TinyDroplet) return; - if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target) + // if a hyper fruit was already handled this frame, just go where it says to go. + // this special-cases some aspire maps that have doubled-up objects (one hyper, one not) at the same time instant. + // handling this "properly" elsewhere is impossible as there is no feasible way to ensure + // that the hyperfruit gets judged second (especially if it coincides with a last fruit in a juice stream). + if (lastHyperDashStartTime != Time.Current) { - double timeDifference = target.StartTime - hitObject.StartTime; - double positionDifference = target.EffectiveX - X; - double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target) + { + double timeDifference = target.StartTime - hitObject.StartTime; + double positionDifference = target.EffectiveX - X; + double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX); + SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX); + } + else + SetHyperDashState(); } - else - SetHyperDashState(); if (result.IsHit) CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle; @@ -292,6 +300,8 @@ namespace osu.Game.Rulesets.Catch.UI if (wasHyperDashing) runHyperDashStateTransition(false); + + lastHyperDashStartTime = null; } else { @@ -301,6 +311,8 @@ namespace osu.Game.Rulesets.Catch.UI if (!wasHyperDashing) runHyperDashStateTransition(true); + + lastHyperDashStartTime = Time.Current; } }