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
-
+
-
+