diff --git a/osu.Android.props b/osu.Android.props
index 4657896fac..0b43fd73f5 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
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..160da75aa9 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
@@ -1,9 +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;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK;
@@ -17,34 +18,49 @@ namespace osu.Game.Rulesets.Catch.Tests
{
base.LoadComplete();
- foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep)));
+ AddStep("show pear", () => SetContents(() => createDrawableFruit(0)));
+ AddStep("show grape", () => SetContents(() => createDrawableFruit(1)));
+ AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2)));
+ AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3)));
+
+ AddStep("show banana", () => SetContents(createDrawableBanana));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
- foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true)));
+ AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true)));
+ AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true)));
+ AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true)));
+ AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
}
- private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
- setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
+ private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
+ SetProperties(new DrawableFruit(new Fruit
+ {
+ IndexInBeatmap = indexInBeatmap,
+ HyperDashBindable = { Value = hyperdash }
+ }));
- private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
+ private Drawable createDrawableBanana() =>
+ SetProperties(new DrawableBanana(new Banana()));
- private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
+ private Drawable createDrawableDroplet(bool hyperdash = false) =>
+ SetProperties(new DrawableDroplet(new Droplet
+ {
+ HyperDashBindable = { Value = hyperdash }
+ }));
- private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
+ private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet()));
+
+ protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d)
{
var hitObject = d.HitObject;
+ hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 });
hitObject.StartTime = 1000000000000;
hitObject.Scale = 1.5f;
- if (hyperdash)
- hitObject.HyperDashTarget = new Banana();
-
d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None;
d.Position = Vector2.Zero;
@@ -55,15 +71,5 @@ namespace osu.Game.Rulesets.Catch.Tests
};
return d;
}
-
- public class TestCatchFruit : Fruit
- {
- public TestCatchFruit(FruitVisualRepresentation rep)
- {
- VisualRepresentation = rep;
- }
-
- public override FruitVisualRepresentation VisualRepresentation { get; }
- }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
new file mode 100644
index 0000000000..4448e828e7
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
@@ -0,0 +1,32 @@
+// 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.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneFruitVisualChange : TestSceneFruitObjects
+ {
+ private readonly Bindable indexInBeatmap = new Bindable();
+ private readonly Bindable hyperDash = new Bindable();
+
+ protected override void LoadComplete()
+ {
+ AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit
+ {
+ IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
+ HyperDashBindable = { BindTarget = hyperDash },
+ }))));
+
+ AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet
+ {
+ HyperDashBindable = { BindTarget = hyperDash },
+ }))));
+
+ Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true);
+ Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true);
+ }
+ }
+}
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/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
index a82d0af102..16ef56d845 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
@@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModEasy : ModEasy
+ public class CatchModEasy : ModEasyWithExtraLives
{
public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs
index d61a88b7df..79de8cccc4 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -3,21 +3,22 @@
using System;
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.
///
public int BananaIndex;
- public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
-
public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List samples = new List { new BananaHitSampleInfo() };
@@ -27,6 +28,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, IEquatable
{
private static readonly string[] lookup_names = { "metronomelow", "catch-banana" };
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 89c51459a6..b45f95a8e6 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public class BananaShower : CatchHitObject, IHasDuration
{
- public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
-
public override bool LastInCombo => true;
public override Judgement CreateJudgement() => new IgnoreJudgement();
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 5985ec9b68..a74055bff9 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -16,32 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const float OBJECT_RADIUS = 64;
- private float x;
+ // This value is after XOffset applied.
+ public readonly Bindable XBindable = new Bindable();
+
+ // This value is before XOffset applied.
+ private float originalX;
///
/// The horizontal position of the fruit between 0 and .
///
public float X
{
- get => x + XOffset;
- set => x = value;
+ // TODO: I don't like this asymmetry.
+ get => XBindable.Value;
+ // originalX is set by `XBindable.BindValueChanged`
+ set => XBindable.Value = value + xOffset;
}
- ///
- /// Whether this object can be placed on the catcher's plate.
- ///
- public virtual bool CanBePlated => false;
+ private float xOffset;
///
/// A random offset applied to , set by the .
///
- internal float XOffset { get; set; }
+ internal float XOffset
+ {
+ get => xOffset;
+ set
+ {
+ xOffset = value;
+ XBindable.Value = originalX + xOffset;
+ }
+ }
public double TimePreempt = 1000;
- public int IndexInBeatmap { get; set; }
+ public readonly Bindable IndexInBeatmapBindable = new Bindable();
- public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4);
+ public int IndexInBeatmap
+ {
+ get => IndexInBeatmapBindable.Value;
+ set => IndexInBeatmapBindable.Value = value;
+ }
public virtual bool NewCombo { get; set; }
@@ -63,13 +78,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();
///
@@ -81,17 +89,13 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value;
}
- public float Scale { get; set; } = 1;
+ public readonly Bindable ScaleBindable = new Bindable(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;
+ public float Scale
+ {
+ get => ScaleBindable.Value;
+ set => ScaleBindable.Value = value;
+ }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
@@ -103,22 +107,10 @@ 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,
- Grape,
- Pineapple,
- Raspberry,
- Banana // banananananannaanana
+ protected CatchHitObject()
+ {
+ XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index a865984d45..efb0958a3a 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -1,28 +1,20 @@
// 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
{
public class DrawableBanana : DrawableFruit
{
+ protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
+
public DrawableBanana(Banana h)
: base(h)
{
}
- 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 +38,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..1faa6a5b0f 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -2,77 +2,42 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
-using osu.Framework.Allocation;
+using JetBrains.Annotations;
+using osu.Framework.Bindables;
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 readonly Bindable XBindable = new Bindable();
- public virtual bool StaysOnPlate => HitObject.CanBePlated;
+ protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
- protected DrawableCatchHitObject(CatchHitObject hitObject)
+ protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject)
{
- X = hitObject.X;
+ Anchor = Anchor.BottomLeft;
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ XBindable.BindTo(HitObject.XBindable);
+ }
+
+ protected override void OnFree()
+ {
+ base.OnFree();
+
+ XBindable.UnbindFrom(HitObject.XBindable);
}
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..9e76265394 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)
{
}
@@ -20,7 +21,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece());
+ HyperDash.BindValueChanged(_ => updatePiece(), true);
+ }
+
+ private void updatePiece()
+ {
+ ScaleContainer.Child = new SkinnableDrawable(
+ new CatchSkinComponent(CatchSkinComponents.Droplet),
+ _ => new DropletPiece
+ {
+ HyperDash = { BindTarget = HyperDash }
+ });
}
protected override void UpdateInitialTransforms()
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index c1c34e4157..4338d80345 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -3,14 +3,20 @@
using System;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
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 readonly Bindable VisualRepresentation = new Bindable();
+
+ protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4);
+
+ public DrawableFruit(CatchHitObject h)
: base(h)
{
}
@@ -18,10 +24,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(
- new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
-
ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
+
+ IndexInBeatmap.BindValueChanged(change =>
+ {
+ VisualRepresentation.Value = GetVisualRepresentation(change.NewValue);
+ }, true);
+
+ VisualRepresentation.BindValueChanged(_ => updatePiece());
+ HyperDash.BindValueChanged(_ => updatePiece(), true);
+ }
+
+ private void updatePiece()
+ {
+ ScaleContainer.Child = new SkinnableDrawable(
+ new CatchSkinComponent(getComponent(VisualRepresentation.Value)),
+ _ => new FruitPiece
+ {
+ VisualRepresentation = { BindTarget = VisualRepresentation },
+ HyperDash = { BindTarget = HyperDash },
+ });
}
private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation)
@@ -48,4 +70,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
}
}
}
+
+ public enum FruitVisualRepresentation
+ {
+ Pear,
+ Grape,
+ Pineapple,
+ Raspberry,
+ Banana // banananananannaanana
+ }
}
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..a3908f94b6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+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;
+
+ public readonly Bindable HyperDash = new Bindable();
+
+ public readonly Bindable ScaleBindable = new Bindable(1);
+
+ public readonly Bindable IndexInBeatmap = new Bindable();
+
+ ///
+ /// The multiplicative factor applied to scale relative to scale.
+ ///
+ protected virtual float ScaleFactor => 1;
+
+ ///
+ /// 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([CanBeNull] 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()
+ {
+ XBindable.BindValueChanged(x =>
+ {
+ if (!IsOnPlate) X = x.NewValue;
+ }, true);
+
+ ScaleBindable.BindValueChanged(scale =>
+ {
+ ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor);
+ }, true);
+
+ IndexInBeatmap.BindValueChanged(_ => UpdateComboColour());
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ HyperDash.BindTo(HitObject.HyperDashBindable);
+ ScaleBindable.BindTo(HitObject.ScaleBindable);
+ IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable);
+ }
+
+ protected override void OnFree()
+ {
+ HyperDash.UnbindFrom(HitObject.HyperDashBindable);
+ ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
+ IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable);
+
+ base.OnFree();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
index ae775684d8..8c4d821b4a 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
@@ -1,21 +1,15 @@
// 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;
-
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableTinyDroplet : DrawableDroplet
{
+ protected override float ScaleFactor => base.ScaleFactor / 2;
+
public DrawableTinyDroplet(TinyDroplet h)
: base(h)
{
}
-
- [BackgroundDependencyLoader]
- private void load()
- {
- ScaleContainer.Scale /= 2;
- }
}
}
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/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
deleted file mode 100644
index 4bffdab3d8..0000000000
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ /dev/null
@@ -1,111 +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.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
-{
- internal class FruitPiece : CompositeDrawable
- {
- ///
- /// Because we're adding a border around the fruit, we need to scale down some.
- ///
- public const float RADIUS_ADJUST = 1.1f;
-
- private Circle border;
- private CatchHitObject hitObject;
-
- public FruitPiece()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
- [BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject)
- {
- DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)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
- }
- }
- },
- });
-
- 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,
- }
- }
- });
- }
- }
-
- protected override void Update()
- {
- base.Update();
- border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1);
- }
-
- private Drawable getFruitFor(FruitVisualRepresentation representation)
- {
- switch (representation)
- {
- case FruitVisualRepresentation.Pear:
- return new PearPiece();
-
- case FruitVisualRepresentation.Grape:
- return new GrapePiece();
-
- case FruitVisualRepresentation.Pineapple:
- return new PineapplePiece();
-
- case FruitVisualRepresentation.Banana:
- return new BananaPiece();
-
- case FruitVisualRepresentation.Raspberry:
- return new RaspberryPiece();
- }
-
- return Empty();
- }
- }
-}
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..c90407ae15
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs
@@ -0,0 +1,37 @@
+// 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.Bindables;
+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 readonly Bindable HyperDash = new Bindable();
+
+ public DropletPiece()
+ {
+ Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject)
+ {
+ InternalChild = new Pulp
+ {
+ RelativeSizeAxes = Axes.Both,
+ AccentColour = { BindTarget = drawableObject.AccentColour }
+ };
+
+ if (HyperDash.Value)
+ {
+ AddInternal(new HyperDropletBorderPiece());
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs
new file mode 100644
index 0000000000..31487ee407
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs
@@ -0,0 +1,79 @@
+// 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 JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces
+{
+ internal class FruitPiece : CompositeDrawable
+ {
+ ///
+ /// Because we're adding a border around the fruit, we need to scale down some.
+ ///
+ public const float RADIUS_ADJUST = 1.1f;
+
+ public readonly Bindable VisualRepresentation = new Bindable();
+ public readonly Bindable HyperDash = new Bindable();
+
+ [CanBeNull]
+ private DrawableCatchHitObject drawableHitObject;
+
+ [CanBeNull]
+ private BorderPiece borderPiece;
+
+ public FruitPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader(permitNulls: true)]
+ private void load([CanBeNull] DrawableHitObject drawable)
+ {
+ drawableHitObject = (DrawableCatchHitObject)drawable;
+
+ AddInternal(getFruitFor(VisualRepresentation.Value));
+
+ // if it is not part of a DHO, the border is always invisible.
+ if (drawableHitObject != null)
+ AddInternal(borderPiece = new BorderPiece());
+
+ if (HyperDash.Value)
+ AddInternal(new HyperBorderPiece());
+ }
+
+ protected override void Update()
+ {
+ if (borderPiece != null && drawableHitObject?.HitObject != null)
+ borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1);
+ }
+
+ private Drawable getFruitFor(FruitVisualRepresentation representation)
+ {
+ switch (representation)
+ {
+ case FruitVisualRepresentation.Pear:
+ return new PearPiece();
+
+ case FruitVisualRepresentation.Grape:
+ return new GrapePiece();
+
+ case FruitVisualRepresentation.Pineapple:
+ return new PineapplePiece();
+
+ case FruitVisualRepresentation.Banana:
+ return new BananaPiece();
+
+ case FruitVisualRepresentation.Raspberry:
+ return new RaspberryPiece();
+ }
+
+ return Empty();
+ }
+ }
+}
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..0cd3af01df
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
@@ -0,0 +1,48 @@
+// 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.Bindables;
+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; }
+
+ public readonly Bindable HyperDashBindable = new Bindable();
+
+ ///
+ /// Whether this fruit can initiate a hyperdash.
+ ///
+ public bool HyperDash => HyperDashBindable.Value;
+
+ private CatchHitObject hyperDashTarget;
+
+ ///
+ /// The target fruit if we are to initiate a hyperdash.
+ ///
+ public CatchHitObject HyperDashTarget
+ {
+ get => hyperDashTarget;
+ set
+ {
+ hyperDashTarget = value;
+ HyperDashBindable.Value = value != null;
+ }
+ }
+
+ 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..9df32d8d36 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -57,19 +57,22 @@ namespace osu.Game.Rulesets.Catch.UI
};
}
- public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
-
- public override void Add(DrawableHitObject h)
+ protected override void LoadComplete()
{
- h.OnNewResult += onNewResult;
- h.OnRevertResult += onRevertResult;
+ base.LoadComplete();
- base.Add(h);
-
- var fruit = (DrawableCatchHitObject)h;
- fruit.CheckPosition = CheckIfWeCanCatch;
+ // these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case).
+ NewResult += onNewResult;
+ RevertResult += onRevertResult;
}
+ protected override void OnNewDrawableHitObject(DrawableHitObject d)
+ {
+ ((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.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
index a4d4ec50f8..dcb25f21ba 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs
@@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (hitWindows.IsHitResultAllowed(result))
{
AddStep("Show " + result.GetDescription(), () => SetContents(() =>
- new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
+ {
+ Type = result
+ }, null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs
index ff77df0ae0..4093aeb2a7 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs
@@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModEasy : ModEasy
+ public class ManiaModEasy : ModEasyWithExtraLives
{
public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!";
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index ebce40a785..a3dcd0e57f 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -19,22 +20,42 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
- protected override double FadeInDuration => 50;
+ protected override void ApplyMissAnimations()
+ {
+ if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece))
+ {
+ // this is temporary logic until mania's skin transformer returns IAnimatableJudgements
+ JudgementBody.ScaleTo(1.6f);
+ JudgementBody.ScaleTo(1, 100, Easing.In);
+
+ JudgementBody.MoveTo(Vector2.Zero);
+ JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ JudgementBody.RotateTo(0);
+ JudgementBody.RotateTo(40, 800, Easing.InQuint);
+ JudgementBody.FadeOutFromOne(800);
+
+ LifetimeEnd = JudgementBody.LatestTransformEndTime;
+ }
+
+ base.ApplyMissAnimations();
+ }
protected override void ApplyHitAnimations()
{
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
- JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250);
- this.Delay(FadeInDuration).FadeOut(200);
+ JudgementBody.Delay(50)
+ .ScaleTo(0.75f, 250)
+ .FadeOut(200);
}
- protected override Drawable CreateDefaultJudgement(HitResult result) => new ManiaJudgementPiece(result);
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
- private class ManiaJudgementPiece : DefaultJudgementPiece
+ private class DefaultManiaJudgementPiece : DefaultJudgementPiece
{
- public ManiaJudgementPiece(HitResult result)
+ public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}
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.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index 646f12f710..e4158d8f07 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
- AddAssert("judgement body immediately visible",
- () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1));
- AddAssert("hit lighting hidden",
- () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
+ AddAssert("hit lighting has no transforms", () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any()));
+ AddAssert("hit lighting hidden", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
}
[Test]
@@ -57,10 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
showResult(HitResult.Great);
AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
- AddAssert("judgement body not immediately visible",
- () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
- AddAssert("hit lighting shown",
- () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0));
+ AddUntilStep("hit lighting shown", () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Alpha > 0));
}
private void showResult(HitResult result)
@@ -89,7 +84,13 @@ namespace osu.Game.Rulesets.Osu.Tests
Children = new Drawable[]
{
pool,
- pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
+ pool.Get(j => j.Apply(new JudgementResult(new HitObject
+ {
+ StartTime = Time.Current
+ }, new Judgement())
+ {
+ Type = result,
+ }, null)).With(j =>
{
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
@@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestDrawableOsuJudgement : DrawableOsuJudgement
{
public new SkinnableSprite Lighting => base.Lighting;
- public new Container JudgementBody => base.JudgementBody;
+ public new SkinnableDrawable JudgementBody => base.JudgementBody;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
index 6c077eb214..fe67b63252 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
addMultipleObjectsStep();
- AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
+ AddStep("move hitobject", () =>
+ {
+ var manualClock = new ManualClock();
+ followPointRenderer.Clock = new FramedClock(manualClock);
+
+ manualClock.CurrentTime = getObject(1).HitObject.StartTime;
+ followPointRenderer.UpdateSubTree();
+
+ getObject(2).HitObject.Position = new Vector2(300, 100);
+ });
assertGroups();
+ assertDirections();
}
[TestCase(0, 0)] // Start -> Start
@@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertGroups()
{
- AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
+ AddAssert("has correct group count", () => followPointRenderer.Entries.Count == hitObjectContainer.Count);
AddAssert("group endpoints are correct", () =>
{
for (int i = 0; i < hitObjectContainer.Count; i++)
@@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests
DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
- if (getGroup(i).Start != expectedStart.HitObject)
+ if (getEntry(i).Start != expectedStart.HitObject)
throw new AssertionException($"Object {i} expected to be the start of group {i}.");
- if (getGroup(i).End != expectedEnd?.HitObject)
+ if (getEntry(i).End != expectedEnd?.HitObject)
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
}
@@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests
if (expectedEnd == null)
continue;
+ var manualClock = new ManualClock();
+ followPointRenderer.Clock = new FramedClock(manualClock);
+
+ manualClock.CurrentTime = expectedStart.HitObject.StartTime;
+ followPointRenderer.UpdateSubTree();
+
var points = getGroup(i).ChildrenOfType().ToArray();
if (points.Length == 0)
continue;
@@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
- private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
+ private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index];
+
+ private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType().Single(c => c.Entry == getEntry(index));
private class TestHitObjectContainer : Container
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index 596bc06c68..1278a0ff2d 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -1,17 +1,17 @@
// 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.Containers;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osuTK;
-using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -38,13 +38,37 @@ namespace osu.Game.Rulesets.Osu.Tests
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
+ {
+ var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
+
+ var playfield = new TestOsuPlayfield();
+ playfield.Add(drawable);
+ return playfield;
+ }
+
+ private Drawable testStream(float circleSize, bool auto = false)
+ {
+ var playfield = new TestOsuPlayfield();
+
+ Vector2 pos = new Vector2(-250, 0);
+
+ for (int i = 0; i <= 1000; i += 100)
+ {
+ playfield.Add(createSingle(circleSize, auto, i, pos));
+ pos.X += 50;
+ }
+
+ return playfield;
+ }
+
+ private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset)
{
positionOffset ??= Vector2.Zero;
var circle = new HitCircle
{
StartTime = Time.Current + 1000 + timeOffset,
- Position = positionOffset.Value,
+ Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
};
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
@@ -53,31 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
-
return drawable;
}
protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto)
{
- Anchor = Anchor.Centre,
Depth = depthIndex++
};
- private Drawable testStream(float circleSize, bool auto = false)
- {
- var container = new Container { RelativeSizeAxes = Axes.Both };
-
- Vector2 pos = new Vector2(-250, 0);
-
- for (int i = 0; i <= 1000; i += 100)
- {
- container.Add(testSingle(circleSize, auto, i, pos));
- pos.X += 50;
- }
-
- return container;
- }
-
protected class TestDrawableHitCircle : DrawableHitCircle
{
private readonly bool auto;
@@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests
base.CheckForResult(userTriggered, timeOffset);
}
}
+
+ protected class TestOsuPlayfield : OsuPlayfield
+ {
+ public TestOsuPlayfield()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
index d692be89b2..7e973d0971 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs
@@ -1,8 +1,11 @@
// 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.Diagnostics;
+using osu.Framework.Threading;
using osu.Framework.Utils;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
@@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneShaking : TestSceneHitCircle
{
+ private readonly List scheduledTasks = new List();
+
+ protected override IBeatmap CreateBeatmapForSkinProvider()
+ {
+ // best way to run cleanup before a new step is run
+ foreach (var task in scheduledTasks)
+ task.Cancel();
+
+ scheduledTasks.Clear();
+
+ return base.CreateBeatmapForSkinProvider();
+ }
+
protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto)
{
var drawableHitObject = base.CreateDrawableHitCircle(circle, auto);
@@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
- Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay);
+ scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay));
return drawableHitObject;
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 7375c0e981..ce5dc4855e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
+
internal readonly Container Pieces;
internal readonly Container Connections;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 7ae4f387ca..d592e129d9 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
+ private readonly BindableList controlPoints = new BindableList();
+ private readonly IBindable pathVersion = new Bindable();
+
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
@@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
};
}
- private IBindable pathVersion;
-
protected override void LoadComplete()
{
base.LoadComplete();
- pathVersion = HitObject.Path.Version.GetBoundCopy();
+ controlPoints.BindTo(HitObject.Path.ControlPoints);
+
+ pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject);
@@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
- private BindableList controlPoints => HitObject.Path.ControlPoints;
-
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;
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/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs
index f13c7d2ff6..06b5b6cfb8 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs
@@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModEasy : ModEasy
+ public class OsuModEasy : ModEasyWithExtraLives
{
public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
index a981648444..b989500066 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs
@@ -1,12 +1,14 @@
// 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning;
@@ -15,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
///
/// A single follow point positioned between two adjacent s.
///
- public class FollowPoint : Container, IAnimationTimeReference
+ public class FollowPoint : PoolableDrawable, IAnimationTimeReference
{
private const float width = 8;
@@ -25,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
Origin = Anchor.Centre;
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
+ InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
@@ -46,6 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
});
}
- public double AnimationStartTime { get; set; }
+ public Bindable AnimationStartTime { get; } = new BindableDouble();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 3a9e19b361..6e7b1050cb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Diagnostics;
-using JetBrains.Annotations;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
using osuTK;
@@ -15,150 +12,106 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
///
/// Visualises the s between two s.
///
- public class FollowPointConnection : CompositeDrawable
+ public class FollowPointConnection : PoolableDrawable
{
// Todo: These shouldn't be constants
- private const int spacing = 32;
- private const double preempt = 800;
+ public const int SPACING = 32;
+ public const double PREEMPT = 800;
- public override bool RemoveWhenNotAlive => false;
+ public FollowPointLifetimeEntry Entry;
+ public DrawablePool Pool;
- ///
- /// The start time of .
- ///
- public readonly Bindable StartTime = new BindableDouble();
-
- ///
- /// The which s will exit from.
- ///
- [NotNull]
- public readonly OsuHitObject Start;
-
- ///
- /// Creates a new .
- ///
- /// The which s will exit from.
- public FollowPointConnection([NotNull] OsuHitObject start)
+ protected override void PrepareForUse()
{
- Start = start;
+ base.PrepareForUse();
- RelativeSizeAxes = Axes.Both;
+ Entry.Invalidated += onEntryInvalidated;
- StartTime.BindTo(start.StartTimeBindable);
+ refreshPoints();
}
- protected override void LoadComplete()
+ protected override void FreeAfterUse()
{
- base.LoadComplete();
- bindEvents(Start);
+ base.FreeAfterUse();
+
+ Entry.Invalidated -= onEntryInvalidated;
+
+ // Return points to the pool.
+ ClearInternal(false);
+
+ Entry = null;
}
- private OsuHitObject end;
+ private void onEntryInvalidated() => refreshPoints();
- ///
- /// The which s will enter.
- ///
- [CanBeNull]
- public OsuHitObject End
+ private void refreshPoints()
{
- get => end;
- set
- {
- end = value;
+ ClearInternal(false);
- if (end != null)
- bindEvents(end);
+ OsuHitObject start = Entry.Start;
+ OsuHitObject end = Entry.End;
- if (IsLoaded)
- scheduleRefresh();
- else
- refresh();
- }
- }
+ double startTime = start.GetEndTime();
- private void bindEvents(OsuHitObject obj)
- {
- obj.PositionBindable.BindValueChanged(_ => scheduleRefresh());
- obj.DefaultsApplied += _ => scheduleRefresh();
- }
-
- private void scheduleRefresh()
- {
- Scheduler.AddOnce(refresh);
- }
-
- private void refresh()
- {
- double startTime = Start.GetEndTime();
-
- LifetimeStart = startTime;
-
- if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
- {
- // ensure we always set a lifetime for full LifetimeManagementContainer benefits
- LifetimeEnd = LifetimeStart;
- return;
- }
-
- Vector2 startPosition = Start.StackedEndPosition;
- Vector2 endPosition = End.StackedPosition;
- double endTime = End.StartTime;
+ Vector2 startPosition = start.StackedEndPosition;
+ Vector2 endPosition = end.StackedPosition;
Vector2 distanceVector = endPosition - startPosition;
int distance = (int)distanceVector.Length;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
- double duration = endTime - startTime;
- double? firstTransformStartTime = null;
double finalTransformEndTime = startTime;
- int point = 0;
-
- ClearInternal();
-
- for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
+ for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING)
{
float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
- double fadeOutTime = startTime + fraction * duration;
- double fadeInTime = fadeOutTime - preempt;
+
+ GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime);
FollowPoint fp;
- AddInternal(fp = new FollowPoint());
-
- Debug.Assert(End != null);
+ AddInternal(fp = Pool.Get());
+ fp.ClearTransforms();
fp.Position = pointStartPosition;
fp.Rotation = rotation;
fp.Alpha = 0;
- fp.Scale = new Vector2(1.5f * End.Scale);
+ fp.Scale = new Vector2(1.5f * end.Scale);
- firstTransformStartTime ??= fadeInTime;
-
- fp.AnimationStartTime = fadeInTime;
+ fp.AnimationStartTime.Value = fadeInTime;
using (fp.BeginAbsoluteSequence(fadeInTime))
{
- fp.FadeIn(End.TimeFadeIn);
- fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out);
- fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out);
- fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn);
+ fp.FadeIn(end.TimeFadeIn);
+ fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
+ fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
+ fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn);
- finalTransformEndTime = fadeOutTime + End.TimeFadeIn;
+ finalTransformEndTime = fadeOutTime + end.TimeFadeIn;
}
-
- point++;
}
- int excessPoints = InternalChildren.Count - point;
- for (int i = 0; i < excessPoints; i++)
- RemoveInternal(InternalChildren[^1]);
-
// todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed.
- LifetimeStart = firstTransformStartTime ?? startTime;
- LifetimeEnd = finalTransformEndTime;
+ Entry.LifetimeEnd = finalTransformEndTime;
+ }
+
+ ///
+ /// Computes the fade time of follow point positioned between two hitobjects.
+ ///
+ /// The first , where follow points should originate from.
+ /// The second , which follow points should target.
+ /// The fractional distance along and at which the follow point is to be located.
+ /// The fade-in time of the follow point/
+ /// The fade-out time of the follow point.
+ public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime)
+ {
+ double startTime = start.GetEndTime();
+ double duration = end.StartTime - startTime;
+
+ fadeOutTime = startTime + fraction * duration;
+ fadeInTime = fadeOutTime - PREEMPT;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
new file mode 100644
index 0000000000..a167cb2f0f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs
@@ -0,0 +1,98 @@
+// 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.Bindables;
+using osu.Framework.Graphics.Performance;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
+{
+ public class FollowPointLifetimeEntry : LifetimeEntry
+ {
+ public event Action Invalidated;
+ public readonly OsuHitObject Start;
+
+ public FollowPointLifetimeEntry(OsuHitObject start)
+ {
+ Start = start;
+ LifetimeStart = Start.StartTime;
+
+ bindEvents();
+ }
+
+ private OsuHitObject end;
+
+ public OsuHitObject End
+ {
+ get => end;
+ set
+ {
+ UnbindEvents();
+
+ end = value;
+
+ bindEvents();
+
+ refreshLifetimes();
+ }
+ }
+
+ private void bindEvents()
+ {
+ UnbindEvents();
+
+ // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
+ Start.DefaultsApplied += onDefaultsApplied;
+ Start.PositionBindable.ValueChanged += onPositionChanged;
+
+ if (End != null)
+ {
+ End.DefaultsApplied += onDefaultsApplied;
+ End.PositionBindable.ValueChanged += onPositionChanged;
+ }
+ }
+
+ public void UnbindEvents()
+ {
+ if (Start != null)
+ {
+ Start.DefaultsApplied -= onDefaultsApplied;
+ Start.PositionBindable.ValueChanged -= onPositionChanged;
+ }
+
+ if (End != null)
+ {
+ End.DefaultsApplied -= onDefaultsApplied;
+ End.PositionBindable.ValueChanged -= onPositionChanged;
+ }
+ }
+
+ private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
+
+ private void onPositionChanged(ValueChangedEvent obj) => refreshLifetimes();
+
+ private void refreshLifetimes()
+ {
+ if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
+ {
+ LifetimeEnd = LifetimeStart;
+ return;
+ }
+
+ Vector2 startPosition = Start.StackedEndPosition;
+ Vector2 endPosition = End.StackedPosition;
+ Vector2 distanceVector = endPosition - startPosition;
+
+ // The lifetime start will match the fade-in time of the first follow point.
+ float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length;
+ FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _);
+
+ LifetimeStart = fadeInTime;
+ LifetimeEnd = double.MaxValue; // This will be set by the connection.
+
+ Invalidated?.Invoke();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
index be1392d7c3..3e85e528e8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs
@@ -2,53 +2,74 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
-using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Performance;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
///
/// Visualises connections between s.
///
- public class FollowPointRenderer : LifetimeManagementContainer
+ public class FollowPointRenderer : CompositeDrawable
{
- ///
- /// All the s contained by this .
- ///
- internal IReadOnlyList Connections => connections;
-
- private readonly List connections = new List();
-
public override bool RemoveCompletedTransforms => false;
- ///
- /// Adds the s around an .
- /// This includes s leading into , and s exiting .
- ///
- /// The to add s for.
- public void AddFollowPoints(OsuHitObject hitObject)
- => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
+ public IReadOnlyList Entries => lifetimeEntries;
- ///
- /// Removes the s around an .
- /// This includes s leading into , and s exiting .
- ///
- /// The to remove s for.
- public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
+ private DrawablePool connectionPool;
+ private DrawablePool pointPool;
- ///
- /// Adds a to this .
- ///
- /// The to add.
- /// The index of in .
- private void addConnection(FollowPointConnection connection)
+ private readonly List lifetimeEntries = new List();
+ private readonly Dictionary connectionsInUse = new Dictionary();
+ private readonly Dictionary startTimeMap = new Dictionary();
+ private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
+
+ public FollowPointRenderer()
{
- // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
- int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) =>
+ lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
+ lifetimeManager.EntryBecameDead += onEntryBecameDead;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
{
- int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
+ connectionPool = new DrawablePoolNoLifetime(1, 200),
+ pointPool = new DrawablePoolNoLifetime(50, 1000)
+ };
+ }
+
+ public void AddFollowPoints(OsuHitObject hitObject)
+ {
+ addEntry(hitObject);
+
+ var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy();
+ startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject);
+ startTimeMap[hitObject] = startTimeBindable;
+ }
+
+ public void RemoveFollowPoints(OsuHitObject hitObject)
+ {
+ removeEntry(hitObject);
+
+ startTimeMap[hitObject].UnbindAll();
+ startTimeMap.Remove(hitObject);
+ }
+
+ private void addEntry(OsuHitObject hitObject)
+ {
+ var newEntry = new FollowPointLifetimeEntry(hitObject);
+
+ var index = lifetimeEntries.AddInPlace(newEntry, Comparer.Create((e1, e2) =>
+ {
+ int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
if (comp != 0)
return comp;
@@ -61,19 +82,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
return -1;
}));
- if (index < connections.Count - 1)
+ if (index < lifetimeEntries.Count - 1)
{
// Update the connection's end point to the next connection's start point
// h1 -> -> -> h2
// connection nextGroup
- FollowPointConnection nextConnection = connections[index + 1];
- connection.End = nextConnection.Start;
+ FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
+ newEntry.End = nextEntry.Start;
}
else
{
// The end point may be non-null during re-ordering
- connection.End = null;
+ newEntry.End = null;
}
if (index > 0)
@@ -82,23 +103,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2
// prevGroup connection
- FollowPointConnection previousConnection = connections[index - 1];
- previousConnection.End = connection.Start;
+ FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
+ previousEntry.End = newEntry.Start;
}
- AddInternal(connection);
+ lifetimeManager.AddEntry(newEntry);
}
- ///
- /// Removes a from this .
- ///
- /// The to remove.
- /// Whether was removed.
- private void removeGroup(FollowPointConnection connection)
+ private void removeEntry(OsuHitObject hitObject)
{
- RemoveInternal(connection);
+ int index = lifetimeEntries.FindIndex(e => e.Start == hitObject);
- int index = connections.IndexOf(connection);
+ var entry = lifetimeEntries[index];
+ entry.UnbindEvents();
+
+ lifetimeEntries.RemoveAt(index);
+ lifetimeManager.RemoveEntry(entry);
if (index > 0)
{
@@ -106,18 +126,61 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 -> -> -> h3
// prevGroup connection nextGroup
// The current connection's end point is used since there may not be a next connection
- FollowPointConnection previousConnection = connections[index - 1];
- previousConnection.End = connection.End;
+ FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
+ previousEntry.End = entry.End;
}
-
- connections.Remove(connection);
}
- private void onStartTimeChanged(FollowPointConnection connection)
+ protected override bool CheckChildrenLife()
{
- // Naive but can be improved if performance becomes an issue
- removeGroup(connection);
- addConnection(connection);
+ bool anyAliveChanged = base.CheckChildrenLife();
+ anyAliveChanged |= lifetimeManager.Update(Time.Current);
+ return anyAliveChanged;
+ }
+
+ private void onEntryBecameAlive(LifetimeEntry entry)
+ {
+ var connection = connectionPool.Get(c =>
+ {
+ c.Entry = (FollowPointLifetimeEntry)entry;
+ c.Pool = pointPool;
+ });
+
+ connectionsInUse[entry] = connection;
+
+ AddInternal(connection);
+ }
+
+ private void onEntryBecameDead(LifetimeEntry entry)
+ {
+ RemoveInternal(connectionsInUse[entry]);
+ connectionsInUse.Remove(entry);
+ }
+
+ private void onStartTimeChanged(OsuHitObject hitObject)
+ {
+ removeEntry(hitObject);
+ addEntry(hitObject);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ foreach (var entry in lifetimeEntries)
+ entry.UnbindEvents();
+ lifetimeEntries.Clear();
+ }
+
+ private class DrawablePoolNoLifetime : DrawablePool
+ where T : PoolableDrawable, new()
+ {
+ public override bool RemoveWhenNotAlive => false;
+
+ public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
+ : base(initialSize, maximumSize)
+ {
+ }
}
}
}
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/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index a26db06ede..4b7f048c1b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -53,9 +53,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
- protected override void OnApply(HitObject hitObject)
+ protected override void OnApply()
{
- base.OnApply(hitObject);
+ base.OnApply();
IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable);
PositionBindable.BindTo(HitObject.PositionBindable);
@@ -70,9 +70,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
}
- protected override void OnFree(HitObject hitObject)
+ protected override void OnFree()
{
- base.OnFree(hitObject);
+ base.OnFree();
IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable);
PositionBindable.UnbindFrom(HitObject.PositionBindable);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 47fb53379f..13f5960bd4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -44,26 +44,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
- private double fadeOutDelay;
- protected override double FadeOutDelay => fadeOutDelay;
-
protected override void ApplyHitAnimations()
{
bool hitLightingEnabled = config.Get(OsuSetting.HitLighting);
- if (hitLightingEnabled)
- {
- JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
+ Lighting.Alpha = 0;
+ if (hitLightingEnabled && Lighting.Drawable != null)
+ {
+ // todo: this animation changes slightly based on new/old legacy skin versions.
Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
- }
- else
- {
- JudgementBody.Alpha = 1;
- }
- fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
+ // extend the lifetime to cover lighting fade
+ LifetimeEnd = Lighting.LatestTransformEndTime;
+ }
base.ApplyHitAnimations();
}
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 b62c04eed9..322de83a77 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -20,7 +20,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;
@@ -87,18 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.BindValueChanged(updateSlidingSample);
}
- protected override void OnApply(HitObject hitObject)
+ protected override void OnApply()
{
- base.OnApply(hitObject);
+ base.OnApply();
// Ensure that the version will change after the upcoming BindTo().
pathVersion.Value = int.MaxValue;
PathVersion.BindTo(HitObject.Path.Version);
}
- protected override void OnFree(HitObject hitObject)
+ protected override void OnFree()
{
- base.OnFree(hitObject);
+ base.OnFree();
PathVersion.UnbindFrom(HitObject.Path.Version);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index fd0f35d20d..3a92938d75 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -4,7 +4,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
@@ -36,9 +35,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindValueChanged(_ => updatePosition());
}
- protected override void OnFree(HitObject hitObject)
+ protected override void OnFree()
{
- base.OnFree(hitObject);
+ base.OnFree();
pathVersion.UnbindFrom(drawableSlider.PathVersion);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
index bf2236c945..102166f8dd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs
@@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
- private readonly IBindable state = new Bindable();
private readonly IBindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
@@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
- state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
@@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
base.LoadComplete();
- state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour =>
{
explode.Colour = colour.NewValue;
@@ -68,15 +65,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
+
+ drawableObject.ApplyCustomUpdateState += updateState;
+ updateState(drawableObject, drawableObject.State.Value);
}
- private void updateState(ValueChangedEvent state)
+ private void updateState(DrawableHitObject drawableObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{
glow.FadeOut(400);
- switch (state.NewValue)
+ switch (state)
{
case ArmedState.Hit:
const double flash_in = 40;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index c5bf790377..ca5ca7ac59 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
private void trackingChanged(ValueChangedEvent tracking) =>
- box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint);
+ box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index 1551d1c149..21af9a479e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
private SkinnableSpriteText hitCircleText;
- private readonly IBindable state = new Bindable();
private readonly Bindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
@@ -113,7 +112,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (overlayAboveNumber)
AddInternal(hitCircleOverlay.CreateProxy());
- state.BindTo(drawableObject.State);
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
@@ -137,19 +135,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
- state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+
+ drawableObject.ApplyCustomUpdateState += updateState;
+ updateState(drawableObject, drawableObject.State.Value);
}
- private void updateState(ValueChangedEvent state)
+ private void updateState(DrawableHitObject drawableObject, ArmedState state)
{
const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
{
- switch (state.NewValue)
+ switch (state)
{
case ArmedState.Hit:
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index c816502d61..8ff752952c 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -3,9 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -20,15 +20,12 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
-using osu.Game.Skinning;
using osuTK;
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;
@@ -40,86 +37,81 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
- private readonly Bindable playfieldBorderStyle = new BindableBool();
-
private readonly IDictionary> poolDictionary = new Dictionary>();
+ private readonly Container judgementAboveHitObjectLayer;
+
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
- playfieldBorder = new PlayfieldBorder
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 3
- },
- spinnerProxies = new ProxyContainer
- {
- RelativeSizeAxes = Axes.Both
- },
- followPoints = new FollowPointRenderer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 2,
- },
- judgementLayer = new JudgementContainer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = 1,
- },
- // Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
- // Todo: Remove when hitobjects are properly pooled
- new SkinProvidingContainer(null)
- {
- Child = HitObjectContainer,
- },
- approachCircles = new ProxyContainer
- {
- RelativeSizeAxes = Axes.Both,
- Depth = -1,
- },
+ playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
+ spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
+ followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
+ judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
+ HitObjectContainer,
+ judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
+ approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
- CheckHittable = hitPolicy.IsHittable;
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
- poolDictionary.Add(result, new DrawableJudgementPool(result));
+ poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
AddRangeInternal(poolDictionary.Values);
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());
+ }
+
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
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)
@@ -134,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.
@@ -178,11 +149,13 @@ namespace osu.Game.Rulesets.Osu.UI
private class DrawableJudgementPool : DrawablePool
{
private readonly HitResult result;
+ private readonly Action onLoaded;
- public DrawableJudgementPool(HitResult result)
+ public DrawableJudgementPool(HitResult result, Action onLoaded)
: base(10)
{
this.result = result;
+ this.onLoaded = onLoaded;
}
protected override DrawableOsuJudgement CreateNewDrawable()
@@ -192,6 +165,8 @@ namespace osu.Game.Rulesets.Osu.UI
// just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
+ onLoaded?.Invoke(judgement);
+
return judgement;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
index c51b47dc6e..d1ad4c9d8d 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
@@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModEasy : ModEasy
{
- public override string Description => @"Beats move slower, less accuracy required, and three lives!";
+ public override string Description => @"Beats move slower, and less accuracy required!";
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 120cf264c3..370760f03e 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.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;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private SkinnableDrawable mascot;
private ProxyContainer topLevelHitContainer;
- private ProxyContainer barlineContainer;
+ private ScrollingHitObjectContainer barlineContainer;
private Container rightArea;
private Container leftArea;
@@ -83,10 +84,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- barlineContainer = new ProxyContainer
- {
- RelativeSizeAxes = Axes.Both,
- },
+ barlineContainer = new ScrollingHitObjectContainer(),
new Container
{
Name = "Hit objects",
@@ -159,18 +157,37 @@ namespace osu.Game.Rulesets.Taiko.UI
public override void Add(DrawableHitObject h)
{
- h.OnNewResult += OnNewResult;
- base.Add(h);
-
switch (h)
{
case DrawableBarLine barline:
- barlineContainer.Add(barline.CreateProxy());
+ barlineContainer.Add(barline);
break;
case DrawableTaikoHitObject taikoObject:
+ h.OnNewResult += OnNewResult;
topLevelHitContainer.Add(taikoObject.CreateProxiedContent());
+ base.Add(h);
break;
+
+ default:
+ throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type");
+ }
+ }
+
+ public override bool Remove(DrawableHitObject h)
+ {
+ switch (h)
+ {
+ case DrawableBarLine barline:
+ return barlineContainer.Remove(barline);
+
+ case DrawableTaikoHitObject _:
+ h.OnNewResult -= OnNewResult;
+ // todo: consider tidying of proxied content if required.
+ return base.Remove(h);
+
+ default:
+ throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type");
}
}
diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
index b7a41ffd1c..481cb3230e 100644
--- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
+++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
@@ -44,6 +43,36 @@ namespace osu.Game.Tests.Editing
Assert.That(stateChangedFired, Is.EqualTo(2));
}
+ [Test]
+ public void TestApplyThenUndoThenApplySameChange()
+ {
+ var (handler, beatmap) = createChangeHandler();
+
+ Assert.That(handler.CanUndo.Value, Is.False);
+ Assert.That(handler.CanRedo.Value, Is.False);
+
+ string originalHash = handler.CurrentStateHash;
+
+ addArbitraryChange(beatmap);
+ handler.SaveState();
+
+ Assert.That(handler.CanUndo.Value, Is.True);
+ Assert.That(handler.CanRedo.Value, Is.False);
+ Assert.That(stateChangedFired, Is.EqualTo(1));
+
+ string hash = handler.CurrentStateHash;
+
+ // undo a change without saving
+ handler.RestoreState(-1);
+
+ Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
+ Assert.That(stateChangedFired, Is.EqualTo(2));
+
+ addArbitraryChange(beatmap);
+ handler.SaveState();
+ Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
+ }
+
[Test]
public void TestSaveSameStateDoesNotSave()
{
@@ -139,7 +168,7 @@ namespace osu.Game.Tests.Editing
private void addArbitraryChange(EditorBeatmap beatmap)
{
- beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) });
+ beatmap.Add(new HitCircle { StartTime = 2760 });
}
}
}
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
new file mode 100644
index 0000000000..a5c937119e
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -0,0 +1,79 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.NonVisual.Skinning
+{
+ [HeadlessTest]
+ public class LegacySkinAnimationTest : OsuTestScene
+ {
+ private const string animation_name = "animation";
+ private const int frame_count = 6;
+
+ [Cached(typeof(IAnimationTimeReference))]
+ private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference();
+
+ private TextureAnimation animation;
+
+ [Test]
+ public void TestAnimationTimeReferenceChange()
+ {
+ ISkin skin = new TestSkin();
+
+ AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false)));
+ AddAssert("frame count correct", () => animation.FrameCount == frame_count);
+ assertPlaybackPosition(0);
+
+ AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
+ assertPlaybackPosition(-1000);
+
+ AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
+ assertPlaybackPosition(-500);
+ }
+
+ private void assertPlaybackPosition(double expectedPosition)
+ => AddAssert($"playback position is {expectedPosition}", () => animation.PlaybackPosition == expectedPosition);
+
+ private class TestSkin : ISkin
+ {
+ private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray();
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
+ {
+ return lookup_names.Contains(componentName) ? Texture.WhitePixel : null;
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
+ public IBindable GetConfig(TLookup lookup) => throw new NotSupportedException();
+ }
+
+ private class TestAnimationTimeReference : IAnimationTimeReference
+ {
+ public ManualClock ManualClock { get; }
+ public IFrameBasedClock Clock { get; }
+ public Bindable AnimationStartTime { get; } = new BindableDouble();
+
+ public TestAnimationTimeReference()
+ {
+ ManualClock = new ManualClock();
+ Clock = new FramedClock(ManualClock);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 9ef9649f77..5323f58a66 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background
private class FadeAccessibleResults : ResultsScreen
{
public FadeAccessibleResults(ScoreInfo score)
- : base(score)
+ : base(score, true)
{
}
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/Gameplay/TestSceneParticleExplosion.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
new file mode 100644
index 0000000000..82095cb809
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [TestFixture]
+ public class TestSceneParticleExplosion : OsuTestScene
+ {
+ private ParticleExplosion explosion;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ AddStep("create initial", () =>
+ {
+ Child = explosion = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(400)
+ };
+ });
+
+ AddWaitStep("wait for playback", 5);
+
+ AddRepeatStep(@"restart animation", () =>
+ {
+ explosion.Restart();
+ }, 10);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
index 3e777119c4..cd7d692b0a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
@@ -261,9 +261,9 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
- protected override void OnApply(HitObject hitObject)
+ protected override void OnApply()
{
- base.OnApply(hitObject);
+ base.OnApply();
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs
index 709e71d195..717485bcc1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs
@@ -3,7 +3,6 @@
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -14,44 +13,41 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneStarCounter : OsuTestScene
{
+ private readonly StarCounter starCounter;
+ private readonly OsuSpriteText starsLabel;
+
public TestSceneStarCounter()
{
- StarCounter stars = new StarCounter
+ starCounter = new StarCounter
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Current = 5,
};
- Add(stars);
+ Add(starCounter);
- SpriteText starsLabel = new OsuSpriteText
+ starsLabel = new OsuSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Scale = new Vector2(2),
Y = 50,
- Text = stars.Current.ToString("0.00"),
};
Add(starsLabel);
- AddRepeatStep(@"random value", delegate
- {
- stars.Current = RNG.NextSingle() * (stars.StarCount + 1);
- starsLabel.Text = stars.Current.ToString("0.00");
- }, 10);
+ setStars(5);
- AddStep(@"Stop animation", delegate
- {
- stars.StopAnimation();
- });
+ AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10);
+ AddSliderStep("exact value", 0f, 10f, 5f, setStars);
+ AddStep("stop animation", () => starCounter.StopAnimation());
+ AddStep("reset", () => setStars(0));
+ }
- AddStep(@"Reset", delegate
- {
- stars.Current = 0;
- starsLabel.Text = stars.Current.ToString("0.00");
- });
+ private void setStars(float stars)
+ {
+ starCounter.Current = stars;
+ starsLabel.Text = starCounter.Current.ToString("0.00");
}
}
}
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.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index ff96a999ec..b2be7cdf88 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay;
public TestResultsScreen(ScoreInfo score)
- : base(score)
+ : base(score, true)
{
}
@@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking
public HotkeyRetryOverlay RetryOverlay;
public UnrankedSoloResultsScreen(ScoreInfo score)
- : base(score)
+ : base(score, true)
{
Score.Beatmap.OnlineBeatmapID = 0;
Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 4699784327..44c9361ff8 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
get
{
- foreach (var item in ScrollableContent)
+ foreach (var item in Scroll.Children)
{
yield return item;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
similarity index 95%
rename from osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
index 2e9f919cfd..cd226662d7 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs
@@ -11,12 +11,12 @@ using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestScenePaginatedContainerHeader : OsuTestScene
+ public class TestSceneProfileSubsectionHeader : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
- private PaginatedContainerHeader header;
+ private ProfileSubsectionHeader header;
[Test]
public void TestHiddenCounter()
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createHeader(string text, CounterVisibilityState state, int initialValue = 0)
{
Clear();
- Add(header = new PaginatedContainerHeader(text, state)
+ Add(header = new ProfileSubsectionHeader(text, state)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
index b9122d254d..aaad72f65c 100644
--- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs
@@ -12,7 +12,19 @@ using osuTK.Input;
namespace osu.Game.Graphics.Containers
{
- public class OsuScrollContainer : ScrollContainer
+ public class OsuScrollContainer : OsuScrollContainer
+ {
+ public OsuScrollContainer()
+ {
+ }
+
+ public OsuScrollContainer(Direction direction)
+ : base(direction)
+ {
+ }
+ }
+
+ public class OsuScrollContainer : ScrollContainer where T : Drawable
{
public const float SCROLL_BAR_HEIGHT = 10;
public const float SCROLL_BAR_PADDING = 3;
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/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs
new file mode 100644
index 0000000000..e0d2b50c55
--- /dev/null
+++ b/osu.Game/Graphics/ParticleExplosion.cs
@@ -0,0 +1,144 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Vertices;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
+
+namespace osu.Game.Graphics
+{
+ ///
+ /// An explosion of textured particles based on how osu-stable randomises the explosion pattern.
+ ///
+ public class ParticleExplosion : Sprite
+ {
+ private readonly int particleCount;
+ private readonly double duration;
+ private double startTime;
+
+ private readonly List parts = new List();
+
+ public ParticleExplosion(Texture texture, int particleCount, double duration)
+ {
+ Texture = texture;
+ this.particleCount = particleCount;
+ this.duration = duration;
+ Blending = BlendingParameters.Additive;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Restart();
+ }
+
+ ///
+ /// Restart the animation from the current point in time.
+ /// Supports transform time offset chaining.
+ ///
+ public void Restart()
+ {
+ startTime = TransformStartTime;
+ this.FadeOutFromOne(duration);
+
+ parts.Clear();
+ for (int i = 0; i < particleCount; i++)
+ parts.Add(new ParticlePart(duration));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this);
+
+ private class ParticleExplosionDrawNode : SpriteDrawNode
+ {
+ private readonly List parts = new List();
+
+ private ParticleExplosion source => (ParticleExplosion)Source;
+
+ private double startTime;
+ private double currentTime;
+ private Vector2 sourceSize;
+
+ public ParticleExplosionDrawNode(Sprite source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ parts.Clear();
+ parts.AddRange(source.parts);
+
+ sourceSize = source.Size;
+ startTime = source.startTime;
+ currentTime = source.Time.Current;
+ }
+
+ protected override void Blit(Action vertexAction)
+ {
+ var time = currentTime - startTime;
+
+ foreach (var p in parts)
+ {
+ Vector2 pos = p.PositionAtTime(time);
+ float alpha = p.AlphaAtTime(time);
+
+ var rect = new RectangleF(
+ pos.X * sourceSize.X - Texture.DisplayWidth / 2,
+ pos.Y * sourceSize.Y - Texture.DisplayHeight / 2,
+ Texture.DisplayWidth,
+ Texture.DisplayHeight);
+
+ // convert to screen space.
+ var quad = new Quad(
+ Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
+ );
+
+ DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
+ new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
+ null, TextureCoords);
+ }
+ }
+ }
+
+ private readonly struct ParticlePart
+ {
+ private readonly double duration;
+ private readonly float direction;
+ private readonly float distance;
+
+ public ParticlePart(double availableDuration)
+ {
+ distance = RNG.NextSingle(0.5f);
+ duration = RNG.NextDouble(availableDuration / 3, availableDuration);
+ direction = RNG.NextSingle(0, MathF.PI * 2);
+ }
+
+ public float AlphaAtTime(double time) => 1 - progressAtTime(time);
+
+ public Vector2 PositionAtTime(double time)
+ {
+ var travelledDistance = distance * progressAtTime(time);
+ return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction));
+ }
+
+ private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1);
+ }
+ }
+}
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/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs
index b13d6485ac..894a21fcf3 100644
--- a/osu.Game/Graphics/UserInterface/StarCounter.cs
+++ b/osu.Game/Graphics/UserInterface/StarCounter.cs
@@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface
public override void DisplayAt(float scale)
{
- scale = Math.Clamp(scale, min_star_scale, 1);
+ scale = (float)Interpolation.Lerp(min_star_scale, 1, Math.Clamp(scale, 0, 1));
this.FadeTo(scale, fading_duration);
Icon.ScaleTo(scale, scaling_duration, scaling_easing);
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index e5d3a89a88..74eb2b0126 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -69,6 +69,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
+ new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
};
@@ -163,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")]
@@ -175,26 +176,29 @@ namespace osu.Game.Input.Bindings
[Description("Toggle notifications")]
ToggleNotifications,
- [Description("Pause")]
+ [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")]
+ TogglePauseReplay,
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index cb0e2cfa8e..bb638bcf3a 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -420,7 +420,7 @@ namespace osu.Game
break;
case ScorePresentType.Results:
- screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo));
+ screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break;
}
}, validScreens: new[] { typeof(PlaySongSelect) });
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/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index 4b7de8de90..780d7ea986 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Sections.Beatmaps
{
- public class PaginatedBeatmapContainer : PaginatedContainer
+ public class PaginatedBeatmapContainer : PaginatedProfileSubsection
{
private const float panel_padding = 10f;
private readonly BeatmapSetType type;
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/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
index 556f3139dd..e5bb1f8008 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
@@ -13,7 +13,7 @@ using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Sections.Historical
{
- public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer
+ public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection
{
public PaginatedMostPlayedBeatmapContainer(Bindable user)
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
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/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
index 1b8bd23eb4..008d89d881 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
@@ -11,7 +11,7 @@ using System.Collections.Generic;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
- public class PaginatedKudosuHistoryContainer : PaginatedContainer
+ public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection
{
public PaginatedKudosuHistoryContainer(Bindable user)
: base(user, missingText: "This user hasn't received any kudosu!")
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
similarity index 72%
rename from osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
rename to osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
index c1107ce907..51e5622f68 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs
@@ -6,62 +6,51 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
-using osu.Game.Rulesets;
using osu.Game.Users;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
- public abstract class PaginatedContainer : FillFlowContainer
+ public abstract class PaginatedProfileSubsection : ProfileSubsection
{
[Resolved]
private IAPIProvider api { get; set; }
+ [Resolved]
+ protected RulesetStore Rulesets { get; private set; }
+
protected int VisiblePages;
protected int ItemsPerPage;
- protected readonly Bindable User = new Bindable();
- protected FillFlowContainer ItemsContainer;
- protected RulesetStore Rulesets;
+ protected FillFlowContainer ItemsContainer { get; private set; }
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
- private readonly string missingText;
private ShowMoreButton moreButton;
private OsuSpriteText missing;
- private PaginatedContainerHeader header;
+ private readonly string missingText;
- private readonly string headerText;
- private readonly CounterVisibilityState counterVisibilityState;
-
- protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ : base(user, headerText, counterVisibilityState)
{
- this.headerText = headerText;
this.missingText = missingText;
- this.counterVisibilityState = counterVisibilityState;
- User.BindTo(user);
}
- [BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
+ protected override Drawable CreateContent() => new FillFlowContainer
{
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
- Direction = FillDirection.Vertical;
-
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- header = new PaginatedContainerHeader(headerText, counterVisibilityState)
- {
- Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
- },
ItemsContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
@@ -81,13 +70,14 @@ namespace osu.Game.Overlays.Profile.Sections
Font = OsuFont.GetFont(size: 15),
Text = missingText,
Alpha = 0,
- },
- };
+ }
+ }
+ };
- Rulesets = rulesets;
-
- User.ValueChanged += onUserChanged;
- User.TriggerChange();
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ User.BindValueChanged(onUserChanged, true);
}
private void onUserChanged(ValueChangedEvent e)
@@ -124,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Sections
moreButton.Hide();
moreButton.IsLoading = false;
- if (!string.IsNullOrEmpty(missing.Text))
+ if (!string.IsNullOrEmpty(missingText))
missing.Show();
return;
@@ -142,8 +132,6 @@ namespace osu.Game.Overlays.Profile.Sections
protected virtual int GetCount(User user) => 0;
- protected void SetCount(int value) => header.Current.Value = value;
-
protected virtual void OnItemsReceived(List items)
{
}
@@ -154,8 +142,9 @@ namespace osu.Game.Overlays.Profile.Sections
protected override void Dispose(bool isDisposing)
{
- base.Dispose(isDisposing);
retrievalRequest?.Cancel();
+ loadCancellation?.Cancel();
+ base.Dispose(isDisposing);
}
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
new file mode 100644
index 0000000000..3e331f85e9
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs
@@ -0,0 +1,51 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Users;
+using JetBrains.Annotations;
+
+namespace osu.Game.Overlays.Profile.Sections
+{
+ public abstract class ProfileSubsection : FillFlowContainer
+ {
+ protected readonly Bindable User = new Bindable();
+
+ private readonly string headerText;
+ private readonly CounterVisibilityState counterVisibilityState;
+
+ private ProfileSubsectionHeader header;
+
+ protected ProfileSubsection(Bindable user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
+ {
+ this.headerText = headerText;
+ this.counterVisibilityState = counterVisibilityState;
+ User.BindTo(user);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ Direction = FillDirection.Vertical;
+
+ Children = new[]
+ {
+ header = new ProfileSubsectionHeader(headerText, counterVisibilityState)
+ {
+ Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1
+ },
+ CreateContent()
+ };
+ }
+
+ [NotNull]
+ protected abstract Drawable CreateContent();
+
+ protected void SetCount(int value) => header.Current.Value = value;
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs
similarity index 95%
rename from osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs
rename to osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs
index 8c617e5fbd..5858cebe89 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs
+++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs
@@ -14,7 +14,7 @@ using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Sections
{
- public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue
+ public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue
{
private readonly BindableWithCurrent current = new BindableWithCurrent();
@@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections
private CounterPill counterPill;
- public PaginatedContainerHeader(string text, CounterVisibilityState counterState)
+ public ProfileSubsectionHeader(string text, CounterVisibilityState counterState)
{
this.text = text;
this.counterState = counterState;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 1ce3079d52..53f6d375ca 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -14,7 +14,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
- public class PaginatedScoreContainer : PaginatedContainer
+ public class PaginatedScoreContainer : PaginatedProfileSubsection
{
private readonly ScoreType type;
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
index 08f39c6272..d7101a8147 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
@@ -13,7 +13,7 @@ using osu.Framework.Allocation;
namespace osu.Game.Overlays.Profile.Sections.Recent
{
- public class PaginatedRecentActivityContainer : PaginatedContainer
+ public class PaginatedRecentActivityContainer : PaginatedProfileSubsection