diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 904f5edf2b..2cdbaddf72 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index a24fcaad8d..bb4261ea13 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; @@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills // 0.0 +--------+-+---------------> Release Difference / ms // release_threshold if (isOverlapping) - holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime))); + holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold); // Decay and increase individualStrains in own column individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs index 758c8dd347..71618a4bc3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs @@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }, columnBackgrounds = new ColumnFlow(stageDefinition) { - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + Masking = false, }, new HitTargetInsetContainer { @@ -126,8 +127,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy }, new Container { + X = isLastColumn ? -0.16f : 0, Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, Scale = new Vector2(0.740f, 1), diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index f444448797..5614a13a48 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI private readonly FillFlowContainer> columns; private readonly StageDefinition stageDefinition; + public new bool Masking + { + get => base.Masking; + set => base.Masking = value; + } + public ColumnFlow(StageDefinition stageDefinition) { this.stageDefinition = stageDefinition; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs index 3c8358b4cd..0e36c1dc45 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderDrawing.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -24,38 +25,57 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); [Test] - public void TestTouchInputAfterTouchingComposeArea() + public void TestTouchInputPlaceHitCircleDirectly() { AddStep("tap circle", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "HitCircle"))); - // this input is just for interacting with compose area - AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); - - AddStep("move current time", () => InputManager.Key(Key.Right)); - - AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single().ToScreenSpace(new Vector2(10, 10)))); + AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single())); AddAssert("circle placed correctly", () => { var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); Assert.Multiple(() => { - Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f)); - Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f)); + Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f)); + Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f)); }); return true; }); + } - AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); + [Test] + public void TestTouchInputPlaceCircleAfterTouchingComposeArea() + { + AddStep("tap circle", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "HitCircle"))); - // this input is just for interacting with compose area AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); + AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle); - AddStep("move current time", () => InputManager.Key(Key.Right)); + AddStep("move forward", () => InputManager.Key(Key.Right)); + + AddStep("tap to place circle", () => tap(this.ChildrenOfType().Single())); + AddAssert("circle placed correctly", () => + { + var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); + Assert.Multiple(() => + { + Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f)); + Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f)); + }); + + return true; + }); + } + + [Test] + public void TestTouchInputPlaceSliderDirectly() + { + AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(50, 20))))); AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(200, 50))))); AddAssert("selection not initiated", () => this.ChildrenOfType().All(d => d.State == Visibility.Hidden)); + AddAssert("blueprint visible", () => this.ChildrenOfType().Single().Alpha > 0); AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); AddAssert("slider placed correctly", () => { @@ -76,12 +96,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } - private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre); + [Test] + public void TestTouchInputPlaceSliderAfterTouchingComposeArea() + { + AddStep("tap slider", () => tap(this.ChildrenOfType().Single(b => b.Button.Label == "Slider"))); + + AddStep("tap playfield", () => tap(this.ChildrenOfType().Single())); + AddStep("tap and hold another spot", () => hold(this.ChildrenOfType().Single(), new Vector2(50, 0))); + AddUntilStep("wait for slider placement", () => EditorBeatmap.HitObjects.SingleOrDefault(h => h.StartTime == EditorClock.CurrentTimeAccurate) is Slider); + AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); + + AddStep("move forward", () => InputManager.Key(Key.Right)); + + AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(50, 20))))); + AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType().Single().ToScreenSpace(new Vector2(200, 50))))); + AddAssert("selection not initiated", () => this.ChildrenOfType().All(d => d.State == Visibility.Hidden)); + AddAssert("blueprint visible", () => this.ChildrenOfType().Single().IsPresent); + AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); + AddAssert("slider placed correctly", () => + { + var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); + Assert.Multiple(() => + { + Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f)); + Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f)); + Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2)); + Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); + + // the final position may be slightly off from the mouse position when drawing, account for that. + Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5)); + Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5)); + }); + + return true; + }); + } + + private void tap(Drawable drawable, Vector2 offset = default) => tap(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset)); private void tap(Vector2 position) { - InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); + hold(position); InputManager.EndTouch(new Touch(TouchSource.Touch1, position)); } + + private void hold(Drawable drawable, Vector2 offset = default) => hold(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset)); + + private void hold(Vector2 position) + { + InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 5f99b6414e..9b2760f998 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); + const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS; + const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER; + // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpFromEndDistance : osuCurrObj.JumpDistance; double currVelocity = currDistance / osuCurrObj.StrainTime; @@ -79,14 +83,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators wideAngleBonus = calcWideAngleBonus(currAngle); acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2. + if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2. acuteAngleBonus = 0; else { acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // 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.Min(angleBonus, diameter * 1.25 / 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.Clamp(currDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). + * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(currDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter. } // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. @@ -109,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. - double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); + double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); velocityChangeBonus = overlapVelocityBuff * distRatio; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs index d10d2c5c05..d503dd2bcc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators islandCount.Count++; // repeated island (ex: triplet -> triplet) - double power = logistic(island.Delta, 2.75, 0.24, 14); + double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33); effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); islandCounts[countIndex] = (islandCount.Island, islandCount.Count); @@ -172,8 +173,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though) } - private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x)))); - private class Island : IEquatable { private readonly double deltaDifferenceEpsilon; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index c220352ee0..a5f6468f17 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -10,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class SpeedEvaluator { - private const double single_spacing_threshold = 125; // 1.25 circles distance between centers - private const double min_speed_bonus = 75; // ~200BPM + private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers + private const double min_speed_bonus = 200; // 200 BPM 1/4th private const double speed_balancing_factor = 40; private const double distance_multiplier = 0.94; @@ -43,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double speedBonus = 0.0; // Add additional scaling bonus for streams/bursts higher than 200bpm - if (strainTime < min_speed_bonus) - speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); + if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus) + speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2); double travelDistance = osuPrevObj?.TravelDistance ?? 0; double distance = travelDistance + osuCurrObj.MinimumJumpDistance; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 92f0e01246..7d94df1e21 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? Math.Min(aimRating, aimRatingNoSliders) / aimRating : 1; - double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(); - double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(); + double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains(); + double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains(); if (mods.Any(m => m is OsuModTouchDevice)) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 574af0108d..c67a011a68 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2; + public const int MIN_DELTA_TIME = 25; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index b3d530e3af..faf91e4652 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { currentStrain *= strainDecay(current.DeltaTime); currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; - ObjectStrains.Add(currentStrain); return currentStrain; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 96180c0aa1..559a871df1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double ReducedStrainBaseline => 0.75; - protected List ObjectStrains = new List(); protected double Difficulty; protected OsuStrainSkill(Mod[] mods) @@ -60,20 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return Difficulty; } - /// - /// Returns the number of strains weighted against the top strain. - /// The result is scaled by clock rate as it affects the total number of strains. - /// - public double CountDifficultStrains() - { - if (Difficulty == 0) - return 0.0; - - double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical - // Use a weighted sum of all strains. Constants are arbitrary and give nice values - return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); - } - public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index e5aa25c1eb..d2c4bbb618 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; - ObjectStrains.Add(totalStrain); return totalStrain; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs index b13663cb44..163b42bcfd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osuTK; @@ -31,12 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints public override void EndPlacement(bool commit) { if (!commit && PlacementActive != PlacementState.Finished) - { - gridToolboxGroup.StartPosition.Value = originalOrigin; - gridToolboxGroup.Spacing.Value = originalSpacing; - if (!gridToolboxGroup.GridLinesRotation.Disabled) - gridToolboxGroup.GridLinesRotation.Value = originalRotation; - } + resetGridState(); base.EndPlacement(commit); @@ -103,6 +99,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints public override void UpdateTimeAndPosition(SnapResult result) { + if (State.Value == Visibility.Hidden) + return; + var pos = ToLocalSpace(result.ScreenSpacePosition); if (PlacementActive != PlacementState.Active) @@ -122,5 +121,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints } } } + + protected override void PopOut() + { + base.PopOut(); + resetGridState(); + } + + private void resetGridState() + { + gridToolboxGroup.StartPosition.Value = originalOrigin; + gridToolboxGroup.Spacing.Value = originalSpacing; + if (!gridToolboxGroup.GridLinesRotation.Disabled) + gridToolboxGroup.GridLinesRotation.Value = originalRotation; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index cb57c8e6e0..4f2f6516a8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -156,6 +156,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + // this allows sliders to be drawn outside compose area (after starting from a point within the compose area). + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || PlacementActive == PlacementState.Active; + + // ReceivePositionalInputAtSubTree generally always returns true when masking is disabled, but we don't want that, + // otherwise a slider path tooltip will be displayed anywhere in the editor (outside compose area). + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos); + private void beginNewSegment(PathControlPoint lastPoint) { segmentStart = lastPoint; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs index 9f63e84867..25428c8b2f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; @@ -11,26 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators { public class ColourEvaluator { - /// - /// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2). - /// - /// The input value. - /// The center of the sigmoid, where the largest gradient occurs and value is equal to middle. - /// The radius of the sigmoid, outside of which values are near the minimum/maximum. - /// The middle of the sigmoid output. - /// The height of the sigmoid output. This will be equal to max value - min value. - private static double sigmoid(double val, double center, double width, double middle, double height) - { - double sigmoid = Math.Tanh(Math.E * -(val - center) / width); - return sigmoid * (height / 2) + middle; - } - /// /// Evaluate the difficulty of the first note of a . /// public static double EvaluateDifficultyOf(MonoStreak monoStreak) { - return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; + return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; } /// @@ -38,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) { - return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); + return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); } /// @@ -46,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators /// public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) { - return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1)); + return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E)); } public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index f1a2d6b5f2..7619328e68 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge Add(metadataClient); // add button to observe for daily challenge changes and perform its logic. - Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)); + Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index f392841ac7..d7c92a64b1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.TernaryButtons; @@ -82,6 +83,45 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestPlacementOutsideComposeScreen() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("select circle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1); + + AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 20f))); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddAssert("no circle placed", () => editorBeatmap.HitObjects.Count == 1); + } + + [Test] + public void TestDragSliderOutsideComposeScreen() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("select slider", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "Slider").TriggerClick()); + + AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single())); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 80f))); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("slider placed", () => editorBeatmap.HitObjects.Count == 1); + } + [Test] public void TestPlacementOnlyWorksWithTiming() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 91942c391a..ab3d5907ef 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -55,21 +55,21 @@ namespace osu.Game.Tests.Visual.Online { Username = @"flyte", Id = 3103765, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", }), new UserBrickPanel(new APIUser { Username = @"peppy", Id = 2, Colour = "99EB47", - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", }), new UserGridPanel(new APIUser { Username = @"flyte", Id = 3103765, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", Status = { Value = UserStatus.Online } }) { Width = 300 }, boundPanel1 = new UserGridPanel(new APIUser @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online Username = @"peppy", Id = 2, CountryCode = CountryCode.AU, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", IsSupporter = true, SupportLevel = 3, }) { Width = 300 }, @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online Username = @"flyte", Id = 3103765, CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } }) { Width = 300 }, new UserRankPanel(new APIUser @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online Id = 2, Colour = "99EB47", CountryCode = CountryCode.AU, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } }) { Width = 300 } } @@ -168,6 +168,14 @@ namespace osu.Game.Tests.Visual.Online CountryRank = RNG.Next(100000) }); }); + AddStep("set statistics to something big", () => + { + API.UpdateStatistics(new UserStatistics + { + GlobalRank = RNG.Next(1_000_000, 100_000_000), + CountryRank = RNG.Next(1_000_000, 100_000_000) + }); + }); AddStep("set statistics to empty", () => { API.UpdateStatistics(new UserStatistics()); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index 8f72be37df..d8baca6d23 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface break; case Key.Q: - buttons.OnExit = action; + buttons.OnExit = _ => action(); break; case Key.O: diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs index 41543669eb..4925facd8a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneMainMenuButton.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestStandardButton() { AddStep("add button", () => Child = new MainMenuButton( - ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P) + ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.P) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], - Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], - Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) + Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index b07e8399c0..1cb6b69f91 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills protected virtual int SectionLength => 400; private double currentSectionPeak; // We also keep track of the peak strain level in the current section. - private double currentSectionEnd; private readonly List strainPeaks = new List(); + protected readonly List ObjectStrains = new List(); // Store individual strains protected StrainSkill(Mod[] mods) : base(mods) @@ -57,7 +57,25 @@ namespace osu.Game.Rulesets.Difficulty.Skills currentSectionEnd += SectionLength; } - currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); + double strain = StrainValueAt(current); + currentSectionPeak = Math.Max(strain, currentSectionPeak); + + // Store the strain value for the object + ObjectStrains.Add(strain); + } + + /// + /// Calculates the number of strains weighted against the top strain. + /// The result is scaled by clock rate as it affects the total number of strains. + /// + public virtual double CountTopWeightedStrains() + { + if (ObjectStrains.Count == 0) + return 0.0; + + double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical + // Use a weighted sum of all strains. Constants are arbitrary and give nice values + return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88)))); } /// diff --git a/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs new file mode 100644 index 0000000000..b9efcd683d --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/DifficultyCalculationUtils.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + public static class DifficultyCalculationUtils + { + /// + /// Converts BPM value into milliseconds + /// + /// Beats per minute + /// Which rhythm delimiter to use, default is 1/4 + /// BPM conveted to milliseconds + public static double BPMToMilliseconds(double bpm, int delimiter = 4) + { + return 60000.0 / delimiter / bpm; + } + + /// + /// Converts milliseconds value into a BPM value + /// + /// Milliseconds + /// Which rhythm delimiter to use, default is 1/4 + /// Milliseconds conveted to beats per minute + public static double MillisecondsToBPM(double ms, int delimiter = 4) + { + return 60000.0 / (ms * delimiter); + } + + /// + /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function) + /// + /// Value to calculate the function for + /// Maximum value returnable by the function + /// Growth rate of the function + /// How much the function midpoint is offset from zero + /// The output of logistic function of + public static double Logistic(double x, double midpointOffset, double multiplier, double maxValue = 1) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x))); + + /// + /// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function) + /// + /// Maximum value returnable by the function + /// Exponent + /// The output of logistic function + public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent)); + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 16b76a300c..4b64548f9c 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -537,22 +537,23 @@ namespace osu.Game.Rulesets.Edit #region IPlacementHandler - public void BeginPlacement(HitObject hitObject) + public void ShowPlacement(HitObject hitObject) { EditorBeatmap.PlacementObject.Value = hitObject; } - public void EndPlacement(HitObject hitObject, bool commit) + public void HidePlacement() { EditorBeatmap.PlacementObject.Value = null; + } - if (commit) - { - EditorBeatmap.Add(hitObject); + public void CommitPlacement(HitObject hitObject) + { + EditorBeatmap.PlacementObject.Value = null; + EditorBeatmap.Add(hitObject); - if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime) - EditorClock.SeekSmoothlyTo(hitObject.StartTime); - } + if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime) + EditorClock.SeekSmoothlyTo(hitObject.StartTime); } public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs index 760c7b0936..4df2a52743 100644 --- a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -63,18 +64,26 @@ namespace osu.Game.Rulesets.Edit startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); } + private bool placementBegun; + protected override void BeginPlacement(bool commitStart = false) { base.BeginPlacement(commitStart); - placementHandler.BeginPlacement(HitObject); + if (State.Value == Visibility.Visible) + placementHandler.ShowPlacement(HitObject); + + placementBegun = true; } public override void EndPlacement(bool commit) { base.EndPlacement(commit); - placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); + if (IsValidForPlacement && commit) + placementHandler.CommitPlacement(HitObject); + else + placementHandler.HidePlacement(); } /// @@ -127,5 +136,19 @@ namespace osu.Game.Rulesets.Edit /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + + protected override void PopIn() + { + base.PopIn(); + + if (placementBegun) + placementHandler.ShowPlacement(HitObject); + } + + protected override void PopOut() + { + base.PopOut(); + placementHandler.HidePlacement(); + } } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index a36de02433..52b8a5c796 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,7 +7,6 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit @@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Edit /// /// A blueprint which governs the placement of something. /// - public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler + public abstract partial class PlacementBlueprint : VisibilityContainer, IKeyBindingHandler { /// /// Whether the is currently mid-placement, but has not necessarily finished being placed. @@ -31,12 +30,17 @@ namespace osu.Game.Rulesets.Edit /// protected virtual bool IsValidForPlacement => true; + // the blueprint should still be considered for input even if it is hidden, + // especially when such input is the reason for making the blueprint become visible. + public override bool PropagatePositionalInputSubTree => true; + public override bool PropagateNonPositionalInputSubTree => true; + protected PlacementBlueprint() { RelativeSizeAxes = Axes.Both; - // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle - // on the same frame it is made visible via a PlacementState change. + // the blueprint should still be considered for input even if it is hidden, + // especially when such input is the reason for making the blueprint become visible. AlwaysPresent = true; } @@ -104,8 +108,6 @@ namespace osu.Game.Rulesets.Edit { } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - protected override bool Handle(UIEvent e) { base.Handle(e); @@ -127,6 +129,9 @@ namespace osu.Game.Rulesets.Edit } } + protected override void PopIn() => this.FadeIn(); + protected override void PopOut() => this.FadeOut(); + public enum PlacementState { Waiting, diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 69e676667d..0ffd1072cd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -316,9 +316,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Refreshes the current placement tool. /// - private void refreshTool() + private void refreshPlacement() { - removePlacement(); + CurrentPlacement?.EndPlacement(false); + CurrentPlacement?.Expire(); + CurrentPlacement = null; + ensurePlacementCreated(); } @@ -344,21 +347,24 @@ namespace osu.Game.Screens.Edit.Compose.Components { case PlacementBlueprint.PlacementState.Waiting: if (!Composer.CursorInPlacementArea) - removePlacement(); + CurrentPlacement.Hide(); + else + CurrentPlacement.Show(); + + break; + + case PlacementBlueprint.PlacementState.Active: + CurrentPlacement.Show(); break; case PlacementBlueprint.PlacementState.Finished: - removePlacement(); + refreshPlacement(); break; } - } - if (Composer.CursorInPlacementArea) - ensurePlacementCreated(); - - // updates the placement with the latest editor clock time. - if (CurrentPlacement != null) + // updates the placement with the latest editor clock time. updatePlacementTimeAndPosition(); + } } protected override bool OnMouseMove(MouseMoveEvent e) @@ -386,7 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void hitObjectAdded(HitObject obj) { // refresh the tool to handle the case of placement completing. - refreshTool(); + refreshPlacement(); // on successful placement, the new combo button should be reset as this is the most common user interaction. if (Beatmap.SelectedHitObjects.Count == 0) @@ -415,14 +421,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public void CommitIfPlacementActive() { CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active); - removePlacement(); - } - - private void removePlacement() - { - CurrentPlacement?.EndPlacement(false); - CurrentPlacement?.Expire(); - CurrentPlacement = null; + refreshPlacement(); } private CompositionTool currentTool; diff --git a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs index 57960a76a1..e2046cd532 100644 --- a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs +++ b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs @@ -10,17 +10,21 @@ namespace osu.Game.Screens.Edit.Compose public interface IPlacementHandler { /// - /// Notifies that a placement has begun. + /// Notifies that a placement blueprint became visible on the screen. /// - /// The being placed. - void BeginPlacement(HitObject hitObject); + /// The representing the placement. + void ShowPlacement(HitObject hitObject); /// - /// Notifies that a placement has finished. + /// Notifies that a visible placement blueprint has been hidden. + /// + void HidePlacement(); + + /// + /// Notifies that a placement has been committed. /// /// The that has been placed. - /// Whether the object should be committed. - void EndPlacement(HitObject hitObject, bool commit); + void CommitPlacement(HitObject hitObject); /// /// Deletes a . diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 0997ab8003..41920605b0 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Menu public Action? OnEditBeatmap; public Action? OnEditSkin; - public Action? OnExit; + public Action? OnExit; public Action? OnBeatmapListing; public Action? OnSolo; public Action? OnSettings; @@ -104,11 +104,11 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { - new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S) + new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), (_, _) => OnSettings?.Invoke(), Key.O, Key.S) { Padding = new MarginPadding { Right = WEDGE_WIDTH }, }, - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel) + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel) { Padding = new MarginPadding { Right = WEDGE_WIDTH }, VisibleStateMin = ButtonSystemState.Play, @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P) + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => OnSolo?.Invoke(), Key.P) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); @@ -141,22 +141,22 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); - buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S)); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) { Padding = new MarginPadding { Left = WEDGE_WIDTH }, }); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D)); if (host.CanExit) - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q)); buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsEdit); @@ -179,7 +179,7 @@ namespace osu.Game.Screens.Menu sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); } - private void onMultiplayer(MainMenuButton _) + private void onMultiplayer(MainMenuButton mainMenuButton, UIEvent uiEvent) { if (api.State.Value != APIState.Online) { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.Menu OnMultiplayer?.Invoke(); } - private void onPlaylists(MainMenuButton _) + private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent) { if (api.State.Value != APIState.Online) { @@ -201,7 +201,7 @@ namespace osu.Game.Screens.Menu OnPlaylists?.Invoke(); } - private void onDailyChallenge(MainMenuButton button) + private void onDailyChallenge(MainMenuButton button, UIEvent uiEvent) { if (api.State.Value != APIState.Online) { diff --git a/osu.Game/Screens/Menu/DailyChallengeButton.cs b/osu.Game/Screens/Menu/DailyChallengeButton.cs index 4dbebf0ae9..44a53efa7b 100644 --- a/osu.Game/Screens/Menu/DailyChallengeButton.cs +++ b/osu.Game/Screens/Menu/DailyChallengeButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps.Drawables; @@ -50,7 +51,7 @@ namespace osu.Game.Screens.Menu [Resolved] private SessionStatics statics { get; set; } = null!; - public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) + public DailyChallengeButton(string sampleName, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) { BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 346bdcb751..35c6bab81b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -153,9 +153,9 @@ namespace osu.Game.Screens.Menu else this.Push(new DailyChallengeIntro(room)); }, - OnExit = () => + OnExit = e => { - exitConfirmedViaHoldOrClick = true; + exitConfirmedViaHoldOrClick = e is MouseEvent; this.Exit(); } } diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 4df5e6d309..f8824795d8 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Menu protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT); - private readonly Action? clickAction; + private readonly Action? clickAction; private readonly Container background; private readonly Drawable backgroundContent; @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Menu public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos); - public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, params Key[] triggerKeys) { this.sampleName = sampleName; this.clickAction = clickAction; @@ -263,7 +263,7 @@ namespace osu.Game.Screens.Menu protected override bool OnClick(ClickEvent e) { - trigger(); + trigger(e); return true; } @@ -274,19 +274,19 @@ namespace osu.Game.Screens.Menu if (TriggerKeys.Contains(e.Key)) { - trigger(); + trigger(e); return true; } return false; } - private void trigger() + private void trigger(UIEvent e) { sampleChannel = sampleClick?.GetChannel(); sampleChannel?.Play(); - clickAction?.Invoke(this); + clickAction?.Invoke(this, e); boxHoverLayer.ClearTransforms(); boxHoverLayer.Alpha = 0.9f; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 9b4bf7d633..f5671cd934 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Play } }, }, - idleTracker = new IdleTracker(750), + idleTracker = new IdleTracker(1500), sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) }; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e8fe191a24..95268c35da 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -222,12 +222,6 @@ namespace osu.Game.Screens.Select InternalChild = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - // Avoid clash between scrollbar and osu! logo. - Top = 10, - Bottom = 100, - }, Children = new Drawable[] { setPool, @@ -1266,6 +1260,38 @@ namespace osu.Game.Screens.Select return base.OnDragStart(e); } + + protected override ScrollbarContainer CreateScrollbar(Direction direction) + { + return new PaddedScrollbar(); + } + + protected partial class PaddedScrollbar : OsuScrollbar + { + public PaddedScrollbar() + : base(Direction.Vertical) + { + } + } + + private const float top_padding = 10; + private const float bottom_padding = 70; + + protected override float ToScrollbarPosition(float scrollPosition) + { + if (Precision.AlmostEquals(0, ScrollableExtent)) + return 0; + + return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent); + } + + protected override float FromScrollbarPosition(float scrollbarPosition) + { + if (Precision.AlmostEquals(0, ScrollbarMovementExtent)) + return 0; + + return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))); + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index a52325dea2..aa8aff3adc 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -59,20 +59,20 @@ namespace osu.Game.Tests.Visual protected override void LoadComplete() { base.LoadComplete(); - ResetPlacement(); } - public void BeginPlacement(HitObject hitObject) + public void ShowPlacement(HitObject hitObject) { } - public void EndPlacement(HitObject hitObject, bool commit) + public void HidePlacement() { - if (commit) - AddHitObject(CreateHitObject(hitObject)); + } - ResetPlacement(); + public void CommitPlacement(HitObject hitObject) + { + AddHitObject(CreateHitObject(hitObject)); } protected void ResetPlacement() @@ -89,6 +89,10 @@ namespace osu.Game.Tests.Visual protected override void Update() { base.Update(); + + if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished) + ResetPlacement(); + updatePlacementTimeAndPosition(); } diff --git a/osu.Game/Users/UserRankPanel.cs b/osu.Game/Users/UserRankPanel.cs index 0d57b7bb7d..6d8d7dd143 100644 --- a/osu.Game/Users/UserRankPanel.cs +++ b/osu.Game/Users/UserRankPanel.cs @@ -152,7 +152,7 @@ namespace osu.Game.Users Margin = new MarginPadding { Top = main_content_height }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 80, Vertical = padding }, + Padding = new MarginPadding(padding), ColumnDimensions = new[] { new Dimension(), diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7405a7c587..855ba3374b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + @@ -35,9 +35,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - +