diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint10.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint10.png new file mode 100644 index 0000000000..3e2fe66a1c Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint10.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint30.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint30.png new file mode 100644 index 0000000000..d883279d81 Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/sliderpoint30.png differ diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgementSliderTicks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgementSliderTicks.cs new file mode 100644 index 0000000000..5843a9233c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgementSliderTicks.cs @@ -0,0 +1,160 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneDrawableJudgementSliderTicks : OsuSkinnableTestScene + { + private bool classic; + private readonly JudgementPooler[] judgementPools; + + public TestSceneDrawableJudgementSliderTicks() + { + judgementPools = new JudgementPooler[Rows * Cols]; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + int cellIndex = 0; + + SetContents(_ => + { + var container = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + judgementPools[cellIndex] = new JudgementPooler(new[] + { + HitResult.Great, + HitResult.Miss, + HitResult.LargeTickHit, + HitResult.SliderTailHit, + HitResult.LargeTickMiss, + HitResult.IgnoreMiss, + }), + new GridContainer + { + Padding = new MarginPadding { Top = 26f }, + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + Content = + new[] + { + new[] + { + Empty(), + new OsuSpriteText + { + Text = "hit", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }, + new OsuSpriteText + { + Text = "miss", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }, + }, + }.Concat(new[] + { + "head", + "tick", + "repeat", + "tail", + "slider", + }.Select(label => new Drawable[] + { + new OsuSpriteText + { + Text = label, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + new Container { RelativeSizeAxes = Axes.Both }, + new Container { RelativeSizeAxes = Axes.Both }, + })).ToArray(), + }, + }, + }; + + cellIndex++; + + return container; + }); + + AddToggleStep("Toggle classic behaviour", c => classic = c); + + AddStep("Show judgements", createAllJudgements); + } + + private void createAllJudgements() + { + for (int cellIndex = 0; cellIndex < Rows * Cols; cellIndex++) + { + var slider = new Slider { StartTime = Time.Current, ClassicSliderBehaviour = classic }; + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + var drawableHitObjects = new DrawableOsuHitObject[] + { + new DrawableSliderHead(new SliderHeadCircle { StartTime = Time.Current, ClassicSliderBehaviour = classic }), + new DrawableSliderTick(new SliderTick { StartTime = Time.Current }), + new DrawableSliderRepeat(new SliderRepeat(slider) { StartTime = Time.Current }), + new DrawableSliderTail(new SliderTailCircle(slider) { StartTime = Time.Current, ClassicSliderBehaviour = classic }), + new DrawableSlider(slider), + }; + + var containers = Cell(cellIndex).ChildrenOfType>().ToArray(); + + for (int i = 0; i < drawableHitObjects.Length; i++) + { + createJudgement(judgementPools[cellIndex], containers[i * 2], drawableHitObjects[i], true); + createJudgement(judgementPools[cellIndex], containers[i * 2 + 1], drawableHitObjects[i], false); + } + } + } + + private void createJudgement(JudgementPooler pool, Container container, DrawableOsuHitObject drawableHitObject, bool hit) + { + container.Clear(false); + + if (!drawableHitObject.DisplayResult) + return; + + var hitObject = drawableHitObject.HitObject; + var result = new OsuJudgementResult(hitObject, hitObject.Judgement) + { + Type = hit ? hitObject.Judgement.MaxResult : hitObject.Judgement.MinResult, + }; + + var judgement = pool.Get(result.Type, d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + d.Scale = new Vector2(0.7f); + d.Apply(result, null); + }); + + if (judgement != null) + container.Add(judgement); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 8b3fcb23cd..0f7812d91a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [Resolved] private OsuConfigManager config { get; set; } = null!; - private Vector2 screenSpacePosition; + private Vector2? screenSpacePosition; [BackgroundDependencyLoader] private void load() @@ -65,7 +65,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.ResetAnimation(); Lighting.SetColourFrom(this, Result); - Position = Parent!.ToLocalSpace(screenSpacePosition); + + if (screenSpacePosition != null) + Position = Parent!.ToLocalSpace(screenSpacePosition.Value); } protected override void ApplyHitAnimations() @@ -87,7 +89,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplyHitAnimations(); } - protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result); + protected override Drawable CreateDefaultJudgement(HitResult result) => + // Tick hits don't show a judgement by default + result.IsHit() && result.IsTick() ? Empty() : new OsuJudgementPiece(result); private partial class OsuJudgementPiece : DefaultJudgementPiece { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 76b9fdc3ce..55e985c568 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -16,17 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; - public override bool DisplayResult - { - get - { - if (HitObject?.ClassicSliderBehaviour == true) - return false; - - return base.DisplayResult; - } - } - private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs index 3776201626..7bb54487c0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs @@ -3,6 +3,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK.Graphics; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (targetJudgement == null || targetResult == null) Colour = Color4.White; else - Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent; + Colour = targetResult.IsHit && !targetResult.Type.IsTick() ? targetJudgement.AccentColour : Color4.Transparent; } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs index 2d1d5826b1..ecc0f3fd0a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs @@ -29,6 +29,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon switch (result) { + case HitResult.LargeTickHit: + case HitResult.SliderTailHit: + return null; + case HitResult.IgnoreMiss: case HitResult.LargeTickMiss: return new ArgonJudgementPieceSliderTickMiss(result); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyJudgementPieceSliderTickHit.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyJudgementPieceSliderTickHit.cs new file mode 100644 index 0000000000..8c89f4c9c8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyJudgementPieceSliderTickHit.cs @@ -0,0 +1,23 @@ +// 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.Sprites; +using osu.Game.Rulesets.Judgements; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Skinning.Legacy +{ + public partial class LegacyJudgementPieceSliderTickHit : Sprite, IAnimatableJudgement + { + public void PlayAnimation() + { + // https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L804-L806 + this.MoveToOffset(new Vector2(0, -10), 300, Easing.Out) + .Then() + .FadeOut(60); + } + + public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy(); + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index af1df6dc9c..7118b6f95e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -5,7 +5,9 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; @@ -115,6 +117,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; + case SkinComponentLookup resultComponent: + switch (resultComponent.Component) + { + case HitResult.LargeTickHit: + case HitResult.SliderTailHit: + if (getSliderPointTexture(resultComponent.Component) is Texture texture) + return new LegacyJudgementPieceSliderTickHit { Texture = texture }; + + break; + + // If the corresponding hit result displays a judgement and the miss texture isn't provided by this skin, don't look up the miss texture from any further skins. + case HitResult.LargeTickMiss: + case HitResult.IgnoreMiss: + if (getSliderPointTexture(resultComponent.Component == HitResult.LargeTickMiss + ? HitResult.LargeTickHit + : HitResult.SliderTailHit) != null) + return base.GetDrawableComponent(lookup) ?? Drawable.Empty(); + + break; + } + + return base.GetDrawableComponent(lookup); + + Texture? getSliderPointTexture(HitResult result) + { + // https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L799 + if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2m) + // Note that osu!stable used sliderpoint30 for heads and repeats, and sliderpoint10 for ticks, but the mapping is intentionally changed here so that each texture represents one type of HitResult. + return GetTexture(result == HitResult.LargeTickHit ? "sliderpoint30" : "sliderpoint10"); + + return null; + } + case OsuSkinComponentLookup osuComponent: switch (osuComponent.Component) { diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 7d9f5eb1a8..e379c44314 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Osu.UI HitResult.Ok, HitResult.Meh, HitResult.Miss, + HitResult.LargeTickHit, + HitResult.SliderTailHit, HitResult.LargeTickMiss, HitResult.IgnoreMiss, }, onJudgementLoaded));