diff --git a/osu.Android.props b/osu.Android.props index eaedcb7bc3..2a08cb7867 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 466cbdaf8d..33fdcdaf1e 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Tests public float Position { - get => HitObject?.X ?? position; + get => HitObject?.EffectiveX ?? position; set => position = value; } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 31c285ef22..1cbfa6338e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Tests private void attemptCatch(Fruit fruit) { - fruit.X += catcher.X; + fruit.X = fruit.OriginalX + catcher.X; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 00ce9ea8c2..fac5d03833 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps case JuiceStream juiceStream: // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. - lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X; + lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X; // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. lastStartTime = juiceStream.StartTime; @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps catchObject.XOffset = 0; if (catchObject is TinyDroplet) - catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X); + catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.OriginalX, CatchPlayfield.WIDTH - catchObject.OriginalX); else if (catchObject is Droplet) rng.Next(); // osu!stable retrieved a random droplet rotation } @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) { - float offsetPosition = hitObject.X; + float offsetPosition = hitObject.OriginalX; double startTime = hitObject.StartTime; if (lastPosition == null) @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps if (positionDiff == 0) { applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng); - hitObject.XOffset = offsetPosition - hitObject.X; + hitObject.XOffset = offsetPosition - hitObject.OriginalX; return; } @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps if (Math.Abs(positionDiff) < timeDiff / 3) applyOffset(ref offsetPosition, positionDiff); - hitObject.XOffset = offsetPosition - hitObject.X; + hitObject.XOffset = offsetPosition - hitObject.OriginalX; lastPosition = offsetPosition; lastStartTime = startTime; @@ -230,9 +230,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps currentObject.HyperDashTarget = null; currentObject.DistanceToHyperDash = 0; - int thisDirection = nextObject.X > currentObject.X ? 1 : -1; + int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable - double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); + double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext); if (distanceToHyper < 0) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index dcd410e08f..d936ef97ac 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -32,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. var scalingFactor = normalized_hitobject_radius / halfCatcherWidth; - NormalizedPosition = BaseObject.X * scalingFactor; - LastNormalizedPosition = LastObject.X * scalingFactor; + NormalizedPosition = BaseObject.EffectiveX * scalingFactor; + LastNormalizedPosition = LastObject.EffectiveX * scalingFactor; // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure StrainTime = Math.Max(40, DeltaTime); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index b86b3a7496..ae45182960 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -4,7 +4,6 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -16,38 +15,46 @@ namespace osu.Game.Rulesets.Catch.Objects { public const float OBJECT_RADIUS = 64; - // This value is after XOffset applied. - public readonly Bindable XBindable = new Bindable(); - - // This value is before XOffset applied. - private float originalX; + public readonly Bindable OriginalXBindable = new Bindable(); /// - /// The horizontal position of the fruit between 0 and . + /// The horizontal position of the hit object between 0 and . /// public float X { - // TODO: I don't like this asymmetry. - get => XBindable.Value; - // originalX is set by `XBindable.BindValueChanged` - set => XBindable.Value = value + xOffset; + set => OriginalXBindable.Value = value; } - private float xOffset; + float IHasXPosition.X => OriginalXBindable.Value; + + public readonly Bindable XOffsetBindable = new Bindable(); /// - /// A random offset applied to , set by the . + /// A random offset applied to the horizontal position, set by the beatmap processing. /// - internal float XOffset + public float XOffset { - get => xOffset; - set - { - xOffset = value; - XBindable.Value = originalX + xOffset; - } + set => XOffsetBindable.Value = value; } + /// + /// The horizontal position of the hit object between 0 and . + /// + /// + /// This value is the original value specified in the beatmap, not affected by the beatmap processing. + /// Use for a gameplay. + /// + public float OriginalX => OriginalXBindable.Value; + + /// + /// The effective horizontal position of the hit object between 0 and . + /// + /// + /// This value is the original value plus the offset applied by the beatmap processing. + /// Use if a value not affected by the offset is desired. + /// + public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value; + public double TimePreempt = 1000; public readonly Bindable IndexInBeatmapBindable = new Bindable(); @@ -113,10 +120,5 @@ namespace osu.Game.Rulesets.Catch.Objects } protected override HitWindows CreateHitWindows() => HitWindows.Empty; - - protected CatchHitObject() - { - XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index bfd124c691..0c065948ef 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -15,11 +15,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class DrawableCatchHitObject : DrawableHitObject { - public readonly Bindable XBindable = new Bindable(); + public readonly Bindable OriginalXBindable = new Bindable(); + public readonly Bindable XOffsetBindable = new Bindable(); protected override double InitialLifetimeOffset => HitObject.TimePreempt; - protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; + protected override float SamplePlaybackPosition => HitObject.EffectiveX / CatchPlayfield.WIDTH; public int RandomSeed => HitObject?.RandomSeed ?? 0; @@ -38,14 +39,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.OnApply(); - XBindable.BindTo(HitObject.XBindable); + OriginalXBindable.BindTo(HitObject.OriginalXBindable); + XOffsetBindable.BindTo(HitObject.XOffsetBindable); } protected override void OnFree() { base.OnFree(); - XBindable.UnbindFrom(HitObject.XBindable); + OriginalXBindable.UnbindFrom(HitObject.OriginalXBindable); + XOffsetBindable.UnbindFrom(HitObject.XOffsetBindable); } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 7df06bd92d..27cd7ed2bc 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -55,10 +55,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - XBindable.BindValueChanged(x => - { - X = x.NewValue; - }, true); + OriginalXBindable.BindValueChanged(updateXPosition); + XOffsetBindable.BindValueChanged(updateXPosition, true); ScaleBindable.BindValueChanged(scale => { @@ -69,6 +67,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); } + private void updateXPosition(ValueChangedEvent _) + { + X = OriginalXBindable.Value + XOffsetBindable.Value; + } + protected override void OnApply() { base.OnApply(); diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index d5819935ad..35fd58826e 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new TinyDroplet { StartTime = t + lastEvent.Value.Time, - X = X + Path.PositionAt( + X = OriginalX + Path.PositionAt( lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X, }); } @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = dropletSamples, StartTime = e.Time, - X = X + Path.PositionAt(e.PathProgress).X, + X = OriginalX + Path.PositionAt(e.PathProgress).X, }); break; @@ -104,14 +104,14 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = this.GetNodeSamples(nodeIndex++), StartTime = e.Time, - X = X + Path.PositionAt(e.PathProgress).X, + X = OriginalX + Path.PositionAt(e.PathProgress).X, }); break; } } } - public float EndX => X + this.CurvePositionAt(1).X; + public float EndX => OriginalX + this.CurvePositionAt(1).X; public double Duration { diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index dfc81ee8d9..32e8ab5da7 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Replays void moveToNext(PalpableCatchHitObject h) { - float positionChange = Math.Abs(lastPosition - h.X); + float positionChange = Math.Abs(lastPosition - h.EffectiveX); double timeAvailable = h.StartTime - lastTime; // So we can either make it there without a dash or not. @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Replays // todo: get correct catcher size, based on difficulty CS. const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f; - if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X) + if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX) { // we are already in the correct range. lastTime = h.StartTime; @@ -66,12 +66,12 @@ namespace osu.Game.Rulesets.Catch.Replays if (impossibleJump) { - addFrame(h.StartTime, h.X); + addFrame(h.StartTime, h.EffectiveX); } else if (h.HyperDash) { addFrame(h.StartTime - timeAvailable, lastPosition); - addFrame(h.StartTime, h.X); + addFrame(h.StartTime, h.EffectiveX); } else if (dashRequired) { @@ -80,23 +80,23 @@ namespace osu.Game.Rulesets.Catch.Replays double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable; double timeAtDashSpeed = timeWeNeedToSave / 2; - float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable); + float midPosition = (float)Interpolation.Lerp(lastPosition, h.EffectiveX, (float)timeAtDashSpeed / timeAvailable); // dash movement addFrame(h.StartTime - timeAvailable + 1, lastPosition, true); addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition); - addFrame(h.StartTime, h.X); + addFrame(h.StartTime, h.EffectiveX); } else { double timeBefore = positionChange / movement_speed; addFrame(h.StartTime - timeBefore, lastPosition); - addFrame(h.StartTime, h.X); + addFrame(h.StartTime, h.EffectiveX); } lastTime = h.StartTime; - lastPosition = h.X; + lastPosition = h.EffectiveX; } foreach (var obj in Beatmap.HitObjects) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index f164c2655a..ed875e7002 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Catch.UI var halfCatchWidth = catchWidth * 0.5f; // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. - var catchObjectPosition = fruit.X; + var catchObjectPosition = fruit.EffectiveX; var catcherPosition = Position.X; return catchObjectPosition >= catcherPosition - halfCatchWidth && @@ -250,10 +250,10 @@ namespace osu.Game.Rulesets.Catch.UI { var target = hitObject.HyperDashTarget; var timeDifference = target.StartTime - hitObject.StartTime; - double positionDifference = target.X - X; + double positionDifference = target.EffectiveX - X; var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - SetHyperDashState(Math.Abs(velocity), target.X); + SetHyperDashState(Math.Abs(velocity), target.EffectiveX); } else SetHyperDashState(); diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs new file mode 100644 index 0000000000..9684cbb167 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -0,0 +1,90 @@ +// 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.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public class LegacyManiaJudgementPiece : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + private readonly Drawable animation; + + public LegacyManiaJudgementPiece(HitResult result, Drawable animation) + { + this.result = result; + this.animation = animation; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + float? scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; + + if (scorePosition != null) + scorePosition -= Stage.HIT_TARGET_POSITION + 150; + + Y = scorePosition ?? 0; + + if (animation != null) + { + InternalChild = animation.With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }); + } + } + + public void PlayAnimation() + { + if (animation == null) + return; + + (animation as IFramedAnimation)?.GotoFrame(0); + + switch (result) + { + case HitResult.None: + break; + + case HitResult.Miss: + animation.ScaleTo(1.6f); + animation.ScaleTo(1, 100, Easing.In); + + animation.MoveTo(Vector2.Zero); + animation.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + animation.RotateTo(0); + animation.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); + break; + + default: + animation.ScaleTo(0.8f); + animation.ScaleTo(1, 250, Easing.OutElastic); + + animation.Delay(50).ScaleTo(0.75f, 250); + + this.Delay(50).FadeOut(200); + break; + } + } + + public Drawable GetAboveHitObjectsProxiedContent() => null; + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 89f639e2fe..7e2a8823b6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -136,7 +136,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value ?? default_hitresult_skin_filenames[result]; - return this.GetAnimation(filename, true, true); + var animation = this.GetAnimation(filename, true, true); + return animation == null ? null : new LegacyManiaJudgementPiece(result, animation); } public override SampleChannel GetSample(ISampleInfo sampleInfo) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a3dcd0e57f..34d972e60f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -5,7 +5,6 @@ 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 { @@ -20,37 +19,6 @@ namespace osu.Game.Rulesets.Mania.UI { } - protected override void ApplyMissAnimations() - { - if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece)) - { - // this is temporary logic until mania's skin transformer returns IAnimatableJudgements - JudgementBody.ScaleTo(1.6f); - JudgementBody.ScaleTo(1, 100, Easing.In); - - JudgementBody.MoveTo(Vector2.Zero); - JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - JudgementBody.RotateTo(0); - JudgementBody.RotateTo(40, 800, Easing.InQuint); - JudgementBody.FadeOutFromOne(800); - - LifetimeEnd = JudgementBody.LatestTransformEndTime; - } - - base.ApplyMissAnimations(); - } - - protected override void ApplyHitAnimations() - { - JudgementBody.ScaleTo(0.8f); - JudgementBody.ScaleTo(1, 250, Easing.OutElastic); - - JudgementBody.Delay(50) - .ScaleTo(0.75f, 250) - .FadeOut(200); - } - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); private class DefaultManiaJudgementPiece : DefaultJudgementPiece @@ -66,6 +34,27 @@ namespace osu.Game.Rulesets.Mania.UI JudgementText.Font = JudgementText.Font.With(size: 25); } + + public override void PlayAnimation() + { + base.PlayAnimation(); + + switch (Result) + { + case HitResult.None: + case HitResult.Miss: + break; + + default: + this.ScaleTo(0.8f); + this.ScaleTo(1, 250, Easing.OutElastic); + + this.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); + break; + } + } } } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 3d7960ffe3..dc34bffab1 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Y = HIT_TARGET_POSITION + 150, + Y = HIT_TARGET_POSITION + 150 }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } diff --git a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs new file mode 100644 index 0000000000..07c7b4d1db --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs @@ -0,0 +1,52 @@ +// 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.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public abstract class HitObjectApplicationTestScene : OsuTestScene + { + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo + { + Direction = { Value = ScrollingDirection.Left }, + TimeRange = { Value = 1000 }, + }; + + private ScrollingHitObjectContainer hitObjectContainer; + + [SetUpSteps] + public void SetUp() + => AddStep("create SHOC", () => Child = hitObjectContainer = new ScrollingHitObjectContainer + { + RelativeSizeAxes = Axes.X, + Height = 200, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Clock = new FramedClock(new StopwatchClock()) + }); + + protected void AddHitObject(DrawableHitObject hitObject) + => AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject)); + + protected void RemoveHitObject(DrawableHitObject hitObject) + => AddStep("remove from SHOC", () => hitObjectContainer.Remove(hitObject)); + + protected TObject PrepareObject(TObject hitObject) + where TObject : TaikoHitObject + { + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + return hitObject; + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index f6aec20d53..ff309f278e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } }; - hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true)) + hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime(true)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs new file mode 100644 index 0000000000..65230a07bc --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.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 NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneBarLineApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewBarLine() + { + DrawableBarLine barLine = new DrawableBarLine(PrepareObject(new BarLine + { + StartTime = 400, + Major = true + })); + + AddHitObject(barLine); + RemoveHitObject(barLine); + + AddStep("apply new bar line", () => barLine.Apply(PrepareObject(new BarLine + { + StartTime = 200, + Major = false + }), null)); + AddHitObject(barLine); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index c3fa03d404..7695ca067b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -145,9 +145,13 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addBarLine(bool major, double delay = scroll_time) { - BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay }; + BarLine bl = new BarLine + { + StartTime = DrawableRuleset.Playfield.Time.Current + delay, + Major = major + }; - DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); + DrawableRuleset.Playfield.Add(bl); } private void addSwell(double duration = default_duration) diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs index 6306195704..bbfc02f975 100644 --- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.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 osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -8,7 +9,13 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class BarLine : TaikoHitObject, IBarLine { - public bool Major { get; set; } + public bool Major + { + get => MajorBindable.Value; + set => MajorBindable.Value = value; + } + + public readonly Bindable MajorBindable = new BindableBool(); public override Judgement CreateJudgement() => new IgnoreJudgement(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index aadcc420df..9e50faabc1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -1,7 +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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects; using osuTK; @@ -15,49 +19,123 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public class DrawableBarLine : DrawableHitObject { + public new BarLine HitObject => (BarLine)base.HitObject; + /// /// The width of the line tracker. /// private const float tracker_width = 2f; /// - /// Fade out time calibrated to a pre-empt of 1000ms. + /// The vertical offset of the triangles from the line tracker. /// - private const float base_fadeout_time = 100f; + private const float triangle_offset = 10f; + + /// + /// The size of the triangles. + /// + private const float triangle_size = 20f; /// /// The visual line tracker. /// - protected SkinnableDrawable Line; + private SkinnableDrawable line; /// - /// The bar line. + /// Container with triangles. Only visible for major lines. /// - protected readonly BarLine BarLine; + private Container triangleContainer; - public DrawableBarLine(BarLine barLine) + private readonly Bindable major = new Bindable(); + + public DrawableBarLine() + : this(null) + { + } + + public DrawableBarLine([CanBeNull] BarLine barLine) : base(barLine) { - BarLine = barLine; + } + [BackgroundDependencyLoader] + private void load() + { Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; RelativeSizeAxes = Axes.Y; Width = tracker_width; - AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box + AddRangeInternal(new Drawable[] { - RelativeSizeAxes = Axes.Both, - EdgeSmoothness = new Vector2(0.5f, 0), - }) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0.75f, + line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box + { + RelativeSizeAxes = Axes.Both, + EdgeSmoothness = new Vector2(0.5f, 0), + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + triangleContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new EquilateralTriangle + { + Name = "Top", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Position = new Vector2(0, -triangle_offset), + Size = new Vector2(-triangle_size), + EdgeSmoothness = new Vector2(1), + }, + new EquilateralTriangle + { + Name = "Bottom", + Anchor = Anchor.BottomCentre, + Origin = Anchor.TopCentre, + Position = new Vector2(0, triangle_offset), + Size = new Vector2(triangle_size), + EdgeSmoothness = new Vector2(1), + } + } + } }); } - protected override void UpdateHitStateTransforms(ArmedState state) => this.FadeOut(150); + protected override void LoadComplete() + { + base.LoadComplete(); + major.BindValueChanged(updateMajor); + } + + private void updateMajor(ValueChangedEvent major) + { + line.Alpha = major.NewValue ? 1f : 0.75f; + triangleContainer.Alpha = major.NewValue ? 1 : 0; + } + + protected override void OnApply() + { + base.OnApply(); + major.BindTo(HitObject.MajorBindable); + } + + protected override void OnFree() + { + base.OnFree(); + major.UnbindFrom(HitObject.MajorBindable); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + using (BeginAbsoluteSequence(HitObject.StartTime)) + this.FadeOutFromOne(150).Expire(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs deleted file mode 100644 index 62aab3524b..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osuTK; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableBarLineMajor : DrawableBarLine - { - /// - /// The vertical offset of the triangles from the line tracker. - /// - private const float triangle_offfset = 10f; - - /// - /// The size of the triangles. - /// - private const float triangle_size = 20f; - - private readonly Container triangleContainer; - - public DrawableBarLineMajor(BarLine barLine) - : base(barLine) - { - AddInternal(triangleContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new EquilateralTriangle - { - Name = "Top", - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Position = new Vector2(0, -triangle_offfset), - Size = new Vector2(-triangle_size), - EdgeSmoothness = new Vector2(1), - }, - new EquilateralTriangle - { - Name = "Bottom", - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre, - Position = new Vector2(0, triangle_offfset), - Size = new Vector2(triangle_size), - EdgeSmoothness = new Vector2(1), - } - } - }); - - Line.Alpha = 1f; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - using (triangleContainer.BeginAbsoluteSequence(HitObject.StartTime)) - triangleContainer.FadeOut(150); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs b/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs new file mode 100644 index 0000000000..cb878e8ea0 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class BarLinePlayfield : ScrollingPlayfield + { + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(15); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index e6aacf34dc..bbf8cb8de0 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); + new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar)); FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty()) { diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 370760f03e..d20b190c86 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -38,10 +39,15 @@ namespace osu.Game.Rulesets.Taiko.UI private SkinnableDrawable mascot; private ProxyContainer topLevelHitContainer; - private ScrollingHitObjectContainer barlineContainer; private Container rightArea; private Container leftArea; + /// + /// is purposefully not called on this to prevent i.e. being able to interact + /// with bar lines in the editor. + /// + private BarLinePlayfield barLinePlayfield; + private Container hitTargetOffsetContent; public TaikoPlayfield(ControlPointInfo controlPoints) @@ -84,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - barlineContainer = new ScrollingHitObjectContainer(), + barLinePlayfield = new BarLinePlayfield(), new Container { Name = "Hit objects", @@ -155,12 +161,50 @@ namespace osu.Game.Rulesets.Taiko.UI mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT); } + #region Pooling support + + public override void Add(HitObject h) + { + switch (h) + { + case BarLine barLine: + barLinePlayfield.Add(barLine); + break; + + case TaikoHitObject taikoHitObject: + base.Add(taikoHitObject); + break; + + default: + throw new ArgumentException($"Unsupported {nameof(HitObject)} type: {h.GetType()}"); + } + } + + public override bool Remove(HitObject h) + { + switch (h) + { + case BarLine barLine: + return barLinePlayfield.Remove(barLine); + + case TaikoHitObject taikoHitObject: + return base.Remove(taikoHitObject); + + default: + throw new ArgumentException($"Unsupported {nameof(HitObject)} type: {h.GetType()}"); + } + } + + #endregion + + #region Non-pooling support + public override void Add(DrawableHitObject h) { switch (h) { - case DrawableBarLine barline: - barlineContainer.Add(barline); + case DrawableBarLine barLine: + barLinePlayfield.Add(barLine); break; case DrawableTaikoHitObject taikoObject: @@ -170,7 +214,7 @@ namespace osu.Game.Rulesets.Taiko.UI break; default: - throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type"); + throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type: {h.GetType()}"); } } @@ -178,8 +222,8 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (h) { - case DrawableBarLine barline: - return barlineContainer.Remove(barline); + case DrawableBarLine barLine: + return barLinePlayfield.Remove(barLine); case DrawableTaikoHitObject _: h.OnNewResult -= OnNewResult; @@ -187,10 +231,12 @@ namespace osu.Game.Rulesets.Taiko.UI return base.Remove(h); default: - throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type"); + throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type: {h.GetType()}"); } } + #endregion + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements.Value) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 742d433760..347d9e3ba7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Anchor = Anchor.TopCentre, Y = -separation, - HandleDrag = e => OnRotation?.Invoke(e.Delta.X), + HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e)), OperationStarted = operationStarted, OperationEnded = operationEnded } @@ -242,6 +242,15 @@ namespace osu.Game.Screens.Edit.Compose.Components private int activeOperations; + private float convertDragEventToAngleOfRotation(DragEvent e) + { + // Adjust coordinate system to the center of SelectionBox + float startAngle = MathF.Atan2(e.LastMousePosition.Y - DrawHeight / 2, e.LastMousePosition.X - DrawWidth / 2); + float endAngle = MathF.Atan2(e.MousePosition.Y - DrawHeight / 2, e.MousePosition.X - DrawWidth / 2); + + return (endAngle - startAngle) * 180 / MathF.PI; + } + private void operationEnded() { if (--activeOperations == 0) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 729119fa36..f59b36bc42 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Play // after an initial delay, start the debounced load check. // this will continue to execute even after resuming back on restart. - Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0)); + Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0)); showMuteWarningIfNeeded(); } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 35a6140cbc..77d390875b 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -35,6 +35,7 @@ namespace osu.Game.Skinning public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; + public float ScorePosition = 300 * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; public bool KeysUnderNotes; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index a99710ea96..9db6c8bf66 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -25,6 +25,7 @@ namespace osu.Game.Skinning LeftLineWidth, RightLineWidth, HitPosition, + ScorePosition, LightPosition, HitTargetImage, ShowJudgementLine, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 3dbec23194..6a04a95040 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -94,6 +94,10 @@ namespace osu.Game.Skinning currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; + case "ScorePosition": + currentConfig.ScorePosition = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + break; + case "JudgementLine": currentConfig.ShowJudgementLine = pair.Value == "1"; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 63a22eba62..80b2fef35c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -169,6 +169,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); + case LegacyManiaSkinConfigurationLookups.ScorePosition: + return SkinUtils.As(new Bindable(existing.ScorePosition)); + case LegacyManiaSkinConfigurationLookups.LightPosition: return SkinUtils.As(new Bindable(existing.LightPosition)); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b4c7dca12f..960959f367 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 7542aded86..a5bcb91c74 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - +