diff --git a/global.json b/global.json
index 10b61047ac..2c93a533e4 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.2.3"
+ "Microsoft.Build.Traversal": "3.0.2"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 6e3d5eec1f..9a3d42d6b7 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index b17611f23f..62d8c17058 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -59,7 +59,7 @@ namespace osu.Desktop
try
{
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", "");
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
if (checkExists(stableInstallPath))
return stableInstallPath;
@@ -138,8 +138,8 @@ namespace osu.Desktop
break;
// SDL2 DesktopWindow
- case DesktopWindow desktopWindow:
- desktopWindow.CursorState.Value |= CursorState.Hidden;
+ case SDL2DesktopWindow desktopWindow:
+ desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 285a813d97..6ca7079654 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,9 +22,9 @@ namespace osu.Desktop
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
- bool useSdl = args.Contains("--sdl");
+ bool useOsuTK = args.Contains("--tk");
- using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl))
+ using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK))
{
host.ExceptionThrown += handleException;
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 62e8f7c518..adf9c452f6 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,12 +24,12 @@
-
+
-
-
+
+
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/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index 3c636a5b97..f552c3c27b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests
NewCombo = i % 8 == 0,
Samples = new List(new[]
{
- new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 }
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100)
})
});
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
new file mode 100644
index 0000000000..64695153b5
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.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.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ [TestFixture]
+ public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 6eeda2c731..194a12a9b7 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -1,26 +1,194 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestSceneCatcher : CatchSkinnableTestScene
+ public class TestSceneCatcher : OsuTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private Container droppedObjectContainer;
+
+ private TestCatcher catcher;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- SetContents(() => new Catcher(new Container())
+ var difficulty = new BeatmapDifficulty
+ {
+ CircleSize = 0,
+ };
+
+ var trailContainer = new Container();
+ droppedObjectContainer = new Container();
+ catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
+
+ Child = new Container
{
- RelativePositionAxes = Axes.None,
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ trailContainer,
+ droppedObjectContainer,
+ catcher
+ }
+ };
+ });
+
+ [Test]
+ public void TestCatcherCatchWidth()
+ {
+ var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
+ AddStep("catch fruit", () =>
+ {
+ attemptCatch(new Fruit { X = -halfWidth + 1 });
+ attemptCatch(new Fruit { X = halfWidth - 1 });
});
+ checkPlate(2);
+ AddStep("miss fruit", () =>
+ {
+ attemptCatch(new Fruit { X = -halfWidth - 1 });
+ attemptCatch(new Fruit { X = halfWidth + 1 });
+ });
+ checkPlate(2);
+ }
+
+ [Test]
+ public void TestFruitChangesCatcherState()
+ {
+ AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 }));
+ checkState(CatcherAnimationState.Fail);
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ checkState(CatcherAnimationState.Idle);
+ AddStep("catch kiai fruit", () => attemptCatch(new TestKiaiFruit()));
+ checkState(CatcherAnimationState.Kiai);
+ }
+
+ [Test]
+ public void TestNormalFruitResetsHyperDashState()
+ {
+ AddStep("catch hyper fruit", () => attemptCatch(new Fruit
+ {
+ HyperDashTarget = new Fruit { X = 100 }
+ }));
+ checkHyperDash(true);
+ AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
+ checkHyperDash(false);
+ }
+
+ [Test]
+ public void TestTinyDropletMissPreservesCatcherState()
+ {
+ AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
+ {
+ HyperDashTarget = new Fruit { X = 100 }
+ }));
+ AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
+ AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
+ // catcher state and hyper dash state is preserved
+ checkState(CatcherAnimationState.Kiai);
+ checkHyperDash(true);
+ }
+
+ [Test]
+ public void TestBananaMissPreservesCatcherState()
+ {
+ AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
+ {
+ HyperDashTarget = new Fruit { X = 100 }
+ }));
+ AddStep("miss banana", () => attemptCatch(new Banana { X = 100 }));
+ // catcher state is preserved but hyper dash state is reset
+ checkState(CatcherAnimationState.Kiai);
+ checkHyperDash(false);
+ }
+
+ [Test]
+ public void TestCatcherStacking()
+ {
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ checkPlate(1);
+ AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
+ checkPlate(10);
+ AddAssert("caught objects are stacked", () =>
+ catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < -20));
+ }
+
+ [Test]
+ public void TestCatcherExplosionAndDropping()
+ {
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
+ AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1);
+ AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
+ AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9));
+ AddStep("explode", () => catcher.Explode());
+ AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
+ AddUntilStep("wait explosion", () => !droppedObjectContainer.Any());
+ AddStep("catch fruits", () => attemptCatch(new Fruit(), 10));
+ AddStep("drop", () => catcher.Drop());
+ AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10);
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestHitLighting(bool enabled)
+ {
+ AddStep($"{(enabled ? "enable" : "disable")} hit lighting", () => config.Set(OsuSetting.HitLighting, enabled));
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddAssert("check hit lighting", () => catcher.ChildrenOfType().Any() == enabled);
+ }
+
+ private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
+
+ private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);
+
+ private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
+
+ private void attemptCatch(CatchHitObject hitObject, int count = 1)
+ {
+ hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ for (var i = 0; i < count; i++)
+ catcher.AttemptCatch(hitObject);
+ }
+
+ public class TestCatcher : Catcher
+ {
+ public IEnumerable CaughtObjects => this.ChildrenOfType();
+
+ public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
+ : base(trailsTarget, droppedObjectTarget, difficulty)
+ {
+ }
+ }
+
+ public class TestKiaiFruit : Fruit
+ {
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index e055f08dc2..281ddc7eaa 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -6,18 +6,17 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
-using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -29,82 +28,68 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
- private Catcher catcher => this.ChildrenOfType().First().MovableCatcher;
+ private Catcher catcher => this.ChildrenOfType().First();
+
+ private float circleSize;
public TestSceneCatcherArea()
{
- AddSliderStep("CircleSize", 0, 8, 5, createCatcher);
- AddToggleStep("Hyperdash", t =>
- CreatedDrawables.OfType().Select(i => i.Child)
- .OfType().ForEach(c => c.ToggleHyperDash(t)));
+ AddSliderStep("circle size", 0, 8, 5, createCatcher);
+ AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t)));
- AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
- {
- X = catcher.X
- }), 20);
- AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
- {
- X = catcher.X,
- LastInCombo = true,
- }), 20);
- AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
- {
- X = catcher.X
- }), 20);
- AddRepeatStep("miss fruit", () => catchFruit(new Fruit
- {
- X = catcher.X + 100,
- LastInCombo = true,
- }, true), 20);
+ AddStep("catch fruit", () => attemptCatch(new Fruit()));
+ AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true }));
+ AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit()));
+ AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true }));
}
- [TestCase(true)]
- [TestCase(false)]
- public void TestHitLighting(bool enable)
+ private void attemptCatch(Fruit fruit)
{
- AddStep("create catcher", () => createCatcher(5));
-
- AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable));
- AddStep("catch fruit", () => catchFruit(new TestFruit(false)
+ fruit.X += catcher.X;
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
{
- X = catcher.X
- }));
- AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
- {
- X = catcher.X,
- LastInCombo = true
- }));
- AddAssert("check hit explosion", () => catcher.ChildrenOfType().Any() == enable);
- }
+ CircleSize = circleSize
+ });
- private void catchFruit(Fruit fruit, bool miss = false)
- {
- this.ChildrenOfType().ForEach(area =>
+ foreach (var area in this.ChildrenOfType())
{
DrawableFruit drawable = new DrawableFruit(fruit);
area.Add(drawable);
Schedule(() =>
{
- area.AttemptCatch(fruit);
- area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
+ bool caught = area.AttemptCatch(fruit);
+ area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement())
+ {
+ Type = caught ? HitResult.Great : HitResult.Miss
+ });
drawable.Expire();
});
- });
+ }
}
private void createCatcher(float size)
{
- SetContents(() => new CatchInputManager(catchRuleset)
+ circleSize = size;
+
+ SetContents(() =>
{
- RelativeSizeAxes = Axes.Both,
- Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
+ var droppedObjectContainer = new Container();
+
+ return new CatchInputManager(catchRuleset)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.TopCentre,
- CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
- },
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ droppedObjectContainer,
+ new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size })
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
+ }
+ }
+ };
});
}
@@ -114,26 +99,13 @@ namespace osu.Game.Rulesets.Catch.Tests
catchRuleset = rulesets.GetRuleset(2);
}
- public class TestFruit : Fruit
- {
- public TestFruit(bool kiai)
- {
- var kiaiCpi = new ControlPointInfo();
- kiaiCpi.Add(0, new EffectControlPoint { KiaiMode = kiai });
-
- ApplyDefaultsToSelf(kiaiCpi, new BeatmapDifficulty());
- }
- }
-
private class TestCatcherArea : CatcherArea
{
- public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
- : base(beatmapDifficulty)
+ public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
+ : base(droppedObjectContainer, beatmapDifficulty)
{
}
- public new Catcher MovableCatcher => base.MovableCatcher;
-
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index d35f828e28..3e4995482d 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Tests
if (juice.NestedHitObjects.Last() is CatchHitObject tail)
tail.LastInCombo = true; // usually the (Catch)BeatmapProcessor would do this for us when necessary
- addToPlayfield(new DrawableJuiceStream(juice, drawableRuleset.CreateDrawableRepresentation));
+ addToPlayfield(new DrawableJuiceStream(juice));
}
private void spawnBananas(bool hit = false)
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
index 385d8ed7fa..3a651605d3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.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 System;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
-using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -17,99 +19,69 @@ namespace osu.Game.Rulesets.Catch.Tests
{
base.LoadComplete();
- foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
- AddStep($"show {rep}", () => SetContents(() => createDrawable(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(() => createDrawable(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 createDrawableTinyDroplet()
- {
- var droplet = new TestCatchTinyDroplet
+ private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) =>
+ new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
{
- Scale = 1.5f,
+ IndexInBeatmap = indexInBeatmap,
+ HyperDashBindable = { Value = hyperdash }
+ }));
+
+ private Drawable createDrawableBanana() =>
+ new TestDrawableCatchHitObjectSpecimen(new DrawableBanana(new Banana()));
+
+ private Drawable createDrawableDroplet(bool hyperdash = false) =>
+ new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
+ {
+ HyperDashBindable = { Value = hyperdash }
+ }));
+
+ private Drawable createDrawableTinyDroplet() => new TestDrawableCatchHitObjectSpecimen(new DrawableTinyDroplet(new TinyDroplet()));
+ }
+
+ public class TestDrawableCatchHitObjectSpecimen : CompositeDrawable
+ {
+ public readonly ManualClock ManualClock;
+
+ public TestDrawableCatchHitObjectSpecimen(DrawableCatchHitObject d)
+ {
+ AutoSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ ManualClock = new ManualClock();
+ Clock = new FramedClock(ManualClock);
+
+ var hitObject = d.HitObject;
+ hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ hitObject.Scale = 1.5f;
+ hitObject.StartTime = 500;
+
+ d.Anchor = Anchor.Centre;
+ d.HitObjectApplied += _ =>
+ {
+ d.LifetimeStart = double.NegativeInfinity;
+ d.LifetimeEnd = double.PositiveInfinity;
};
- return new DrawableTinyDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- private Drawable createDrawableDroplet(bool hyperdash = false)
- {
- var droplet = new TestCatchDroplet
- {
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableDroplet(droplet)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
- {
- Fruit fruit = new TestCatchFruit(rep)
- {
- Scale = 1.5f,
- HyperDashTarget = hyperdash ? new Banana() : null
- };
-
- return new DrawableFruit(fruit)
- {
- Anchor = Anchor.Centre,
- RelativePositionAxes = Axes.None,
- Position = Vector2.Zero,
- Alpha = 1,
- LifetimeStart = double.NegativeInfinity,
- LifetimeEnd = double.PositiveInfinity,
- };
- }
-
- public class TestCatchFruit : Fruit
- {
- public TestCatchFruit(FruitVisualRepresentation rep)
- {
- VisualRepresentation = rep;
- StartTime = 1000000000000;
- }
-
- public override FruitVisualRepresentation VisualRepresentation { get; }
- }
-
- public class TestCatchDroplet : Droplet
- {
- public TestCatchDroplet()
- {
- StartTime = 1000000000000;
- }
- }
-
- public class TestCatchTinyDroplet : TinyDroplet
- {
- public TestCatchTinyDroplet()
- {
- StartTime = 1000000000000;
- }
+ InternalChild = d;
}
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs
new file mode 100644
index 0000000000..2ffebb7de1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs
@@ -0,0 +1,96 @@
+// 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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Tests.Visual;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneFruitRandomness : OsuTestScene
+ {
+ private readonly TestDrawableFruit drawableFruit;
+ private readonly TestDrawableBanana drawableBanana;
+
+ public TestSceneFruitRandomness()
+ {
+ drawableFruit = new TestDrawableFruit(new Fruit());
+ drawableBanana = new TestDrawableBanana(new Banana());
+
+ Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 });
+ Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana));
+
+ AddSliderStep("start time", 500, 600, 0, x =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x;
+ });
+ }
+
+ [Test]
+ public void TestFruitRandomness()
+ {
+ // Use values such that the banana colour changes (2/3 of the integers are okay)
+ const int initial_start_time = 500;
+ const int another_start_time = 501;
+
+ float fruitRotation = 0;
+ float bananaRotation = 0;
+ float bananaScale = 0;
+ Color4 bananaColour = new Color4();
+
+ AddStep("Initialize start time", () =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
+
+ fruitRotation = drawableFruit.InnerRotation;
+ bananaRotation = drawableBanana.InnerRotation;
+ bananaScale = drawableBanana.InnerScale;
+ bananaColour = drawableBanana.AccentColour.Value;
+ });
+
+ AddStep("change start time", () =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time;
+ });
+
+ AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation);
+ AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation);
+ AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale);
+ AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour);
+
+ AddStep("reset start time", () =>
+ {
+ drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time;
+ });
+
+ AddAssert("rotation and scale restored", () =>
+ drawableFruit.InnerRotation == fruitRotation &&
+ drawableBanana.InnerRotation == bananaRotation &&
+ drawableBanana.InnerScale == bananaScale &&
+ drawableBanana.AccentColour.Value == bananaColour);
+ }
+
+ private class TestDrawableFruit : DrawableFruit
+ {
+ public float InnerRotation => ScaleContainer.Rotation;
+
+ public TestDrawableFruit(Fruit h)
+ : base(h)
+ {
+ }
+ }
+
+ private class TestDrawableBanana : DrawableBanana
+ {
+ public float InnerRotation => ScaleContainer.Rotation;
+ public float InnerScale => ScaleContainer.Scale.X;
+
+ public TestDrawableBanana(Banana h)
+ : base(h)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs
new file mode 100644
index 0000000000..125e0c674c
--- /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(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
+ {
+ IndexInBeatmapBindable = { BindTarget = indexInBeatmap },
+ HyperDashBindable = { BindTarget = hyperDash },
+ }))));
+
+ AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(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.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 1b8368794c..07cb73e5ff 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () =>
{
- Child = setupSkinHierarchy(catcherArea = new CatcherArea
+ Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index dfe3bf8af4..61ecd79e3d 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
-
+
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 4ecfb7b16d..3f71da713e 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -1,22 +1,26 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
+using System;
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Utils;
+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() };
@@ -26,11 +30,45 @@ namespace osu.Game.Rulesets.Catch.Objects
Samples = samples;
}
- private class BananaHitSampleInfo : HitSampleInfo
- {
- private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
+ // override any external colour changes with banananana
+ Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => getBananaColour();
- public override IEnumerable LookupNames => lookupNames;
+ private Color4 getBananaColour()
+ {
+ switch (StatelessRNG.NextInt(3, RandomSeed))
+ {
+ 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 = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
+
+ public override IEnumerable LookupNames => lookup_names;
+
+ public BananaHitSampleInfo(int volume = 0)
+ : base(string.Empty, volume: volume)
+ {
+ }
+
+ public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default)
+ => new BananaHitSampleInfo(newVolume.GetOr(Volume));
+
+ public bool Equals(BananaHitSampleInfo? other)
+ => other != null;
+
+ public override bool Equals(object? obj)
+ => obj is BananaHitSampleInfo other && Equals(other);
+
+ public override int GetHashCode() => lookup_names.GetHashCode();
}
}
}
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..b86b3a7496 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,19 @@ namespace osu.Game.Rulesets.Catch.Objects
set => LastInComboBindable.Value = value;
}
- public float Scale { get; set; } = 1;
+ public readonly Bindable ScaleBindable = new Bindable(1);
+
+ public float Scale
+ {
+ get => ScaleBindable.Value;
+ set => ScaleBindable.Value = value;
+ }
///
- /// Whether this fruit can initiate a hyperdash.
+ /// The seed value used for visual randomness such as fruit rotation.
+ /// The value is truncated to an integer.
///
- public bool HyperDash => HyperDashTarget != null;
-
- ///
- /// The target fruit if we are to initiate a hyperdash.
- ///
- public CatchHitObject HyperDashTarget;
+ public int RandomSeed => (int)StartTime;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
@@ -103,22 +113,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..8e9d80106b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -1,26 +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 System.Collections.Generic;
+using JetBrains.Annotations;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableBanana : DrawableFruit
{
- public DrawableBanana(Banana h)
+ protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana;
+
+ public DrawableBanana()
+ : this(null)
+ {
+ }
+
+ public DrawableBanana([CanBeNull] Banana h)
: base(h)
{
}
- private Color4? colour;
-
- protected override Color4 GetComboColour(IReadOnlyList comboColours)
+ protected override void LoadComplete()
{
- // override any external colour changes with banananana
- return colour ??= getBananaColour();
+ base.LoadComplete();
+
+ // start time affects the random seed which is used to determine the banana colour
+ StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
}
protected override void UpdateInitialTransforms()
@@ -30,14 +35,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
const float end_scale = 0.6f;
const float random_scale_range = 1.6f;
- ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle()))
+ ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
- ScaleContainer.RotateTo(getRandomAngle())
+ ScaleContainer.RotateTo(getRandomAngle(1))
.Then()
- .RotateTo(getRandomAngle(), HitObject.TimePreempt);
+ .RotateTo(getRandomAngle(2), HitObject.TimePreempt);
- float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
+ float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
}
public override void PlaySamples()
@@ -46,20 +51,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..9b2f95e221 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs
@@ -1,26 +1,27 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
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;
- public DrawableBananaShower(BananaShower s, Func> createDrawableRepresentation = null)
+ public DrawableBananaShower()
+ : this(null)
+ {
+ }
+
+ public DrawableBananaShower([CanBeNull] BananaShower s)
: base(s)
{
- this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
- X = 0;
AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both });
}
@@ -34,18 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
- bananaContainer.Clear();
- }
-
- protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
- {
- switch (hitObject)
- {
- case Banana banana:
- return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
- }
-
- return base.CreateNestedHitObject(hitObject);
+ bananaContainer.Clear(false);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index 7922510a49..6aa8ff439e 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -2,77 +2,48 @@
// 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;
+using osu.Game.Utils;
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
{
+ public readonly Bindable XBindable = new Bindable();
+
protected override double InitialLifetimeOffset => HitObject.TimePreempt;
- public virtual bool StaysOnPlate => HitObject.CanBePlated;
-
- public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
-
protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
- protected DrawableCatchHitObject(CatchHitObject hitObject)
+ public int RandomSeed => HitObject?.RandomSeed ?? 0;
+
+ protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject)
: base(hitObject)
{
- X = hitObject.X;
+ Anchor = Anchor.BottomLeft;
+ }
+
+ ///
+ /// Get a random number in range [0,1) based on seed .
+ ///
+ public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed, series);
+
+ 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..b8acea625b 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs
@@ -1,18 +1,24 @@
// 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.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()
+ : this(null)
+ {
+ }
+
+ public DrawableDroplet([CanBeNull] CatchHitObject h)
: base(h)
{
}
@@ -20,7 +26,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()
@@ -28,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
base.UpdateInitialTransforms();
// roughly matches osu-stable
- float startRotation = RNG.NextSingle() * 20;
+ float startRotation = RandomSingle(1) * 20;
double duration = HitObject.TimePreempt + 2000;
ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
index c1c34e4157..ef9df02a68 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs
@@ -2,15 +2,27 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Utils;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+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()
+ : this(null)
+ {
+ }
+
+ public DrawableFruit([CanBeNull] Fruit h)
: base(h)
{
}
@@ -18,10 +30,31 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- ScaleContainer.Child = new SkinnableDrawable(
- new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece());
+ IndexInBeatmap.BindValueChanged(change =>
+ {
+ VisualRepresentation.Value = GetVisualRepresentation(change.NewValue);
+ }, true);
- ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
+ VisualRepresentation.BindValueChanged(_ => updatePiece());
+ HyperDash.BindValueChanged(_ => updatePiece(), true);
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
+ }
+
+ 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 +81,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..a496a35842 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs
@@ -1,37 +1,33 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-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;
- public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS);
+ public DrawableJuiceStream()
+ : this(null)
+ {
+ }
- public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null)
+ public DrawableJuiceStream([CanBeNull] JuiceStream s)
: base(s)
{
- this.createDrawableRepresentation = createDrawableRepresentation;
RelativeSizeAxes = Axes.X;
Origin = Anchor.BottomLeft;
- X = 0;
AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, });
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
- hitObject.Origin = Anchor.BottomCentre;
-
base.AddNestedHitObject(hitObject);
dropletContainer.Add(hitObject);
}
@@ -39,19 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
- dropletContainer.Clear();
- }
-
- protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
- {
- switch (hitObject)
- {
- case CatchHitObject catchObject:
- return createDrawableRepresentation?.Invoke(catchObject)?.With(o =>
- ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false);
- }
-
- throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}.");
+ dropletContainer.Clear(false);
}
}
}
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..0877b5e248
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
@@ -0,0 +1,85 @@
+// 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;
+
+ public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor;
+
+ 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..8f5a04dfda 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs
@@ -1,21 +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.Allocation;
+using JetBrains.Annotations;
namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public class DrawableTinyDroplet : DrawableDroplet
{
- public DrawableTinyDroplet(TinyDroplet h)
- : base(h)
+ protected override float ScaleFactor => base.ScaleFactor / 2;
+
+ public DrawableTinyDroplet()
+ : this(null)
{
}
- [BackgroundDependencyLoader]
- private void load()
+ public DrawableTinyDroplet([CanBeNull] TinyDroplet h)
+ : base(h)
{
- 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/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index e209d012fa..d5819935ad 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -50,12 +50,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
base.CreateNestedHitObjects(cancellationToken);
- var dropletSamples = Samples.Select(s => new HitSampleInfo
- {
- Bank = s.Bank,
- Name = @"slidertick",
- Volume = s.Volume
- }).ToList();
+ var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null;
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..b8648f46f0 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
private readonly string lookupName;
- private readonly IBindable accentColour = new Bindable();
+ private readonly Bindable accentColour = new Bindable();
+ private readonly Bindable hyperDash = new Bindable();
private Sprite colouredSprite;
public LegacyFruitPiece(string lookupName)
@@ -31,9 +32,10 @@ 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);
+ hyperDash.BindTo(drawableCatchObject.HyperDash);
InternalChildren = new Drawable[]
{
@@ -51,9 +53,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
},
};
- if (drawableCatchObject.HitObject.HyperDash)
+ if (hyperDash.Value)
{
- var hyperDash = new Sprite
+ var hyperDashOverlay = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -67,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
Catcher.DEFAULT_HYPER_DASH_COLOUR,
};
- AddInternal(hyperDash);
+ AddInternal(hyperDashOverlay);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 735d7fc300..df87359ed6 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -2,6 +2,7 @@
// 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.Game.Beatmaps;
@@ -35,41 +36,53 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation)
{
- var explodingFruitContainer = new Container
+ var droppedObjectContainer = new Container
{
RelativeSizeAxes = Axes.Both,
};
- CatcherArea = new CatcherArea(difficulty)
+ CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
{
- CreateDrawableRepresentation = createDrawableRepresentation,
- ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
};
InternalChildren = new[]
{
- explodingFruitContainer,
+ droppedObjectContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer,
CatcherArea,
};
}
- public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
-
- public override void Add(DrawableHitObject h)
+ [BackgroundDependencyLoader]
+ private void load()
{
- h.OnNewResult += onNewResult;
- h.OnRevertResult += onRevertResult;
-
- base.Add(h);
-
- var fruit = (DrawableCatchHitObject)h;
- fruit.CheckPosition = CheckIfWeCanCatch;
+ RegisterPool(50);
+ RegisterPool(50);
+ RegisterPool(100);
+ RegisterPool(100);
+ RegisterPool(10);
+ RegisterPool(2);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // 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..2a3447c80a 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -16,7 +17,6 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -46,19 +46,15 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const double BASE_SPEED = 1.0;
- public Container ExplodingFruitTarget;
-
- private Container caughtFruitContainer { get; } = new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.BottomCentre,
- };
-
[NotNull]
private readonly Container trailsTarget;
private CatcherTrailDisplay trails;
+ private readonly Container droppedObjectTarget;
+
+ private readonly Container caughtFruitContainer;
+
public CatcherAnimationState CurrentState { get; private set; }
///
@@ -91,9 +87,9 @@ namespace osu.Game.Rulesets.Catch.UI
///
private readonly float catchWidth;
- private CatcherSprite catcherIdle;
- private CatcherSprite catcherKiai;
- private CatcherSprite catcherFail;
+ private readonly CatcherSprite catcherIdle;
+ private readonly CatcherSprite catcherKiai;
+ private readonly CatcherSprite catcherFail;
private CatcherSprite currentCatcher;
@@ -107,9 +103,13 @@ namespace osu.Game.Rulesets.Catch.UI
private float hyperDashTargetPosition;
private Bindable hitLighting;
- public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
+ private readonly DrawablePool hitExplosionPool;
+ private readonly Container hitExplosionContainer;
+
+ public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
+ this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
@@ -118,16 +118,15 @@ namespace osu.Game.Rulesets.Catch.UI
Scale = calculateScale(difficulty);
catchWidth = CalculateCatchWidth(Scale);
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
- {
- hitLighting = config.GetBindable(OsuSetting.HitLighting);
InternalChildren = new Drawable[]
{
- caughtFruitContainer,
+ hitExplosionPool = new DrawablePool(10),
+ caughtFruitContainer = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ },
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{
Anchor = Anchor.TopCentre,
@@ -142,9 +141,19 @@ namespace osu.Game.Rulesets.Catch.UI
{
Anchor = Anchor.TopCentre,
Alpha = 0,
- }
+ },
+ hitExplosionContainer = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ },
};
+ }
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ hitLighting = config.GetBindable(OsuSetting.HitLighting);
trails = new CatcherTrailDisplay(this);
updateCatcher();
@@ -166,65 +175,28 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
///
- private static Vector2 calculateScale(BeatmapDifficulty difficulty)
- => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+ private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
///
/// Calculates the width of the area used for attempting catches in gameplay.
///
/// The scale of the catcher.
- internal static float CalculateCatchWidth(Vector2 scale)
- => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
+ internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
///
/// Calculates the width of the area used for attempting catches in gameplay.
///
/// The beatmap difficulty.
- internal static float CalculateCatchWidth(BeatmapDifficulty difficulty)
- => CalculateCatchWidth(calculateScale(difficulty));
-
- ///
- /// Add a caught fruit to the catcher's stack.
- ///
- /// The fruit that was caught.
- public void PlaceOnPlate(DrawableCatchHitObject fruit)
- {
- var ourRadius = fruit.DisplayRadius;
- float theirRadius = 0;
-
- const float allowance = 10;
-
- while (caughtFruitContainer.Any(f =>
- f.LifetimeEnd == double.MaxValue &&
- Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
- {
- var diff = (ourRadius + theirRadius) / allowance;
- fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2;
- fruit.Y -= RNG.NextSingle() * diff;
- }
-
- fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
-
- caughtFruitContainer.Add(fruit);
-
- if (hitLighting.Value)
- {
- AddInternal(new HitExplosion(fruit)
- {
- X = fruit.X,
- Scale = new Vector2(fruit.HitObject.Scale)
- });
- }
- }
+ internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
///
/// 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;
@@ -237,7 +209,10 @@ namespace osu.Game.Rulesets.Catch.UI
catchObjectPosition >= catcherPosition - halfCatchWidth &&
catchObjectPosition <= catcherPosition + halfCatchWidth;
- // only update hyperdash state if we are not catching a tiny droplet.
+ if (validCatch)
+ placeCaughtObject(fruit);
+
+ // droplet doesn't affect the catcher state
if (fruit is TinyDroplet) return validCatch;
if (validCatch && fruit.HyperDash)
@@ -291,24 +266,17 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- private void runHyperDashStateTransition(bool hyperDashing)
+ public void UpdatePosition(float position)
{
- updateTrailVisibility();
+ position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
- if (hyperDashing)
- {
- this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- }
- else
- {
- this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
- }
+ if (position == X)
+ return;
+
+ Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
+ X = position;
}
- private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
-
public bool OnPressed(CatchAction action)
{
switch (action)
@@ -347,56 +315,34 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- public void UpdatePosition(float position)
- {
- position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
-
- if (position == X)
- return;
-
- Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
- X = position;
- }
-
///
/// Drop any fruit off the plate.
///
- public void Drop()
- {
- foreach (var f in caughtFruitContainer.ToArray())
- Drop(f);
- }
+ public void Drop() => clearPlate(DroppedObjectAnimation.Drop);
///
- /// Explode any fruit off the plate.
+ /// Explode all fruit off the plate.
///
- public void Explode()
- {
- foreach (var f in caughtFruitContainer.ToArray())
- Explode(f);
- }
+ public void Explode() => clearPlate(DroppedObjectAnimation.Explode);
- public void Drop(DrawableHitObject fruit)
+ private void runHyperDashStateTransition(bool hyperDashing)
{
- removeFromPlateWithTransform(fruit, f =>
+ updateTrailVisibility();
+
+ if (hyperDashing)
{
- f.MoveToY(f.Y + 75, 750, Easing.InSine);
- f.FadeOut(750);
- });
- }
-
- public void Explode(DrawableHitObject fruit)
- {
- var originalX = fruit.X * Scale.X;
-
- removeFromPlateWithTransform(fruit, f =>
+ this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ }
+ else
{
- f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine);
- f.MoveToX(f.X + originalX * 6, 1000);
- f.FadeOut(750);
- });
+ this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint);
+ }
}
+ private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing;
+
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
@@ -469,33 +415,143 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher();
}
- private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action)
+ private void placeCaughtObject(PalpableCatchHitObject source)
{
- if (ExplodingFruitTarget != null)
+ var caughtObject = createCaughtObject(source);
+
+ if (caughtObject == null) return;
+
+ caughtObject.RelativePositionAxes = Axes.None;
+ caughtObject.X = source.X - X;
+ caughtObject.IsOnPlate = true;
+
+ caughtObject.Anchor = Anchor.TopCentre;
+ caughtObject.Origin = Anchor.Centre;
+ caughtObject.Scale *= 0.5f;
+ caughtObject.LifetimeStart = source.StartTime;
+ caughtObject.LifetimeEnd = double.MaxValue;
+
+ adjustPositionInStack(caughtObject);
+
+ caughtFruitContainer.Add(caughtObject);
+
+ addLighting(caughtObject);
+
+ if (!caughtObject.StaysOnPlate)
+ removeFromPlate(caughtObject, DroppedObjectAnimation.Explode);
+ }
+
+ private void adjustPositionInStack(DrawablePalpableCatchHitObject caughtObject)
+ {
+ const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2;
+ const float allowance = 10;
+
+ float caughtObjectRadius = caughtObject.DisplayRadius;
+
+ while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, caughtObject.Position) < (caughtObjectRadius + radius_div_2) / (allowance / 2)))
{
- fruit.Anchor = Anchor.TopLeft;
- fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+ float diff = (caughtObjectRadius + radius_div_2) / allowance;
- if (!caughtFruitContainer.Remove(fruit))
- // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
- // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
- return;
-
- ExplodingFruitTarget.Add(fruit);
+ caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2;
+ caughtObject.Y -= RNG.NextSingle() * diff;
}
- var actionTime = Clock.CurrentTime;
+ caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
+ }
- fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState;
- onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value);
+ private void addLighting(DrawablePalpableCatchHitObject caughtObject)
+ {
+ if (!hitLighting.Value) return;
- void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state)
+ HitExplosion hitExplosion = hitExplosionPool.Get();
+ hitExplosion.X = caughtObject.X;
+ hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale);
+ hitExplosion.ObjectColour = caughtObject.AccentColour.Value;
+ hitExplosionContainer.Add(hitExplosion);
+ }
+
+ private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source)
+ {
+ switch (source)
{
- using (fruit.BeginAbsoluteSequence(actionTime))
- action(fruit);
+ case Banana banana:
+ return new DrawableBanana(banana);
- fruit.Expire();
+ case Fruit fruit:
+ return new DrawableFruit(fruit);
+
+ case TinyDroplet tiny:
+ return new DrawableTinyDroplet(tiny);
+
+ case Droplet droplet:
+ return new DrawableDroplet(droplet);
+
+ default:
+ return null;
}
}
+
+ private void clearPlate(DroppedObjectAnimation animation)
+ {
+ var caughtObjects = caughtFruitContainer.Children.ToArray();
+ caughtFruitContainer.Clear(false);
+
+ droppedObjectTarget.AddRange(caughtObjects);
+
+ foreach (var caughtObject in caughtObjects)
+ drop(caughtObject, animation);
+ }
+
+ private void removeFromPlate(DrawablePalpableCatchHitObject caughtObject, DroppedObjectAnimation animation)
+ {
+ if (!caughtFruitContainer.Remove(caughtObject))
+ throw new InvalidOperationException("Can only drop a caught object on the plate");
+
+ droppedObjectTarget.Add(caughtObject);
+
+ drop(caughtObject, animation);
+ }
+
+ private void drop(DrawablePalpableCatchHitObject d, DroppedObjectAnimation animation)
+ {
+ var originalX = d.X * Scale.X;
+ var startTime = Clock.CurrentTime;
+
+ d.Anchor = Anchor.TopLeft;
+ d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget);
+
+ // we cannot just apply the transforms because DHO clears transforms when state is updated
+ d.ApplyCustomUpdateState += (o, state) => animate(o, animation, originalX, startTime);
+ if (d.IsLoaded)
+ animate(d, animation, originalX, startTime);
+ }
+
+ private void animate(Drawable d, DroppedObjectAnimation animation, float originalX, double startTime)
+ {
+ using (d.BeginAbsoluteSequence(startTime))
+ {
+ switch (animation)
+ {
+ case DroppedObjectAnimation.Drop:
+ d.MoveToY(d.Y + 75, 750, Easing.InSine);
+ d.FadeOut(750);
+ break;
+
+ case DroppedObjectAnimation.Explode:
+ d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
+ d.MoveToX(d.X + originalX * 6, 1000);
+ d.FadeOut(750);
+ break;
+ }
+
+ d.Expire();
+ }
+ }
+
+ private enum DroppedObjectAnimation
+ {
+ Drop,
+ Explode
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 5e794a76aa..539776354c 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -1,7 +1,6 @@
// 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.Containers;
using osu.Game.Beatmaps;
@@ -10,7 +9,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -21,19 +19,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float CATCHER_SIZE = 106.75f;
- public Func> CreateDrawableRepresentation;
-
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
- public Container ExplodingFruitTarget
- {
- set => MovableCatcher.ExplodingFruitTarget = value;
- }
-
- private DrawableCatchHitObject lastPlateableFruit;
-
- public CatcherArea(BeatmapDifficulty difficulty = null)
+ public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@@ -47,70 +36,29 @@ namespace osu.Game.Rulesets.Catch.UI
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
- MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
+ MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
};
}
- public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
+ public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
{
if (!result.Type.IsScorable())
return;
- void runAfterLoaded(Action action)
- {
- if (lastPlateableFruit == null)
- return;
-
- // this is required to make this run after the last caught fruit runs updateState() at least once.
- // TODO: find a better alternative
- if (lastPlateableFruit.IsLoaded)
- action();
- else
- lastPlateableFruit.OnLoadComplete += _ => action();
- }
-
- if (result.IsHit && fruit.HitObject.CanBePlated)
- {
- // create a new (cloned) fruit to stay on the plate. the original is faded out immediately.
- var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject);
-
- if (caughtFruit == null) return;
-
- caughtFruit.RelativePositionAxes = Axes.None;
- caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0);
- caughtFruit.IsOnPlate = true;
-
- caughtFruit.Anchor = Anchor.TopCentre;
- caughtFruit.Origin = Anchor.Centre;
- caughtFruit.Scale *= 0.5f;
- caughtFruit.LifetimeStart = caughtFruit.HitObject.StartTime;
- caughtFruit.LifetimeEnd = double.MaxValue;
-
- MovableCatcher.PlaceOnPlate(caughtFruit);
- lastPlateableFruit = caughtFruit;
-
- if (!fruit.StaysOnPlate)
- runAfterLoaded(() => MovableCatcher.Explode(caughtFruit));
- }
-
- if (fruit.HitObject.LastInCombo)
+ if (hitObject.HitObject.LastInCombo)
{
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
- runAfterLoaded(() => MovableCatcher.Explode());
+ MovableCatcher.Explode();
else
MovableCatcher.Drop();
}
- comboDisplay.OnNewResult(fruit, result);
+ comboDisplay.OnNewResult(hitObject, result);
}
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
- public void OnReleased(CatchAction action)
- {
- }
-
public bool AttemptCatch(CatchHitObject obj)
{
return MovableCatcher.AttemptCatch(obj);
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index ebe45aa3ab..46733181e3 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -8,7 +8,6 @@ using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@@ -40,30 +39,6 @@ namespace osu.Game.Rulesets.Catch.UI
protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
- public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h)
- {
- switch (h)
- {
- case Banana banana:
- return new DrawableBanana(banana);
-
- case Fruit fruit:
- return new DrawableFruit(fruit);
-
- case JuiceStream stream:
- return new DrawableJuiceStream(stream, CreateDrawableRepresentation);
-
- case BananaShower shower:
- return new DrawableBananaShower(shower, CreateDrawableRepresentation);
-
- case TinyDroplet tiny:
- return new DrawableTinyDroplet(tiny);
-
- case Droplet droplet:
- return new DrawableDroplet(droplet);
- }
-
- return null;
- }
+ public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h) => null;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
index 04a86f83be..24ca778248 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -5,35 +5,43 @@ 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.Utils;
-using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
- public class HitExplosion : CompositeDrawable
+ public class HitExplosion : PoolableDrawable
{
- private readonly CircularContainer largeFaint;
+ private Color4 objectColour;
- public HitExplosion(DrawableCatchHitObject fruit)
+ public Color4 ObjectColour
+ {
+ get => objectColour;
+ set
+ {
+ if (objectColour == value) return;
+
+ objectColour = value;
+ onColourChanged();
+ }
+ }
+
+ private readonly CircularContainer largeFaint;
+ private readonly CircularContainer smallFaint;
+ private readonly CircularContainer directionalGlow1;
+ private readonly CircularContainer directionalGlow2;
+
+ public HitExplosion()
{
Size = new Vector2(20);
Anchor = Anchor.TopCentre;
Origin = Anchor.BottomCentre;
- Color4 objectColour = fruit.AccentColour.Value;
-
// scale roughly in-line with visual appearance of notes
-
- const float angle_variangle = 15; // should be less than 45
-
- const float roundness = 100;
-
const float initial_height = 10;
- var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
-
InternalChildren = new Drawable[]
{
largeFaint = new CircularContainer
@@ -42,33 +50,17 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
- // we want our size to be very small so the glow dominates it.
- Size = new Vector2(0.8f),
Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
- Roundness = 160,
- Radius = 200,
- },
},
- new CircularContainer
+ smallFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
- Roundness = 20,
- Radius = 50,
- },
},
- new CircularContainer
+ directionalGlow1 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -76,16 +68,8 @@ namespace osu.Game.Rulesets.Catch.UI
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
- Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
},
- new CircularContainer
+ directionalGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -93,30 +77,57 @@ namespace osu.Game.Rulesets.Catch.UI
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
- Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
}
};
}
- protected override void LoadComplete()
+ protected override void PrepareForUse()
{
- base.LoadComplete();
+ base.PrepareForUse();
const double duration = 400;
+ // we want our size to be very small so the glow dominates it.
+ largeFaint.Size = new Vector2(0.8f);
largeFaint
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2);
+ const float angle_variangle = 15; // should be less than 45
+ directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle);
+ directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle);
+
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
Expire(true);
}
+
+ private void onColourChanged()
+ {
+ const float roundness = 100;
+
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ };
+
+ smallFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ };
+
+ directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
+ Roundness = roundness,
+ Radius = 40,
+ };
+ }
}
}
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.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 892f27d27f..fa7bfd7169 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index b5ec1e1a2a..1f92929392 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -78,9 +78,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime;
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
if (PlacementActive)
{
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 27a279e044..5e09054667 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -48,9 +48,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return true;
}
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
if (!PlacementActive)
Column = result.Playfield as Column;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index 684004b558..3db89c8ae6 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
}
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
if (result.Playfield != null)
{
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/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index a4029e7893..3a00933e4d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
+ protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
index f265419aa0..98931dceed 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs
@@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
public class DrawableHoldNoteTick : DrawableManiaHitObject
{
- public override bool DisplayResult => false;
-
///
/// References the time at which the user started holding the hold note.
///
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index c28a1c13d8..d2a9b69b60 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
maniaObject.CheckHittable = hitPolicy.IsHittable;
- HitObjectContainer.Add(hitObject);
+ base.Add(hitObject);
}
public override bool Remove(DrawableHitObject h)
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
- if (!result.IsHit || !DisplayJudgements.Value)
+ if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index d99f6cb8d3..a3dcd0e57f 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -1,10 +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 osu.Framework.Allocation;
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,52 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
- [BackgroundDependencyLoader]
- private void load()
+ protected override void ApplyMissAnimations()
{
- if (JudgementText != null)
- JudgementText.Font = JudgementText.Font.With(size: 25);
- }
+ 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);
- protected override double FadeInDuration => 50;
+ 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 DefaultManiaJudgementPiece(result);
+
+ private class DefaultManiaJudgementPiece : DefaultJudgementPiece
+ {
+ public DefaultManiaJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ JudgementText.Font = JudgementText.Font.With(size: 25);
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index e7a2de266d..3d7960ffe3 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
+ // Tick judgements should not display text.
+ if (judgedObject is DrawableHoldNoteTick)
+ return;
+
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs
new file mode 100644
index 0000000000..a652fb32f4
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Tests.Beatmaps;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneObjectBeatSnap : TestSceneOsuEditor
+ {
+ private OsuPlayfield playfield;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+ AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First());
+ }
+
+ [Test]
+ public void TestBeatSnapHitCircle()
+ {
+ double firstTimingPointTime() => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time;
+
+ AddStep("seek some milliseconds forward", () => EditorClock.Seek(firstTimingPointTime() + 10));
+
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
+ AddStep("enter placement mode", () => InputManager.Key(Key.Number2));
+ AddStep("place first object", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("ensure object snapped back to correct time", () => EditorBeatmap.HitObjects.First().StartTime == firstTimingPointTime());
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
index 1ca94df26b..7bdf131e0d 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
base.SetUpSteps();
AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First());
+ AddStep("seek to first control point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
}
[TestCase(true)]
@@ -51,7 +52,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);
});
}
@@ -66,13 +67,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("start slider placement", () => InputManager.Click(MouseButton.Left));
- AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0)));
+ AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.225f, 0)));
AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
- AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0)));
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@@ -86,5 +87,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/Mods/OsuModTestScene.cs b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs
index 7697f46160..d3cb3bcf59 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs
@@ -5,7 +5,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
- public class OsuModTestScene : ModTestScene
+ public abstract class OsuModTestScene : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
index 40f1c4a52f..1ac3ad9194 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
@@ -17,15 +20,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
- Mod = new OsuModHidden(),
+ Mod = new TestOsuModHidden(),
Autoplay = true,
- PassCondition = checkSomeHit
+ PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(0)
});
[Test]
public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData
{
- Mod = new OsuModHidden(),
+ Mod = new TestOsuModHidden(),
Autoplay = true,
Beatmap = new Beatmap
{
@@ -54,13 +57,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}
}
},
- PassCondition = checkSomeHit
+ PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2)
});
[Test]
public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData
{
- Mod = new OsuModHidden(),
+ Mod = new TestOsuModHidden(),
Autoplay = true,
Beatmap = new Beatmap
{
@@ -89,12 +92,41 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}
}
},
+ PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2)
+ });
+
+ [Test]
+ public void TestWithSliderReuse() => CreateModTest(new ModTestData
+ {
+ Mod = new TestOsuModHidden(),
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ },
+ new Slider
+ {
+ StartTime = 4000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ },
+ }
+ },
PassCondition = checkSomeHit
});
- private bool checkSomeHit()
+ private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
+
+ private bool objectWithIncreasedVisibilityHasIndex(int index)
+ => Player.Mods.Value.OfType().Single().FirstObject == Player.ChildrenOfType().Single().HitObjects[index];
+
+ private class TestOsuModHidden : OsuModHidden
{
- return Player.ScoreProcessor.JudgedHits >= 4;
+ public new HitObject FirstObject => base.FirstObject;
}
}
}
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.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index c400e2f2ea..d40484f5ed 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -108,8 +108,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("change samples", () => slider.HitObject.Samples = new[]
{
- new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP },
- new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE },
+ new HitSampleInfo(HitSampleInfo.HIT_CLAP),
+ new HitSampleInfo(HitSampleInfo.HIT_WHISTLE),
});
AddAssert("head samples updated", () => assertSamples(slider.HitObject.HeadCircle));
@@ -136,15 +136,15 @@ namespace osu.Game.Rulesets.Osu.Tests
slider = (DrawableSlider)createSlider(repeats: 1);
for (int i = 0; i < 2; i++)
- slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } });
+ slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo(HitSampleInfo.HIT_FINISH) });
Add(slider);
});
AddStep("change samples", () => slider.HitObject.Samples = new[]
{
- new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP },
- new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE },
+ new HitSampleInfo(HitSampleInfo.HIT_CLAP),
+ new HitSampleInfo(HitSampleInfo.HIT_WHISTLE),
});
AddAssert("head samples not updated", () => assertSamples(slider.HitObject.HeadCircle));
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 3639c3616f..d6a03da807 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index e14d6647d2..c45a04053f 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e);
}
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
index ba1d35c35c..eb7011e8b0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Path path;
private readonly Slider slider;
- private readonly int controlPointIndex;
+ public int ControlPointIndex { get; set; }
private IBindable sliderPosition;
private IBindable pathVersion;
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{
this.slider = slider;
- this.controlPointIndex = controlPointIndex;
+ ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices();
- int nextIndex = controlPointIndex + 1;
+ int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index c06904c0c2..e9838de63d 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private OsuColour colours { get; set; }
private IBindable sliderPosition;
+ private IBindable sliderScale;
private IBindable controlPointPosition;
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
@@ -69,13 +70,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(10),
+ Size = new Vector2(20),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = new Vector2(14),
+ Size = new Vector2(28),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
@@ -102,6 +103,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPointPosition = ControlPoint.Position.GetBoundCopy();
controlPointPosition.BindValueChanged(_ => updateMarkerDisplay());
+ sliderScale = slider.ScaleBindable.GetBoundCopy();
+ sliderScale.BindValueChanged(_ => updateMarkerDisplay());
+
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
updateMarkerDisplay();
@@ -143,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
+ private Vector2 dragStartPosition;
+
protected override bool OnDragStart(DragStartEvent e)
{
if (RequestSelection == null)
@@ -150,6 +156,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left)
{
+ dragStartPosition = ControlPoint.Position.Value;
changeHandler?.BeginChange();
return true;
}
@@ -174,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
slider.Path.ControlPoints[i].Position.Value -= movementDelta;
}
else
- ControlPoint.Position.Value += e.Delta;
+ ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
}
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
@@ -194,6 +201,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
colour = colour.Lighten(1);
marker.Colour = colour;
+ marker.Scale = new Vector2(slider.Scale);
}
}
}
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 17541866ec..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;
@@ -66,6 +69,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
+ // If inserting in the path (not appending),
+ // update indices of existing connections after insert location
+ if (e.NewStartingIndex < Pieces.Count)
+ {
+ foreach (var connection in Connections)
+ {
+ if (connection.ControlPointIndex >= e.NewStartingIndex)
+ connection.ControlPointIndex += e.NewItems.Count;
+ }
+ }
+
for (int i = 0; i < e.NewItems.Count; i++)
{
var point = (PathControlPoint)e.NewItems[i];
@@ -88,6 +102,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Connections.RemoveAll(c => c.ControlPoint == point);
}
+ // If removing before the end of the path,
+ // update indices of connections after remove location
+ if (e.OldStartingIndex < Pieces.Count)
+ {
+ foreach (var connection in Connections)
+ {
+ if (connection.ControlPointIndex >= e.OldStartingIndex)
+ connection.ControlPointIndex -= e.OldItems.Count;
+ }
+ }
+
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 4b99cc23ed..b71e1914f7 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -67,9 +67,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
switch (state)
{
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/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 7c1dd46c02..45f314af7b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -23,25 +24,21 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
- protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner);
+ protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick);
- public override void ApplyToDrawableHitObjects(IEnumerable drawables)
+ public override void ApplyToBeatmap(IBeatmap beatmap)
{
- foreach (var d in drawables)
- d.HitObjectApplied += applyFadeInAdjustment;
+ base.ApplyToBeatmap(beatmap);
- base.ApplyToDrawableHitObjects(drawables);
- }
+ foreach (var obj in beatmap.HitObjects.OfType())
+ applyFadeInAdjustment(obj);
- private void applyFadeInAdjustment(DrawableHitObject hitObject)
- {
- if (!(hitObject is DrawableOsuHitObject d))
- return;
-
- d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier;
-
- foreach (var nested in d.NestedHitObjects)
- applyFadeInAdjustment(nested);
+ static void applyFadeInAdjustment(OsuHitObject osuObject)
+ {
+ osuObject.TimeFadeIn = osuObject.TimePreempt * fade_in_duration_multiplier;
+ foreach (var nested in osuObject.NestedHitObjects.OfType())
+ applyFadeInAdjustment(nested);
+ }
}
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
@@ -56,37 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods
applyState(hitObject, false);
}
- private void applyState(DrawableHitObject drawable, bool increaseVisibility)
+ private void applyState(DrawableHitObject drawableObject, bool increaseVisibility)
{
- if (!(drawable is DrawableOsuHitObject d))
+ if (!(drawableObject is DrawableOsuHitObject drawableOsuObject))
return;
- var h = d.HitObject;
+ OsuHitObject hitObject = drawableOsuObject.HitObject;
- var fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadeIn;
- var fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier;
+ (double startTime, double duration) fadeOut = getFadeOutParameters(drawableOsuObject);
- // new duration from completed fade in to end (before fading out)
- var longFadeDuration = h.GetEndTime() - fadeOutStartTime;
-
- switch (drawable)
+ switch (drawableObject)
{
- case DrawableSliderTail sliderTail:
- // use stored values from head circle to achieve same fade sequence.
- var tailFadeOutParameters = getFadeOutParametersFromSliderHead(h);
-
- using (drawable.BeginAbsoluteSequence(tailFadeOutParameters.startTime, true))
- sliderTail.FadeOut(tailFadeOutParameters.duration);
+ case DrawableSliderTail _:
+ using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true))
+ drawableObject.FadeOut(fadeOut.duration);
break;
case DrawableSliderRepeat sliderRepeat:
- // use stored values from head circle to achieve same fade sequence.
- var repeatFadeOutParameters = getFadeOutParametersFromSliderHead(h);
-
- using (drawable.BeginAbsoluteSequence(repeatFadeOutParameters.startTime, true))
+ using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true))
// only apply to circle piece – reverse arrow is not affected by hidden.
- sliderRepeat.CirclePiece.FadeOut(repeatFadeOutParameters.duration);
+ sliderRepeat.CirclePiece.FadeOut(fadeOut.duration);
break;
@@ -101,29 +88,23 @@ namespace osu.Game.Rulesets.Osu.Mods
else
{
// we don't want to see the approach circle
- using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt, true))
circle.ApproachCircle.Hide();
}
- // fade out immediately after fade in.
- using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
- fadeTarget.FadeOut(fadeOutDuration);
+ using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true))
+ fadeTarget.FadeOut(fadeOut.duration);
break;
case DrawableSlider slider:
- associateNestedSliderCirclesWithHead(slider.HitObject);
-
- using (slider.BeginAbsoluteSequence(fadeOutStartTime, true))
- slider.Body.FadeOut(longFadeDuration, Easing.Out);
+ using (slider.BeginAbsoluteSequence(fadeOut.startTime, true))
+ slider.Body.FadeOut(fadeOut.duration, Easing.Out);
break;
case DrawableSliderTick sliderTick:
- // slider ticks fade out over up to one second
- var tickFadeOutDuration = Math.Min(sliderTick.HitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
-
- using (sliderTick.BeginAbsoluteSequence(sliderTick.HitObject.StartTime - tickFadeOutDuration, true))
- sliderTick.FadeOut(tickFadeOutDuration);
+ using (sliderTick.BeginAbsoluteSequence(fadeOut.startTime, true))
+ sliderTick.FadeOut(fadeOut.duration);
break;
@@ -131,30 +112,55 @@ namespace osu.Game.Rulesets.Osu.Mods
// hide elements we don't care about.
// todo: hide background
- using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true))
- spinner.FadeOut(fadeOutDuration);
+ using (spinner.BeginAbsoluteSequence(fadeOut.startTime, true))
+ spinner.FadeOut(fadeOut.duration);
break;
}
}
- private readonly Dictionary correspondingSliderHeadForObject = new Dictionary();
-
- private void associateNestedSliderCirclesWithHead(Slider slider)
+ private (double startTime, double duration) getFadeOutParameters(DrawableOsuHitObject drawableObject)
{
- var sliderHead = slider.NestedHitObjects.Single(obj => obj is SliderHeadCircle);
-
- foreach (var nested in slider.NestedHitObjects)
+ switch (drawableObject)
{
- if ((nested is SliderRepeat || nested is SliderEndCircle) && !correspondingSliderHeadForObject.ContainsKey(nested))
- correspondingSliderHeadForObject[nested] = (SliderHeadCircle)sliderHead;
- }
- }
+ case DrawableSliderTail tail:
+ // Use the same fade sequence as the slider head.
+ Debug.Assert(tail.Slider != null);
+ return getParameters(tail.Slider.HeadCircle);
- private (double startTime, double duration) getFadeOutParametersFromSliderHead(OsuHitObject h)
- {
- var sliderHead = correspondingSliderHeadForObject[h];
- return (sliderHead.StartTime - sliderHead.TimePreempt + sliderHead.TimeFadeIn, sliderHead.TimePreempt * fade_out_duration_multiplier);
+ case DrawableSliderRepeat repeat:
+ // Use the same fade sequence as the slider head.
+ Debug.Assert(repeat.Slider != null);
+ return getParameters(repeat.Slider.HeadCircle);
+
+ default:
+ return getParameters(drawableObject.HitObject);
+ }
+
+ static (double startTime, double duration) getParameters(OsuHitObject hitObject)
+ {
+ var fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn;
+ var fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier;
+
+ // new duration from completed fade in to end (before fading out)
+ var longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime;
+
+ switch (hitObject)
+ {
+ case Slider _:
+ return (fadeOutStartTime, longFadeDuration);
+
+ case SliderTick _:
+ var tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
+ return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration);
+
+ case Spinner _:
+ return (fadeOutStartTime + longFadeDuration, fadeOutDuration);
+
+ default:
+ return (fadeOutStartTime, fadeOutDuration);
+ }
+ }
}
}
}
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 2e63160d36..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;
@@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.Delay(800).FadeOut();
break;
}
+
+ Expire();
}
public Drawable ProxiedLayer => ApproachCircle;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index bcaf73d34f..628d95dff4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
@@ -52,9 +51,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);
@@ -62,9 +61,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindTo(HitObject.ScaleBindable);
}
- protected override void OnFree(HitObject hitObject)
+ protected override void OnFree()
{
- base.OnFree(hitObject);
+ base.OnFree();
IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable);
PositionBindable.UnbindFrom(HitObject.PositionBindable);
@@ -85,14 +84,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- // Manually set to reduce the number of future alive objects to a bare minimum.
- LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
- }
-
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 98898ce1b4..13f5960bd4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -4,9 +4,9 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
-using osuTK;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -17,15 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[Resolved]
private OsuConfigManager config { get; set; }
- public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
- : base(result, judgedObject)
- {
- }
-
- public DrawableOsuJudgement()
- {
- }
-
[BackgroundDependencyLoader]
private void load()
{
@@ -39,48 +30,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
- public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
- {
- base.Apply(result, judgedObject);
-
- if (judgedObject?.HitObject is OsuHitObject osuObject)
- {
- Position = osuObject.StackedPosition;
- Scale = new Vector2(osuObject.Scale);
- }
- }
-
protected override void PrepareForUse()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
- }
- private double fadeOutDelay;
- protected override double FadeOutDelay => fadeOutDelay;
+ if (JudgedObject?.HitObject is OsuHitObject osuObject)
+ {
+ Position = osuObject.StackedPosition;
+ Scale = new Vector2(osuObject.Scale);
+ }
+ }
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;
+
+ // extend the lifetime to cover lighting fade
+ LifetimeEnd = Lighting.LatestTransformEndTime;
}
- fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
-
- JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations();
}
+
+ protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result);
+
+ private class OsuJudgementPiece : DefaultJudgementPiece
+ {
+ public OsuJudgementPiece(HitResult result)
+ : base(result)
+ {
+ }
+
+ public override void PlayAnimation()
+ {
+ base.PlayAnimation();
+
+ if (Result != HitResult.Miss)
+ JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+ }
+ }
}
}
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 f7b1894058..af5b609ec8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
+using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI;
@@ -19,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;
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container tailContainer;
private Container tickContainer;
private Container repeatContainer;
- private Container samplesContainer;
+ private PausableSkinnableSound slidingSample;
public DrawableSlider()
: this(null)
@@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container { RelativeSizeAxes = Axes.Both },
- samplesContainer = new Container { RelativeSizeAxes = Axes.Both }
+ slidingSample = new PausableSkinnableSound { Looping = true }
};
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@@ -86,42 +87,35 @@ 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);
- }
- private PausableSkinnableSound slidingSample;
+ slidingSample.Samples = null;
+ }
protected override void LoadSamples()
{
base.LoadSamples();
- samplesContainer.Clear();
- slidingSample = null;
-
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
- var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
- clone.Name = "sliderslide";
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide");
- samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone)
- {
- Looping = true
- });
+ slidingSample.Samples = new ISampleInfo[] { clone };
}
}
@@ -193,13 +187,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- Body.FadeInFromZero(HitObject.TimeFadeIn);
- }
-
public readonly Bindable Tracking = new Bindable();
protected override void Update()
@@ -262,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || Time.Current < HitObject.EndTime)
return;
- ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
public override void PlaySamples()
@@ -273,6 +260,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.PlaySamples();
}
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ Body.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
@@ -294,14 +288,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
+ if (sliderBody?.SnakingOut.Value == true)
+ Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;
}
- this.FadeOut(fade_out_time, Easing.OutQuint);
+ this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
- public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;
-
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
private class DefaultSliderBody : PlaySliderBody
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index fd0f35d20d..acc95ab036 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -2,24 +2,25 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
+using JetBrains.Annotations;
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;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderHead : DrawableHitCircle
{
+ [CanBeNull]
+ public Slider Slider => DrawableSlider?.HitObject;
+
+ protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+
private readonly IBindable pathVersion = new Bindable();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
- private DrawableSlider drawableSlider;
-
- private Slider slider => drawableSlider?.HitObject;
-
public DrawableSliderHead()
{
}
@@ -36,34 +37,34 @@ 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);
+ pathVersion.UnbindFrom(DrawableSlider.PathVersion);
}
- protected override void OnParentReceived(DrawableHitObject parent)
+ protected override void OnApply()
{
- base.OnParentReceived(parent);
+ base.OnApply();
- drawableSlider = (DrawableSlider)parent;
+ pathVersion.BindTo(DrawableSlider.PathVersion);
- pathVersion.BindTo(drawableSlider.PathVersion);
-
- OnShake = drawableSlider.Shake;
- CheckHittable = (d, t) => drawableSlider.CheckHittable?.Invoke(d, t) ?? true;
+ OnShake = DrawableSlider.Shake;
+ CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
}
protected override void Update()
{
base.Update();
- double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
+ Debug.Assert(Slider != null);
+
+ double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)
- Position = slider.CurvePositionAt(completionProgress);
+ Position = Slider.CurvePositionAt(completionProgress);
}
public Action OnShake;
@@ -72,8 +73,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updatePosition()
{
- if (slider != null)
- Position = HitObject.Position - slider.Position;
+ if (Slider != null)
+ Position = HitObject.Position - Slider.Position;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 0735d48ae1..a684df98cb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
+ [CanBeNull]
+ public Slider Slider => DrawableSlider?.HitObject;
+
+ protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+
private double animDuration;
public Drawable CirclePiece { get; private set; }
@@ -26,8 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => false;
- private DrawableSlider drawableSlider;
-
public DrawableSliderRepeat()
: base(null)
{
@@ -60,19 +64,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
- protected override void OnParentReceived(DrawableHitObject parent)
+ protected override void OnApply()
{
- base.OnParentReceived(parent);
+ base.OnApply();
- drawableSlider = (DrawableSlider)parent;
-
- Position = HitObject.Position - drawableSlider.Position;
+ Position = HitObject.Position - DrawableSlider.Position;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (HitObject.StartTime <= Time.Current)
- ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
+ ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateInitialTransforms()
@@ -114,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (IsHit) return;
bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0;
- List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
+ List curve = ((PlaySliderBody)DrawableSlider.Body.Drawable).CurrentCurve;
Position = isRepeatAtEnd ? end : start;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index eff72168ee..6a8e02e886 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -15,6 +16,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
+ [CanBeNull]
+ public Slider Slider => DrawableSlider?.HitObject;
+
+ protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+
///
/// The judgement text is provided by the .
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index faccf5d4d1..c7bfdb02fb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => false;
+ protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+
private SkinnableDrawable scaleContainer;
public DrawableSliderTick()
@@ -62,11 +64,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
- protected override void OnParentReceived(DrawableHitObject parent)
+ protected override void OnApply()
{
- base.OnParentReceived(parent);
+ base.OnApply();
- Position = HitObject.Position - ((DrawableSlider)parent).HitObject.Position;
+ Position = HitObject.Position - DrawableSlider.HitObject.Position;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 87c7146a64..aea37acf6f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -9,6 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container ticks;
private SpinnerBonusDisplay bonusDisplay;
- private Container samplesContainer;
+ private PausableSkinnableSound spinningSample;
private Bindable isSpinning;
private bool spinnerFrequencyModulate;
@@ -81,7 +82,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre,
Y = -120,
},
- samplesContainer = new Container { RelativeSizeAxes = Axes.Both }
+ spinningSample = new PausableSkinnableSound
+ {
+ Volume = { Value = 0 },
+ Looping = true,
+ Frequency = { Value = spinning_sample_initial_frequency }
+ }
};
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
@@ -95,30 +101,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
- private PausableSkinnableSound spinningSample;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
+ protected override void OnFree()
+ {
+ base.OnFree();
+
+ spinningSample.Samples = null;
+ }
+
protected override void LoadSamples()
{
base.LoadSamples();
- samplesContainer.Clear();
- spinningSample = null;
-
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
- var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
- clone.Name = "spinnerspin";
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin");
- samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone)
- {
- Volume = { Value = 0 },
- Looping = true,
- Frequency = { Value = spinning_sample_initial_frequency }
- });
+ spinningSample.Samples = new ISampleInfo[] { clone };
+ spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
}
@@ -157,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- this.FadeOut(160);
+ this.FadeOut(160).Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index fc9a7c00e6..726fbd3ea6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -7,6 +7,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public override bool DisplayResult => false;
+ protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject;
+
public DrawableSpinnerTick()
: base(null)
{
@@ -17,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
}
+ protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
+
///
/// Apply a judgement result.
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
index c455c66e8d..d0e1055dce 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs
@@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class CirclePiece : CompositeDrawable
{
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; }
+
+ private TrianglesPiece triangles;
+
public CirclePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -26,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
[BackgroundDependencyLoader]
- private void load(TextureStore textures, DrawableHitObject drawableHitObject)
+ private void load(TextureStore textures)
{
InternalChildren = new Drawable[]
{
@@ -36,13 +41,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"),
},
- new TrianglesPiece(drawableHitObject.GetHashCode())
+ triangles = new TrianglesPiece
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Alpha = 0.5f,
}
};
+
+ drawableObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(drawableObject);
+ }
+
+ private void onHitObjectApplied(DrawableHitObject obj)
+ {
+ if (obj.HitObject == null)
+ return;
+
+ triangles.Reset((int)obj.HitObject.StartTime);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject != null)
+ drawableObject.HitObjectApplied -= onHitObjectApplied;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs
index 6381ddca69..09299a3622 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs
@@ -1,14 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class ExplodePiece : Container
{
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; }
+
+ private TrianglesPiece triangles;
+
public ExplodePiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -18,13 +25,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Blending = BlendingParameters.Additive;
Alpha = 0;
+ }
- Child = new TrianglesPiece
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = triangles = new TrianglesPiece
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
};
+
+ drawableObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(drawableObject);
+ }
+
+ private void onHitObjectApplied(DrawableHitObject obj)
+ {
+ if (obj.HitObject == null)
+ return;
+
+ triangles.Reset((int)obj.HitObject.StartTime);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject != null)
+ drawableObject.HitObjectApplied -= onHitObjectApplied;
}
}
}
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/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
index 6cdb0d3df3..53dc7ecea3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs
@@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class TrianglesPiece : Triangles
{
- protected override bool ExpireOffScreenTriangles => false;
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 755ce0866a..1670df24a8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -221,14 +221,7 @@ namespace osu.Game.Rulesets.Osu.Objects
var sampleList = new List();
if (firstSample != null)
- {
- sampleList.Add(new HitSampleInfo
- {
- Bank = firstSample.Bank,
- Volume = firstSample.Volume,
- Name = @"slidertick",
- });
- }
+ sampleList.Add(firstSample.With("slidertick"));
foreach (var tick in NestedHitObjects.OfType())
tick.Samples = sampleList;
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
index 235dc8710a..2c443cb96b 100644
--- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public SpinnerBonusTick()
{
- Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
+ Samples.Add(new HitSampleInfo("spinnerbonus"));
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
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..975b444699 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;
}
}
@@ -201,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuHitObjectLifetimeEntry(HitObject hitObject)
: base(hitObject)
{
+ // Prevent past objects in idles states from remaining alive as their end times are skipped in non-frame-stable contexts.
+ LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss);
}
protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt;
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index b59f3a4344..a89645d881 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index c5191ab241..17e7fb81f6 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -43,10 +43,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
return false;
}
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index 468d980b23..e53b331f46 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
EndPlacement(true);
}
- public override void UpdatePosition(SnapResult result)
+ public override void UpdateTimeAndPosition(SnapResult result)
{
- base.UpdatePosition(result);
+ base.UpdateTimeAndPosition(result);
if (PlacementActive)
{
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/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index bf44a80037..be659f6ca5 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Filled = HitObject.FirstTick
});
+ protected override double MaximumJudgementOffset => HitObject.HitWindow;
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!userTriggered)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index 4a3759794b..29a96a7a40 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (isRimType != rimSamples.Any())
{
if (isRimType)
- HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP });
+ HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
else
{
foreach (var sample in rimSamples)
@@ -125,9 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (s.Name != HitSampleInfo.HIT_FINISH)
continue;
- var sClone = s.Clone();
- sClone.Name = HitSampleInfo.HIT_WHISTLE;
- corrected[i] = sClone;
+ corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE);
}
return corrected;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index d8d75a7614..ff5b221273 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
- HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH });
+ HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index cbfc5a8628..b5e35f88b5 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -1,12 +1,9 @@
// 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.Rulesets.Objects.Drawables;
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Judgements;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -25,21 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
}
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- switch (Result.Type)
- {
- case HitResult.Ok:
- JudgementBody.Colour = colours.GreenLight;
- break;
-
- case HitResult.Great:
- JudgementBody.Colour = colours.BlueLight;
- break;
- }
- }
-
protected override void ApplyHitAnimations()
{
this.MoveToY(-100, 500);
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/Audio/SampleInfoEqualityTest.cs b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs
new file mode 100644
index 0000000000..149096608f
--- /dev/null
+++ b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs
@@ -0,0 +1,78 @@
+// 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.Game.Audio;
+
+namespace osu.Game.Tests.Audio
+{
+ [TestFixture]
+ public class SampleInfoEqualityTest
+ {
+ [Test]
+ public void TestSameSingleSamplesAreEqual()
+ {
+ var first = new SampleInfo("sample");
+ var second = new SampleInfo("sample");
+
+ assertEquality(first, second);
+ }
+
+ [Test]
+ public void TestDifferentSingleSamplesAreNotEqual()
+ {
+ var first = new SampleInfo("first");
+ var second = new SampleInfo("second");
+
+ assertNonEquality(first, second);
+ }
+
+ [Test]
+ public void TestDifferentCountSampleSetsAreNotEqual()
+ {
+ var first = new SampleInfo("sample", "extra");
+ var second = new SampleInfo("sample");
+
+ assertNonEquality(first, second);
+ }
+
+ [Test]
+ public void TestDifferentSampleSetsOfSameCountAreNotEqual()
+ {
+ var first = new SampleInfo("first", "common");
+ var second = new SampleInfo("common", "second");
+
+ assertNonEquality(first, second);
+ }
+
+ [Test]
+ public void TestSameOrderSameSampleSetsAreEqual()
+ {
+ var first = new SampleInfo("first", "second");
+ var second = new SampleInfo("first", "second");
+
+ assertEquality(first, second);
+ }
+
+ [Test]
+ public void TestDifferentOrderSameSampleSetsAreEqual()
+ {
+ var first = new SampleInfo("first", "second");
+ var second = new SampleInfo("second", "first");
+
+ assertEquality(first, second);
+ }
+
+ private void assertEquality(SampleInfo first, SampleInfo second)
+ {
+ Assert.That(first.Equals(second), Is.True);
+ Assert.That(first.GetHashCode(), Is.EqualTo(second.GetHashCode()));
+ }
+
+ private void assertNonEquality(SampleInfo first, SampleInfo second)
+ {
+ Assert.That(first.Equals(second), Is.False);
+ Assert.That(first.GetHashCode(), Is.Not.EqualTo(second.GetHashCode()));
+ }
+ }
+}
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/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
index bb56131b04..44a908b756 100644
--- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
+++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs
@@ -139,7 +139,7 @@ namespace osu.Game.Tests.Editing
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
- new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
+ new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } },
(OsuHitObject)current.HitObjects[2],
}
};
@@ -268,12 +268,12 @@ namespace osu.Game.Tests.Editing
HitObjects =
{
(OsuHitObject)current.HitObjects[0],
- new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } },
+ new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } },
(OsuHitObject)current.HitObjects[2],
(OsuHitObject)current.HitObjects[3],
- new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } },
+ new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } },
(OsuHitObject)current.HitObjects[5],
- new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } },
+ new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_CLAP) } },
(OsuHitObject)current.HitObjects[7],
}
};
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/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
index 1a1babb4a8..9931ee4a45 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -21,13 +21,13 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
+using JetBrains.Annotations;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -46,6 +46,50 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUp]
public void Setup() => Schedule(() => testClock.CurrentTime = 0);
+ [TestCase("pooled")]
+ [TestCase("non-pooled")]
+ public void TestHitObjectLifetime(string pooled)
+ {
+ var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject());
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ createTest(beatmap);
+
+ assertPosition(0, 0f);
+ assertDead(3);
+
+ setTime(3 * time_range);
+ assertPosition(3, 0f);
+ assertDead(0);
+
+ setTime(0 * time_range);
+ assertPosition(0, 0f);
+ assertDead(3);
+ }
+
+ [TestCase("pooled")]
+ [TestCase("non-pooled")]
+ public void TestNestedHitObject(string pooled)
+ {
+ var beatmap = createBeatmap(i =>
+ {
+ var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject();
+ h.Duration = 300;
+ h.ChildTimeOffset = i % 3 * 100;
+ return h;
+ });
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range });
+ createTest(beatmap);
+
+ assertPosition(0, 0f);
+ assertHeight(0);
+ assertChildPosition(0);
+
+ setTime(5 * time_range);
+ assertPosition(5, 0f);
+ assertHeight(5);
+ assertChildPosition(5);
+ }
+
[Test]
public void TestRelativeBeatLengthScaleSingleTimingPoint()
{
@@ -147,8 +191,37 @@ namespace osu.Game.Tests.Visual.Gameplay
assertPosition(1, 1);
}
+ ///
+ /// Get a corresponding to the 'th .
+ /// When the hit object is not alive, `null` is returned.
+ ///
+ [CanBeNull]
+ private DrawableTestHitObject getDrawableHitObject(int index)
+ {
+ var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index);
+ return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject);
+ }
+
+ private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight;
+
+ private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null);
+
+ private void assertHeight(int index) => AddAssert($"hitobject {index} height", () =>
+ {
+ var d = getDrawableHitObject(index);
+ return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f);
+ });
+
+ private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () =>
+ {
+ var d = getDrawableHitObject(index);
+ return d is DrawableTestParentHitObject && Precision.AlmostEquals(
+ d.NestedHitObjects.First().DrawPosition.Y,
+ yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f);
+ });
+
private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
- () => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY));
+ () => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY));
private void setTime(double time)
{
@@ -160,12 +233,16 @@ namespace osu.Game.Tests.Visual.Gameplay
/// The hitobjects are spaced milliseconds apart.
///
/// The .
- private IBeatmap createBeatmap()
+ private IBeatmap createBeatmap(Func createAction = null)
{
- var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
+ var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
for (int i = 0; i < 10; i++)
- beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
+ {
+ var h = createAction?.Invoke(i) ?? new TestHitObject();
+ h.StartTime = i * time_range;
+ beatmap.HitObjects.Add(h);
+ }
return beatmap;
}
@@ -225,7 +302,21 @@ namespace osu.Game.Tests.Visual.Gameplay
TimeRange.Value = time_range;
}
- public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h);
+ public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h)
+ {
+ switch (h)
+ {
+ case TestPooledHitObject _:
+ case TestPooledParentHitObject _:
+ return null;
+
+ case TestParentHitObject p:
+ return new DrawableTestParentHitObject(p);
+
+ default:
+ return new DrawableTestHitObject(h);
+ }
+ }
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
@@ -265,6 +356,9 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
});
+
+ RegisterPool(1);
+ RegisterPool(1);
}
}
@@ -277,30 +371,46 @@ namespace osu.Game.Tests.Visual.Gameplay
public override bool CanConvert() => true;
- protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
- {
- yield return new TestHitObject
- {
- StartTime = original.StartTime,
- Duration = (original as IHasDuration)?.Duration ?? 100
- };
- }
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) =>
+ throw new NotImplementedException();
}
#endregion
#region HitObject
- private class TestHitObject : ConvertHitObject, IHasDuration
+ private class TestHitObject : HitObject, IHasDuration
{
public double EndTime => StartTime + Duration;
- public double Duration { get; set; }
+ public double Duration { get; set; } = 100;
+ }
+
+ private class TestPooledHitObject : TestHitObject
+ {
+ }
+
+ private class TestParentHitObject : TestHitObject
+ {
+ public double ChildTimeOffset;
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset });
+ }
+ }
+
+ private class TestPooledParentHitObject : TestParentHitObject
+ {
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset });
+ }
}
private class DrawableTestHitObject : DrawableHitObject
{
- public DrawableTestHitObject(TestHitObject hitObject)
+ public DrawableTestHitObject([CanBeNull] TestHitObject hitObject)
: base(hitObject)
{
Anchor = Anchor.TopCentre;
@@ -324,6 +434,52 @@ namespace osu.Game.Tests.Visual.Gameplay
}
});
}
+
+ protected override void Update() => LifetimeEnd = HitObject.EndTime;
+ }
+
+ private class DrawableTestPooledHitObject : DrawableTestHitObject
+ {
+ public DrawableTestPooledHitObject()
+ : base(null)
+ {
+ InternalChildren[0].Colour = Color4.LightSkyBlue;
+ InternalChildren[1].Colour = Color4.Blue;
+ }
+ }
+
+ private class DrawableTestParentHitObject : DrawableTestHitObject
+ {
+ private readonly Container container;
+
+ public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject)
+ : base(hitObject)
+ {
+ InternalChildren[0].Colour = Color4.LightYellow;
+ InternalChildren[1].Colour = Color4.Yellow;
+
+ AddInternal(container = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ });
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) =>
+ new DrawableTestHitObject((TestHitObject)hitObject);
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject);
+
+ protected override void ClearNestedHitObjects() => container.Clear(false);
+ }
+
+ private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject
+ {
+ public DrawableTestPooledParentHitObject()
+ : base(null)
+ {
+ InternalChildren[0].Colour = Color4.LightSeaGreen;
+ InternalChildren[1].Colour = Color4.Green;
+ }
}
#endregion
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/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index aa531ba106..35c6d62cb7 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -643,6 +643,55 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap);
}
+ [Test]
+ public void TestChangingRulesetOnMultiRulesetBeatmap()
+ {
+ int changeCount = 0;
+
+ AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
+ AddStep("bind beatmap changed", () =>
+ {
+ Beatmap.ValueChanged += onChange;
+ changeCount = 0;
+ });
+
+ changeRuleset(0);
+
+ createSongSelect();
+
+ AddStep("import multi-ruleset map", () =>
+ {
+ var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray();
+ manager.Import(createTestBeatmapSet(usableRulesets)).Wait();
+ });
+
+ int previousSetID = 0;
+
+ AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
+
+ AddStep("record set ID", () => previousSetID = Beatmap.Value.BeatmapSetInfo.ID);
+ AddAssert("selection changed once", () => changeCount == 1);
+
+ AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
+
+ changeRuleset(3);
+
+ AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3);
+
+ AddUntilStep("selection changed", () => changeCount > 1);
+
+ AddAssert("Selected beatmap still same set", () => Beatmap.Value.BeatmapSetInfo.ID == previousSetID);
+ AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3);
+
+ AddAssert("selection changed only fired twice", () => changeCount == 2);
+
+ AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange);
+ AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
+
+ // ReSharper disable once AccessToModifiedClosure
+ void onChange(ValueChangedEvent valueChangedEvent) => changeCount++;
+ }
+
[Test]
public void TestDifficultyIconSelectingForDifferentRuleset()
{
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.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index c692bcd5e4..83d7b4135a 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 5d55196dcf..bc6b994988 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index 999ce61ac8..71417d1cc6 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -243,7 +243,7 @@ namespace osu.Game.Tournament.IPC
string stableInstallPath;
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
- stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", "");
+ stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
if (ipcFileExistsInDirectory(stableInstallPath))
return stableInstallPath;
diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs
index 8ebcbf4e15..bdfb1728f3 100644
--- a/osu.Game.Tournament/Models/TournamentMatch.cs
+++ b/osu.Game.Tournament/Models/TournamentMatch.cs
@@ -4,10 +4,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Drawing;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Tournament.Screens.Ladder.Components;
-using SixLabors.Primitives;
namespace osu.Game.Tournament.Models
{
diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
index efec4cffdd..ca46c3b050 100644
--- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Drawing;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -16,7 +17,6 @@ using osu.Game.Tournament.Screens.Ladder;
using osu.Game.Tournament.Screens.Ladder.Components;
using osuTK;
using osuTK.Graphics;
-using SixLabors.Primitives;
namespace osu.Game.Tournament.Screens.Editors
{
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs
index f2065e7e88..1c805bb42e 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Drawing;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -13,7 +14,6 @@ using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
-using SixLabors.Primitives;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index 9cce40c9d3..b049542bb0 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -9,6 +9,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs
index 8efaeb3795..3d90dd0189 100644
--- a/osu.Game/Audio/HitSampleInfo.cs
+++ b/osu.Game/Audio/HitSampleInfo.cs
@@ -1,8 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Utils;
namespace osu.Game.Audio
{
@@ -10,7 +14,7 @@ namespace osu.Game.Audio
/// Describes a gameplay hit sample.
///
[Serializable]
- public class HitSampleInfo : ISampleInfo
+ public class HitSampleInfo : ISampleInfo, IEquatable
{
public const string HIT_WHISTLE = @"hitwhistle";
public const string HIT_FINISH = @"hitfinish";
@@ -22,29 +26,38 @@ namespace osu.Game.Audio
///
public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
- ///
- /// The bank to load the sample from.
- ///
- public string Bank;
-
///
/// The name of the sample to load.
///
- public string Name;
+ public readonly string Name;
+
+ ///
+ /// The bank to load the sample from.
+ ///
+ public readonly string? Bank;
///
/// An optional suffix to provide priority lookup. Falls back to non-suffixed .
///
- public string Suffix;
+ public readonly string? Suffix;
///
/// The sample volume.
///
- public int Volume { get; set; }
+ public int Volume { get; }
+
+ public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 0)
+ {
+ Name = name;
+ Bank = bank;
+ Suffix = suffix;
+ Volume = volume;
+ }
///
/// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first).
///
+ [JsonIgnore]
public virtual IEnumerable LookupNames
{
get
@@ -56,6 +69,23 @@ namespace osu.Game.Audio
}
}
- public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone();
+ ///
+ /// Creates a new with overridden values.
+ ///
+ /// An optional new sample name.
+ /// An optional new sample bank.
+ /// An optional new lookup suffix.
+ /// An optional new volume.
+ /// The new .
+ public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional