diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 1eb0975010..c01aff0aa0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -38,17 +38,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods new Fruit { X = 0, - StartTime = 250 + StartTime = 1000 }, new Fruit { X = CatchPlayfield.WIDTH, - StartTime = 500 + StartTime = 2000 }, new JuiceStream { X = CatchPlayfield.CENTER_X, - StartTime = 750, + StartTime = 3000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 89063319d6..e8ecd2ca1b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests hitObject.Scale = 1.5f; if (hyperdash) - hitObject.HyperDashTarget = new Banana(); + ((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana(); d.Anchor = Anchor.Centre; d.RelativePositionAxes = Axes.None; diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index a08c5b6fb1..00ce9ea8c2 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -192,24 +192,24 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void initialiseHyperDash(IBeatmap beatmap) { - List objectWithDroplets = new List(); + List palpableObjects = new List(); foreach (var currentObject in beatmap.HitObjects) { if (currentObject is Fruit fruitObject) - objectWithDroplets.Add(fruitObject); + palpableObjects.Add(fruitObject); if (currentObject is JuiceStream) { - foreach (var currentJuiceElement in currentObject.NestedHitObjects) + foreach (var juice in currentObject.NestedHitObjects) { - if (!(currentJuiceElement is TinyDroplet)) - objectWithDroplets.Add((CatchHitObject)currentJuiceElement); + if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet)) + palpableObjects.Add(palpableObject); } } } - objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; @@ -221,10 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int lastDirection = 0; double lastExcess = halfCatcherWidth; - for (int i = 0; i < objectWithDroplets.Count - 1; i++) + for (int i = 0; i < palpableObjects.Count - 1; i++) { - CatchHitObject currentObject = objectWithDroplets[i]; - CatchHitObject nextObject = objectWithDroplets[i + 1]; + var currentObject = palpableObjects[i]; + var nextObject = palpableObjects[i + 1]; // Reset variables in-case values have changed (e.g. after applying HR) currentObject.HyperDashTarget = null; diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index 3e21b8fbaf..dcd410e08f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing { private const float normalized_hitobject_radius = 41.0f; - public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject; + public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject; - public new CatchHitObject LastObject => (CatchHitObject)base.LastObject; + public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject; public readonly float NormalizedPosition; public readonly float LastNormalizedPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 4ecfb7b16d..d1033f7801 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -2,13 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { - public class Banana : Fruit + public class Banana : Fruit, IHasComboInformation { /// /// Index of banana in current shower. @@ -26,6 +29,29 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } + private Color4? colour; + + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) + { + // override any external colour changes with banananana + return colour ??= getBananaColour(); + } + + private Color4 getBananaColour() + { + switch (RNG.Next(0, 3)) + { + default: + return new Color4(255, 240, 0, 255); + + case 1: + return new Color4(255, 192, 0, 255); + + case 2: + return new Color4(214, 221, 28, 255); + } + } + private class BananaHitSampleInfo : HitSampleInfo { private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 5985ec9b68..ccd2422381 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -27,11 +27,6 @@ namespace osu.Game.Rulesets.Catch.Objects set => x = value; } - /// - /// Whether this object can be placed on the catcher's plate. - /// - public virtual bool CanBePlated => false; - /// /// A random offset applied to , set by the . /// @@ -63,13 +58,6 @@ namespace osu.Game.Rulesets.Catch.Objects set => ComboIndexBindable.Value = value; } - /// - /// Difference between the distance to the next object - /// and the distance that would have triggered a hyper dash. - /// A value close to 0 indicates a difficult jump (for difficulty calculation). - /// - public float DistanceToHyperDash { get; set; } - public Bindable LastInComboBindable { get; } = new Bindable(); /// @@ -83,16 +71,6 @@ namespace osu.Game.Rulesets.Catch.Objects public float Scale { get; set; } = 1; - /// - /// Whether this fruit can initiate a hyperdash. - /// - public bool HyperDash => HyperDashTarget != null; - - /// - /// The target fruit if we are to initiate a hyperdash. - /// - public CatchHitObject HyperDashTarget; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -105,14 +83,6 @@ namespace osu.Game.Rulesets.Catch.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; } - /// - /// Represents a single object that can be caught by the catcher. - /// - public abstract class PalpableCatchHitObject : CatchHitObject - { - public override bool CanBePlated => true; - } - public enum FruitVisualRepresentation { Pear, diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index a865984d45..7748b1c565 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Utils; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -15,14 +13,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } - private Color4? colour; - - protected override Color4 GetComboColour(IReadOnlyList comboColours) - { - // override any external colour changes with banananana - return colour ??= getBananaColour(); - } - protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); @@ -46,20 +36,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (Samples != null) Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; } - - private Color4 getBananaColour() - { - switch (RNG.Next(0, 3)) - { - default: - return new Color4(255, 240, 0, 255); - - case 1: - return new Color4(255, 192, 0, 255); - - case 2: - return new Color4(214, 221, 28, 255); - } - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index 4ce80aceb8..bf771f690e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableBananaShower : DrawableCatchHitObject + public class DrawableBananaShower : DrawableCatchHitObject { private readonly Func> createDrawableRepresentation; private readonly Container bananaContainer; @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables switch (hitObject) { case Banana banana: - return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); + return createDrawableRepresentation?.Invoke(banana); } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 7922510a49..f9f534f9ab 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -2,69 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject - where TObject : PalpableCatchHitObject - { - protected Container ScaleContainer { get; private set; } - - protected PalpableDrawableCatchHitObject(TObject hitObject) - : base(hitObject) - { - Origin = Anchor.Centre; - Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); - Masking = false; - } - - [BackgroundDependencyLoader] - private void load() - { - AddRangeInternal(new Drawable[] - { - ScaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - }); - - ScaleContainer.Scale = new Vector2(HitObject.Scale); - } - - protected override Color4 GetComboColour(IReadOnlyList comboColours) => - comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; - } - - public abstract class DrawableCatchHitObject : DrawableCatchHitObject - where TObject : CatchHitObject - { - public new TObject HitObject; - - protected DrawableCatchHitObject(TObject hitObject) - : base(hitObject) - { - HitObject = hitObject; - Anchor = Anchor.BottomLeft; - } - } - public abstract class DrawableCatchHitObject : DrawableHitObject { protected override double InitialLifetimeOffset => HitObject.TimePreempt; - public virtual bool StaysOnPlate => HitObject.CanBePlated; - public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; @@ -73,6 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables : base(hitObject) { X = hitObject.X; + Anchor = Anchor.BottomLeft; } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 688240fd86..74cd240aa3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -4,15 +4,16 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : PalpableDrawableCatchHitObject + public class DrawableDroplet : DrawablePalpableCatchHitObject { public override bool StaysOnPlate => false; - public DrawableDroplet(Droplet h) + public DrawableDroplet(CatchHitObject h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index c1c34e4157..96e24bf76c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -4,13 +4,14 @@ using System; using osu.Framework.Allocation; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : PalpableDrawableCatchHitObject + public class DrawableFruit : DrawablePalpableCatchHitObject { - public DrawableFruit(Fruit h) + public DrawableFruit(CatchHitObject h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 7bc016d94f..a7a5bfa5ad 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableJuiceStream : DrawableCatchHitObject + public class DrawableJuiceStream : DrawableCatchHitObject { private readonly Func> createDrawableRepresentation; private readonly Container dropletContainer; @@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables switch (hitObject) { case CatchHitObject catchObject: - return createDrawableRepresentation?.Invoke(catchObject)?.With(o => - ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); + return createDrawableRepresentation?.Invoke(catchObject); } throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}."); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs new file mode 100644 index 0000000000..9339a1c420 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject + { + public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; + + /// + /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. + /// + public virtual bool StaysOnPlate => true; + + protected readonly Container ScaleContainer; + + protected DrawablePalpableCatchHitObject(CatchHitObject h) + : base(h) + { + Origin = Anchor.Centre; + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + + AddInternal(ScaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }); + } + + [BackgroundDependencyLoader] + private void load() + { + ScaleContainer.Scale = new Vector2(HitObject.Scale); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs deleted file mode 100644 index c2499446fa..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects.Drawables; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables -{ - public class DropletPiece : CompositeDrawable - { - public DropletPiece() - { - Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); - } - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) - { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - var hitObject = drawableCatchObject.HitObject; - - InternalChild = new Pulp - { - RelativeSizeAxes = Axes.Both, - AccentColour = { BindTarget = drawableObject.AccentColour } - }; - - if (hitObject.HyperDash) - { - AddInternal(new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(2f), - Depth = 1, - Children = new Drawable[] - { - new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - BorderThickness = 6, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0.3f, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - } - } - } - } - }); - } - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs similarity index 88% rename from osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs index ebb0bf0f2c..fa8837dec5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class BananaPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs new file mode 100644 index 0000000000..1e7a0b0685 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class BorderPiece : Circle + { + public BorderPiece() + { + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + BorderColour = Color4.White; + BorderThickness = 6f * FruitPiece.RADIUS_ADJUST; + + // Border is drawn only when there is a child drawable. + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }; + } + } +} + diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs new file mode 100644 index 0000000000..bcef30fda8 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class DropletPiece : CompositeDrawable + { + public DropletPiece() + { + Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; + + InternalChild = new Pulp + { + RelativeSizeAxes = Axes.Both, + AccentColour = { BindTarget = drawableObject.AccentColour } + }; + + if (drawableCatchObject.HitObject.HyperDash) + { + AddInternal(new HyperDropletBorderPiece()); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs similarity index 50% rename from osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index 4bffdab3d8..208c9f8316 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -5,12 +5,9 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { internal class FruitPiece : CompositeDrawable { @@ -19,8 +16,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// public const float RADIUS_ADJUST = 1.1f; - private Circle border; - private CatchHitObject hitObject; + private BorderPiece border; + private PalpableCatchHitObject hitObject; public FruitPiece() { @@ -30,52 +27,18 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; hitObject = drawableCatchObject.HitObject; AddRangeInternal(new[] { - getFruitFor(drawableCatchObject.HitObject.VisualRepresentation), - border = new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Color4.White, - BorderThickness = 6f * RADIUS_ADJUST, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both - } - } - }, + getFruitFor(hitObject.VisualRepresentation), + border = new BorderPiece(), }); if (hitObject.HyperDash) { - AddInternal(new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - BorderThickness = 12f * RADIUS_ADJUST, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0.3f, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - } - } - }); + AddInternal(new HyperBorderPiece()); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs index 1d1faf893b..15349c18d5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class GrapePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs new file mode 100644 index 0000000000..60bb07e89d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.UI; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class HyperBorderPiece : BorderPiece + { + public HyperBorderPiece() + { + BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; + BorderThickness = 12f * FruitPiece.RADIUS_ADJUST; + + Child.Alpha = 0.3f; + Child.Blending = BlendingParameters.Additive; + Child.Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR; + } + } +} + diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs new file mode 100644 index 0000000000..1bd9fd6bb2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class HyperDropletBorderPiece : HyperBorderPiece + { + public HyperDropletBorderPiece() + { + Size /= 2; + BorderThickness = 6f; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs index 7f14217cda..3372a06996 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class PearPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs index c328ba1837..7f80c58178 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class PineapplePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs similarity index 96% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs index be70c3400c..1df548e70a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public abstract class PulpFormation : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs index 22ce3ba5b3..288ece95b2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class RaspberryPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs new file mode 100644 index 0000000000..995f61c386 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects +{ + /// + /// Represents a single object that can be caught by the catcher. + /// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects. + /// + public abstract class PalpableCatchHitObject : CatchHitObject, IHasComboInformation + { + /// + /// Difference between the distance to the next object + /// and the distance that would have triggered a hyper dash. + /// A value close to 0 indicates a difficult jump (for difficulty calculation). + /// + public float DistanceToHyperDash { get; set; } + + /// + /// Whether this fruit can initiate a hyperdash. + /// + public bool HyperDash => HyperDashTarget != null; + + /// + /// The target fruit if we are to initiate a hyperdash. + /// + public CatchHitObject HyperDashTarget; + + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; + } +} diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index a4f54bfe82..dfc81ee8d9 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays float lastPosition = CatchPlayfield.CENTER_X; double lastTime = 0; - void moveToNext(CatchHitObject h) + void moveToNext(PalpableCatchHitObject h) { float positionChange = Math.Abs(lastPosition - h.X); double timeAvailable = h.StartTime - lastTime; @@ -101,23 +101,16 @@ namespace osu.Game.Rulesets.Catch.Replays foreach (var obj in Beatmap.HitObjects) { - switch (obj) + if (obj is PalpableCatchHitObject palpableObject) { - case Fruit _: - moveToNext(obj); - break; + moveToNext(palpableObject); } foreach (var nestedObj in obj.NestedHitObjects.Cast()) { - switch (nestedObj) + if (nestedObj is PalpableCatchHitObject palpableNestedObject) { - case Banana _: - case TinyDroplet _: - case Droplet _: - case Fruit _: - moveToNext(nestedObj); - break; + moveToNext(palpableNestedObject); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 381d066750..1494ef3888 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject, ISkinSource skin) { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; accentColour.BindTo(drawableCatchObject.AccentColour); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 735d7fc300..abbdeacd9a 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -55,21 +55,18 @@ namespace osu.Game.Rulesets.Catch.UI HitObjectContainer, CatcherArea, }; + + NewResult += onNewResult; + RevertResult += onRevertResult; } - public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); - - public override void Add(DrawableHitObject h) + protected override void OnNewDrawableHitObject(DrawableHitObject d) { - h.OnNewResult += onNewResult; - h.OnRevertResult += onRevertResult; - - base.Add(h); - - var fruit = (DrawableCatchHitObject)h; - fruit.CheckPosition = CheckIfWeCanCatch; + ((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch; } + private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a221ca7966..0f0b9df76e 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -220,11 +220,11 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Let the catcher attempt to catch a fruit. /// - /// The fruit to catch. + /// The fruit to catch. /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject fruit) + public bool AttemptCatch(CatchHitObject hitObject) { - if (!fruit.CanBePlated) + if (!(hitObject is PalpableCatchHitObject fruit)) return false; var halfCatchWidth = catchWidth * 0.5f; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 5e794a76aa..ad79a23279 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.UI }; } - public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { if (!result.Type.IsScorable()) return; @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete += _ => action(); } - if (result.IsHit && fruit.HitObject.CanBePlated) + if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.UI if (caughtFruit == null) return; caughtFruit.RelativePositionAxes = Axes.None; - caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); + caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(hitObject.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); caughtFruit.IsOnPlate = true; caughtFruit.Anchor = Anchor.TopCentre; @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.UI runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); } - if (fruit.HitObject.LastInCombo) + if (hitObject.HitObject.LastInCombo) { if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) runAfterLoaded(() => MovableCatcher.Explode()); @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.Drop(); } - comboDisplay.OnNewResult(fruit, result); + comboDisplay.OnNewResult(hitObject, result); } public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 654b752001..538a51db5f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -96,6 +96,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor throw new System.NotImplementedException(); } + public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) + { + throw new System.NotImplementedException(); + } + public override float GetBeatSnapDistanceAt(double referenceTime) { throw new System.NotImplementedException(); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 1ca94df26b..6b532e5014 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var first = (OsuHitObject)objects.First(); var second = (OsuHitObject)objects.Last(); - return first.Position == second.Position; + return Precision.AlmostEquals(first.EndPosition, second.Position); }); } @@ -86,5 +86,64 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return Precision.AlmostEquals(first.EndPosition, second.Position); }); } + + [Test] + public void TestSecondCircleInSelectionAlsoSnaps() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); + + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); + + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddStep("increment time", () => EditorClock.SeekForward(true)); + + AddStep("move mouse right", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.2f, 0))); + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddStep("increment time", () => EditorClock.SeekForward(true)); + + AddStep("move mouse down", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Width * 0.2f))); + AddStep("place third object", () => InputManager.Click(MouseButton.Left)); + + AddStep("enter selection mode", () => InputManager.Key(Key.Number1)); + + AddStep("select objects 2 and 3", () => + { + // add selection backwards to test non-sequential time ordering + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[2]); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]); + }); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); + + AddAssert("object 3 snapped to 1", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var third = (OsuHitObject)objects.Last(); + + return Precision.AlmostEquals(first.EndPosition, third.Position); + }); + + AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f))); + + AddAssert("object 2 snapped to 1", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var second = (OsuHitObject)objects.ElementAt(1); + + return Precision.AlmostEquals(first.EndPosition, second.Position); + }); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 1232369a0b..9af2a99470 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -174,6 +174,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private class SnapProvider : IPositionSnapProvider { + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs deleted file mode 100644 index 776aacd143..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Edit -{ - public class DrawableOsuEditPool : DrawableOsuPool - where T : DrawableHitObject, new() - { - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - - public DrawableOsuEditPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) - : base(checkHittable, onLoaded, initialSize, maximumSize) - { - } - - protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState); - - private void updateState(DrawableHitObject hitObject, ArmedState state) - { - if (state == ArmedState.Idle) - return; - - // adjust the visuals of certain object types to make them stay on screen for longer than usual. - switch (hitObject) - { - default: - // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) - return; - - case DrawableSlider _: - // no specifics to sliders but let them fade slower below. - break; - - case DrawableHitCircle circle: // also handles slider heads - circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension) - .Expire(); - break; - } - - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 547dff88b5..5fdb79cbbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Graphics.Pooling; +using System.Linq; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -26,8 +29,51 @@ namespace osu.Game.Rulesets.Osu.Edit { protected override GameplayCursorContainer CreateCursor() => null; - protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) - => new DrawableOsuEditPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + protected override void OnNewDrawableHitObject(DrawableHitObject d) + { + d.ApplyCustomUpdateState += updateState; + } + + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + private const double editor_hit_object_fade_out_extension = 700; + + private void updateState(DrawableHitObject hitObject, ArmedState state) + { + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) + { + default: + // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) + return; + + case DrawableSlider _: + // no specifics to sliders but let them fade slower below. + break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + } } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index bfa8ab4431..0490e8b8ce 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -105,11 +105,20 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) { if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return snapResult; + return new SnapResult(screenSpacePosition, null); + } + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition); + if (positionSnap.ScreenSpacePosition != screenSpacePosition) + return positionSnap; + // will be null if distance snap is disabled or not feasible for the current time value. if (distanceSnapGrid == null) return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index d1ceca6d8f..abb51ae420 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach + public class DrawableHitCircle : DrawableOsuHitObject { public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs deleted file mode 100644 index 1b5fd50022..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables -{ - public class DrawableOsuPool : DrawablePool - where T : DrawableHitObject, new() - { - private readonly Func checkHittable; - private readonly Action onLoaded; - - public DrawableOsuPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) - : base(initialSize, maximumSize) - { - this.checkHittable = checkHittable; - this.onLoaded = onLoaded; - } - - protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o => - { - var osuObject = (DrawableOsuHitObject)(object)o; - - osuObject.CheckHittable = checkHittable; - osuObject.OnLoadComplete += onLoaded; - }); - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 14c494d909..6340367593 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -19,7 +19,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach + public class DrawableSlider : DrawableOsuHitObject { public new Slider HitObject => (Slider)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 3bd150c4d3..8ff752952c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -25,8 +26,6 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { - public readonly Func CheckHittable; - private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; @@ -56,7 +55,6 @@ namespace osu.Game.Rulesets.Osu.UI }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); - CheckHittable = hitPolicy.IsHittable; var hitWindows = new OsuHitWindows(); @@ -68,6 +66,29 @@ namespace osu.Game.Rulesets.Osu.UI NewResult += onNewResult; } + protected override void OnNewDrawableHitObject(DrawableHitObject drawable) + { + ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; + + Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); + drawable.OnLoadComplete += onDrawableHitObjectLoaded; + } + + private void onDrawableHitObjectLoaded(Drawable drawable) + { + // note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded. + switch (drawable) + { + case DrawableSpinner _: + spinnerProxies.Add(drawable.CreateProxy()); + break; + + case DrawableHitCircle hitCircle: + approachCircles.Add(hitCircle.ProxiedLayer.CreateProxy()); + break; + } + } + private void onJudgmentLoaded(DrawableOsuJudgement judgement) { judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent()); @@ -78,28 +99,19 @@ namespace osu.Game.Rulesets.Osu.UI { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); - registerPool(10, 100); + RegisterPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(5, 50); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(5, 50); - registerPool(2, 20); - registerPool(10, 100); - registerPool(10, 100); + RegisterPool(2, 20); + RegisterPool(10, 100); + RegisterPool(10, 100); } - private void registerPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(CreatePool(initialSize, maximumSize)); - - protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) - where TDrawable : DrawableHitObject, new() - => new DrawableOsuPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); - protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override void OnHitObjectAdded(HitObject hitObject) @@ -114,27 +126,6 @@ namespace osu.Game.Rulesets.Osu.UI followPoints.RemoveFollowPoints((OsuHitObject)hitObject); } - public void OnHitObjectLoaded(Drawable drawable) - { - switch (drawable) - { - case DrawableSliderHead _: - case DrawableSliderTail _: - case DrawableSliderTick _: - case DrawableSliderRepeat _: - case DrawableSpinnerTick _: - break; - - case DrawableSpinner _: - spinnerProxies.Add(drawable.CreateProxy()); - break; - - case IDrawableHitObjectWithProxiedApproach approach: - approachCircles.Add(approach.ProxiedLayer.CreateProxy()); - break; - } - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 8190cf5f89..11830ebe35 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -153,6 +153,9 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IPositionSnapProvider { + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => 10; diff --git a/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs new file mode 100644 index 0000000000..cf5ecf5bf2 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs @@ -0,0 +1,181 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.Profile.Sections.Historical; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Users; +using NUnit.Framework; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using System; +using System.Linq; +using osu.Framework.Testing; +using osu.Framework.Graphics.Shapes; +using static osu.Game.Users.User; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestScenePlayHistorySubsection : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); + + private readonly Bindable user = new Bindable(); + private readonly PlayHistorySubsection section; + + public TestScenePlayHistorySubsection() + { + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + section = new PlayHistorySubsection(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + } + + [Test] + public void TestNullValues() + { + AddStep("Load user", () => user.Value = user_with_null_values); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestEmptyValues() + { + AddStep("Load user", () => user.Value = user_with_empty_values); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestOneValue() + { + AddStep("Load user", () => user.Value = user_with_one_value); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestTwoValues() + { + AddStep("Load user", () => user.Value = user_with_two_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestConstantValues() + { + AddStep("Load user", () => user.Value = user_with_constant_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestConstantZeroValues() + { + AddStep("Load user", () => user.Value = user_with_zero_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestFilledValues() + { + AddStep("Load user", () => user.Value = user_with_filled_values); + AddAssert("Section is visible", () => section.Alpha == 1); + AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlaycounts.Length == getChartValuesLength()); + } + + [Test] + public void TestMissingValues() + { + AddStep("Load user", () => user.Value = user_with_missing_values); + AddAssert("Section is visible", () => section.Alpha == 1); + AddAssert("Array length is 7", () => getChartValuesLength() == 7); + } + + private int getChartValuesLength() => this.ChildrenOfType().Single().Values.Length; + + private static readonly User user_with_null_values = new User + { + Id = 1 + }; + + private static readonly User user_with_empty_values = new User + { + Id = 2, + MonthlyPlaycounts = Array.Empty() + }; + + private static readonly User user_with_one_value = new User + { + Id = 3, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 100 } + } + }; + + private static readonly User user_with_two_values = new User + { + Id = 4, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 2 } + } + }; + + private static readonly User user_with_constant_values = new User + { + Id = 5, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 5 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 5 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 5 } + } + }; + + private static readonly User user_with_zero_values = new User + { + Id = 6, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 0 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 0 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 0 } + } + }; + + private static readonly User user_with_filled_values = new User + { + Id = 7, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 }, + new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 }, + new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 }, + new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 }, + new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } + } + }; + + private static readonly User user_with_missing_values = new User + { + Id = 8, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2020, 1, 1), Count = 100 }, + new UserHistoryCount { Date = new DateTime(2020, 7, 1), Count = 200 } + } + }; + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a4b99bb6e6..89a6ee8b07 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } - public Func LookupKeyBindings { private get; set; } + public Func LookupKeyBindings { get; set; } } public enum OsuSetting diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index f32f8e0c67..81968de304 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +15,7 @@ namespace osu.Game.Graphics.Containers /// /// A container that can scroll to each section inside it. /// + [Cached] public class SectionsContainer : Container where T : Drawable { diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index 42b523fc5c..70db26c817 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -119,7 +119,11 @@ namespace osu.Game.Graphics.UserInterface protected float GetYPosition(float value) { - if (ActualMaxValue == ActualMinValue) return 0; + if (ActualMaxValue == ActualMinValue) + // show line at top if the only value on the graph is positive, + // and at bottom if the only value on the graph is zero or negative. + // just kind of makes most sense intuitively. + return value > 1 ? 0 : 1; return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue); } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 2736c20da9..74eb2b0126 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -164,10 +164,10 @@ namespace osu.Game.Input.Bindings [Description("Toggle now playing overlay")] ToggleNowPlaying, - [Description("Previous Selection")] + [Description("Previous selection")] SelectPrevious, - [Description("Next Selection")] + [Description("Next selection")] SelectNext, [Description("Home")] @@ -176,26 +176,26 @@ namespace osu.Game.Input.Bindings [Description("Toggle notifications")] ToggleNotifications, - [Description("Pause Gameplay")] + [Description("Pause gameplay")] PauseGameplay, // Editor - [Description("Setup Mode")] + [Description("Setup mode")] EditorSetupMode, - [Description("Compose Mode")] + [Description("Compose mode")] EditorComposeMode, - [Description("Design Mode")] + [Description("Design mode")] EditorDesignMode, - [Description("Timing Mode")] + [Description("Timing mode")] EditorTimingMode, [Description("Hold for HUD")] HoldForHUD, - [Description("Random Skin")] + [Description("Random skin")] RandomSkin, [Description("Pause / resume replay")] diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index e6edfb1e3e..f06e02e5e1 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -37,11 +38,11 @@ namespace osu.Game.Overlays.Music bool wasPlaying = musicController.IsPlaying; if (musicController.TogglePause()) - onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track")); + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", action)); return true; case GlobalAction.MusicNext: - musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track"))); + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", action))); return true; @@ -51,11 +52,11 @@ namespace osu.Game.Overlays.Music switch (res) { case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicActionToast("Restart track")); + onScreenDisplay?.Display(new MusicActionToast("Restart track", action)); break; case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicActionToast("Previous track")); + onScreenDisplay?.Display(new MusicActionToast("Previous track", action)); break; } }); @@ -72,9 +73,18 @@ namespace osu.Game.Overlays.Music private class MusicActionToast : Toast { - public MusicActionToast(string action) - : base("Music Playback", action, string.Empty) + private readonly GlobalAction action; + + public MusicActionToast(string value, GlobalAction action) + : base("Music Playback", value, string.Empty) { + this.action = action; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant(); } } } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 1497ca8fa8..4a6316df3f 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -16,10 +16,13 @@ namespace osu.Game.Overlays.OSD private const int toast_minimum_width = 240; private readonly Container content; + protected override Container Content => content; protected readonly OsuSpriteText ValueText; + protected readonly OsuSpriteText ShortcutText; + protected Toast(string description, string value, string shortcut) { Anchor = Anchor.Centre; @@ -68,7 +71,7 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.Centre, Text = value }, - new OsuSpriteText + ShortcutText = new OsuSpriteText { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs new file mode 100644 index 0000000000..b82773155d --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public abstract class ChartProfileSubsection : ProfileSubsection + { + private ProfileLineChart chart; + + protected ChartProfileSubsection(Bindable user, string headerText) + : base(user, headerText) + { + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = 10, + Left = 20, + Right = 40 + }, + Child = chart = new ProfileLineChart() + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserChanged, true); + } + + private void onUserChanged(ValueChangedEvent e) + { + var values = GetValues(e.NewValue); + + if (values == null || values.Length <= 1) + { + Hide(); + return; + } + + chart.Values = fillZeroValues(values); + Show(); + } + + /// + /// Add entries for any missing months (filled with zero values). + /// + private UserHistoryCount[] fillZeroValues(UserHistoryCount[] historyEntries) + { + var filledHistoryEntries = new List(); + + foreach (var entry in historyEntries) + { + var lastFilled = filledHistoryEntries.LastOrDefault(); + + while (lastFilled?.Date.AddMonths(1) < entry.Date) + { + filledHistoryEntries.Add(lastFilled = new UserHistoryCount + { + Count = 0, + Date = lastFilled.Date.AddMonths(1) + }); + } + + filledHistoryEntries.Add(entry); + } + + return filledHistoryEntries.ToArray(); + } + + protected abstract UserHistoryCount[] GetValues(User user); + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs new file mode 100644 index 0000000000..2f15886c3a --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class PlayHistorySubsection : ChartProfileSubsection + { + public PlayHistorySubsection(Bindable user) + : base(user, "Play History") + { + } + + protected override UserHistoryCount[] GetValues(User user) => user?.MonthlyPlaycounts; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs new file mode 100644 index 0000000000..f02aa36b6c --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -0,0 +1,259 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using JetBrains.Annotations; +using System; +using System.Linq; +using osu.Game.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class ProfileLineChart : CompositeDrawable + { + private UserHistoryCount[] values; + + [NotNull] + public UserHistoryCount[] Values + { + get => values; + set + { + if (value.Length == 0) + throw new ArgumentException("At least one value expected!", nameof(value)); + + graph.Values = values = value; + + createRowTicks(); + createColumnTicks(); + } + } + + private readonly UserHistoryGraph graph; + private readonly Container rowTicksContainer; + private readonly Container columnTicksContainer; + private readonly Container rowLinesContainer; + private readonly Container columnLinesContainer; + + public ProfileLineChart() + { + RelativeSizeAxes = Axes.X; + Height = 250; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + rowTicksContainer = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + rowLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + columnLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + } + } + }, + graph = new UserHistoryGraph + { + RelativeSizeAxes = Axes.Both + } + } + } + }, + new[] + { + Empty(), + columnTicksContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 10 } + } + } + } + }; + } + + private void createRowTicks() + { + rowTicksContainer.Clear(); + rowLinesContainer.Clear(); + + var min = values.Select(v => v.Count).Min(); + var max = values.Select(v => v.Count).Max(); + + var tickInterval = getTickInterval(max - min, 6); + + for (long currentTick = 0; currentTick <= max; currentTick += tickInterval) + { + if (currentTick < min) + continue; + + float y; + + // special-case the min == max case to match LineGraph. + // lerp isn't really well-defined over a zero interval anyway. + if (min == max) + y = currentTick > 1 ? 1 : 0; + else + y = Interpolation.ValueAt(currentTick, 0, 1f, min, max); + + // y axis is inverted in graph-like coordinates. + addRowTick(-y, currentTick); + } + } + + private void createColumnTicks() + { + columnTicksContainer.Clear(); + columnLinesContainer.Clear(); + + var totalMonths = values.Length; + + int monthsPerTick = 1; + + if (totalMonths > 80) + monthsPerTick = 12; + else if (totalMonths >= 45) + monthsPerTick = 3; + else if (totalMonths > 20) + monthsPerTick = 2; + + for (int i = 0; i < totalMonths; i += monthsPerTick) + { + var x = (float)i / (totalMonths - 1); + addColumnTick(x, values[i].Date); + } + } + + private void addRowTick(float y, double value) + { + rowTicksContainer.Add(new TickText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativePositionAxes = Axes.Y, + Margin = new MarginPadding { Right = 3 }, + Text = value.ToString("N0"), + Font = OsuFont.GetFont(size: 12), + Y = y + }); + + rowLinesContainer.Add(new TickLine + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + RelativePositionAxes = Axes.Y, + Height = 0.1f, + EdgeSmoothness = Vector2.One, + Y = y + }); + } + + private void addColumnTick(float x, DateTime value) + { + columnTicksContainer.Add(new TickText + { + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + Text = value.ToString("MMM yyyy"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Rotation = 45, + X = x + }); + + columnLinesContainer.Add(new TickLine + { + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 0.1f, + EdgeSmoothness = Vector2.One, + X = x + }); + } + + private long getTickInterval(long range, int maxTicksCount) + { + // this interval is what would be achieved if the interval was divided perfectly evenly into maxTicksCount ticks. + // can contain ugly fractional parts. + var exactTickInterval = (float)range / (maxTicksCount - 1); + + // the ideal ticks start with a 1, 2 or 5, and are multipliers of powers of 10. + // first off, use log10 to calculate the number of digits in the "exact" interval. + var numberOfDigits = Math.Floor(Math.Log10(exactTickInterval)); + var tickBase = Math.Pow(10, numberOfDigits); + + // then see how the exact tick relates to the power of 10. + var exactTickMultiplier = exactTickInterval / tickBase; + + double tickMultiplier; + + // round up the fraction to start with a 1, 2 or 5. closest match wins. + if (exactTickMultiplier < 1.5) + tickMultiplier = 1.0; + else if (exactTickMultiplier < 3) + tickMultiplier = 2.0; + else if (exactTickMultiplier < 7) + tickMultiplier = 5.0; + else + tickMultiplier = 10.0; + + return Math.Max((long)(tickMultiplier * tickBase), 1); + } + + private class TickText : OsuSpriteText + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Foreground1; + } + } + + private class TickLine : Box + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Background6; + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs new file mode 100644 index 0000000000..e594e8d020 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class ReplaysSubsection : ChartProfileSubsection + { + public ReplaysSubsection(Bindable user) + : base(user, "Replays Watched History") + { + } + + protected override UserHistoryCount[] GetValues(User user) => user?.ReplaysWatchedCounts; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index bfc47bd88c..6e2b9873cf 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -18,8 +18,10 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new Drawable[] { + new PlayHistorySubsection(User), new PaginatedMostPlayedBeatmapContainer(User), new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero), + new ReplaysSubsection(User) }; } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b90aa6863a..35852f60ea 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -442,6 +442,9 @@ namespace osu.Game.Rulesets.Edit public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float DurationToDistance(double referenceTime, double duration); diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index cce631464f..4664f3808c 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -8,12 +8,22 @@ namespace osu.Game.Rulesets.Edit public interface IPositionSnapProvider { /// - /// Given a position, find a valid time snap. + /// Given a position, find a valid time and position snap. /// + /// + /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction. + /// /// The screen-space position to be snapped. /// The time and position post-snapping. SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + /// + /// Given a position, find a value position snap, restricting time to its input value. + /// + /// The screen-space position to be snapped. + /// The position post-snapping. Time will always be null. + SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition); + /// /// Retrieves the distance between two points within a timing point that are one beat length apart. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0fa76a733f..571b05cc05 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public event Action OnRevertResult; + /// + /// Invoked when a new nested hit object is created by . + /// + internal event Action OnNestedDrawableCreated; + /// /// Whether a visual indicator should be displayed when a scoring result occurs. /// @@ -141,6 +146,11 @@ namespace osu.Game.Rulesets.Objects.Drawables private Container samplesContainer; + /// + /// Whether the initialization logic in has applied. + /// + internal bool IsInitialized; + /// /// Creates a new . /// @@ -214,10 +224,15 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h) + var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h); + var drawableNested = pooledDrawableNested ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); + // Invoke the event only if this nested object is just created by `CreateNestedHitObject`. + if (pooledDrawableNested == null) + OnNestedDrawableCreated?.Invoke(drawableNested); + drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; @@ -514,11 +529,10 @@ namespace osu.Game.Rulesets.Objects.Drawables private void updateComboColour() { - if (!(HitObject is IHasComboInformation)) return; + if (!(HitObject is IHasComboInformation combo)) return; - var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; - - AccentColour.Value = GetComboColour(comboColours); + var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + AccentColour.Value = combo.GetComboColour(comboColours); } /// @@ -529,6 +543,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// This will only be called if the implements . /// /// A list of combo colours provided by the beatmap or skin. Can be null if not available. + [Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527 protected virtual Color4 GetComboColour(IReadOnlyList comboColours) { if (!(HitObject is IHasComboInformation combo)) diff --git a/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs b/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs deleted file mode 100644 index 8f4c95c634..0000000000 --- a/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; - -namespace osu.Game.Rulesets.Objects.Drawables -{ - public interface IDrawableHitObjectWithProxiedApproach - { - Drawable ProxiedLayer { get; } - } -} diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 211c077d4f..4f66802079 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Bindables; +using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Types { @@ -35,5 +38,13 @@ namespace osu.Game.Rulesets.Objects.Types /// Whether this is the last object in the current combo. /// bool LastInCombo { get; set; } + + /// + /// Retrieves the colour of the combo described by this object from a set of possible combo colours. + /// Defaults to using to decide the colour. + /// + /// A list of possible combo colours provided by the beatmap or skin. + /// The colour of the combo described by this object. + Color4 GetComboColour([NotNull] IReadOnlyList comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White; } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 82ec653f31..2f589f4ce9 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osuTK; +using System.Diagnostics; namespace osu.Game.Rulesets.UI { @@ -113,6 +114,16 @@ namespace osu.Game.Rulesets.UI } } + private void onNewDrawableHitObject(DrawableHitObject d) + { + d.OnNestedDrawableCreated += onNewDrawableHitObject; + + OnNewDrawableHitObject(d); + + Debug.Assert(!d.IsInitialized); + d.IsInitialized = true; + } + /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// @@ -124,6 +135,11 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { + if (h.IsInitialized) + throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead."); + + onNewDrawableHitObject(h); + HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); } @@ -157,6 +173,17 @@ namespace osu.Game.Rulesets.UI { } + /// + /// Invoked before a new is added to this . + /// It is invoked only once even if the drawable is pooled and used multiple times for different s. + /// + /// + /// This is also invoked for nested s. + /// + protected virtual void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// @@ -321,10 +348,12 @@ namespace osu.Game.Rulesets.UI { var dho = (DrawableHitObject)d; - // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. - // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (!dho.IsLoaded) + if (!dho.IsInitialized) { + onNewDrawableHitObject(dho); + + // If this is the first time this DHO is being used, then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. foreach (var m in mods.OfType()) m.ApplyToDrawableHitObjects(dho.Yield()); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index df9cadebfc..def5f396f1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -187,7 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return false; - if (movementBlueprint != null) + if (movementBlueprints != null) { isDraggingBlueprint = true; changeHandler?.BeginChange(); @@ -299,7 +299,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBlueprints.Remove(blueprint); - if (movementBlueprint == blueprint) + if (movementBlueprints?.Contains(blueprint) == true) finishSelectionMovement(); OnBlueprintRemoved(hitObject); @@ -424,8 +424,8 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2? movementBlueprintOriginalPosition; - private SelectionBlueprint movementBlueprint; + private Vector2[] movementBlueprintOriginalPositions; + private SelectionBlueprint[] movementBlueprints; private bool isDraggingBlueprint; /// @@ -442,8 +442,8 @@ namespace osu.Game.Screens.Edit.Compose.Components return; // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject - movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); - movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct + movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray(); + movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); } /// @@ -453,30 +453,47 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a movement was active. private bool moveCurrentSelection(DragEvent e) { - if (movementBlueprint == null) + if (movementBlueprints == null) return false; if (snapProvider == null) return true; - Debug.Assert(movementBlueprintOriginalPosition != null); + Debug.Assert(movementBlueprintOriginalPositions != null); - HitObject draggedObject = movementBlueprint.HitObject; + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // check for positional snap for every object in selection (for things like object-object snapping) + for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++) + { + var testPosition = movementBlueprintOriginalPositions[i] + distanceTravelled; + + var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition); + + if (positionalResult.ScreenSpacePosition == testPosition) continue; + + // attempt to move the objects, and abort any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition))) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // hitobject in the selection. // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; // Retrieve a snapped position. var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. - if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) + if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition))) return true; if (result.Time.HasValue) { // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - draggedObject.StartTime; + double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; foreach (HitObject obj in Beatmap.SelectedHitObjects) { @@ -494,11 +511,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a movement was active. private bool finishSelectionMovement() { - if (movementBlueprint == null) + if (movementBlueprints == null) return false; - movementBlueprintOriginalPosition = null; - movementBlueprint = null; + movementBlueprintOriginalPositions = null; + movementBlueprints = null; return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index f6675902fc..20836c0e68 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -224,6 +224,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => track.Length / Zoom; + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 975433d407..657c5834b2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,8 +18,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -28,32 +27,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineHitObjectBlueprint : SelectionBlueprint { - private readonly Circle circle; + private const float thickness = 5; + private const float shadow_radius = 5; + private const float circle_size = 24; + + public Action OnDragHandled; [UsedImplicitly] private readonly Bindable startTime; - public Action OnDragHandled; + private Bindable indexInCurrentComboBindable; + private Bindable comboIndexBindable; + private readonly Circle circle; private readonly DragBar dragBar; - private readonly List shadowComponents = new List(); - - private DrawableHitObject drawableHitObject; - - private Bindable comboColour; - private readonly Container mainComponents; - private readonly OsuSpriteText comboIndexText; - private Bindable comboIndex; - - private const float thickness = 5; - - private const float shadow_radius = 5; - - private const float circle_size = 24; + [Resolved] + private ISkinSource skin { get; set; } public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) @@ -152,46 +145,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateShadows(); } - [BackgroundDependencyLoader(true)] - private void load(HitObjectComposer composer) - { - if (composer != null) - { - // best effort to get the drawable representation for grabbing colour and what not. - drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject); - } - } - protected override void LoadComplete() { base.LoadComplete(); if (HitObject is IHasComboInformation comboInfo) { - comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); - comboIndex.BindValueChanged(combo => - { - comboIndexText.Text = (combo.NewValue + 1).ToString(); - }, true); + indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); + indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); + + comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); + comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + + skin.SourceChanged += updateComboColour; } + } - if (drawableHitObject != null) - { - comboColour = drawableHitObject.AccentColour.GetBoundCopy(); - comboColour.BindValueChanged(colour => - { - if (HitObject is IHasDuration) - mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); - else - mainComponents.Colour = drawableHitObject.AccentColour.Value; + private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); - var col = mainComponents.Colour.TopLeft.Linear; - float brightness = col.R + col.G + col.B; + private void updateComboColour() + { + if (!(HitObject is IHasComboInformation combo)) + return; - // decide the combo index colour based on brightness? - comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; - }, true); - } + var comboColours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + var comboColour = combo.GetComboColour(comboColours); + + if (HitObject is IHasDuration) + mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White); + else + mainComponents.Colour = comboColour; + + var col = mainComponents.Colour.TopLeft.Linear; + float brightness = col.R + col.G + col.B; + + // decide the combo index colour based on brightness? + comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; } protected override void Update() diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 46d5eb40b4..c297a03dbf 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -43,6 +44,21 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); + return wrapSkinnableContent(composer); + } + + protected override Drawable CreateTimelineContent() + { + if (ruleset == null || composer == null) + return base.CreateTimelineContent(); + + return wrapSkinnableContent(new TimelineBlueprintContainer(composer)); + } + + private Drawable wrapSkinnableContent(Drawable content) + { + Debug.Assert(ruleset != null); + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation @@ -51,9 +67,7 @@ namespace osu.Game.Screens.Edit.Compose // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. - return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); + return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content)); } - - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 85467d3bbb..3d5c0ddad4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -375,6 +375,9 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(ScrollEvent e) { + if (e.ControlPressed || e.AltPressed || e.SuperPressed) + return false; + const double precision = 1; double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y; diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index b802b3405a..6e2737256a 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -3,10 +3,12 @@ using System; using System.IO; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -21,6 +23,9 @@ namespace osu.Game.Screens.Edit.Setup private readonly IBindable currentFile = new Bindable(); + [Resolved] + private SectionsContainer sectionsContainer { get; set; } + public FileChooserLabelledTextBox() { currentFile.BindValueChanged(onFileSelected); @@ -47,14 +52,16 @@ namespace osu.Game.Screens.Edit.Setup public void DisplayFileChooser() { - Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) + FileSelector fileSelector; + + Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) { RelativeSizeAxes = Axes.X, Height = 400, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, CurrentFile = { BindTarget = currentFile } }; + + sectionsContainer.ScrollTo(fileSelector); } internal class FileChooserOsuTextBox : OsuTextBox